Index: /tags/4.0.0-beta1/.editorconfig
===================================================================
--- /tags/4.0.0-beta1/.editorconfig	(revision 12265)
+++ /tags/4.0.0-beta1/.editorconfig	(revision 12265)
@@ -0,0 +1,24 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# WordPress Coding Standards
+# https://make.wordpress.org/core/handbook/coding-standards/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+
+[*.yml]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[{*.txt}]
+end_of_line = crlf
Index: /tags/4.0.0-beta1/.gitignore
===================================================================
--- /tags/4.0.0-beta1/.gitignore	(revision 12265)
+++ /tags/4.0.0-beta1/.gitignore	(revision 12265)
@@ -0,0 +1,24 @@
+.DS_Store
+phpunit.xml
+.idea
+
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+src/vendor
+vendor
+
+node_modules
+npm-debug.log
+
+# The build directory.
+build
Index: /tags/4.0.0-beta1/.jshintignore
===================================================================
--- /tags/4.0.0-beta1/.jshintignore	(revision 12265)
+++ /tags/4.0.0-beta1/.jshintignore	(revision 12265)
@@ -0,0 +1,3 @@
+// 3rd party libraries
+src/bp-core/js/vendor/**/*
+src/bp-messages/js/autocomplete/*
Index: /tags/4.0.0-beta1/.jshintrc
===================================================================
--- /tags/4.0.0-beta1/.jshintrc	(revision 12265)
+++ /tags/4.0.0-beta1/.jshintrc	(revision 12265)
@@ -0,0 +1,23 @@
+{
+	"boss": true,
+	"curly": true,
+	"eqeqeq": true,
+	"eqnull": true,
+	"es3": true,
+	"expr": true,
+	"immed": true,
+	"noarg": true,
+	"quotmark": "single",
+	"undef": true,
+	"unused": true,
+
+	"browser": true,
+
+	"globals": {
+		"_": false,
+		"ajaxurl": false,
+		"Backbone": false,
+		"jQuery": false,
+		"wp": false
+	}
+}
Index: /tags/4.0.0-beta1/.npmrc
===================================================================
--- /tags/4.0.0-beta1/.npmrc	(revision 12265)
+++ /tags/4.0.0-beta1/.npmrc	(revision 12265)
@@ -0,0 +1,1 @@
+save-prefix='~'
Index: /tags/4.0.0-beta1/.travis.yml
===================================================================
--- /tags/4.0.0-beta1/.travis.yml	(revision 12265)
+++ /tags/4.0.0-beta1/.travis.yml	(revision 12265)
@@ -0,0 +1,122 @@
+# Travis CI Configuration File
+
+# Use new Travis container-based infrastructure
+# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
+sudo: false
+
+language: php
+
+dist: trusty
+
+cache:
+  apt: true
+  directories:
+    - $HOME/.composer/cache/files
+    - $HOME/.npm
+    - /tmp/node
+
+git:
+  depth: 10
+
+php:
+  - 7.2
+  - 5.6
+
+# WordPress comes from the Git mirror, where 'master' mirrors svn 'trunk' and
+# x.y mirrors the latest from the x.y branch
+env:
+  - WP_VERSION=master BP_TRAVISCI=travis:phpunit
+  - WP_VERSION=4.9 BP_TRAVISCI=travis:phpunit
+  - WP_VERSION=4.8 BP_TRAVISCI=travis:phpunit
+  - WP_VERSION=4.7 BP_TRAVISCI=travis:phpunit
+  - WP_VERSION=4.6 BP_TRAVISCI=travis:phpunit
+
+matrix:
+  include:
+    - php: 7.2
+      env: WP_VERSION=master BP_TRAVISCI=travis:grunt
+    - php: 5.3
+      env: WP_VERSION=master BP_TRAVISCI=travis:phpunit
+      dist: precise
+    - php: 5.3
+      env: WP_VERSION=4.8 BP_TRAVISCI=travis:phpunit
+      dist: precise
+    - php: 5.3
+      env: WP_VERSION=4.7 BP_TRAVISCI=travis:phpunit
+      dist: precise
+    - php: 5.3
+      env: WP_VERSION=4.6 BP_TRAVISCI=travis:phpunit
+      dist: precise
+  exclude:
+    - php: 7.2
+      env: WP_VERSION=4.6 BP_TRAVISCI=travis:phpunit
+
+# before_install: Failures in this section will result in build status 'errored'
+before_install:
+  # set up WP install
+  - export WP_DEVELOP_DIR=/tmp/wordpress/
+  - mkdir -p $WP_DEVELOP_DIR
+  - git clone --depth=1 --branch="$WP_VERSION" git://develop.git.wordpress.org/ $WP_DEVELOP_DIR
+  - plugin_slug=$(basename $(pwd))
+  - plugin_dir=$WP_DEVELOP_DIR/src/wp-content/plugins/$plugin_slug
+  - cd ..
+  - mv $plugin_slug $plugin_dir
+  - |
+    # Remove Xdebug for a huge performance increase:
+    if [[ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]]; then
+      phpenv config-rm xdebug.ini
+    fi
+
+  # set up tests config
+  - cd $WP_DEVELOP_DIR
+  - echo $WP_DEVELOP_DIR
+  - cp wp-tests-config-sample.php wp-tests-config.php
+  - sed -i "s/youremptytestdbnamehere/wordpress_test/" wp-tests-config.php
+  - sed -i "s/yourusernamehere/root/" wp-tests-config.php
+  - sed -i "s/yourpasswordhere//" wp-tests-config.php
+
+  # set up database
+  - mysql -e 'CREATE DATABASE wordpress_test;' -uroot
+
+  # prepare for running the tests
+  - cd $plugin_dir
+
+  # setup NodeJS version using NVM
+  - export PATH="$HOME/.composer/vendor/bin:$PATH"
+  - node --version
+  - nvm install 6
+  - node --version
+  - npm install -g grunt-cli
+  - npm --version
+  - mysql --version
+  - phpenv versions
+  - php --version
+  - php -m
+  - phpunit --version
+  - curl --version
+  - grunt --version
+  - git --version
+  - svn --version
+
+# before_script: Failures in this section will result in build status 'errored'
+before_script:
+  - if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.3" ]]; then composer global require --dev phpunit/phpunit ^4; fi
+  - if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.6" ]]; then composer global require --dev phpunit/phpunit ^5; fi
+  - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.2" ]]; then composer global require --dev phpunit/phpunit ^6; fi
+  - mkdir -p /tmp/node/node_modules
+  - ln -s /tmp/node/node_modules $plugin_dir/node_modules
+  - npm install
+
+# Run tests
+script: grunt $BP_TRAVISCI
+
+notifications:
+  email: false
+
+  irc:
+    channels:
+      - secure: "Z+gAlyOafrw0Xg294np+7IpqOKTsswGjAWyOZsbzT7Xo8uY1nmz8tTuU50Gpo8jo5HQqG9G0QgI3zB3Cxy3M5dF+J5oLl1lBz1zL0mtHiq1HoXi2cbVnGCCpxtMvYII6UmPUGAg8zvNUQol+cw1jb3PzLwEDfGcDH64o/Z3j8Gg="
+    template:
+      - "Build %{build_number} (%{branch} - %{commit}): %{message} %{build_url}"
+  slack:
+    secure: JQT+HWoVhLzoG00HmJscYZO+Gy7ywnIlrUbCaI7DxFdFOIkKxsTLLq9cKE4y3VqgZAARTOKjP1eQ7a5OEg1vT11Ow+rB7iUAMNmITxXO4feTOx1i2673xRNBro/SZMhlLd7DY2erbMDZaG9hBQB4xLXRYOvP29pLxf5zTSyZMyE=
Index: /tags/4.0.0-beta1/Gruntfile.js
===================================================================
--- /tags/4.0.0-beta1/Gruntfile.js	(revision 12265)
+++ /tags/4.0.0-beta1/Gruntfile.js	(revision 12265)
@@ -0,0 +1,362 @@
+/* jshint node:true */
+/* global module */
+module.exports = function( grunt ) {
+	var SOURCE_DIR = 'src/',
+		BUILD_DIR = 'build/',
+
+		BP_CSS = [
+			'**/*.css'
+		],
+
+		// CSS exclusions, for excluding files from certain tasks, e.g. rtlcss
+		BP_EXCLUDED_CSS = [
+			'!**/*-rtl.css'
+		],
+
+		BP_JS = [
+			'**/*.js'
+		],
+
+		BP_EXCLUDED_MISC = [
+		],
+
+		// SASS generated "Twenty*"" CSS files
+		BP_SCSS_CSS_FILES = [
+			'!bp-templates/bp-legacy/css/twenty*.css',
+			'!bp-templates/bp-nouveau/css/buddypress.css',
+			'!bp-core/admin/css/hello.css'
+		],
+
+		autoprefixer = require('autoprefixer'),
+		stylelintConfigCss  = require('stylelint-config-wordpress/index.js'),
+		stylelintConfigScss = require('stylelint-config-wordpress/scss.js');
+
+	require( 'matchdep' ).filterDev( ['grunt-*', '!grunt-legacy-util'] ).forEach( grunt.loadNpmTasks );
+	grunt.util = require( 'grunt-legacy-util' );
+
+	grunt.initConfig( {
+		pkg: grunt.file.readJSON( 'package.json' ),
+		checkDependencies: {
+			options: {
+				packageManager: 'npm'
+			},
+			src: {}
+		},
+		jshint: {
+			options: grunt.file.readJSON( '.jshintrc' ),
+			grunt: {
+				src: ['Gruntfile.js']
+			},
+			core: {
+				expand: true,
+				cwd: SOURCE_DIR,
+				src: BP_JS,
+
+				/**
+				 * Limit JSHint's run to a single specified file:
+				 *
+				 * grunt jshint:core --file=filename.js
+				 *
+				 * Optionally, include the file path:
+				 *
+				 * grunt jshint:core --file=path/to/filename.js
+				 *
+				 * @param {String} filepath
+				 * @returns {Bool}
+				 */
+				filter: function( filepath ) {
+					var index, file = grunt.option( 'file' );
+
+					// Don't filter when no target file is specified
+					if ( ! file ) {
+						return true;
+					}
+
+					// Normalise filepath for Windows
+					filepath = filepath.replace( /\\/g, '/' );
+					index = filepath.lastIndexOf( '/' + file );
+
+					// Match only the filename passed from cli
+					if ( filepath === file || ( -1 !== index && index === filepath.length - ( file.length + 1 ) ) ) {
+						return true;
+					}
+
+					return false;
+				}
+			}
+		},
+		sass: {
+			options: {
+				outputStyle: 'expanded',
+				indentType: 'tab',
+				indentWidth: '1'
+			},
+			legacy: {
+				cwd: SOURCE_DIR,
+				extDot: 'last',
+				expand: true,
+				ext: '.css',
+				flatten: true,
+				src: ['bp-templates/bp-legacy/css/*.scss'],
+				dest: SOURCE_DIR + 'bp-templates/bp-legacy/css/'
+			},
+			nouveau: {
+				cwd: SOURCE_DIR,
+				extDot: 'last',
+				expand: true,
+				ext: '.css',
+				flatten: true,
+				src: ['bp-templates/bp-nouveau/sass/buddypress.scss'],
+				dest: SOURCE_DIR + 'bp-templates/bp-nouveau/css/'
+			},
+			admin: {
+				cwd: SOURCE_DIR,
+				extDot: 'last',
+				expand: true,
+				ext: '.css',
+				flatten: true,
+				src: ['bp-core/admin/sass/*.scss'],
+				dest: SOURCE_DIR + 'bp-core/admin/css/'
+			}
+		},
+		rtlcss: {
+			options: {
+				opts: {
+					processUrls: false,
+					autoRename: false,
+					clean: true
+				},
+				saveUnmodified: false
+			},
+			core: {
+				expand: true,
+				cwd: SOURCE_DIR,
+				dest: SOURCE_DIR,
+				extDot: 'last',
+				ext: '-rtl.css',
+				src: BP_CSS.concat( BP_EXCLUDED_CSS, BP_EXCLUDED_MISC )
+			}
+		},
+		checktextdomain: {
+			options: {
+				correct_domain: false,
+				text_domain: 'buddypress',
+				keywords: [
+					'__:1,2d',
+					'_e:1,2d',
+					'_x:1,2c,3d',
+					'_n:1,2,4d',
+					'_ex:1,2c,3d',
+					'_nx:1,2,4c,5d',
+					'esc_attr__:1,2d',
+					'esc_attr_e:1,2d',
+					'esc_attr_x:1,2c,3d',
+					'esc_html__:1,2d',
+					'esc_html_e:1,2d',
+					'esc_html_x:1,2c,3d',
+					'_n_noop:1,2,3d',
+					'_nx_noop:1,2,3c,4d'
+				]
+			},
+			files: {
+				cwd: SOURCE_DIR,
+				src: ['**/*.php'].concat( BP_EXCLUDED_MISC ),
+				expand: true
+			}
+		},
+		makepot: {
+			target: {
+				options: {
+					cwd: BUILD_DIR,
+					domainPath: '.',
+					mainFile: 'bp-loader.php',
+					potFilename: 'buddypress.pot',
+					processPot: function( pot ) {
+						pot.headers['report-msgid-bugs-to'] = 'https://buddypress.trac.wordpress.org';
+						pot.headers['last-translator'] = 'JOHN JAMES JACOBY <jjj@buddypress.org>';
+						pot.headers['language-team'] = 'ENGLISH <jjj@buddypress.org>';
+						return pot;
+					},
+					type: 'wp-plugin'
+				}
+			}
+		},
+		imagemin: {
+			core: {
+				expand: true,
+				cwd: SOURCE_DIR,
+				src: ['**/*.{gif,jpg,jpeg,png}'].concat( BP_EXCLUDED_MISC ),
+				dest: SOURCE_DIR
+			}
+		},
+		clean: {
+			all: [ BUILD_DIR ]
+		},
+		copy: {
+			files: {
+				files: [
+					{
+						cwd: SOURCE_DIR,
+						dest: BUILD_DIR,
+						dot: true,
+						expand: true,
+						src: ['**', '!**/.{svn,git}/**'].concat( BP_EXCLUDED_MISC )
+					},
+					{
+						dest: BUILD_DIR,
+						dot: true,
+						expand: true,
+						src: ['composer.json']
+					}
+				]
+			}
+		},
+		uglify: {
+			core: {
+				cwd: BUILD_DIR,
+				dest: BUILD_DIR,
+				extDot: 'last',
+				expand: true,
+				ext: '.min.js',
+				src: BP_JS
+			}
+		},
+		stylelint: {
+			css: {
+				options: {
+					config: stylelintConfigCss,
+					format: 'css'
+				},
+				expand: true,
+				cwd: SOURCE_DIR,
+				src: BP_CSS.concat( BP_EXCLUDED_CSS, BP_EXCLUDED_MISC, BP_SCSS_CSS_FILES )
+			},
+			scss: {
+				options: {
+					config: stylelintConfigScss,
+					format: 'scss'
+				},
+				expand: true,
+				cwd: SOURCE_DIR,
+				src: [ '**/*.scss' ]
+			}
+		},
+		phplint: {
+			good: ['**/*.php'].concat( BP_EXCLUDED_MISC )
+		},
+		postcss: {
+			options: {
+				map: false,
+				processors: [
+					autoprefixer({
+						browsers: ['extends @wordpress/browserslist-config'],
+						cascade: false
+					})
+				],
+				failOnError: false
+			},
+			css: {
+				expand: true,
+				cwd: SOURCE_DIR,
+				dest: SOURCE_DIR,
+				src: BP_CSS.concat( BP_EXCLUDED_CSS, BP_EXCLUDED_MISC )
+			}
+		},
+		cssmin: {
+			minify: {
+				cwd: BUILD_DIR,
+				dest: BUILD_DIR,
+				extDot: 'last',
+				expand: true,
+				ext: '.min.css',
+				src: BP_CSS
+			}
+		},
+		phpunit: {
+			'default': {
+				cmd: 'phpunit',
+				args: ['-c', 'phpunit.xml.dist']
+			},
+			'multisite': {
+				cmd: 'phpunit',
+				args: ['-c', 'tests/phpunit/multisite.xml']
+			},
+			'codecoverage': {
+				cmd: 'phpunit',
+				args: ['-c', 'tests/phpunit/codecoverage.xml' ]
+			}
+		},
+		exec: {
+			bpdefault: {
+				command: 'svn export --force https://github.com/buddypress/BP-Default.git/trunk bp-themes/bp-default',
+				cwd: BUILD_DIR,
+				stdout: false
+			},
+			cli: {
+				command: 'svn export --force https://github.com/buddypress/wp-cli-buddypress.git/tags/1.7 cli',
+				cwd: BUILD_DIR,
+				stdout: false
+			}
+		},
+		jsvalidate:{
+			options:{
+				globals: {},
+				esprimaOptions:{},
+				verbose: false
+			},
+			build: {
+				files: {
+					src: [BUILD_DIR + '/**/*.js']
+				}
+			},
+			src: {
+				files: {
+					src: [SOURCE_DIR + '/**/*.js'].concat( BP_EXCLUDED_MISC )
+				}
+			}
+		},
+		patch: {
+			options: {
+				tracUrl: 'buddypress.trac.wordpress.org'
+			}
+		},
+		upload_patch: {
+			options: {
+				tracUrl: 'buddypress.trac.wordpress.org'
+			}
+		}
+	});
+
+
+	/**
+	 * Register tasks.
+	 */
+	grunt.registerTask( 'src',     ['checkDependencies', 'jsvalidate:src', 'jshint', 'stylelint', 'sass', 'postcss', 'rtlcss'] );
+	grunt.registerTask( 'commit',  ['src', 'checktextdomain', 'imagemin','phplint'] );
+	grunt.registerTask( 'build',   ['commit', 'clean:all', 'copy:files', 'uglify', 'jsvalidate:build', 'cssmin', 'makepot', 'exec:bpdefault', 'exec:cli'] );
+	grunt.registerTask( 'release', ['build'] );
+
+	// Testing tasks.
+	grunt.registerMultiTask( 'phpunit', 'Runs PHPUnit tests, including the ajax and multisite tests.', function() {
+		grunt.util.spawn( {
+			args: this.data.args,
+			cmd:  this.data.cmd,
+			opts: { stdio: 'inherit' }
+		}, this.async() );
+	});
+
+	grunt.registerTask( 'test', 'Run all unit test tasks.', ['phpunit:default', 'phpunit:multisite'] );
+
+	grunt.registerTask( 'jstest', 'Runs all JavaScript tasks.', [ 'jsvalidate:src', 'jshint' ] );
+
+	// Travis CI Tasks.
+	grunt.registerTask( 'travis:grunt', 'Runs Grunt build task.', [ 'build' ]);
+	grunt.registerTask( 'travis:phpunit', ['jsvalidate:src', 'jshint', 'checktextdomain', 'phplint', 'test'] );
+	grunt.registerTask( 'travis:codecoverage', 'Runs PHPUnit tasks with code-coverage generation.', ['phpunit:codecoverage'] );
+
+	// Patch task.
+	grunt.renameTask( 'patch_wordpress', 'patch' );
+
+	// Default task.
+	grunt.registerTask( 'default', ['src'] );
+};
Index: /tags/4.0.0-beta1/bp-loader.php
===================================================================
--- /tags/4.0.0-beta1/bp-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/bp-loader.php	(revision 12265)
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * The BuddyPress Plugin
+ *
+ * BuddyPress is social networking software with a twist from the creators of WordPress.
+ *
+ * @package BuddyPress
+ * @subpackage Main
+ */
+
+/**
+ * Plugin Name: BuddyPress
+ * Plugin URI:  https://buddypress.org
+ * Description: BuddyPress adds community features to WordPress. Member Profiles, Activity Streams, Direct Messaging, Notifications, and more!
+ * Author:      The BuddyPress Community
+ * Author URI:  https://buddypress.org/
+ * Version:     4.0.0-beta1
+ * Text Domain: buddypress
+ * Domain Path: /bp-languages/
+ * License:     GPLv2 or later (license.txt)
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+// Assume you want to load from build
+$bp_loader = dirname( __FILE__ ) . '/build/bp-loader.php';
+
+// Load from source if no build exists
+if ( ! file_exists( $bp_loader ) || defined( 'BP_LOAD_SOURCE' ) ) {
+	$bp_loader = dirname( __FILE__ ) . '/src/bp-loader.php';
+	$subdir = 'src';
+} else {
+	$subdir = 'build';
+}
+
+// Set source subdirectory
+define( 'BP_SOURCE_SUBDIRECTORY', $subdir );
+
+// Define overrides - only applicable to those running trunk
+if ( ! defined( 'BP_PLUGIN_DIR' ) ) {
+	define( 'BP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
+}
+if ( ! defined( 'BP_PLUGIN_URL' ) ) {
+	// Be nice to symlinked directories
+	define( 'BP_PLUGIN_URL', plugins_url( trailingslashit( basename( constant( 'BP_PLUGIN_DIR' ) ) ) ) );
+}
+
+// Include BuddyPress
+include( $bp_loader );
+
+// Unset the loader, since it's loaded in global scope
+unset( $bp_loader );
Index: /tags/4.0.0-beta1/composer.json
===================================================================
--- /tags/4.0.0-beta1/composer.json	(revision 12265)
+++ /tags/4.0.0-beta1/composer.json	(revision 12265)
@@ -0,0 +1,33 @@
+{
+	"name": "buddypress/buddypress",
+	"description": "BuddyPress adds community features to WordPress. Member Profiles, Activity Streams, Direct Messaging, Notifications, and more!",
+	"type": "wordpress-plugin",
+	"keywords": [
+		"activity",
+		"community",
+		"friends",
+		"groups",
+		"messaging",
+		"notifications",
+		"profiles",
+		"social network"
+	],
+	"homepage": "https://buddypress.org",
+	"license": "GPL-2.0+",
+	"authors": [ {
+		"name": "BuddyPress Community",
+		"homepage": "https://buddypress.org/about/"
+	} ],
+	"support": {
+		"forum": "https://buddypress.org/support/",
+		"docs": "https://codex.buddypress.org/",
+		"issues": "https://buddypress.trac.wordpress.org/",
+		"rss": "https://buddypress.org/feed/",
+		"source": "https://buddypress.trac.wordpress.org/browser/",
+		"wiki": "https://codex.buddypress.org/"
+	},
+	"require": {
+		"composer/installers": "~1.0",
+		"php": ">=5.3.0"
+	}
+}
Index: /tags/4.0.0-beta1/npm-shrinkwrap.json
===================================================================
--- /tags/4.0.0-beta1/npm-shrinkwrap.json	(revision 12265)
+++ /tags/4.0.0-beta1/npm-shrinkwrap.json	(revision 12265)
@@ -0,0 +1,9011 @@
+{
+	"name": "buddypress",
+	"version": "4.0.0-beta1",
+	"lockfileVersion": 1,
+	"requires": true,
+	"dependencies": {
+		"@wordpress/browserslist-config": {
+			"version": "2.1.4",
+			"resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-2.1.4.tgz",
+			"integrity": "sha512-J8vd88IFsjYwYZSIVATDjWa0pH997zvQ6quor30UdwhqdZ4ZWxIh4AEADqZA3I4EwskWVmg9n9DF7jVUDZPxwg==",
+			"dev": true
+		},
+		"JSONStream": {
+			"version": "0.8.4",
+			"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.8.4.tgz",
+			"integrity": "sha1-kWV9/m/4V0gwZhMrRhi2Lo9Ih70=",
+			"dev": true,
+			"requires": {
+				"jsonparse": "0.0.5",
+				"through": ">=2.2.7 <3"
+			}
+		},
+		"abbrev": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+			"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+			"dev": true
+		},
+		"ajv": {
+			"version": "5.5.2",
+			"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+			"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+			"dev": true,
+			"requires": {
+				"co": "^4.6.0",
+				"fast-deep-equal": "^1.0.0",
+				"fast-json-stable-stringify": "^2.0.0",
+				"json-schema-traverse": "^0.3.0"
+			},
+			"dependencies": {
+				"co": {
+					"version": "4.6.0",
+					"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+					"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+					"dev": true
+				}
+			}
+		},
+		"ajv-keywords": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+			"integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+			"dev": true
+		},
+		"amdefine": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+			"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+			"dev": true
+		},
+		"ansi-escapes": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+			"integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
+			"dev": true
+		},
+		"ansi-gray": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
+			"integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
+			"dev": true,
+			"requires": {
+				"ansi-wrap": "0.1.0"
+			}
+		},
+		"ansi-regex": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+			"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+			"dev": true
+		},
+		"ansi-styles": {
+			"version": "3.2.1",
+			"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+			"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+			"dev": true,
+			"requires": {
+				"color-convert": "^1.9.0"
+			}
+		},
+		"ansi-wrap": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
+			"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
+			"dev": true
+		},
+		"aproba": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+			"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+			"dev": true
+		},
+		"archive-type": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/archive-type/-/archive-type-3.2.0.tgz",
+			"integrity": "sha1-nNnABpV+vpX62tW9YJiUKoE3N/Y=",
+			"dev": true,
+			"requires": {
+				"file-type": "^3.1.0"
+			},
+			"dependencies": {
+				"file-type": {
+					"version": "3.9.0",
+					"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
+					"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=",
+					"dev": true
+				}
+			}
+		},
+		"are-we-there-yet": {
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+			"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+			"dev": true,
+			"requires": {
+				"delegates": "^1.0.0",
+				"readable-stream": "^2.0.6"
+			}
+		},
+		"argparse": {
+			"version": "1.0.10",
+			"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+			"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+			"dev": true,
+			"requires": {
+				"sprintf-js": "~1.0.2"
+			},
+			"dependencies": {
+				"sprintf-js": {
+					"version": "1.0.3",
+					"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+					"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+					"dev": true
+				}
+			}
+		},
+		"arr-diff": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+			"integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+			"dev": true
+		},
+		"arr-flatten": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+			"integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+			"dev": true
+		},
+		"arr-union": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+			"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+			"dev": true
+		},
+		"array-differ": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
+			"integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
+			"dev": true
+		},
+		"array-find-index": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+			"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+			"dev": true
+		},
+		"array-union": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+			"integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+			"dev": true,
+			"requires": {
+				"array-uniq": "^1.0.1"
+			}
+		},
+		"array-uniq": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+			"integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+			"dev": true
+		},
+		"array-unique": {
+			"version": "0.3.2",
+			"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+			"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+			"dev": true
+		},
+		"arrify": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+			"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+			"dev": true
+		},
+		"asn1": {
+			"version": "0.2.4",
+			"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+			"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+			"dev": true,
+			"requires": {
+				"safer-buffer": "~2.1.0"
+			}
+		},
+		"assert-plus": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+			"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+			"dev": true
+		},
+		"assign-symbols": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+			"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+			"dev": true
+		},
+		"async": {
+			"version": "1.5.2",
+			"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+			"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+			"dev": true
+		},
+		"async-each-series": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-1.1.0.tgz",
+			"integrity": "sha1-9C/YFV048hpbjqB8KOBj7RcAsTg=",
+			"dev": true,
+			"optional": true
+		},
+		"async-foreach": {
+			"version": "0.1.3",
+			"resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+			"integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+			"dev": true
+		},
+		"asynckit": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+			"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+			"dev": true
+		},
+		"atob": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+			"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+			"dev": true
+		},
+		"autoprefixer": {
+			"version": "8.5.2",
+			"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.5.2.tgz",
+			"integrity": "sha512-X3BmI+0YBTGaqw0cR9FCPTyTAAPRpijzIUPkysRumGmJC6r8vhix1RjzLNjX01Kbb/iSezjZ9OR3G92pa31+WQ==",
+			"dev": true,
+			"requires": {
+				"browserslist": "^3.2.8",
+				"caniuse-lite": "^1.0.30000846",
+				"normalize-range": "^0.1.2",
+				"num2fraction": "^1.2.2",
+				"postcss": "^6.0.22",
+				"postcss-value-parser": "^3.2.3"
+			}
+		},
+		"aws-sign2": {
+			"version": "0.7.0",
+			"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+			"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+			"dev": true
+		},
+		"aws4": {
+			"version": "1.8.0",
+			"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+			"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+			"dev": true
+		},
+		"balanced-match": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+			"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+			"dev": true
+		},
+		"base": {
+			"version": "0.11.2",
+			"resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+			"integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+			"dev": true,
+			"requires": {
+				"cache-base": "^1.0.1",
+				"class-utils": "^0.3.5",
+				"component-emitter": "^1.2.1",
+				"define-property": "^1.0.0",
+				"isobject": "^3.0.1",
+				"mixin-deep": "^1.2.0",
+				"pascalcase": "^0.1.1"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+					"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^1.0.0"
+					}
+				},
+				"is-accessor-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+					"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-data-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+					"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-descriptor": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+					"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+					"dev": true,
+					"requires": {
+						"is-accessor-descriptor": "^1.0.0",
+						"is-data-descriptor": "^1.0.0",
+						"kind-of": "^6.0.2"
+					}
+				}
+			}
+		},
+		"bcrypt-pbkdf": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+			"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"tweetnacl": "^0.14.3"
+			}
+		},
+		"beeper": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz",
+			"integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=",
+			"dev": true
+		},
+		"bin-build": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/bin-build/-/bin-build-2.2.0.tgz",
+			"integrity": "sha1-EfjdYfcP/Por3KpbRvXo/t1CIcw=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"archive-type": "^3.0.1",
+				"decompress": "^3.0.0",
+				"download": "^4.1.2",
+				"exec-series": "^1.0.0",
+				"rimraf": "^2.2.6",
+				"tempfile": "^1.0.0",
+				"url-regex": "^3.0.0"
+			},
+			"dependencies": {
+				"tempfile": {
+					"version": "1.1.1",
+					"resolved": "https://registry.npmjs.org/tempfile/-/tempfile-1.1.1.tgz",
+					"integrity": "sha1-W8xOrsxKsscH2LwR2ZzMmiyyh/I=",
+					"dev": true,
+					"optional": true,
+					"requires": {
+						"os-tmpdir": "^1.0.0",
+						"uuid": "^2.0.1"
+					}
+				},
+				"uuid": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
+					"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"bin-check": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/bin-check/-/bin-check-2.0.0.tgz",
+			"integrity": "sha1-hvjm9CU4k99g3DFpV/WvAqywWTA=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"executable": "^1.0.0"
+			}
+		},
+		"bin-version": {
+			"version": "1.0.4",
+			"resolved": "https://registry.npmjs.org/bin-version/-/bin-version-1.0.4.tgz",
+			"integrity": "sha1-nrSY7m/Xb3q5p8FgQ2+JV5Q1144=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"find-versions": "^1.0.0"
+			}
+		},
+		"bin-version-check": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-2.1.0.tgz",
+			"integrity": "sha1-5OXfKQuQaffRETJAMe/BP90RpbA=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"bin-version": "^1.0.0",
+				"minimist": "^1.1.0",
+				"semver": "^4.0.3",
+				"semver-truncate": "^1.0.0"
+			},
+			"dependencies": {
+				"semver": {
+					"version": "4.3.6",
+					"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
+					"integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"bin-wrapper": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-3.0.2.tgz",
+			"integrity": "sha1-Z9MwYmLksaXy+I7iNGT2plVneus=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"bin-check": "^2.0.0",
+				"bin-version-check": "^2.1.0",
+				"download": "^4.0.0",
+				"each-async": "^1.1.1",
+				"lazy-req": "^1.0.0",
+				"os-filter-obj": "^1.0.0"
+			}
+		},
+		"bl": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+			"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
+			"dev": true,
+			"requires": {
+				"readable-stream": "^2.3.5",
+				"safe-buffer": "^5.1.1"
+			}
+		},
+		"block-stream": {
+			"version": "0.0.9",
+			"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+			"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+			"dev": true,
+			"requires": {
+				"inherits": "~2.0.0"
+			}
+		},
+		"bluebird": {
+			"version": "3.5.1",
+			"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
+			"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
+			"dev": true
+		},
+		"body": {
+			"version": "5.1.0",
+			"resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz",
+			"integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=",
+			"dev": true,
+			"requires": {
+				"continuable-cache": "^0.3.1",
+				"error": "^7.0.0",
+				"raw-body": "~1.1.0",
+				"safe-json-parse": "~1.0.1"
+			}
+		},
+		"bower-config": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/bower-config/-/bower-config-1.4.1.tgz",
+			"integrity": "sha1-hf2d82fCuNu9DKpMXyutQM2Ewsw=",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.1.3",
+				"mout": "^1.0.0",
+				"optimist": "^0.6.1",
+				"osenv": "^0.1.3",
+				"untildify": "^2.1.0"
+			}
+		},
+		"brace-expansion": {
+			"version": "1.1.11",
+			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+			"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+			"dev": true,
+			"requires": {
+				"balanced-match": "^1.0.0",
+				"concat-map": "0.0.1"
+			}
+		},
+		"braces": {
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+			"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+			"dev": true,
+			"requires": {
+				"arr-flatten": "^1.1.0",
+				"array-unique": "^0.3.2",
+				"extend-shallow": "^2.0.1",
+				"fill-range": "^4.0.0",
+				"isobject": "^3.0.1",
+				"repeat-element": "^1.1.2",
+				"snapdragon": "^0.8.1",
+				"snapdragon-node": "^2.0.1",
+				"split-string": "^3.0.2",
+				"to-regex": "^3.0.1"
+			},
+			"dependencies": {
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				}
+			}
+		},
+		"browserify-zlib": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
+			"integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=",
+			"dev": true,
+			"requires": {
+				"pako": "~0.2.0"
+			}
+		},
+		"browserslist": {
+			"version": "3.2.8",
+			"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
+			"integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
+			"dev": true,
+			"requires": {
+				"caniuse-lite": "^1.0.30000844",
+				"electron-to-chromium": "^1.3.47"
+			}
+		},
+		"buffer-alloc": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+			"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+			"dev": true,
+			"requires": {
+				"buffer-alloc-unsafe": "^1.1.0",
+				"buffer-fill": "^1.0.0"
+			}
+		},
+		"buffer-alloc-unsafe": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+			"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
+			"dev": true
+		},
+		"buffer-crc32": {
+			"version": "0.2.13",
+			"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+			"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+			"dev": true
+		},
+		"buffer-fill": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+			"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
+			"dev": true
+		},
+		"buffer-from": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+			"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+			"dev": true
+		},
+		"buffer-to-vinyl": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/buffer-to-vinyl/-/buffer-to-vinyl-1.1.0.tgz",
+			"integrity": "sha1-APFfruOreh3aLN5tkSG//dB7ImI=",
+			"dev": true,
+			"requires": {
+				"file-type": "^3.1.0",
+				"readable-stream": "^2.0.2",
+				"uuid": "^2.0.1",
+				"vinyl": "^1.0.0"
+			},
+			"dependencies": {
+				"file-type": {
+					"version": "3.9.0",
+					"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
+					"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=",
+					"dev": true
+				},
+				"uuid": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
+					"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
+					"dev": true
+				}
+			}
+		},
+		"builtin-modules": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+			"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+			"dev": true
+		},
+		"bytes": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz",
+			"integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=",
+			"dev": true
+		},
+		"cache-base": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+			"integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+			"dev": true,
+			"requires": {
+				"collection-visit": "^1.0.0",
+				"component-emitter": "^1.2.1",
+				"get-value": "^2.0.6",
+				"has-value": "^1.0.0",
+				"isobject": "^3.0.1",
+				"set-value": "^2.0.0",
+				"to-object-path": "^0.3.0",
+				"union-value": "^1.0.0",
+				"unset-value": "^1.0.0"
+			}
+		},
+		"cache-swap": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/cache-swap/-/cache-swap-0.3.0.tgz",
+			"integrity": "sha1-HFQaoQilAQb2ML3Zj+HeyLoTP1E=",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.1.2",
+				"mkdirp": "^0.5.1",
+				"object-assign": "^4.0.1",
+				"rimraf": "^2.4.0"
+			}
+		},
+		"camelcase": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+			"integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+			"dev": true
+		},
+		"camelcase-keys": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+			"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+			"dev": true,
+			"requires": {
+				"camelcase": "^2.0.0",
+				"map-obj": "^1.0.0"
+			}
+		},
+		"caniuse-db": {
+			"version": "1.0.30000878",
+			"resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000878.tgz",
+			"integrity": "sha512-xv+UnUOYvS53KH1Yb+qro28Ojd7hCqf/NIVap64KDopQ2sNPQbk9/Dv9/ekuhgEy5EafUZPY5OJEQmNOzX825A==",
+			"dev": true
+		},
+		"caniuse-lite": {
+			"version": "1.0.30000878",
+			"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000878.tgz",
+			"integrity": "sha512-/dCGTdLCnjVJno1mFRn7Y6eit3AYaeFzSrMQHCoK0LEQaWl5snuLex1Ky4b8/Qu2ig5NgTX4cJx65hH9546puA==",
+			"dev": true
+		},
+		"capture-stack-trace": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz",
+			"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
+			"dev": true
+		},
+		"caseless": {
+			"version": "0.12.0",
+			"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+			"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+			"dev": true
+		},
+		"caw": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/caw/-/caw-1.2.0.tgz",
+			"integrity": "sha1-/7Im/n78VHKI3GLuPpcHPCEtEDQ=",
+			"dev": true,
+			"requires": {
+				"get-proxy": "^1.0.1",
+				"is-obj": "^1.0.0",
+				"object-assign": "^3.0.0",
+				"tunnel-agent": "^0.4.0"
+			},
+			"dependencies": {
+				"object-assign": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+					"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=",
+					"dev": true
+				}
+			}
+		},
+		"chalk": {
+			"version": "2.4.1",
+			"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+			"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+			"dev": true,
+			"requires": {
+				"ansi-styles": "^3.2.1",
+				"escape-string-regexp": "^1.0.5",
+				"supports-color": "^5.3.0"
+			}
+		},
+		"chardet": {
+			"version": "0.4.2",
+			"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+			"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
+			"dev": true
+		},
+		"check-dependencies": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/check-dependencies/-/check-dependencies-1.1.0.tgz",
+			"integrity": "sha512-GDrbGzzJ6Gc6tQh87HBMGhrJ4UWIlR9MKJwgvlrJyj/gWvTYYb2jQetKbajt/EYK5Y8/4g7gH2LEvq8GdUWTag==",
+			"dev": true,
+			"requires": {
+				"bower-config": "^1.4.0",
+				"chalk": "^2.1.0",
+				"findup-sync": "^2.0.0",
+				"lodash.camelcase": "^4.3.0",
+				"minimist": "^1.2.0",
+				"semver": "^5.4.1"
+			},
+			"dependencies": {
+				"findup-sync": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+					"integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+					"dev": true,
+					"requires": {
+						"detect-file": "^1.0.0",
+						"is-glob": "^3.1.0",
+						"micromatch": "^3.0.4",
+						"resolve-dir": "^1.0.1"
+					}
+				}
+			}
+		},
+		"circular-json": {
+			"version": "0.3.3",
+			"resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+			"integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
+			"dev": true
+		},
+		"clap": {
+			"version": "1.2.3",
+			"resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz",
+			"integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"chalk": "^1.1.3"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true,
+					"optional": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"optional": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"class-utils": {
+			"version": "0.3.6",
+			"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+			"integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+			"dev": true,
+			"requires": {
+				"arr-union": "^3.1.0",
+				"define-property": "^0.2.5",
+				"isobject": "^3.0.0",
+				"static-extend": "^0.1.1"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "0.2.5",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+					"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^0.1.0"
+					}
+				}
+			}
+		},
+		"clean-css": {
+			"version": "4.1.11",
+			"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz",
+			"integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=",
+			"dev": true,
+			"requires": {
+				"source-map": "0.5.x"
+			},
+			"dependencies": {
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				}
+			}
+		},
+		"cli": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz",
+			"integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=",
+			"dev": true,
+			"requires": {
+				"exit": "0.1.2",
+				"glob": "^7.1.1"
+			},
+			"dependencies": {
+				"glob": {
+					"version": "7.1.2",
+					"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+					"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+					"dev": true,
+					"requires": {
+						"fs.realpath": "^1.0.0",
+						"inflight": "^1.0.4",
+						"inherits": "2",
+						"minimatch": "^3.0.4",
+						"once": "^1.3.0",
+						"path-is-absolute": "^1.0.0"
+					}
+				}
+			}
+		},
+		"cli-cursor": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+			"integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+			"dev": true,
+			"requires": {
+				"restore-cursor": "^2.0.0"
+			}
+		},
+		"cli-width": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+			"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+			"dev": true
+		},
+		"cliui": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+			"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+			"dev": true,
+			"requires": {
+				"string-width": "^1.0.1",
+				"strip-ansi": "^3.0.1",
+				"wrap-ansi": "^2.0.0"
+			},
+			"dependencies": {
+				"is-fullwidth-code-point": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+					"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+					"dev": true,
+					"requires": {
+						"number-is-nan": "^1.0.0"
+					}
+				},
+				"string-width": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+					"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+					"dev": true,
+					"requires": {
+						"code-point-at": "^1.0.0",
+						"is-fullwidth-code-point": "^1.0.0",
+						"strip-ansi": "^3.0.0"
+					}
+				}
+			}
+		},
+		"clone": {
+			"version": "1.0.4",
+			"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+			"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+			"dev": true
+		},
+		"clone-regexp": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz",
+			"integrity": "sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==",
+			"dev": true,
+			"requires": {
+				"is-regexp": "^1.0.0",
+				"is-supported-regexp-flag": "^1.0.0"
+			}
+		},
+		"clone-stats": {
+			"version": "0.0.1",
+			"resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz",
+			"integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=",
+			"dev": true
+		},
+		"co": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz",
+			"integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=",
+			"dev": true
+		},
+		"coa": {
+			"version": "1.0.4",
+			"resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz",
+			"integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"q": "^1.1.2"
+			}
+		},
+		"code-point-at": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+			"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+			"dev": true
+		},
+		"coffeescript": {
+			"version": "1.10.0",
+			"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz",
+			"integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=",
+			"dev": true
+		},
+		"collection-visit": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+			"integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+			"dev": true,
+			"requires": {
+				"map-visit": "^1.0.0",
+				"object-visit": "^1.0.0"
+			}
+		},
+		"color-convert": {
+			"version": "1.9.2",
+			"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz",
+			"integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==",
+			"dev": true,
+			"requires": {
+				"color-name": "1.1.1"
+			}
+		},
+		"color-diff": {
+			"version": "0.1.7",
+			"resolved": "https://registry.npmjs.org/color-diff/-/color-diff-0.1.7.tgz",
+			"integrity": "sha1-bbeM2UgqjkWdQIIer0tQMoPcuOI=",
+			"dev": true
+		},
+		"color-name": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
+			"integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=",
+			"dev": true
+		},
+		"color-support": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+			"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+			"dev": true
+		},
+		"colorguard": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/colorguard/-/colorguard-1.2.1.tgz",
+			"integrity": "sha512-qYVKTg626qpDg4/eBnPXidEPXn5+krbYqHVfyyEFBWV5z3IF4p44HKY/eE2t1ohlcrlIkDgHmFJMfQ8qMLnSFw==",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.1.1",
+				"color-diff": "^0.1.3",
+				"log-symbols": "^1.0.2",
+				"object-assign": "^4.0.1",
+				"pipetteur": "^2.0.0",
+				"plur": "^2.0.0",
+				"postcss": "^5.0.4",
+				"postcss-reporter": "^1.2.1",
+				"text-table": "^0.2.0",
+				"yargs": "^1.2.6"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "3.2.3",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+							"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+							"dev": true,
+							"requires": {
+								"has-flag": "^1.0.0"
+							}
+						}
+					}
+				},
+				"postcss-reporter": {
+					"version": "1.4.1",
+					"resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-1.4.1.tgz",
+					"integrity": "sha1-wTbwpbFhkV83ndN2XGEHX357mvI=",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.0.0",
+						"lodash": "^4.1.0",
+						"log-symbols": "^1.0.2",
+						"postcss": "^5.0.0"
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				},
+				"yargs": {
+					"version": "1.3.3",
+					"resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz",
+					"integrity": "sha1-BU3oth8i7v23IHBZ6u+da4P7kxo=",
+					"dev": true
+				}
+			}
+		},
+		"colors": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+			"integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+			"dev": true
+		},
+		"combined-stream": {
+			"version": "1.0.6",
+			"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+			"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+			"dev": true,
+			"requires": {
+				"delayed-stream": "~1.0.0"
+			}
+		},
+		"commander": {
+			"version": "2.8.1",
+			"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
+			"integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
+			"dev": true,
+			"requires": {
+				"graceful-readlink": ">= 1.0.0"
+			}
+		},
+		"component-emitter": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+			"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+			"dev": true
+		},
+		"concat-map": {
+			"version": "0.0.1",
+			"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+			"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+			"dev": true
+		},
+		"concat-stream": {
+			"version": "1.6.2",
+			"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+			"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+			"dev": true,
+			"requires": {
+				"buffer-from": "^1.0.0",
+				"inherits": "^2.0.3",
+				"readable-stream": "^2.2.2",
+				"typedarray": "^0.0.6"
+			}
+		},
+		"console-browserify": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+			"integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+			"dev": true,
+			"requires": {
+				"date-now": "^0.1.4"
+			}
+		},
+		"console-control-strings": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+			"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+			"dev": true
+		},
+		"console-stream": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz",
+			"integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ=",
+			"dev": true,
+			"optional": true
+		},
+		"continuable-cache": {
+			"version": "0.3.1",
+			"resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz",
+			"integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=",
+			"dev": true
+		},
+		"convert-source-map": {
+			"version": "1.5.1",
+			"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz",
+			"integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=",
+			"dev": true
+		},
+		"copy-descriptor": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+			"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+			"dev": true
+		},
+		"core-util-is": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+			"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+			"dev": true
+		},
+		"cosmiconfig": {
+			"version": "2.2.2",
+			"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz",
+			"integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==",
+			"dev": true,
+			"requires": {
+				"is-directory": "^0.3.1",
+				"js-yaml": "^3.4.3",
+				"minimist": "^1.2.0",
+				"object-assign": "^4.1.0",
+				"os-homedir": "^1.0.1",
+				"parse-json": "^2.2.0",
+				"require-from-string": "^1.1.0"
+			}
+		},
+		"create-error-class": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
+			"integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
+			"dev": true,
+			"requires": {
+				"capture-stack-trace": "^1.0.0"
+			}
+		},
+		"cross-spawn": {
+			"version": "5.1.0",
+			"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+			"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"lru-cache": "^4.0.1",
+				"shebang-command": "^1.2.0",
+				"which": "^1.2.9"
+			}
+		},
+		"css-color-names": {
+			"version": "0.0.3",
+			"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.3.tgz",
+			"integrity": "sha1-3gzvFvTYqoIioyDVttfpu62nufY=",
+			"dev": true
+		},
+		"css-rule-stream": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/css-rule-stream/-/css-rule-stream-1.1.0.tgz",
+			"integrity": "sha1-N4bnGYmD2WWibjGVfgkHjLt3BaI=",
+			"dev": true,
+			"requires": {
+				"css-tokenize": "^1.0.1",
+				"duplexer2": "0.0.2",
+				"ldjson-stream": "^1.2.1",
+				"through2": "^0.6.3"
+			},
+			"dependencies": {
+				"duplexer2": {
+					"version": "0.0.2",
+					"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+					"integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "~1.1.9"
+					}
+				},
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+					"dev": true
+				},
+				"readable-stream": {
+					"version": "1.1.14",
+					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+					"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+					"dev": true,
+					"requires": {
+						"core-util-is": "~1.0.0",
+						"inherits": "~2.0.1",
+						"isarray": "0.0.1",
+						"string_decoder": "~0.10.x"
+					}
+				},
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				}
+			}
+		},
+		"css-tokenize": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/css-tokenize/-/css-tokenize-1.0.1.tgz",
+			"integrity": "sha1-RiXLHtohwUOFi3+B1oA8HSb8FL4=",
+			"dev": true,
+			"requires": {
+				"inherits": "^2.0.1",
+				"readable-stream": "^1.0.33"
+			},
+			"dependencies": {
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+					"dev": true
+				},
+				"readable-stream": {
+					"version": "1.1.14",
+					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+					"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+					"dev": true,
+					"requires": {
+						"core-util-is": "~1.0.0",
+						"inherits": "~2.0.1",
+						"isarray": "0.0.1",
+						"string_decoder": "~0.10.x"
+					}
+				},
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				}
+			}
+		},
+		"csso": {
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz",
+			"integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"clap": "^1.0.9",
+				"source-map": "^0.5.3"
+			},
+			"dependencies": {
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"currently-unhandled": {
+			"version": "0.4.1",
+			"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+			"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+			"dev": true,
+			"requires": {
+				"array-find-index": "^1.0.1"
+			}
+		},
+		"cycle": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
+			"integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=",
+			"dev": true,
+			"optional": true
+		},
+		"dashdash": {
+			"version": "1.14.1",
+			"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+			"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+			"dev": true,
+			"requires": {
+				"assert-plus": "^1.0.0"
+			}
+		},
+		"date-now": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+			"integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+			"dev": true
+		},
+		"dateformat": {
+			"version": "1.0.12",
+			"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+			"integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+			"dev": true,
+			"requires": {
+				"get-stdin": "^4.0.1",
+				"meow": "^3.3.0"
+			}
+		},
+		"debug": {
+			"version": "2.6.9",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+			"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+			"dev": true,
+			"requires": {
+				"ms": "2.0.0"
+			}
+		},
+		"decamelize": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+			"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+			"dev": true
+		},
+		"decode-uri-component": {
+			"version": "0.2.0",
+			"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+			"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+			"dev": true
+		},
+		"decompress": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/decompress/-/decompress-3.0.0.tgz",
+			"integrity": "sha1-rx3VDQbjv8QyRh033hGzjA2ZG+0=",
+			"dev": true,
+			"requires": {
+				"buffer-to-vinyl": "^1.0.0",
+				"concat-stream": "^1.4.6",
+				"decompress-tar": "^3.0.0",
+				"decompress-tarbz2": "^3.0.0",
+				"decompress-targz": "^3.0.0",
+				"decompress-unzip": "^3.0.0",
+				"stream-combiner2": "^1.1.1",
+				"vinyl-assign": "^1.0.1",
+				"vinyl-fs": "^2.2.0"
+			}
+		},
+		"decompress-tar": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-3.1.0.tgz",
+			"integrity": "sha1-IXx4n5uURQ76rcXF5TeXj8MzxGY=",
+			"dev": true,
+			"requires": {
+				"is-tar": "^1.0.0",
+				"object-assign": "^2.0.0",
+				"strip-dirs": "^1.0.0",
+				"tar-stream": "^1.1.1",
+				"through2": "^0.6.1",
+				"vinyl": "^0.4.3"
+			},
+			"dependencies": {
+				"clone": {
+					"version": "0.2.0",
+					"resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz",
+					"integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=",
+					"dev": true
+				},
+				"object-assign": {
+					"version": "2.1.1",
+					"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz",
+					"integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=",
+					"dev": true
+				},
+				"vinyl": {
+					"version": "0.4.6",
+					"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz",
+					"integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=",
+					"dev": true,
+					"requires": {
+						"clone": "^0.2.0",
+						"clone-stats": "^0.0.1"
+					}
+				}
+			}
+		},
+		"decompress-tarbz2": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-3.1.0.tgz",
+			"integrity": "sha1-iyOTVoE1X58YnYclag+L3ZbZZm0=",
+			"dev": true,
+			"requires": {
+				"is-bzip2": "^1.0.0",
+				"object-assign": "^2.0.0",
+				"seek-bzip": "^1.0.3",
+				"strip-dirs": "^1.0.0",
+				"tar-stream": "^1.1.1",
+				"through2": "^0.6.1",
+				"vinyl": "^0.4.3"
+			},
+			"dependencies": {
+				"clone": {
+					"version": "0.2.0",
+					"resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz",
+					"integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=",
+					"dev": true
+				},
+				"object-assign": {
+					"version": "2.1.1",
+					"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz",
+					"integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=",
+					"dev": true
+				},
+				"vinyl": {
+					"version": "0.4.6",
+					"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz",
+					"integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=",
+					"dev": true,
+					"requires": {
+						"clone": "^0.2.0",
+						"clone-stats": "^0.0.1"
+					}
+				}
+			}
+		},
+		"decompress-targz": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-3.1.0.tgz",
+			"integrity": "sha1-ssE9+YFmJomRtxXWRH9kLpaW9aA=",
+			"dev": true,
+			"requires": {
+				"is-gzip": "^1.0.0",
+				"object-assign": "^2.0.0",
+				"strip-dirs": "^1.0.0",
+				"tar-stream": "^1.1.1",
+				"through2": "^0.6.1",
+				"vinyl": "^0.4.3"
+			},
+			"dependencies": {
+				"clone": {
+					"version": "0.2.0",
+					"resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz",
+					"integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=",
+					"dev": true
+				},
+				"object-assign": {
+					"version": "2.1.1",
+					"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz",
+					"integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=",
+					"dev": true
+				},
+				"vinyl": {
+					"version": "0.4.6",
+					"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz",
+					"integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=",
+					"dev": true,
+					"requires": {
+						"clone": "^0.2.0",
+						"clone-stats": "^0.0.1"
+					}
+				}
+			}
+		},
+		"decompress-unzip": {
+			"version": "3.4.0",
+			"resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-3.4.0.tgz",
+			"integrity": "sha1-YUdbQVIGa74/7hL51inRX+ZHjus=",
+			"dev": true,
+			"requires": {
+				"is-zip": "^1.0.0",
+				"read-all-stream": "^3.0.0",
+				"stat-mode": "^0.2.0",
+				"strip-dirs": "^1.0.0",
+				"through2": "^2.0.0",
+				"vinyl": "^1.0.0",
+				"yauzl": "^2.2.1"
+			},
+			"dependencies": {
+				"through2": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+					"integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "^2.1.5",
+						"xtend": "~4.0.1"
+					}
+				}
+			}
+		},
+		"deep-extend": {
+			"version": "0.6.0",
+			"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+			"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+			"dev": true
+		},
+		"define-property": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+			"integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+			"dev": true,
+			"requires": {
+				"is-descriptor": "^1.0.2",
+				"isobject": "^3.0.1"
+			},
+			"dependencies": {
+				"is-accessor-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+					"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-data-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+					"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-descriptor": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+					"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+					"dev": true,
+					"requires": {
+						"is-accessor-descriptor": "^1.0.0",
+						"is-data-descriptor": "^1.0.0",
+						"kind-of": "^6.0.2"
+					}
+				}
+			}
+		},
+		"del": {
+			"version": "2.2.2",
+			"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+			"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+			"dev": true,
+			"requires": {
+				"globby": "^5.0.0",
+				"is-path-cwd": "^1.0.0",
+				"is-path-in-cwd": "^1.0.0",
+				"object-assign": "^4.0.1",
+				"pify": "^2.0.0",
+				"pinkie-promise": "^2.0.0",
+				"rimraf": "^2.2.8"
+			},
+			"dependencies": {
+				"globby": {
+					"version": "5.0.0",
+					"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+					"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+					"dev": true,
+					"requires": {
+						"array-union": "^1.0.1",
+						"arrify": "^1.0.0",
+						"glob": "^7.0.3",
+						"object-assign": "^4.0.1",
+						"pify": "^2.0.0",
+						"pinkie-promise": "^2.0.0"
+					}
+				}
+			}
+		},
+		"delayed-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+			"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+			"dev": true
+		},
+		"delegates": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+			"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+			"dev": true
+		},
+		"detect-file": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+			"integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+			"dev": true
+		},
+		"diff": {
+			"version": "3.5.0",
+			"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+			"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+			"dev": true
+		},
+		"doiuse": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/doiuse/-/doiuse-2.6.0.tgz",
+			"integrity": "sha1-GJLRC2Gpo1at2/K2FJM+gfi7ODQ=",
+			"dev": true,
+			"requires": {
+				"browserslist": "^1.1.1",
+				"caniuse-db": "^1.0.30000187",
+				"css-rule-stream": "^1.1.0",
+				"duplexer2": "0.0.2",
+				"jsonfilter": "^1.1.2",
+				"ldjson-stream": "^1.2.1",
+				"lodash": "^4.0.0",
+				"multimatch": "^2.0.0",
+				"postcss": "^5.0.8",
+				"source-map": "^0.4.2",
+				"through2": "^0.6.3",
+				"yargs": "^3.5.4"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"browserslist": {
+					"version": "1.7.7",
+					"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+					"integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+					"dev": true,
+					"requires": {
+						"caniuse-db": "^1.0.30000639",
+						"electron-to-chromium": "^1.2.7"
+					}
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "2.0.0",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+							"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+							"dev": true
+						}
+					}
+				},
+				"duplexer2": {
+					"version": "0.0.2",
+					"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+					"integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "~1.1.9"
+					}
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"is-fullwidth-code-point": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+					"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+					"dev": true,
+					"requires": {
+						"number-is-nan": "^1.0.0"
+					}
+				},
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+					"dev": true
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					},
+					"dependencies": {
+						"source-map": {
+							"version": "0.5.7",
+							"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+							"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+							"dev": true
+						}
+					}
+				},
+				"readable-stream": {
+					"version": "1.1.14",
+					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+					"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+					"dev": true,
+					"requires": {
+						"core-util-is": "~1.0.0",
+						"inherits": "~2.0.1",
+						"isarray": "0.0.1",
+						"string_decoder": "~0.10.x"
+					}
+				},
+				"source-map": {
+					"version": "0.4.4",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+					"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+					"dev": true,
+					"requires": {
+						"amdefine": ">=0.0.4"
+					}
+				},
+				"string-width": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+					"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+					"dev": true,
+					"requires": {
+						"code-point-at": "^1.0.0",
+						"is-fullwidth-code-point": "^1.0.0",
+						"strip-ansi": "^3.0.0"
+					}
+				},
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "3.2.3",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+					"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+					"dev": true,
+					"requires": {
+						"has-flag": "^1.0.0"
+					}
+				},
+				"yargs": {
+					"version": "3.32.0",
+					"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
+					"integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
+					"dev": true,
+					"requires": {
+						"camelcase": "^2.0.1",
+						"cliui": "^3.0.3",
+						"decamelize": "^1.1.1",
+						"os-locale": "^1.4.0",
+						"string-width": "^1.0.1",
+						"window-size": "^0.1.4",
+						"y18n": "^3.2.0"
+					}
+				}
+			}
+		},
+		"dom-serializer": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+			"integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+			"dev": true,
+			"requires": {
+				"domelementtype": "~1.1.1",
+				"entities": "~1.1.1"
+			},
+			"dependencies": {
+				"domelementtype": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+					"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
+					"dev": true
+				},
+				"entities": {
+					"version": "1.1.1",
+					"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+					"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
+					"dev": true
+				}
+			}
+		},
+		"domelementtype": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+			"integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
+			"dev": true
+		},
+		"domhandler": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz",
+			"integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=",
+			"dev": true,
+			"requires": {
+				"domelementtype": "1"
+			}
+		},
+		"domutils": {
+			"version": "1.5.1",
+			"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+			"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
+			"dev": true,
+			"requires": {
+				"dom-serializer": "0",
+				"domelementtype": "1"
+			}
+		},
+		"download": {
+			"version": "4.4.3",
+			"resolved": "https://registry.npmjs.org/download/-/download-4.4.3.tgz",
+			"integrity": "sha1-qlX9rTktldS2jowr4D4MKqIbqaw=",
+			"dev": true,
+			"requires": {
+				"caw": "^1.0.1",
+				"concat-stream": "^1.4.7",
+				"each-async": "^1.0.0",
+				"filenamify": "^1.0.1",
+				"got": "^5.0.0",
+				"gulp-decompress": "^1.2.0",
+				"gulp-rename": "^1.2.0",
+				"is-url": "^1.2.0",
+				"object-assign": "^4.0.1",
+				"read-all-stream": "^3.0.0",
+				"readable-stream": "^2.0.2",
+				"stream-combiner2": "^1.1.1",
+				"vinyl": "^1.0.0",
+				"vinyl-fs": "^2.2.0",
+				"ware": "^1.2.0"
+			}
+		},
+		"duplexer": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+			"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
+			"dev": true
+		},
+		"duplexer2": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+			"integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+			"dev": true,
+			"requires": {
+				"readable-stream": "^2.0.2"
+			}
+		},
+		"duplexify": {
+			"version": "3.6.0",
+			"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz",
+			"integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==",
+			"dev": true,
+			"requires": {
+				"end-of-stream": "^1.0.0",
+				"inherits": "^2.0.1",
+				"readable-stream": "^2.0.0",
+				"stream-shift": "^1.0.0"
+			}
+		},
+		"each-async": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz",
+			"integrity": "sha1-3uUim98KtrogEqOV4bhpq/iBNHM=",
+			"dev": true,
+			"requires": {
+				"onetime": "^1.0.0",
+				"set-immediate-shim": "^1.0.0"
+			}
+		},
+		"ecc-jsbn": {
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+			"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"jsbn": "~0.1.0",
+				"safer-buffer": "^2.1.0"
+			}
+		},
+		"electron-to-chromium": {
+			"version": "1.3.61",
+			"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.61.tgz",
+			"integrity": "sha512-XjTdsm6x71Y48lF9EEvGciwXD70b20g0t+3YbrE+0fPFutqV08DSNrZXkoXAp3QuzX7TpL/OW+/VsNoR9GkuNg==",
+			"dev": true
+		},
+		"encoding": {
+			"version": "0.1.12",
+			"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+			"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+			"dev": true,
+			"requires": {
+				"iconv-lite": "~0.4.13"
+			}
+		},
+		"end-of-stream": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+			"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+			"dev": true,
+			"requires": {
+				"once": "^1.4.0"
+			}
+		},
+		"entities": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
+			"integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=",
+			"dev": true
+		},
+		"error": {
+			"version": "7.0.2",
+			"resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz",
+			"integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=",
+			"dev": true,
+			"requires": {
+				"string-template": "~0.2.1",
+				"xtend": "~4.0.0"
+			}
+		},
+		"error-ex": {
+			"version": "1.3.2",
+			"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+			"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+			"dev": true,
+			"requires": {
+				"is-arrayish": "^0.2.1"
+			}
+		},
+		"es6-promise": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
+			"integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
+			"dev": true,
+			"optional": true
+		},
+		"escape-string-regexp": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+			"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+			"dev": true
+		},
+		"esprima": {
+			"version": "2.7.3",
+			"resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+			"integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
+			"dev": true
+		},
+		"eventemitter2": {
+			"version": "0.4.14",
+			"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+			"integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
+			"dev": true
+		},
+		"exec-buffer": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz",
+			"integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"execa": "^0.7.0",
+				"p-finally": "^1.0.0",
+				"pify": "^3.0.0",
+				"rimraf": "^2.5.4",
+				"tempfile": "^2.0.0"
+			},
+			"dependencies": {
+				"pify": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+					"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"exec-series": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/exec-series/-/exec-series-1.0.3.tgz",
+			"integrity": "sha1-bSV6m+rEgqhyx3g7yGFYOfx3FDo=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"async-each-series": "^1.1.0",
+				"object-assign": "^4.1.0"
+			}
+		},
+		"execa": {
+			"version": "0.7.0",
+			"resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+			"integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"cross-spawn": "^5.0.1",
+				"get-stream": "^3.0.0",
+				"is-stream": "^1.1.0",
+				"npm-run-path": "^2.0.0",
+				"p-finally": "^1.0.0",
+				"signal-exit": "^3.0.0",
+				"strip-eof": "^1.0.0"
+			}
+		},
+		"execall": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/execall/-/execall-1.0.0.tgz",
+			"integrity": "sha1-c9CQTjlbPKsGWLCNCewlMH8pu3M=",
+			"dev": true,
+			"requires": {
+				"clone-regexp": "^1.0.0"
+			}
+		},
+		"executable": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/executable/-/executable-1.1.0.tgz",
+			"integrity": "sha1-h3mA6REvM5EGbaNyZd562ENKtNk=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"meow": "^3.1.0"
+			}
+		},
+		"exit": {
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+			"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+			"dev": true
+		},
+		"expand-brackets": {
+			"version": "2.1.4",
+			"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+			"integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+			"dev": true,
+			"requires": {
+				"debug": "^2.3.3",
+				"define-property": "^0.2.5",
+				"extend-shallow": "^2.0.1",
+				"posix-character-classes": "^0.1.0",
+				"regex-not": "^1.0.0",
+				"snapdragon": "^0.8.1",
+				"to-regex": "^3.0.1"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "0.2.5",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+					"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^0.1.0"
+					}
+				},
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				}
+			}
+		},
+		"expand-range": {
+			"version": "1.8.2",
+			"resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+			"integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+			"dev": true,
+			"requires": {
+				"fill-range": "^2.1.0"
+			},
+			"dependencies": {
+				"fill-range": {
+					"version": "2.2.4",
+					"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
+					"integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+					"dev": true,
+					"requires": {
+						"is-number": "^2.1.0",
+						"isobject": "^2.0.0",
+						"randomatic": "^3.0.0",
+						"repeat-element": "^1.1.2",
+						"repeat-string": "^1.5.2"
+					}
+				},
+				"is-number": {
+					"version": "2.1.0",
+					"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+					"integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+					"dev": true,
+					"requires": {
+						"kind-of": "^3.0.2"
+					}
+				},
+				"isobject": {
+					"version": "2.1.0",
+					"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+					"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+					"dev": true,
+					"requires": {
+						"isarray": "1.0.0"
+					}
+				},
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"expand-tilde": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+			"integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+			"dev": true,
+			"requires": {
+				"homedir-polyfill": "^1.0.1"
+			}
+		},
+		"extend": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+			"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+			"dev": true
+		},
+		"extend-shallow": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+			"integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+			"dev": true,
+			"requires": {
+				"assign-symbols": "^1.0.0",
+				"is-extendable": "^1.0.1"
+			},
+			"dependencies": {
+				"is-extendable": {
+					"version": "1.0.1",
+					"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+					"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+					"dev": true,
+					"requires": {
+						"is-plain-object": "^2.0.4"
+					}
+				}
+			}
+		},
+		"external-editor": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+			"integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
+			"dev": true,
+			"requires": {
+				"chardet": "^0.4.0",
+				"iconv-lite": "^0.4.17",
+				"tmp": "^0.0.33"
+			}
+		},
+		"extglob": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+			"integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+			"dev": true,
+			"requires": {
+				"array-unique": "^0.3.2",
+				"define-property": "^1.0.0",
+				"expand-brackets": "^2.1.4",
+				"extend-shallow": "^2.0.1",
+				"fragment-cache": "^0.2.1",
+				"regex-not": "^1.0.0",
+				"snapdragon": "^0.8.1",
+				"to-regex": "^3.0.1"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+					"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^1.0.0"
+					}
+				},
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				},
+				"is-accessor-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+					"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-data-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+					"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-descriptor": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+					"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+					"dev": true,
+					"requires": {
+						"is-accessor-descriptor": "^1.0.0",
+						"is-data-descriptor": "^1.0.0",
+						"kind-of": "^6.0.2"
+					}
+				}
+			}
+		},
+		"extract-zip": {
+			"version": "1.6.7",
+			"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
+			"integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"concat-stream": "1.6.2",
+				"debug": "2.6.9",
+				"mkdirp": "0.5.1",
+				"yauzl": "2.4.1"
+			},
+			"dependencies": {
+				"fd-slicer": {
+					"version": "1.0.1",
+					"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+					"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+					"dev": true,
+					"optional": true,
+					"requires": {
+						"pend": "~1.2.0"
+					}
+				},
+				"yauzl": {
+					"version": "2.4.1",
+					"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+					"integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+					"dev": true,
+					"optional": true,
+					"requires": {
+						"fd-slicer": "~1.0.1"
+					}
+				}
+			}
+		},
+		"extsprintf": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+			"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+			"dev": true
+		},
+		"eyes": {
+			"version": "0.1.8",
+			"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
+			"integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=",
+			"dev": true,
+			"optional": true
+		},
+		"fancy-log": {
+			"version": "1.3.2",
+			"resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz",
+			"integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=",
+			"dev": true,
+			"requires": {
+				"ansi-gray": "^0.1.1",
+				"color-support": "^1.1.3",
+				"time-stamp": "^1.0.0"
+			}
+		},
+		"fast-deep-equal": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+			"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+			"dev": true
+		},
+		"fast-json-stable-stringify": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+			"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+			"dev": true
+		},
+		"faye-websocket": {
+			"version": "0.10.0",
+			"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+			"integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+			"dev": true,
+			"requires": {
+				"websocket-driver": ">=0.5.1"
+			}
+		},
+		"fd-slicer": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+			"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
+			"dev": true,
+			"requires": {
+				"pend": "~1.2.0"
+			}
+		},
+		"figures": {
+			"version": "1.7.0",
+			"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+			"integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+			"dev": true,
+			"requires": {
+				"escape-string-regexp": "^1.0.5",
+				"object-assign": "^4.1.0"
+			}
+		},
+		"file-entry-cache": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+			"integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+			"dev": true,
+			"requires": {
+				"flat-cache": "^1.2.1",
+				"object-assign": "^4.0.1"
+			}
+		},
+		"file-sync-cmp": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz",
+			"integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=",
+			"dev": true
+		},
+		"file-type": {
+			"version": "4.4.0",
+			"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz",
+			"integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=",
+			"dev": true
+		},
+		"filename-regex": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+			"integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+			"dev": true
+		},
+		"filename-reserved-regex": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz",
+			"integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=",
+			"dev": true
+		},
+		"filenamify": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz",
+			"integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=",
+			"dev": true,
+			"requires": {
+				"filename-reserved-regex": "^1.0.0",
+				"strip-outer": "^1.0.0",
+				"trim-repeated": "^1.0.0"
+			}
+		},
+		"fill-range": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+			"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+			"dev": true,
+			"requires": {
+				"extend-shallow": "^2.0.1",
+				"is-number": "^3.0.0",
+				"repeat-string": "^1.6.1",
+				"to-regex-range": "^2.1.0"
+			},
+			"dependencies": {
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				}
+			}
+		},
+		"find-up": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+			"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+			"dev": true,
+			"requires": {
+				"path-exists": "^2.0.0",
+				"pinkie-promise": "^2.0.0"
+			}
+		},
+		"find-versions": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-1.2.1.tgz",
+			"integrity": "sha1-y96fEuOFdaCvG+G5osXV/Y8Ya2I=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"array-uniq": "^1.0.0",
+				"get-stdin": "^4.0.1",
+				"meow": "^3.5.0",
+				"semver-regex": "^1.0.0"
+			}
+		},
+		"findup": {
+			"version": "0.1.5",
+			"resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz",
+			"integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=",
+			"dev": true,
+			"requires": {
+				"colors": "~0.6.0-1",
+				"commander": "~2.1.0"
+			},
+			"dependencies": {
+				"colors": {
+					"version": "0.6.2",
+					"resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
+					"integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=",
+					"dev": true
+				},
+				"commander": {
+					"version": "2.1.0",
+					"resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz",
+					"integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=",
+					"dev": true
+				}
+			}
+		},
+		"findup-sync": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+			"integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
+			"dev": true,
+			"requires": {
+				"glob": "~5.0.0"
+			},
+			"dependencies": {
+				"glob": {
+					"version": "5.0.15",
+					"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+					"integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+					"dev": true,
+					"requires": {
+						"inflight": "^1.0.4",
+						"inherits": "2",
+						"minimatch": "2 || 3",
+						"once": "^1.3.0",
+						"path-is-absolute": "^1.0.0"
+					}
+				}
+			}
+		},
+		"first-chunk-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz",
+			"integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=",
+			"dev": true
+		},
+		"flat-cache": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz",
+			"integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=",
+			"dev": true,
+			"requires": {
+				"circular-json": "^0.3.1",
+				"del": "^2.0.2",
+				"graceful-fs": "^4.1.2",
+				"write": "^0.2.1"
+			}
+		},
+		"flatten": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz",
+			"integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
+			"dev": true
+		},
+		"for-in": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+			"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+			"dev": true
+		},
+		"for-own": {
+			"version": "0.1.5",
+			"resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+			"integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+			"dev": true,
+			"requires": {
+				"for-in": "^1.0.1"
+			}
+		},
+		"forever-agent": {
+			"version": "0.6.1",
+			"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+			"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+			"dev": true
+		},
+		"form-data": {
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+			"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+			"dev": true,
+			"requires": {
+				"asynckit": "^0.4.0",
+				"combined-stream": "1.0.6",
+				"mime-types": "^2.1.12"
+			}
+		},
+		"fragment-cache": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+			"integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+			"dev": true,
+			"requires": {
+				"map-cache": "^0.2.2"
+			}
+		},
+		"fs-constants": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+			"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+			"dev": true
+		},
+		"fs-extra": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz",
+			"integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"graceful-fs": "^4.1.2",
+				"jsonfile": "^2.1.0",
+				"klaw": "^1.0.0"
+			}
+		},
+		"fs.realpath": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+			"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+			"dev": true
+		},
+		"fstream": {
+			"version": "1.0.11",
+			"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+			"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.1.2",
+				"inherits": "~2.0.0",
+				"mkdirp": ">=0.5 0",
+				"rimraf": "2"
+			}
+		},
+		"gather-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/gather-stream/-/gather-stream-1.0.0.tgz",
+			"integrity": "sha1-szmUr0V6gRVwDUEPMXczy+egkEs=",
+			"dev": true
+		},
+		"gauge": {
+			"version": "2.7.4",
+			"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+			"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+			"dev": true,
+			"requires": {
+				"aproba": "^1.0.3",
+				"console-control-strings": "^1.0.0",
+				"has-unicode": "^2.0.0",
+				"object-assign": "^4.1.0",
+				"signal-exit": "^3.0.0",
+				"string-width": "^1.0.1",
+				"strip-ansi": "^3.0.1",
+				"wide-align": "^1.1.0"
+			},
+			"dependencies": {
+				"is-fullwidth-code-point": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+					"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+					"dev": true,
+					"requires": {
+						"number-is-nan": "^1.0.0"
+					}
+				},
+				"string-width": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+					"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+					"dev": true,
+					"requires": {
+						"code-point-at": "^1.0.0",
+						"is-fullwidth-code-point": "^1.0.0",
+						"strip-ansi": "^3.0.0"
+					}
+				}
+			}
+		},
+		"gaze": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+			"integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+			"dev": true,
+			"requires": {
+				"globule": "^1.0.0"
+			}
+		},
+		"get-caller-file": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+			"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+			"dev": true
+		},
+		"get-proxy": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-1.1.0.tgz",
+			"integrity": "sha1-iUhUSRvFkbDxR9euVw9cZ4tyVus=",
+			"dev": true,
+			"requires": {
+				"rc": "^1.1.2"
+			}
+		},
+		"get-stdin": {
+			"version": "4.0.1",
+			"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+			"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+			"dev": true
+		},
+		"get-stream": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+			"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+			"dev": true,
+			"optional": true
+		},
+		"get-value": {
+			"version": "2.0.6",
+			"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+			"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+			"dev": true
+		},
+		"getobject": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz",
+			"integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=",
+			"dev": true
+		},
+		"getpass": {
+			"version": "0.1.7",
+			"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+			"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+			"dev": true,
+			"requires": {
+				"assert-plus": "^1.0.0"
+			}
+		},
+		"gettext-parser": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-2.0.0.tgz",
+			"integrity": "sha512-FDs/7XjNw58ToQwJFO7avZZbPecSYgw8PBYhd0An+4JtZSrSzKhEvTsVV2uqdO7VziWTOGSgLGD5YRPdsCjF7Q==",
+			"dev": true,
+			"requires": {
+				"encoding": "^0.1.12",
+				"safe-buffer": "^5.1.2"
+			}
+		},
+		"gifsicle": {
+			"version": "3.0.4",
+			"resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-3.0.4.tgz",
+			"integrity": "sha1-9Fy17RAWW2ZdySng6TKLbIId+js=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"bin-build": "^2.0.0",
+				"bin-wrapper": "^3.0.0",
+				"logalot": "^2.0.0"
+			}
+		},
+		"glob": {
+			"version": "7.0.6",
+			"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
+			"integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
+			"dev": true,
+			"requires": {
+				"fs.realpath": "^1.0.0",
+				"inflight": "^1.0.4",
+				"inherits": "2",
+				"minimatch": "^3.0.2",
+				"once": "^1.3.0",
+				"path-is-absolute": "^1.0.0"
+			}
+		},
+		"glob-base": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+			"integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+			"dev": true,
+			"requires": {
+				"glob-parent": "^2.0.0",
+				"is-glob": "^2.0.0"
+			},
+			"dependencies": {
+				"glob-parent": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+					"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+					"dev": true,
+					"requires": {
+						"is-glob": "^2.0.0"
+					}
+				},
+				"is-extglob": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+					"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+					"dev": true
+				},
+				"is-glob": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+					"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				}
+			}
+		},
+		"glob-parent": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+			"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+			"dev": true,
+			"requires": {
+				"is-glob": "^3.1.0",
+				"path-dirname": "^1.0.0"
+			}
+		},
+		"glob-stream": {
+			"version": "5.3.5",
+			"resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz",
+			"integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=",
+			"dev": true,
+			"requires": {
+				"extend": "^3.0.0",
+				"glob": "^5.0.3",
+				"glob-parent": "^3.0.0",
+				"micromatch": "^2.3.7",
+				"ordered-read-streams": "^0.3.0",
+				"through2": "^0.6.0",
+				"to-absolute-glob": "^0.1.1",
+				"unique-stream": "^2.0.2"
+			},
+			"dependencies": {
+				"arr-diff": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+					"integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+					"dev": true,
+					"requires": {
+						"arr-flatten": "^1.0.1"
+					}
+				},
+				"array-unique": {
+					"version": "0.2.1",
+					"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+					"integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+					"dev": true
+				},
+				"braces": {
+					"version": "1.8.5",
+					"resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+					"integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+					"dev": true,
+					"requires": {
+						"expand-range": "^1.8.1",
+						"preserve": "^0.2.0",
+						"repeat-element": "^1.1.2"
+					}
+				},
+				"expand-brackets": {
+					"version": "0.1.5",
+					"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+					"integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+					"dev": true,
+					"requires": {
+						"is-posix-bracket": "^0.1.0"
+					}
+				},
+				"extglob": {
+					"version": "0.3.2",
+					"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+					"integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				},
+				"glob": {
+					"version": "5.0.15",
+					"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+					"integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+					"dev": true,
+					"requires": {
+						"inflight": "^1.0.4",
+						"inherits": "2",
+						"minimatch": "2 || 3",
+						"once": "^1.3.0",
+						"path-is-absolute": "^1.0.0"
+					}
+				},
+				"is-extglob": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+					"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+					"dev": true
+				},
+				"is-glob": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+					"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				},
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				},
+				"micromatch": {
+					"version": "2.3.11",
+					"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+					"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+					"dev": true,
+					"requires": {
+						"arr-diff": "^2.0.0",
+						"array-unique": "^0.2.1",
+						"braces": "^1.8.2",
+						"expand-brackets": "^0.1.4",
+						"extglob": "^0.3.1",
+						"filename-regex": "^2.0.0",
+						"is-extglob": "^1.0.0",
+						"is-glob": "^2.0.1",
+						"kind-of": "^3.0.2",
+						"normalize-path": "^2.0.1",
+						"object.omit": "^2.0.0",
+						"parse-glob": "^3.0.4",
+						"regex-cache": "^0.4.2"
+					}
+				}
+			}
+		},
+		"global-modules": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+			"integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+			"dev": true,
+			"requires": {
+				"global-prefix": "^1.0.1",
+				"is-windows": "^1.0.1",
+				"resolve-dir": "^1.0.0"
+			}
+		},
+		"global-prefix": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+			"integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+			"dev": true,
+			"requires": {
+				"expand-tilde": "^2.0.2",
+				"homedir-polyfill": "^1.0.1",
+				"ini": "^1.3.4",
+				"is-windows": "^1.0.1",
+				"which": "^1.2.14"
+			}
+		},
+		"globby": {
+			"version": "6.1.0",
+			"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+			"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+			"dev": true,
+			"requires": {
+				"array-union": "^1.0.1",
+				"glob": "^7.0.3",
+				"object-assign": "^4.0.1",
+				"pify": "^2.0.0",
+				"pinkie-promise": "^2.0.0"
+			}
+		},
+		"globjoin": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+			"integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=",
+			"dev": true
+		},
+		"globule": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+			"integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+			"dev": true,
+			"requires": {
+				"glob": "~7.1.1",
+				"lodash": "~4.17.10",
+				"minimatch": "~3.0.2"
+			},
+			"dependencies": {
+				"glob": {
+					"version": "7.1.2",
+					"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+					"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+					"dev": true,
+					"requires": {
+						"fs.realpath": "^1.0.0",
+						"inflight": "^1.0.4",
+						"inherits": "2",
+						"minimatch": "^3.0.4",
+						"once": "^1.3.0",
+						"path-is-absolute": "^1.0.0"
+					}
+				}
+			}
+		},
+		"glogg": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz",
+			"integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==",
+			"dev": true,
+			"requires": {
+				"sparkles": "^1.0.0"
+			}
+		},
+		"got": {
+			"version": "5.7.1",
+			"resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz",
+			"integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=",
+			"dev": true,
+			"requires": {
+				"create-error-class": "^3.0.1",
+				"duplexer2": "^0.1.4",
+				"is-redirect": "^1.0.0",
+				"is-retry-allowed": "^1.0.0",
+				"is-stream": "^1.0.0",
+				"lowercase-keys": "^1.0.0",
+				"node-status-codes": "^1.0.0",
+				"object-assign": "^4.0.1",
+				"parse-json": "^2.1.0",
+				"pinkie-promise": "^2.0.0",
+				"read-all-stream": "^3.0.0",
+				"readable-stream": "^2.0.5",
+				"timed-out": "^3.0.0",
+				"unzip-response": "^1.0.2",
+				"url-parse-lax": "^1.0.0"
+			}
+		},
+		"graceful-fs": {
+			"version": "4.1.11",
+			"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+			"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+			"dev": true
+		},
+		"graceful-readlink": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
+			"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
+			"dev": true
+		},
+		"grunt": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.3.tgz",
+			"integrity": "sha512-/JzmZNPfKorlCrrmxWqQO4JVodO+DVd5XX4DkocL/1WlLlKVLE9+SdEIempOAxDhWPysLle6afvn/hg7Ck2k9g==",
+			"dev": true,
+			"requires": {
+				"coffeescript": "~1.10.0",
+				"dateformat": "~1.0.12",
+				"eventemitter2": "~0.4.13",
+				"exit": "~0.1.1",
+				"findup-sync": "~0.3.0",
+				"glob": "~7.0.0",
+				"grunt-cli": "~1.2.0",
+				"grunt-known-options": "~1.1.0",
+				"grunt-legacy-log": "~2.0.0",
+				"grunt-legacy-util": "~1.1.1",
+				"iconv-lite": "~0.4.13",
+				"js-yaml": "~3.5.2",
+				"minimatch": "~3.0.2",
+				"mkdirp": "~0.5.1",
+				"nopt": "~3.0.6",
+				"path-is-absolute": "~1.0.0",
+				"rimraf": "~2.6.2"
+			},
+			"dependencies": {
+				"grunt-cli": {
+					"version": "1.2.0",
+					"resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz",
+					"integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=",
+					"dev": true,
+					"requires": {
+						"findup-sync": "~0.3.0",
+						"grunt-known-options": "~1.1.0",
+						"nopt": "~3.0.6",
+						"resolve": "~1.1.0"
+					}
+				}
+			}
+		},
+		"grunt-check-dependencies": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/grunt-check-dependencies/-/grunt-check-dependencies-1.0.0.tgz",
+			"integrity": "sha1-UYiVh8V+gn3enN9pt1CuCy+IHFA=",
+			"dev": true,
+			"requires": {
+				"check-dependencies": "^1.0.1",
+				"lodash.clonedeep": "^4.5.0"
+			}
+		},
+		"grunt-checktextdomain": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/grunt-checktextdomain/-/grunt-checktextdomain-1.0.1.tgz",
+			"integrity": "sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=",
+			"dev": true,
+			"requires": {
+				"chalk": "~0.2.1",
+				"text-table": "~0.2.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "0.2.0",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz",
+					"integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "0.2.1",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-0.2.1.tgz",
+					"integrity": "sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "~0.2.0",
+						"has-color": "~0.1.0"
+					}
+				}
+			}
+		},
+		"grunt-contrib-clean": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.1.0.tgz",
+			"integrity": "sha1-Vkq/LQN4qYOhW54/MO51tzjEBjg=",
+			"dev": true,
+			"requires": {
+				"async": "^1.5.2",
+				"rimraf": "^2.5.1"
+			}
+		},
+		"grunt-contrib-copy": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz",
+			"integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.1.1",
+				"file-sync-cmp": "^0.1.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-contrib-cssmin": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-2.2.1.tgz",
+			"integrity": "sha512-IXNomhQ5ekVZbDbj/ik5YccoD9khU6LT2fDXqO1+/Txjq8cp0tQKjVS8i8EAbHOrSDkL7/UD6A7b+xj98gqh9w==",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"clean-css": "~4.1.1",
+				"maxmin": "^2.1.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-contrib-imagemin": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-imagemin/-/grunt-contrib-imagemin-2.0.1.tgz",
+			"integrity": "sha512-91zBrvh350QSpsxyCTXni0djMXavF3elBmvFgnbp/2CgIx53QYe+Cvf2+wZmrcb8U0qp+MjHl0Ahjct4+R6PLQ==",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"imagemin": "^5.3.1",
+				"imagemin-gifsicle": "^5.0.0",
+				"imagemin-jpegtran": "^5.0.0",
+				"imagemin-optipng": "^5.1.0",
+				"imagemin-svgo": "^5.1.0",
+				"p-map": "^1.1.1",
+				"plur": "^2.1.2",
+				"pretty-bytes": "^4.0.2"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"pretty-bytes": {
+					"version": "4.0.2",
+					"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
+					"integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-contrib-jshint": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz",
+			"integrity": "sha1-Np2QmyWTxA6L55lAshNAhQx5Oaw=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.1.1",
+				"hooker": "^0.2.3",
+				"jshint": "~2.9.4"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-contrib-uglify": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-3.0.1.tgz",
+			"integrity": "sha1-/etfk4pMgEL46Grkb2NVTo6VEcs=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"maxmin": "^1.1.0",
+				"uglify-js": "~3.0.4",
+				"uri-path": "^1.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"gzip-size": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-1.0.0.tgz",
+					"integrity": "sha1-Zs+LEBBHInuVus5uodoMF37Vwi8=",
+					"dev": true,
+					"requires": {
+						"browserify-zlib": "^0.1.4",
+						"concat-stream": "^1.4.1"
+					}
+				},
+				"maxmin": {
+					"version": "1.1.0",
+					"resolved": "https://registry.npmjs.org/maxmin/-/maxmin-1.1.0.tgz",
+					"integrity": "sha1-cTZehKmd2Piz99X94vANHn9zvmE=",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.0.0",
+						"figures": "^1.0.1",
+						"gzip-size": "^1.0.0",
+						"pretty-bytes": "^1.0.0"
+					}
+				},
+				"pretty-bytes": {
+					"version": "1.0.4",
+					"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
+					"integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=",
+					"dev": true,
+					"requires": {
+						"get-stdin": "^4.0.1",
+						"meow": "^3.1.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-contrib-watch": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz",
+			"integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==",
+			"dev": true,
+			"requires": {
+				"async": "^2.6.0",
+				"gaze": "^1.1.0",
+				"lodash": "^4.17.10",
+				"tiny-lr": "^1.1.1"
+			},
+			"dependencies": {
+				"async": {
+					"version": "2.6.1",
+					"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+					"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+					"dev": true,
+					"requires": {
+						"lodash": "^4.17.10"
+					}
+				}
+			}
+		},
+		"grunt-exec": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/grunt-exec/-/grunt-exec-2.0.0.tgz",
+			"integrity": "sha1-pXWmILHaRBbCksAd9WTJKWuAqyM=",
+			"dev": true
+		},
+		"grunt-jsvalidate": {
+			"version": "0.2.2",
+			"resolved": "https://registry.npmjs.org/grunt-jsvalidate/-/grunt-jsvalidate-0.2.2.tgz",
+			"integrity": "sha1-/QlEJYiNbmPfqgbPsJ7gUrjrvo8=",
+			"dev": true,
+			"requires": {
+				"esprima": "~1.0.0"
+			},
+			"dependencies": {
+				"esprima": {
+					"version": "1.0.4",
+					"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz",
+					"integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-known-options": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz",
+			"integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==",
+			"dev": true
+		},
+		"grunt-legacy-log": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz",
+			"integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==",
+			"dev": true,
+			"requires": {
+				"colors": "~1.1.2",
+				"grunt-legacy-log-utils": "~2.0.0",
+				"hooker": "~0.2.3",
+				"lodash": "~4.17.5"
+			}
+		},
+		"grunt-legacy-log-utils": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz",
+			"integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==",
+			"dev": true,
+			"requires": {
+				"chalk": "~2.4.1",
+				"lodash": "~4.17.10"
+			}
+		},
+		"grunt-legacy-util": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz",
+			"integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==",
+			"dev": true,
+			"requires": {
+				"async": "~1.5.2",
+				"exit": "~0.1.1",
+				"getobject": "~0.1.0",
+				"hooker": "~0.2.3",
+				"lodash": "~4.17.10",
+				"underscore.string": "~3.3.4",
+				"which": "~1.3.0"
+			}
+		},
+		"grunt-patch-wordpress": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/grunt-patch-wordpress/-/grunt-patch-wordpress-1.0.0.tgz",
+			"integrity": "sha512-2wvtOqxMhiu7QCujEAvFFvLfUYec0fKDvFRnwKUSHizl5eH8cgXv6t0yc7W4yEeito/u8v8r8fzDHRhXLkIXzA==",
+			"dev": true,
+			"requires": {
+				"grunt": ">=0.4.5",
+				"inquirer": "^5.1.0",
+				"request": "^2.83.0",
+				"underscore": "~1.8.3",
+				"underscore.string": "~3.3.4",
+				"xmlrpc": "^1.3.1"
+			}
+		},
+		"grunt-phplint": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/grunt-phplint/-/grunt-phplint-0.1.0.tgz",
+			"integrity": "sha1-bb4uauxTqiKc+sCtmnyZ4kGEhI0=",
+			"dev": true,
+			"requires": {
+				"cache-swap": "~0.3.0",
+				"grunt": "~0.4.1"
+			},
+			"dependencies": {
+				"argparse": {
+					"version": "0.1.16",
+					"resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz",
+					"integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=",
+					"dev": true,
+					"requires": {
+						"underscore": "~1.7.0",
+						"underscore.string": "~2.4.0"
+					},
+					"dependencies": {
+						"underscore.string": {
+							"version": "2.4.0",
+							"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz",
+							"integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=",
+							"dev": true
+						}
+					}
+				},
+				"async": {
+					"version": "0.1.22",
+					"resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz",
+					"integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=",
+					"dev": true
+				},
+				"coffee-script": {
+					"version": "1.3.3",
+					"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz",
+					"integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=",
+					"dev": true
+				},
+				"colors": {
+					"version": "0.6.2",
+					"resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
+					"integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=",
+					"dev": true
+				},
+				"dateformat": {
+					"version": "1.0.2-1.2.3",
+					"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz",
+					"integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=",
+					"dev": true
+				},
+				"esprima": {
+					"version": "1.0.4",
+					"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz",
+					"integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=",
+					"dev": true
+				},
+				"findup-sync": {
+					"version": "0.1.3",
+					"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
+					"integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=",
+					"dev": true,
+					"requires": {
+						"glob": "~3.2.9",
+						"lodash": "~2.4.1"
+					},
+					"dependencies": {
+						"glob": {
+							"version": "3.2.11",
+							"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
+							"integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=",
+							"dev": true,
+							"requires": {
+								"inherits": "2",
+								"minimatch": "0.3"
+							}
+						},
+						"lodash": {
+							"version": "2.4.2",
+							"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz",
+							"integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=",
+							"dev": true
+						},
+						"minimatch": {
+							"version": "0.3.0",
+							"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
+							"integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=",
+							"dev": true,
+							"requires": {
+								"lru-cache": "2",
+								"sigmund": "~1.0.0"
+							}
+						}
+					}
+				},
+				"glob": {
+					"version": "3.1.21",
+					"resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
+					"integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=",
+					"dev": true,
+					"requires": {
+						"graceful-fs": "~1.2.0",
+						"inherits": "1",
+						"minimatch": "~0.2.11"
+					},
+					"dependencies": {
+						"inherits": {
+							"version": "1.0.2",
+							"resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz",
+							"integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=",
+							"dev": true
+						}
+					}
+				},
+				"graceful-fs": {
+					"version": "1.2.3",
+					"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz",
+					"integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=",
+					"dev": true
+				},
+				"grunt": {
+					"version": "0.4.5",
+					"resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz",
+					"integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=",
+					"dev": true,
+					"requires": {
+						"async": "~0.1.22",
+						"coffee-script": "~1.3.3",
+						"colors": "~0.6.2",
+						"dateformat": "1.0.2-1.2.3",
+						"eventemitter2": "~0.4.13",
+						"exit": "~0.1.1",
+						"findup-sync": "~0.1.2",
+						"getobject": "~0.1.0",
+						"glob": "~3.1.21",
+						"grunt-legacy-log": "~0.1.0",
+						"grunt-legacy-util": "~0.2.0",
+						"hooker": "~0.2.3",
+						"iconv-lite": "~0.2.11",
+						"js-yaml": "~2.0.5",
+						"lodash": "~0.9.2",
+						"minimatch": "~0.2.12",
+						"nopt": "~1.0.10",
+						"rimraf": "~2.2.8",
+						"underscore.string": "~2.2.1",
+						"which": "~1.0.5"
+					}
+				},
+				"grunt-legacy-log": {
+					"version": "0.1.3",
+					"resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz",
+					"integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=",
+					"dev": true,
+					"requires": {
+						"colors": "~0.6.2",
+						"grunt-legacy-log-utils": "~0.1.1",
+						"hooker": "~0.2.3",
+						"lodash": "~2.4.1",
+						"underscore.string": "~2.3.3"
+					},
+					"dependencies": {
+						"lodash": {
+							"version": "2.4.2",
+							"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz",
+							"integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=",
+							"dev": true
+						},
+						"underscore.string": {
+							"version": "2.3.3",
+							"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz",
+							"integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=",
+							"dev": true
+						}
+					}
+				},
+				"grunt-legacy-log-utils": {
+					"version": "0.1.1",
+					"resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz",
+					"integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=",
+					"dev": true,
+					"requires": {
+						"colors": "~0.6.2",
+						"lodash": "~2.4.1",
+						"underscore.string": "~2.3.3"
+					},
+					"dependencies": {
+						"lodash": {
+							"version": "2.4.2",
+							"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz",
+							"integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=",
+							"dev": true
+						},
+						"underscore.string": {
+							"version": "2.3.3",
+							"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz",
+							"integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=",
+							"dev": true
+						}
+					}
+				},
+				"grunt-legacy-util": {
+					"version": "0.2.0",
+					"resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz",
+					"integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=",
+					"dev": true,
+					"requires": {
+						"async": "~0.1.22",
+						"exit": "~0.1.1",
+						"getobject": "~0.1.0",
+						"hooker": "~0.2.3",
+						"lodash": "~0.9.2",
+						"underscore.string": "~2.2.1",
+						"which": "~1.0.5"
+					}
+				},
+				"iconv-lite": {
+					"version": "0.2.11",
+					"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz",
+					"integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=",
+					"dev": true
+				},
+				"js-yaml": {
+					"version": "2.0.5",
+					"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz",
+					"integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=",
+					"dev": true,
+					"requires": {
+						"argparse": "~ 0.1.11",
+						"esprima": "~ 1.0.2"
+					}
+				},
+				"lodash": {
+					"version": "0.9.2",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz",
+					"integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=",
+					"dev": true
+				},
+				"lru-cache": {
+					"version": "2.7.3",
+					"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
+					"integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=",
+					"dev": true
+				},
+				"minimatch": {
+					"version": "0.2.14",
+					"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+					"integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
+					"dev": true,
+					"requires": {
+						"lru-cache": "2",
+						"sigmund": "~1.0.0"
+					}
+				},
+				"nopt": {
+					"version": "1.0.10",
+					"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+					"integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+					"dev": true,
+					"requires": {
+						"abbrev": "1"
+					}
+				},
+				"rimraf": {
+					"version": "2.2.8",
+					"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
+					"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
+					"dev": true
+				},
+				"underscore": {
+					"version": "1.7.0",
+					"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
+					"integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=",
+					"dev": true
+				},
+				"underscore.string": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz",
+					"integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=",
+					"dev": true
+				},
+				"which": {
+					"version": "1.0.9",
+					"resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz",
+					"integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-postcss": {
+			"version": "0.9.0",
+			"resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.9.0.tgz",
+			"integrity": "sha512-lglLcVaoOIqH0sFv7RqwUKkEFGQwnlqyAKbatxZderwZGV1nDyKHN7gZS9LUiTx1t5GOvRBx0BEalHMyVwFAIA==",
+			"dev": true,
+			"requires": {
+				"chalk": "^2.1.0",
+				"diff": "^3.0.0",
+				"postcss": "^6.0.11"
+			}
+		},
+		"grunt-rtlcss": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/grunt-rtlcss/-/grunt-rtlcss-2.0.1.tgz",
+			"integrity": "sha1-6eYc5DdAY5f546Sxv2aeR+cf/MM=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"rtlcss": "^2.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-sass": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-2.0.0.tgz",
+			"integrity": "sha1-kHTPnXtFkuIPd4jKpye4+aoGtgo=",
+			"dev": true,
+			"requires": {
+				"each-async": "^1.0.0",
+				"node-sass": "^4.0.0",
+				"object-assign": "^4.0.1"
+			}
+		},
+		"grunt-stylelint": {
+			"version": "0.8.0",
+			"resolved": "https://registry.npmjs.org/grunt-stylelint/-/grunt-stylelint-0.8.0.tgz",
+			"integrity": "sha1-cAtdw9X3yY/2MvZXYBWqjFZnzlI=",
+			"dev": true,
+			"requires": {
+				"chalk": "1.1.3"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"grunt-wp-i18n": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/grunt-wp-i18n/-/grunt-wp-i18n-1.0.2.tgz",
+			"integrity": "sha512-s7DdR1wRYWvgeQOELL/s8AmWz0scVUq+h0yYVWvLx23kTXOBslvaD35NTvubyM+MO+k29bgND0gDDvS6sHixfg==",
+			"dev": true,
+			"requires": {
+				"grunt": "^1.0.2",
+				"node-wp-i18n": "^1.0.5"
+			}
+		},
+		"gulp-decompress": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/gulp-decompress/-/gulp-decompress-1.2.0.tgz",
+			"integrity": "sha1-jutlpeAV+O2FMsr+KEVJYGJvDcc=",
+			"dev": true,
+			"requires": {
+				"archive-type": "^3.0.0",
+				"decompress": "^3.0.0",
+				"gulp-util": "^3.0.1",
+				"readable-stream": "^2.0.2"
+			}
+		},
+		"gulp-rename": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz",
+			"integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==",
+			"dev": true
+		},
+		"gulp-sourcemaps": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz",
+			"integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=",
+			"dev": true,
+			"requires": {
+				"convert-source-map": "^1.1.1",
+				"graceful-fs": "^4.1.2",
+				"strip-bom": "^2.0.0",
+				"through2": "^2.0.0",
+				"vinyl": "^1.0.0"
+			},
+			"dependencies": {
+				"through2": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+					"integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "^2.1.5",
+						"xtend": "~4.0.1"
+					}
+				}
+			}
+		},
+		"gulp-util": {
+			"version": "3.0.8",
+			"resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz",
+			"integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=",
+			"dev": true,
+			"requires": {
+				"array-differ": "^1.0.0",
+				"array-uniq": "^1.0.2",
+				"beeper": "^1.0.0",
+				"chalk": "^1.0.0",
+				"dateformat": "^2.0.0",
+				"fancy-log": "^1.1.0",
+				"gulplog": "^1.0.0",
+				"has-gulplog": "^0.1.0",
+				"lodash._reescape": "^3.0.0",
+				"lodash._reevaluate": "^3.0.0",
+				"lodash._reinterpolate": "^3.0.0",
+				"lodash.template": "^3.0.0",
+				"minimist": "^1.1.0",
+				"multipipe": "^0.1.2",
+				"object-assign": "^3.0.0",
+				"replace-ext": "0.0.1",
+				"through2": "^2.0.0",
+				"vinyl": "^0.5.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"dateformat": {
+					"version": "2.2.0",
+					"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
+					"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
+					"dev": true
+				},
+				"object-assign": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+					"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=",
+					"dev": true
+				},
+				"replace-ext": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz",
+					"integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				},
+				"through2": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+					"integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "^2.1.5",
+						"xtend": "~4.0.1"
+					}
+				},
+				"vinyl": {
+					"version": "0.5.3",
+					"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
+					"integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=",
+					"dev": true,
+					"requires": {
+						"clone": "^1.0.0",
+						"clone-stats": "^0.0.1",
+						"replace-ext": "0.0.1"
+					}
+				}
+			}
+		},
+		"gulplog": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz",
+			"integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=",
+			"dev": true,
+			"requires": {
+				"glogg": "^1.0.0"
+			}
+		},
+		"gzip-size": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz",
+			"integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=",
+			"dev": true,
+			"requires": {
+				"duplexer": "^0.1.1"
+			}
+		},
+		"har-schema": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+			"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+			"dev": true
+		},
+		"har-validator": {
+			"version": "5.1.0",
+			"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
+			"integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
+			"dev": true,
+			"requires": {
+				"ajv": "^5.3.0",
+				"har-schema": "^2.0.0"
+			}
+		},
+		"has-ansi": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+			"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+			"dev": true,
+			"requires": {
+				"ansi-regex": "^2.0.0"
+			}
+		},
+		"has-color": {
+			"version": "0.1.7",
+			"resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz",
+			"integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=",
+			"dev": true
+		},
+		"has-flag": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+			"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+			"dev": true
+		},
+		"has-gulplog": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz",
+			"integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=",
+			"dev": true,
+			"requires": {
+				"sparkles": "^1.0.0"
+			}
+		},
+		"has-unicode": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+			"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+			"dev": true
+		},
+		"has-value": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+			"integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+			"dev": true,
+			"requires": {
+				"get-value": "^2.0.6",
+				"has-values": "^1.0.0",
+				"isobject": "^3.0.0"
+			}
+		},
+		"has-values": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+			"integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+			"dev": true,
+			"requires": {
+				"is-number": "^3.0.0",
+				"kind-of": "^4.0.0"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "4.0.0",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+					"integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"hasha": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz",
+			"integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"is-stream": "^1.0.1",
+				"pinkie-promise": "^2.0.0"
+			}
+		},
+		"homedir-polyfill": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
+			"integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
+			"dev": true,
+			"requires": {
+				"parse-passwd": "^1.0.0"
+			}
+		},
+		"hooker": {
+			"version": "0.2.3",
+			"resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+			"integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=",
+			"dev": true
+		},
+		"hosted-git-info": {
+			"version": "2.7.1",
+			"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+			"integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+			"dev": true
+		},
+		"html-comment-regex": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz",
+			"integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=",
+			"dev": true,
+			"optional": true
+		},
+		"html-tags": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-1.2.0.tgz",
+			"integrity": "sha1-x43mW1Zjqll5id0rerSSANfk25g=",
+			"dev": true
+		},
+		"htmlparser2": {
+			"version": "3.8.3",
+			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
+			"integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=",
+			"dev": true,
+			"requires": {
+				"domelementtype": "1",
+				"domhandler": "2.3",
+				"domutils": "1.5",
+				"entities": "1.0",
+				"readable-stream": "1.1"
+			},
+			"dependencies": {
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+					"dev": true
+				},
+				"readable-stream": {
+					"version": "1.1.14",
+					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+					"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+					"dev": true,
+					"requires": {
+						"core-util-is": "~1.0.0",
+						"inherits": "~2.0.1",
+						"isarray": "0.0.1",
+						"string_decoder": "~0.10.x"
+					}
+				},
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				}
+			}
+		},
+		"http-parser-js": {
+			"version": "0.4.13",
+			"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz",
+			"integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=",
+			"dev": true
+		},
+		"http-signature": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+			"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+			"dev": true,
+			"requires": {
+				"assert-plus": "^1.0.0",
+				"jsprim": "^1.2.2",
+				"sshpk": "^1.7.0"
+			}
+		},
+		"iconv-lite": {
+			"version": "0.4.24",
+			"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+			"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+			"dev": true,
+			"requires": {
+				"safer-buffer": ">= 2.1.2 < 3"
+			}
+		},
+		"ignore": {
+			"version": "3.3.10",
+			"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+			"integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+			"dev": true
+		},
+		"imagemin": {
+			"version": "5.3.1",
+			"resolved": "https://registry.npmjs.org/imagemin/-/imagemin-5.3.1.tgz",
+			"integrity": "sha1-8Zwu7h5xumxlWMUV+fyWaAGJptQ=",
+			"dev": true,
+			"requires": {
+				"file-type": "^4.1.0",
+				"globby": "^6.1.0",
+				"make-dir": "^1.0.0",
+				"p-pipe": "^1.1.0",
+				"pify": "^2.3.0",
+				"replace-ext": "^1.0.0"
+			}
+		},
+		"imagemin-gifsicle": {
+			"version": "5.2.0",
+			"resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-5.2.0.tgz",
+			"integrity": "sha512-K01m5QuPK+0en8oVhiOOAicF7KjrHlCZxS++mfLI2mV/Ksfq/Y9nCXCWDz6jRv13wwlqe5T7hXT+ji2DnLc2yQ==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"exec-buffer": "^3.0.0",
+				"gifsicle": "^3.0.0",
+				"is-gif": "^1.0.0"
+			}
+		},
+		"imagemin-jpegtran": {
+			"version": "5.0.2",
+			"resolved": "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-5.0.2.tgz",
+			"integrity": "sha1-5ogiY7j3kW/duABkDPddLpcNKtY=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"exec-buffer": "^3.0.0",
+				"is-jpg": "^1.0.0",
+				"jpegtran-bin": "^3.0.0"
+			}
+		},
+		"imagemin-optipng": {
+			"version": "5.2.1",
+			"resolved": "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-5.2.1.tgz",
+			"integrity": "sha1-0i2kEsCfX/AKQzmWC5ioix2+hpU=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"exec-buffer": "^3.0.0",
+				"is-png": "^1.0.0",
+				"optipng-bin": "^3.0.0"
+			}
+		},
+		"imagemin-svgo": {
+			"version": "5.2.4",
+			"resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-5.2.4.tgz",
+			"integrity": "sha512-1bNZdlWVKdfxzu0xDD1pWjwK/G8FLcztUh/GWaI7xLgCFrn0j35o+uBbY7VcdY2AmKgiLYTXhrzrbkQk6xj8aA==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"is-svg": "^2.0.0",
+				"svgo": "^0.7.0"
+			}
+		},
+		"imurmurhash": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+			"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+			"dev": true
+		},
+		"in-publish": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+			"integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+			"dev": true
+		},
+		"indent-string": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+			"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+			"dev": true,
+			"requires": {
+				"repeating": "^2.0.0"
+			}
+		},
+		"indexes-of": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+			"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+			"dev": true
+		},
+		"inflight": {
+			"version": "1.0.6",
+			"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+			"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+			"dev": true,
+			"requires": {
+				"once": "^1.3.0",
+				"wrappy": "1"
+			}
+		},
+		"inherits": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+			"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+			"dev": true
+		},
+		"ini": {
+			"version": "1.3.5",
+			"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+			"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+			"dev": true
+		},
+		"inquirer": {
+			"version": "5.2.0",
+			"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz",
+			"integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==",
+			"dev": true,
+			"requires": {
+				"ansi-escapes": "^3.0.0",
+				"chalk": "^2.0.0",
+				"cli-cursor": "^2.1.0",
+				"cli-width": "^2.0.0",
+				"external-editor": "^2.1.0",
+				"figures": "^2.0.0",
+				"lodash": "^4.3.0",
+				"mute-stream": "0.0.7",
+				"run-async": "^2.2.0",
+				"rxjs": "^5.5.2",
+				"string-width": "^2.1.0",
+				"strip-ansi": "^4.0.0",
+				"through": "^2.3.6"
+			},
+			"dependencies": {
+				"ansi-regex": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+					"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+					"dev": true
+				},
+				"figures": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+					"integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+					"dev": true,
+					"requires": {
+						"escape-string-regexp": "^1.0.5"
+					}
+				},
+				"strip-ansi": {
+					"version": "4.0.0",
+					"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+					"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+					"dev": true,
+					"requires": {
+						"ansi-regex": "^3.0.0"
+					}
+				}
+			}
+		},
+		"invert-kv": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+			"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+			"dev": true
+		},
+		"ip-regex": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz",
+			"integrity": "sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=",
+			"dev": true,
+			"optional": true
+		},
+		"irregular-plurals": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz",
+			"integrity": "sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y=",
+			"dev": true
+		},
+		"is-absolute": {
+			"version": "0.1.7",
+			"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz",
+			"integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=",
+			"dev": true,
+			"requires": {
+				"is-relative": "^0.1.0"
+			}
+		},
+		"is-accessor-descriptor": {
+			"version": "0.1.6",
+			"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+			"integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+			"dev": true,
+			"requires": {
+				"kind-of": "^3.0.2"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"is-arrayish": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+			"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+			"dev": true
+		},
+		"is-buffer": {
+			"version": "1.1.6",
+			"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+			"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+			"dev": true
+		},
+		"is-builtin-module": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+			"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+			"dev": true,
+			"requires": {
+				"builtin-modules": "^1.0.0"
+			}
+		},
+		"is-bzip2": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-bzip2/-/is-bzip2-1.0.0.tgz",
+			"integrity": "sha1-XuWOqlounIDiFAe+3yOuWsCRs/w=",
+			"dev": true
+		},
+		"is-data-descriptor": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+			"integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+			"dev": true,
+			"requires": {
+				"kind-of": "^3.0.2"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"is-descriptor": {
+			"version": "0.1.6",
+			"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+			"integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+			"dev": true,
+			"requires": {
+				"is-accessor-descriptor": "^0.1.6",
+				"is-data-descriptor": "^0.1.4",
+				"kind-of": "^5.0.0"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "5.1.0",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+					"integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+					"dev": true
+				}
+			}
+		},
+		"is-directory": {
+			"version": "0.3.1",
+			"resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+			"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+			"dev": true
+		},
+		"is-dotfile": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+			"integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+			"dev": true
+		},
+		"is-equal-shallow": {
+			"version": "0.1.3",
+			"resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+			"integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+			"dev": true,
+			"requires": {
+				"is-primitive": "^2.0.0"
+			}
+		},
+		"is-extendable": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+			"integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+			"dev": true
+		},
+		"is-extglob": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+			"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+			"dev": true
+		},
+		"is-finite": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+			"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+			"dev": true,
+			"requires": {
+				"number-is-nan": "^1.0.0"
+			}
+		},
+		"is-fullwidth-code-point": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+			"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+			"dev": true
+		},
+		"is-gif": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-gif/-/is-gif-1.0.0.tgz",
+			"integrity": "sha1-ptKumIkwB7/6l6HYwB1jIFgyCX4=",
+			"dev": true,
+			"optional": true
+		},
+		"is-glob": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+			"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+			"dev": true,
+			"requires": {
+				"is-extglob": "^2.1.0"
+			}
+		},
+		"is-gzip": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz",
+			"integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=",
+			"dev": true
+		},
+		"is-jpg": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-1.0.1.tgz",
+			"integrity": "sha1-KW1X/dmc4BBDSnKD40armhA16XU=",
+			"dev": true,
+			"optional": true
+		},
+		"is-natural-number": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-2.1.1.tgz",
+			"integrity": "sha1-fUxXKDd+84bD4ZSpkRv1fG3DNec=",
+			"dev": true
+		},
+		"is-number": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+			"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+			"dev": true,
+			"requires": {
+				"kind-of": "^3.0.2"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"is-obj": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+			"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
+			"dev": true
+		},
+		"is-path-cwd": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+			"integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+			"dev": true
+		},
+		"is-path-in-cwd": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+			"integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+			"dev": true,
+			"requires": {
+				"is-path-inside": "^1.0.0"
+			}
+		},
+		"is-path-inside": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+			"integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+			"dev": true,
+			"requires": {
+				"path-is-inside": "^1.0.1"
+			}
+		},
+		"is-plain-object": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+			"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+			"dev": true,
+			"requires": {
+				"isobject": "^3.0.1"
+			}
+		},
+		"is-png": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz",
+			"integrity": "sha1-1XSxK/J1wDUEVVcLDltXqwYgd84=",
+			"dev": true,
+			"optional": true
+		},
+		"is-posix-bracket": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+			"integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+			"dev": true
+		},
+		"is-primitive": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+			"integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+			"dev": true
+		},
+		"is-promise": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+			"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+			"dev": true
+		},
+		"is-redirect": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
+			"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
+			"dev": true
+		},
+		"is-regexp": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+			"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
+			"dev": true
+		},
+		"is-relative": {
+			"version": "0.1.3",
+			"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz",
+			"integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=",
+			"dev": true
+		},
+		"is-retry-allowed": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
+			"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=",
+			"dev": true
+		},
+		"is-stream": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+			"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+			"dev": true
+		},
+		"is-supported-regexp-flag": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz",
+			"integrity": "sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==",
+			"dev": true
+		},
+		"is-svg": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz",
+			"integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"html-comment-regex": "^1.1.0"
+			}
+		},
+		"is-tar": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz",
+			"integrity": "sha1-L2suF5LB9bs2UZrKqdZcDSb+hT0=",
+			"dev": true
+		},
+		"is-typedarray": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+			"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+			"dev": true
+		},
+		"is-url": {
+			"version": "1.2.4",
+			"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+			"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+			"dev": true
+		},
+		"is-utf8": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+			"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+			"dev": true
+		},
+		"is-valid-glob": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz",
+			"integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=",
+			"dev": true
+		},
+		"is-windows": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+			"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+			"dev": true
+		},
+		"is-zip": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-zip/-/is-zip-1.0.0.tgz",
+			"integrity": "sha1-R7Co/004p2QxzP2ZqOFaTIa6IyU=",
+			"dev": true
+		},
+		"isarray": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+			"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+			"dev": true
+		},
+		"isexe": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+			"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+			"dev": true
+		},
+		"isobject": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+			"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+			"dev": true
+		},
+		"isstream": {
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+			"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+			"dev": true
+		},
+		"jpegtran-bin": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-3.2.0.tgz",
+			"integrity": "sha1-9g7PSumZwL2tLp+83ytvCYHnops=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"bin-build": "^2.0.0",
+				"bin-wrapper": "^3.0.0",
+				"logalot": "^2.0.0"
+			}
+		},
+		"js-base64": {
+			"version": "2.4.8",
+			"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.8.tgz",
+			"integrity": "sha512-hm2nYpDrwoO/OzBhdcqs/XGT6XjSuSSCVEpia+Kl2J6x4CYt5hISlVL/AYU1khoDXv0AQVgxtdJySb9gjAn56Q==",
+			"dev": true
+		},
+		"js-yaml": {
+			"version": "3.5.5",
+			"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz",
+			"integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=",
+			"dev": true,
+			"requires": {
+				"argparse": "^1.0.2",
+				"esprima": "^2.6.0"
+			}
+		},
+		"jsbn": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+			"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+			"dev": true,
+			"optional": true
+		},
+		"jshint": {
+			"version": "2.9.6",
+			"resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.6.tgz",
+			"integrity": "sha512-KO9SIAKTlJQOM4lE64GQUtGBRpTOuvbrRrSZw3AhUxMNG266nX9hK2cKA4SBhXOj0irJGyNyGSLT62HGOVDEOA==",
+			"dev": true,
+			"requires": {
+				"cli": "~1.0.0",
+				"console-browserify": "1.1.x",
+				"exit": "0.1.x",
+				"htmlparser2": "3.8.x",
+				"lodash": "~4.17.10",
+				"minimatch": "~3.0.2",
+				"phantom": "~4.0.1",
+				"phantomjs-prebuilt": "~2.1.7",
+				"shelljs": "0.3.x",
+				"strip-json-comments": "1.0.x",
+				"unicode-5.2.0": "^0.7.5"
+			},
+			"dependencies": {
+				"strip-json-comments": {
+					"version": "1.0.4",
+					"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
+					"integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=",
+					"dev": true
+				}
+			}
+		},
+		"json-schema": {
+			"version": "0.2.3",
+			"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+			"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+			"dev": true
+		},
+		"json-schema-traverse": {
+			"version": "0.3.1",
+			"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+			"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+			"dev": true
+		},
+		"json-stable-stringify": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
+			"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+			"dev": true,
+			"requires": {
+				"jsonify": "~0.0.0"
+			}
+		},
+		"json-stringify-safe": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+			"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+			"dev": true
+		},
+		"jsonfile": {
+			"version": "2.4.0",
+			"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+			"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"graceful-fs": "^4.1.6"
+			}
+		},
+		"jsonfilter": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/jsonfilter/-/jsonfilter-1.1.2.tgz",
+			"integrity": "sha1-Ie987cdRk4E8dZMulqmL4gW6WhE=",
+			"dev": true,
+			"requires": {
+				"JSONStream": "^0.8.4",
+				"minimist": "^1.1.0",
+				"stream-combiner": "^0.2.1",
+				"through2": "^0.6.3"
+			}
+		},
+		"jsonify": {
+			"version": "0.0.0",
+			"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+			"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
+			"dev": true
+		},
+		"jsonparse": {
+			"version": "0.0.5",
+			"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz",
+			"integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=",
+			"dev": true
+		},
+		"jsprim": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+			"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+			"dev": true,
+			"requires": {
+				"assert-plus": "1.0.0",
+				"extsprintf": "1.3.0",
+				"json-schema": "0.2.3",
+				"verror": "1.10.0"
+			}
+		},
+		"kew": {
+			"version": "0.7.0",
+			"resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
+			"integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=",
+			"dev": true,
+			"optional": true
+		},
+		"kind-of": {
+			"version": "6.0.2",
+			"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+			"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+			"dev": true
+		},
+		"klaw": {
+			"version": "1.3.1",
+			"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+			"integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"graceful-fs": "^4.1.9"
+			}
+		},
+		"known-css-properties": {
+			"version": "0.0.7",
+			"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.0.7.tgz",
+			"integrity": "sha1-kQQ0Oirf2O87B73uejJeTUTtk3E=",
+			"dev": true
+		},
+		"lazy-req": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/lazy-req/-/lazy-req-1.1.0.tgz",
+			"integrity": "sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=",
+			"dev": true,
+			"optional": true
+		},
+		"lazystream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
+			"integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
+			"dev": true,
+			"requires": {
+				"readable-stream": "^2.0.5"
+			}
+		},
+		"lcid": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+			"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+			"dev": true,
+			"requires": {
+				"invert-kv": "^1.0.0"
+			}
+		},
+		"ldjson-stream": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/ldjson-stream/-/ldjson-stream-1.2.1.tgz",
+			"integrity": "sha1-kb7O2lrE7SsX5kn7d356v6AYnCs=",
+			"dev": true,
+			"requires": {
+				"split2": "^0.2.1",
+				"through2": "^0.6.1"
+			}
+		},
+		"livereload-js": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.3.0.tgz",
+			"integrity": "sha512-j1R0/FeGa64Y+NmqfZhyoVRzcFlOZ8sNlKzHjh4VvLULFACZhn68XrX5DFg2FhMvSMJmROuFxRSa560ECWKBMg==",
+			"dev": true
+		},
+		"load-json-file": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+			"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.1.2",
+				"parse-json": "^2.2.0",
+				"pify": "^2.0.0",
+				"pinkie-promise": "^2.0.0",
+				"strip-bom": "^2.0.0"
+			}
+		},
+		"lodash": {
+			"version": "4.17.10",
+			"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+			"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
+			"dev": true
+		},
+		"lodash._basecopy": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+			"integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+			"dev": true
+		},
+		"lodash._basetostring": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz",
+			"integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=",
+			"dev": true
+		},
+		"lodash._basevalues": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz",
+			"integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=",
+			"dev": true
+		},
+		"lodash._getnative": {
+			"version": "3.9.1",
+			"resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+			"integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+			"dev": true
+		},
+		"lodash._isiterateecall": {
+			"version": "3.0.9",
+			"resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
+			"integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
+			"dev": true
+		},
+		"lodash._reescape": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz",
+			"integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=",
+			"dev": true
+		},
+		"lodash._reevaluate": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz",
+			"integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=",
+			"dev": true
+		},
+		"lodash._reinterpolate": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
+			"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
+			"dev": true
+		},
+		"lodash._root": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
+			"integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=",
+			"dev": true
+		},
+		"lodash.assign": {
+			"version": "4.2.0",
+			"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+			"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+			"dev": true
+		},
+		"lodash.camelcase": {
+			"version": "4.3.0",
+			"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+			"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
+			"dev": true
+		},
+		"lodash.clonedeep": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+			"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+			"dev": true
+		},
+		"lodash.escape": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
+			"integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=",
+			"dev": true,
+			"requires": {
+				"lodash._root": "^3.0.0"
+			}
+		},
+		"lodash.isarguments": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+			"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+			"dev": true
+		},
+		"lodash.isarray": {
+			"version": "3.0.4",
+			"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+			"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+			"dev": true
+		},
+		"lodash.isequal": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+			"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+			"dev": true
+		},
+		"lodash.keys": {
+			"version": "3.1.2",
+			"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+			"integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+			"dev": true,
+			"requires": {
+				"lodash._getnative": "^3.0.0",
+				"lodash.isarguments": "^3.0.0",
+				"lodash.isarray": "^3.0.0"
+			}
+		},
+		"lodash.mergewith": {
+			"version": "4.6.1",
+			"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+			"integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+			"dev": true
+		},
+		"lodash.restparam": {
+			"version": "3.6.1",
+			"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
+			"integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
+			"dev": true
+		},
+		"lodash.template": {
+			"version": "3.6.2",
+			"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
+			"integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=",
+			"dev": true,
+			"requires": {
+				"lodash._basecopy": "^3.0.0",
+				"lodash._basetostring": "^3.0.0",
+				"lodash._basevalues": "^3.0.0",
+				"lodash._isiterateecall": "^3.0.0",
+				"lodash._reinterpolate": "^3.0.0",
+				"lodash.escape": "^3.0.0",
+				"lodash.keys": "^3.0.0",
+				"lodash.restparam": "^3.0.0",
+				"lodash.templatesettings": "^3.0.0"
+			}
+		},
+		"lodash.templatesettings": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz",
+			"integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=",
+			"dev": true,
+			"requires": {
+				"lodash._reinterpolate": "^3.0.0",
+				"lodash.escape": "^3.0.0"
+			}
+		},
+		"log-symbols": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
+			"integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"logalot": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz",
+			"integrity": "sha1-X46MkNME7fElMJUaVVSruMXj9VI=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"figures": "^1.3.5",
+				"squeak": "^1.0.0"
+			}
+		},
+		"longest": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+			"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+			"dev": true,
+			"optional": true
+		},
+		"loud-rejection": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+			"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+			"dev": true,
+			"requires": {
+				"currently-unhandled": "^0.4.1",
+				"signal-exit": "^3.0.0"
+			}
+		},
+		"lowercase-keys": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+			"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+			"dev": true
+		},
+		"lpad-align": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz",
+			"integrity": "sha1-IfYArBwwlcPG5JfuZyce4ISB/p4=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"get-stdin": "^4.0.1",
+				"indent-string": "^2.1.0",
+				"longest": "^1.0.0",
+				"meow": "^3.3.0"
+			}
+		},
+		"lru-cache": {
+			"version": "4.1.3",
+			"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
+			"integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
+			"dev": true,
+			"requires": {
+				"pseudomap": "^1.0.2",
+				"yallist": "^2.1.2"
+			}
+		},
+		"make-dir": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+			"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+			"dev": true,
+			"requires": {
+				"pify": "^3.0.0"
+			},
+			"dependencies": {
+				"pify": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+					"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+					"dev": true
+				}
+			}
+		},
+		"map-cache": {
+			"version": "0.2.2",
+			"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+			"integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+			"dev": true
+		},
+		"map-obj": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+			"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+			"dev": true
+		},
+		"map-visit": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+			"integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+			"dev": true,
+			"requires": {
+				"object-visit": "^1.0.0"
+			}
+		},
+		"matchdep": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-1.0.1.tgz",
+			"integrity": "sha1-pXozgESR+64girqPaDgEN6vC3KU=",
+			"dev": true,
+			"requires": {
+				"findup-sync": "~0.3.0",
+				"micromatch": "^2.3.7",
+				"resolve": "~1.1.6",
+				"stack-trace": "0.0.9"
+			},
+			"dependencies": {
+				"arr-diff": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+					"integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+					"dev": true,
+					"requires": {
+						"arr-flatten": "^1.0.1"
+					}
+				},
+				"array-unique": {
+					"version": "0.2.1",
+					"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+					"integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+					"dev": true
+				},
+				"braces": {
+					"version": "1.8.5",
+					"resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+					"integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+					"dev": true,
+					"requires": {
+						"expand-range": "^1.8.1",
+						"preserve": "^0.2.0",
+						"repeat-element": "^1.1.2"
+					}
+				},
+				"expand-brackets": {
+					"version": "0.1.5",
+					"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+					"integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+					"dev": true,
+					"requires": {
+						"is-posix-bracket": "^0.1.0"
+					}
+				},
+				"extglob": {
+					"version": "0.3.2",
+					"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+					"integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				},
+				"is-extglob": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+					"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+					"dev": true
+				},
+				"is-glob": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+					"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				},
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				},
+				"micromatch": {
+					"version": "2.3.11",
+					"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+					"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+					"dev": true,
+					"requires": {
+						"arr-diff": "^2.0.0",
+						"array-unique": "^0.2.1",
+						"braces": "^1.8.2",
+						"expand-brackets": "^0.1.4",
+						"extglob": "^0.3.1",
+						"filename-regex": "^2.0.0",
+						"is-extglob": "^1.0.0",
+						"is-glob": "^2.0.1",
+						"kind-of": "^3.0.2",
+						"normalize-path": "^2.0.1",
+						"object.omit": "^2.0.0",
+						"parse-glob": "^3.0.4",
+						"regex-cache": "^0.4.2"
+					}
+				},
+				"stack-trace": {
+					"version": "0.0.9",
+					"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz",
+					"integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=",
+					"dev": true
+				}
+			}
+		},
+		"math-random": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
+			"integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
+			"dev": true
+		},
+		"maxmin": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz",
+			"integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"figures": "^1.0.1",
+				"gzip-size": "^3.0.0",
+				"pretty-bytes": "^3.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"meow": {
+			"version": "3.7.0",
+			"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+			"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+			"dev": true,
+			"requires": {
+				"camelcase-keys": "^2.0.0",
+				"decamelize": "^1.1.2",
+				"loud-rejection": "^1.0.0",
+				"map-obj": "^1.0.1",
+				"minimist": "^1.1.3",
+				"normalize-package-data": "^2.3.4",
+				"object-assign": "^4.0.1",
+				"read-pkg-up": "^1.0.1",
+				"redent": "^1.0.0",
+				"trim-newlines": "^1.0.0"
+			}
+		},
+		"merge-stream": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
+			"integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=",
+			"dev": true,
+			"requires": {
+				"readable-stream": "^2.0.1"
+			}
+		},
+		"micromatch": {
+			"version": "3.1.10",
+			"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+			"integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+			"dev": true,
+			"requires": {
+				"arr-diff": "^4.0.0",
+				"array-unique": "^0.3.2",
+				"braces": "^2.3.1",
+				"define-property": "^2.0.2",
+				"extend-shallow": "^3.0.2",
+				"extglob": "^2.0.4",
+				"fragment-cache": "^0.2.1",
+				"kind-of": "^6.0.2",
+				"nanomatch": "^1.2.9",
+				"object.pick": "^1.3.0",
+				"regex-not": "^1.0.0",
+				"snapdragon": "^0.8.1",
+				"to-regex": "^3.0.2"
+			}
+		},
+		"mime-db": {
+			"version": "1.35.0",
+			"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz",
+			"integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==",
+			"dev": true
+		},
+		"mime-types": {
+			"version": "2.1.19",
+			"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz",
+			"integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==",
+			"dev": true,
+			"requires": {
+				"mime-db": "~1.35.0"
+			}
+		},
+		"mimic-fn": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+			"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+			"dev": true
+		},
+		"minimatch": {
+			"version": "3.0.4",
+			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+			"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+			"dev": true,
+			"requires": {
+				"brace-expansion": "^1.1.7"
+			}
+		},
+		"minimist": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+			"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+			"dev": true
+		},
+		"mixin-deep": {
+			"version": "1.3.1",
+			"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+			"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+			"dev": true,
+			"requires": {
+				"for-in": "^1.0.2",
+				"is-extendable": "^1.0.1"
+			},
+			"dependencies": {
+				"is-extendable": {
+					"version": "1.0.1",
+					"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+					"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+					"dev": true,
+					"requires": {
+						"is-plain-object": "^2.0.4"
+					}
+				}
+			}
+		},
+		"mkdirp": {
+			"version": "0.5.1",
+			"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+			"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+			"dev": true,
+			"requires": {
+				"minimist": "0.0.8"
+			},
+			"dependencies": {
+				"minimist": {
+					"version": "0.0.8",
+					"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+					"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+					"dev": true
+				}
+			}
+		},
+		"mout": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz",
+			"integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==",
+			"dev": true
+		},
+		"ms": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+			"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+			"dev": true
+		},
+		"multimatch": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz",
+			"integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=",
+			"dev": true,
+			"requires": {
+				"array-differ": "^1.0.0",
+				"array-union": "^1.0.1",
+				"arrify": "^1.0.0",
+				"minimatch": "^3.0.0"
+			}
+		},
+		"multipipe": {
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
+			"integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=",
+			"dev": true,
+			"requires": {
+				"duplexer2": "0.0.2"
+			},
+			"dependencies": {
+				"duplexer2": {
+					"version": "0.0.2",
+					"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+					"integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "~1.1.9"
+					}
+				},
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+					"dev": true
+				},
+				"readable-stream": {
+					"version": "1.1.14",
+					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+					"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+					"dev": true,
+					"requires": {
+						"core-util-is": "~1.0.0",
+						"inherits": "~2.0.1",
+						"isarray": "0.0.1",
+						"string_decoder": "~0.10.x"
+					}
+				},
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				}
+			}
+		},
+		"mute-stream": {
+			"version": "0.0.7",
+			"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+			"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+			"dev": true
+		},
+		"nan": {
+			"version": "2.10.0",
+			"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
+			"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
+			"dev": true
+		},
+		"nanomatch": {
+			"version": "1.2.13",
+			"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+			"integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+			"dev": true,
+			"requires": {
+				"arr-diff": "^4.0.0",
+				"array-unique": "^0.3.2",
+				"define-property": "^2.0.2",
+				"extend-shallow": "^3.0.2",
+				"fragment-cache": "^0.2.1",
+				"is-windows": "^1.0.2",
+				"kind-of": "^6.0.2",
+				"object.pick": "^1.3.0",
+				"regex-not": "^1.0.0",
+				"snapdragon": "^0.8.1",
+				"to-regex": "^3.0.1"
+			}
+		},
+		"node-gyp": {
+			"version": "3.8.0",
+			"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+			"integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+			"dev": true,
+			"requires": {
+				"fstream": "^1.0.0",
+				"glob": "^7.0.3",
+				"graceful-fs": "^4.1.2",
+				"mkdirp": "^0.5.0",
+				"nopt": "2 || 3",
+				"npmlog": "0 || 1 || 2 || 3 || 4",
+				"osenv": "0",
+				"request": "^2.87.0",
+				"rimraf": "2",
+				"semver": "~5.3.0",
+				"tar": "^2.0.0",
+				"which": "1"
+			},
+			"dependencies": {
+				"semver": {
+					"version": "5.3.0",
+					"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+					"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+					"dev": true
+				}
+			}
+		},
+		"node-sass": {
+			"version": "4.9.3",
+			"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz",
+			"integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==",
+			"dev": true,
+			"requires": {
+				"async-foreach": "^0.1.3",
+				"chalk": "^1.1.1",
+				"cross-spawn": "^3.0.0",
+				"gaze": "^1.0.0",
+				"get-stdin": "^4.0.1",
+				"glob": "^7.0.3",
+				"in-publish": "^2.0.0",
+				"lodash.assign": "^4.2.0",
+				"lodash.clonedeep": "^4.3.2",
+				"lodash.mergewith": "^4.6.0",
+				"meow": "^3.7.0",
+				"mkdirp": "^0.5.1",
+				"nan": "^2.10.0",
+				"node-gyp": "^3.8.0",
+				"npmlog": "^4.0.0",
+				"request": "2.87.0",
+				"sass-graph": "^2.2.4",
+				"stdout-stream": "^1.4.0",
+				"true-case-path": "^1.0.2"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"cross-spawn": {
+					"version": "3.0.1",
+					"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+					"integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+					"dev": true,
+					"requires": {
+						"lru-cache": "^4.0.1",
+						"which": "^1.2.9"
+					}
+				},
+				"har-validator": {
+					"version": "5.0.3",
+					"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
+					"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+					"dev": true,
+					"requires": {
+						"ajv": "^5.1.0",
+						"har-schema": "^2.0.0"
+					}
+				},
+				"oauth-sign": {
+					"version": "0.8.2",
+					"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+					"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+					"dev": true
+				},
+				"request": {
+					"version": "2.87.0",
+					"resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
+					"integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
+					"dev": true,
+					"requires": {
+						"aws-sign2": "~0.7.0",
+						"aws4": "^1.6.0",
+						"caseless": "~0.12.0",
+						"combined-stream": "~1.0.5",
+						"extend": "~3.0.1",
+						"forever-agent": "~0.6.1",
+						"form-data": "~2.3.1",
+						"har-validator": "~5.0.3",
+						"http-signature": "~1.2.0",
+						"is-typedarray": "~1.0.0",
+						"isstream": "~0.1.2",
+						"json-stringify-safe": "~5.0.1",
+						"mime-types": "~2.1.17",
+						"oauth-sign": "~0.8.2",
+						"performance-now": "^2.1.0",
+						"qs": "~6.5.1",
+						"safe-buffer": "^5.1.1",
+						"tough-cookie": "~2.3.3",
+						"tunnel-agent": "^0.6.0",
+						"uuid": "^3.1.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				},
+				"tough-cookie": {
+					"version": "2.3.4",
+					"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+					"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+					"dev": true,
+					"requires": {
+						"punycode": "^1.4.1"
+					}
+				},
+				"tunnel-agent": {
+					"version": "0.6.0",
+					"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+					"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+					"dev": true,
+					"requires": {
+						"safe-buffer": "^5.0.1"
+					}
+				}
+			}
+		},
+		"node-status-codes": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz",
+			"integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=",
+			"dev": true
+		},
+		"node-wp-i18n": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/node-wp-i18n/-/node-wp-i18n-1.2.1.tgz",
+			"integrity": "sha512-Wvl98XA/GBnQFe20iAMwlsXvikVOFTVz//cbRsF8OPQf0NIkJdeWToNYww/dZl52lHtS1KUEIVRyWcol5ibHog==",
+			"dev": true,
+			"requires": {
+				"bluebird": "^3.4.1",
+				"gettext-parser": "^2.0.0",
+				"glob": "^7.0.5",
+				"lodash": "^4.14.2",
+				"minimist": "^1.2.0",
+				"mkdirp": "^0.5.1",
+				"tmp": "^0.0.33"
+			}
+		},
+		"nopt": {
+			"version": "3.0.6",
+			"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+			"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+			"dev": true,
+			"requires": {
+				"abbrev": "1"
+			}
+		},
+		"normalize-package-data": {
+			"version": "2.4.0",
+			"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+			"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+			"dev": true,
+			"requires": {
+				"hosted-git-info": "^2.1.4",
+				"is-builtin-module": "^1.0.0",
+				"semver": "2 || 3 || 4 || 5",
+				"validate-npm-package-license": "^3.0.1"
+			}
+		},
+		"normalize-path": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+			"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+			"dev": true,
+			"requires": {
+				"remove-trailing-separator": "^1.0.1"
+			}
+		},
+		"normalize-range": {
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+			"integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+			"dev": true
+		},
+		"normalize-selector": {
+			"version": "0.2.0",
+			"resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz",
+			"integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=",
+			"dev": true
+		},
+		"npm-run-path": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+			"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"path-key": "^2.0.0"
+			}
+		},
+		"npmlog": {
+			"version": "4.1.2",
+			"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+			"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+			"dev": true,
+			"requires": {
+				"are-we-there-yet": "~1.1.2",
+				"console-control-strings": "~1.1.0",
+				"gauge": "~2.7.3",
+				"set-blocking": "~2.0.0"
+			}
+		},
+		"num2fraction": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+			"integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+			"dev": true
+		},
+		"number-is-nan": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+			"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+			"dev": true
+		},
+		"oauth-sign": {
+			"version": "0.9.0",
+			"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+			"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+			"dev": true
+		},
+		"object-assign": {
+			"version": "4.1.1",
+			"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+			"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+			"dev": true
+		},
+		"object-copy": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+			"integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+			"dev": true,
+			"requires": {
+				"copy-descriptor": "^0.1.0",
+				"define-property": "^0.2.5",
+				"kind-of": "^3.0.3"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "0.2.5",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+					"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^0.1.0"
+					}
+				},
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"object-visit": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+			"integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+			"dev": true,
+			"requires": {
+				"isobject": "^3.0.0"
+			}
+		},
+		"object.omit": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+			"integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+			"dev": true,
+			"requires": {
+				"for-own": "^0.1.4",
+				"is-extendable": "^0.1.1"
+			}
+		},
+		"object.pick": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+			"integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+			"dev": true,
+			"requires": {
+				"isobject": "^3.0.1"
+			}
+		},
+		"once": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+			"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+			"dev": true,
+			"requires": {
+				"wrappy": "1"
+			}
+		},
+		"onecolor": {
+			"version": "3.0.5",
+			"resolved": "https://registry.npmjs.org/onecolor/-/onecolor-3.0.5.tgz",
+			"integrity": "sha1-Nu/zIgE3nv3xGA+0ReUajiQl+fY=",
+			"dev": true
+		},
+		"onetime": {
+			"version": "1.1.0",
+			"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+			"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+			"dev": true
+		},
+		"optimist": {
+			"version": "0.6.1",
+			"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+			"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+			"dev": true,
+			"requires": {
+				"minimist": "~0.0.1",
+				"wordwrap": "~0.0.2"
+			},
+			"dependencies": {
+				"minimist": {
+					"version": "0.0.10",
+					"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+					"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
+					"dev": true
+				}
+			}
+		},
+		"optipng-bin": {
+			"version": "3.1.4",
+			"resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-3.1.4.tgz",
+			"integrity": "sha1-ldNPLEiHBPb9cGBr/qDGWfHZXYQ=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"bin-build": "^2.0.0",
+				"bin-wrapper": "^3.0.0",
+				"logalot": "^2.0.0"
+			}
+		},
+		"ordered-read-streams": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz",
+			"integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=",
+			"dev": true,
+			"requires": {
+				"is-stream": "^1.0.1",
+				"readable-stream": "^2.0.1"
+			}
+		},
+		"os-filter-obj": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-1.0.3.tgz",
+			"integrity": "sha1-WRUzDZDs7VV9LZOKMcbdIU2cY60=",
+			"dev": true,
+			"optional": true
+		},
+		"os-homedir": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+			"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+			"dev": true
+		},
+		"os-locale": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+			"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+			"dev": true,
+			"requires": {
+				"lcid": "^1.0.0"
+			}
+		},
+		"os-tmpdir": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+			"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+			"dev": true
+		},
+		"osenv": {
+			"version": "0.1.5",
+			"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+			"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+			"dev": true,
+			"requires": {
+				"os-homedir": "^1.0.0",
+				"os-tmpdir": "^1.0.0"
+			}
+		},
+		"p-finally": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+			"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+			"dev": true
+		},
+		"p-map": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
+			"integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
+			"dev": true
+		},
+		"p-pipe": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz",
+			"integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=",
+			"dev": true
+		},
+		"pako": {
+			"version": "0.2.9",
+			"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+			"integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
+			"dev": true
+		},
+		"parse-glob": {
+			"version": "3.0.4",
+			"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+			"integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+			"dev": true,
+			"requires": {
+				"glob-base": "^0.3.0",
+				"is-dotfile": "^1.0.0",
+				"is-extglob": "^1.0.0",
+				"is-glob": "^2.0.0"
+			},
+			"dependencies": {
+				"is-extglob": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+					"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+					"dev": true
+				},
+				"is-glob": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+					"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				}
+			}
+		},
+		"parse-json": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+			"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+			"dev": true,
+			"requires": {
+				"error-ex": "^1.2.0"
+			}
+		},
+		"parse-passwd": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+			"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+			"dev": true
+		},
+		"pascalcase": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+			"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+			"dev": true
+		},
+		"path-dirname": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+			"integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+			"dev": true
+		},
+		"path-exists": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+			"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+			"dev": true,
+			"requires": {
+				"pinkie-promise": "^2.0.0"
+			}
+		},
+		"path-is-absolute": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+			"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+			"dev": true
+		},
+		"path-is-inside": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+			"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+			"dev": true
+		},
+		"path-key": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+			"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+			"dev": true,
+			"optional": true
+		},
+		"path-type": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+			"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.1.2",
+				"pify": "^2.0.0",
+				"pinkie-promise": "^2.0.0"
+			}
+		},
+		"pend": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+			"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+			"dev": true
+		},
+		"performance-now": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+			"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+			"dev": true
+		},
+		"phantom": {
+			"version": "4.0.12",
+			"resolved": "https://registry.npmjs.org/phantom/-/phantom-4.0.12.tgz",
+			"integrity": "sha512-Tz82XhtPmwCk1FFPmecy7yRGZG2btpzY2KI9fcoPT7zT9det0CcMyfBFPp1S8DqzsnQnm8ZYEfdy528mwVtksA==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"phantomjs-prebuilt": "^2.1.16",
+				"split": "^1.0.1",
+				"winston": "^2.4.0"
+			}
+		},
+		"phantomjs-prebuilt": {
+			"version": "2.1.16",
+			"resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz",
+			"integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"es6-promise": "^4.0.3",
+				"extract-zip": "^1.6.5",
+				"fs-extra": "^1.0.0",
+				"hasha": "^2.2.0",
+				"kew": "^0.7.0",
+				"progress": "^1.1.8",
+				"request": "^2.81.0",
+				"request-progress": "^2.0.1",
+				"which": "^1.2.10"
+			}
+		},
+		"pify": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+			"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+			"dev": true
+		},
+		"pinkie": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+			"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+			"dev": true
+		},
+		"pinkie-promise": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+			"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+			"dev": true,
+			"requires": {
+				"pinkie": "^2.0.0"
+			}
+		},
+		"pipetteur": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/pipetteur/-/pipetteur-2.0.3.tgz",
+			"integrity": "sha1-GVV2CVno0aEcsqUOyD7sRwYz5J8=",
+			"dev": true,
+			"requires": {
+				"onecolor": "^3.0.4",
+				"synesthesia": "^1.0.1"
+			}
+		},
+		"plur": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz",
+			"integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=",
+			"dev": true,
+			"requires": {
+				"irregular-plurals": "^1.0.0"
+			}
+		},
+		"posix-character-classes": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+			"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+			"dev": true
+		},
+		"postcss": {
+			"version": "6.0.23",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+			"integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+			"dev": true,
+			"requires": {
+				"chalk": "^2.4.1",
+				"source-map": "^0.6.1",
+				"supports-color": "^5.4.0"
+			}
+		},
+		"postcss-less": {
+			"version": "0.14.0",
+			"resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-0.14.0.tgz",
+			"integrity": "sha1-xjGwicbM5CK5oQ86lY0r7dOBkyQ=",
+			"dev": true,
+			"requires": {
+				"postcss": "^5.0.21"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "2.0.0",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+							"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+							"dev": true
+						}
+					}
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "3.2.3",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+					"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+					"dev": true,
+					"requires": {
+						"has-flag": "^1.0.0"
+					}
+				}
+			}
+		},
+		"postcss-media-query-parser": {
+			"version": "0.2.3",
+			"resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
+			"integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=",
+			"dev": true
+		},
+		"postcss-reporter": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-3.0.0.tgz",
+			"integrity": "sha1-CeoPN6RExWk4eGBuCbAY6+/3z48=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"lodash": "^4.1.0",
+				"log-symbols": "^1.0.2",
+				"postcss": "^5.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "3.2.3",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+							"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+							"dev": true,
+							"requires": {
+								"has-flag": "^1.0.0"
+							}
+						}
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"postcss-resolve-nested-selector": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
+			"integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=",
+			"dev": true
+		},
+		"postcss-scss": {
+			"version": "1.0.6",
+			"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-1.0.6.tgz",
+			"integrity": "sha512-4EFYGHcEw+H3E06PT/pQQri06u/1VIIPjeJQaM8skB80vZuXMhp4cSNV5azmdNkontnOID/XYWEvEEELLFB1ww==",
+			"dev": true,
+			"requires": {
+				"postcss": "^6.0.23"
+			}
+		},
+		"postcss-selector-parser": {
+			"version": "2.2.3",
+			"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz",
+			"integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=",
+			"dev": true,
+			"requires": {
+				"flatten": "^1.0.2",
+				"indexes-of": "^1.0.1",
+				"uniq": "^1.0.1"
+			}
+		},
+		"postcss-value-parser": {
+			"version": "3.3.0",
+			"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz",
+			"integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
+			"dev": true
+		},
+		"prepend-http": {
+			"version": "1.0.4",
+			"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+			"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
+			"dev": true
+		},
+		"preserve": {
+			"version": "0.2.0",
+			"resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+			"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+			"dev": true
+		},
+		"pretty-bytes": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz",
+			"integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=",
+			"dev": true,
+			"requires": {
+				"number-is-nan": "^1.0.0"
+			}
+		},
+		"process-nextick-args": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+			"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+			"dev": true
+		},
+		"progress": {
+			"version": "1.1.8",
+			"resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+			"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+			"dev": true,
+			"optional": true
+		},
+		"pseudomap": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+			"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+			"dev": true
+		},
+		"psl": {
+			"version": "1.1.29",
+			"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
+			"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
+			"dev": true
+		},
+		"punycode": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+			"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+			"dev": true
+		},
+		"q": {
+			"version": "1.5.1",
+			"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+			"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
+			"dev": true,
+			"optional": true
+		},
+		"qs": {
+			"version": "6.5.2",
+			"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+			"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+			"dev": true
+		},
+		"randomatic": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz",
+			"integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==",
+			"dev": true,
+			"requires": {
+				"is-number": "^4.0.0",
+				"kind-of": "^6.0.0",
+				"math-random": "^1.0.1"
+			},
+			"dependencies": {
+				"is-number": {
+					"version": "4.0.0",
+					"resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+					"integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+					"dev": true
+				}
+			}
+		},
+		"raw-body": {
+			"version": "1.1.7",
+			"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz",
+			"integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=",
+			"dev": true,
+			"requires": {
+				"bytes": "1",
+				"string_decoder": "0.10"
+			},
+			"dependencies": {
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				}
+			}
+		},
+		"rc": {
+			"version": "1.2.8",
+			"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+			"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+			"dev": true,
+			"requires": {
+				"deep-extend": "^0.6.0",
+				"ini": "~1.3.0",
+				"minimist": "^1.2.0",
+				"strip-json-comments": "~2.0.1"
+			}
+		},
+		"read-all-stream": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
+			"integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=",
+			"dev": true,
+			"requires": {
+				"pinkie-promise": "^2.0.0",
+				"readable-stream": "^2.0.0"
+			}
+		},
+		"read-file-stdin": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",
+			"integrity": "sha1-JezP86FTtoCa+ssj7hU4fbng7mE=",
+			"dev": true,
+			"requires": {
+				"gather-stream": "^1.0.0"
+			}
+		},
+		"read-pkg": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+			"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+			"dev": true,
+			"requires": {
+				"load-json-file": "^1.0.0",
+				"normalize-package-data": "^2.3.2",
+				"path-type": "^1.0.0"
+			}
+		},
+		"read-pkg-up": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+			"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+			"dev": true,
+			"requires": {
+				"find-up": "^1.0.0",
+				"read-pkg": "^1.0.0"
+			}
+		},
+		"readable-stream": {
+			"version": "2.3.6",
+			"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+			"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+			"dev": true,
+			"requires": {
+				"core-util-is": "~1.0.0",
+				"inherits": "~2.0.3",
+				"isarray": "~1.0.0",
+				"process-nextick-args": "~2.0.0",
+				"safe-buffer": "~5.1.1",
+				"string_decoder": "~1.1.1",
+				"util-deprecate": "~1.0.1"
+			}
+		},
+		"redent": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+			"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+			"dev": true,
+			"requires": {
+				"indent-string": "^2.1.0",
+				"strip-indent": "^1.0.1"
+			}
+		},
+		"regex-cache": {
+			"version": "0.4.4",
+			"resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+			"integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+			"dev": true,
+			"requires": {
+				"is-equal-shallow": "^0.1.3"
+			}
+		},
+		"regex-not": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+			"integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+			"dev": true,
+			"requires": {
+				"extend-shallow": "^3.0.2",
+				"safe-regex": "^1.1.0"
+			}
+		},
+		"remove-trailing-separator": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+			"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+			"dev": true
+		},
+		"repeat-element": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+			"integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+			"dev": true
+		},
+		"repeat-string": {
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+			"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+			"dev": true
+		},
+		"repeating": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+			"integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+			"dev": true,
+			"requires": {
+				"is-finite": "^1.0.0"
+			}
+		},
+		"replace-ext": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+			"integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
+			"dev": true
+		},
+		"request": {
+			"version": "2.88.0",
+			"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+			"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+			"dev": true,
+			"requires": {
+				"aws-sign2": "~0.7.0",
+				"aws4": "^1.8.0",
+				"caseless": "~0.12.0",
+				"combined-stream": "~1.0.6",
+				"extend": "~3.0.2",
+				"forever-agent": "~0.6.1",
+				"form-data": "~2.3.2",
+				"har-validator": "~5.1.0",
+				"http-signature": "~1.2.0",
+				"is-typedarray": "~1.0.0",
+				"isstream": "~0.1.2",
+				"json-stringify-safe": "~5.0.1",
+				"mime-types": "~2.1.19",
+				"oauth-sign": "~0.9.0",
+				"performance-now": "^2.1.0",
+				"qs": "~6.5.2",
+				"safe-buffer": "^5.1.2",
+				"tough-cookie": "~2.4.3",
+				"tunnel-agent": "^0.6.0",
+				"uuid": "^3.3.2"
+			},
+			"dependencies": {
+				"tunnel-agent": {
+					"version": "0.6.0",
+					"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+					"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+					"dev": true,
+					"requires": {
+						"safe-buffer": "^5.0.1"
+					}
+				}
+			}
+		},
+		"request-progress": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz",
+			"integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"throttleit": "^1.0.0"
+			}
+		},
+		"require-directory": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+			"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+			"dev": true
+		},
+		"require-from-string": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz",
+			"integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=",
+			"dev": true
+		},
+		"require-main-filename": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+			"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+			"dev": true
+		},
+		"resolve": {
+			"version": "1.1.7",
+			"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+			"integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+			"dev": true
+		},
+		"resolve-dir": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+			"integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+			"dev": true,
+			"requires": {
+				"expand-tilde": "^2.0.0",
+				"global-modules": "^1.0.0"
+			}
+		},
+		"resolve-from": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+			"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
+			"dev": true
+		},
+		"resolve-url": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+			"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+			"dev": true
+		},
+		"restore-cursor": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+			"integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+			"dev": true,
+			"requires": {
+				"onetime": "^2.0.0",
+				"signal-exit": "^3.0.2"
+			},
+			"dependencies": {
+				"onetime": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+					"integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+					"dev": true,
+					"requires": {
+						"mimic-fn": "^1.0.0"
+					}
+				}
+			}
+		},
+		"ret": {
+			"version": "0.1.15",
+			"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+			"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+			"dev": true
+		},
+		"rimraf": {
+			"version": "2.6.2",
+			"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+			"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+			"dev": true,
+			"requires": {
+				"glob": "^7.0.5"
+			}
+		},
+		"rtlcss": {
+			"version": "2.4.0",
+			"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.4.0.tgz",
+			"integrity": "sha512-hdjFhZ5FCI0ABOfyXOMOhBtwPWtANLCG7rOiOcRf+yi5eDdxmDjqBruWouEnwVdzfh/TWF6NNncIEsigOCFZOA==",
+			"dev": true,
+			"requires": {
+				"chalk": "^2.3.0",
+				"findup": "^0.1.5",
+				"mkdirp": "^0.5.1",
+				"postcss": "^6.0.14",
+				"strip-json-comments": "^2.0.0"
+			}
+		},
+		"run-async": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+			"integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+			"dev": true,
+			"requires": {
+				"is-promise": "^2.1.0"
+			}
+		},
+		"rxjs": {
+			"version": "5.5.11",
+			"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz",
+			"integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==",
+			"dev": true,
+			"requires": {
+				"symbol-observable": "1.0.1"
+			}
+		},
+		"safe-buffer": {
+			"version": "5.1.2",
+			"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+			"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+			"dev": true
+		},
+		"safe-json-parse": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz",
+			"integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=",
+			"dev": true
+		},
+		"safe-regex": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+			"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+			"dev": true,
+			"requires": {
+				"ret": "~0.1.10"
+			}
+		},
+		"safer-buffer": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+			"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+			"dev": true
+		},
+		"sass-graph": {
+			"version": "2.2.4",
+			"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+			"integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+			"dev": true,
+			"requires": {
+				"glob": "^7.0.0",
+				"lodash": "^4.0.0",
+				"scss-tokenizer": "^0.2.3",
+				"yargs": "^7.0.0"
+			}
+		},
+		"sax": {
+			"version": "1.2.4",
+			"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+			"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+			"dev": true
+		},
+		"scss-tokenizer": {
+			"version": "0.2.3",
+			"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+			"integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+			"dev": true,
+			"requires": {
+				"js-base64": "^2.1.8",
+				"source-map": "^0.4.2"
+			},
+			"dependencies": {
+				"source-map": {
+					"version": "0.4.4",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+					"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+					"dev": true,
+					"requires": {
+						"amdefine": ">=0.0.4"
+					}
+				}
+			}
+		},
+		"seek-bzip": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
+			"integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=",
+			"dev": true,
+			"requires": {
+				"commander": "~2.8.1"
+			}
+		},
+		"semver": {
+			"version": "5.5.1",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
+			"integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
+			"dev": true
+		},
+		"semver-regex": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz",
+			"integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=",
+			"dev": true,
+			"optional": true
+		},
+		"semver-truncate": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz",
+			"integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"semver": "^5.3.0"
+			}
+		},
+		"set-blocking": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+			"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+			"dev": true
+		},
+		"set-immediate-shim": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+			"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+			"dev": true
+		},
+		"set-value": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+			"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+			"dev": true,
+			"requires": {
+				"extend-shallow": "^2.0.1",
+				"is-extendable": "^0.1.1",
+				"is-plain-object": "^2.0.3",
+				"split-string": "^3.0.1"
+			},
+			"dependencies": {
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				}
+			}
+		},
+		"shebang-command": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+			"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"shebang-regex": "^1.0.0"
+			}
+		},
+		"shebang-regex": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+			"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+			"dev": true,
+			"optional": true
+		},
+		"shelljs": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz",
+			"integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=",
+			"dev": true
+		},
+		"sigmund": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
+			"integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=",
+			"dev": true
+		},
+		"signal-exit": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+			"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+			"dev": true
+		},
+		"slice-ansi": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz",
+			"integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==",
+			"dev": true,
+			"requires": {
+				"is-fullwidth-code-point": "^2.0.0"
+			}
+		},
+		"snapdragon": {
+			"version": "0.8.2",
+			"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+			"integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+			"dev": true,
+			"requires": {
+				"base": "^0.11.1",
+				"debug": "^2.2.0",
+				"define-property": "^0.2.5",
+				"extend-shallow": "^2.0.1",
+				"map-cache": "^0.2.2",
+				"source-map": "^0.5.6",
+				"source-map-resolve": "^0.5.0",
+				"use": "^3.1.0"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "0.2.5",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+					"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^0.1.0"
+					}
+				},
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				}
+			}
+		},
+		"snapdragon-node": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+			"integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+			"dev": true,
+			"requires": {
+				"define-property": "^1.0.0",
+				"isobject": "^3.0.0",
+				"snapdragon-util": "^3.0.1"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+					"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^1.0.0"
+					}
+				},
+				"is-accessor-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+					"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-data-descriptor": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+					"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+					"dev": true,
+					"requires": {
+						"kind-of": "^6.0.0"
+					}
+				},
+				"is-descriptor": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+					"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+					"dev": true,
+					"requires": {
+						"is-accessor-descriptor": "^1.0.0",
+						"is-data-descriptor": "^1.0.0",
+						"kind-of": "^6.0.2"
+					}
+				}
+			}
+		},
+		"snapdragon-util": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+			"integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+			"dev": true,
+			"requires": {
+				"kind-of": "^3.2.0"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"source-map": {
+			"version": "0.6.1",
+			"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+			"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+			"dev": true
+		},
+		"source-map-resolve": {
+			"version": "0.5.2",
+			"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+			"integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+			"dev": true,
+			"requires": {
+				"atob": "^2.1.1",
+				"decode-uri-component": "^0.2.0",
+				"resolve-url": "^0.2.1",
+				"source-map-url": "^0.4.0",
+				"urix": "^0.1.0"
+			}
+		},
+		"source-map-url": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+			"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+			"dev": true
+		},
+		"sparkles": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz",
+			"integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==",
+			"dev": true
+		},
+		"spdx-correct": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
+			"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
+			"dev": true,
+			"requires": {
+				"spdx-expression-parse": "^3.0.0",
+				"spdx-license-ids": "^3.0.0"
+			}
+		},
+		"spdx-exceptions": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
+			"integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
+			"dev": true
+		},
+		"spdx-expression-parse": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+			"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+			"dev": true,
+			"requires": {
+				"spdx-exceptions": "^2.1.0",
+				"spdx-license-ids": "^3.0.0"
+			}
+		},
+		"spdx-license-ids": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz",
+			"integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==",
+			"dev": true
+		},
+		"specificity": {
+			"version": "0.3.2",
+			"resolved": "https://registry.npmjs.org/specificity/-/specificity-0.3.2.tgz",
+			"integrity": "sha512-Nc/QN/A425Qog7j9aHmwOrlwX2e7pNI47ciwxwy4jOlvbbMHkNNJchit+FX+UjF3IAdiaaV5BKeWuDUnws6G1A==",
+			"dev": true
+		},
+		"split": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
+			"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"through": "2"
+			}
+		},
+		"split-string": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+			"integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+			"dev": true,
+			"requires": {
+				"extend-shallow": "^3.0.0"
+			}
+		},
+		"split2": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz",
+			"integrity": "sha1-At2smtwD7Au3jBKC7Aecpuha6QA=",
+			"dev": true,
+			"requires": {
+				"through2": "~0.6.1"
+			}
+		},
+		"sprintf-js": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz",
+			"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=",
+			"dev": true
+		},
+		"squeak": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz",
+			"integrity": "sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"console-stream": "^0.1.1",
+				"lpad-align": "^1.0.1"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true,
+					"optional": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"optional": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"sshpk": {
+			"version": "1.14.2",
+			"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
+			"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+			"dev": true,
+			"requires": {
+				"asn1": "~0.2.3",
+				"assert-plus": "^1.0.0",
+				"bcrypt-pbkdf": "^1.0.0",
+				"dashdash": "^1.12.0",
+				"ecc-jsbn": "~0.1.1",
+				"getpass": "^0.1.1",
+				"jsbn": "~0.1.0",
+				"safer-buffer": "^2.0.2",
+				"tweetnacl": "~0.14.0"
+			}
+		},
+		"stack-trace": {
+			"version": "0.0.10",
+			"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+			"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
+			"dev": true,
+			"optional": true
+		},
+		"stat-mode": {
+			"version": "0.2.2",
+			"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz",
+			"integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=",
+			"dev": true
+		},
+		"static-extend": {
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+			"integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+			"dev": true,
+			"requires": {
+				"define-property": "^0.2.5",
+				"object-copy": "^0.1.0"
+			},
+			"dependencies": {
+				"define-property": {
+					"version": "0.2.5",
+					"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+					"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+					"dev": true,
+					"requires": {
+						"is-descriptor": "^0.1.0"
+					}
+				}
+			}
+		},
+		"stdout-stream": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
+			"integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
+			"dev": true,
+			"requires": {
+				"readable-stream": "^2.0.1"
+			}
+		},
+		"stream-combiner": {
+			"version": "0.2.2",
+			"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
+			"integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=",
+			"dev": true,
+			"requires": {
+				"duplexer": "~0.1.1",
+				"through": "~2.3.4"
+			}
+		},
+		"stream-combiner2": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
+			"integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=",
+			"dev": true,
+			"requires": {
+				"duplexer2": "~0.1.0",
+				"readable-stream": "^2.0.2"
+			}
+		},
+		"stream-shift": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
+			"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
+			"dev": true
+		},
+		"string-template": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
+			"integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=",
+			"dev": true
+		},
+		"string-width": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+			"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+			"dev": true,
+			"requires": {
+				"is-fullwidth-code-point": "^2.0.0",
+				"strip-ansi": "^4.0.0"
+			},
+			"dependencies": {
+				"ansi-regex": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+					"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+					"dev": true
+				},
+				"strip-ansi": {
+					"version": "4.0.0",
+					"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+					"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+					"dev": true,
+					"requires": {
+						"ansi-regex": "^3.0.0"
+					}
+				}
+			}
+		},
+		"string_decoder": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+			"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+			"dev": true,
+			"requires": {
+				"safe-buffer": "~5.1.0"
+			}
+		},
+		"strip-ansi": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+			"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+			"dev": true,
+			"requires": {
+				"ansi-regex": "^2.0.0"
+			}
+		},
+		"strip-bom": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+			"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+			"dev": true,
+			"requires": {
+				"is-utf8": "^0.2.0"
+			}
+		},
+		"strip-bom-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz",
+			"integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=",
+			"dev": true,
+			"requires": {
+				"first-chunk-stream": "^1.0.0",
+				"strip-bom": "^2.0.0"
+			}
+		},
+		"strip-dirs": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz",
+			"integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0",
+				"get-stdin": "^4.0.1",
+				"is-absolute": "^0.1.5",
+				"is-natural-number": "^2.0.0",
+				"minimist": "^1.1.0",
+				"sum-up": "^1.0.1"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"strip-eof": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+			"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+			"dev": true,
+			"optional": true
+		},
+		"strip-indent": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+			"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+			"dev": true,
+			"requires": {
+				"get-stdin": "^4.0.1"
+			}
+		},
+		"strip-json-comments": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+			"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+			"dev": true
+		},
+		"strip-outer": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
+			"integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
+			"dev": true,
+			"requires": {
+				"escape-string-regexp": "^1.0.2"
+			}
+		},
+		"style-search": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
+			"integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=",
+			"dev": true
+		},
+		"stylehacks": {
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-2.3.2.tgz",
+			"integrity": "sha1-ZMg+BDimjJ7fRJ6MVSp9mrYAmws=",
+			"dev": true,
+			"requires": {
+				"browserslist": "^1.1.3",
+				"chalk": "^1.1.1",
+				"log-symbols": "^1.0.2",
+				"minimist": "^1.2.0",
+				"plur": "^2.1.2",
+				"postcss": "^5.0.18",
+				"postcss-reporter": "^1.3.3",
+				"postcss-selector-parser": "^2.0.0",
+				"read-file-stdin": "^0.2.1",
+				"text-table": "^0.2.0",
+				"write-file-stdout": "0.0.2"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"browserslist": {
+					"version": "1.7.7",
+					"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+					"integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+					"dev": true,
+					"requires": {
+						"caniuse-db": "^1.0.30000639",
+						"electron-to-chromium": "^1.2.7"
+					}
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "3.2.3",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+							"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+							"dev": true,
+							"requires": {
+								"has-flag": "^1.0.0"
+							}
+						}
+					}
+				},
+				"postcss-reporter": {
+					"version": "1.4.1",
+					"resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-1.4.1.tgz",
+					"integrity": "sha1-wTbwpbFhkV83ndN2XGEHX357mvI=",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.0.0",
+						"lodash": "^4.1.0",
+						"log-symbols": "^1.0.2",
+						"postcss": "^5.0.0"
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"stylelint": {
+			"version": "7.10.1",
+			"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-7.10.1.tgz",
+			"integrity": "sha1-IJp85eeB/CpiSJ+7MewCAexnXbI=",
+			"dev": true,
+			"requires": {
+				"autoprefixer": "^6.0.0",
+				"balanced-match": "^0.4.0",
+				"chalk": "^1.1.1",
+				"colorguard": "^1.2.0",
+				"cosmiconfig": "^2.1.1",
+				"debug": "^2.6.0",
+				"doiuse": "^2.4.1",
+				"execall": "^1.0.0",
+				"file-entry-cache": "^2.0.0",
+				"get-stdin": "^5.0.0",
+				"globby": "^6.0.0",
+				"globjoin": "^0.1.4",
+				"html-tags": "^1.1.1",
+				"ignore": "^3.2.0",
+				"imurmurhash": "^0.1.4",
+				"known-css-properties": "^0.0.7",
+				"lodash": "^4.17.4",
+				"log-symbols": "^1.0.2",
+				"meow": "^3.3.0",
+				"micromatch": "^2.3.11",
+				"normalize-selector": "^0.2.0",
+				"postcss": "^5.0.20",
+				"postcss-less": "^0.14.0",
+				"postcss-media-query-parser": "^0.2.0",
+				"postcss-reporter": "^3.0.0",
+				"postcss-resolve-nested-selector": "^0.1.1",
+				"postcss-scss": "^0.4.0",
+				"postcss-selector-parser": "^2.1.1",
+				"postcss-value-parser": "^3.1.1",
+				"resolve-from": "^2.0.0",
+				"specificity": "^0.3.0",
+				"string-width": "^2.0.0",
+				"style-search": "^0.1.0",
+				"stylehacks": "^2.3.2",
+				"sugarss": "^0.2.0",
+				"svg-tags": "^1.0.0",
+				"table": "^4.0.1"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"arr-diff": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+					"integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+					"dev": true,
+					"requires": {
+						"arr-flatten": "^1.0.1"
+					}
+				},
+				"array-unique": {
+					"version": "0.2.1",
+					"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+					"integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+					"dev": true
+				},
+				"autoprefixer": {
+					"version": "6.7.7",
+					"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
+					"integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=",
+					"dev": true,
+					"requires": {
+						"browserslist": "^1.7.6",
+						"caniuse-db": "^1.0.30000634",
+						"normalize-range": "^0.1.2",
+						"num2fraction": "^1.2.2",
+						"postcss": "^5.2.16",
+						"postcss-value-parser": "^3.2.3"
+					}
+				},
+				"balanced-match": {
+					"version": "0.4.2",
+					"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+					"integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=",
+					"dev": true
+				},
+				"braces": {
+					"version": "1.8.5",
+					"resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+					"integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+					"dev": true,
+					"requires": {
+						"expand-range": "^1.8.1",
+						"preserve": "^0.2.0",
+						"repeat-element": "^1.1.2"
+					}
+				},
+				"browserslist": {
+					"version": "1.7.7",
+					"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+					"integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+					"dev": true,
+					"requires": {
+						"caniuse-db": "^1.0.30000639",
+						"electron-to-chromium": "^1.2.7"
+					}
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "2.0.0",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+							"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+							"dev": true
+						}
+					}
+				},
+				"expand-brackets": {
+					"version": "0.1.5",
+					"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+					"integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+					"dev": true,
+					"requires": {
+						"is-posix-bracket": "^0.1.0"
+					}
+				},
+				"extglob": {
+					"version": "0.3.2",
+					"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+					"integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				},
+				"get-stdin": {
+					"version": "5.0.1",
+					"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
+					"integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=",
+					"dev": true
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"is-extglob": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+					"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+					"dev": true
+				},
+				"is-glob": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+					"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+					"dev": true,
+					"requires": {
+						"is-extglob": "^1.0.0"
+					}
+				},
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				},
+				"micromatch": {
+					"version": "2.3.11",
+					"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+					"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+					"dev": true,
+					"requires": {
+						"arr-diff": "^2.0.0",
+						"array-unique": "^0.2.1",
+						"braces": "^1.8.2",
+						"expand-brackets": "^0.1.4",
+						"extglob": "^0.3.1",
+						"filename-regex": "^2.0.0",
+						"is-extglob": "^1.0.0",
+						"is-glob": "^2.0.1",
+						"kind-of": "^3.0.2",
+						"normalize-path": "^2.0.1",
+						"object.omit": "^2.0.0",
+						"parse-glob": "^3.0.4",
+						"regex-cache": "^0.4.2"
+					}
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					}
+				},
+				"postcss-scss": {
+					"version": "0.4.1",
+					"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.4.1.tgz",
+					"integrity": "sha1-rXcbgfD3L19IRdCKpg+TVXZT1Uw=",
+					"dev": true,
+					"requires": {
+						"postcss": "^5.2.13"
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "3.2.3",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+					"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+					"dev": true,
+					"requires": {
+						"has-flag": "^1.0.0"
+					}
+				}
+			}
+		},
+		"stylelint-config-wordpress": {
+			"version": "11.0.0",
+			"resolved": "https://registry.npmjs.org/stylelint-config-wordpress/-/stylelint-config-wordpress-11.0.0.tgz",
+			"integrity": "sha1-bxBvYUVNnnMygoptmba9vCP/TmQ=",
+			"dev": true,
+			"requires": {
+				"stylelint-scss": "^1.4.4"
+			}
+		},
+		"stylelint-scss": {
+			"version": "1.5.2",
+			"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-1.5.2.tgz",
+			"integrity": "sha512-0Ps/isz5HZuM9TtQiMFoeNWq6PvrJUoCWLWCD+uGqcfnAJ8CJjikBEyjtDOFNVu+7cvqOeVm/L+jGNdYQ8zu1g==",
+			"dev": true,
+			"requires": {
+				"lodash": "^4.11.1",
+				"postcss-media-query-parser": "^0.2.3",
+				"postcss-resolve-nested-selector": "^0.1.1",
+				"postcss-selector-parser": "^2.0.0",
+				"postcss-value-parser": "^3.3.0",
+				"stylelint": "^7.0.3"
+			}
+		},
+		"sugarss": {
+			"version": "0.2.0",
+			"resolved": "https://registry.npmjs.org/sugarss/-/sugarss-0.2.0.tgz",
+			"integrity": "sha1-rDQjdWMyfG/4l7ZHQr9q7BkK054=",
+			"dev": true,
+			"requires": {
+				"postcss": "^5.2.4"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					},
+					"dependencies": {
+						"supports-color": {
+							"version": "2.0.0",
+							"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+							"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+							"dev": true
+						}
+					}
+				},
+				"has-flag": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+					"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+					"dev": true
+				},
+				"postcss": {
+					"version": "5.2.18",
+					"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+					"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+					"dev": true,
+					"requires": {
+						"chalk": "^1.1.3",
+						"js-base64": "^2.1.9",
+						"source-map": "^0.5.6",
+						"supports-color": "^3.2.3"
+					}
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "3.2.3",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+					"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+					"dev": true,
+					"requires": {
+						"has-flag": "^1.0.0"
+					}
+				}
+			}
+		},
+		"sum-up": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz",
+			"integrity": "sha1-HGYfZnBX9jvLeHWqFDi8FiUlFW4=",
+			"dev": true,
+			"requires": {
+				"chalk": "^1.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "2.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+					"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+					"dev": true
+				},
+				"chalk": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+					"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^2.2.1",
+						"escape-string-regexp": "^1.0.2",
+						"has-ansi": "^2.0.0",
+						"strip-ansi": "^3.0.0",
+						"supports-color": "^2.0.0"
+					}
+				},
+				"supports-color": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+					"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+					"dev": true
+				}
+			}
+		},
+		"supports-color": {
+			"version": "5.5.0",
+			"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+			"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+			"dev": true,
+			"requires": {
+				"has-flag": "^3.0.0"
+			}
+		},
+		"svg-tags": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+			"integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
+			"dev": true
+		},
+		"svgo": {
+			"version": "0.7.2",
+			"resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz",
+			"integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"coa": "~1.0.1",
+				"colors": "~1.1.2",
+				"csso": "~2.3.1",
+				"js-yaml": "~3.7.0",
+				"mkdirp": "~0.5.1",
+				"sax": "~1.2.1",
+				"whet.extend": "~0.9.9"
+			},
+			"dependencies": {
+				"js-yaml": {
+					"version": "3.7.0",
+					"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz",
+					"integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=",
+					"dev": true,
+					"optional": true,
+					"requires": {
+						"argparse": "^1.0.7",
+						"esprima": "^2.6.0"
+					}
+				}
+			}
+		},
+		"symbol-observable": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+			"integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=",
+			"dev": true
+		},
+		"synesthesia": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/synesthesia/-/synesthesia-1.0.1.tgz",
+			"integrity": "sha1-XvlepUjA1cbm+btLDQcx3/hkp3c=",
+			"dev": true,
+			"requires": {
+				"css-color-names": "0.0.3"
+			}
+		},
+		"table": {
+			"version": "4.0.3",
+			"resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz",
+			"integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==",
+			"dev": true,
+			"requires": {
+				"ajv": "^6.0.1",
+				"ajv-keywords": "^3.0.0",
+				"chalk": "^2.1.0",
+				"lodash": "^4.17.4",
+				"slice-ansi": "1.0.0",
+				"string-width": "^2.1.1"
+			},
+			"dependencies": {
+				"ajv": {
+					"version": "6.5.3",
+					"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz",
+					"integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==",
+					"dev": true,
+					"requires": {
+						"fast-deep-equal": "^2.0.1",
+						"fast-json-stable-stringify": "^2.0.0",
+						"json-schema-traverse": "^0.4.1",
+						"uri-js": "^4.2.2"
+					}
+				},
+				"fast-deep-equal": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+					"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+					"dev": true
+				},
+				"json-schema-traverse": {
+					"version": "0.4.1",
+					"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+					"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+					"dev": true
+				}
+			}
+		},
+		"tar": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+			"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+			"dev": true,
+			"requires": {
+				"block-stream": "*",
+				"fstream": "^1.0.2",
+				"inherits": "2"
+			}
+		},
+		"tar-stream": {
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz",
+			"integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==",
+			"dev": true,
+			"requires": {
+				"bl": "^1.0.0",
+				"buffer-alloc": "^1.1.0",
+				"end-of-stream": "^1.0.0",
+				"fs-constants": "^1.0.0",
+				"readable-stream": "^2.3.0",
+				"to-buffer": "^1.1.0",
+				"xtend": "^4.0.0"
+			}
+		},
+		"temp-dir": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
+			"integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=",
+			"dev": true,
+			"optional": true
+		},
+		"tempfile": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz",
+			"integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"temp-dir": "^1.0.0",
+				"uuid": "^3.0.1"
+			}
+		},
+		"text-table": {
+			"version": "0.2.0",
+			"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+			"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+			"dev": true
+		},
+		"throttleit": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+			"integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+			"dev": true,
+			"optional": true
+		},
+		"through": {
+			"version": "2.3.8",
+			"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+			"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+			"dev": true
+		},
+		"through2": {
+			"version": "0.6.5",
+			"resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
+			"integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=",
+			"dev": true,
+			"requires": {
+				"readable-stream": ">=1.0.33-1 <1.1.0-0",
+				"xtend": ">=4.0.0 <4.1.0-0"
+			},
+			"dependencies": {
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+					"dev": true
+				},
+				"readable-stream": {
+					"version": "1.0.34",
+					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+					"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+					"dev": true,
+					"requires": {
+						"core-util-is": "~1.0.0",
+						"inherits": "~2.0.1",
+						"isarray": "0.0.1",
+						"string_decoder": "~0.10.x"
+					}
+				},
+				"string_decoder": {
+					"version": "0.10.31",
+					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+					"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+					"dev": true
+				}
+			}
+		},
+		"through2-filter": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz",
+			"integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=",
+			"dev": true,
+			"requires": {
+				"through2": "~2.0.0",
+				"xtend": "~4.0.0"
+			},
+			"dependencies": {
+				"through2": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+					"integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "^2.1.5",
+						"xtend": "~4.0.1"
+					}
+				}
+			}
+		},
+		"time-stamp": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
+			"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=",
+			"dev": true
+		},
+		"timed-out": {
+			"version": "3.1.3",
+			"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz",
+			"integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=",
+			"dev": true
+		},
+		"tiny-lr": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz",
+			"integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==",
+			"dev": true,
+			"requires": {
+				"body": "^5.1.0",
+				"debug": "^3.1.0",
+				"faye-websocket": "~0.10.0",
+				"livereload-js": "^2.3.0",
+				"object-assign": "^4.1.0",
+				"qs": "^6.4.0"
+			},
+			"dependencies": {
+				"debug": {
+					"version": "3.1.0",
+					"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+					"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+					"dev": true,
+					"requires": {
+						"ms": "2.0.0"
+					}
+				}
+			}
+		},
+		"tmp": {
+			"version": "0.0.33",
+			"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+			"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+			"dev": true,
+			"requires": {
+				"os-tmpdir": "~1.0.2"
+			}
+		},
+		"to-absolute-glob": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz",
+			"integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=",
+			"dev": true,
+			"requires": {
+				"extend-shallow": "^2.0.1"
+			},
+			"dependencies": {
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				}
+			}
+		},
+		"to-buffer": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
+			"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
+			"dev": true
+		},
+		"to-object-path": {
+			"version": "0.3.0",
+			"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+			"integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+			"dev": true,
+			"requires": {
+				"kind-of": "^3.0.2"
+			},
+			"dependencies": {
+				"kind-of": {
+					"version": "3.2.2",
+					"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+					"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+					"dev": true,
+					"requires": {
+						"is-buffer": "^1.1.5"
+					}
+				}
+			}
+		},
+		"to-regex": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+			"integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+			"dev": true,
+			"requires": {
+				"define-property": "^2.0.2",
+				"extend-shallow": "^3.0.2",
+				"regex-not": "^1.0.2",
+				"safe-regex": "^1.1.0"
+			}
+		},
+		"to-regex-range": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+			"integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+			"dev": true,
+			"requires": {
+				"is-number": "^3.0.0",
+				"repeat-string": "^1.6.1"
+			}
+		},
+		"tough-cookie": {
+			"version": "2.4.3",
+			"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+			"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+			"dev": true,
+			"requires": {
+				"psl": "^1.1.24",
+				"punycode": "^1.4.1"
+			}
+		},
+		"trim-newlines": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+			"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+			"dev": true
+		},
+		"trim-repeated": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
+			"integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=",
+			"dev": true,
+			"requires": {
+				"escape-string-regexp": "^1.0.2"
+			}
+		},
+		"true-case-path": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz",
+			"integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=",
+			"dev": true,
+			"requires": {
+				"glob": "^6.0.4"
+			},
+			"dependencies": {
+				"glob": {
+					"version": "6.0.4",
+					"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+					"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+					"dev": true,
+					"requires": {
+						"inflight": "^1.0.4",
+						"inherits": "2",
+						"minimatch": "2 || 3",
+						"once": "^1.3.0",
+						"path-is-absolute": "^1.0.0"
+					}
+				}
+			}
+		},
+		"tunnel-agent": {
+			"version": "0.4.3",
+			"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
+			"integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
+			"dev": true
+		},
+		"tweetnacl": {
+			"version": "0.14.5",
+			"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+			"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+			"dev": true,
+			"optional": true
+		},
+		"typedarray": {
+			"version": "0.0.6",
+			"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+			"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+			"dev": true
+		},
+		"uglify-js": {
+			"version": "3.0.28",
+			"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.0.28.tgz",
+			"integrity": "sha512-0h/qGay016GG2lVav3Kz174F3T2Vjlz2v6HCt+WDQpoXfco0hWwF5gHK9yh88mUYvIC+N7Z8NT8WpjSp1yoqGA==",
+			"dev": true,
+			"requires": {
+				"commander": "~2.11.0",
+				"source-map": "~0.5.1"
+			},
+			"dependencies": {
+				"commander": {
+					"version": "2.11.0",
+					"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+					"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+					"dev": true
+				},
+				"source-map": {
+					"version": "0.5.7",
+					"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+					"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+					"dev": true
+				}
+			}
+		},
+		"underscore": {
+			"version": "1.8.3",
+			"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
+			"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
+			"dev": true
+		},
+		"underscore.string": {
+			"version": "3.3.4",
+			"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz",
+			"integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=",
+			"dev": true,
+			"requires": {
+				"sprintf-js": "^1.0.3",
+				"util-deprecate": "^1.0.2"
+			}
+		},
+		"unicode-5.2.0": {
+			"version": "0.7.5",
+			"resolved": "https://registry.npmjs.org/unicode-5.2.0/-/unicode-5.2.0-0.7.5.tgz",
+			"integrity": "sha512-KVGLW1Bri30x00yv4HNM8kBxoqFXr0Sbo55735nvrlsx4PYBZol3UtoWgO492fSwmsetzPEZzy73rbU8OGXJcA==",
+			"dev": true
+		},
+		"union-value": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+			"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+			"dev": true,
+			"requires": {
+				"arr-union": "^3.1.0",
+				"get-value": "^2.0.6",
+				"is-extendable": "^0.1.1",
+				"set-value": "^0.4.3"
+			},
+			"dependencies": {
+				"extend-shallow": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+					"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+					"dev": true,
+					"requires": {
+						"is-extendable": "^0.1.0"
+					}
+				},
+				"set-value": {
+					"version": "0.4.3",
+					"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+					"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+					"dev": true,
+					"requires": {
+						"extend-shallow": "^2.0.1",
+						"is-extendable": "^0.1.1",
+						"is-plain-object": "^2.0.1",
+						"to-object-path": "^0.3.0"
+					}
+				}
+			}
+		},
+		"uniq": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+			"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+			"dev": true
+		},
+		"unique-stream": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz",
+			"integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=",
+			"dev": true,
+			"requires": {
+				"json-stable-stringify": "^1.0.0",
+				"through2-filter": "^2.0.0"
+			}
+		},
+		"unset-value": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+			"integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+			"dev": true,
+			"requires": {
+				"has-value": "^0.3.1",
+				"isobject": "^3.0.0"
+			},
+			"dependencies": {
+				"has-value": {
+					"version": "0.3.1",
+					"resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+					"integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+					"dev": true,
+					"requires": {
+						"get-value": "^2.0.3",
+						"has-values": "^0.1.4",
+						"isobject": "^2.0.0"
+					},
+					"dependencies": {
+						"isobject": {
+							"version": "2.1.0",
+							"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+							"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+							"dev": true,
+							"requires": {
+								"isarray": "1.0.0"
+							}
+						}
+					}
+				},
+				"has-values": {
+					"version": "0.1.4",
+					"resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+					"integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+					"dev": true
+				}
+			}
+		},
+		"untildify": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz",
+			"integrity": "sha1-F+soB5h/dpUunASF/DEdBqgmouA=",
+			"dev": true,
+			"requires": {
+				"os-homedir": "^1.0.0"
+			}
+		},
+		"unzip-response": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz",
+			"integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=",
+			"dev": true
+		},
+		"uri-js": {
+			"version": "4.2.2",
+			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+			"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+			"dev": true,
+			"requires": {
+				"punycode": "^2.1.0"
+			},
+			"dependencies": {
+				"punycode": {
+					"version": "2.1.1",
+					"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+					"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+					"dev": true
+				}
+			}
+		},
+		"uri-path": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz",
+			"integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=",
+			"dev": true
+		},
+		"urix": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+			"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+			"dev": true
+		},
+		"url-parse-lax": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
+			"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
+			"dev": true,
+			"requires": {
+				"prepend-http": "^1.0.1"
+			}
+		},
+		"url-regex": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz",
+			"integrity": "sha1-260eDJ4p4QXdCx8J9oYvf9tIJyQ=",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"ip-regex": "^1.0.1"
+			}
+		},
+		"use": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+			"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+			"dev": true
+		},
+		"util-deprecate": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+			"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+			"dev": true
+		},
+		"uuid": {
+			"version": "3.3.2",
+			"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+			"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+			"dev": true
+		},
+		"vali-date": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
+			"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
+			"dev": true
+		},
+		"validate-npm-package-license": {
+			"version": "3.0.4",
+			"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+			"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+			"dev": true,
+			"requires": {
+				"spdx-correct": "^3.0.0",
+				"spdx-expression-parse": "^3.0.0"
+			}
+		},
+		"verror": {
+			"version": "1.10.0",
+			"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+			"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+			"dev": true,
+			"requires": {
+				"assert-plus": "^1.0.0",
+				"core-util-is": "1.0.2",
+				"extsprintf": "^1.2.0"
+			}
+		},
+		"vinyl": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz",
+			"integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=",
+			"dev": true,
+			"requires": {
+				"clone": "^1.0.0",
+				"clone-stats": "^0.0.1",
+				"replace-ext": "0.0.1"
+			},
+			"dependencies": {
+				"replace-ext": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz",
+					"integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=",
+					"dev": true
+				}
+			}
+		},
+		"vinyl-assign": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/vinyl-assign/-/vinyl-assign-1.2.1.tgz",
+			"integrity": "sha1-TRmIkbVRWRHXcajNnFSApGoHSkU=",
+			"dev": true,
+			"requires": {
+				"object-assign": "^4.0.1",
+				"readable-stream": "^2.0.0"
+			}
+		},
+		"vinyl-fs": {
+			"version": "2.4.4",
+			"resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz",
+			"integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=",
+			"dev": true,
+			"requires": {
+				"duplexify": "^3.2.0",
+				"glob-stream": "^5.3.2",
+				"graceful-fs": "^4.0.0",
+				"gulp-sourcemaps": "1.6.0",
+				"is-valid-glob": "^0.3.0",
+				"lazystream": "^1.0.0",
+				"lodash.isequal": "^4.0.0",
+				"merge-stream": "^1.0.0",
+				"mkdirp": "^0.5.0",
+				"object-assign": "^4.0.0",
+				"readable-stream": "^2.0.4",
+				"strip-bom": "^2.0.0",
+				"strip-bom-stream": "^1.0.0",
+				"through2": "^2.0.0",
+				"through2-filter": "^2.0.0",
+				"vali-date": "^1.0.0",
+				"vinyl": "^1.0.0"
+			},
+			"dependencies": {
+				"through2": {
+					"version": "2.0.3",
+					"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+					"integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+					"dev": true,
+					"requires": {
+						"readable-stream": "^2.1.5",
+						"xtend": "~4.0.1"
+					}
+				}
+			}
+		},
+		"ware": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz",
+			"integrity": "sha1-0bFPOdLiy0q4xAmPdW/ksWTkc9Q=",
+			"dev": true,
+			"requires": {
+				"wrap-fn": "^0.1.0"
+			}
+		},
+		"websocket-driver": {
+			"version": "0.7.0",
+			"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
+			"integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
+			"dev": true,
+			"requires": {
+				"http-parser-js": ">=0.4.0",
+				"websocket-extensions": ">=0.1.1"
+			}
+		},
+		"websocket-extensions": {
+			"version": "0.1.3",
+			"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+			"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
+			"dev": true
+		},
+		"whet.extend": {
+			"version": "0.9.9",
+			"resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz",
+			"integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=",
+			"dev": true,
+			"optional": true
+		},
+		"which": {
+			"version": "1.3.1",
+			"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+			"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+			"dev": true,
+			"requires": {
+				"isexe": "^2.0.0"
+			}
+		},
+		"which-module": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+			"integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+			"dev": true
+		},
+		"wide-align": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+			"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+			"dev": true,
+			"requires": {
+				"string-width": "^1.0.2 || 2"
+			}
+		},
+		"window-size": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz",
+			"integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=",
+			"dev": true
+		},
+		"winston": {
+			"version": "2.4.4",
+			"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz",
+			"integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==",
+			"dev": true,
+			"optional": true,
+			"requires": {
+				"async": "~1.0.0",
+				"colors": "1.0.x",
+				"cycle": "1.0.x",
+				"eyes": "0.1.x",
+				"isstream": "0.1.x",
+				"stack-trace": "0.0.x"
+			},
+			"dependencies": {
+				"async": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz",
+					"integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=",
+					"dev": true,
+					"optional": true
+				},
+				"colors": {
+					"version": "1.0.3",
+					"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
+					"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
+					"dev": true,
+					"optional": true
+				}
+			}
+		},
+		"wordwrap": {
+			"version": "0.0.3",
+			"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+			"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+			"dev": true
+		},
+		"wrap-ansi": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+			"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+			"dev": true,
+			"requires": {
+				"string-width": "^1.0.1",
+				"strip-ansi": "^3.0.1"
+			},
+			"dependencies": {
+				"is-fullwidth-code-point": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+					"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+					"dev": true,
+					"requires": {
+						"number-is-nan": "^1.0.0"
+					}
+				},
+				"string-width": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+					"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+					"dev": true,
+					"requires": {
+						"code-point-at": "^1.0.0",
+						"is-fullwidth-code-point": "^1.0.0",
+						"strip-ansi": "^3.0.0"
+					}
+				}
+			}
+		},
+		"wrap-fn": {
+			"version": "0.1.5",
+			"resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz",
+			"integrity": "sha1-8htuQQFv9KfjFyDbxjoJAWvfmEU=",
+			"dev": true,
+			"requires": {
+				"co": "3.1.0"
+			}
+		},
+		"wrappy": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+			"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+			"dev": true
+		},
+		"write": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+			"integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+			"dev": true,
+			"requires": {
+				"mkdirp": "^0.5.1"
+			}
+		},
+		"write-file-stdout": {
+			"version": "0.0.2",
+			"resolved": "https://registry.npmjs.org/write-file-stdout/-/write-file-stdout-0.0.2.tgz",
+			"integrity": "sha1-wlLXx8WxtAKJdjDjRTx7/mkNnKE=",
+			"dev": true
+		},
+		"xmlbuilder": {
+			"version": "8.2.2",
+			"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
+			"integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=",
+			"dev": true
+		},
+		"xmlrpc": {
+			"version": "1.3.2",
+			"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",
+			"integrity": "sha1-JrLqNHhI0Ciqx+dRS1NRl23j6D0=",
+			"dev": true,
+			"requires": {
+				"sax": "1.2.x",
+				"xmlbuilder": "8.2.x"
+			}
+		},
+		"xtend": {
+			"version": "4.0.1",
+			"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+			"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+			"dev": true
+		},
+		"y18n": {
+			"version": "3.2.1",
+			"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+			"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+			"dev": true
+		},
+		"yallist": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+			"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+			"dev": true
+		},
+		"yargs": {
+			"version": "7.1.0",
+			"resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+			"integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+			"dev": true,
+			"requires": {
+				"camelcase": "^3.0.0",
+				"cliui": "^3.2.0",
+				"decamelize": "^1.1.1",
+				"get-caller-file": "^1.0.1",
+				"os-locale": "^1.4.0",
+				"read-pkg-up": "^1.0.1",
+				"require-directory": "^2.1.1",
+				"require-main-filename": "^1.0.1",
+				"set-blocking": "^2.0.0",
+				"string-width": "^1.0.2",
+				"which-module": "^1.0.0",
+				"y18n": "^3.2.1",
+				"yargs-parser": "^5.0.0"
+			},
+			"dependencies": {
+				"camelcase": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+					"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+					"dev": true
+				},
+				"is-fullwidth-code-point": {
+					"version": "1.0.0",
+					"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+					"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+					"dev": true,
+					"requires": {
+						"number-is-nan": "^1.0.0"
+					}
+				},
+				"string-width": {
+					"version": "1.0.2",
+					"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+					"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+					"dev": true,
+					"requires": {
+						"code-point-at": "^1.0.0",
+						"is-fullwidth-code-point": "^1.0.0",
+						"strip-ansi": "^3.0.0"
+					}
+				}
+			}
+		},
+		"yargs-parser": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+			"integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+			"dev": true,
+			"requires": {
+				"camelcase": "^3.0.0"
+			},
+			"dependencies": {
+				"camelcase": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+					"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+					"dev": true
+				}
+			}
+		},
+		"yauzl": {
+			"version": "2.10.0",
+			"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+			"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+			"dev": true,
+			"requires": {
+				"buffer-crc32": "~0.2.3",
+				"fd-slicer": "~1.1.0"
+			}
+		}
+	}
+}
Index: /tags/4.0.0-beta1/package.json
===================================================================
--- /tags/4.0.0-beta1/package.json	(revision 12265)
+++ /tags/4.0.0-beta1/package.json	(revision 12265)
@@ -0,0 +1,60 @@
+{
+	"author": "The BuddyPress Contributors",
+	"bugs": {
+		"url": "https://buddypress.trac.wordpress.org/"
+	},
+	"description": "BuddyPress adds community features to WordPress. Member Profiles, Activity Streams, Direct Messaging, Notifications, and more!",
+	"devDependencies": {
+		"@wordpress/browserslist-config": "~2.1.4",
+		"autoprefixer": "~8.5.1",
+		"grunt": "~1.0.2",
+		"grunt-check-dependencies": "~1.0.0",
+		"grunt-checktextdomain": "~1.0.1",
+		"grunt-contrib-clean": "~1.1.0",
+		"grunt-contrib-copy": "~1.0.0",
+		"grunt-contrib-cssmin": "~2.2.1",
+		"grunt-contrib-imagemin": "~2.0.1",
+		"grunt-contrib-jshint": "~1.1.0",
+		"grunt-contrib-uglify": "~3.0.1",
+		"grunt-contrib-watch": "~1.1.0",
+		"grunt-exec": "~2.0.0",
+		"grunt-jsvalidate": "~0.2.2",
+		"grunt-legacy-util": "~1.1.1",
+		"grunt-patch-wordpress": "~1.0.0",
+		"grunt-phplint": "~0.1.0",
+		"grunt-postcss": "~0.9.0",
+		"grunt-rtlcss": "~2.0.1",
+		"grunt-sass": "~2.0.0",
+		"grunt-stylelint": "~0.8.0",
+		"grunt-wp-i18n": "~1.0.2",
+		"matchdep": "~1.0.1",
+		"postcss-scss": "~1.0.5",
+		"stylelint": "~7.10.1",
+		"stylelint-config-wordpress": "~11.0.0"
+	},
+	"engines": {
+		"node": ">=6.9.1"
+	},
+	"keywords": [
+		"activity",
+		"community",
+		"friends",
+		"groups",
+		"messaging",
+		"notifications",
+		"profiles",
+		"social network"
+	],
+	"homepage": "https://buddypress.org",
+	"license": "GPL-2.0-or-later",
+	"name": "buddypress",
+	"repository": {
+		"type": "svn",
+		"url": "https://buddypress.svn.wordpress.org/trunk/"
+	},
+	"version": "4.0.0-beta1",
+	"dependencies": {},
+	"browserslist": [
+		"extends @wordpress/browserslist-config"
+	]
+}
Index: /tags/4.0.0-beta1/phpunit.xml.dist
===================================================================
--- /tags/4.0.0-beta1/phpunit.xml.dist	(revision 12265)
+++ /tags/4.0.0-beta1/phpunit.xml.dist	(revision 12265)
@@ -0,0 +1,14 @@
+<phpunit
+	bootstrap="tests/phpunit/bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">tests/phpunit/testcases/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
Index: /tags/4.0.0-beta1/src/bp-activity/actions/delete.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/delete.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/delete.php	(revision 12265)
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Activity: Delete action
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * Delete specific activity item and redirect to previous page.
+ *
+ * @since 1.1.0
+ *
+ * @param int $activity_id Activity id to be deleted. Defaults to 0.
+ * @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' ) )
+		return false;
+
+	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 ) )
+		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() );
+}
+add_action( 'bp_actions', 'bp_activity_action_delete_activity' );
Index: /tags/4.0.0-beta1/src/bp-activity/actions/favorite.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/favorite.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/favorite.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Activity: Favorite action
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * 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 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 ) );
+}
+add_action( 'bp_actions', 'bp_activity_action_mark_favorite' );
Index: /tags/4.0.0-beta1/src/bp-activity/actions/feeds.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/feeds.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/feeds.php	(revision 12265)
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Activity: RSS feed actions
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * 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;
+	}
+
+	// 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() )
+		)
+	) );
+}
+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' );
Index: /tags/4.0.0-beta1/src/bp-activity/actions/post.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/post.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/post.php	(revision 12265)
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Activity: Post action
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * Post user/group activity update.
+ *
+ * @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 );
+	}
+
+	// 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() );
+}
+add_action( 'bp_actions', 'bp_activity_action_post_update' );
Index: /tags/4.0.0-beta1/src/bp-activity/actions/reply.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/reply.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/reply.php	(revision 12265)
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Activity: Reply action
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * Post new activity comment.
+ *
+ * @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 );
+	}
+
+	$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 );
+}
+add_action( 'bp_actions', 'bp_activity_action_post_comment' );
Index: /tags/4.0.0-beta1/src/bp-activity/actions/spam.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/spam.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/spam.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Activity: Spam action
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * Mark specific activity item as spam and redirect to previous page.
+ *
+ * @since 1.6.0
+ *
+ * @param int $activity_id Activity id to be deleted. Defaults to 0.
+ * @return bool 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 ) )
+		return false;
+
+	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 ) )
+		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() );
+}
+add_action( 'bp_actions', 'bp_activity_action_spam_activity' );
Index: /tags/4.0.0-beta1/src/bp-activity/actions/unfavorite.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/actions/unfavorite.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/actions/unfavorite.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Activity: Unfavorite action
+ *
+ * @package BuddyPress
+ * @subpackage ActivityActions
+ * @since 3.0.0
+ */
+
+/**
+ * Remove activity from favorites.
+ *
+ * @since 1.2.0
+ *
+ * @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' );
Index: /tags/4.0.0-beta1/src/bp-activity/admin/css/admin-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/admin/css/admin-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/admin/css/admin-rtl.css	(revision 12265)
@@ -0,0 +1,105 @@
+.akismet-status {
+	float: left;
+}
+
+.akismet-status a {
+	color: #aaa;
+	font-style: italic;
+}
+
+.akismet-history {
+	margin: 13px;
+}
+
+.akismet-history div {
+	margin-bottom: 13px;
+}
+
+.akismet-history span {
+	color: #999;
+}
+
+#wp-bp-activities-wrap {
+	padding: 5px 0;
+}
+
+#bp-activities {
+	height: 120px;
+}
+
+#bp-replyhead {
+	font-size: 1em;
+	line-height: 1.4;
+	margin: 0;
+}
+
+#bp-replysubmit {
+	margin: 0;
+	padding: 0 0 3px;
+	text-align: center;
+}
+
+#bp-replysubmit .error {
+	color: #f00;
+	line-height: 21px;
+	text-align: center;
+	vertical-align: center;
+}
+
+#bp-replysubmit img.waiting {
+	float: left;
+	padding: 4px 10px 0;
+	vertical-align: top;
+}
+
+#bp-activities-form .column-response img {
+	float: right;
+	margin-bottom: 5px;
+	margin-left: 10px;
+	margin-top: 1px;
+}
+
+.activity-errors {
+	list-style-type: disc;
+	margin-right: 2em;
+}
+
+#bp_activity_action div.inside,
+#bp_activity_content div.inside {
+	line-height: 0;
+}
+
+#bp_activity_action h3,
+#bp_activity_content h3 {
+	cursor: auto;
+}
+
+#bp_activity_action td.mceIframeContainer,
+#bp_activity_content td.mceIframeContainer {
+	background-color: #fff;
+}
+
+#post-body #bp-activities-action_resize,
+#post-body #bp-activities-content_resize {
+	position: inherit;
+	margin-top: -2px;
+}
+
+#bp_activity_link input {
+	width: 99%;
+}
+
+#bp-activities-primaryid {
+	margin-bottom: 1em;
+}
+
+.column-action {
+	width: 12%;
+}
+
+@media screen and (max-width: 782px) {
+
+	body.toplevel_page_bp-activity .wp-list-table tr:not(.inline-edit-row):not(.no-items) td:not(.check-column) {
+		display: table-cell;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/admin/css/admin.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/admin/css/admin.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/admin/css/admin.css	(revision 12265)
@@ -0,0 +1,105 @@
+.akismet-status {
+	float: right;
+}
+
+.akismet-status a {
+	color: #aaa;
+	font-style: italic;
+}
+
+.akismet-history {
+	margin: 13px;
+}
+
+.akismet-history div {
+	margin-bottom: 13px;
+}
+
+.akismet-history span {
+	color: #999;
+}
+
+#wp-bp-activities-wrap {
+	padding: 5px 0;
+}
+
+#bp-activities {
+	height: 120px;
+}
+
+#bp-replyhead {
+	font-size: 1em;
+	line-height: 1.4;
+	margin: 0;
+}
+
+#bp-replysubmit {
+	margin: 0;
+	padding: 0 0 3px;
+	text-align: center;
+}
+
+#bp-replysubmit .error {
+	color: #f00;
+	line-height: 21px;
+	text-align: center;
+	vertical-align: center;
+}
+
+#bp-replysubmit img.waiting {
+	float: right;
+	padding: 4px 10px 0;
+	vertical-align: top;
+}
+
+#bp-activities-form .column-response img {
+	float: left;
+	margin-bottom: 5px;
+	margin-right: 10px;
+	margin-top: 1px;
+}
+
+.activity-errors {
+	list-style-type: disc;
+	margin-left: 2em;
+}
+
+#bp_activity_action div.inside,
+#bp_activity_content div.inside {
+	line-height: 0;
+}
+
+#bp_activity_action h3,
+#bp_activity_content h3 {
+	cursor: auto;
+}
+
+#bp_activity_action td.mceIframeContainer,
+#bp_activity_content td.mceIframeContainer {
+	background-color: #fff;
+}
+
+#post-body #bp-activities-action_resize,
+#post-body #bp-activities-content_resize {
+	position: inherit;
+	margin-top: -2px;
+}
+
+#bp_activity_link input {
+	width: 99%;
+}
+
+#bp-activities-primaryid {
+	margin-bottom: 1em;
+}
+
+.column-action {
+	width: 12%;
+}
+
+@media screen and (max-width: 782px) {
+
+	body.toplevel_page_bp-activity .wp-list-table tr:not(.inline-edit-row):not(.no-items) td:not(.check-column) {
+		display: table-cell;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/admin/js/admin.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/admin/js/admin.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/admin/js/admin.js	(revision 12265)
@@ -0,0 +1,175 @@
+/* global bp_activity_admin_vars, postboxes, wpAjax */
+(function( $ ) {
+
+/**
+ * Activity reply object for the activity index screen
+ *
+ * @since 1.6.0
+ */
+var activityReply = {
+
+	/**
+	 * Attach event handler functions to the relevant elements.
+	 *
+	 * @since 1.6.0
+	 */
+	init : function() {
+		$(document).on( 'click', '.row-actions a.reply',              activityReply.open );
+		$(document).on( 'click', '#bp-activities-container a.cancel', activityReply.close );
+		$(document).on( 'click', '#bp-activities-container a.save',   activityReply.send );
+
+		// Close textarea on escape
+		$(document).on( 'keyup', '#bp-activities:visible', function( e ) {
+			if ( 27 === e.which ) {
+				activityReply.close();
+			}
+		});
+	},
+
+	/**
+	 * Reveals the entire row when "reply" is pressed.
+	 *
+	 * @since 1.6.0
+	 */
+	open : function() {
+		// Hide the container row, and move it to the new location
+		var box = $( '#bp-activities-container' ).hide();
+		$( this ).parents( 'tr' ).after( box );
+
+		// Fade the whole row in, and set focus on the text area.
+		box.fadeIn( '300' );
+		$( '#bp-activities' ).focus();
+
+		return false;
+	},
+
+	/**
+	 * Hide and reset the entire row when "cancel", or escape, are pressed.
+	 *
+	 * @since 1.6.0
+	 */
+	close : function() {
+		// Hide the container row
+		$('#bp-activities-container').fadeOut( '200', function () {
+
+			// Empty and unfocus the text area
+			$( '#bp-activities' ).val( '' ).blur();
+
+			// Remove any error message and disable the spinner
+			$( '#bp-replysubmit .error' ).html( '' ).hide();
+			$( '#bp-replysubmit .waiting' ).hide();
+		});
+
+		return false;
+	},
+
+	/**
+	 * Submits "form" via AJAX back to WordPress.
+	 *
+	 * @since 1.6.0
+	 */
+	send : function() {
+		// Hide any existing error message, and show the loading spinner
+		$( '#bp-replysubmit .error' ).hide();
+		$( '#bp-replysubmit .waiting' ).show();
+
+		// Grab the nonce
+		var reply = {};
+		reply['_ajax_nonce-bp-activity-admin-reply'] = $( '#bp-activities-container input[name="_ajax_nonce-bp-activity-admin-reply"]' ).val();
+
+		// Get the rest of the data
+		reply.action    = 'bp-activity-admin-reply';
+		reply.content   = $( '#bp-activities' ).val();
+		reply.parent_id = $( '#bp-activities-container' ).prev().data( 'parent_id' );
+		reply.root_id   = $( '#bp-activities-container' ).prev().data( 'root_id' );
+
+		// Make the AJAX call
+		$.ajax({
+			data    : reply,
+			type    : 'POST',
+			url     : ajaxurl,
+
+			// Callbacks
+			error   : function( r ) { activityReply.error( r ); },
+			success : function( r ) { activityReply.show( r ); }
+		});
+
+		return false;
+	},
+
+	/**
+	 * send() error message handler
+	 *
+	 * @since 1.6.0
+	 */
+	error : function( r ) {
+		var er = r.statusText;
+		$('#bp-replysubmit .waiting').hide();
+
+		if ( r.responseText ) {
+			er = r.responseText.replace( /<.[^<>]*?>/g, '' );
+		}
+
+		if ( er ) {
+			$('#bp-replysubmit .error').html( er ).show();
+		}
+	},
+
+	/**
+	 * send() success handler
+	 *
+	 * @since 1.6.0
+	 */
+	show : function ( xml ) {
+		var bg, id, response;
+
+		// Handle any errors in the response
+		if ( typeof( xml ) === 'string' ) {
+			activityReply.error( { 'responseText': xml } );
+			return false;
+		}
+
+		response = wpAjax.parseAjaxResponse( xml );
+		if ( response.errors ) {
+			activityReply.error( { 'responseText': wpAjax.broken } );
+			return false;
+		}
+		response = response.responses[0];
+
+		// Close and reset the reply row, and add the new Activity item into the list.
+		$('#bp-activities-container').fadeOut( '200', function () {
+
+			// Empty and unfocus the text area
+			$( '#bp-activities' ).val( '' ).blur();
+
+			// Remove any error message and disable the spinner
+			$( '#bp-replysubmit .error' ).html( '' ).hide();
+			$( '#bp-replysubmit .waiting' ).hide();
+
+			// Insert new activity item
+			$( '#bp-activities-container' ).before( response.data );
+
+			// Get background colour and animate the flash
+			id = $( '#activity-' + response.id );
+			bg = id.closest( '.widefat' ).css( 'backgroundColor' );
+			id.animate( { 'backgroundColor': '#CEB' }, 300 ).animate( { 'backgroundColor': bg }, 300 );
+		});
+	}
+};
+
+$(document).ready( function () {
+	// Create the Activity reply object after domready event
+	activityReply.init();
+
+	// On the edit screen, unload the close/open toggle js for the action & content metaboxes
+	$( '#bp_activity_action h3, #bp_activity_content h3' ).unbind( 'click' );
+
+	// redo the post box toggles to reset the one made by comment.js in favor
+	// of activity administration page id so that metaboxes are still collapsible
+	// in single Activity Administration screen.
+	if ( typeof postboxes !== 'undefined' ) {
+		postboxes.add_postbox_toggles( bp_activity_admin_vars.page );
+	}
+});
+
+})(jQuery);
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-admin.php	(revision 12265)
@@ -0,0 +1,1065 @@
+<?php
+/**
+ * BuddyPress Activity component admin screen.
+ *
+ * Props to WordPress core for the Comments admin screen, and its contextual
+ * help text, on which this implementation is heavily based.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityAdmin
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+// Include WP's list table class.
+if ( !class_exists( 'WP_List_Table' ) ) require( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
+
+// Per_page screen option. Has to be hooked in extremely early.
+if ( is_admin() && ! empty( $_REQUEST['page'] ) && 'bp-activity' == $_REQUEST['page'] )
+	add_filter( 'set-screen-option', 'bp_activity_admin_screen_options', 10, 3 );
+
+/**
+ * Register the Activity component admin screen.
+ *
+ * @since 1.6.0
+ */
+function bp_activity_add_admin_menu() {
+
+	// Add our screen.
+	$hook = add_menu_page(
+		_x( 'Activity', 'Admin Dashbord SWA page title', 'buddypress' ),
+		_x( 'Activity', 'Admin Dashbord SWA menu', 'buddypress' ),
+		'bp_moderate',
+		'bp-activity',
+		'bp_activity_admin',
+		'div'
+	);
+
+	// Hook into early actions to load custom CSS and our init handler.
+	add_action( "load-$hook", 'bp_activity_admin_load' );
+}
+add_action( bp_core_admin_hook(), 'bp_activity_add_admin_menu' );
+
+/**
+ * Add activity component to custom menus array.
+ *
+ * Several BuddyPress components have top-level menu items in the Dashboard,
+ * which all appear together in the middle of the Dashboard menu. This function
+ * adds the Activity page to the array of these menu items.
+ *
+ * @since 1.7.0
+ *
+ * @param array $custom_menus The list of top-level BP menu items.
+ * @return array $custom_menus List of top-level BP menu items, with Activity added.
+ */
+function bp_activity_admin_menu_order( $custom_menus = array() ) {
+	array_push( $custom_menus, 'bp-activity' );
+	return $custom_menus;
+}
+add_filter( 'bp_admin_menu_order', 'bp_activity_admin_menu_order' );
+
+/**
+ * AJAX receiver for Activity replies via the admin screen.
+ *
+ * Processes requests to add new activity comments, and echoes HTML for a new
+ * table row.
+ *
+ * @since 1.6.0
+ */
+function bp_activity_admin_reply() {
+	// Check nonce.
+	check_ajax_referer( 'bp-activity-admin-reply', '_ajax_nonce-bp-activity-admin-reply' );
+
+	$parent_id = ! empty( $_REQUEST['parent_id'] ) ? (int) $_REQUEST['parent_id'] : 0;
+	$root_id   = ! empty( $_REQUEST['root_id'] )   ? (int) $_REQUEST['root_id']   : 0;
+
+	// $parent_id is required
+	if ( empty( $parent_id ) )
+		die( '-1' );
+
+	// If $root_id not set (e.g. for root items), use $parent_id.
+	if ( empty( $root_id ) )
+		$root_id = $parent_id;
+
+	// Check that a reply has been entered.
+	if ( empty( $_REQUEST['content'] ) )
+		die( __( 'ERROR: Please type a reply.', 'buddypress' ) );
+
+	// Check parent activity exists.
+	$parent_activity = new BP_Activity_Activity( $parent_id );
+	if ( empty( $parent_activity->component ) )
+		die( __( 'ERROR: The item you are trying to reply to cannot be found, or it has been deleted.', 'buddypress' ) );
+
+	// @todo: Check if user is allowed to create new activity items
+	// if ( ! current_user_can( 'bp_new_activity' ) )
+	if ( ! bp_current_user_can( 'bp_moderate' ) )
+		die( '-1' );
+
+	// Add new activity comment.
+	$new_activity_id = bp_activity_new_comment( array(
+		'activity_id' => $root_id,              // ID of the root activity item.
+		'content'     => $_REQUEST['content'],
+		'parent_id'   => $parent_id,            // ID of a parent comment.
+	) );
+
+	// Fetch the new activity item, as we need it to create table markup to return.
+	$new_activity = new BP_Activity_Activity( $new_activity_id );
+
+	// This needs to be set for the BP_Activity_List_Table constructor to work.
+	set_current_screen( 'toplevel_page_bp-activity' );
+
+	// Set up an output buffer.
+	ob_start();
+	$list_table = new BP_Activity_List_Table();
+	$list_table->single_row( (array) $new_activity );
+
+	// Get table markup.
+	$response =  array(
+		'data'     => ob_get_contents(),
+		'id'       => $new_activity_id,
+		'position' => -1,
+		'what'     => 'bp_activity',
+	);
+	ob_end_clean();
+
+	// Send response.
+	$r = new WP_Ajax_Response();
+	$r->add( $response );
+	$r->send();
+
+	exit();
+}
+add_action( 'wp_ajax_bp-activity-admin-reply', 'bp_activity_admin_reply' );
+
+/**
+ * Handle save/update of screen options for the Activity component admin screen.
+ *
+ * @since 1.6.0
+ *
+ * @param string $value     Will always be false unless another plugin filters it first.
+ * @param string $option    Screen option name.
+ * @param string $new_value Screen option form value.
+ * @return string|int Option value. False to abandon update.
+ */
+function bp_activity_admin_screen_options( $value, $option, $new_value ) {
+	if ( 'toplevel_page_bp_activity_per_page' != $option && 'toplevel_page_bp_activity_network_per_page' != $option )
+		return $value;
+
+	// Per page.
+	$new_value = (int) $new_value;
+	if ( $new_value < 1 || $new_value > 999 )
+		return $value;
+
+	return $new_value;
+}
+
+/**
+ * Hide the advanced edit meta boxes by default, so we don't clutter the screen.
+ *
+ * @since 1.6.0
+ *
+ * @param array     $hidden Array of items to hide.
+ * @param WP_Screen $screen Screen identifier.
+ * @return array Hidden Meta Boxes.
+ */
+function bp_activity_admin_edit_hidden_metaboxes( $hidden, $screen ) {
+	if ( empty( $screen->id ) || 'toplevel_page_bp-activity' !== $screen->id && 'toplevel_page_bp-activity-network' !== $screen->id ) {
+		return $hidden;
+	}
+
+	// Hide the primary link meta box by default.
+	$hidden  = array_merge( (array) $hidden, array( 'bp_activity_itemids', 'bp_activity_link', 'bp_activity_type', 'bp_activity_userid', ) );
+
+	/**
+	 * Filters default hidden metaboxes so plugins can alter list.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array     $hidden Default metaboxes to hide.
+	 * @param WP_Screen $screen Screen identifier.
+	 */
+	return apply_filters( 'bp_hide_meta_boxes', array_unique( $hidden ), $screen );
+}
+add_filter( 'default_hidden_meta_boxes', 'bp_activity_admin_edit_hidden_metaboxes', 10, 2 );
+
+/**
+ * Set up the Activity admin page.
+ *
+ * Does the following:
+ *   - Register contextual help and screen options for this admin page.
+ *   - Enqueues scripts and styles.
+ *   - Catches POST and GET requests related to Activity.
+ *
+ * @since 1.6.0
+ *
+ * @global object                 $bp                     BuddyPress global settings.
+ * @global BP_Activity_List_Table $bp_activity_list_table Activity screen list table.
+ */
+function bp_activity_admin_load() {
+	global $bp_activity_list_table;
+
+	$bp       = buddypress();
+	$doaction = bp_admin_list_table_current_bulk_action();
+	$min      = bp_core_get_minified_asset_suffix();
+
+	/**
+	 * Fires at top of Activity admin page.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $doaction Current $_GET action being performed in admin screen.
+	 */
+	do_action( 'bp_activity_admin_load', $doaction );
+
+	// Edit screen.
+	if ( 'edit' == $doaction && ! empty( $_GET['aid'] ) ) {
+		// Columns screen option.
+		add_screen_option( 'layout_columns', array( 'default' => 2, 'max' => 2, ) );
+
+		get_current_screen()->add_help_tab( array(
+			'id'      => 'bp-activity-edit-overview',
+			'title'   => __( 'Overview', 'buddypress' ),
+			'content' =>
+				'<p>' . __( 'You edit activities made on your site similar to the way you edit a comment. This is useful if you need to change which page the activity links to, or when you notice that the author has made a typographical error.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'The two big editing areas for the activity title and content are fixed in place, but you can reposition all the other boxes using drag and drop, and can minimize or expand them by clicking the title bar of each box. Use the Screen Options tab to unhide more boxes (Primary Item/Secondary Item, Link, Type, Author ID) or to choose a 1- or 2-column layout for this screen.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'You can also moderate the activity from this screen using the Status box, where you can also change the timestamp of the activity.', 'buddypress' ) . '</p>'
+		) );
+
+		get_current_screen()->add_help_tab( array(
+			'id'      => 'bp-activity-edit-advanced',
+			'title'   => __( 'Item, Link, Type', 'buddypress' ),
+			'content' =>
+				'<p>' . __( '<strong>Primary Item/Secondary Item</strong> - These identify the object that created the activity. For example, the fields could reference a comment left on a specific site. Some types of activity may only use one, or none, of these fields.', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<strong>Link</strong> - Used by some types of activity (blog posts and comments) to store a link back to the original content.', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<strong>Type</strong> - Each distinct kind of activity has its own type. For example, <code>created_group</code> is used when a group is created and <code>joined_group</code> is used when a user joins a group.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'For information about when and how BuddyPress uses all of these settings, see the Managing Activity link in the panel to the side.', 'buddypress' ) . '</p>'
+		) );
+
+		// Help panel - sidebar links.
+		get_current_screen()->set_help_sidebar(
+			'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+			'<p>' . __( '<a href="https://codex.buddypress.org/administrator-guide/activity-stream-management-panels/">Managing Activity</a>', 'buddypress' ) . '</p>' .
+			'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+		);
+
+		// Register metaboxes for the edit screen.
+		add_meta_box( 'submitdiv',           _x( 'Status', 'activity admin edit screen', 'buddypress' ), 'bp_activity_admin_edit_metabox_status', get_current_screen()->id, 'side', 'core' );
+		add_meta_box( 'bp_activity_itemids', _x( 'Primary Item/Secondary Item', 'activity admin edit screen', 'buddypress' ), 'bp_activity_admin_edit_metabox_itemids', get_current_screen()->id, 'normal', 'core' );
+		add_meta_box( 'bp_activity_link',    _x( 'Link', 'activity admin edit screen', 'buddypress' ), 'bp_activity_admin_edit_metabox_link', get_current_screen()->id, 'normal', 'core' );
+		add_meta_box( 'bp_activity_type',    _x( 'Type', 'activity admin edit screen', 'buddypress' ), 'bp_activity_admin_edit_metabox_type', get_current_screen()->id, 'normal', 'core' );
+		add_meta_box( 'bp_activity_userid',  _x( 'Author ID', 'activity admin edit screen', 'buddypress' ), 'bp_activity_admin_edit_metabox_userid', get_current_screen()->id, 'normal', 'core' );
+
+		/**
+		 * Fires after the registration of all of the default activity meta boxes.
+		 *
+		 * @since 2.4.0
+		 */
+		do_action( 'bp_activity_admin_meta_boxes' );
+
+		// Enqueue JavaScript files.
+		wp_enqueue_script( 'postbox' );
+		wp_enqueue_script( 'dashboard' );
+		wp_enqueue_script( 'comment' );
+
+	// Index screen.
+	} else {
+		// Create the Activity screen list table.
+		$bp_activity_list_table = new BP_Activity_List_Table();
+
+		// The per_page screen option.
+		add_screen_option( 'per_page', array( 'label' => _x( 'Activity', 'Activity items per page (screen options)', 'buddypress' )) );
+
+		// Help panel - overview text.
+		get_current_screen()->add_help_tab( array(
+			'id'      => 'bp-activity-overview',
+			'title'   => __( 'Overview', 'buddypress' ),
+			'content' =>
+				'<p>' . __( 'You can manage activities made on your site similar to the way you manage comments and other content. This screen is customizable in the same ways as other management screens, and you can act on activities using the on-hover action links or the Bulk Actions.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'There are many different types of activities. Some are generated automatically by BuddyPress and other plugins, and some are entered directly by a user in the form of status update. To help manage the different activity types, use the filter dropdown box to switch between them.', 'buddypress' ) . '</p>'
+		) );
+
+		// Help panel - moderation text.
+		get_current_screen()->add_help_tab( array(
+			'id'		=> 'bp-activity-moderating',
+			'title'		=> __( 'Moderating Activity', 'buddypress' ),
+			'content'	=>
+				'<p>' . __( 'In the <strong>Activity</strong> column, above each activity it says &#8220;Submitted on,&#8221; followed by the date and time the activity item was generated on your site. Clicking on the date/time link will take you to that activity on your live site. Hovering over any activity gives you options to reply, edit, spam mark, or delete that activity.', 'buddypress' ) . '</p>' .
+				'<p>' . __( "In the <strong>In Response To</strong> column, if the activity was in reply to another activity, it shows that activity's author's picture and name, and a link to that activity on your live site. If there is a small bubble, the number in it shows how many other activities are related to this one; these are usually comments. Clicking the bubble will filter the activity screen to show only related activity items.", 'buddypress' ) . '</p>'
+		) );
+
+		// Help panel - sidebar links.
+		get_current_screen()->set_help_sidebar(
+			'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+			'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+		);
+
+		// Add accessible hidden heading and text for Activity screen pagination.
+		get_current_screen()->set_screen_reader_content( array(
+			/* translators: accessibility text */
+			'heading_pagination' => __( 'Activity list navigation', 'buddypress' ),
+		) );
+
+	}
+
+	// Enqueue CSS and JavaScript.
+	wp_enqueue_script( 'bp_activity_admin_js', $bp->plugin_url . "bp-activity/admin/js/admin{$min}.js",   array( 'jquery', 'wp-ajax-response' ), bp_get_version(), true );
+	wp_localize_script( 'bp_activity_admin_js', 'bp_activity_admin_vars', array(
+		'page' => get_current_screen()->id
+	) );
+	wp_enqueue_style( 'bp_activity_admin_css', $bp->plugin_url . "bp-activity/admin/css/admin{$min}.css", array(),                               bp_get_version()       );
+
+	wp_style_add_data( 'bp_activity_admin_css', 'rtl', true );
+	if ( $min ) {
+		wp_style_add_data( 'bp_activity_admin_css', 'suffix', $min );
+	}
+
+	/**
+	 * Fires after the activity js and style has been enqueued.
+	 *
+	 * @since 2.4.0
+	 */
+	do_action( 'bp_activity_admin_enqueue_scripts' );
+
+	// Handle spam/un-spam/delete of activities.
+	if ( !empty( $doaction ) && ! in_array( $doaction, array( '-1', 'edit', 'save', ) ) ) {
+
+		// Build redirection URL.
+		$redirect_to = remove_query_arg( array( 'aid', 'deleted', 'error', 'spammed', 'unspammed', ), wp_get_referer() );
+		$redirect_to = add_query_arg( 'paged', $bp_activity_list_table->get_pagenum(), $redirect_to );
+
+		// Get activity IDs.
+		$activity_ids = array_map( 'absint', (array) $_REQUEST['aid'] );
+
+		/**
+		 * Filters list of IDs being spammed/un-spammed/deleted.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $activity_ids Activity IDs to spam/un-spam/delete.
+		 */
+		$activity_ids = apply_filters( 'bp_activity_admin_action_activity_ids', $activity_ids );
+
+		// Is this a bulk request?
+		if ( 'bulk_' == substr( $doaction, 0, 5 ) && ! empty( $_REQUEST['aid'] ) ) {
+			// Check this is a valid form submission.
+			check_admin_referer( 'bulk-activities' );
+
+			// Trim 'bulk_' off the action name to avoid duplicating a ton of code.
+			$doaction = substr( $doaction, 5 );
+
+		// This is a request to delete, spam, or un-spam, a single item.
+		} elseif ( !empty( $_REQUEST['aid'] ) ) {
+
+			// Check this is a valid form submission.
+			check_admin_referer( 'spam-activity_' . $activity_ids[0] );
+		}
+
+		// Initialise counters for how many of each type of item we perform an action on.
+		$deleted = $spammed = $unspammed = 0;
+
+		// Store any errors that occurs when updating the database items.
+		$errors = array();
+
+		// "We'd like to shoot the monster, could you move, please?"
+		foreach ( $activity_ids as $activity_id ) {
+			// @todo: Check the permissions on each
+			// if ( ! current_user_can( 'bp_edit_activity', $activity_id ) )
+			// continue;
+			// Get the activity from the database.
+			$activity = new BP_Activity_Activity( $activity_id );
+			if ( empty( $activity->component ) ) {
+				$errors[] = $activity_id;
+				continue;
+			}
+
+			switch ( $doaction ) {
+				case 'delete' :
+					if ( 'activity_comment' == $activity->type )
+						bp_activity_delete_comment( $activity->item_id, $activity->id );
+					else
+						bp_activity_delete( array( 'id' => $activity->id ) );
+
+					$deleted++;
+					break;
+
+				case 'ham' :
+					/**
+					 * Remove moderation and blacklist checks in case we want to ham an activity
+					 * which contains one of these listed keys.
+					 */
+					remove_action( 'bp_activity_before_save', 'bp_activity_check_moderation_keys', 2 );
+					remove_action( 'bp_activity_before_save', 'bp_activity_check_blacklist_keys', 2 );
+
+					bp_activity_mark_as_ham( $activity );
+					$result = $activity->save();
+
+					// Check for any error during activity save.
+					if ( ! $result )
+						$errors[] = $activity->id;
+					else
+						$unspammed++;
+					break;
+
+				case 'spam' :
+					bp_activity_mark_as_spam( $activity );
+					$result = $activity->save();
+
+					// Check for any error during activity save.
+					if ( ! $result )
+						$errors[] = $activity->id;
+					else
+						$spammed++;
+					break;
+
+				default:
+					break;
+			}
+
+			// Release memory.
+			unset( $activity );
+		}
+
+		/**
+		 * Fires before redirect for plugins to do something with activity.
+		 *
+		 * Passes an activity array counts how many were spam, not spam, deleted, and IDs that were errors.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array  $value        Array holding spam, not spam, deleted counts, error IDs.
+		 * @param string $redirect_to  URL to redirect to.
+		 * @param array  $activity_ids Original array of activity IDs.
+		 */
+		do_action( 'bp_activity_admin_action_after', array( $spammed, $unspammed, $deleted, $errors ), $redirect_to, $activity_ids );
+
+		// Add arguments to the redirect URL so that on page reload, we can easily display what we've just done.
+		if ( $spammed )
+			$redirect_to = add_query_arg( 'spammed', $spammed, $redirect_to );
+
+		if ( $unspammed )
+			$redirect_to = add_query_arg( 'unspammed', $unspammed, $redirect_to );
+
+		if ( $deleted )
+			$redirect_to = add_query_arg( 'deleted', $deleted, $redirect_to );
+
+		// If an error occurred, pass back the activity ID that failed.
+		if ( ! empty( $errors ) )
+			$redirect_to = add_query_arg( 'error', implode ( ',', array_map( 'absint', $errors ) ), $redirect_to );
+
+		/**
+		 * Filters redirect URL after activity spamming/un-spamming/deletion occurs.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $redirect_to URL to redirect to.
+		 */
+		wp_redirect( apply_filters( 'bp_activity_admin_action_redirect', $redirect_to ) );
+		exit;
+
+
+	// Save the edit.
+	} elseif ( $doaction && 'save' == $doaction ) {
+		// Build redirection URL.
+		$redirect_to = remove_query_arg( array( 'action', 'aid', 'deleted', 'error', 'spammed', 'unspammed', ), $_SERVER['REQUEST_URI'] );
+
+		// Get activity ID.
+		$activity_id = (int) $_REQUEST['aid'];
+
+		// Check this is a valid form submission.
+		check_admin_referer( 'edit-activity_' . $activity_id );
+
+		// Get the activity from the database.
+		$activity = new BP_Activity_Activity( $activity_id );
+
+		// If the activity doesn't exist, just redirect back to the index.
+		if ( empty( $activity->component ) ) {
+			wp_redirect( $redirect_to );
+			exit;
+		}
+
+		// Check the form for the updated properties.
+		// Store any error that occurs when updating the database item.
+		$error = 0;
+
+		// Activity spam status.
+		$prev_spam_status = $new_spam_status = false;
+		if ( ! empty( $_POST['activity_status'] ) ) {
+			$prev_spam_status = $activity->is_spam;
+			$new_spam_status  = ( 'spam' == $_POST['activity_status'] ) ? true : false;
+		}
+
+		// Activity action.
+		if ( isset( $_POST['bp-activities-action'] ) )
+			$activity->action = $_POST['bp-activities-action'];
+
+		// Activity content.
+		if ( isset( $_POST['bp-activities-content'] ) )
+			$activity->content = $_POST['bp-activities-content'];
+
+		// Activity primary link.
+		if ( ! empty( $_POST['bp-activities-link'] ) )
+			$activity->primary_link = $_POST['bp-activities-link'];
+
+		// Activity user ID.
+		if ( ! empty( $_POST['bp-activities-userid'] ) )
+			$activity->user_id = (int) $_POST['bp-activities-userid'];
+
+		// Activity item primary ID.
+		if ( isset( $_POST['bp-activities-primaryid'] ) )
+			$activity->item_id = (int) $_POST['bp-activities-primaryid'];
+
+		// Activity item secondary ID.
+		if ( isset( $_POST['bp-activities-secondaryid'] ) )
+			$activity->secondary_item_id = (int) $_POST['bp-activities-secondaryid'];
+
+		// Activity type.
+		if ( ! empty( $_POST['bp-activities-type'] ) ) {
+			$actions = bp_activity_admin_get_activity_actions();
+
+			// Check that the new type is a registered activity type.
+			if ( in_array( $_POST['bp-activities-type'], $actions ) ) {
+				$activity->type = $_POST['bp-activities-type'];
+			}
+		}
+
+		// Activity timestamp.
+		if ( ! empty( $_POST['aa'] ) && ! empty( $_POST['mm'] ) && ! empty( $_POST['jj'] ) && ! empty( $_POST['hh'] ) && ! empty( $_POST['mn'] ) && ! empty( $_POST['ss'] ) ) {
+			$aa = $_POST['aa'];
+			$mm = $_POST['mm'];
+			$jj = $_POST['jj'];
+			$hh = $_POST['hh'];
+			$mn = $_POST['mn'];
+			$ss = $_POST['ss'];
+			$aa = ( $aa <= 0 ) ? date( 'Y' ) : $aa;
+			$mm = ( $mm <= 0 ) ? date( 'n' ) : $mm;
+			$jj = ( $jj > 31 ) ? 31 : $jj;
+			$jj = ( $jj <= 0 ) ? date( 'j' ) : $jj;
+			$hh = ( $hh > 23 ) ? $hh -24 : $hh;
+			$mn = ( $mn > 59 ) ? $mn -60 : $mn;
+			$ss = ( $ss > 59 ) ? $ss -60 : $ss;
+
+			// Reconstruct the date into a timestamp.
+			$gmt_date = sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $aa, $mm, $jj, $hh, $mn, $ss );
+
+			$activity->date_recorded = $gmt_date;
+		}
+
+		// Has the spam status has changed?
+		if ( $new_spam_status != $prev_spam_status ) {
+			if ( $new_spam_status )
+				bp_activity_mark_as_spam( $activity );
+			else
+				bp_activity_mark_as_ham( $activity );
+		}
+
+		// Save.
+		$result = $activity->save();
+
+		// Clear the activity stream first page cache, in case this activity's timestamp was changed.
+		wp_cache_delete( 'bp_activity_sitewide_front', 'bp' );
+
+		// Check for any error during activity save.
+		if ( false === $result )
+			$error = $activity->id;
+
+		/**
+		 * Fires before redirect so plugins can do something first on save action.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $value Array holding activity object and ID that holds error.
+		 */
+		do_action_ref_array( 'bp_activity_admin_edit_after', array( &$activity, $error ) );
+
+		// If an error occurred, pass back the activity ID that failed.
+		if ( $error )
+			$redirect_to = add_query_arg( 'error', $error, $redirect_to );
+		else
+			$redirect_to = add_query_arg( 'updated', $activity->id, $redirect_to );
+
+		/**
+		 * Filters URL to redirect to after saving.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $redirect_to URL to redirect to.
+		 */
+		wp_redirect( apply_filters( 'bp_activity_admin_edit_redirect', $redirect_to ) );
+		exit;
+
+
+	// If a referrer and a nonce is supplied, but no action, redirect back.
+	} elseif ( ! empty( $_GET['_wp_http_referer'] ) ) {
+		wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), stripslashes( $_SERVER['REQUEST_URI'] ) ) );
+		exit;
+	}
+}
+
+/**
+ * Output the Activity component admin screens.
+ *
+ * @since 1.6.0
+ */
+function bp_activity_admin() {
+	// Decide whether to load the index or edit screen.
+	$doaction = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
+
+	// Display the single activity edit screen.
+	if ( 'edit' == $doaction && ! empty( $_GET['aid'] ) )
+		bp_activity_admin_edit();
+
+	// Otherwise, display the Activity index screen.
+	else
+		bp_activity_admin_index();
+}
+
+/**
+ * Display the single activity edit screen.
+ *
+ * @since 1.6.0
+ */
+function bp_activity_admin_edit() {
+
+	// @todo: Check if user is allowed to edit activity items
+	// if ( ! current_user_can( 'bp_edit_activity' ) )
+	if ( ! is_super_admin() )
+		die( '-1' );
+
+	// Get the activity from the database.
+	$activity = bp_activity_get( array(
+		'in'               => ! empty( $_REQUEST['aid'] ) ? (int) $_REQUEST['aid'] : 0,
+		'max'              => 1,
+		'show_hidden'      => true,
+		'spam'             => 'all',
+		'display_comments' => 0
+	) );
+
+	if ( ! empty( $activity['activities'][0] ) ) {
+		$activity = $activity['activities'][0];
+
+		// Workaround to use WP's touch_time() without duplicating that function.
+		$GLOBALS['comment'] = new stdClass;
+		$GLOBALS['comment']->comment_date = $activity->date_recorded;
+	} else {
+		$activity = '';
+	}
+
+	// Construct URL for form.
+	$form_url = remove_query_arg( array( 'action', 'deleted', 'error', 'spammed', 'unspammed', ), $_SERVER['REQUEST_URI'] );
+	$form_url = add_query_arg( 'action', 'save', $form_url );
+
+	/**
+	 * Fires before activity edit form is displays so plugins can modify the activity.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $value Array holding single activity object that was passed by reference.
+	 */
+	do_action_ref_array( 'bp_activity_admin_edit', array( &$activity ) ); ?>
+
+	<div class="wrap">
+		<h1><?php printf( __( 'Editing Activity (ID #%s)', 'buddypress' ), number_format_i18n( (int) $_REQUEST['aid'] ) ); ?></h1>
+
+		<?php if ( ! empty( $activity ) ) : ?>
+
+			<form action="<?php echo esc_url( $form_url ); ?>" id="bp-activities-edit-form" method="post">
+				<div id="poststuff">
+
+					<div id="post-body" class="metabox-holder columns-<?php echo 1 == get_current_screen()->get_columns() ? '1' : '2'; ?>">
+						<div id="post-body-content">
+							<div id="postdiv">
+								<div id="bp_activity_action" class="postbox">
+									<h2><?php _e( 'Action', 'buddypress' ); ?></h2>
+									<div class="inside">
+										<label for="bp-activities-action" class="screen-reader-text"><?php
+											/* translators: accessibility text */
+											_e( 'Edit activity action', 'buddypress' );
+										?></label>
+										<?php wp_editor( stripslashes( $activity->action ), 'bp-activities-action', array( 'media_buttons' => false, 'textarea_rows' => 7, 'teeny' => true, 'quicktags' => array( 'buttons' => 'strong,em,link,block,del,ins,img,code,spell,close' ) ) ); ?>
+									</div>
+								</div>
+
+								<div id="bp_activity_content" class="postbox">
+									<h2><?php _e( 'Content', 'buddypress' ); ?></h2>
+									<div class="inside">
+										<label for="bp-activities-content" class="screen-reader-text"><?php
+											/* translators: accessibility text */
+											_e( 'Edit activity content', 'buddypress' );
+										?></label>
+										<?php wp_editor( stripslashes( $activity->content ), 'bp-activities-content', array( 'media_buttons' => false, 'teeny' => true, 'quicktags' => array( 'buttons' => 'strong,em,link,block,del,ins,img,code,spell,close' ) ) ); ?>
+									</div>
+								</div>
+							</div>
+						</div><!-- #post-body-content -->
+
+						<div id="postbox-container-1" class="postbox-container">
+							<?php do_meta_boxes( get_current_screen()->id, 'side', $activity ); ?>
+						</div>
+
+						<div id="postbox-container-2" class="postbox-container">
+							<?php do_meta_boxes( get_current_screen()->id, 'normal', $activity ); ?>
+							<?php do_meta_boxes( get_current_screen()->id, 'advanced', $activity ); ?>
+						</div>
+					</div><!-- #post-body -->
+
+				</div><!-- #poststuff -->
+				<?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
+				<?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>
+				<?php wp_nonce_field( 'edit-activity_' . $activity->id ); ?>
+			</form>
+
+		<?php else : ?>
+
+			<p><?php
+				printf(
+					'%1$s <a href="%2$s">%3$s</a>',
+					__( 'No activity found with this ID.', 'buddypress' ),
+					esc_url( bp_get_admin_url( 'admin.php?page=bp-activity' ) ),
+					__( 'Go back and try again.', 'buddypress' )
+				);
+			?></p>
+
+		<?php endif; ?>
+
+	</div><!-- .wrap -->
+
+<?php
+}
+
+/**
+ * Status metabox for the Activity admin edit screen.
+ *
+ * @since 1.6.0
+ *
+ * @param object $item Activity item.
+ */
+function bp_activity_admin_edit_metabox_status( $item ) {
+?>
+
+	<div class="submitbox" id="submitcomment">
+
+		<div id="minor-publishing">
+			<div id="minor-publishing-actions">
+				<div id="preview-action">
+					<a class="button preview" href="<?php echo esc_attr( bp_activity_get_permalink( $item->id, $item ) ); ?>" target="_blank"><?php _e( 'View Activity', 'buddypress' ); ?></a>
+				</div>
+
+				<div class="clear"></div>
+			</div><!-- #minor-publishing-actions -->
+
+			<div id="misc-publishing-actions">
+				<div class="misc-pub-section" id="comment-status-radio">
+					<label class="approved" for="activity-status-approved"><input type="radio" name="activity_status" id="activity-status-approved" value="ham" <?php checked( $item->is_spam, 0 ); ?>><?php _e( 'Approved', 'buddypress' ); ?></label><br />
+					<label class="spam" for="activity-status-spam"><input type="radio" name="activity_status" id="activity-status-spam" value="spam" <?php checked( $item->is_spam, 1 ); ?>><?php _e( 'Spam', 'buddypress' ); ?></label>
+				</div>
+
+				<div class="misc-pub-section curtime misc-pub-section-last">
+					<?php
+					// Translators: Publish box date format, see http://php.net/date.
+					$datef = __( 'M j, Y @ G:i', 'buddypress' );
+					$date  = date_i18n( $datef, strtotime( $item->date_recorded ) );
+					?>
+					<span id="timestamp"><?php printf( __( 'Submitted on: %s', 'buddypress' ), '<strong>' . $date . '</strong>' ); ?></span>&nbsp;<a href="#edit_timestamp" class="edit-timestamp hide-if-no-js" tabindex='4'><?php _e( 'Edit', 'buddypress' ); ?></a>
+
+					<div id='timestampdiv' class='hide-if-js'>
+						<?php touch_time( 1, 0, 5 ); ?>
+					</div><!-- #timestampdiv -->
+				</div>
+			</div> <!-- #misc-publishing-actions -->
+
+			<div class="clear"></div>
+		</div><!-- #minor-publishing -->
+
+		<div id="major-publishing-actions">
+			<div id="publishing-action">
+				<?php submit_button( __( 'Update', 'buddypress' ), 'primary', 'save', false ); ?>
+			</div>
+			<div class="clear"></div>
+		</div><!-- #major-publishing-actions -->
+
+	</div><!-- #submitcomment -->
+
+<?php
+}
+
+/**
+ * Primary link metabox for the Activity admin edit screen.
+ *
+ * @since 1.6.0
+ *
+ * @param object $item Activity item.
+ */
+function bp_activity_admin_edit_metabox_link( $item ) {
+?>
+
+	<label class="screen-reader-text" for="bp-activities-link"><?php
+		/* translators: accessibility text */
+		_e( 'Link', 'buddypress' );
+	?></label>
+	<input type="url" name="bp-activities-link" id="bp-activities-link" value="<?php echo esc_url( $item->primary_link ); ?>" aria-describedby="bp-activities-link-description" />
+	<p id="bp-activities-link-description"><?php _e( 'Activity generated by posts and comments uses the link field for a permalink back to the content item.', 'buddypress' ); ?></p>
+
+<?php
+}
+
+/**
+ * User ID metabox for the Activity admin edit screen.
+ *
+ * @since 1.6.0
+ *
+ * @param object $item Activity item.
+ */
+function bp_activity_admin_edit_metabox_userid( $item ) {
+?>
+
+	<label class="screen-reader-text" for="bp-activities-userid"><?php
+		/* translators: accessibility text */
+		_e( 'Author ID', 'buddypress' );
+	?></label>
+	<input type="number" name="bp-activities-userid" id="bp-activities-userid" value="<?php echo esc_attr( $item->user_id ); ?>" min="1" />
+
+<?php
+}
+
+/**
+ * Get flattened array of all registered activity actions.
+ *
+ * Format is [activity_type] => Pretty name for activity type.
+ *
+ * @since 2.0.0
+ *
+ * @return array $actions
+ */
+function bp_activity_admin_get_activity_actions() {
+	$actions  = array();
+
+	// Walk through the registered actions, and build an array of actions/values.
+	foreach ( bp_activity_get_actions() as $action ) {
+		$action = array_values( (array) $action );
+
+		for ( $i = 0, $i_count = count( $action ); $i < $i_count; $i++ ) {
+			$actions[ $action[$i]['key'] ] = $action[$i]['value'];
+		}
+	}
+
+	// This was a mis-named activity type from before BP 1.6.
+	unset( $actions['friends_register_activity_action'] );
+
+	// Sort array by the human-readable value.
+	natsort( $actions );
+
+	return $actions;
+}
+
+/**
+ * Activity type metabox for the Activity admin edit screen.
+ *
+ * @since 1.6.0
+ *
+ * @param object $item Activity item.
+ */
+function bp_activity_admin_edit_metabox_type( $item ) {
+	$bp = buddypress();
+
+	$actions  = array();
+	$selected = $item->type;
+
+	// Walk through the registered actions, and build an array of actions/values.
+	foreach ( bp_activity_get_actions() as $action ) {
+		$action = array_values( (array) $action );
+
+		for ( $i = 0, $i_count = count( $action ); $i < $i_count; $i++ )
+			$actions[ $action[$i]['key'] ] = $action[$i]['value'];
+	}
+
+	// This was a mis-named activity type from before BP 1.6.
+	unset( $actions['friends_register_activity_action'] );
+
+	// Sort array by the human-readable value.
+	natsort( $actions );
+
+	/*
+	 * If the activity type is not registered properly (eg, a plugin has
+	 * not called bp_activity_set_action()), add the raw type to the end
+	 * of the list.
+	 */
+	if ( ! isset( $actions[ $selected ] ) ) {
+		_doing_it_wrong( __FUNCTION__, sprintf( __( 'This activity item has a type (%s) that is not registered using bp_activity_set_action(), so no label is available.', 'buddypress' ), $selected ), '2.0.0' );
+		$actions[ $selected ] = $selected;
+	}
+
+	?>
+
+	<label for="bp-activities-type" class="screen-reader-text"><?php
+		/* translators: accessibility text */
+		esc_html_e( 'Select activity type', 'buddypress' );
+	?></label>
+	<select name="bp-activities-type" id="bp-activities-type">
+		<?php foreach ( $actions as $k => $v ) : ?>
+			<option value="<?php echo esc_attr( $k ); ?>" <?php selected( $k,  $selected ); ?>><?php echo esc_html( $v ); ?></option>
+		<?php endforeach; ?>
+	</select>
+
+<?php
+}
+
+/**
+ * Primary item ID/Secondary item ID metabox for the Activity admin edit screen.
+ *
+ * @since 1.6.0
+ *
+ * @param object $item Activity item.
+ */
+function bp_activity_admin_edit_metabox_itemids( $item ) {
+?>
+
+	<label for="bp-activities-primaryid"><?php _e( 'Primary Item ID', 'buddypress' ); ?></label>
+	<input type="number" name="bp-activities-primaryid" id="bp-activities-primaryid" value="<?php echo esc_attr( $item->item_id ); ?>" min="0" />
+	<br />
+
+	<label for="bp-activities-secondaryid"><?php _e( 'Secondary Item ID', 'buddypress' ); ?></label>
+	<input type="number" name="bp-activities-secondaryid" id="bp-activities-secondaryid" value="<?php echo esc_attr( $item->secondary_item_id ); ?>" min="0" />
+
+	<p><?php _e( 'These identify the object that created this activity. For example, the fields could reference a pair of site and comment IDs.', 'buddypress' ); ?></p>
+
+<?php
+}
+
+/**
+ * Display the Activity admin index screen, which contains a list of all the activities.
+ *
+ * @since 1.6.0
+ *
+ * @global BP_Activity_List_Table $bp_activity_list_table Activity screen list table.
+ * @global string                 $plugin_page            The current plugin page.
+ */
+function bp_activity_admin_index() {
+	global $bp_activity_list_table, $plugin_page;
+
+	$messages = array();
+
+	// If the user has just made a change to an activity item, build status messages.
+	if ( ! empty( $_REQUEST['deleted'] ) || ! empty( $_REQUEST['spammed'] ) || ! empty( $_REQUEST['unspammed'] ) || ! empty( $_REQUEST['error'] ) || ! empty( $_REQUEST['updated'] ) ) {
+		$deleted   = ! empty( $_REQUEST['deleted']   ) ? (int) $_REQUEST['deleted']   : 0;
+		$errors    = ! empty( $_REQUEST['error']     ) ? $_REQUEST['error']           : '';
+		$spammed   = ! empty( $_REQUEST['spammed']   ) ? (int) $_REQUEST['spammed']   : 0;
+		$unspammed = ! empty( $_REQUEST['unspammed'] ) ? (int) $_REQUEST['unspammed'] : 0;
+		$updated   = ! empty( $_REQUEST['updated']   ) ? (int) $_REQUEST['updated']   : 0;
+
+		$errors = array_map( 'absint', explode( ',', $errors ) );
+
+		// Make sure we don't get any empty values in $errors.
+		for ( $i = 0, $errors_count = count( $errors ); $i < $errors_count; $i++ ) {
+			if ( 0 === $errors[$i] ) {
+				unset( $errors[$i] );
+			}
+		}
+
+		// Reindex array.
+		$errors = array_values( $errors );
+
+		if ( $deleted > 0 )
+			$messages[] = sprintf( _n( '%s activity item has been permanently deleted.', '%s activity items have been permanently deleted.', $deleted, 'buddypress' ), number_format_i18n( $deleted ) );
+
+		if ( ! empty( $errors ) ) {
+			if ( 1 == count( $errors ) ) {
+				$messages[] = sprintf( __( 'An error occurred when trying to update activity ID #%s.', 'buddypress' ), number_format_i18n( $errors[0] ) );
+
+			} else {
+				$error_msg  = __( 'Errors occurred when trying to update these activity items:', 'buddypress' );
+				$error_msg .= '<ul class="activity-errors">';
+
+				// Display each error as a list item.
+				foreach ( $errors as $error ) {
+					// Translators: This is a bulleted list of item IDs.
+					$error_msg .= '<li>' . sprintf( __( '#%s', 'buddypress' ), number_format_i18n( $error ) ) . '</li>';
+				}
+
+				$error_msg  .= '</ul>';
+				$messages[] = $error_msg;
+			}
+		}
+
+		if ( $spammed > 0 )
+			$messages[] = sprintf( _n( '%s activity item has been successfully spammed.', '%s activity items have been successfully spammed.', $spammed, 'buddypress' ), number_format_i18n( $spammed ) );
+
+		if ( $unspammed > 0 )
+			$messages[] = sprintf( _n( '%s activity item has been successfully unspammed.', '%s activity items have been successfully unspammed.', $unspammed, 'buddypress' ), number_format_i18n( $unspammed ) );
+
+		if ( $updated > 0 )
+			$messages[] = __( 'The activity item has been updated successfully.', 'buddypress' );
+	}
+
+	// Prepare the activity items for display.
+	$bp_activity_list_table->prepare_items();
+
+	/**
+	 * Fires before edit form is displayed so plugins can modify the activity messages.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $messages Array of messages to display at top of page.
+	 */
+	do_action( 'bp_activity_admin_index', $messages ); ?>
+
+	<div class="wrap">
+		<h1>
+			<?php if ( !empty( $_REQUEST['aid'] ) ) : ?>
+				<?php printf( __( 'Activity related to ID #%s', 'buddypress' ), number_format_i18n( (int) $_REQUEST['aid'] ) ); ?>
+			<?php else : ?>
+				<?php _ex( 'Activity', 'Admin SWA page', 'buddypress' ); ?>
+			<?php endif; ?>
+
+			<?php if ( !empty( $_REQUEST['s'] ) ) : ?>
+				<span class="subtitle"><?php printf( __( 'Search results for &#8220;%s&#8221;', 'buddypress' ), wp_html_excerpt( esc_html( stripslashes( $_REQUEST['s'] ) ), 50 ) ); ?></span>
+			<?php endif; ?>
+		</h1>
+
+		<?php // If the user has just made a change to an activity item, display the status messages. ?>
+		<?php if ( !empty( $messages ) ) : ?>
+			<div id="moderated" class="<?php echo ( ! empty( $_REQUEST['error'] ) ) ? 'error' : 'updated'; ?>"><p><?php echo implode( "<br/>\n", $messages ); ?></p></div>
+		<?php endif; ?>
+
+		<?php // Display each activity on its own row. ?>
+		<?php $bp_activity_list_table->views(); ?>
+
+		<form id="bp-activities-form" action="" method="get">
+			<?php $bp_activity_list_table->search_box( __( 'Search all Activity', 'buddypress' ), 'bp-activity' ); ?>
+			<input type="hidden" name="page" value="<?php echo esc_attr( $plugin_page ); ?>" />
+			<?php $bp_activity_list_table->display(); ?>
+		</form>
+
+		<?php // This markup is used for the reply form. ?>
+		<table style="display: none;">
+			<tr id="bp-activities-container" style="display: none;">
+				<td colspan="4">
+					<form method="get" action="">
+
+						<h3 id="bp-replyhead"><?php _e( 'Reply to Activity', 'buddypress' ); ?></h3>
+						<label for="bp-activities" class="screen-reader-text"><?php
+							/* translators: accessibility text */
+							_e( 'Reply', 'buddypress' );
+						?></label>
+						<?php wp_editor( '', 'bp-activities', array( 'dfw' => false, 'media_buttons' => false, 'quicktags' => array( 'buttons' => 'strong,em,link,block,del,ins,img,code,spell,close' ), 'tinymce' => false, ) ); ?>
+
+						<p id="bp-replysubmit" class="submit">
+							<a href="#" class="cancel button-secondary alignleft"><?php _e( 'Cancel', 'buddypress' ); ?></a>
+							<a href="#" class="save button-primary alignright"><?php _e( 'Reply', 'buddypress' ); ?></a>
+
+							<img class="waiting" style="display:none;" src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" alt="" />
+							<span class="error" style="display:none;"></span>
+							<br class="clear" />
+						</p>
+
+						<?php wp_nonce_field( 'bp-activity-admin-reply', '_ajax_nonce-bp-activity-admin-reply', false ); ?>
+
+					</form>
+				</td>
+			</tr>
+		</table>
+	</div>
+
+<?php
+}
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-adminbar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-adminbar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-adminbar.php	(revision 12265)
@@ -0,0 +1,47 @@
+<?php
+/**
+ * BuddyPress Activity Toolbar.
+ *
+ * Handles the activity functions related to the WordPress Toolbar.
+ *
+ * @package BuddyPress
+ * @subpackage Activity
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add the Activity top-level menu link when viewing single activity item.
+ *
+ * @since 2.6.0
+ *
+ * @return null Null if user does not have access to editing functionality.
+ */
+function bp_activity_admin_menu() {
+	global $wp_admin_bar;
+
+	// Only show if viewing a single activity item.
+	if ( ! bp_is_single_activity() ) {
+		return;
+	}
+
+	// Only show this menu to super admins
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return;
+	}
+
+	$activity_edit_link = add_query_arg( array(
+		'page' => 'bp-activity',
+		'aid' => bp_current_action(),
+		'action' => 'edit'
+	), bp_get_admin_url( 'admin.php' ) );
+
+	// Add the top-level Edit Activity button.
+	$wp_admin_bar->add_menu( array(
+		'id'    => 'activity-admin',
+		'title' => __( 'Edit Activity', 'buddypress' ),
+		'href'  => esc_url( $activity_edit_link ),
+	) );
+}
+add_action( 'admin_bar_menu', 'bp_activity_admin_menu', 99 );
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-akismet.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-akismet.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-akismet.php	(revision 12265)
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Akismet support for BuddyPress' Activity Stream.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityAkismet
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Loads Akismet filtering for activity.
+ *
+ * @since 1.6.0
+ * @since 2.3.0 We only support Akismet 3+.
+ */
+function bp_activity_setup_akismet() {
+	/**
+	 * Filters if BuddyPress Activity Akismet support has been disabled by another plugin.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Return value of bp_is_akismet_active boolean function.
+	 */
+	if ( ! apply_filters( 'bp_activity_use_akismet', bp_is_akismet_active() ) ) {
+		return;
+	}
+
+	// Instantiate Akismet for BuddyPress.
+	buddypress()->activity->akismet = new BP_Akismet();
+}
+add_action( 'bp_activity_setup_globals', 'bp_activity_setup_akismet' );
+
+/**
+ * Delete old spam activity meta data.
+ *
+ * This is done as a clean-up mechanism, as _bp_akismet_submission meta can
+ * grow to be quite large.
+ *
+ * @since 1.6.0
+ *
+ * @global wpdb $wpdb WordPress database object.
+ */
+function bp_activity_akismet_delete_old_metadata() {
+	global $wpdb;
+
+	$bp = buddypress();
+
+	/**
+	 * Filters the threshold for how many days old Akismet metadata needs to be before being automatically deleted.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param integer 15 How many days old metadata needs to be.
+	 */
+	$interval = apply_filters( 'bp_activity_akismet_delete_meta_interval', 15 );
+
+	// Enforce a minimum of 1 day.
+	$interval = max( 1, absint( $interval ) );
+
+	// _bp_akismet_submission meta values are large, so expire them after $interval days regardless of the activity status
+	$sql          = $wpdb->prepare( "SELECT a.id FROM {$bp->activity->table_name} a LEFT JOIN {$bp->activity->table_name_meta} m ON a.id = m.activity_id WHERE m.meta_key = %s AND DATE_SUB(%s, INTERVAL {$interval} DAY) > a.date_recorded LIMIT 10000", '_bp_akismet_submission', current_time( 'mysql', 1 ) );
+	$activity_ids = $wpdb->get_col( $sql );
+
+	if ( ! empty( $activity_ids ) ) {
+		foreach ( $activity_ids as $activity_id )
+			bp_activity_delete_meta( $activity_id, '_bp_akismet_submission' );
+	}
+}
+add_action( 'bp_activity_akismet_delete_old_metadata', 'bp_activity_akismet_delete_old_metadata' );
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-cache.php	(revision 12265)
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Functions related to the BuddyPress Activity component and the WP Cache.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityCache
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Slurp up activitymeta for a specified set of activity items.
+ *
+ * It grabs all activitymeta associated with all of the activity items passed
+ * in $activity_ids and adds it to the WP cache. This improves efficiency when
+ * using querying activitymeta inline.
+ *
+ * @since 1.6.0
+ *
+ * @param int|string|array|bool $activity_ids Accepts a single activity ID, or a comma-
+ *                                            separated list or array of activity ids.
+ */
+function bp_activity_update_meta_cache( $activity_ids = false ) {
+	$bp = buddypress();
+
+	$cache_args = array(
+		'object_ids' 	   => $activity_ids,
+		'object_type' 	   => $bp->activity->id,
+		'object_column'    => 'activity_id',
+		'cache_group'      => 'activity_meta',
+		'meta_table' 	   => $bp->activity->table_name_meta,
+		'cache_key_prefix' => 'bp_activity_meta'
+	);
+
+	bp_update_meta_cache( $cache_args );
+}
+
+/**
+ * Clear a cached activity item when that item is updated.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_Activity_Activity $activity Activity object.
+ */
+function bp_activity_clear_cache_for_activity( $activity ) {
+	wp_cache_delete( $activity->id, 'bp_activity' );
+	wp_cache_delete( 'bp_activity_sitewide_front', 'bp' );
+}
+add_action( 'bp_activity_after_save', 'bp_activity_clear_cache_for_activity' );
+
+/**
+ * Clear cached data for deleted activity items.
+ *
+ * @since 2.0.0
+ *
+ * @param array $deleted_ids IDs of deleted activity items.
+ */
+function bp_activity_clear_cache_for_deleted_activity( $deleted_ids ) {
+	foreach ( (array) $deleted_ids as $deleted_id ) {
+		wp_cache_delete( $deleted_id, 'bp_activity' );
+	}
+}
+add_action( 'bp_activity_deleted_activities', 'bp_activity_clear_cache_for_deleted_activity' );
+
+/**
+ * Reset cache incrementor for the Activity component.
+ *
+ * Called whenever an activity item is created, updated, or deleted, this
+ * function effectively invalidates all cached results of activity queries.
+ *
+ * @since 2.7.0
+ *
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_reset_cache_incrementor() {
+	$without_last_activity = bp_core_reset_incrementor( 'bp_activity' );
+	$with_last_activity    = bp_core_reset_incrementor( 'bp_activity_with_last_activity' );
+	return $without_last_activity && $with_last_activity;
+}
+add_action( 'bp_activity_delete',    'bp_activity_reset_cache_incrementor' );
+add_action( 'bp_activity_add',       'bp_activity_reset_cache_incrementor' );
+add_action( 'added_activity_meta',   'bp_activity_reset_cache_incrementor' );
+add_action( 'updated_activity_meta', 'bp_activity_reset_cache_incrementor' );
+add_action( 'deleted_activity_meta', 'bp_activity_reset_cache_incrementor' );
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-cssjs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-cssjs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-cssjs.php	(revision 12265)
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Activity component CSS/JS
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScripts
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Enqueue @mentions JS.
+ *
+ * @since 2.1.0
+ */
+function bp_activity_mentions_script() {
+	if ( ! bp_activity_maybe_load_mentions_scripts() ) {
+		return;
+	}
+
+	// Special handling for New/Edit screens in wp-admin.
+	if ( is_admin() ) {
+		if (
+			! get_current_screen() ||
+			! in_array( get_current_screen()->base, array( 'page', 'post' ) ) ||
+			! post_type_supports( get_current_screen()->post_type, 'editor' ) ) {
+			return;
+		}
+	}
+
+
+	$min = bp_core_get_minified_asset_suffix();
+
+	wp_enqueue_script( 'bp-mentions', buddypress()->plugin_url . "bp-activity/js/mentions{$min}.js", array( 'jquery', 'jquery-atwho' ), bp_get_version(), true );
+	wp_enqueue_style( 'bp-mentions-css', buddypress()->plugin_url . "bp-activity/css/mentions{$min}.css", array(), bp_get_version() );
+
+	wp_style_add_data( 'bp-mentions-css', 'rtl', true );
+	if ( $min ) {
+		wp_style_add_data( 'bp-mentions-css', 'suffix', $min );
+	}
+
+	// If the script has been enqueued, let's attach our mentions TinyMCE init callback.
+	add_filter( 'tiny_mce_before_init', 'bp_add_mentions_on_tinymce_init', 10, 2 );
+
+	/**
+	 * Fires at the end of the Activity Mentions script.
+	 *
+	 * This is the hook where BP components can add their own prefetched results
+	 * friends to the page for quicker @mentions lookups.
+	 *
+	 * @since 2.1.0
+	 */
+	do_action( 'bp_activity_mentions_prime_results' );
+}
+add_action( 'bp_enqueue_scripts', 'bp_activity_mentions_script' );
+add_action( 'bp_admin_enqueue_scripts', 'bp_activity_mentions_script' );
+
+/**
+ * Bind the mentions listener to a wp_editor instance when TinyMCE initializes.
+ *
+ * @since 2.3.3
+ *
+ * @param array  $settings   An array with TinyMCE config.
+ * @param string $editor_id Unique editor identifier, e.g. 'content'.
+ * @return array  $mceInit   An array with TinyMCE config.
+ */
+function bp_add_mentions_on_tinymce_init( $settings, $editor_id ) {
+	// We only apply the mentions init to the visual post editor in the WP dashboard.
+	if ( 'content' === $editor_id ) {
+		$settings['init_instance_callback'] = 'window.bp.mentions.tinyMCEinit';
+	}
+
+	return $settings;
+}
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-embeds.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-embeds.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-embeds.php	(revision 12265)
@@ -0,0 +1,348 @@
+<?php
+/**
+ * Functions related to embedding single activity items externally.
+ *
+ * @since 2.6.0
+ *
+ * @package BuddyPress
+ * @subpackage ActivityEmbeds
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Loads our activity oEmbed component.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_setup_oembed() {
+	buddypress()->activity->oembed = new BP_Activity_oEmbed_Extension;
+}
+add_action( 'bp_loaded', 'bp_activity_setup_oembed' );
+
+/**
+ * Catch links in embed excerpt so top.location.href can be added.
+ *
+ * Due to <iframe sandbox="allow-top-navigation">, links in embeds can only be
+ * clicked if invoked with top.location.href via JS.
+ *
+ * @since 2.6.0
+ *
+ * @param  string $text Embed excerpt
+ * @return string
+ */
+function bp_activity_embed_excerpt_onclick_location_filter( $text ) {
+	return preg_replace_callback( '/<a\s+[^>]*href=\"([^\"]*)\"/iU', 'bp_activity_embed_excerpt_onclick_location_filter_callback', $text );
+}
+	/**
+	 * Add onclick="top.location.href" to a link.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  array $matches Items matched by bp_activity_embed_excerpt_onclick_location_filter().
+	 * @return string
+	 */
+	function bp_activity_embed_excerpt_onclick_location_filter_callback( $matches ) {
+		return sprintf( '<a href="%1$s" onclick="top.location.href=\'%1$s\'"', $matches[1] );
+	}
+
+/**
+ * Add inline styles for BP activity embeds.
+ *
+ * @since  2.6.0
+ */
+function bp_activity_embed_add_inline_styles() {
+	if ( false === bp_is_single_activity() ) {
+		return;
+	}
+
+	$min = bp_core_get_minified_asset_suffix();
+
+	if ( is_rtl() ) {
+		$css = bp_locate_template_asset( "css/embeds-activity-rtl{$min}.css" );
+	} else {
+		$css = bp_locate_template_asset( "css/embeds-activity{$min}.css" );
+	}
+
+	// Bail if file wasn't found.
+	if ( false === $css ) {
+		return;
+	}
+
+	// Grab contents of CSS file and do some rudimentary CSS protection.
+	$css = file_get_contents( $css['file'] );
+	$css = wp_kses( $css, array( "\'", '\"' ) );
+
+	printf( '<style type="text/css">%s</style>', $css );
+}
+add_action( 'embed_head', 'bp_activity_embed_add_inline_styles', 20 );
+
+/**
+ * Query for the activity item on the activity embed template.
+ *
+ * Basically a wrapper for {@link bp_has_activities()}, but allows us to
+ * use the activity loop without requerying for it again.
+ *
+ * @since 2.6.0
+ *
+ * @param  int $activity_id The activity ID.
+ * @return bool
+ */
+function bp_activity_embed_has_activity( $activity_id = 0 ) {
+	global $activities_template;
+
+	if ( empty( $activity_id ) ) {
+		return false;
+	}
+
+	if ( ! empty( $activities_template->activities ) ) {
+		$activity = (array) $activities_template->activities;
+		$activity = reset( $activity );
+
+		// No need to requery if we already got the embed activity
+		if ( (int) $activity_id === $activity->id ) {
+			return $activities_template->has_activities();
+		}
+	}
+
+	return bp_has_activities( array(
+		'display_comments' => 'threaded',
+		'show_hidden'      => true,
+		'include'          => (int) $activity_id,
+	) );
+}
+
+/**
+ * Outputs excerpt for an activity embed item.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_embed_excerpt( $content = '' ) {
+	echo bp_activity_get_embed_excerpt( $content );
+}
+
+	/**
+	 * Generates excerpt for an activity embed item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $content The content to generate an excerpt for.
+	 * @return string
+	 */
+	function bp_activity_get_embed_excerpt( $content = '' ) {
+		if ( empty( $content ) && ! empty( $GLOBALS['activities_template']->in_the_loop ) ) {
+			$content = $GLOBALS['activities_template']->activity->content;
+		}
+
+		/*
+		 * bp_activity_truncate_entry() includes the 'Read More' link, which is why
+		 * we're using this instead of bp_create_excerpt().
+		 */
+		$content = html_entity_decode( $content );
+		$content = bp_activity_truncate_entry( $content, array(
+			'html' => false,
+			'filter_shortcodes' => true,
+			'strip_tags'        => true,
+			'force_truncate'    => true
+		) );
+
+		/**
+		 * Filter the activity embed excerpt.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @var string $content Embed Excerpt.
+		 * @var string $unmodified_content Unmodified activity content.
+		 */
+		return apply_filters( 'bp_activity_get_embed_excerpt', $content, $GLOBALS['activities_template']->activity->content );
+	}
+
+/**
+ * Outputs the first embedded item in the activity oEmbed template.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_embed_media() {
+	// Bail if oEmbed request explicitly hides media.
+	if ( isset( $_GET['hide_media'] ) && true == wp_validate_boolean( $_GET['hide_media'] ) ) {
+		/**
+		 * Do something after media is rendered for an activity oEmbed item.
+		 *
+		 * @since 2.6.0
+		 */
+		do_action( 'bp_activity_embed_after_media' );
+
+		return;
+	}
+
+	/**
+	 * Should we display media in the oEmbed template?
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param bool $retval Defaults to true.
+	 */
+	$allow_media = apply_filters( 'bp_activity_embed_display_media', true );
+
+	// Find oEmbeds from only WP registered providers.
+	bp_remove_all_filters( 'oembed_providers' );
+	$media = bp_core_extract_media_from_content( $GLOBALS['activities_template']->activity->content, 'embeds' );
+	bp_restore_all_filters( 'oembed_providers' );
+
+	// oEmbeds have precedence over inline video / audio.
+	if ( isset( $media['embeds'] ) && true === $allow_media ) {
+		// Autoembed first URL.
+		$oembed_defaults = wp_embed_defaults();
+		$oembed_args = array(
+			'width'    => $oembed_defaults['width'],
+			'height'   => $oembed_defaults['height'],
+			'discover' => true
+		);
+		$url      = $media['embeds'][0]['url'];
+		$cachekey = '_oembed_response_' . md5( $url . serialize( $oembed_args ) );
+
+		// Try to fetch oEmbed response from meta.
+		$oembed = bp_activity_get_meta( bp_get_activity_id(), $cachekey );
+
+		// No cache, so fetch full oEmbed response now!
+		if ( '' === $oembed ) {
+			$o = _wp_oembed_get_object();
+			$oembed = $o->fetch( $o->get_provider( $url, $oembed_args ), $url, $oembed_args );
+
+			// Cache oEmbed response.
+			bp_activity_update_meta( bp_get_activity_id(), $cachekey, $oembed );
+		}
+
+		$content = '';
+
+		/**
+		 * Filters the default embed display max width.
+		 *
+		 * This is used if the oEmbed response does not return a thumbnail width.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param int $width.
+		 */
+		$width = (int) apply_filters( 'bp_activity_embed_display_media_width', 550 );
+
+		// Set thumbnail.
+		if ( 'photo' === $oembed->type ) {
+			$thumbnail = $oembed->url;
+		} elseif ( isset( $oembed->thumbnail_url ) ) {
+			$thumbnail = $oembed->thumbnail_url;
+
+		/* Non-oEmbed standard attributes */
+		// Mixcloud
+		} elseif ( isset( $oembed->image ) ) {
+			$thumbnail = $oembed->image;
+		// ReverbNation
+		} elseif ( isset( $oembed->{'thumbnail-url'} ) ) {
+			$thumbnail = $oembed->{'thumbnail-url'};
+		}
+
+		// Display thumb and related oEmbed meta.
+		if ( true === isset ( $thumbnail ) ) {
+			$play_icon = $caption = '';
+
+			// Add play icon for non-photos.
+			if ( 'photo' !== $oembed->type ) {
+				/**
+				 * ion-play icon from Ionicons.
+				 *
+				 * @link    http://ionicons.com/
+				 * @license MIT
+				 */
+				$play_icon = <<<EOD
+<svg id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M405.2,232.9L126.8,67.2c-3.4-2-6.9-3.2-10.9-3.2c-10.9,0-19.8,9-19.8,20H96v344h0.1c0,11,8.9,20,19.8,20  c4.1,0,7.5-1.4,11.2-3.4l278.1-165.5c6.6-5.5,10.8-13.8,10.8-23.1C416,246.7,411.8,238.5,405.2,232.9z"/></svg>
+EOD;
+
+				$play_icon = sprintf( '<a rel="nofollow" class="play-btn" href="%1$s" onclick="top.location.href=\'%1$s\'">%2$s</a>', esc_url( $url ), $play_icon );
+			}
+
+			// Thumb width
+			$thumb_width = isset( $oembed->thumbnail_width ) && 'photo' !== $oembed->type && (int) $oembed->thumbnail_width < 550 ? (int) $oembed->thumbnail_width : $width;
+
+			$float_width = 350;
+
+			// Set up thumb.
+			$content = sprintf( '<div class="thumb" style="max-width:%1$spx">%2$s<a href="%3$s" rel="nofollow" onclick="top.location.href=\'%3$s\'"><img src="%4$s" /></a></div>', $thumb_width, $play_icon, esc_url( $url ), esc_url( $thumbnail ) );
+
+			// Show title.
+			if ( isset( $oembed->title ) ) {
+				$caption .= sprintf( '<p class="caption-title"><strong>%s</strong></p>', apply_filters( 'single_post_title', $oembed->title ) );
+			}
+
+			// Show description (non-oEmbed standard)
+			if ( isset( $oembed->description ) ) {
+				$caption .= sprintf( '<div class="caption-description">%s</div>', apply_filters( 'bp_activity_get_embed_excerpt', $oembed->description ) );
+			}
+
+			// Show author info.
+			if ( isset( $oembed->provider_name ) && isset( $oembed->author_name ) ) {
+				/* translators: By [oEmbed author] on [oEmbed provider]. eg. By BuddyPress on YouTube. */
+				$anchor_text = sprintf( __( 'By %1$s on %2$s', 'buddypress' ), $oembed->author_name, $oembed->provider_name );
+
+			} elseif ( isset( $oembed->provider_name ) ) {
+				$anchor_text = sprintf( __( 'View on %s', 'buddypress' ), $oembed->provider_name );
+			}
+
+			if ( true === isset( $anchor_text ) )  {
+				$caption .= sprintf( '<a rel="nofollow" href="%1$s" onclick="top.location.href=\'%1$s\'">%2$s</a>', esc_url( $url ), apply_filters( 'the_title', $anchor_text ) );
+			}
+
+			// Set up caption.
+			if ( '' !== $caption ) {
+				$css_class = isset( $oembed->provider_name ) ? sprintf( ' provider-%s', sanitize_html_class( strtolower( $oembed->provider_name ) ) ) : '';
+				$caption = sprintf( '<div class="caption%1$s" style="width:%2$s">%3$s</div>',
+					$css_class,
+					$thumb_width > $float_width ? 100 . '%' : round( ( $width - (int) $thumb_width ) / $width * 100 ) . '%',
+					$caption
+				);
+
+				$content .= $caption;
+			}
+		}
+
+		// Print rich content.
+		if ( '' !== $content ) {
+			printf( '<div class="bp-activity-embed-display-media %s" style="max-width:%spx">%s</div>',
+				$thumb_width < $float_width ? 'two-col' : 'one-col',
+				$thumb_width < $float_width ? $width : $thumb_width,
+				$content
+			);
+		}
+
+	// Video / audio.
+	} elseif ( true === $allow_media ) {
+		// Call BP_Embed if it hasn't already loaded.
+		bp_embed_init();
+
+		// Run shortcode and embed routine.
+		$content = buddypress()->embed->run_shortcode( $GLOBALS['activities_template']->activity->content );
+		$content = buddypress()->embed->autoembed( $content );
+
+		// Try to find inline video / audio.
+		$media = bp_core_extract_media_from_content( $content, 96 );
+
+		// Video takes precedence. HTML5-only.
+		if ( isset( $media['videos'] ) && 'shortcodes' === $media['videos'][0]['source'] ) {
+			printf( '<video controls preload="metadata"><source src="%1$s"><p>%2$s</p></video>',
+				esc_url( $media['videos'][0]['url'] ),
+				esc_html__( 'Your browser does not support HTML5 video', 'buddypress' )
+			);
+
+		// No video? Try audio. HTML5-only.
+		} elseif ( isset( $media['audio'] ) && 'shortcodes' === $media['audio'][0]['source'] ) {
+			printf( '<audio controls preload="metadata"><source src="%1$s"><p>%2$s</p></audio>',
+				esc_url( $media['audio'][0]['url'] ),
+				esc_html__( 'Your browser does not support HTML5 audio', 'buddypress' )
+			);
+		}
+
+	}
+
+	/** This hook is documented in /bp-activity/bp-activity-embeds.php */
+	do_action( 'bp_activity_embed_after_media' );
+}
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-filters.php	(revision 12265)
@@ -0,0 +1,824 @@
+<?php
+/**
+ * Filters related to the Activity component.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityFilters
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/* Filters *******************************************************************/
+
+// Apply WordPress defined filters.
+add_filter( 'bp_get_activity_action',                'bp_activity_filter_kses', 1 );
+add_filter( 'bp_get_activity_content_body',          'bp_activity_filter_kses', 1 );
+add_filter( 'bp_get_activity_content',               'bp_activity_filter_kses', 1 );
+add_filter( 'bp_get_activity_parent_content',        'bp_activity_filter_kses', 1 );
+add_filter( 'bp_get_activity_latest_update',         'bp_activity_filter_kses', 1 );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'bp_activity_filter_kses', 1 );
+add_filter( 'bp_get_activity_feed_item_description', 'bp_activity_filter_kses', 1 );
+add_filter( 'bp_activity_content_before_save',       'bp_activity_filter_kses', 1 );
+add_filter( 'bp_activity_action_before_save',        'bp_activity_filter_kses', 1 );
+add_filter( 'bp_activity_latest_update_content',     'bp_activity_filter_kses', 1 );
+
+add_filter( 'bp_get_activity_action',                'force_balance_tags' );
+add_filter( 'bp_get_activity_content_body',          'force_balance_tags' );
+add_filter( 'bp_get_activity_content',               'force_balance_tags' );
+add_filter( 'bp_get_activity_latest_update',         'force_balance_tags' );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'force_balance_tags' );
+add_filter( 'bp_get_activity_feed_item_description', 'force_balance_tags' );
+add_filter( 'bp_activity_content_before_save',       'force_balance_tags' );
+add_filter( 'bp_activity_action_before_save',        'force_balance_tags' );
+
+if ( function_exists( 'wp_encode_emoji' ) ) {
+	add_filter( 'bp_activity_content_before_save', 'wp_encode_emoji' );
+}
+
+add_filter( 'bp_get_activity_action',                'wptexturize' );
+add_filter( 'bp_get_activity_content_body',          'wptexturize' );
+add_filter( 'bp_get_activity_content',               'wptexturize' );
+add_filter( 'bp_get_activity_parent_content',        'wptexturize' );
+add_filter( 'bp_get_activity_latest_update',         'wptexturize' );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'wptexturize' );
+add_filter( 'bp_activity_get_embed_excerpt',         'wptexturize' );
+
+add_filter( 'bp_get_activity_action',                'convert_smilies' );
+add_filter( 'bp_get_activity_content_body',          'convert_smilies' );
+add_filter( 'bp_get_activity_content',               'convert_smilies' );
+add_filter( 'bp_get_activity_parent_content',        'convert_smilies' );
+add_filter( 'bp_get_activity_latest_update',         'convert_smilies' );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'convert_smilies' );
+add_filter( 'bp_activity_get_embed_excerpt',         'convert_smilies' );
+
+add_filter( 'bp_get_activity_action',                'convert_chars' );
+add_filter( 'bp_get_activity_content_body',          'convert_chars' );
+add_filter( 'bp_get_activity_content',               'convert_chars' );
+add_filter( 'bp_get_activity_parent_content',        'convert_chars' );
+add_filter( 'bp_get_activity_latest_update',         'convert_chars' );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'convert_chars' );
+add_filter( 'bp_activity_get_embed_excerpt',         'convert_chars' );
+
+add_filter( 'bp_get_activity_action',                'wpautop' );
+add_filter( 'bp_get_activity_content_body',          'wpautop' );
+add_filter( 'bp_get_activity_content',               'wpautop' );
+add_filter( 'bp_get_activity_feed_item_description', 'wpautop' );
+add_filter( 'bp_activity_get_embed_excerpt',         'wpautop' );
+
+add_filter( 'bp_get_activity_action',                'make_clickable', 9 );
+add_filter( 'bp_get_activity_content_body',          'make_clickable', 9 );
+add_filter( 'bp_get_activity_content',               'make_clickable', 9 );
+add_filter( 'bp_get_activity_parent_content',        'make_clickable', 9 );
+add_filter( 'bp_get_activity_latest_update',         'make_clickable', 9 );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'make_clickable', 9 );
+add_filter( 'bp_get_activity_feed_item_description', 'make_clickable', 9 );
+add_filter( 'bp_activity_get_embed_excerpt',         'make_clickable', 9 );
+
+add_filter( 'bp_acomment_name',                      'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_action',                'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_content',               'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_content_body',          'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_parent_content',        'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_latest_update',         'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'stripslashes_deep', 5 );
+add_filter( 'bp_get_activity_feed_item_description', 'stripslashes_deep', 5 );
+
+add_filter( 'bp_activity_primary_link_before_save',  'esc_url_raw' );
+
+// Apply BuddyPress-defined filters.
+add_filter( 'bp_get_activity_content',               'bp_activity_make_nofollow_filter' );
+add_filter( 'bp_get_activity_content_body',          'bp_activity_make_nofollow_filter' );
+add_filter( 'bp_get_activity_parent_content',        'bp_activity_make_nofollow_filter' );
+add_filter( 'bp_get_activity_latest_update',         'bp_activity_make_nofollow_filter' );
+add_filter( 'bp_get_activity_latest_update_excerpt', 'bp_activity_make_nofollow_filter' );
+add_filter( 'bp_get_activity_feed_item_description', 'bp_activity_make_nofollow_filter' );
+
+add_filter( 'pre_comment_content',                   'bp_activity_at_name_filter' );
+add_filter( 'the_content',                           'bp_activity_at_name_filter' );
+add_filter( 'bp_activity_get_embed_excerpt',         'bp_activity_at_name_filter' );
+
+add_filter( 'bp_get_activity_parent_content',        'bp_create_excerpt' );
+
+add_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+add_filter( 'bp_get_activity_content',      'bp_activity_truncate_entry', 5 );
+
+add_filter( 'bp_get_total_favorite_count_for_user', 'bp_core_number_format' );
+add_filter( 'bp_get_total_mention_count_for_user',  'bp_core_number_format' );
+
+add_filter( 'bp_activity_get_embed_excerpt', 'bp_activity_embed_excerpt_onclick_location_filter', 9 );
+
+// Personal data export.
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_activity_register_personal_data_exporter' );
+
+/* Actions *******************************************************************/
+
+// At-name filter.
+add_action( 'bp_activity_before_save', 'bp_activity_at_name_filter_updates' );
+
+// Activity stream moderation.
+add_action( 'bp_activity_before_save', 'bp_activity_check_moderation_keys', 2, 1 );
+add_action( 'bp_activity_before_save', 'bp_activity_check_blacklist_keys',  2, 1 );
+
+/** Functions *****************************************************************/
+
+/**
+ * Types of activity stream items to moderate.
+ *
+ * @since 1.6.0
+ *
+ * @return array $types List of the activity types to moderate.
+ */
+function bp_activity_get_moderated_activity_types() {
+	$types = array(
+		'activity_comment',
+		'activity_update'
+	);
+
+	/**
+	 * Filters the default activity types that BuddyPress should moderate.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $types Default activity types to moderate.
+	 */
+	return apply_filters( 'bp_activity_check_activity_types', $types );
+}
+
+/**
+ * Moderate the posted activity item, if it contains moderate keywords.
+ *
+ * @since 1.6.0
+ *
+ * @param BP_Activity_Activity $activity The activity object to check.
+ */
+function bp_activity_check_moderation_keys( $activity ) {
+
+	// Only check specific types of activity updates.
+	if ( ! in_array( $activity->type, bp_activity_get_moderated_activity_types() ) ) {
+		return;
+	}
+
+	// Send back the error so activity update fails.
+	// @todo This is temporary until some kind of moderation is built.
+	$moderate = bp_core_check_for_moderation( $activity->user_id, '', $activity->content, 'wp_error' );
+	if ( is_wp_error( $moderate ) ) {
+		$activity->errors = $moderate;
+
+		// Backpat.
+		$activity->component = false;
+	}
+}
+
+/**
+ * Mark the posted activity as spam, if it contains blacklist keywords.
+ *
+ * @since 1.6.0
+ *
+ * @param BP_Activity_Activity $activity The activity object to check.
+ */
+function bp_activity_check_blacklist_keys( $activity ) {
+
+	// Only check specific types of activity updates.
+	if ( ! in_array( $activity->type, bp_activity_get_moderated_activity_types() ) ) {
+		return;
+	}
+
+	// Send back the error so activity update fails.
+	// @todo This is temporary until some kind of trash status is built.
+	$blacklist = bp_core_check_for_blacklist( $activity->user_id, '', $activity->content, 'wp_error' );
+	if ( is_wp_error( $blacklist ) ) {
+		$activity->errors = $blacklist;
+
+		// Backpat.
+		$activity->component = false;
+	}
+}
+
+/**
+ * Custom kses filtering for activity content.
+ *
+ * @since 1.1.0
+ *
+ * @param string $content The activity content.
+ * @return string $content Filtered activity content.
+ */
+function bp_activity_filter_kses( $content ) {
+	/**
+	 * Filters the allowed HTML tags for BuddyPress Activity content.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $value Array of allowed HTML tags and attributes.
+	 */
+	$activity_allowedtags = apply_filters( 'bp_activity_allowed_tags', bp_get_allowedtags() );
+	return wp_kses( $content, $activity_allowedtags );
+}
+
+/**
+ * Find and link @-mentioned users in the contents of a given item.
+ *
+ * @since 1.2.0
+ *
+ * @param string $content     The contents of a given item.
+ * @param int    $activity_id The activity id. Deprecated.
+ * @return string $content Content filtered for mentions.
+ */
+function bp_activity_at_name_filter( $content, $activity_id = 0 ) {
+
+	// Are mentions disabled?
+	if ( ! bp_activity_do_mentions() ) {
+		return $content;
+	}
+
+	// Try to find mentions.
+	$usernames = bp_activity_find_mentions( $content );
+
+	// No mentions? Stop now!
+	if ( empty( $usernames ) )
+		return $content;
+
+	// We don't want to link @mentions that are inside of links, so we
+	// temporarily remove them.
+	$replace_count = 0;
+	$replacements = array();
+	foreach ( $usernames as $username ) {
+		// Prevent @ name linking inside <a> tags.
+		preg_match_all( '/(<a.*?(?!<\/a>)@' . $username . '.*?<\/a>)/', $content, $content_matches );
+		if ( ! empty( $content_matches[1] ) ) {
+			foreach ( $content_matches[1] as $replacement ) {
+				$replacements[ '#BPAN' . $replace_count ] = $replacement;
+				$content = str_replace( $replacement, '#BPAN' . $replace_count, $content );
+				$replace_count++;
+			}
+		}
+	}
+
+	// Linkify the mentions with the username.
+	foreach ( (array) $usernames as $user_id => $username ) {
+		$content = preg_replace( '/(@' . $username . '\b)/', "<a class='bp-suggestions-mention' href='" . bp_core_get_user_domain( $user_id ) . "' rel='nofollow'>@$username</a>", $content );
+	}
+
+	// Put everything back.
+	if ( ! empty( $replacements ) ) {
+		foreach ( $replacements as $placeholder => $original ) {
+			$content = str_replace( $placeholder, $original, $content );
+		}
+	}
+
+	// Return the content.
+	return $content;
+}
+
+/**
+ * Catch mentions in an activity item before it is saved into the database.
+ *
+ * If mentions are found, replace @mention text with user links and add our
+ * hook to send mention notifications after the activity item is saved.
+ *
+ * @since 1.5.0
+ *
+ * @param BP_Activity_Activity $activity Activity Object.
+ */
+function bp_activity_at_name_filter_updates( $activity ) {
+	// Are mentions disabled?
+	if ( ! bp_activity_do_mentions() ) {
+		return;
+	}
+
+	// If activity was marked as spam, stop the rest of this function.
+	if ( ! empty( $activity->is_spam ) )
+		return;
+
+	// Try to find mentions.
+	$usernames = bp_activity_find_mentions( $activity->content );
+
+	// We have mentions!
+	if ( ! empty( $usernames ) ) {
+		// Replace @mention text with userlinks.
+		foreach( (array) $usernames as $user_id => $username ) {
+			$activity->content = preg_replace( '/(@' . $username . '\b)/', "<a class='bp-suggestions-mention' href='" . bp_core_get_user_domain( $user_id ) . "' rel='nofollow'>@$username</a>", $activity->content );
+		}
+
+		// Add our hook to send @mention emails after the activity item is saved.
+		add_action( 'bp_activity_after_save', 'bp_activity_at_name_send_emails' );
+
+		// Temporary variable to avoid having to run bp_activity_find_mentions() again.
+		buddypress()->activity->mentioned_users = $usernames;
+	}
+}
+
+/**
+ * Sends emails and BP notifications for users @-mentioned in an activity item.
+ *
+ * @since 1.7.0
+ *
+ * @param BP_Activity_Activity $activity The BP_Activity_Activity object.
+ */
+function bp_activity_at_name_send_emails( $activity ) {
+	// Are mentions disabled?
+	if ( ! bp_activity_do_mentions() ) {
+		return;
+	}
+
+	// If our temporary variable doesn't exist, stop now.
+	if ( empty( buddypress()->activity->mentioned_users ) )
+		return;
+
+	// Grab our temporary variable from bp_activity_at_name_filter_updates().
+	$usernames = buddypress()->activity->mentioned_users;
+
+	// Get rid of temporary variable.
+	unset( buddypress()->activity->mentioned_users );
+
+	// Send @mentions and setup BP notifications.
+	foreach( (array) $usernames as $user_id => $username ) {
+
+		/**
+		 * Filters BuddyPress' ability to send email notifications for @mentions.
+		 *
+		 * @since 1.6.0
+		 * @since 2.5.0 Introduced `$user_id` and `$activity` parameters.
+		 *
+		 * @param bool                 $value     Whether or not BuddyPress should send a notification to the mentioned users.
+		 * @param array                $usernames Array of users potentially notified.
+		 * @param int                  $user_id   ID of the current user being notified.
+		 * @param BP_Activity_Activity $activity  Activity object.
+		 */
+		if ( apply_filters( 'bp_activity_at_name_do_notifications', true, $usernames, $user_id, $activity ) ) {
+			bp_activity_at_message_notification( $activity->id, $user_id );
+		}
+
+		// Updates mention count for the user.
+		bp_activity_update_mention_count_for_user( $user_id, $activity->id );
+	}
+}
+
+/**
+ * Catch links in activity text so rel=nofollow can be added.
+ *
+ * @since 1.2.0
+ *
+ * @param string $text Activity text.
+ * @return string $text Text with rel=nofollow added to any links.
+ */
+function bp_activity_make_nofollow_filter( $text ) {
+	return preg_replace_callback( '|<a (.+?)>|i', 'bp_activity_make_nofollow_filter_callback', $text );
+}
+
+	/**
+	 * Add rel=nofollow to a link.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $matches Items matched by preg_replace_callback() in bp_activity_make_nofollow_filter().
+	 * @return string $text Link with rel=nofollow added.
+	 */
+	function bp_activity_make_nofollow_filter_callback( $matches ) {
+		$text = $matches[1];
+		$text = str_replace( array( ' rel="nofollow"', " rel='nofollow'"), '', $text );
+		return "<a $text rel=\"nofollow\">";
+	}
+
+/**
+ * Truncate long activity entries when viewed in activity streams.
+ *
+ * This method can only be used inside the Activity loop.
+ *
+ * @since 1.5.0
+ * @since 2.6.0 Added $args parameter.
+ *
+ * @param string $text The original activity entry text.
+ * @param array  $args {
+ *     Optional parameters. See $options argument of {@link bp_create_excerpt()}
+ *     for all available parameters.
+ * }
+ * @return string $excerpt The truncated text.
+ */
+function bp_activity_truncate_entry( $text, $args = array() ) {
+	global $activities_template;
+
+	/**
+	 * Provides a filter that lets you choose whether to skip this filter on a per-activity basis.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param bool $value If true, text should be checked to see if it needs truncating.
+	 */
+	$maybe_truncate_text = apply_filters(
+		'bp_activity_maybe_truncate_entry',
+		isset( $activities_template->activity->type ) && ! in_array( $activities_template->activity->type, array( 'new_blog_post', ), true )
+	);
+
+	// The full text of the activity update should always show on the single activity screen.
+	if ( empty( $args['force_truncate'] ) && ( ! $maybe_truncate_text || bp_is_single_activity() ) ) {
+		return $text;
+	}
+
+	/**
+	 * Filters the appended text for the activity excerpt.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value Internationalized "Read more" text.
+	 */
+	$append_text    = apply_filters( 'bp_activity_excerpt_append_text', __( '[Read more]', 'buddypress' ) );
+
+	$excerpt_length = bp_activity_get_excerpt_length();
+
+	$args = wp_parse_args( $args, array( 'ending' => __( '&hellip;', 'buddypress' ) ) );
+
+	// Run the text through the excerpt function. If it's too short, the original text will be returned.
+	$excerpt        = bp_create_excerpt( $text, $excerpt_length, $args );
+
+	/*
+	 * If the text returned by bp_create_excerpt() is different from the original text (ie it's
+	 * been truncated), add the "Read More" link. Note that bp_create_excerpt() is stripping
+	 * shortcodes, so we have strip them from the $text before the comparison.
+	 */
+	if ( strlen( $excerpt ) < strlen( strip_shortcodes( $text ) ) ) {
+		$id = !empty( $activities_template->activity->current_comment->id ) ? 'acomment-read-more-' . $activities_template->activity->current_comment->id : 'activity-read-more-' . bp_get_activity_id();
+
+		$excerpt = sprintf( '%1$s<span class="activity-read-more" id="%2$s"><a href="%3$s" rel="nofollow">%4$s</a></span>', $excerpt, $id, bp_get_activity_thread_permalink(), $append_text );
+	}
+
+	/**
+	 * Filters the composite activity excerpt entry.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $excerpt     Excerpt text and markup to be displayed.
+	 * @param string $text        The original activity entry text.
+	 * @param string $append_text The final append text applied.
+	 */
+	return apply_filters( 'bp_activity_truncate_entry', $excerpt, $text, $append_text );
+}
+
+/**
+ * Include extra JavaScript dependencies for activity component.
+ *
+ * @since 2.0.0
+ *
+ * @param array $js_handles The original dependencies.
+ * @return array $js_handles The new dependencies.
+ */
+function bp_activity_get_js_dependencies( $js_handles = array() ) {
+	if ( bp_activity_do_heartbeat() ) {
+		$js_handles[] = 'heartbeat';
+	}
+
+	return $js_handles;
+}
+add_filter( 'bp_core_get_js_dependencies', 'bp_activity_get_js_dependencies', 10, 1 );
+
+/**
+ * Add a just-posted classes to the most recent activity item.
+ *
+ * We use these classes to avoid pagination issues when items are loaded
+ * dynamically into the activity stream.
+ *
+ * @since 2.0.0
+ *
+ * @param string $classes Array of classes for most recent activity item.
+ * @return string $classes
+ */
+function bp_activity_newest_class( $classes = '' ) {
+	$bp = buddypress();
+
+	if ( ! empty( $bp->activity->last_recorded ) && $bp->activity->last_recorded == bp_get_activity_date_recorded() ) {
+		$classes .= ' new-update';
+	}
+
+	$classes .= ' just-posted';
+	return $classes;
+}
+
+/**
+ * Check if Activity Heartbeat feature i on to add a timestamp class.
+ *
+ * @since 2.0.0
+ *
+ * @param string $classes Array of classes for timestamp.
+ * @return string $classes
+ */
+function bp_activity_timestamp_class( $classes = '' ) {
+
+	if ( ! bp_activity_do_heartbeat() ) {
+		return $classes;
+	}
+
+	$activity_date = bp_get_activity_date_recorded();
+
+	if ( empty( $activity_date ) ) {
+		return $classes;
+	}
+
+	$classes .= ' date-recorded-' . strtotime( $activity_date );
+
+	return $classes;
+}
+add_filter( 'bp_get_activity_css_class', 'bp_activity_timestamp_class', 9, 1 );
+
+/**
+ * Use WordPress Heartbeat API to check for latest activity update.
+ *
+ * @since 2.0.0
+ *
+ * @param array $response Array containing Heartbeat API response.
+ * @param array $data     Array containing data for Heartbeat API response.
+ * @return array $response
+ */
+function bp_activity_heartbeat_last_recorded( $response = array(), $data = array() ) {
+	if ( empty( $data['bp_activity_last_recorded'] ) ) {
+		return $response;
+	}
+
+	// Use the querystring argument stored in the cookie (to preserve
+	// filters), but force the offset to get only new items.
+	$activity_latest_args = bp_parse_args(
+		bp_ajax_querystring( 'activity' ),
+		array( 'since' => date( 'Y-m-d H:i:s', $data['bp_activity_last_recorded'] ) ),
+		'activity_latest_args'
+	);
+
+	if ( ! empty( $data['bp_activity_last_recorded_search_terms'] ) && empty( $activity_latest_args['search_terms'] ) ) {
+		$activity_latest_args['search_terms'] = addslashes( $data['bp_activity_last_recorded_search_terms'] );
+	}
+
+	$newest_activities = array();
+	$last_activity_recorded = 0;
+
+	// Temporarily add a just-posted class for new activity items.
+	add_filter( 'bp_get_activity_css_class', 'bp_activity_newest_class', 10, 1 );
+
+	ob_start();
+	if ( bp_has_activities( $activity_latest_args ) ) {
+		while ( bp_activities() ) {
+			bp_the_activity();
+
+			$atime = strtotime( bp_get_activity_date_recorded() );
+			if ( $last_activity_recorded < $atime ) {
+				$last_activity_recorded = $atime;
+			}
+
+			bp_get_template_part( 'activity/entry' );
+		}
+	}
+
+	$newest_activities['activities']    = ob_get_contents();
+	$newest_activities['last_recorded'] = $last_activity_recorded;
+	ob_end_clean();
+
+	// Remove the temporary filter.
+	remove_filter( 'bp_get_activity_css_class', 'bp_activity_newest_class', 10 );
+
+	if ( ! empty( $newest_activities['last_recorded'] ) ) {
+		$response['bp_activity_newest_activities'] = $newest_activities;
+	}
+
+	return $response;
+}
+add_filter( 'heartbeat_received', 'bp_activity_heartbeat_last_recorded', 10, 2 );
+add_filter( 'heartbeat_nopriv_received', 'bp_activity_heartbeat_last_recorded', 10, 2 );
+
+/**
+ * Set the strings for WP HeartBeat API where needed.
+ *
+ * @since 2.0.0
+ *
+ * @param array $strings Localized strings.
+ * @return array $strings
+ */
+function bp_activity_heartbeat_strings( $strings = array() ) {
+
+	if ( ! bp_activity_do_heartbeat() ) {
+		return $strings;
+	}
+
+	$global_pulse = 0;
+
+	/**
+	 * Filter that checks whether the global heartbeat settings already exist.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $value Heartbeat settings array.
+	 */
+	$heartbeat_settings = apply_filters( 'heartbeat_settings', array() );
+	if ( ! empty( $heartbeat_settings['interval'] ) ) {
+		// 'Fast' is 5
+		$global_pulse = is_numeric( $heartbeat_settings['interval'] ) ? absint( $heartbeat_settings['interval'] ) : 5;
+	}
+
+	/**
+	 * Filters the pulse frequency to be used for the BuddyPress Activity heartbeat.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int $value The frequency in seconds between pulses.
+	 */
+	$bp_activity_pulse = apply_filters( 'bp_activity_heartbeat_pulse', 15 );
+
+	/**
+	 * Use the global pulse value unless:
+	 * a. the BP-specific value has been specifically filtered, or
+	 * b. it doesn't exist (ie, BP will be the only one using the heartbeat,
+	 *    so we're responsible for enabling it)
+	 */
+	if ( has_filter( 'bp_activity_heartbeat_pulse' ) || empty( $global_pulse ) ) {
+		$pulse = $bp_activity_pulse;
+	} else {
+		$pulse = $global_pulse;
+	}
+
+	$strings = array_merge( $strings, array(
+		'newest' => __( 'Load Newest', 'buddypress' ),
+		'pulse'  => absint( $pulse ),
+	) );
+
+	return $strings;
+}
+add_filter( 'bp_core_get_js_strings', 'bp_activity_heartbeat_strings', 10, 1 );
+
+/** Scopes ********************************************************************/
+
+/**
+ * Set up activity arguments for use with the 'just-me' scope.
+ *
+ * @since 2.2.0
+ *
+ * @param array $retval Empty array by default.
+ * @param array $filter Current activity arguments.
+ * @return array $retval
+ */
+function bp_activity_filter_just_me_scope( $retval = array(), $filter = array() ) {
+
+	// Determine the user_id.
+	if ( ! empty( $filter['user_id'] ) ) {
+		$user_id = $filter['user_id'];
+	} else {
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+	}
+
+	// Should we show all items regardless of sitewide visibility?
+	$show_hidden = array();
+	if ( ! empty( $user_id ) && $user_id !== bp_loggedin_user_id() ) {
+		$show_hidden = array(
+			'column' => 'hide_sitewide',
+			'value'  => 0
+		);
+	}
+
+	$retval = array(
+		'relation' => 'AND',
+		array(
+			'column' => 'user_id',
+			'value'  => $user_id
+		),
+		$show_hidden,
+
+		// Overrides.
+		'override' => array(
+			'display_comments' => 'stream',
+			'filter'           => array( 'user_id' => 0 ),
+			'show_hidden'      => true
+		),
+	);
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_just-me_scope_args', 'bp_activity_filter_just_me_scope', 10, 2 );
+
+/**
+ * Set up activity arguments for use with the 'favorites' scope.
+ *
+ * @since 2.2.0
+ *
+ * @param array $retval Empty array by default.
+ * @param array $filter Current activity arguments.
+ * @return array $retval
+ */
+function bp_activity_filter_favorites_scope( $retval = array(), $filter = array() ) {
+
+	// Determine the user_id.
+	if ( ! empty( $filter['user_id'] ) ) {
+		$user_id = $filter['user_id'];
+	} else {
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+	}
+
+	// Determine the favorites.
+	$favs = bp_activity_get_user_favorites( $user_id );
+	if ( empty( $favs ) ) {
+		$favs = array( 0 );
+	}
+
+	// Should we show all items regardless of sitewide visibility?
+	$show_hidden = array();
+	if ( ! empty( $user_id ) && ( $user_id !== bp_loggedin_user_id() ) ) {
+		$show_hidden = array(
+			'column' => 'hide_sitewide',
+			'value'  => 0
+		);
+	}
+
+	$retval = array(
+		'relation' => 'AND',
+		array(
+			'column'  => 'id',
+			'compare' => 'IN',
+			'value'   => (array) $favs
+		),
+		$show_hidden,
+
+		// Overrides.
+		'override' => array(
+			'display_comments' => true,
+			'filter'           => array( 'user_id' => 0 ),
+			'show_hidden'      => true
+		),
+	);
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_favorites_scope_args', 'bp_activity_filter_favorites_scope', 10, 2 );
+
+
+/**
+ * Set up activity arguments for use with the 'favorites' scope.
+ *
+ * @since 2.2.0
+ *
+ * @param array $retval Empty array by default.
+ * @param array $filter Current activity arguments.
+ * @return array $retval
+ */
+function bp_activity_filter_mentions_scope( $retval = array(), $filter = array() ) {
+
+	// Are mentions disabled?
+	if ( ! bp_activity_do_mentions() ) {
+		return $retval;
+	}
+
+	// Determine the user_id.
+	if ( ! empty( $filter['user_id'] ) ) {
+		$user_id = $filter['user_id'];
+	} else {
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+	}
+
+	// Should we show all items regardless of sitewide visibility?
+	$show_hidden = array();
+	if ( ! empty( $user_id ) && $user_id !== bp_loggedin_user_id() ) {
+		$show_hidden = array(
+			'column' => 'hide_sitewide',
+			'value'  => 0
+		);
+	}
+
+	$retval = array(
+		'relation' => 'AND',
+		array(
+			'column'  => 'content',
+			'compare' => 'LIKE',
+
+			// Start search at @ symbol and stop search at closing tag delimiter.
+			'value'   => '@' . bp_activity_get_user_mentionname( $user_id ) . '<'
+		),
+		$show_hidden,
+
+		// Overrides.
+		'override' => array(
+			'display_comments' => 'stream',
+			'filter'           => array( 'user_id' => 0 ),
+			'show_hidden'      => true
+		),
+	);
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_mentions_scope_args', 'bp_activity_filter_mentions_scope', 10, 2 );
+
+/**
+ * Registers Activity personal data exporter.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_activity_register_personal_data_exporter( $exporters ) {
+	$exporters['buddypress-activity'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Activity Data', 'buddypress' ),
+		'callback'               => 'bp_activity_personal_data_exporter',
+	);
+
+	return $exporters;
+}
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-functions.php	(revision 12265)
@@ -0,0 +1,4266 @@
+<?php
+/**
+ * BuddyPress Activity Functions.
+ *
+ * Functions for the Activity Streams component.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Check whether the $bp global lists an activity directory page.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if activity directory page is found, otherwise false.
+ */
+function bp_activity_has_directory() {
+	return (bool) !empty( buddypress()->pages->activity->id );
+}
+
+/**
+ * Are mentions enabled or disabled?
+ *
+ * The Mentions feature does a number of things, all of which will be turned
+ * off if you disable mentions:
+ *   - Detecting and auto-linking @username in all BP/WP content.
+ *   - Sending BP notifications and emails to users when they are mentioned
+ *     using the @username syntax.
+ *   - The Public Message button on user profiles.
+ *
+ * Mentions are enabled by default. To disable, put the following line in
+ * bp-custom.php or your theme's functions.php file:
+ *
+ *   add_filter( 'bp_activity_do_mentions', '__return_false' );
+ *
+ * @since 1.8.0
+ *
+ * @return bool $retval True to enable mentions, false to disable.
+ */
+function bp_activity_do_mentions() {
+
+	/**
+	 * Filters whether or not mentions are enabled.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param bool $enabled True to enable mentions, false to disable.
+	 */
+	return (bool) apply_filters( 'bp_activity_do_mentions', true );
+}
+
+/**
+ * Should BuddyPress load the mentions scripts and related assets, including results to prime the
+ * mentions suggestions?
+ *
+ * @since 2.1.0
+ *
+ * @return bool True if mentions scripts should be loaded.
+ */
+function bp_activity_maybe_load_mentions_scripts() {
+	$mentions_enabled = bp_activity_do_mentions() && bp_is_user_active();
+	$load_mentions    = $mentions_enabled && ( bp_is_activity_component() || is_admin() );
+
+	/**
+	 * Filters whether or not BuddyPress should load mentions scripts and assets.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $load_mentions    True to load mentions assets, false otherwise.
+	 * @param bool $mentions_enabled True if mentions are enabled.
+	 */
+	return (bool) apply_filters( 'bp_activity_maybe_load_mentions_scripts', $load_mentions, $mentions_enabled );
+}
+
+/**
+ * Locate usernames in an activity content string, as designated by an @ sign.
+ *
+ * @since 1.5.0
+ *
+ * @param string $content The content of the activity, usually found in
+ *                        $activity->content.
+ * @return array|bool Associative array with user ID as key and username as
+ *                    value. Boolean false if no mentions found.
+ */
+function bp_activity_find_mentions( $content ) {
+
+	$pattern = '/[@]+([A-Za-z0-9-_\.@]+)\b/';
+	preg_match_all( $pattern, $content, $usernames );
+
+	// Make sure there's only one instance of each username.
+	$usernames = array_unique( $usernames[1] );
+
+	// Bail if no usernames.
+	if ( empty( $usernames ) ) {
+		return false;
+	}
+
+	$mentioned_users = array();
+
+	// We've found some mentions! Check to see if users exist.
+	foreach( (array) array_values( $usernames ) as $username ) {
+		$user_id = bp_activity_get_userid_from_mentionname( $username );
+
+		// The user ID exists, so let's add it to our array.
+		if ( ! empty( $user_id ) ) {
+			$mentioned_users[ $user_id ] = $username;
+		}
+	}
+
+	if ( empty( $mentioned_users ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters the mentioned users.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $mentioned_users Associative array with user IDs as keys and usernames as values.
+	 */
+	return apply_filters( 'bp_activity_mentioned_users', $mentioned_users );
+}
+
+/**
+ * Reset a user's unread mentions list and count.
+ *
+ * @since 1.5.0
+ *
+ * @param int $user_id The id of the user whose unread mentions are being reset.
+ */
+function bp_activity_clear_new_mentions( $user_id ) {
+	bp_delete_user_meta( $user_id, 'bp_new_mention_count' );
+	bp_delete_user_meta( $user_id, 'bp_new_mentions'      );
+
+	/**
+	 * Fires once mentions has been reset for a given user.
+	 *
+	 * @since  2.5.0
+	 *
+	 * @param  int $user_id The id of the user whose unread mentions are being reset.
+	 */
+	do_action( 'bp_activity_clear_new_mentions', $user_id );
+}
+
+/**
+ * Adjusts mention count for mentioned users in activity items.
+ *
+ * This function is useful if you only have the activity ID handy and you
+ * haven't parsed an activity item for @mentions yet.
+ *
+ * Currently, only used in {@link bp_activity_delete()}.
+ *
+ * @since 1.5.0
+ *
+ * @param int    $activity_id The unique id for the activity item.
+ * @param string $action      Can be 'delete' or 'add'. Defaults to 'add'.
+ * @return bool
+ */
+function bp_activity_adjust_mention_count( $activity_id = 0, $action = 'add' ) {
+
+	// Bail if no activity ID passed.
+	if ( empty( $activity_id ) ) {
+		return false;
+	}
+
+	// Get activity object.
+	$activity  = new BP_Activity_Activity( $activity_id );
+
+	// Try to find mentions.
+	$usernames = bp_activity_find_mentions( strip_tags( $activity->content ) );
+
+	// Still empty? Stop now.
+	if ( empty( $usernames ) ) {
+		return false;
+	}
+
+	// Increment mention count foreach mentioned user.
+	foreach( (array) array_keys( $usernames ) as $user_id ) {
+		bp_activity_update_mention_count_for_user( $user_id, $activity_id, $action );
+	}
+}
+
+/**
+ * Update the mention count for a given user.
+ *
+ * This function should be used when you've already parsed your activity item
+ * for @mentions.
+ *
+ * @since 1.7.0
+ *
+ * @param int    $user_id     The user ID.
+ * @param int    $activity_id The unique ID for the activity item.
+ * @param string $action      'delete' or 'add'. Default: 'add'.
+ * @return bool
+ */
+function bp_activity_update_mention_count_for_user( $user_id, $activity_id, $action = 'add' ) {
+
+	if ( empty( $user_id ) || empty( $activity_id ) ) {
+		return false;
+	}
+
+	// Adjust the mention list and count for the member.
+	$new_mention_count = (int) bp_get_user_meta( $user_id, 'bp_new_mention_count', true );
+	$new_mentions      =       bp_get_user_meta( $user_id, 'bp_new_mentions',      true );
+
+	// Make sure new mentions is an array.
+	if ( empty( $new_mentions ) ) {
+		$new_mentions = array();
+	}
+
+	switch ( $action ) {
+		case 'delete' :
+			$key = array_search( $activity_id, $new_mentions );
+
+			if ( $key !== false ) {
+				unset( $new_mentions[$key] );
+			}
+
+			break;
+
+		case 'add' :
+		default :
+			if ( !in_array( $activity_id, $new_mentions ) ) {
+				$new_mentions[] = (int) $activity_id;
+			}
+
+			break;
+	}
+
+	// Get an updated mention count.
+	$new_mention_count = count( $new_mentions );
+
+	// Resave the user_meta.
+	bp_update_user_meta( $user_id, 'bp_new_mention_count', $new_mention_count );
+	bp_update_user_meta( $user_id, 'bp_new_mentions',      $new_mentions );
+
+	return true;
+}
+
+/**
+ * Determine a user's "mentionname", the name used for that user in @-mentions.
+ *
+ * @since 1.9.0
+ *
+ * @param int|string $user_id ID of the user to get @-mention name for.
+ * @return string $mentionname User name appropriate for @-mentions.
+ */
+function bp_activity_get_user_mentionname( $user_id ) {
+	$mentionname = '';
+
+	$userdata = bp_core_get_core_userdata( $user_id );
+
+	if ( $userdata ) {
+		if ( bp_is_username_compatibility_mode() ) {
+			$mentionname = str_replace( ' ', '-', $userdata->user_login );
+		} else {
+			$mentionname = $userdata->user_nicename;
+		}
+	}
+
+	return $mentionname;
+}
+
+/**
+ * Get a user ID from a "mentionname", the name used for a user in @-mentions.
+ *
+ * @since 1.9.0
+ *
+ * @param string $mentionname Username of user in @-mentions.
+ * @return int|bool ID of the user, if one is found. Otherwise false.
+ */
+function bp_activity_get_userid_from_mentionname( $mentionname ) {
+	$user_id = false;
+
+	/*
+	 * In username compatibility mode, hyphens are ambiguous between
+	 * actual hyphens and converted spaces.
+	 *
+	 * @todo There is the potential for username clashes between 'foo bar'
+	 * and 'foo-bar' in compatibility mode. Come up with a system for
+	 * unique mentionnames.
+	 */
+	if ( bp_is_username_compatibility_mode() ) {
+		// First, try the raw username.
+		$userdata = get_user_by( 'login', $mentionname );
+
+		// Doing a direct query to use proper regex. Necessary to
+		// account for hyphens + spaces in the same user_login.
+		if ( empty( $userdata ) || ! is_a( $userdata, 'WP_User' ) ) {
+			global $wpdb;
+			$regex   = esc_sql( str_replace( '-', '[ \-]', $mentionname ) );
+			$user_id = $wpdb->get_var( "SELECT ID FROM {$wpdb->users} WHERE user_login REGEXP '{$regex}'" );
+		} else {
+			$user_id = $userdata->ID;
+		}
+
+	// When username compatibility mode is disabled, the mentionname is
+	// the same as the nicename.
+	} else {
+		$user_id = bp_core_get_userid_from_nicename( $mentionname );
+	}
+
+
+	return $user_id;
+}
+
+/** Actions ******************************************************************/
+
+/**
+ * Register an activity 'type' and its action description/callback.
+ *
+ * Activity actions are strings used to describe items in the activity stream,
+ * such as 'Joe became a registered member' or 'Bill and Susie are now
+ * friends'. Each activity type (such as 'new_member' or 'friendship_created')
+ * used by a component should be registered using this function.
+ *
+ * While it's possible to post items to the activity stream whose types are
+ * not registered using bp_activity_set_action(), it is not recommended;
+ * unregistered types will not be displayed properly in the activity admin
+ * panel, and dynamic action generation (which is essential for multilingual
+ * sites, etc) will not work.
+ *
+ * @since 1.1.0
+ *
+ * @param  string        $component_id    The unique string ID of the component.
+ * @param  string        $type            The action type.
+ * @param  string        $description     The action description.
+ * @param  callable|bool $format_callback Callback for formatting the action string.
+ * @param  string|bool   $label           String to describe this action in the activity stream filter dropdown.
+ * @param  array         $context         Optional. Activity stream contexts where the filter should appear.
+ *                                        Values: 'activity', 'member', 'member_groups', 'group'.
+ * @param  int           $position        Optional. The position of the action when listed in dropdowns.
+ * @return bool False if any param is empty, otherwise true.
+ */
+function bp_activity_set_action( $component_id, $type, $description, $format_callback = false, $label = false, $context = array(), $position = 0 ) {
+	$bp = buddypress();
+
+	// Return false if any of the above values are not set.
+	if ( empty( $component_id ) || empty( $type ) || empty( $description ) ) {
+		return false;
+	}
+
+	// Set activity action.
+	if ( ! isset( $bp->activity->actions ) || ! is_object( $bp->activity->actions ) ) {
+		$bp->activity->actions = new stdClass;
+	}
+
+	// Verify callback.
+	if ( ! is_callable( $format_callback ) ) {
+		$format_callback = '';
+	}
+
+	if ( ! isset( $bp->activity->actions->{$component_id} ) || ! is_object( $bp->activity->actions->{$component_id} ) ) {
+		$bp->activity->actions->{$component_id} = new stdClass;
+	}
+
+	/**
+	 * Filters the action type being set for the current activity item.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array    $array           Array of arguments for action type being set.
+	 * @param string   $component_id    ID of the current component being set.
+	 * @param string   $type            Action type being set.
+	 * @param string   $description     Action description for action being set.
+	 * @param callable $format_callback Callback for formatting the action string.
+	 * @param string   $label           String to describe this action in the activity stream filter dropdown.
+	 * @param array    $context         Activity stream contexts where the filter should appear. 'activity', 'member',
+	 *                                  'member_groups', 'group'.
+	 */
+	$bp->activity->actions->{$component_id}->{$type} = apply_filters( 'bp_activity_set_action', array(
+		'key'             => $type,
+		'value'           => $description,
+		'format_callback' => $format_callback,
+		'label'           => $label,
+		'context'         => $context,
+		'position'        => $position,
+	), $component_id, $type, $description, $format_callback, $label, $context );
+
+	// Sort the actions of the affected component.
+	$action_array = (array) $bp->activity->actions->{$component_id};
+	$action_array = bp_sort_by_key( $action_array, 'position', 'num' );
+
+	// Restore keys.
+	$bp->activity->actions->{$component_id} = new stdClass;
+	foreach ( $action_array as $key_ordered ) {
+		$bp->activity->actions->{$component_id}->{$key_ordered['key']} = $key_ordered;
+	}
+
+	return true;
+}
+
+/**
+ * Set tracking arguments for a given post type.
+ *
+ * @since 2.2.0
+ *
+ * @global $wp_post_types
+ *
+ * @param string $post_type The name of the post type, as registered with WordPress. Eg 'post' or 'page'.
+ * @param array  $args {
+ *     An associative array of tracking parameters. All items are optional.
+ *     @type string   $bp_activity_admin_filter String to use in the Dashboard > Activity dropdown.
+ *     @type string   $bp_activity_front_filter String to use in the front-end dropdown.
+ *     @type string   $bp_activity_new_post     String format to use for generating the activity action. Should be a
+ *                                              translatable string where %1$s is replaced by a user link and %2$s is
+ *                                              the URL of the newly created post.
+ *     @type string   $bp_activity_new_post_ms  String format to use for generating the activity action on Multisite.
+ *                                              Should be a translatable string where %1$s is replaced by a user link,
+ *                                              %2$s is the URL of the newly created post, and %3$s is a link to
+ *                                              the site.
+ *     @type string   $component_id             ID of the BuddyPress component to associate the activity item.
+ *     @type string   $action_id                Value for the 'type' param of the new activity item.
+ *     @type callable $format_callback          Callback for formatting the activity action string.
+ *                                              Default: 'bp_activity_format_activity_action_custom_post_type_post'.
+ *     @type array    $contexts                 The directory contexts in which the filter will show.
+ *                                              Default: array( 'activity' ).
+ *     @type array    $position                 Position of the item in filter dropdowns.
+ *     @type string   $singular                 Singular, translatable name of the post type item. If no value is
+ *                                              provided, it's pulled from the 'singular_name' of the post type.
+ *     @type bool     $activity_comment         Whether to allow comments on the activity items. Defaults to true if
+ *                                              the post type does not natively support comments, otherwise false.
+ * }
+ * @return bool
+ */
+function bp_activity_set_post_type_tracking_args( $post_type = '', $args = array() ) {
+	global $wp_post_types;
+
+	if ( empty( $wp_post_types[ $post_type ] ) || ! post_type_supports( $post_type, 'buddypress-activity' ) || ! is_array( $args ) ) {
+		return false;
+	}
+
+	$activity_labels = array(
+		/* Post labels */
+		'bp_activity_admin_filter',
+		'bp_activity_front_filter',
+		'bp_activity_new_post',
+		'bp_activity_new_post_ms',
+		/* Comment labels */
+		'bp_activity_comments_admin_filter',
+		'bp_activity_comments_front_filter',
+		'bp_activity_new_comment',
+		'bp_activity_new_comment_ms'
+	);
+
+	// Labels are loaded into the post type object.
+	foreach ( $activity_labels as $label_type ) {
+		if ( ! empty( $args[ $label_type ] ) ) {
+			$wp_post_types[ $post_type ]->labels->{$label_type} = $args[ $label_type ];
+			unset( $args[ $label_type ] );
+		}
+	}
+
+	// If there are any additional args, put them in the bp_activity attribute of the post type.
+	if ( ! empty( $args ) ) {
+		$wp_post_types[ $post_type ]->bp_activity = $args;
+	}
+}
+
+/**
+ * Get tracking arguments for a specific post type.
+ *
+ * @since 2.2.0
+ * @since 2.5.0 Add post type comments tracking args
+ *
+ * @param  string $post_type Name of the post type.
+ * @return object The tracking arguments of the post type.
+ */
+function bp_activity_get_post_type_tracking_args( $post_type ) {
+	if ( ! post_type_supports( $post_type, 'buddypress-activity' ) ) {
+		return false;
+	}
+
+	$post_type_object           = get_post_type_object( $post_type );
+	$post_type_support_comments = post_type_supports( $post_type, 'comments' );
+
+	$post_type_activity = array(
+		'component_id'            => buddypress()->activity->id,
+		'action_id'               => 'new_' . $post_type,
+		'format_callback'         => 'bp_activity_format_activity_action_custom_post_type_post',
+		'front_filter'            => $post_type_object->labels->name,
+		'contexts'                => array( 'activity' ),
+		'position'                => 0,
+		'singular'                => strtolower( $post_type_object->labels->singular_name ),
+		'activity_comment'        => ! $post_type_support_comments,
+		'comment_action_id'       => false,
+		'comment_format_callback' => 'bp_activity_format_activity_action_custom_post_type_comment',
+	);
+
+	if ( ! empty( $post_type_object->bp_activity ) ) {
+		$post_type_activity = bp_parse_args( (array) $post_type_object->bp_activity, $post_type_activity, $post_type . '_tracking_args' );
+	}
+
+	$post_type_activity = (object) $post_type_activity;
+
+	// Try to get the admin filter from the post type labels.
+	if ( ! empty( $post_type_object->labels->bp_activity_admin_filter ) ) {
+		$post_type_activity->admin_filter = $post_type_object->labels->bp_activity_admin_filter;
+
+	// Fall back to a generic name.
+	} else {
+		$post_type_activity->admin_filter = _x( 'New item published', 'Post Type generic activity post admin filter', 'buddypress' );
+	}
+
+	// Check for the front filter in the post type labels.
+	if ( ! empty( $post_type_object->labels->bp_activity_front_filter ) ) {
+		$post_type_activity->front_filter = $post_type_object->labels->bp_activity_front_filter;
+	}
+
+	// Try to get the action for new post type action on non-multisite installations.
+	if ( ! empty( $post_type_object->labels->bp_activity_new_post ) ) {
+		$post_type_activity->new_post_type_action = $post_type_object->labels->bp_activity_new_post;
+	}
+
+	// Try to get the action for new post type action on multisite installations.
+	if ( ! empty( $post_type_object->labels->bp_activity_new_post_ms ) ) {
+		$post_type_activity->new_post_type_action_ms = $post_type_object->labels->bp_activity_new_post_ms;
+	}
+
+	// If the post type supports comments and has a comment action id, build the comments tracking args
+	if ( $post_type_support_comments && ! empty( $post_type_activity->comment_action_id ) ) {
+		// Init a new container for the activity type for comments
+		$post_type_activity->comments_tracking = new stdClass();
+
+		// Build the activity type for comments
+		$post_type_activity->comments_tracking->component_id = $post_type_activity->component_id;
+		$post_type_activity->comments_tracking->action_id    = $post_type_activity->comment_action_id;
+
+		// Try to get the comments admin filter from the post type labels.
+		if ( ! empty( $post_type_object->labels->bp_activity_comments_admin_filter ) ) {
+			$post_type_activity->comments_tracking->admin_filter = $post_type_object->labels->bp_activity_comments_admin_filter;
+
+		// Fall back to a generic name.
+		} else {
+			$post_type_activity->comments_tracking->admin_filter = _x( 'New item comment posted', 'Post Type generic comments activity admin filter', 'buddypress' );
+		}
+
+		$post_type_activity->comments_tracking->format_callback = $post_type_activity->comment_format_callback;
+
+		// Check for the comments front filter in the post type labels.
+		if ( ! empty( $post_type_object->labels->bp_activity_comments_front_filter ) ) {
+			$post_type_activity->comments_tracking->front_filter = $post_type_object->labels->bp_activity_comments_front_filter;
+
+		// Fall back to a generic name.
+		} else {
+			$post_type_activity->comments_tracking->front_filter = _x( 'Item comments', 'Post Type generic comments activity front filter', 'buddypress' );
+		}
+
+		$post_type_activity->comments_tracking->contexts = $post_type_activity->contexts;
+		$post_type_activity->comments_tracking->position = (int) $post_type_activity->position + 1;
+
+		// Try to get the action for new post type comment action on non-multisite installations.
+		if ( ! empty( $post_type_object->labels->bp_activity_new_comment ) ) {
+			$post_type_activity->comments_tracking->new_post_type_comment_action = $post_type_object->labels->bp_activity_new_comment;
+		}
+
+		// Try to get the action for new post type comment action on multisite installations.
+		if ( ! empty( $post_type_object->labels->bp_activity_new_comment_ms ) ) {
+			$post_type_activity->comments_tracking->new_post_type_comment_action_ms = $post_type_object->labels->bp_activity_new_comment_ms;
+		}
+	}
+
+	// Finally make sure we'll be able to find the post type this activity type is associated to.
+	$post_type_activity->post_type = $post_type;
+
+	/**
+	 * Filters tracking arguments for a specific post type.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param object $post_type_activity The tracking arguments of the post type.
+	 * @param string $post_type          Name of the post type.
+	 */
+	return apply_filters( 'bp_activity_get_post_type_tracking_args', $post_type_activity, $post_type );
+}
+
+/**
+ * Get tracking arguments for all post types.
+ *
+ * @since 2.2.0
+ * @since 2.5.0 Include post type comments tracking args if needed
+ *
+ * @return array List of post types with their tracking arguments.
+ */
+function bp_activity_get_post_types_tracking_args() {
+	// Fetch all public post types.
+	$post_types = get_post_types( array( 'public' => true ), 'names' );
+
+	$post_types_tracking_args = array();
+
+	foreach ( $post_types as $post_type ) {
+		$track_post_type = bp_activity_get_post_type_tracking_args( $post_type );
+
+		if ( ! empty( $track_post_type ) ) {
+			// Set the post type comments tracking args
+			if ( ! empty( $track_post_type->comments_tracking->action_id ) ) {
+				// Used to check support for comment tracking by activity type (new_post_type_comment)
+				$track_post_type->comments_tracking->comments_tracking = true;
+
+				// Used to be able to find the post type this activity type is associated to.
+				$track_post_type->comments_tracking->post_type = $post_type;
+
+				$post_types_tracking_args[ $track_post_type->comments_tracking->action_id ] = $track_post_type->comments_tracking;
+
+				// Used to check support for comment tracking by activity type (new_post_type)
+				$track_post_type->comments_tracking = true;
+			}
+
+			$post_types_tracking_args[ $track_post_type->action_id ] = $track_post_type;
+		}
+
+	}
+
+	/**
+	 * Filters tracking arguments for all post types.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $post_types_tracking_args Array of post types with
+	 *                                        their tracking arguments.
+	 */
+	return apply_filters( 'bp_activity_get_post_types_tracking_args', $post_types_tracking_args );
+}
+
+/**
+ * Check if the *Post Type* activity supports a specific feature.
+ *
+ * @since 2.5.0
+ *
+ * @param  string $activity_type The activity type to check.
+ * @param  string $feature       The feature to check. Currently supports:
+ *                               'post-type-comment-tracking', 'post-type-comment-reply' & 'comment-reply'.
+ *                               See inline doc for more info.
+ * @return bool
+ */
+function bp_activity_type_supports( $activity_type = '', $feature = '' ) {
+	$retval = false;
+
+	$bp = buddypress();
+
+	switch ( $feature ) {
+		/**
+		 * Does this activity type support comment tracking?
+		 *
+		 * eg. 'new_blog_post' and 'new_blog_comment' will both return true.
+		 */
+		case 'post-type-comment-tracking' :
+			// Set the activity track global if not set yet
+			if ( empty( $bp->activity->track ) ) {
+				$bp->activity->track = bp_activity_get_post_types_tracking_args();
+			}
+
+			if ( ! empty( $bp->activity->track[ $activity_type ]->comments_tracking ) ) {
+				$retval = true;
+			}
+			break;
+
+		/**
+		 * Is this a parent activity type that support post comments?
+		 *
+		 * eg. 'new_blog_post' will return true; 'new_blog_comment' will return false.
+		 */
+		case 'post-type-comment-reply' :
+			// Set the activity track global if not set yet.
+			if ( empty( $bp->activity->track ) ) {
+				$bp->activity->track = bp_activity_get_post_types_tracking_args();
+			}
+
+			if ( ! empty( $bp->activity->track[ $activity_type ]->comments_tracking ) && ! empty( $bp->activity->track[ $activity_type ]->comment_action_id ) ) {
+				$retval = true;
+			}
+			break;
+
+		/**
+		 * Does this activity type support comment & reply?
+		 */
+		case 'comment-reply' :
+			// Set the activity track global if not set yet.
+			if ( empty( $bp->activity->track ) ) {
+				$bp->activity->track = bp_activity_get_post_types_tracking_args();
+			}
+
+			// Post Type activities
+			if ( ! empty( $bp->activity->track[ $activity_type ] ) ) {
+				if ( isset( $bp->activity->track[ $activity_type ]->activity_comment ) ) {
+					$retval = $bp->activity->track[ $activity_type ]->activity_comment;
+				}
+
+				// Eventually override with comment synchronization feature.
+				if ( isset( $bp->activity->track[ $activity_type ]->comments_tracking ) ) {
+					$retval = $bp->activity->track[ $activity_type ]->comments_tracking && ! bp_disable_blogforum_comments();
+				}
+
+			// Retired Forums component
+			} elseif ( 'new_forum_topic' === $activity_type || 'new_forum_post' === $activity_type ) {
+				$retval = ! bp_disable_blogforum_comments();
+
+			// By Default, all other activity types are supporting comments.
+			} else {
+				$retval = true;
+			}
+			break;
+	}
+
+	return $retval;
+}
+
+/**
+ * Get a specific tracking argument for a given activity type
+ *
+ * @since 2.5.0
+ *
+ * @param  string       $activity_type the activity type.
+ * @param  string       $arg           the key of the tracking argument.
+ * @return mixed        the value of the tracking arg, false if not found.
+ */
+function bp_activity_post_type_get_tracking_arg( $activity_type, $arg = '' ) {
+	if ( empty( $activity_type ) || empty( $arg ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	// Set the activity track global if not set yet
+	if ( empty( $bp->activity->track ) ) {
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+	}
+
+	if ( isset( $bp->activity->track[ $activity_type ]->{$arg} ) ) {
+		return $bp->activity->track[ $activity_type ]->{$arg};
+	} else {
+		return false;
+	}
+}
+
+/**
+ * Get all components' activity actions, sorted by their position attribute.
+ *
+ * @since 2.2.0
+ *
+ * @return object Actions ordered by their position.
+ */
+function bp_activity_get_actions() {
+	$bp = buddypress();
+
+	// Set the activity track global if not set yet.
+	if ( empty( $bp->activity->track ) ) {
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+	}
+
+	// Create the actions for the post types, if they haven't already been created.
+	if ( ! empty( $bp->activity->track ) ) {
+		foreach ( $bp->activity->track as $post_type ) {
+			if ( isset( $bp->activity->actions->{$post_type->component_id}->{$post_type->action_id} ) ) {
+				continue;
+			}
+
+			bp_activity_set_action(
+				$post_type->component_id,
+				$post_type->action_id,
+				$post_type->admin_filter,
+				$post_type->format_callback,
+				$post_type->front_filter,
+				$post_type->contexts,
+				$post_type->position
+			);
+		}
+	}
+
+	return $bp->activity->actions;
+}
+
+/**
+ * Retrieve the current action from a component and key.
+ *
+ * @since 1.1.0
+ *
+ * @param string $component_id The unique string ID of the component.
+ * @param string $key          The action key.
+ * @return string|bool Action value if found, otherwise false.
+ */
+function bp_activity_get_action( $component_id, $key ) {
+
+	// Return false if any of the above values are not set.
+	if ( empty( $component_id ) || empty( $key ) ) {
+		return false;
+	}
+
+	$actions = bp_activity_get_actions();
+	$retval  = false;
+
+	if ( isset( $actions->{$component_id}->{$key} ) ) {
+		$retval = $actions->{$component_id}->{$key};
+	}
+
+	/**
+	 * Filters the current action by component and key.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string|bool $retval       The action key.
+	 * @param string      $component_id The unique string ID of the component.
+	 * @param string      $key          The action key.
+	 */
+	return apply_filters( 'bp_activity_get_action', $retval, $component_id, $key );
+}
+
+/**
+ * Fetch details of all registered activity types.
+ *
+ * @since 1.7.0
+ *
+ * @return array array( type => description ), ...
+ */
+function bp_activity_get_types() {
+	$actions  = array();
+
+	// Walk through the registered actions, and build an array of actions/values.
+	foreach ( bp_activity_get_actions() as $action ) {
+		$action = array_values( (array) $action );
+
+		for ( $i = 0, $i_count = count( $action ); $i < $i_count; $i++ ) {
+			$actions[ $action[$i]['key'] ] = $action[$i]['value'];
+		}
+	}
+
+	// This was a mis-named activity type from before BP 1.6.
+	unset( $actions['friends_register_activity_action'] );
+
+	/**
+	 * Filters the available activity types.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $actions Array of registered activity types.
+	 */
+	return apply_filters( 'bp_activity_get_types', $actions );
+}
+
+/**
+ * Gets the current activity context.
+ *
+ * The "context" is the current view type, corresponding roughly to the
+ * current component. Use this context to determine which activity actions
+ * should be whitelisted for the filter dropdown.
+ *
+ * @since 2.8.0
+ *
+ * @return string Activity context. 'member', 'member_groups', 'group', 'activity'.
+ */
+function bp_activity_get_current_context() {
+	// On member pages, default to 'member', unless this is a user's Groups activity.
+	if ( bp_is_user() ) {
+		if ( bp_is_active( 'groups' ) && bp_is_current_action( bp_get_groups_slug() ) ) {
+			$context = 'member_groups';
+		} else {
+			$context = 'member';
+		}
+
+	// On individual group pages, default to 'group'.
+	} elseif ( bp_is_active( 'groups' ) && bp_is_group() ) {
+		$context = 'group';
+
+	// 'activity' everywhere else.
+	} else {
+		$context = 'activity';
+	}
+
+	return $context;
+}
+
+/**
+ * Gets a flat list of activity actions compatible with a given context.
+ *
+ * @since 2.8.0
+ *
+ * @param string $context Optional. Name of the context. Defaults to the current context.
+ * @return array
+ */
+function bp_activity_get_actions_for_context( $context = '' ) {
+	if ( ! $context ) {
+		$context = bp_activity_get_current_context();
+	}
+
+	$actions = array();
+	foreach ( bp_activity_get_actions() as $component_actions ) {
+		foreach ( $component_actions as $component_action ) {
+			if ( in_array( $context, (array) $component_action['context'], true ) ) {
+				$actions[] = $component_action;
+			}
+		}
+	}
+
+	return $actions;
+}
+
+/** Favorites ****************************************************************/
+
+/**
+ * Get a users favorite activity stream items.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id ID of the user whose favorites are being queried.
+ * @return array IDs of the user's favorite activity items.
+ */
+function bp_activity_get_user_favorites( $user_id = 0 ) {
+
+	// Fallback to logged in user if no user_id is passed.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	// Get favorites for user.
+	$favs = bp_get_user_meta( $user_id, 'bp_favorite_activities', true );
+
+	/**
+	 * Filters the favorited activity items for a specified user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $favs Array of user's favorited activity items.
+	 */
+	return apply_filters( 'bp_activity_get_user_favorites', $favs );
+}
+
+/**
+ * Add an activity stream item as a favorite for a user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $activity_id ID of the activity item being favorited.
+ * @param int $user_id     ID of the user favoriting the activity item.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_add_user_favorite( $activity_id, $user_id = 0 ) {
+
+	// Fallback to logged in user if no user_id is passed.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	$my_favs = bp_get_user_meta( $user_id, 'bp_favorite_activities', true );
+	if ( empty( $my_favs ) || ! is_array( $my_favs ) ) {
+		$my_favs = array();
+	}
+
+	// Bail if the user has already favorited this activity item.
+	if ( in_array( $activity_id, $my_favs ) ) {
+		return false;
+	}
+
+	// Add to user's favorites.
+	$my_favs[] = $activity_id;
+
+	// Update the total number of users who have favorited this activity.
+	$fav_count = bp_activity_get_meta( $activity_id, 'favorite_count' );
+	$fav_count = !empty( $fav_count ) ? (int) $fav_count + 1 : 1;
+
+	// Update user meta.
+	bp_update_user_meta( $user_id, 'bp_favorite_activities', $my_favs );
+
+	// Update activity meta counts.
+	if ( bp_activity_update_meta( $activity_id, 'favorite_count', $fav_count ) ) {
+
+		/**
+		 * Fires if bp_activity_update_meta() for favorite_count is successful and before returning a true value for success.
+		 *
+		 * @since 1.2.1
+		 *
+		 * @param int $activity_id ID of the activity item being favorited.
+		 * @param int $user_id     ID of the user doing the favoriting.
+		 */
+		do_action( 'bp_activity_add_user_favorite', $activity_id, $user_id );
+
+		// Success.
+		return true;
+
+	// Saving meta was unsuccessful for an unknown reason.
+	} else {
+
+		/**
+		 * Fires if bp_activity_update_meta() for favorite_count is unsuccessful and before returning a false value for failure.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $activity_id ID of the activity item being favorited.
+		 * @param int $user_id     ID of the user doing the favoriting.
+		 */
+		do_action( 'bp_activity_add_user_favorite_fail', $activity_id, $user_id );
+
+		return false;
+	}
+}
+
+/**
+ * Remove an activity stream item as a favorite for a user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $activity_id ID of the activity item being unfavorited.
+ * @param int $user_id     ID of the user unfavoriting the activity item.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_remove_user_favorite( $activity_id, $user_id = 0 ) {
+
+	// Fallback to logged in user if no user_id is passed.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	$my_favs = bp_get_user_meta( $user_id, 'bp_favorite_activities', true );
+	$my_favs = array_flip( (array) $my_favs );
+
+	// Bail if the user has not previously favorited the item.
+	if ( ! isset( $my_favs[ $activity_id ] ) ) {
+		return false;
+	}
+
+	// Remove the fav from the user's favs.
+	unset( $my_favs[$activity_id] );
+	$my_favs = array_unique( array_flip( $my_favs ) );
+
+	// Update the total number of users who have favorited this activity.
+	$fav_count = bp_activity_get_meta( $activity_id, 'favorite_count' );
+	if ( ! empty( $fav_count ) ) {
+
+		// Deduct from total favorites.
+		if ( bp_activity_update_meta( $activity_id, 'favorite_count', (int) $fav_count - 1 ) ) {
+
+			// Update users favorites.
+			if ( bp_update_user_meta( $user_id, 'bp_favorite_activities', $my_favs ) ) {
+
+				/**
+				 * Fires if bp_update_user_meta() is successful and before returning a true value for success.
+				 *
+				 * @since 1.2.1
+				 *
+				 * @param int $activity_id ID of the activity item being unfavorited.
+				 * @param int $user_id     ID of the user doing the unfavoriting.
+				 */
+				do_action( 'bp_activity_remove_user_favorite', $activity_id, $user_id );
+
+				// Success.
+				return true;
+
+			// Error updating.
+			} else {
+				return false;
+			}
+
+		// Error updating favorite count.
+		} else {
+			return false;
+		}
+
+	// Error getting favorite count.
+	} else {
+		return false;
+	}
+}
+
+/**
+ * Check whether an activity item exists with a given content string.
+ *
+ * @since 1.1.0
+ *
+ * @param string $content The content to filter by.
+ * @return int|null The ID of the located activity item. Null if none is found.
+ */
+function bp_activity_check_exists_by_content( $content ) {
+
+	/**
+	 * Filters the results of the check for whether an activity item exists by specified content.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param BP_Activity_Activity $value ID of the activity if found, else null.
+	 */
+	return apply_filters( 'bp_activity_check_exists_by_content', BP_Activity_Activity::check_exists_by_content( $content ) );
+}
+
+/**
+ * Retrieve the last time activity was updated.
+ *
+ * @since 1.0.0
+ *
+ * @return string Date last updated.
+ */
+function bp_activity_get_last_updated() {
+
+	/**
+	 * Filters the value for the last updated time for an activity item.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param BP_Activity_Activity $last_updated Date last updated.
+	 */
+	return apply_filters( 'bp_activity_get_last_updated', BP_Activity_Activity::get_last_updated() );
+}
+
+/**
+ * Retrieve the number of favorite activity stream items a user has.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id ID of the user whose favorite count is being requested.
+ * @return int Total favorite count for the user.
+ */
+function bp_activity_total_favorites_for_user( $user_id = 0 ) {
+
+	// Fallback on displayed user, and then logged in user.
+	if ( empty( $user_id ) ) {
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+	}
+
+	return BP_Activity_Activity::total_favorite_count( $user_id );
+}
+
+/** Meta *********************************************************************/
+
+/**
+ * Delete a meta entry from the DB for an activity stream item.
+ *
+ * @since 1.2.0
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int    $activity_id ID of the activity item whose metadata is being deleted.
+ * @param string $meta_key    Optional. The key of the metadata being deleted. If
+ *                            omitted, all metadata associated with the activity
+ *                            item will be deleted.
+ * @param string $meta_value  Optional. If present, the metadata will only be
+ *                            deleted if the meta_value matches this parameter.
+ * @param bool   $delete_all  Optional. If true, delete matching metadata entries
+ *                            for all objects, ignoring the specified object_id. Otherwise,
+ *                            only delete matching metadata entries for the specified
+ *                            activity item. Default: false.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_delete_meta( $activity_id, $meta_key = '', $meta_value = '', $delete_all = false ) {
+
+	// Legacy - if no meta_key is passed, delete all for the item.
+	if ( empty( $meta_key ) ) {
+		$all_meta = bp_activity_get_meta( $activity_id );
+		$keys     = ! empty( $all_meta ) ? array_keys( $all_meta ) : array();
+
+		// With no meta_key, ignore $delete_all.
+		$delete_all = false;
+	} else {
+		$keys = array( $meta_key );
+	}
+
+	$retval = true;
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	foreach ( $keys as $key ) {
+		$retval = delete_metadata( 'activity', $activity_id, $key, $meta_value, $delete_all );
+	}
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Get metadata for a given activity item.
+ *
+ * @since 1.2.0
+ *
+ * @param int    $activity_id ID of the activity item whose metadata is being requested.
+ * @param string $meta_key    Optional. If present, only the metadata matching
+ *                            that meta key will be returned. Otherwise, all metadata for the
+ *                            activity item will be fetched.
+ * @param bool   $single      Optional. If true, return only the first value of the
+ *                            specified meta_key. This parameter has no effect if meta_key is not
+ *                            specified. Default: true.
+ * @return mixed The meta value(s) being requested.
+ */
+function bp_activity_get_meta( $activity_id = 0, $meta_key = '', $single = true ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = get_metadata( 'activity', $activity_id, $meta_key, $single );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	/**
+	 * Filters the metadata for a specified activity item.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param mixed  $retval      The meta values for the activity item.
+	 * @param int    $activity_id ID of the activity item.
+	 * @param string $meta_key    Meta key for the value being requested.
+	 * @param bool   $single      Whether to return one matched meta key row or all.
+	 */
+	return apply_filters( 'bp_activity_get_meta', $retval, $activity_id, $meta_key, $single );
+}
+
+/**
+ * Update a piece of activity meta.
+ *
+ * @since 1.2.0
+ *
+ * @param int    $activity_id ID of the activity item whose metadata is being updated.
+ * @param string $meta_key    Key of the metadata being updated.
+ * @param mixed  $meta_value  Value to be set.
+ * @param mixed  $prev_value  Optional. If specified, only update existing metadata entries
+ *                            with the specified value. Otherwise, update all entries.
+ * @return bool|int Returns false on failure. On successful update of existing
+ *                  metadata, returns true. On successful creation of new metadata,
+ *                  returns the integer ID of the new metadata row.
+ */
+function bp_activity_update_meta( $activity_id, $meta_key, $meta_value, $prev_value = '' ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = update_metadata( 'activity', $activity_id, $meta_key, $meta_value, $prev_value );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Add a piece of activity metadata.
+ *
+ * @since 2.0.0
+ *
+ * @param int    $activity_id ID of the activity item.
+ * @param string $meta_key    Metadata key.
+ * @param mixed  $meta_value  Metadata value.
+ * @param bool   $unique      Optional. Whether to enforce a single metadata value for the
+ *                            given key. If true, and the object already has a value for
+ *                            the key, no change will be made. Default: false.
+ * @return int|bool The meta ID on successful update, false on failure.
+ */
+function bp_activity_add_meta( $activity_id, $meta_key, $meta_value, $unique = false ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = add_metadata( 'activity', $activity_id, $meta_key, $meta_value, $unique );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/** Clean up *****************************************************************/
+
+/**
+ * Completely remove a user's activity data.
+ *
+ * @since 1.5.0
+ *
+ * @param int $user_id ID of the user whose activity is being deleted.
+ * @return bool
+ */
+function bp_activity_remove_all_user_data( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	// Clear the user's activity from the sitewide stream and clear their activity tables.
+	bp_activity_delete( array( 'user_id' => $user_id ) );
+
+	// Remove any usermeta.
+	bp_delete_user_meta( $user_id, 'bp_latest_update'       );
+	bp_delete_user_meta( $user_id, 'bp_favorite_activities' );
+
+	// Execute additional code
+	do_action( 'bp_activity_remove_data', $user_id ); // Deprecated! Do not use!
+
+	/**
+	 * Fires after the removal of all of a user's activity data.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $user_id ID of the user being deleted.
+	 */
+	do_action( 'bp_activity_remove_all_user_data', $user_id );
+}
+add_action( 'wpmu_delete_user',  'bp_activity_remove_all_user_data' );
+add_action( 'delete_user',       'bp_activity_remove_all_user_data' );
+
+/**
+ * Mark all of the user's activity as spam.
+ *
+ * @since 1.6.0
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int $user_id ID of the user whose activity is being spammed.
+ * @return bool
+ */
+function bp_activity_spam_all_user_data( $user_id = 0 ) {
+	global $wpdb;
+
+	// Do not delete user data unless a logged in user says so.
+	if ( empty( $user_id ) || ! is_user_logged_in() ) {
+		return false;
+	}
+
+	// Get all the user's activities.
+	$activities = bp_activity_get( array(
+		'display_comments' => 'stream',
+		'filter'           => array( 'user_id' => $user_id ),
+		'show_hidden'      => true
+	) );
+
+	$bp = buddypress();
+
+	// Mark each as spam.
+	foreach ( (array) $activities['activities'] as $activity ) {
+
+		// Create an activity object.
+		$activity_obj = new BP_Activity_Activity;
+		foreach ( $activity as $k => $v ) {
+			$activity_obj->$k = $v;
+		}
+
+		// Mark as spam.
+		bp_activity_mark_as_spam( $activity_obj );
+
+		/*
+		 * If Akismet is present, update the activity history meta.
+		 *
+		 * This is usually taken care of when BP_Activity_Activity::save() happens, but
+		 * as we're going to be updating all the activity statuses directly, for efficiency,
+		 * we need to update manually.
+		 */
+		if ( ! empty( $bp->activity->akismet ) ) {
+			$bp->activity->akismet->update_activity_spam_meta( $activity_obj );
+		}
+
+		// Tidy up.
+		unset( $activity_obj );
+	}
+
+	// Mark all of this user's activities as spam.
+	$wpdb->query( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET is_spam = 1 WHERE user_id = %d", $user_id ) );
+
+	/**
+	 * Fires after all activity data from a user has been marked as spam.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int   $user_id    ID of the user whose activity is being marked as spam.
+	 * @param array $activities Array of activity items being marked as spam.
+	 */
+	do_action( 'bp_activity_spam_all_user_data', $user_id, $activities['activities'] );
+}
+add_action( 'bp_make_spam_user', 'bp_activity_spam_all_user_data' );
+
+/**
+ * Mark all of the user's activity as ham (not spam).
+ *
+ * @since 1.6.0
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int $user_id ID of the user whose activity is being hammed.
+ * @return bool
+ */
+function bp_activity_ham_all_user_data( $user_id = 0 ) {
+	global $wpdb;
+
+	// Do not delete user data unless a logged in user says so.
+	if ( empty( $user_id ) || ! is_user_logged_in() ) {
+		return false;
+	}
+
+	// Get all the user's activities.
+	$activities = bp_activity_get( array(
+		'display_comments' => 'stream',
+		'filter'           => array( 'user_id' => $user_id ),
+		'show_hidden'      => true,
+		'spam'             => 'all'
+	) );
+
+	$bp = buddypress();
+
+	// Mark each as not spam.
+	foreach ( (array) $activities['activities'] as $activity ) {
+
+		// Create an activity object.
+		$activity_obj = new BP_Activity_Activity;
+		foreach ( $activity as $k => $v ) {
+			$activity_obj->$k = $v;
+		}
+
+		// Mark as not spam.
+		bp_activity_mark_as_ham( $activity_obj );
+
+		/*
+		 * If Akismet is present, update the activity history meta.
+		 *
+		 * This is usually taken care of when BP_Activity_Activity::save() happens, but
+		 * as we're going to be updating all the activity statuses directly, for efficiency,
+		 * we need to update manually.
+		 */
+		if ( ! empty( $bp->activity->akismet ) ) {
+			$bp->activity->akismet->update_activity_ham_meta( $activity_obj );
+		}
+
+		// Tidy up.
+		unset( $activity_obj );
+	}
+
+	// Mark all of this user's activities as not spam.
+	$wpdb->query( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET is_spam = 0 WHERE user_id = %d", $user_id ) );
+
+	/**
+	 * Fires after all activity data from a user has been marked as ham.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int   $user_id    ID of the user whose activity is being marked as ham.
+	 * @param array $activities Array of activity items being marked as ham.
+	 */
+	do_action( 'bp_activity_ham_all_user_data', $user_id, $activities['activities'] );
+}
+add_action( 'bp_make_ham_user', 'bp_activity_ham_all_user_data' );
+
+/**
+ * Allow core components and dependent plugins to register activity actions.
+ *
+ * @since 1.2.0
+ */
+function bp_register_activity_actions() {
+	/**
+	 * Fires on bp_init to allow core components and dependent plugins to register activity actions.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_register_activity_actions' );
+}
+add_action( 'bp_init', 'bp_register_activity_actions', 8 );
+
+/**
+ * Register the activity stream actions for updates.
+ *
+ * @since 1.6.0
+ */
+function bp_activity_register_activity_actions() {
+	$bp = buddypress();
+
+	bp_activity_set_action(
+		$bp->activity->id,
+		'activity_update',
+		__( 'Posted a status update', 'buddypress' ),
+		'bp_activity_format_activity_action_activity_update',
+		__( 'Updates', 'buddypress' ),
+		array( 'activity', 'group', 'member', 'member_groups' )
+	);
+
+	bp_activity_set_action(
+		$bp->activity->id,
+		'activity_comment',
+		__( 'Replied to a status update', 'buddypress' ),
+		'bp_activity_format_activity_action_activity_comment',
+		__( 'Activity Comments', 'buddypress' )
+	);
+
+	/**
+	 * Fires at the end of the activity actions registration.
+	 *
+	 * Allows plugin authors to add their own activity actions alongside the core actions.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_activity_register_activity_actions' );
+
+	// Backpat. Don't use this.
+	do_action( 'updates_register_activity_actions' );
+}
+add_action( 'bp_register_activity_actions', 'bp_activity_register_activity_actions' );
+
+/**
+ * Generate an activity action string for an activity item.
+ *
+ * @since 2.0.0
+ *
+ * @param object $activity Activity data object.
+ * @return string|bool Returns false if no callback is found, otherwise returns
+ *                     the formatted action string.
+ */
+function bp_activity_generate_action_string( $activity ) {
+
+	// Check for valid input.
+	if ( empty( $activity->component ) || empty( $activity->type ) ) {
+		return false;
+	}
+
+	// Check for registered format callback.
+	$actions = bp_activity_get_actions();
+	if ( empty( $actions->{$activity->component}->{$activity->type}['format_callback'] ) ) {
+		return false;
+	}
+
+	// We apply the format_callback as a filter.
+	add_filter( 'bp_activity_generate_action_string', $actions->{$activity->component}->{$activity->type}['format_callback'], 10, 2 );
+
+	/**
+	 * Filters the string for the activity action being returned.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_Activity_Activity $action   Action string being requested.
+	 * @param BP_Activity_Activity $activity Activity item object.
+	 */
+	$action = apply_filters( 'bp_activity_generate_action_string', $activity->action, $activity );
+
+	// Remove the filter for future activity items.
+	remove_filter( 'bp_activity_generate_action_string', $actions->{$activity->component}->{$activity->type}['format_callback'], 10 );
+
+	return $action;
+}
+
+/**
+ * Format 'activity_update' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string $action
+ */
+function bp_activity_format_activity_action_activity_update( $action, $activity ) {
+	$action = sprintf( __( '%s posted an update', 'buddypress' ), bp_core_get_userlink( $activity->user_id ) );
+
+	/**
+	 * Filters the formatted activity action update string.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string               $action   Activity action string value.
+	 * @param BP_Activity_Activity $activity Activity item object.
+	 */
+	return apply_filters( 'bp_activity_new_update_action', $action, $activity );
+}
+
+/**
+ * Format 'activity_comment' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string $action
+ */
+function bp_activity_format_activity_action_activity_comment( $action, $activity ) {
+	$action = sprintf( __( '%s posted a new activity comment', 'buddypress' ), bp_core_get_userlink( $activity->user_id ) );
+
+	/**
+	 * Filters the formatted activity action comment string.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string               $action   Activity action string value.
+	 * @param BP_Activity_Activity $activity Activity item object.
+	 */
+	return apply_filters( 'bp_activity_comment_action', $action, $activity );
+}
+
+/**
+ * Format activity action strings for custom post types.
+ *
+ * @since 2.2.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string $action
+ */
+function bp_activity_format_activity_action_custom_post_type_post( $action, $activity ) {
+	$bp = buddypress();
+
+	// Fetch all the tracked post types once.
+	if ( empty( $bp->activity->track ) ) {
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+	}
+
+	if ( empty( $activity->type ) || empty( $bp->activity->track[ $activity->type ] ) ) {
+		return $action;
+	}
+
+	$user_link = bp_core_get_userlink( $activity->user_id );
+	$blog_url  = get_home_url( $activity->item_id );
+
+	if ( empty( $activity->post_url ) ) {
+		$post_url = add_query_arg( 'p', $activity->secondary_item_id, trailingslashit( $blog_url ) );
+	} else {
+		$post_url = $activity->post_url;
+	}
+
+	if ( is_multisite() ) {
+		$blog_link = '<a href="' . esc_url( $blog_url ) . '">' . get_blog_option( $activity->item_id, 'blogname' ) . '</a>';
+
+		if ( ! empty( $bp->activity->track[ $activity->type ]->new_post_type_action_ms ) ) {
+			$action = sprintf( $bp->activity->track[ $activity->type ]->new_post_type_action_ms, $user_link, $post_url, $blog_link );
+		} else {
+			$action = sprintf( _x( '%1$s wrote a new <a href="%2$s">item</a>, on the site %3$s', 'Activity Custom Post Type post action', 'buddypress' ), $user_link, esc_url( $post_url ), $blog_link );
+		}
+	} else {
+		if ( ! empty( $bp->activity->track[ $activity->type ]->new_post_type_action ) ) {
+			$action = sprintf( $bp->activity->track[ $activity->type ]->new_post_type_action, $user_link, $post_url );
+		} else {
+			$action = sprintf( _x( '%1$s wrote a new <a href="%2$s">item</a>', 'Activity Custom Post Type post action', 'buddypress' ), $user_link, esc_url( $post_url ) );
+		}
+	}
+
+	/**
+	 * Filters the formatted custom post type activity post action string.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string               $action   Activity action string value.
+	 * @param BP_Activity_Activity $activity Activity item object.
+	 */
+	return apply_filters( 'bp_activity_custom_post_type_post_action', $action, $activity );
+}
+
+/**
+ * Format activity action strings for custom post types comments.
+ *
+ * @since 2.5.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ *
+ * @return string
+ */
+function bp_activity_format_activity_action_custom_post_type_comment( $action, $activity ) {
+	$bp = buddypress();
+
+	// Fetch all the tracked post types once.
+	if ( empty( $bp->activity->track ) ) {
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+	}
+
+	if ( empty( $activity->type ) || empty( $bp->activity->track[ $activity->type ] ) ) {
+		return $action;
+	}
+
+	$user_link = bp_core_get_userlink( $activity->user_id );
+
+	if ( is_multisite() ) {
+		$blog_link = '<a href="' . esc_url( get_home_url( $activity->item_id ) ) . '">' . get_blog_option( $activity->item_id, 'blogname' ) . '</a>';
+
+		if ( ! empty( $bp->activity->track[ $activity->type ]->new_post_type_comment_action_ms ) ) {
+			$action = sprintf( $bp->activity->track[ $activity->type ]->new_post_type_comment_action_ms, $user_link, $activity->primary_link, $blog_link );
+		} else {
+			$action = sprintf( _x( '%1$s commented on the <a href="%2$s">item</a>, on the site %3$s', 'Activity Custom Post Type comment action', 'buddypress' ), $user_link, $activity->primary_link, $blog_link );
+		}
+	} else {
+		if ( ! empty( $bp->activity->track[ $activity->type ]->new_post_type_comment_action ) ) {
+			$action = sprintf( $bp->activity->track[ $activity->type ]->new_post_type_comment_action, $user_link, $activity->primary_link );
+		} else {
+			$action = sprintf( _x( '%1$s commented on the <a href="%2$s">item</a>', 'Activity Custom Post Type post comment action', 'buddypress' ), $user_link, $activity->primary_link );
+		}
+	}
+
+	/**
+	 * Filters the formatted custom post type activity comment action string.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string               $action   Activity action string value.
+	 * @param BP_Activity_Activity $activity Activity item object.
+	 */
+	return apply_filters( 'bp_activity_custom_post_type_comment_action', $action, $activity );
+}
+
+/*
+ * Business functions are where all the magic happens in BuddyPress. They will
+ * handle the actual saving or manipulation of information. Usually they will
+ * hand off to a database class for data access, then return
+ * true or false on success or failure.
+ */
+
+/**
+ * Retrieve an activity or activities.
+ *
+ * The bp_activity_get() function shares all arguments with BP_Activity_Activity::get().
+ * The following is a list of bp_activity_get() parameters that have different
+ * default values from BP_Activity_Activity::get() (value in parentheses is
+ * the default for the bp_activity_get()).
+ *   - 'per_page' (false)
+ *
+ * @since 1.2.0
+ * @since 2.4.0 Introduced the `$fields` parameter.
+ *
+ * @see BP_Activity_Activity::get() For more information on accepted arguments
+ *      and the format of the returned value.
+ *
+ * @param array|string $args See BP_Activity_Activity::get() for description.
+ * @return array $activity See BP_Activity_Activity::get() for description.
+ */
+function bp_activity_get( $args = '' ) {
+
+	$r = bp_parse_args( $args, array(
+		'max'               => false,        // Maximum number of results to return.
+		'fields'            => 'all',
+		'page'              => 1,            // Page 1 without a per_page will result in no pagination.
+		'per_page'          => false,        // results per page
+		'sort'              => 'DESC',       // sort ASC or DESC
+		'display_comments'  => false,        // False for no comments. 'stream' for within stream display, 'threaded' for below each activity item.
+
+		'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:
+		 * array(
+		 *     'user_id'      => false, // User ID to filter on.
+		 *     'object'       => false, // Object to filter on e.g. groups, profile, status, friends.
+		 *     'action'       => false, // Action to filter on e.g. activity_update, profile_updated.
+		 *     'primary_id'   => false, // Object ID to filter on e.g. a group_id or blog_id etc.
+		 *     'secondary_id' => false, // Secondary object ID to filter on e.g. a post_id.
+		 * );
+		 */
+		'filter' => array()
+	), 'activity_get' );
+
+	$activity = BP_Activity_Activity::get( array(
+		'page'              => $r['page'],
+		'per_page'          => $r['per_page'],
+		'max'               => $r['max'],
+		'sort'              => $r['sort'],
+		'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'],
+		'in'                => $r['in'],
+		'spam'              => $r['spam'],
+		'update_meta_cache' => $r['update_meta_cache'],
+		'count_total'       => $r['count_total'],
+		'fields'            => $r['fields'],
+	) );
+
+	/**
+	 * Filters the requested activity item(s).
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param BP_Activity_Activity $activity Requested activity object.
+	 * @param array                $r        Arguments used for the activity query.
+	 */
+	return apply_filters_ref_array( 'bp_activity_get', array( &$activity, &$r ) );
+}
+
+/**
+ * Fetch specific activity items.
+ *
+ * @since 1.2.0
+ *
+ * @see BP_Activity_Activity::get() For more information on accepted arguments.
+ *
+ * @param array|string $args {
+ *     All arguments and defaults are shared with BP_Activity_Activity::get(),
+ *     except for the following:
+ *     @type string|int|array Single activity ID, comma-separated list of IDs,
+ *                            or array of IDs.
+ * }
+ * @return array $activity See BP_Activity_Activity::get() for description.
+ */
+function bp_activity_get_specific( $args = '' ) {
+
+	$r = bp_parse_args( $args, array(
+		'activity_ids'      => false,      // A single activity_id or array of IDs.
+		'display_comments'  => false,      // True or false to display threaded comments for these specific activity items.
+		'max'               => false,      // Maximum number of results to return.
+		'page'              => 1,          // Page 1 without a per_page will result in no pagination.
+		'per_page'          => false,      // Results per page.
+		'show_hidden'       => true,       // When fetching specific items, show all.
+		'sort'              => 'DESC',     // Sort ASC or DESC
+		'spam'              => 'ham_only', // Retrieve items marked as spam.
+		'update_meta_cache' => true,
+	), 'activity_get_specific' );
+
+	$get_args = array(
+		'display_comments'  => $r['display_comments'],
+		'in'                => $r['activity_ids'],
+		'max'               => $r['max'],
+		'page'              => $r['page'],
+		'per_page'          => $r['per_page'],
+		'show_hidden'       => $r['show_hidden'],
+		'sort'              => $r['sort'],
+		'spam'              => $r['spam'],
+		'update_meta_cache' => $r['update_meta_cache'],
+	);
+
+	/**
+	 * Filters the requested specific activity item.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param BP_Activity_Activity $activity Requested activity object.
+	 * @param array                $args     Original passed in arguments.
+	 * @param array                $get_args Constructed arguments used with request.
+	 */
+	return apply_filters( 'bp_activity_get_specific', BP_Activity_Activity::get( $get_args ), $args, $get_args );
+}
+
+/**
+ * Add an activity item.
+ *
+ * @since 1.1.0
+ * @since 2.6.0 Added 'error_type' parameter to $args.
+ *
+ * @param array|string $args {
+ *     An array of arguments.
+ *     @type int|bool $id                Pass an activity ID to update an existing item, or
+ *                                       false to create a new item. Default: false.
+ *     @type string   $action            Optional. The activity action/description, typically
+ *                                       something like "Joe posted an update". Values passed to this param
+ *                                       will be stored in the database and used as a fallback for when the
+ *                                       activity item's format_callback cannot be found (eg, when the
+ *                                       component is disabled). As long as you have registered a
+ *                                       format_callback for your $type, it is unnecessary to include this
+ *                                       argument - BP will generate it automatically.
+ *                                       See {@link bp_activity_set_action()}.
+ *     @type string   $content           Optional. The content of the activity item.
+ *     @type string   $component         The unique name of the component associated with
+ *                                       the activity item - 'groups', 'profile', etc.
+ *     @type string   $type              The specific activity type, used for directory
+ *                                       filtering. 'new_blog_post', 'activity_update', etc.
+ *     @type string   $primary_link      Optional. The URL for this item, as used in
+ *                                       RSS feeds. Defaults to the URL for this activity
+ *                                       item's permalink page.
+ *     @type int|bool $user_id           Optional. The ID of the user associated with the activity
+ *                                       item. May be set to false or 0 if the item is not related
+ *                                       to any user. Default: the ID of the currently logged-in user.
+ *     @type int      $item_id           Optional. The ID of the associated item.
+ *     @type int      $secondary_item_id Optional. The ID of a secondary associated item.
+ *     @type string   $date_recorded     Optional. The GMT time, in Y-m-d h:i:s format, when
+ *                                       the item was recorded. Defaults to the current time.
+ *     @type bool     $hide_sitewide     Should the item be hidden on sitewide streams?
+ *                                       Default: false.
+ *     @type bool     $is_spam           Should the item be marked as spam? Default: false.
+ *     @type string   $error_type        Optional. Error type. Either 'bool' or 'wp_error'. Default: 'bool'.
+ * }
+ * @return WP_Error|bool|int The ID of the activity on success. False on error.
+ */
+function bp_activity_add( $args = '' ) {
+
+	$r = bp_parse_args( $args, array(
+		'id'                => false,                  // Pass an existing activity ID to update an existing entry.
+		'action'            => '',                     // The activity action - e.g. "Jon Doe posted an update"
+		'content'           => '',                     // Optional: The content of the activity item e.g. "BuddyPress is awesome guys!"
+		'component'         => false,                  // The name/ID of the component e.g. groups, profile, mycomponent.
+		'type'              => false,                  // The activity type e.g. activity_update, profile_updated.
+		'primary_link'      => '',                     // Optional: The primary URL for this item in RSS feeds (defaults to activity permalink).
+		'user_id'           => bp_loggedin_user_id(),  // Optional: The user to record the activity for, can be false if this activity is not for a user.
+		'item_id'           => false,                  // Optional: The ID of the specific item being recorded, e.g. a blog_id.
+		'secondary_item_id' => false,                  // Optional: A second ID used to further filter e.g. a comment_id.
+		'recorded_time'     => bp_core_current_time(), // The GMT time that this activity was recorded.
+		'hide_sitewide'     => false,                  // Should this be hidden on the sitewide activity stream?
+		'is_spam'           => false,                  // Is this activity item to be marked as spam?
+		'error_type'        => 'bool'
+	), 'activity_add' );
+
+	// Make sure we are backwards compatible.
+	if ( empty( $r['component'] ) && !empty( $r['component_name'] ) ) {
+		$r['component'] = $r['component_name'];
+	}
+
+	if ( empty( $r['type'] ) && !empty( $r['component_action'] ) ) {
+		$r['type'] = $r['component_action'];
+	}
+
+	// Setup activity to be added.
+	$activity                    = new BP_Activity_Activity( $r['id'] );
+	$activity->user_id           = $r['user_id'];
+	$activity->component         = $r['component'];
+	$activity->type              = $r['type'];
+	$activity->content           = $r['content'];
+	$activity->primary_link      = $r['primary_link'];
+	$activity->item_id           = $r['item_id'];
+	$activity->secondary_item_id = $r['secondary_item_id'];
+	$activity->date_recorded     = $r['recorded_time'];
+	$activity->hide_sitewide     = $r['hide_sitewide'];
+	$activity->is_spam           = $r['is_spam'];
+	$activity->error_type        = $r['error_type'];
+	$activity->action            = ! empty( $r['action'] )
+						? $r['action']
+						: bp_activity_generate_action_string( $activity );
+
+	$save = $activity->save();
+
+	if ( 'wp_error' === $r['error_type'] && is_wp_error( $save ) ) {
+		return $save;
+	} elseif ('bool' === $r['error_type'] && false === $save ) {
+		return false;
+	}
+
+	// If this is an activity comment, rebuild the tree.
+	if ( 'activity_comment' === $activity->type ) {
+		// Also clear the comment cache for the parent activity ID.
+		wp_cache_delete( $activity->item_id, 'bp_activity_comments' );
+
+		BP_Activity_Activity::rebuild_activity_comment_tree( $activity->item_id );
+	}
+
+	wp_cache_delete( 'bp_activity_sitewide_front', 'bp' );
+
+	/**
+	 * Fires at the end of the execution of adding a new activity item, before returning the new activity item ID.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array $r Array of parsed arguments for the activity item being added.
+	 */
+	do_action( 'bp_activity_add', $r );
+
+	return $activity->id;
+}
+
+/**
+ * Post an activity update.
+ *
+ * @since 1.2.0
+ *
+ * @param array|string $args {
+ *     @type string $content    The content of the activity update.
+ *     @type int    $user_id    Optional. Defaults to the logged-in user.
+ *     @type string $error_type Optional. Error type to return. Either 'bool' or 'wp_error'. Defaults to
+ *                              'bool' for boolean. 'wp_error' will return a WP_Error object.
+ * }
+ * @return int|bool|WP_Error $activity_id The activity id on success. On failure, either boolean false or WP_Error
+ *                                        object depending on the 'error_type' $args parameter.
+ */
+function bp_activity_post_update( $args = '' ) {
+
+	$r = wp_parse_args( $args, array(
+		'content'    => false,
+		'user_id'    => bp_loggedin_user_id(),
+		'error_type' => 'bool',
+	) );
+
+	if ( empty( $r['content'] ) || !strlen( trim( $r['content'] ) ) ) {
+		return false;
+	}
+
+	if ( bp_is_user_inactive( $r['user_id'] ) ) {
+		return false;
+	}
+
+	// Record this on the user's profile.
+	$activity_content = $r['content'];
+	$primary_link     = bp_core_get_userlink( $r['user_id'], false, true );
+
+	/**
+	 * Filters the new activity content for current activity item.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $activity_content Activity content posted by user.
+	 */
+	$add_content = apply_filters( 'bp_activity_new_update_content', $activity_content );
+
+	/**
+	 * Filters the activity primary link for current activity item.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $primary_link Link to the profile for the user who posted the activity.
+	 */
+	$add_primary_link = apply_filters( 'bp_activity_new_update_primary_link', $primary_link );
+
+	// Now write the values.
+	$activity_id = bp_activity_add( array(
+		'user_id'      => $r['user_id'],
+		'content'      => $add_content,
+		'primary_link' => $add_primary_link,
+		'component'    => buddypress()->activity->id,
+		'type'         => 'activity_update',
+		'error_type'   => $r['error_type']
+	) );
+
+	// Bail on failure.
+	if ( false === $activity_id || is_wp_error( $activity_id ) ) {
+		return $activity_id;
+	}
+
+	/**
+	 * Filters the latest update content for the activity item.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $r                Content of the activity update.
+	 * @param string $activity_content Content of the activity update.
+	 */
+	$activity_content = apply_filters( 'bp_activity_latest_update_content', $r['content'], $activity_content );
+
+	// Add this update to the "latest update" usermeta so it can be fetched anywhere.
+	bp_update_user_meta( bp_loggedin_user_id(), 'bp_latest_update', array(
+		'id'      => $activity_id,
+		'content' => $activity_content
+	) );
+
+	/**
+	 * Fires at the end of an activity post update, before returning the updated activity item ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $content     Content of the activity post update.
+	 * @param int    $user_id     ID of the user posting the activity update.
+	 * @param int    $activity_id ID of the activity item being updated.
+	 */
+	do_action( 'bp_activity_posted_update', $r['content'], $r['user_id'], $activity_id );
+
+	return $activity_id;
+}
+
+/**
+ * Create an activity item for a newly published post type post.
+ *
+ * @since 2.2.0
+ *
+ * @param int          $post_id ID of the new post.
+ * @param WP_Post|null $post    Post object.
+ * @param int          $user_id ID of the post author.
+ * @return null|WP_Error|bool|int The ID of the activity on success. False on error.
+ */
+function bp_activity_post_type_publish( $post_id = 0, $post = null, $user_id = 0 ) {
+
+	if ( ! is_a( $post, 'WP_Post' ) ) {
+		return;
+	}
+
+	// Get the post type tracking args.
+	$activity_post_object = bp_activity_get_post_type_tracking_args( $post->post_type );
+
+	if ( 'publish' != $post->post_status || ! empty( $post->post_password ) || empty( $activity_post_object->action_id ) ) {
+		return;
+	}
+
+	if ( empty( $post_id ) ) {
+		$post_id = $post->ID;
+	}
+
+	$blog_id = get_current_blog_id();
+
+	if ( empty( $user_id ) ) {
+		$user_id = (int) $post->post_author;
+	}
+
+	// Bail if an activity item already exists for this post.
+	$existing = bp_activity_get( array(
+		'filter' => array(
+			'action'       => $activity_post_object->action_id,
+			'primary_id'   => $blog_id,
+			'secondary_id' => $post_id,
+		)
+	) );
+
+	if ( ! empty( $existing['activities'] ) ) {
+		return;
+	}
+
+	/**
+	 * Filters whether or not to post the activity.
+	 *
+	 * This is a variable filter, dependent on the post type,
+	 * that lets components or plugins bail early if needed.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param bool $value   Whether or not to continue.
+	 * @param int  $blog_id ID of the current site.
+	 * @param int  $post_id ID of the current post being published.
+	 * @param int  $user_id ID of the current user or post author.
+	 */
+	if ( false === apply_filters( "bp_activity_{$post->post_type}_pre_publish", true, $blog_id, $post_id, $user_id ) ) {
+		return;
+	}
+
+	// Record this in activity streams.
+	$blog_url = get_home_url( $blog_id );
+	$post_url = add_query_arg(
+		'p',
+		$post_id,
+		trailingslashit( $blog_url )
+	);
+
+	// Backward compatibility filters for the 'blogs' component.
+	if ( 'blogs' == $activity_post_object->component_id )  {
+		$activity_content      = apply_filters( 'bp_blogs_activity_new_post_content', $post->post_content, $post, $post_url, $post->post_type );
+		$activity_primary_link = apply_filters( 'bp_blogs_activity_new_post_primary_link', $post_url, $post_id, $post->post_type );
+	} else {
+		$activity_content      = $post->post_content;
+		$activity_primary_link = $post_url;
+	}
+
+	$activity_args = array(
+		'user_id'           => $user_id,
+		'content'           => $activity_content,
+		'primary_link'      => $activity_primary_link,
+		'component'         => $activity_post_object->component_id,
+		'type'              => $activity_post_object->action_id,
+		'item_id'           => $blog_id,
+		'secondary_item_id' => $post_id,
+		'recorded_time'     => $post->post_date_gmt,
+	);
+
+	if ( ! empty( $activity_args['content'] ) ) {
+		// Create the excerpt.
+		$activity_summary = bp_activity_create_summary( $activity_args['content'], $activity_args );
+
+		// Backward compatibility filter for blog posts.
+		if ( 'blogs' == $activity_post_object->component_id )  {
+			$activity_args['content'] = apply_filters( 'bp_blogs_record_activity_content', $activity_summary, $activity_args['content'], $activity_args, $post->post_type );
+		} else {
+			$activity_args['content'] = $activity_summary;
+		}
+	}
+
+	// Set up the action by using the format functions.
+	$action_args = array_merge( $activity_args, array(
+		'post_title' => $post->post_title,
+		'post_url'   => $post_url,
+	) );
+
+	$activity_args['action'] = call_user_func_array( $activity_post_object->format_callback, array( '', (object) $action_args ) );
+
+	// Make sure the action is set.
+	if ( empty( $activity_args['action'] ) ) {
+		return;
+	} else {
+		// Backward compatibility filter for the blogs component.
+		if ( 'blogs' == $activity_post_object->component_id )  {
+			$activity_args['action'] = apply_filters( 'bp_blogs_record_activity_action', $activity_args['action'] );
+		}
+	}
+
+	$activity_id = bp_activity_add( $activity_args );
+
+	/**
+	 * Fires after the publishing of an activity item for a newly published post type post.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param int     $activity_id   ID of the newly published activity item.
+	 * @param WP_Post $post          Post object.
+	 * @param array   $activity_args Array of activity arguments.
+	 */
+	do_action( 'bp_activity_post_type_published', $activity_id, $post, $activity_args );
+
+	return $activity_id;
+}
+
+/**
+ * Update the activity item for a custom post type entry.
+ *
+ * @since 2.2.0
+ *
+ * @param WP_Post|null $post Post item.
+ * @return null|WP_Error|bool True on success, false on failure.
+ */
+function bp_activity_post_type_update( $post = null ) {
+
+	if ( ! is_a( $post, 'WP_Post' ) ) {
+		return;
+	}
+
+	// Get the post type tracking args.
+	$activity_post_object = bp_activity_get_post_type_tracking_args( $post->post_type );
+
+	if ( empty( $activity_post_object->action_id ) ) {
+		return;
+	}
+
+	$activity_id = bp_activity_get_activity_id( array(
+		'component'         => $activity_post_object->component_id,
+		'item_id'           => get_current_blog_id(),
+		'secondary_item_id' => $post->ID,
+		'type'              => $activity_post_object->action_id,
+	) );
+
+	// Activity ID doesn't exist, so stop!
+	if ( empty( $activity_id ) ) {
+		return;
+	}
+
+	// Delete the activity if the post was updated with a password.
+	if ( ! empty( $post->post_password ) ) {
+		bp_activity_delete( array( 'id' => $activity_id ) );
+	}
+
+	// Update the activity entry.
+	$activity = new BP_Activity_Activity( $activity_id );
+
+	if ( ! empty( $post->post_content ) ) {
+		$activity_summary = bp_activity_create_summary( $post->post_content, (array) $activity );
+
+		// Backward compatibility filter for the blogs component.
+		if ( 'blogs' == $activity_post_object->component_id ) {
+			$activity->content = apply_filters( 'bp_blogs_record_activity_content', $activity_summary, $post->post_content, (array) $activity, $post->post_type );
+		} else {
+			$activity->content = $activity_summary;
+		}
+	}
+
+	// Save the updated activity.
+	$updated = $activity->save();
+
+	/**
+	 * Fires after the updating of an activity item for a custom post type entry.
+	 *
+	 * @since 2.2.0
+	 * @since 2.5.0 Add the post type tracking args parameter
+	 *
+	 * @param WP_Post              $post                 Post object.
+	 * @param BP_Activity_Activity $activity             Activity object.
+	 * @param object               $activity_post_object The post type tracking args object.
+	 */
+	do_action( 'bp_activity_post_type_updated', $post, $activity, $activity_post_object );
+
+	return $updated;
+}
+
+/**
+ * Unpublish an activity for the custom post type.
+ *
+ * @since 2.2.0
+ *
+ * @param int          $post_id ID of the post being unpublished.
+ * @param WP_Post|null $post    Post object.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_post_type_unpublish( $post_id = 0, $post = null ) {
+
+	if ( ! is_a( $post, 'WP_Post' ) ) {
+		return;
+	}
+
+	// Get the post type tracking args.
+	$activity_post_object = bp_activity_get_post_type_tracking_args( $post->post_type );
+
+	if ( empty( $activity_post_object->action_id ) ) {
+		return;
+	}
+
+	if ( empty( $post_id ) ) {
+		$post_id = $post->ID;
+	}
+
+	$delete_activity_args = array(
+		'item_id'           => get_current_blog_id(),
+		'secondary_item_id' => $post_id,
+		'component'         => $activity_post_object->component_id,
+		'type'              => $activity_post_object->action_id,
+		'user_id'           => false,
+	);
+
+	$deleted = bp_activity_delete_by_item_id( $delete_activity_args );
+
+	/**
+	 * Fires after the unpublishing for the custom post type.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array   $delete_activity_args Array of arguments for activity deletion.
+	 * @param WP_Post $post                 Post object.
+	 * @param bool    $activity             Whether or not the activity was successfully deleted.
+	 */
+	do_action( 'bp_activity_post_type_unpublished', $delete_activity_args, $post, $deleted );
+
+	return $deleted;
+}
+
+/**
+ * Create an activity item for a newly posted post type comment.
+ *
+ * @since 2.5.0
+ *
+ * @param  int         $comment_id           ID of the comment.
+ * @param  bool        $is_approved          Whether the comment is approved or not.
+ * @param  object|null $activity_post_object The post type tracking args object.
+ * @return null|WP_Error|bool|int The ID of the activity on success. False on error.
+ */
+function bp_activity_post_type_comment( $comment_id = 0, $is_approved = true, $activity_post_object = null ) {
+	// Get the users comment
+	$post_type_comment = get_comment( $comment_id );
+
+	// Don't record activity if the comment hasn't been approved
+	if ( empty( $is_approved ) ) {
+		return false;
+	}
+
+	// Don't record activity if no email address has been included
+	if ( empty( $post_type_comment->comment_author_email ) ) {
+		return false;
+	}
+
+	// Don't record activity if the comment has already been marked as spam
+	if ( 'spam' === $is_approved ) {
+		return false;
+	}
+
+	// Get the user by the comment author email.
+	$user = get_user_by( 'email', $post_type_comment->comment_author_email );
+
+	// If user isn't registered, don't record activity
+	if ( empty( $user ) ) {
+		return false;
+	}
+
+	// Get the user_id
+	$user_id = (int) $user->ID;
+
+	// Get blog and post data
+	$blog_id = get_current_blog_id();
+
+	// Get the post
+	$post_type_comment->post = get_post( $post_type_comment->comment_post_ID );
+
+	if ( ! is_a( $post_type_comment->post, 'WP_Post' ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters whether to publish activities about the comment regarding the post status
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param bool true to bail, false otherwise.
+	 */
+	$is_post_status_not_allowed = (bool) apply_filters( 'bp_activity_post_type_is_post_status_allowed', 'publish' !== $post_type_comment->post->post_status || ! empty( $post_type_comment->post->post_password ) );
+
+	// If this is a password protected post, or not a public post don't record the comment
+	if ( $is_post_status_not_allowed ) {
+		return false;
+	}
+
+	// Set post type
+	$post_type = $post_type_comment->post->post_type;
+
+	if ( empty( $activity_post_object ) ) {
+		// 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
+	$activity_comment_object = $activity_post_object->comments_tracking;
+
+	/**
+	 * Filters whether or not to post the activity about the comment.
+	 *
+	 * This is a variable filter, dependent on the post type,
+	 * that lets components or plugins bail early if needed.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param bool $value      Whether or not to continue.
+	 * @param int  $blog_id    ID of the current site.
+	 * @param int  $post_id    ID of the current post being commented.
+	 * @param int  $user_id    ID of the current user.
+	 * @param int  $comment_id ID of the current comment being posted.
+	 */
+	if ( false === apply_filters( "bp_activity_{$post_type}_pre_comment", true, $blog_id, $post_type_comment->post->ID, $user_id, $comment_id ) ) {
+		return false;
+	}
+
+	// Is this an update ?
+	$activity_id = bp_activity_get_activity_id( array(
+		'user_id'           => $user_id,
+		'component'         => $activity_comment_object->component_id,
+		'type'              => $activity_comment_object->action_id,
+		'item_id'           => $blog_id,
+		'secondary_item_id' => $comment_id,
+	) );
+
+	// Record this in activity streams.
+	$comment_link = get_comment_link( $post_type_comment->comment_ID );
+
+	// Backward compatibility filters for the 'blogs' component.
+	if ( 'blogs' == $activity_comment_object->component_id )  {
+		$activity_content      = apply_filters_ref_array( 'bp_blogs_activity_new_comment_content',      array( $post_type_comment->comment_content, &$post_type_comment, $comment_link ) );
+		$activity_primary_link = apply_filters_ref_array( 'bp_blogs_activity_new_comment_primary_link', array( $comment_link, &$post_type_comment ) );
+	} else {
+		$activity_content      = $post_type_comment->comment_content;
+		$activity_primary_link = $comment_link;
+	}
+
+	$activity_args = array(
+		'id'            => $activity_id,
+		'user_id'       => $user_id,
+		'content'       => $activity_content,
+		'primary_link'  => $activity_primary_link,
+		'component'     => $activity_comment_object->component_id,
+		'recorded_time' => $post_type_comment->comment_date_gmt,
+	);
+
+	if ( bp_disable_blogforum_comments() ) {
+		$blog_url = get_home_url( $blog_id );
+		$post_url = add_query_arg(
+			'p',
+			$post_type_comment->post->ID,
+			trailingslashit( $blog_url )
+		);
+
+		$activity_args['type']              = $activity_comment_object->action_id;
+		$activity_args['item_id']           = $blog_id;
+		$activity_args['secondary_item_id'] = $post_type_comment->comment_ID;
+
+		if ( ! empty( $activity_args['content'] ) ) {
+			// Create the excerpt.
+			$activity_summary = bp_activity_create_summary( $activity_args['content'], $activity_args );
+
+			// Backward compatibility filter for blog comments.
+			if ( 'blogs' == $activity_post_object->component_id )  {
+				$activity_args['content'] = apply_filters( 'bp_blogs_record_activity_content', $activity_summary, $activity_args['content'], $activity_args, $post_type );
+			} else {
+				$activity_args['content'] = $activity_summary;
+			}
+		}
+
+		// Set up the action by using the format functions.
+		$action_args = array_merge( $activity_args, array(
+			'post_title' => $post_type_comment->post->post_title,
+			'post_url'   => $post_url,
+			'blog_url'   => $blog_url,
+			'blog_name'  => get_blog_option( $blog_id, 'blogname' ),
+		) );
+
+		$activity_args['action'] = call_user_func_array( $activity_comment_object->format_callback, array( '', (object) $action_args ) );
+
+		// Make sure the action is set.
+		if ( empty( $activity_args['action'] ) ) {
+			return;
+		} else {
+			// Backward compatibility filter for the blogs component.
+			if ( 'blogs' === $activity_post_object->component_id )  {
+				$activity_args['action'] = apply_filters( 'bp_blogs_record_activity_action', $activity_args['action'] );
+			}
+		}
+
+		$activity_id = bp_activity_add( $activity_args );
+	}
+
+	/**
+	 * Fires after the publishing of an activity item for a newly published post type post.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param int        $activity_id          ID of the newly published activity item.
+	 * @param WP_Comment $post_type_comment    Comment object.
+	 * @param array      $activity_args        Array of activity arguments.
+	 * @param object     $activity_post_object the post type tracking args object.
+	 */
+	do_action_ref_array( 'bp_activity_post_type_comment', array( &$activity_id, $post_type_comment, $activity_args, $activity_post_object ) );
+
+	return $activity_id;
+}
+add_action( 'comment_post', 'bp_activity_post_type_comment', 10, 2 );
+add_action( 'edit_comment', 'bp_activity_post_type_comment', 10    );
+
+/**
+ * Remove an activity item when a comment about a post type is deleted.
+ *
+ * @since 2.5.0
+ *
+ * @param  int         $comment_id           ID of the comment.
+ * @param  object|null $activity_post_object The post type tracking args object.
+ * @return bool True on success. False on error.
+ */
+function bp_activity_post_type_remove_comment( $comment_id = 0, $activity_post_object = null ) {
+	if ( empty( $activity_post_object ) ) {
+		$comment = get_comment( $comment_id );
+		if ( ! $comment ) {
+			return;
+		}
+
+		$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
+	$activity_comment_object = $activity_post_object->comments_tracking;
+
+	if ( empty( $activity_comment_object->action_id ) ) {
+		return false;
+	}
+
+	$deleted = false;
+
+	if ( bp_disable_blogforum_comments() ) {
+		$deleted = bp_activity_delete_by_item_id( array(
+			'item_id'           => get_current_blog_id(),
+			'secondary_item_id' => $comment_id,
+			'component'         => $activity_comment_object->component_id,
+			'type'              => $activity_comment_object->action_id,
+			'user_id'           => false,
+		) );
+	}
+
+	/**
+	 * Fires after the custom post type comment activity was removed.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param bool       $deleted              True if the activity was deleted false otherwise
+	 * @param WP_Comment $comment              Comment object.
+	 * @param object     $activity_post_object The post type tracking args object.
+	 * @param string     $value                The post type comment activity type.
+	 */
+	do_action( 'bp_activity_post_type_remove_comment', $deleted, $comment_id, $activity_post_object, $activity_comment_object->action_id );
+
+	return $deleted;
+}
+add_action( 'delete_comment', 'bp_activity_post_type_remove_comment', 10, 1 );
+
+/**
+ * Add an activity comment.
+ *
+ * @since 1.2.0
+ * @since 2.5.0 Add a new possible parameter $skip_notification for the array of arguments.
+ *              Add the $primary_link parameter for the array of arguments.
+ * @since 2.6.0 Added 'error_type' parameter to $args.
+ *
+ * @param array|string $args {
+ *     An array of arguments.
+ *     @type int    $id                Optional. Pass an ID to update an existing comment.
+ *     @type string $content           The content of the comment.
+ *     @type int    $user_id           Optional. The ID of the user making the comment.
+ *                                     Defaults to the ID of the logged-in user.
+ *     @type int    $activity_id       The ID of the "root" activity item, ie the oldest
+ *                                     ancestor of the comment.
+ *     @type int    $parent_id         Optional. The ID of the parent activity item, ie the item to
+ *                                     which the comment is an immediate reply. If not provided,
+ *                                     this value defaults to the $activity_id.
+ *     @type string $primary_link      Optional. the primary link for the comment.
+ *                                     Defaults to an empty string.
+ *     @type bool   $skip_notification Optional. false to send a comment notification, false otherwise.
+ *                                     Defaults to false.
+ *     @type string $error_type        Optional. Error type. Either 'bool' or 'wp_error'. Default: 'bool'.
+ * }
+ * @return WP_Error|bool|int The ID of the comment on success, otherwise false.
+ */
+function bp_activity_new_comment( $args = '' ) {
+	$bp = buddypress();
+
+	$r = wp_parse_args( $args, array(
+		'id'                => false,
+		'content'           => false,
+		'user_id'           => bp_loggedin_user_id(),
+		'activity_id'       => false, // ID of the root activity item.
+		'parent_id'         => false, // ID of a parent comment (optional).
+		'primary_link'      => '',
+		'skip_notification' => false,
+		'error_type'        => 'bool'
+	) );
+
+	// Error type is boolean; need to initialize some variables for backpat.
+	if ( 'bool' === $r['error_type'] ) {
+		if ( empty( $bp->activity->errors ) ) {
+			$bp->activity->errors = array();
+		}
+	}
+
+	// Default error message.
+	$feedback = __( 'There was an error posting your reply. Please try again.', 'buddypress' );
+
+	// Bail if missing necessary data.
+	if ( empty( $r['content'] ) || empty( $r['user_id'] ) || empty( $r['activity_id'] ) ) {
+		$error = new WP_Error( 'missing_data', $feedback );
+
+		if ( 'wp_error' === $r['error_type'] ) {
+			return $error;
+
+		// Backpat.
+		} else {
+			$bp->activity->errors['new_comment'] = $error;
+			return false;
+		}
+	}
+
+	// Maybe set current activity ID as the parent.
+	if ( empty( $r['parent_id'] ) ) {
+		$r['parent_id'] = $r['activity_id'];
+	}
+
+	$activity_id = $r['activity_id'];
+
+	// Get the parent activity.
+	$activity  = new BP_Activity_Activity( $activity_id );
+
+	// Bail if the parent activity does not exist.
+	if ( empty( $activity->date_recorded ) ) {
+		$error = new WP_Error( 'missing_activity', __( 'The item you were replying to no longer exists.', 'buddypress' ) );
+
+		if ( 'wp_error' === $r['error_type'] ) {
+			return $error;
+
+		// Backpat.
+		} else {
+			$bp->activity->errors['new_comment'] = $error;
+			return false;
+		}
+
+	}
+
+	// Check to see if the parent activity is hidden, and if so, hide this comment publicly.
+	$is_hidden = $activity->hide_sitewide ? 1 : 0;
+
+	/**
+	 * Filters the content of a new comment.
+	 *
+	 * @since 1.2.0
+	 * @since 3.0.0 Added $context parameter to disambiguate from bp_get_activity_comment_content().
+	 *
+	 * @param string $r       Content for the newly posted comment.
+	 * @param string $context This filter's context ("new").
+	 */
+	$comment_content = apply_filters( 'bp_activity_comment_content', $r['content'], 'new' );
+
+	// Insert the activity comment.
+	$comment_id = bp_activity_add( array(
+		'id'                => $r['id'],
+		'content'           => $comment_content,
+		'component'         => buddypress()->activity->id,
+		'type'              => 'activity_comment',
+		'primary_link'      => $r['primary_link'],
+		'user_id'           => $r['user_id'],
+		'item_id'           => $activity_id,
+		'secondary_item_id' => $r['parent_id'],
+		'hide_sitewide'     => $is_hidden,
+		'error_type'        => $r['error_type']
+	) );
+
+	// Bail on failure.
+	if ( false === $comment_id || is_wp_error( $comment_id ) ) {
+		return $comment_id;
+	}
+
+	// Comment caches are stored only with the top-level item.
+	wp_cache_delete( $activity_id, 'bp_activity_comments' );
+
+	// Walk the tree to clear caches for all parent items.
+	$clear_id = $r['parent_id'];
+	while ( $clear_id != $activity_id ) {
+		$clear_object = new BP_Activity_Activity( $clear_id );
+		wp_cache_delete( $clear_id, 'bp_activity' );
+		$clear_id = intval( $clear_object->secondary_item_id );
+	}
+	wp_cache_delete( $activity_id, 'bp_activity' );
+
+	if ( empty( $r[ 'skip_notification' ] ) ) {
+		/**
+		 * Fires near the end of an activity comment posting, before the returning of the comment ID.
+		 * Sends a notification to the user @see bp_activity_new_comment_notification_helper().
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int                  $comment_id ID of the newly posted activity comment.
+		 * @param array                $r          Array of parsed comment arguments.
+		 * @param BP_Activity_Activity $activity   Activity item being commented on.
+		 */
+		do_action( 'bp_activity_comment_posted', $comment_id, $r, $activity );
+	} else {
+		/**
+		 * Fires near the end of an activity comment posting, before the returning of the comment ID.
+		 * without sending a notification to the user
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param int                  $comment_id ID of the newly posted activity comment.
+		 * @param array                $r          Array of parsed comment arguments.
+		 * @param BP_Activity_Activity $activity   Activity item being commented on.
+		 */
+		do_action( 'bp_activity_comment_posted_notification_skipped', $comment_id, $r, $activity );
+	}
+
+	if ( empty( $comment_id ) ) {
+		$error = new WP_Error( 'comment_failed', $feedback );
+
+		if ( 'wp_error' === $r['error_type'] ) {
+			return $error;
+
+		// Backpat.
+		} else {
+			$bp->activity->errors['new_comment'] = $error;
+		}
+	}
+
+	return $comment_id;
+}
+
+/**
+ * Fetch the activity_id for an existing activity entry in the DB.
+ *
+ * @since 1.2.0
+ *
+ * @see BP_Activity_Activity::get() For more information on accepted arguments.
+ *
+ * @param array|string $args See BP_Activity_Activity::get() for description.
+ * @return int $activity_id The ID of the activity item found.
+ */
+function bp_activity_get_activity_id( $args = '' ) {
+
+	$r = bp_parse_args( $args, array(
+		'user_id'           => false,
+		'component'         => false,
+		'type'              => false,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'action'            => false,
+		'content'           => false,
+		'date_recorded'     => false,
+	) );
+
+	/**
+	 * Filters the activity ID being requested.
+	 *
+	 * @since 1.2.0
+	 * @since 2.5.0 Added the `$r` and `$args` parameters.
+	 *
+	 * @param BP_Activity_Activity $value ID returned by BP_Activity_Activity get_id() method with provided arguments.
+	 * @param array                $r     Parsed function arguments.
+	 * @param array                $args  Arguments passed to the function.
+	 */
+	return apply_filters( 'bp_activity_get_activity_id', BP_Activity_Activity::get_id(
+		$r['user_id'],
+		$r['component'],
+		$r['type'],
+		$r['item_id'],
+		$r['secondary_item_id'],
+		$r['action'],
+		$r['content'],
+		$r['date_recorded']
+	), $r, $args );
+}
+
+/**
+ * Delete activity item(s).
+ *
+ * If you're looking to hook into one action that provides the ID(s) of
+ * the activity/activities deleted, then use:
+ *
+ * add_action( 'bp_activity_deleted_activities', 'my_function' );
+ *
+ * The action passes one parameter that is a single activity ID or an
+ * array of activity IDs depending on the number deleted.
+ *
+ * If you are deleting an activity comment please use bp_activity_delete_comment();
+ *
+ * @since 1.0.0
+ *
+ * @see BP_Activity_Activity::get() For more information on accepted arguments.
+ *
+ * @param array|string $args To delete specific activity items, use
+ *                           $args = array( 'id' => $ids ); Otherwise, to use
+ *                           filters for item deletion, the argument format is
+ *                           the same as BP_Activity_Activity::get().
+ *                           See that method for a description.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_delete( $args = '' ) {
+
+	// Pass one or more the of following variables to delete by those variables.
+	$args = bp_parse_args( $args, array(
+		'id'                => false,
+		'action'            => false,
+		'content'           => false,
+		'component'         => false,
+		'type'              => false,
+		'primary_link'      => false,
+		'user_id'           => false,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'date_recorded'     => false,
+		'hide_sitewide'     => false
+	) );
+
+	/**
+	 * Fires before an activity item proceeds to be deleted.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args Array of arguments to be used with the activity deletion.
+	 */
+	do_action( 'bp_before_activity_delete', $args );
+
+	// Adjust the new mention count of any mentioned member.
+	bp_activity_adjust_mention_count( $args['id'], 'delete' );
+
+	$activity_ids_deleted = BP_Activity_Activity::delete( $args );
+	if ( empty( $activity_ids_deleted ) ) {
+		return false;
+	}
+
+	// Check if the user's latest update has been deleted.
+	$user_id = empty( $args['user_id'] )
+		? bp_loggedin_user_id()
+		: $args['user_id'];
+
+	$latest_update = bp_get_user_meta( $user_id, 'bp_latest_update', true );
+	if ( !empty( $latest_update ) ) {
+		if ( in_array( (int) $latest_update['id'], (array) $activity_ids_deleted ) ) {
+			bp_delete_user_meta( $user_id, 'bp_latest_update' );
+		}
+	}
+
+	/**
+	 * Fires after the activity item has been deleted.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $args Array of arguments used with the activity deletion.
+	 */
+	do_action( 'bp_activity_delete', $args );
+
+	/**
+	 * Fires after the activity item has been deleted.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $activity_ids_deleted Array of affected activity item IDs.
+	 */
+	do_action( 'bp_activity_deleted_activities', $activity_ids_deleted );
+
+	wp_cache_delete( 'bp_activity_sitewide_front', 'bp' );
+
+	return true;
+}
+
+	/**
+	 * Delete an activity item by activity id.
+	 *
+	 * You should use bp_activity_delete() instead.
+	 *
+	 * @since 1.1.0
+	 * @deprecated 1.2.0
+	 *
+	 *
+	 * @param array|string $args See BP_Activity_Activity::get for a
+	 *                           description of accepted arguments.
+	 * @return bool True on success, false on failure.
+	 */
+	function bp_activity_delete_by_item_id( $args = '' ) {
+
+		$r = bp_parse_args( $args, array(
+			'item_id'           => false,
+			'component'         => false,
+			'type'              => false,
+			'user_id'           => false,
+			'secondary_item_id' => false
+		) );
+
+		return bp_activity_delete( $r );
+	}
+
+	/**
+	 * Delete an activity item by activity id.
+	 *
+	 * @since 1.1.0
+	 *
+	 *
+	 * @param int $activity_id ID of the activity item to be deleted.
+	 * @return bool True on success, false on failure.
+	 */
+	function bp_activity_delete_by_activity_id( $activity_id ) {
+		return bp_activity_delete( array( 'id' => $activity_id ) );
+	}
+
+	/**
+	 * Delete an activity item by its content.
+	 *
+	 * You should use bp_activity_delete() instead.
+	 *
+	 * @since 1.1.0
+	 * @deprecated 1.2.0
+	 *
+	 *
+	 * @param int    $user_id   The user id.
+	 * @param string $content   The activity id.
+	 * @param string $component The activity component.
+	 * @param string $type      The activity type.
+	 * @return bool True on success, false on failure.
+	 */
+	function bp_activity_delete_by_content( $user_id, $content, $component, $type ) {
+		return bp_activity_delete( array(
+			'user_id'   => $user_id,
+			'content'   => $content,
+			'component' => $component,
+			'type'      => $type
+		) );
+	}
+
+	/**
+	 * Delete a user's activity for a component.
+	 *
+	 * You should use bp_activity_delete() instead.
+	 *
+	 * @since 1.1.0
+	 * @deprecated 1.2.0
+	 *
+	 *
+	 * @param int    $user_id   The user id.
+	 * @param string $component The activity component.
+	 * @return bool True on success, false on failure.
+	 */
+	function bp_activity_delete_for_user_by_component( $user_id, $component ) {
+		return bp_activity_delete( array(
+			'user_id'   => $user_id,
+			'component' => $component
+		) );
+	}
+
+/**
+ * Delete an activity comment.
+ *
+ * @since 1.2.0
+ *
+ * @todo Why is an activity id required? We could look this up.
+ * @todo Why do we encourage users to call this function directly? We could just
+ *       as easily examine the activity type in bp_activity_delete() and then
+ *       call this function with the proper arguments if necessary.
+ *
+ * @param int $activity_id The ID of the "root" activity, ie the comment's
+ *                         oldest ancestor.
+ * @param int $comment_id  The ID of the comment to be deleted.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_delete_comment( $activity_id, $comment_id ) {
+	$deleted = false;
+
+	/**
+	 * Filters whether BuddyPress should delete an activity comment or not.
+	 *
+	 * You may want to hook into this filter if you want to override this function and
+	 * handle the deletion of child comments differently. Make sure you return false.
+	 *
+	 * @since 1.2.0
+	 * @since 2.5.0 Add the deleted parameter (passed by reference)
+	 *
+	 * @param bool $value       Whether BuddyPress should continue or not.
+	 * @param int  $activity_id ID of the root activity item being deleted.
+	 * @param int  $comment_id  ID of the comment being deleted.
+	 * @param bool $deleted     Whether the activity comment has been deleted or not.
+	 */
+	if ( ! apply_filters_ref_array( 'bp_activity_delete_comment_pre', array( true, $activity_id, $comment_id, &$deleted ) ) ) {
+		return $deleted;
+	}
+
+	// Check if comment still exists.
+	$comment = new BP_Activity_Activity( $comment_id );
+	if ( empty( $comment->id ) ) {
+		return false;
+	}
+
+	// Delete any children of this comment.
+	bp_activity_delete_children( $activity_id, $comment_id );
+
+	// Delete the actual comment.
+	if ( ! bp_activity_delete( array( 'id' => $comment_id, 'type' => 'activity_comment' ) ) ) {
+		return false;
+	} else {
+		$deleted = true;
+	}
+
+	// Purge comment cache for the root activity update.
+	wp_cache_delete( $activity_id, 'bp_activity_comments' );
+
+	// Recalculate the comment tree.
+	BP_Activity_Activity::rebuild_activity_comment_tree( $activity_id );
+
+	/**
+	 * Fires at the end of the deletion of an activity comment, before returning success.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $activity_id ID of the activity that has had a comment deleted from.
+	 * @param int $comment_id  ID of the comment that was deleted.
+	 */
+	do_action( 'bp_activity_delete_comment', $activity_id, $comment_id );
+
+	return $deleted;
+}
+
+	/**
+	 * Delete an activity comment's children.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @param int $activity_id The ID of the "root" activity, ie the
+	 *                         comment's oldest ancestor.
+	 * @param int $comment_id  The ID of the comment to be deleted.
+	 */
+	function bp_activity_delete_children( $activity_id, $comment_id ) {
+		// Check if comment still exists.
+		$comment = new BP_Activity_Activity( $comment_id );
+		if ( empty( $comment->id ) ) {
+			return;
+		}
+
+		// Get activity children to delete.
+		$children = BP_Activity_Activity::get_child_comments( $comment_id );
+
+		// Recursively delete all children of this comment.
+		if ( ! empty( $children ) ) {
+			foreach( (array) $children as $child ) {
+				bp_activity_delete_children( $activity_id, $child->id );
+			}
+		}
+
+		// Delete the comment itself.
+		bp_activity_delete( array(
+			'secondary_item_id' => $comment_id,
+			'type'              => 'activity_comment',
+			'item_id'           => $activity_id
+		) );
+	}
+
+/**
+ * Get the permalink for a single activity item.
+ *
+ * When only the $activity_id param is passed, BP has to instantiate a new
+ * BP_Activity_Activity object. To save yourself some processing overhead,
+ * be sure to pass the full $activity_obj parameter as well, if you already
+ * have it available.
+ *
+ * @since 1.2.0
+ *
+ * @param int         $activity_id  The unique id of the activity object.
+ * @param object|bool $activity_obj Optional. The activity object.
+ * @return string $link Permalink for the activity item.
+ */
+function bp_activity_get_permalink( $activity_id, $activity_obj = false ) {
+	$bp = buddypress();
+
+	if ( empty( $activity_obj ) ) {
+		$activity_obj = new BP_Activity_Activity( $activity_id );
+	}
+
+	if ( isset( $activity_obj->current_comment ) ) {
+		$activity_obj = $activity_obj->current_comment;
+	}
+
+	$use_primary_links = array(
+		'new_blog_post',
+		'new_blog_comment',
+		'new_forum_topic',
+		'new_forum_post',
+	);
+
+	if ( ! empty( $bp->activity->track ) ) {
+		$use_primary_links = array_merge( $use_primary_links, array_keys( $bp->activity->track ) );
+	}
+
+	if ( false !== array_search( $activity_obj->type, $use_primary_links ) ) {
+		$link = $activity_obj->primary_link;
+	} else {
+		if ( 'activity_comment' == $activity_obj->type ) {
+			$link = bp_get_root_domain() . '/' . bp_get_activity_root_slug() . '/p/' . $activity_obj->item_id . '/#acomment-' . $activity_obj->id;
+		} else {
+			$link = bp_get_root_domain() . '/' . bp_get_activity_root_slug() . '/p/' . $activity_obj->id . '/';
+		}
+	}
+
+	/**
+	 * Filters the activity permalink for the specified activity item.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $array Array holding activity permalink and activity item object.
+	 */
+	return apply_filters_ref_array( 'bp_activity_get_permalink', array( $link, &$activity_obj ) );
+}
+
+/**
+ * Can a user see a particular activity item?
+ *
+ * @since 3.0.0
+ *
+ * @param  BP_Activity_Activity $activity Activity object.
+ * @param  integer              $user_id  User ID.
+ * @return boolean True on success, false on failure.
+ */
+function bp_activity_user_can_read( $activity, $user_id = 0 ) {
+	$retval = true;
+
+	// Fallback.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// If activity is from a group, do extra cap checks.
+	if ( bp_is_active( 'groups' ) && buddypress()->groups->id === $activity->component ) {
+		// Check to see if the user has access to the activity's parent group.
+		$group = groups_get_group( $activity->item_id );
+		if ( $group ) {
+			// For logged-in user, we can check against the 'user_has_access' prop.
+			if ( bp_loggedin_user_id() === $user_id ) {
+				$retval = $group->user_has_access;
+
+			// Manually check status.
+			} elseif ( 'private' === $group->status || 'hidden' === $group->status ) {
+				// Only group members that are not banned can view.
+				if ( ! groups_is_user_member( $user_id, $activity->item_id ) || groups_is_user_banned( $user_id, $activity->item_id ) ) {
+					$retval = false;
+				}
+			}
+		}
+	}
+
+	// Spammed items are not visible to the public.
+	if ( $activity->is_spam ) {
+		$retval = false;
+	}
+
+	// Site moderators can view anything.
+	if ( bp_current_user_can( 'bp_moderate' ) ) {
+		$retval = true;
+	}
+
+	/**
+	 * Filters whether the current user has access to an activity item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param bool                 $retval   Return value.
+	 * @param int                  $user_id  Current user ID.
+	 * @param BP_Activity_Activity $activity Activity object.
+	 */
+	return apply_filters( 'bp_activity_user_can_read', $retval, $user_id, $activity );
+}
+
+/**
+ * Hide a user's activity.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id The ID of the user whose activity is being hidden.
+ * @return bool True on success, false on failure.
+ */
+function bp_activity_hide_user_activity( $user_id ) {
+	return BP_Activity_Activity::hide_all_for_user( $user_id );
+}
+
+/**
+ * Take content, remove images, and replace them with a single thumbnail image.
+ *
+ * The format of items in the activity stream is such that we do not want to
+ * allow an arbitrary number of arbitrarily large images to be rendered.
+ * However, the activity stream is built to elegantly display a single
+ * thumbnail corresponding to the activity comment. This function looks
+ * through the content, grabs the first image and converts it to a thumbnail,
+ * and removes the rest of the images from the string.
+ *
+ * As of BuddyPress 2.3, this function is no longer in use.
+ *
+ * @since 1.2.0
+ *
+ * @param string      $content The content of the activity item.
+ * @param string|bool $link    Optional. The unescaped URL that the image should link
+ *                             to. If absent, the image will not be a link.
+ * @param array|bool  $args    Optional. The args passed to the activity
+ *                             creation function (eg bp_blogs_record_activity()).
+ * @return string $content The content with images stripped and replaced with a
+ *                         single thumb.
+ */
+function bp_activity_thumbnail_content_images( $content, $link = false, $args = false ) {
+
+	preg_match_all( '/<img[^>]*>/Ui', $content, $matches );
+
+	// Remove <img> tags. Also remove caption shortcodes and caption text if present.
+	$content = preg_replace('|(\[caption(.*?)\])?<img[^>]*>([^\[\[]*\[\/caption\])?|', '', $content );
+
+	if ( !empty( $matches ) && !empty( $matches[0] ) ) {
+
+		// Get the SRC value.
+		preg_match( '/<img.*?(src\=[\'|"]{0,1}.*?[\'|"]{0,1})[\s|>]{1}/i',    $matches[0][0], $src    );
+
+		// Get the width and height.
+		preg_match( '/<img.*?(height\=[\'|"]{0,1}.*?[\'|"]{0,1})[\s|>]{1}/i', $matches[0][0], $height );
+		preg_match( '/<img.*?(width\=[\'|"]{0,1}.*?[\'|"]{0,1})[\s|>]{1}/i',  $matches[0][0], $width  );
+
+		if ( ! empty( $src ) ) {
+			$src = substr( substr( str_replace( 'src=', '', $src[1] ), 0, -1 ), 1 );
+
+			if ( isset( $width[1] ) ) {
+				$width = substr( substr( str_replace( 'width=', '', $width[1] ), 0, -1 ), 1 );
+			}
+
+			if ( isset( $height[1] ) ) {
+				$height = substr( substr( str_replace( 'height=', '', $height[1] ), 0, -1 ), 1 );
+			}
+
+			if ( empty( $width ) || empty( $height ) ) {
+				$width  = 100;
+				$height = 100;
+			}
+
+			$ratio      = (int) $width / (int) $height;
+			$new_height = (int) $height >= 100 ? 100 : $height;
+			$new_width  = $new_height * $ratio;
+			$image      = '<img src="' . esc_url( $src ) . '" width="' . absint( $new_width ) . '" height="' . absint( $new_height ) . '" alt="' . __( 'Thumbnail', 'buddypress' ) . '" class="align-left thumbnail" />';
+
+			if ( !empty( $link ) ) {
+				$image = '<a href="' . esc_url( $link ) . '">' . $image . '</a>';
+			}
+
+			$content = $image . $content;
+		}
+	}
+
+	/**
+	 * Filters the activity content that had a thumbnail replace images.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $content Activity content that had images replaced in.
+	 * @param array  $matches Array of all image tags found in the posted content.
+	 * @param array  $args    Arguments passed into function creating the activity update.
+	 */
+	return apply_filters( 'bp_activity_thumbnail_content_images', $content, $matches, $args );
+}
+
+/**
+ * Gets the excerpt length for activity items.
+ *
+ * @since 2.8.0
+ *
+ * @return int Character length for activity excerpts.
+ */
+function bp_activity_get_excerpt_length() {
+	/**
+	 * Filters the excerpt length for the activity excerpt.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int Character length for activity excerpts.
+	 */
+	return (int) apply_filters( 'bp_activity_excerpt_length', 358 );
+}
+
+/**
+ * Create a rich summary of an activity item for the activity stream.
+ *
+ * More than just a simple excerpt, the summary could contain oEmbeds and other types of media.
+ * Currently, it's only used for blog post items, but it will probably be used for all types of
+ * activity in the future.
+ *
+ * @since 2.3.0
+ *
+ * @param string $content  The content of the activity item.
+ * @param array  $activity The data passed to bp_activity_add() or the values
+ *                         from an Activity obj.
+ * @return string $summary
+ */
+function bp_activity_create_summary( $content, $activity ) {
+	$args = array(
+		'width' => isset( $GLOBALS['content_width'] ) ? (int) $GLOBALS['content_width'] : 'medium',
+	);
+
+	// Get the WP_Post object if this activity type is a blog post.
+	if ( $activity['type'] === 'new_blog_post' ) {
+		$content = get_post( $activity['secondary_item_id'] );
+	}
+
+	/**
+	 * Filter the class name of the media extractor when creating an Activity summary.
+	 *
+	 * Use this filter to change the media extractor used to extract media info for the activity item.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $extractor Class name.
+	 * @param string $content   The content of the activity item.
+	 * @param array  $activity  The data passed to bp_activity_add() or the values from an Activity obj.
+	 */
+	$extractor = apply_filters( 'bp_activity_create_summary_extractor_class', 'BP_Media_Extractor', $content, $activity );
+	$extractor = new $extractor;
+
+	/**
+	 * Filter the arguments passed to the media extractor when creating an Activity summary.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array              $args      Array of bespoke data for the media extractor.
+	 * @param string             $content   The content of the activity item.
+	 * @param array              $activity  The data passed to bp_activity_add() or the values from an Activity obj.
+	 * @param BP_Media_Extractor $extractor The media extractor object.
+	 */
+	$args = apply_filters( 'bp_activity_create_summary_extractor_args', $args, $content, $activity, $extractor );
+
+
+	// Extract media information from the $content.
+	$media = $extractor->extract( $content, BP_Media_Extractor::ALL, $args );
+
+	// If we converted $content to an object earlier, flip it back to a string.
+	if ( is_a( $content, 'WP_Post' ) ) {
+		$content = $content->post_content;
+	}
+
+	$para_count     = substr_count( strtolower( wpautop( $content ) ), '<p>' );
+	$has_audio      = ! empty( $media['has']['audio'] )           && $media['has']['audio'];
+	$has_videos     = ! empty( $media['has']['videos'] )          && $media['has']['videos'];
+	$has_feat_image = ! empty( $media['has']['featured_images'] ) && $media['has']['featured_images'];
+	$has_galleries  = ! empty( $media['has']['galleries'] )       && $media['has']['galleries'];
+	$has_images     = ! empty( $media['has']['images'] )          && $media['has']['images'];
+	$has_embeds     = false;
+
+	// Embeds must be subtracted from the paragraph count.
+	if ( ! empty( $media['has']['embeds'] ) ) {
+		$has_embeds = $media['has']['embeds'] > 0;
+		$para_count -= $media['has']['embeds'];
+	}
+
+	$extracted_media = array();
+	$use_media_type  = '';
+	$image_source    = '';
+
+	// If it's a short article and there's an embed/audio/video, use it.
+	if ( $para_count <= 3 ) {
+		if ( $has_embeds ) {
+			$use_media_type = 'embeds';
+		} elseif ( $has_audio ) {
+			$use_media_type = 'audio';
+		} elseif ( $has_videos ) {
+			$use_media_type = 'videos';
+		}
+	}
+
+	// If not, or in any other situation, try to use an image.
+	if ( ! $use_media_type && $has_images ) {
+		$use_media_type = 'images';
+		$image_source   = 'html';
+
+		// Featured Image > Galleries > inline <img>.
+		if ( $has_feat_image ) {
+			$image_source = 'featured_images';
+
+		} elseif ( $has_galleries ) {
+			$image_source = 'galleries';
+		}
+	}
+
+	// Extract an item from the $media results.
+	if ( $use_media_type ) {
+		if ( $use_media_type === 'images' ) {
+			$extracted_media = wp_list_filter( $media[ $use_media_type ], array( 'source' => $image_source ) );
+			$extracted_media = array_shift( $extracted_media );
+		} else {
+			$extracted_media = array_shift( $media[ $use_media_type ] );
+		}
+
+		/**
+		 * Filter the results of the media extractor when creating an Activity summary.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $extracted_media Extracted media item. See {@link BP_Media_Extractor::extract()} for format.
+		 * @param string $content         Content of the activity item.
+		 * @param array  $activity        The data passed to bp_activity_add() or the values from an Activity obj.
+		 * @param array  $media           All results from the media extraction.
+		 *                                See {@link BP_Media_Extractor::extract()} for format.
+		 * @param string $use_media_type  The kind of media item that was preferentially extracted.
+		 * @param string $image_source    If $use_media_type was "images", the preferential source of the image.
+		 *                                Otherwise empty.
+		 */
+		$extracted_media = apply_filters(
+			'bp_activity_create_summary_extractor_result',
+			$extracted_media,
+			$content,
+			$activity,
+			$media,
+			$use_media_type,
+			$image_source
+		);
+	}
+
+	// Generate a text excerpt for this activity item (and remove any oEmbeds URLs).
+	$summary = bp_create_excerpt( html_entity_decode( $content ), 225, array(
+		'html' => false,
+		'filter_shortcodes' => true,
+		'strip_tags'        => true,
+		'remove_links'      => true
+	) );
+
+	if ( $use_media_type === 'embeds' ) {
+		$summary .= PHP_EOL . PHP_EOL . $extracted_media['url'];
+	} elseif ( $use_media_type === 'images' ) {
+		$summary .= sprintf( ' <img src="%s">', esc_url( $extracted_media['url'] ) );
+	} elseif ( in_array( $use_media_type, array( 'audio', 'videos' ), true ) ) {
+		$summary .= PHP_EOL . PHP_EOL . $extracted_media['original'];  // Full shortcode.
+	}
+
+	/**
+	 * Filters the newly-generated summary for the activity item.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $summary         Activity summary HTML.
+	 * @param string $content         Content of the activity item.
+	 * @param array  $activity        The data passed to bp_activity_add() or the values from an Activity obj.
+	 * @param array  $extracted_media Media item extracted. See {@link BP_Media_Extractor::extract()} for format.
+	 */
+	return apply_filters( 'bp_activity_create_summary', $summary, $content, $activity, $extracted_media );
+}
+
+/**
+ * Fetch whether the current user is allowed to mark items as spam.
+ *
+ * @since 1.6.0
+ *
+ * @return bool True if user is allowed to mark activity items as spam.
+ */
+function bp_activity_user_can_mark_spam() {
+
+	/**
+	 * Filters whether the current user should be able to mark items as spam.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $moderate Whether or not the current user has bp_moderate capability.
+	 */
+	return apply_filters( 'bp_activity_user_can_mark_spam', bp_current_user_can( 'bp_moderate' ) );
+}
+
+/**
+ * Mark an activity item as spam.
+ *
+ * @since 1.6.0
+ *
+ * @todo We should probably save $source to activity meta.
+ *
+ * @param BP_Activity_Activity $activity The activity item to be spammed.
+ * @param string               $source   Optional. Default is "by_a_person" (ie, a person has
+ *                                       manually marked the activity as spam). BP core also
+ *                                       accepts 'by_akismet'.
+ */
+function bp_activity_mark_as_spam( &$activity, $source = 'by_a_person' ) {
+	$bp = buddypress();
+
+	$activity->is_spam = 1;
+
+	// Clear the activity stream first page cache.
+	wp_cache_delete( 'bp_activity_sitewide_front', 'bp' );
+
+	// Clear the activity comment cache for this activity item.
+	wp_cache_delete( $activity->id, 'bp_activity_comments' );
+
+	// If Akismet is active, and this was a manual spam/ham request, stop Akismet checking the activity.
+	if ( 'by_a_person' == $source && !empty( $bp->activity->akismet ) ) {
+		remove_action( 'bp_activity_before_save', array( $bp->activity->akismet, 'check_activity' ), 4 );
+
+		// Build data package for Akismet.
+		$activity_data = BP_Akismet::build_akismet_data_package( $activity );
+
+		// Tell Akismet this is spam.
+		$activity_data = $bp->activity->akismet->send_akismet_request( $activity_data, 'submit', 'spam' );
+
+		// Update meta.
+		add_action( 'bp_activity_after_save', array( $bp->activity->akismet, 'update_activity_spam_meta' ), 1, 1 );
+	}
+
+	/**
+	 * Fires at the end of the process to mark an activity item as spam.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity Activity item being marked as spam.
+	 * @param string               $source   Source of determination of spam status. For example
+	 *                                       "by_a_person" or "by_akismet".
+	 */
+	do_action( 'bp_activity_mark_as_spam', $activity, $source );
+}
+
+/**
+ * Mark an activity item as ham.
+ *
+ * @since 1.6.0
+ *
+ * @param BP_Activity_Activity $activity The activity item to be hammed. Passed by reference.
+ * @param string               $source   Optional. Default is "by_a_person" (ie, a person has
+ *                                       manually marked the activity as spam). BP core also accepts
+ *                                       'by_akismet'.
+ */
+function bp_activity_mark_as_ham( &$activity, $source = 'by_a_person' ) {
+	$bp = buddypress();
+
+	$activity->is_spam = 0;
+
+	// Clear the activity stream first page cache.
+	wp_cache_delete( 'bp_activity_sitewide_front', 'bp' );
+
+	// Clear the activity comment cache for this activity item.
+	wp_cache_delete( $activity->id, 'bp_activity_comments' );
+
+	// If Akismet is active, and this was a manual spam/ham request, stop Akismet checking the activity.
+	if ( 'by_a_person' == $source && !empty( $bp->activity->akismet ) ) {
+		remove_action( 'bp_activity_before_save', array( $bp->activity->akismet, 'check_activity' ), 4 );
+
+		// Build data package for Akismet.
+		$activity_data = BP_Akismet::build_akismet_data_package( $activity );
+
+		// Tell Akismet this is spam.
+		$activity_data = $bp->activity->akismet->send_akismet_request( $activity_data, 'submit', 'ham' );
+
+		// Update meta.
+		add_action( 'bp_activity_after_save', array( $bp->activity->akismet, 'update_activity_ham_meta' ), 1, 1 );
+	}
+
+	/**
+	 * Fires at the end of the process to mark an activity item as ham.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity Activity item being marked as ham.
+	 * @param string               $source   Source of determination of ham status. For example
+	 *                                       "by_a_person" or "by_akismet".
+	 */
+	do_action( 'bp_activity_mark_as_ham', $activity, $source );
+}
+
+/* Emails *********************************************************************/
+
+/**
+ * Send email and BP notifications when a user is mentioned in an update.
+ *
+ * @since 1.2.0
+ *
+ * @param int $activity_id      The ID of the activity update.
+ * @param int $receiver_user_id The ID of the user who is receiving the update.
+ */
+function bp_activity_at_message_notification( $activity_id, $receiver_user_id ) {
+	$notifications = BP_Core_Notification::get_all_for_user( $receiver_user_id, 'all' );
+
+	// Don't leave multiple notifications for the same activity item.
+	foreach( $notifications as $notification ) {
+		if ( $activity_id == $notification->item_id ) {
+			return;
+		}
+	}
+
+	$activity     = new BP_Activity_Activity( $activity_id );
+	$email_type   = 'activity-at-message';
+	$group_name   = '';
+	$message_link = bp_activity_get_permalink( $activity_id );
+	$poster_name  = bp_core_get_user_displayname( $activity->user_id );
+
+	remove_filter( 'bp_get_activity_content_body', 'convert_smilies' );
+	remove_filter( 'bp_get_activity_content_body', 'wpautop' );
+	remove_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+
+	/** This filter is documented in bp-activity/bp-activity-template.php */
+	$content = apply_filters_ref_array( 'bp_get_activity_content_body', array( $activity->content, &$activity ) );
+
+	add_filter( 'bp_get_activity_content_body', 'convert_smilies' );
+	add_filter( 'bp_get_activity_content_body', 'wpautop' );
+	add_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+
+	// Now email the user with the contents of the message (if they have enabled email notifications).
+	if ( 'no' != bp_get_user_meta( $receiver_user_id, 'notification_activity_new_mention', true ) ) {
+		if ( bp_is_active( 'groups' ) && bp_is_group() ) {
+			$email_type = 'groups-at-message';
+			$group_name = bp_get_current_group_name();
+		}
+
+		$unsubscribe_args = array(
+			'user_id'           => $receiver_user_id,
+			'notification_type' => $email_type,
+		);
+
+		$args = array(
+			'tokens' => array(
+				'activity'         => $activity,
+				'usermessage'      => wp_strip_all_tags( $content ),
+				'group.name'       => $group_name,
+				'mentioned.url'    => $message_link,
+				'poster.name'      => $poster_name,
+				'receiver-user.id' => $receiver_user_id,
+				'unsubscribe' 	   => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+			),
+		);
+
+		bp_send_email( $email_type, $receiver_user_id, $args );
+	}
+
+	/**
+	 * Fires after the sending of an @mention email notification.
+	 *
+	 * @since 1.5.0
+	 * @since 2.5.0 $subject, $message, $content arguments unset and deprecated.
+	 *
+	 * @param BP_Activity_Activity $activity         Activity Item object.
+	 * @param string               $deprecated       Removed in 2.5; now an empty string.
+	 * @param string               $deprecated       Removed in 2.5; now an empty string.
+	 * @param string               $deprecated       Removed in 2.5; now an empty string.
+	 * @param int                  $receiver_user_id The ID of the user who is receiving the update.
+	 */
+	do_action( 'bp_activity_sent_mention_email', $activity, '', '', '', $receiver_user_id );
+}
+
+/**
+ * Send email and BP notifications when an activity item receives a comment.
+ *
+ * @since 1.2.0
+ * @since 2.5.0 Updated to use new email APIs.
+ *
+ * @param int   $comment_id   The comment id.
+ * @param int   $commenter_id The ID of the user who posted the comment.
+ * @param array $params       {@link bp_activity_new_comment()}.
+ */
+function bp_activity_new_comment_notification( $comment_id = 0, $commenter_id = 0, $params = array() ) {
+	$original_activity = new BP_Activity_Activity( $params['activity_id'] );
+	$poster_name       = bp_core_get_user_displayname( $commenter_id );
+	$thread_link       = bp_activity_get_permalink( $params['activity_id'] );
+
+	remove_filter( 'bp_get_activity_content_body', 'convert_smilies' );
+	remove_filter( 'bp_get_activity_content_body', 'wpautop' );
+	remove_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+
+	/** This filter is documented in bp-activity/bp-activity-template.php */
+	$content = apply_filters_ref_array( 'bp_get_activity_content_body', array( $params['content'], &$original_activity ) );
+
+	add_filter( 'bp_get_activity_content_body', 'convert_smilies' );
+	add_filter( 'bp_get_activity_content_body', 'wpautop' );
+	add_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+
+	if ( $original_activity->user_id != $commenter_id ) {
+
+		// Send an email if the user hasn't opted-out.
+		if ( 'no' != bp_get_user_meta( $original_activity->user_id, 'notification_activity_new_reply', true ) ) {
+
+			$unsubscribe_args = array(
+				'user_id'           => $original_activity->user_id,
+				'notification_type' => 'activity-comment',
+			);
+
+			$args = array(
+				'tokens' => array(
+					'comment.id'                => $comment_id,
+					'commenter.id'              => $commenter_id,
+					'usermessage'               => wp_strip_all_tags( $content ),
+					'original_activity.user_id' => $original_activity->user_id,
+					'poster.name'               => $poster_name,
+					'thread.url'                => esc_url( $thread_link ),
+					'unsubscribe'               => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+				),
+			);
+
+			bp_send_email( 'activity-comment', $original_activity->user_id, $args );
+		}
+
+		/**
+		 * Fires at the point that notifications should be sent for activity comments.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param BP_Activity_Activity $original_activity The original activity.
+		 * @param int                  $comment_id        ID for the newly received comment.
+		 * @param int                  $commenter_id      ID of the user who made the comment.
+		 * @param array                $params            Arguments used with the original activity comment.
+		 */
+		do_action( 'bp_activity_sent_reply_to_update_notification', $original_activity, $comment_id, $commenter_id, $params );
+	}
+
+
+	/*
+	 * If this is a reply to another comment, send an email notification to the
+	 * author of the immediate parent comment.
+	 */
+	if ( empty( $params['parent_id'] ) || ( $params['activity_id'] == $params['parent_id'] ) ) {
+		return;
+	}
+
+	$parent_comment = new BP_Activity_Activity( $params['parent_id'] );
+
+	if ( $parent_comment->user_id != $commenter_id && $original_activity->user_id != $parent_comment->user_id ) {
+
+		// Send an email if the user hasn't opted-out.
+		if ( 'no' != bp_get_user_meta( $parent_comment->user_id, 'notification_activity_new_reply', true ) ) {
+
+			$unsubscribe_args = array(
+				'user_id'           => $parent_comment->user_id,
+				'notification_type' => 'activity-comment-author',
+			);
+
+			$args = array(
+				'tokens' => array(
+					'comment.id'             => $comment_id,
+					'commenter.id'           => $commenter_id,
+					'usermessage'            => wp_strip_all_tags( $content ),
+					'parent-comment-user.id' => $parent_comment->user_id,
+					'poster.name'            => $poster_name,
+					'thread.url'             => esc_url( $thread_link ),
+					'unsubscribe'            => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+				),
+			);
+
+			bp_send_email( 'activity-comment-author', $parent_comment->user_id, $args );
+		}
+
+		/**
+		 * Fires at the point that notifications should be sent for comments on activity replies.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param BP_Activity_Activity $parent_comment The parent activity.
+		 * @param int                  $comment_id     ID for the newly received comment.
+		 * @param int                  $commenter_id   ID of the user who made the comment.
+		 * @param array                $params         Arguments used with the original activity comment.
+		 */
+		do_action( 'bp_activity_sent_reply_to_reply_notification', $parent_comment, $comment_id, $commenter_id, $params );
+	}
+}
+
+/**
+ * Helper method to map action arguments to function parameters.
+ *
+ * @since 1.9.0
+ *
+ * @param int   $comment_id ID of the comment being notified about.
+ * @param array $params     Parameters to use with notification.
+ */
+function bp_activity_new_comment_notification_helper( $comment_id, $params ) {
+	bp_activity_new_comment_notification( $comment_id, $params['user_id'], $params );
+}
+add_action( 'bp_activity_comment_posted', 'bp_activity_new_comment_notification_helper', 10, 2 );
+
+/** Embeds *******************************************************************/
+
+/**
+ * Set up activity oEmbed cache during the activity loop.
+ *
+ * During an activity loop, this function sets up the hooks necessary to grab
+ * each item's embeds from the cache, or put them in the cache if they are
+ * not there yet.
+ *
+ * This does not cover recursive activity comments, as they do not use a real loop.
+ * For that, see {@link bp_activity_comment_embed()}.
+ *
+ * @since 1.5.0
+ *
+ * @see BP_Embed
+ * @see bp_embed_activity_cache()
+ * @see bp_embed_activity_save_cache()
+ *
+ */
+function bp_activity_embed() {
+	add_filter( 'embed_post_id',         'bp_get_activity_id'                  );
+	add_filter( 'oembed_dataparse',      'bp_activity_oembed_dataparse', 10, 2 );
+	add_filter( 'bp_embed_get_cache',    'bp_embed_activity_cache',      10, 3 );
+	add_action( 'bp_embed_update_cache', 'bp_embed_activity_save_cache', 10, 3 );
+}
+add_action( 'activity_loop_start', 'bp_activity_embed' );
+
+/**
+ * Cache full oEmbed response from oEmbed.
+ *
+ * @since 2.6.0
+ *
+ * @param string $retval Current oEmbed result.
+ * @param object $data   Full oEmbed response.
+ * @param string $url    URL used for the oEmbed request.
+ * @return string
+ */
+function bp_activity_oembed_dataparse( $retval, $data ) {
+	buddypress()->activity->oembed_response = $data;
+
+	return $retval;
+}
+
+/**
+ * Set up activity oEmbed cache while recursing through activity comments.
+ *
+ * While crawling through an activity comment tree
+ * ({@link bp_activity_recurse_comments}), this function sets up the hooks
+ * necessary to grab each comment's embeds from the cache, or put them in
+ * the cache if they are not there yet.
+ *
+ * @since 1.5.0
+ *
+ * @see BP_Embed
+ * @see bp_embed_activity_cache()
+ * @see bp_embed_activity_save_cache()
+ *
+ */
+function bp_activity_comment_embed() {
+	add_filter( 'embed_post_id',         'bp_get_activity_comment_id'          );
+	add_filter( 'bp_embed_get_cache',    'bp_embed_activity_cache',      10, 3 );
+	add_action( 'bp_embed_update_cache', 'bp_embed_activity_save_cache', 10, 3 );
+}
+add_action( 'bp_before_activity_comment', 'bp_activity_comment_embed' );
+
+/**
+ * When a user clicks on a "Read More" item, make sure embeds are correctly parsed and shown for the expanded content.
+ *
+ * @since 1.5.0
+ *
+ * @see BP_Embed
+ *
+ * @param object $activity The activity that is being expanded.
+ */
+function bp_dtheme_embed_read_more( $activity ) {
+	buddypress()->activity->read_more_id = $activity->id;
+
+	add_filter( 'embed_post_id',         function() { return buddypress()->activity->read_more_id; } );
+	add_filter( 'bp_embed_get_cache',    'bp_embed_activity_cache',      10, 3 );
+	add_action( 'bp_embed_update_cache', 'bp_embed_activity_save_cache', 10, 3 );
+}
+add_action( 'bp_dtheme_get_single_activity_content',       'bp_dtheme_embed_read_more' );
+add_action( 'bp_legacy_theme_get_single_activity_content', 'bp_dtheme_embed_read_more' );
+
+/**
+ * Clean up 'embed_post_id' filter after comment recursion.
+ *
+ * This filter must be removed so that the non-comment filters take over again
+ * once the comments are done being processed.
+ *
+ * @since 1.5.0
+ *
+ * @see bp_activity_comment_embed()
+ */
+function bp_activity_comment_embed_after_recurse() {
+	remove_filter( 'embed_post_id', 'bp_get_activity_comment_id' );
+}
+add_action( 'bp_after_activity_comment', 'bp_activity_comment_embed_after_recurse' );
+
+/**
+ * Fetch an activity item's cached embeds.
+ *
+ * Used during {@link BP_Embed::parse_oembed()} via {@link bp_activity_embed()}.
+ *
+ * @since 1.5.0
+ *
+ * @see BP_Embed::parse_oembed()
+ *
+ * @param string $cache    An empty string passed by BP_Embed::parse_oembed() for
+ *                         functions like this one to filter.
+ * @param int    $id       The ID of the activity item.
+ * @param string $cachekey The cache key generated in BP_Embed::parse_oembed().
+ * @return mixed The cached embeds for this activity item.
+ */
+function bp_embed_activity_cache( $cache, $id, $cachekey ) {
+	return bp_activity_get_meta( $id, $cachekey );
+}
+
+/**
+ * Set an activity item's embed cache.
+ *
+ * Used during {@link BP_Embed::parse_oembed()} via {@link bp_activity_embed()}.
+ *
+ * @since 1.5.0
+ *
+ * @see BP_Embed::parse_oembed()
+ *
+ * @param string $cache    An empty string passed by BP_Embed::parse_oembed() for
+ *                         functions like this one to filter.
+ * @param string $cachekey The cache key generated in BP_Embed::parse_oembed().
+ * @param int    $id       The ID of the activity item.
+ */
+function bp_embed_activity_save_cache( $cache, $cachekey, $id ) {
+	bp_activity_update_meta( $id, $cachekey, $cache );
+
+	// Cache full oEmbed response.
+	if ( true === isset( buddypress()->activity->oembed_response ) ) {
+		$cachekey = str_replace( '_oembed', '_oembed_response', $cachekey );
+		bp_activity_update_meta( $id, $cachekey, buddypress()->activity->oembed_response );
+	}
+}
+
+/**
+ * Should we use Heartbeat to refresh activities?
+ *
+ * @since 2.0.0
+ *
+ * @return bool True if activity heartbeat is enabled, otherwise false.
+ */
+function bp_activity_do_heartbeat() {
+	$retval = false;
+
+	if ( bp_is_activity_heartbeat_active() && ( bp_is_activity_directory() || bp_is_group_activity() ) ) {
+		$retval = true;
+	}
+
+	/**
+	 * Filters whether the heartbeat feature in the activity stream should be active.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param bool $retval Whether or not activity heartbeat is active.
+	 */
+	return (bool) apply_filters( 'bp_activity_do_heartbeat', $retval );
+}
+
+/**
+ * AJAX endpoint for Suggestions API lookups.
+ *
+ * @since 2.1.0
+ */
+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 );
+}
+add_action( 'wp_ajax_bp_get_suggestions', 'bp_ajax_get_suggestions' );
+
+/**
+ * Detect a change in post type status, and initiate an activity update if necessary.
+ *
+ * @since 2.2.0
+ *
+ * @todo Support untrashing better.
+ *
+ * @param string $new_status New status for the post.
+ * @param string $old_status Old status for the post.
+ * @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 );
+	}
+}
+add_action( 'transition_post_status', 'bp_activity_catch_transition_post_type_status', 10, 3 );
+
+/**
+ * When a post type comment status transition occurs, update the relevant activity's status.
+ *
+ * @since 2.5.0
+ *
+ * @param string     $new_status New comment status.
+ * @param string     $old_status Previous comment status.
+ * @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 );
+}
+add_action( 'transition_comment_status', 'bp_activity_transition_post_type_comment_status', 10, 3 );
+
+/**
+ * Finds and exports personal data associated with an email address from the Activity tables.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_activity_personal_data_exporter( $email_address, $page ) {
+	$number = 50;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$activities = bp_activity_get( array(
+		'display_comments' => 'stream',
+		'per_page'         => $number,
+		'page'             => $page,
+		'show_hidden'      => true,
+		'filter'           => array(
+			'user_id' => $user->ID,
+		),
+	) );
+
+	$user_data_to_export = array();
+	$activity_actions    = bp_activity_get_actions();
+
+	foreach ( $activities['activities'] as $activity ) {
+		if ( ! empty( $activity_actions->{$activity->component}->{$activity->type}['format_callback'] ) ) {
+			$description = call_user_func( $activity_actions->{$activity->component}->{$activity->type}['format_callback'], '', $activity );
+		} elseif ( ! empty( $activity->action ) ) {
+			$description = $activity->action;
+		} else {
+			$description = $activity->type;
+		}
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Activity Date', 'buddypress' ),
+				'value' => $activity->date_recorded,
+			),
+			array(
+				'name'  => __( 'Activity Description', 'buddypress' ),
+				'value' => $description,
+			),
+			array(
+				'name'  => __( 'Activity URL', 'buddypress' ),
+				'value' => bp_activity_get_permalink( $activity->id, $activity ),
+			),
+		);
+
+		if ( ! empty( $activity->content ) ) {
+			$item_data[] = array(
+				'name'  => __( 'Activity Content', 'buddypress' ),
+				'value' => $activity->content,
+			);
+		}
+
+		/**
+		 * Filters the data associated with an activity item when assembled for a WP personal data export.
+		 *
+		 * Plugins that register activity types whose `action` string doesn't adequately
+		 * describe the activity item for the purposes of data export may filter the activity
+		 * item data here.
+		 *
+		 * @since 4.0.0
+		 *
+		 * @param array                $item_data Array of data describing the activity item.
+		 * @param BP_Activity_Activity $activity  Activity item.
+		 */
+		$item_data = apply_filters( 'bp_activity_personal_data_export_item_data', $item_data, $activity );
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_activity',
+			'group_label' => __( 'Activity', 'buddypress' ),
+			'item_id'     => "bp-activity-{$activity->id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $activities['activities'] ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-loader.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BuddyPress Activity Streams Loader.
+ *
+ * An activity stream component, for users, groups, and site tracking.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityCore
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-activity component.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_activity() {
+	buddypress()->activity = new BP_Activity_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_activity', 6 );
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-notifications.php	(revision 12265)
@@ -0,0 +1,437 @@
+<?php
+/**
+ * BuddyPress Activity Notifications.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityNotifications
+ * @since 1.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Format notifications related to activity.
+ *
+ * @since 1.5.0
+ *
+ * @param string $action            The type of activity item. Just 'new_at_mention' for now.
+ * @param int    $item_id           The activity ID.
+ * @param int    $secondary_item_id In the case of at-mentions, this is the mentioner's ID.
+ * @param int    $total_items       The total number of notifications to format.
+ * @param string $format            'string' to get a BuddyBar-compatible notification, 'array' otherwise.
+ * @param int    $id                Optional. The notification ID.
+ * @return string $return Formatted @mention notification.
+ */
+function bp_activity_format_notifications( $action, $item_id, $secondary_item_id, $total_items, $format = 'string', $id = 0 ) {
+	$action_filter = $action;
+	$return        = false;
+	$activity_id   = $item_id;
+	$user_id       = $secondary_item_id;
+	$user_fullname = bp_core_get_user_displayname( $user_id );
+
+	switch ( $action ) {
+		case 'new_at_mention':
+			$action_filter = 'at_mentions';
+			$link          = bp_loggedin_user_domain() . bp_get_activity_slug() . '/mentions/';
+			$title         = sprintf( __( '@%s Mentions', 'buddypress' ), bp_get_loggedin_user_username() );
+			$amount        = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$text   = sprintf( __( 'You have %1$d new mentions', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+			} else {
+				$text = sprintf( __( '%1$s mentioned you', 'buddypress' ), $user_fullname );
+			}
+		break;
+
+		case 'update_reply':
+			$link   = bp_get_notifications_permalink();
+			$title  = __( 'New Activity reply', 'buddypress' );
+			$amount = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$link   = add_query_arg( 'type', $action, $link );
+				$text   = sprintf( __( 'You have %1$d new replies', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+			} else {
+				$link = add_query_arg( 'rid', (int) $id, bp_activity_get_permalink( $activity_id ) );
+				$text = sprintf( __( '%1$s commented on one of your updates', 'buddypress' ), $user_fullname );
+			}
+		break;
+
+		case 'comment_reply':
+			$link   = bp_get_notifications_permalink();
+			$title  = __( 'New Activity comment reply', 'buddypress' );
+			$amount = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$link   = add_query_arg( 'type', $action, $link );
+				$text   = sprintf( __( 'You have %1$d new comment replies', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+			} else {
+				$link = add_query_arg( 'crid', (int) $id, bp_activity_get_permalink( $activity_id ) );
+				$text = sprintf( __( '%1$s replied to one of your activity comments', 'buddypress' ), $user_fullname );
+			}
+		break;
+	}
+
+	if ( 'string' == $format ) {
+
+		/**
+		 * Filters the activity notification for the string format.
+		 *
+		 * This is a variable filter that is dependent on how many items
+		 * need notified about. The two possible hooks are bp_activity_single_at_mentions_notification
+		 * or bp_activity_multiple_at_mentions_notification.
+		 *
+		 * @since 1.5.0
+		 * @since 2.6.0 use the $action_filter as a new dynamic portion of the filter name.
+		 *
+		 * @param string $string          HTML anchor tag for the interaction.
+		 * @param string $link            The permalink for the interaction.
+		 * @param int    $total_items     How many items being notified about.
+		 * @param int    $activity_id     ID of the activity item being formatted.
+		 * @param int    $user_id         ID of the user who inited the interaction.
+		 */
+		$return = apply_filters( 'bp_activity_' . $amount . '_' . $action_filter . '_notification', '<a href="' . esc_url( $link ) . '">' . esc_html( $text ) . '</a>', $link, (int) $total_items, $activity_id, $user_id );
+	} else {
+
+		/**
+		 * Filters the activity notification for any non-string format.
+		 *
+		 * This is a variable filter that is dependent on how many items need notified about.
+		 * The two possible hooks are bp_activity_single_at_mentions_notification
+		 * or bp_activity_multiple_at_mentions_notification.
+		 *
+		 * @since 1.5.0
+		 * @since 2.6.0 use the $action_filter as a new dynamic portion of the filter name.
+		 *
+		 * @param array  $array           Array holding the content and permalink for the interaction notification.
+		 * @param string $link            The permalink for the interaction.
+		 * @param int    $total_items     How many items being notified about.
+		 * @param int    $activity_id     ID of the activity item being formatted.
+		 * @param int    $user_id         ID of the user who inited the interaction.
+		 */
+		$return = apply_filters( 'bp_activity_' . $amount . '_' . $action_filter . '_notification', array(
+			'text' => $text,
+			'link' => $link
+		), $link, (int) $total_items, $activity_id, $user_id );
+	}
+
+	/**
+	 * Fires right before returning the formatted activity notifications.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $action            The type of activity item.
+	 * @param int    $item_id           The activity ID.
+	 * @param int    $secondary_item_id The user ID who inited the interaction.
+	 * @param int    $total_items       Total amount of items to format.
+	 */
+	do_action( 'activity_format_notifications', $action, $item_id, $secondary_item_id, $total_items );
+
+	return $return;
+}
+
+/**
+ * Notify a member when their nicename is mentioned in an activity stream item.
+ *
+ * Hooked to the 'bp_activity_sent_mention_email' action, we piggy back off the
+ * existing email code for now, since it does the heavy lifting for us. In the
+ * future when we separate emails from Notifications, this will need its own
+ * 'bp_activity_at_name_send_emails' equivalent helper function.
+ *
+ * @since 1.9.0
+ *
+ * @param object $activity           Activity object.
+ * @param string $subject (not used) Notification subject.
+ * @param string $message (not used) Notification message.
+ * @param string $content (not used) Notification content.
+ * @param int    $receiver_user_id   ID of user receiving notification.
+ */
+function bp_activity_at_mention_add_notification( $activity, $subject, $message, $content, $receiver_user_id ) {
+	bp_notifications_add_notification( array(
+			'user_id'           => $receiver_user_id,
+			'item_id'           => $activity->id,
+			'secondary_item_id' => $activity->user_id,
+			'component_name'    => buddypress()->activity->id,
+			'component_action'  => 'new_at_mention',
+			'date_notified'     => bp_core_current_time(),
+			'is_new'            => 1,
+	) );
+}
+add_action( 'bp_activity_sent_mention_email', 'bp_activity_at_mention_add_notification', 10, 5 );
+
+/**
+ * Notify a member one of their activity received a reply.
+ *
+ * @since 2.6.0
+ *
+ * @param BP_Activity_Activity $activity     The original activity.
+ * @param int                  $comment_id   ID for the newly received comment.
+ * @param int                  $commenter_id ID of the user who made the comment.
+ */
+function bp_activity_update_reply_add_notification( $activity, $comment_id, $commenter_id ) {
+	bp_notifications_add_notification( array(
+		'user_id'           => $activity->user_id,
+		'item_id'           => $comment_id,
+		'secondary_item_id' => $commenter_id,
+		'component_name'    => buddypress()->activity->id,
+		'component_action'  => 'update_reply',
+		'date_notified'     => bp_core_current_time(),
+		'is_new'            => 1,
+	) );
+}
+add_action( 'bp_activity_sent_reply_to_update_notification', 'bp_activity_update_reply_add_notification', 10, 3 );
+
+/**
+ * Notify a member one of their activity comment received a reply.
+ *
+ * @since 2.6.0
+ *
+ * @param BP_Activity_Activity $activity_comment The parent activity.
+ * @param int                  $comment_id       ID for the newly received comment.
+ * @param int                  $commenter_id     ID of the user who made the comment.
+ */
+function bp_activity_comment_reply_add_notification( $activity_comment, $comment_id, $commenter_id ) {
+	bp_notifications_add_notification( array(
+		'user_id'           => $activity_comment->user_id,
+		'item_id'           => $comment_id,
+		'secondary_item_id' => $commenter_id,
+		'component_name'    => buddypress()->activity->id,
+		'component_action'  => 'comment_reply',
+		'date_notified'     => bp_core_current_time(),
+		'is_new'            => 1,
+	) );
+}
+add_action( 'bp_activity_sent_reply_to_reply_notification', 'bp_activity_comment_reply_add_notification', 10, 3 );
+
+/**
+ * Mark at-mention notifications as read when users visit their Mentions page.
+ *
+ * @since 1.5.0
+ * @since 2.5.0 Add the $user_id parameter
+ *
+ * @param int $user_id The id of the user whose notifications are marked as read.
+ */
+function bp_activity_remove_screen_notifications( $user_id = 0 ) {
+	// Only mark read if the current user is looking at his own mentions.
+	if ( empty( $user_id ) || (int) $user_id !== (int) bp_loggedin_user_id() ) {
+		return;
+	}
+
+	bp_notifications_mark_notifications_by_type( $user_id, buddypress()->activity->id, 'new_at_mention' );
+}
+add_action( 'bp_activity_clear_new_mentions', 'bp_activity_remove_screen_notifications', 10, 1 );
+
+/**
+ * Mark notifications as read when a user visits an activity permalink.
+ *
+ * @since 2.0.0
+ * @since 3.2.0 Marks replies to parent update and replies to an activity comment as read.
+ *
+ * @param BP_Activity_Activity $activity Activity object.
+ */
+function bp_activity_remove_screen_notifications_single_activity_permalink( $activity ) {
+	if ( ! is_user_logged_in() ) {
+		return;
+	}
+
+	// Mark as read any notifications for the current user related to this activity item.
+	bp_notifications_mark_notifications_by_item_id( bp_loggedin_user_id(), $activity->id, buddypress()->activity->id, 'new_at_mention' );
+
+	$comment_id = 0;
+	// For replies to a parent update.
+	if ( ! empty( $_GET['rid'] ) ) {
+		$comment_id = (int) $_GET['rid'];
+
+	// For replies to an activity comment.
+	} elseif ( ! empty( $_GET['crid'] ) ) {
+		$comment_id = (int) $_GET['crid'];
+	}
+
+	// Mark individual activity reply notification as read.
+	if ( ! empty( $comment_id ) ) {
+		BP_Notifications_Notification::update(
+			array(
+				'is_new' => false
+			),
+			array(
+				'user_id' => bp_loggedin_user_id(),
+				'id'      => $comment_id
+			)
+		);
+	}
+}
+add_action( 'bp_activity_screen_single_activity_permalink', 'bp_activity_remove_screen_notifications_single_activity_permalink' );
+
+/**
+ * Mark non-mention notifications as read when user visits our read permalink.
+ *
+ * In particular, 'update_reply' and 'comment_reply' notifications are handled
+ * here. See {@link bp_activity_format_notifications()} for more info.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_remove_screen_notifications_for_non_mentions() {
+	if ( false === is_singular() || false === is_user_logged_in() || empty( $_GET['nid'] ) ) {
+		return;
+	}
+
+	// Mark notification as read.
+	BP_Notifications_Notification::update(
+		array(
+			'is_new'  => false
+		),
+		array(
+			'user_id' => bp_loggedin_user_id(),
+			'id'      => (int) $_GET['nid']
+		)
+	);
+}
+add_action( 'bp_screens', 'bp_activity_remove_screen_notifications_for_non_mentions' );
+
+/**
+ * Delete at-mention notifications when the corresponding activity item is deleted.
+ *
+ * @since 2.0.0
+ *
+ * @param array $activity_ids_deleted IDs of deleted activity items.
+ */
+function bp_activity_at_mention_delete_notification( $activity_ids_deleted = array() ) {
+	// Let's delete all without checking if content contains any mentions
+	// to avoid a query to get the activity.
+	if ( ! empty( $activity_ids_deleted ) ) {
+		foreach ( $activity_ids_deleted as $activity_id ) {
+			bp_notifications_delete_all_notifications_by_type( $activity_id, buddypress()->activity->id );
+		}
+	}
+}
+add_action( 'bp_activity_deleted_activities', 'bp_activity_at_mention_delete_notification', 10 );
+
+/**
+ * Add a notification for post comments to the post author or post commenter.
+ *
+ * Requires "activity stream commenting on posts and comments" to be enabled.
+ *
+ * @since 2.6.0
+ *
+ * @param int        $activity_id          The activity comment ID.
+ * @param WP_Comment $post_type_comment    WP Comment object.
+ * @param array      $activity_args        Activity comment arguments.
+ * @param object     $activity_post_object The post type tracking args object.
+ */
+function bp_activity_add_notification_for_synced_blog_comment( $activity_id, $post_type_comment, $activity_args, $activity_post_object ) {
+	// If activity comments are disabled for WP posts, stop now!
+	if ( bp_disable_blogforum_comments() || empty( $activity_id ) ) {
+		return;
+	}
+
+	// Send a notification to the blog post author.
+	if ( (int) $post_type_comment->post->post_author !== (int) $activity_args['user_id'] ) {
+		// Only add a notification if comment author is a registered user.
+		// @todo Should we remove this restriction?
+		if ( ! empty( $post_type_comment->user_id ) ) {
+			bp_notifications_add_notification( array(
+				'user_id'           => $post_type_comment->post->post_author,
+				'item_id'           => $activity_id,
+				'secondary_item_id' => $post_type_comment->user_id,
+				'component_name'    => buddypress()->activity->id,
+				'component_action'  => 'update_reply',
+				'date_notified'     => $post_type_comment->comment_date_gmt,
+				'is_new'            => 1,
+			) );
+		}
+	}
+
+	// Send a notification to the parent comment author for follow-up comments.
+	if ( ! empty( $post_type_comment->comment_parent ) ) {
+		$parent_comment = get_comment( $post_type_comment->comment_parent );
+
+		if ( ! empty( $parent_comment->user_id ) && (int) $parent_comment->user_id !== (int) $activity_args['user_id'] ) {
+			bp_notifications_add_notification( array(
+				'user_id'           => $parent_comment->user_id,
+				'item_id'           => $activity_id,
+				'secondary_item_id' => $post_type_comment->user_id,
+				'component_name'    => buddypress()->activity->id,
+				'component_action'  => 'comment_reply',
+				'date_notified'     => $post_type_comment->comment_date_gmt,
+				'is_new'            => 1,
+			) );
+		}
+	}
+}
+add_action( 'bp_blogs_comment_sync_activity_comment', 'bp_activity_add_notification_for_synced_blog_comment', 10, 4 );
+
+/**
+ * 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
+}
+add_action( 'bp_notification_settings', 'bp_activity_screen_notification_settings', 1 );
Index: /tags/4.0.0-beta1/src/bp-activity/bp-activity-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/bp-activity-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/bp-activity-template.php	(revision 12265)
@@ -0,0 +1,3883 @@
+<?php
+/**
+ * BuddyPress Activity Template Functions.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the activity component slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_slug() {
+	echo bp_get_activity_slug();
+}
+	/**
+	 * Return the activity component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string The activity component slug.
+	 */
+	function bp_get_activity_slug() {
+
+		/**
+		 * Filters the activity component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Activity component slug.
+		 */
+		return apply_filters( 'bp_get_activity_slug', buddypress()->activity->slug );
+	}
+
+/**
+ * Output the activity component root slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_root_slug() {
+	echo bp_get_activity_root_slug();
+}
+	/**
+	 * Return the activity component root slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string The activity component root slug.
+	 */
+	function bp_get_activity_root_slug() {
+
+		/**
+		 * Filters the activity component root slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $root_slug Activity component root slug.
+		 */
+		return apply_filters( 'bp_get_activity_root_slug', buddypress()->activity->root_slug );
+	}
+
+/**
+ * Output activity directory permalink.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_directory_permalink() {
+	echo esc_url( bp_get_activity_directory_permalink() );
+}
+	/**
+	 * Return activity directory permalink.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string Activity directory permalink.
+	 */
+	function bp_get_activity_directory_permalink() {
+
+		/**
+		 * Filters the activity directory permalink.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $url Permalink url for the activity directory.
+		 */
+		return apply_filters( 'bp_get_activity_directory_permalink', trailingslashit( bp_get_root_domain() . '/' . bp_get_activity_root_slug() ) );
+	}
+
+/**
+ * Initialize the activity loop.
+ *
+ * Based on the $args passed, bp_has_activities() populates the
+ * $activities_template global, enabling the use of BuddyPress templates and
+ * template functions to display a list of activity items.
+ *
+ * @since 1.0.0
+ * @since 2.4.0 Introduced the `$fields` parameter.
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @param array|string $args {
+ *     Arguments for limiting the contents of the activity loop. Most arguments
+ *     are in the same format as {@link BP_Activity_Activity::get()}. However,
+ *     because the format of the arguments accepted here differs in a number of
+ *     ways, and because bp_has_activities() determines some default arguments in
+ *     a dynamic fashion, we list all accepted arguments here as well.
+ *
+ *     Arguments can be passed as an associative array, or as a URL querystring
+ *     (eg, 'user_id=4&display_comments=threaded').
+ *
+ *     @type int               $page             Which page of results to fetch. Using page=1 without per_page will result
+ *                                               in no pagination. Default: 1.
+ *     @type int|bool          $per_page         Number of results per page. Default: 20.
+ *     @type string            $page_arg         String used as a query parameter in pagination links. Default: 'acpage'.
+ *     @type int|bool          $max              Maximum number of results to return. Default: false (unlimited).
+ *     @type string            $fields           Activity fields to retrieve. 'all' to fetch entire activity objects,
+ *                                               'ids' to get only the activity IDs. Default 'all'.
+ *     @type string|bool       $count_total      If true, an additional DB query is run to count the total activity items
+ *                                               for the query. Default: false.
+ *     @type string            $sort             'ASC' or 'DESC'. Default: 'DESC'.
+ *     @type array|bool        $exclude          Array of activity IDs to exclude. Default: false.
+ *     @type array|bool        $in               Array of IDs to limit query by (IN). 'in' is intended to be used in
+ *                                               conjunction with other filter parameters. Default: false.
+ *     @type array|bool        $include          Array of exact activity IDs to query. Providing an 'include' array will
+ *                                               override all other filters passed in the argument array. When viewing the
+ *                                               permalink page for a single activity item, this value defaults to the ID of
+ *                                               that item. Otherwise the default is false.
+ *     @type array             $meta_query       Limit by activitymeta by passing an array of meta_query conditions. See
+ *                                               {@link WP_Meta_Query::queries} for a description of the syntax.
+ *     @type array             $date_query       Limit by date by passing an array of date_query conditions. See first
+ *                                               parameter of {@link WP_Date_Query::__construct()} for syntax.
+ *     @type array             $filter_query     Advanced activity filtering.  See {@link BP_Activity_Query::__construct()}.
+ *     @type string            $search_terms     Limit results by a search term. Default: false.
+ *     @type string            $scope            Use a BuddyPress pre-built filter.
+ *                                                 - 'just-me' retrieves items belonging only to a user; this is equivalent
+ *                                                   to passing a 'user_id' argument.
+ *                                                 - 'friends' retrieves items belonging to the friends of a user.
+ *                                                 - 'groups' retrieves items belonging to groups to which a user belongs to.
+ *                                                 - 'favorites' retrieves a user's favorited activity items.
+ *                                                 - 'mentions' retrieves items where a user has received an @-mention.
+ *                                               The default value of 'scope' is set to one of the above if that value
+ *                                               appears in the appropriate place in the URL; eg, 'scope' will be 'groups'
+ *                                               when visiting http://example.com/members/joe/activity/groups/. Otherwise
+ *                                               defaults to false.
+ *     @type int|array|bool    $user_id          The ID(s) of user(s) whose activity should be fetched. Pass a single ID or
+ *                                               an array of IDs. When viewing a user profile page (but not that user's
+ *                                               activity subpages, ie My Friends, My Groups, etc), 'user_id' defaults to
+ *                                               the ID of the displayed user. Otherwise the default is false.
+ *     @type string|array|bool $object           Filters by the `component` column in the database, which is generally the
+ *                                               component ID in the case of BuddyPress components, or the plugin slug in
+ *                                               the case of plugins. For example, 'groups' will limit results to those that
+ *                                               are associated with the BP Groups component. Accepts a single component
+ *                                               string, or an array of multiple components. Defaults to 'groups' when
+ *                                               viewing the page of a single group, the My Groups activity filter, or the
+ *                                               Activity > Groups filter of a user profile. Otherwise defaults to false.
+ *     @type string|array|bool $action           Filters by the `type` column in the database, which is a string
+ *                                               categorizing the activity item (eg, 'new_blog_post', 'created_group').
+ *                                               Accepts a comma-delimited string or an array of types. Default: false.
+ *     @type int|array|bool    $primary_id       Filters by the `item_id` column in the database. The meaning of
+ *                                               'primary_id' differs between components/types; for example, in the case of
+ *                                               'created_group', 'primary_id' is the ID of the group. Accepts a single ID,
+ *                                               or an array of multiple IDs. When viewing a single group, defaults to the
+ *                                               current group ID. When viewing a user's Groups stream page, defaults to the
+ *                                               IDs of the user's groups. Otherwise defaults to false.
+ *     @type int|array|bool    $secondary_id     Filters by the `secondary_item_id` column in the database. The meaning of
+ *                                               'secondary_id' differs between components/types. Accepts a single ID, or an
+ *                                               array of multiple IDs. Defaults to false.
+ *     @type int               $offset           Return only activity items with an ID greater than or equal to this one.
+ *                                               Note that providing an offset will disable pagination. Default: false.
+ *     @type string|bool       $display_comments How to handle activity comments. Possible values:
+ *                                                 - 'threaded' - comments appear in a threaded tree, under their parent
+ *                                                   items.
+ *                                                 - 'stream' - the activity stream is presented in a flat manner, with
+ *                                                   comments sorted in chronological order alongside other activity items.
+ *                                                 - false - don't fetch activity comments at all.
+ *                                               Default: 'threaded'.
+ *     @type bool              $show_hidden      Whether to show items marked hide_sitewide. Defaults to false, except in
+ *                                               the following cases:
+ *                                                 - User is viewing his own activity stream.
+ *                                                 - User is viewing the activity stream of a non-public group of which he
+ *                                                   is a member.
+ *     @type string|bool       $spam             Spam status. 'ham_only', 'spam_only', or false to show all activity
+ *                                               regardless of spam status. Default: 'ham_only'.
+ *     @type bool              $populate_extras  Whether to pre-fetch the activity metadata for the queried items.
+ *                                               Default: true.
+ * }
+ * @return bool Returns true when activities are found, otherwise false.
+ */
+function bp_has_activities( $args = '' ) {
+	global $activities_template;
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	/*
+	 * Smart Defaults.
+	 */
+
+	// User filtering.
+	$user_id = bp_displayed_user_id()
+		? bp_displayed_user_id()
+		: false;
+
+	// Group filtering.
+	if ( bp_is_group() ) {
+		$object      = $bp->groups->id;
+		$primary_id  = bp_get_current_group_id();
+		$show_hidden = (bool) ( groups_is_user_member( bp_loggedin_user_id(), $primary_id ) || bp_current_user_can( 'bp_moderate' ) );
+	} else {
+		$object      = false;
+		$primary_id  = false;
+		$show_hidden = false;
+	}
+
+	// The default scope should recognize custom slugs.
+	$scope = array_key_exists( bp_current_action(), (array) $bp->loaded_components )
+		? $bp->loaded_components[ bp_current_action() ]
+		: bp_current_action();
+
+	// Support for permalinks on single item pages: /groups/my-group/activity/124/.
+	$include = bp_is_current_action( bp_get_activity_slug() )
+		? bp_action_variable( 0 )
+		: false;
+
+	$search_terms_default = false;
+	$search_query_arg = bp_core_get_component_search_query_arg( 'activity' );
+	if ( ! empty( $_REQUEST[ $search_query_arg ] ) ) {
+		$search_terms_default = stripslashes( $_REQUEST[ $search_query_arg ] );
+	}
+
+	/*
+	 * Parse Args.
+	 */
+
+	// Note: any params used for filtering can be a single value, or multiple
+	// values comma separated.
+	$r = bp_parse_args( $args, array(
+		'display_comments'  => 'threaded',   // False for none, stream/threaded - show comments in the stream or threaded under items.
+		'include'           => $include,     // Pass an activity_id or string of IDs comma-separated.
+		'exclude'           => false,        // Pass an activity_id or string of IDs comma-separated.
+		'in'                => false,        // Comma-separated list or array of activity IDs among which to search.
+		'sort'              => 'DESC',       // Sort DESC or ASC.
+		'page'              => 1,            // Which page to load.
+		'per_page'          => 20,           // Number of items per page.
+		'page_arg'          => 'acpage',     // See https://buddypress.trac.wordpress.org/ticket/3679.
+		'max'               => false,        // Max number to return.
+		'fields'            => 'all',
+		'count_total'       => false,
+		'show_hidden'       => $show_hidden, // Show activity items that are hidden site-wide?
+		'spam'              => 'ham_only',   // Hide spammed items.
+
+		// Scope - pre-built activity filters for a user (friends/groups/favorites/mentions).
+		'scope'             => $scope,
+
+		// Filtering
+		'user_id'           => $user_id,     // user_id to filter on.
+		'object'            => $object,      // Object to filter on e.g. groups, profile, status, friends.
+		'action'            => false,        // Action to filter on e.g. activity_update, profile_updated.
+		'primary_id'        => $primary_id,  // Object ID to filter on e.g. a group_id or blog_id etc.
+		'secondary_id'      => false,        // Secondary object ID to filter on e.g. a post_id.
+		'offset'            => false,        // Return only items >= this ID.
+		'since'             => false,        // Return only items recorded since this Y-m-d H:i:s date.
+
+		'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.  See BP_Activity_Query for format.
+
+		// Searching.
+		'search_terms'      => $search_terms_default,
+		'update_meta_cache' => true,
+	), 'has_activities' );
+
+	/*
+	 * Smart Overrides.
+	 */
+
+	// Translate various values for 'display_comments'
+	// This allows disabling comments via ?display_comments=0
+	// or =none or =false. Final true is a strict type check. See #5029.
+	if ( in_array( $r['display_comments'], array( 0, '0', 'none', 'false' ), true ) ) {
+		$r['display_comments'] = false;
+	}
+
+	// Ignore pagination if an offset is passed.
+	if ( ! empty( $r['offset'] ) ) {
+		$r['page'] = 0;
+	}
+
+	// Search terms.
+	if ( ! empty( $_REQUEST['s'] ) && empty( $r['search_terms'] ) ) {
+		$r['search_terms'] = $_REQUEST['s'];
+	}
+
+	// Do not exceed the maximum per page.
+	if ( ! empty( $r['max'] ) && ( (int) $r['per_page'] > (int) $r['max'] ) ) {
+		$r['per_page'] = $r['max'];
+	}
+
+	/**
+	 * Filters whether BuddyPress should enable afilter support.
+	 *
+	 * 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.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value True if BuddyPress should enable afilter support.
+	 */
+	if ( isset( $_GET['afilter'] ) && apply_filters( 'bp_activity_enable_afilter_support', false ) ) {
+		$r['filter'] = array(
+			'object' => $_GET['afilter']
+		);
+	} elseif ( ! empty( $r['user_id'] ) || ! empty( $r['object'] ) || ! empty( $r['action'] ) || ! empty( $r['primary_id'] ) || ! empty( $r['secondary_id'] ) || ! empty( $r['offset'] ) || ! empty( $r['since'] ) ) {
+		$r['filter'] = array(
+			'user_id'      => $r['user_id'],
+			'object'       => $r['object'],
+			'action'       => $r['action'],
+			'primary_id'   => $r['primary_id'],
+			'secondary_id' => $r['secondary_id'],
+			'offset'       => $r['offset'],
+			'since'        => $r['since']
+		);
+	} else {
+		$r['filter'] = false;
+	}
+
+	// If specific activity items have been requested, override the $hide_spam
+	// argument. This prevents backpat errors with AJAX.
+	if ( ! empty( $r['include'] ) && ( 'ham_only' === $r['spam'] ) ) {
+		$r['spam'] = 'all';
+	}
+
+	/*
+	 * Query
+	 */
+
+	$activities_template = new BP_Activity_Template( $r );
+
+	/**
+	 * Filters whether or not there are activity items to display.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool   $value               Whether or not there are activity items to display.
+	 * @param string $activities_template Current activities template being used.
+	 * @param array  $r                   Array of arguments passed into the BP_Activity_Template class.
+	 */
+	return apply_filters( 'bp_has_activities', $activities_template->has_activities(), $activities_template, $r );
+}
+
+/**
+ * Determine if there are still activities left in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @return bool Returns true when activities are found.
+ */
+function bp_activities() {
+	global $activities_template;
+	return $activities_template->user_activities();
+}
+
+/**
+ * Get the current activity object in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @return object The current activity within the loop.
+ */
+function bp_the_activity() {
+	global $activities_template;
+	return $activities_template->the_activity();
+}
+
+/**
+ * Output the URL for the Load More link.
+ *
+ * @since 2.1.0
+ */
+function bp_activity_load_more_link() {
+	echo esc_url( bp_get_activity_load_more_link() );
+}
+	/**
+	 * Get the URL for the Load More link.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return string $link
+	 */
+	function bp_get_activity_load_more_link() {
+		global $activities_template;
+
+		$url  = bp_get_requested_url();
+		$link = add_query_arg( $activities_template->pag_arg, $activities_template->pag_page + 1, $url );
+
+		/**
+		 * Filters the Load More link URL.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $link                The "Load More" link URL with appropriate query args.
+		 * @param string $url                 The original URL.
+		 * @param object $activities_template The activity template loop global.
+		 */
+		return apply_filters( 'bp_get_activity_load_more_link', $link, $url, $activities_template );
+	}
+
+/**
+ * Output the activity pagination count.
+ *
+ * @since 1.0.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ */
+function bp_activity_pagination_count() {
+	echo bp_get_activity_pagination_count();
+}
+
+	/**
+	 * Return the activity pagination count.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The pagination text.
+	 */
+	function bp_get_activity_pagination_count() {
+		global $activities_template;
+
+		$start_num = intval( ( $activities_template->pag_page - 1 ) * $activities_template->pag_num ) + 1;
+		$from_num  = bp_core_number_format( $start_num );
+		$to_num    = bp_core_number_format( ( $start_num + ( $activities_template->pag_num - 1 ) > $activities_template->total_activity_count ) ? $activities_template->total_activity_count : $start_num + ( $activities_template->pag_num - 1 ) );
+		$total     = bp_core_number_format( $activities_template->total_activity_count );
+
+		if ( 1 == $activities_template->total_activity_count ) {
+			$message = __( 'Viewing 1 item', 'buddypress' );
+		} else {
+			$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s item', 'Viewing %1$s - %2$s of %3$s items', $activities_template->total_activity_count, 'buddypress' ), $from_num, $to_num, $total );
+		}
+
+		return $message;
+	}
+
+/**
+ * Output the activity pagination links.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_pagination_links() {
+	echo bp_get_activity_pagination_links();
+}
+
+	/**
+	 * Return the activity pagination links.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The pagination links.
+	 */
+	function bp_get_activity_pagination_links() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity pagination link output.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $pag_links Output for the activity pagination links.
+		 */
+		return apply_filters( 'bp_get_activity_pagination_links', $activities_template->pag_links );
+	}
+
+/**
+ * Return true when there are more activity items to be shown than currently appear.
+ *
+ * @since 1.5.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @return bool $has_more_items True if more items, false if not.
+ */
+function bp_activity_has_more_items() {
+	global $activities_template;
+
+	if ( ! empty( $activities_template->has_more_items )  ) {
+		$has_more_items = true;
+	} else {
+		$remaining_pages = 0;
+
+		if ( ! empty( $activities_template->pag_page ) ) {
+			$remaining_pages = floor( ( $activities_template->total_activity_count - 1 ) / ( $activities_template->pag_num * $activities_template->pag_page ) );
+		}
+
+		$has_more_items = (int) $remaining_pages > 0;
+	}
+
+	/**
+	 * Filters whether there are more activity items to display.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $has_more_items Whether or not there are more activity items to display.
+	 */
+	return apply_filters( 'bp_activity_has_more_items', $has_more_items );
+}
+
+/**
+ * Output the activity count.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_count() {
+	echo bp_get_activity_count();
+}
+
+	/**
+	 * Return the activity count.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int The activity count.
+	 */
+	function bp_get_activity_count() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity count for the activity template.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $activity_count The count for total activity.
+		 */
+		return apply_filters( 'bp_get_activity_count', (int) $activities_template->activity_count );
+	}
+
+/**
+ * Output the number of activities per page.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_per_page() {
+	echo bp_get_activity_per_page();
+}
+
+	/**
+	 * Return the number of activities per page.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int The activities per page.
+	 */
+	function bp_get_activity_per_page() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity posts per page value.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $pag_num How many post should be displayed for pagination.
+		 */
+		return apply_filters( 'bp_get_activity_per_page', (int) $activities_template->pag_num );
+	}
+
+/**
+ * Output the activities title.
+ *
+ * @since 1.0.0
+ *
+ * @todo Deprecate.
+ */
+function bp_activities_title() {
+	echo bp_get_activities_title();
+}
+
+	/**
+	 * Return the activities title.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global string $bp_activity_title
+	 * @todo Deprecate.
+	 *
+	 * @return string The activities title.
+	 */
+	function bp_get_activities_title() {
+		global $bp_activity_title;
+
+		/**
+		 * Filters the activities title for the activity template.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $bp_activity_title The title to be displayed.
+		 */
+		return apply_filters( 'bp_get_activities_title', $bp_activity_title );
+	}
+
+/**
+ * {@internal Missing Description}
+ *
+ * @since 1.0.0
+ *
+ * @todo Deprecate.
+ */
+function bp_activities_no_activity() {
+	echo bp_get_activities_no_activity();
+}
+
+	/**
+	 * {@internal Missing Description}
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global string $bp_activity_no_activity
+	 * @todo Deprecate.
+	 *
+	 * @return string
+	 */
+	function bp_get_activities_no_activity() {
+		global $bp_activity_no_activity;
+
+		/**
+		 * Filters the text used when there is no activity to display.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $bp_activity_no_activity Text to display for no activity.
+		 */
+		return apply_filters( 'bp_get_activities_no_activity', $bp_activity_no_activity );
+	}
+
+/**
+ * Output the activity ID.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_id() {
+	echo bp_get_activity_id();
+}
+
+	/**
+	 * Return the activity ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int The activity ID.
+	 */
+	function bp_get_activity_id() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity ID being displayed.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $id The activity ID.
+		 */
+		return apply_filters( 'bp_get_activity_id', $activities_template->activity->id );
+	}
+
+/**
+ * Output the activity item ID.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_item_id() {
+	echo bp_get_activity_item_id();
+}
+
+	/**
+	 * Return the activity item ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int The activity item ID.
+	 */
+	function bp_get_activity_item_id() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity item ID being displayed.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $item_id The activity item ID.
+		 */
+		return apply_filters( 'bp_get_activity_item_id', $activities_template->activity->item_id );
+	}
+
+/**
+ * Output the activity secondary item ID.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_secondary_item_id() {
+	echo bp_get_activity_secondary_item_id();
+}
+
+	/**
+	 * Return the activity secondary item ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int The activity secondary item ID.
+	 */
+	function bp_get_activity_secondary_item_id() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity secondary item ID being displayed.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $secondary_item_id The activity secondary item ID.
+		 */
+		return apply_filters( 'bp_get_activity_secondary_item_id', $activities_template->activity->secondary_item_id );
+	}
+
+/**
+ * Output the date the activity was recorded.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_date_recorded() {
+	echo bp_get_activity_date_recorded();
+}
+
+	/**
+	 * Return the date the activity was recorded.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The date the activity was recorded.
+	 */
+	function bp_get_activity_date_recorded() {
+		global $activities_template;
+
+		/**
+		 * Filters the date the activity was recorded.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $date_recorded The activity's date.
+		 */
+		return apply_filters( 'bp_get_activity_date_recorded', $activities_template->activity->date_recorded );
+	}
+
+/**
+ * Output the display name of the member who posted the activity.
+ *
+ * @since 2.1.0
+ *
+ */
+function bp_activity_member_display_name() {
+	echo bp_get_activity_member_display_name();
+}
+
+	/**
+	 * Return the display name of the member who posted the activity.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The date the activity was recorded.
+	 */
+	function bp_get_activity_member_display_name() {
+		global $activities_template;
+
+		$retval = isset( $activities_template->activity->display_name )
+			? $activities_template->activity->display_name
+			: '';
+
+		/**
+		 * Filters the display name of the member who posted the activity.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param int $retval Display name for the member who posted.
+		 */
+		return apply_filters( 'bp_get_activity_member_display_name', $retval );
+	}
+
+/**
+ * Output the activity object name.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_object_name() {
+	echo bp_get_activity_object_name();
+}
+
+	/**
+	 * Return the activity object name.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity object name.
+	 */
+	function bp_get_activity_object_name() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity object name.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $activity_component The activity object name.
+		 */
+		return apply_filters( 'bp_get_activity_object_name', $activities_template->activity->component );
+	}
+
+/**
+ * Output the activity type.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_type() {
+	echo bp_get_activity_type();
+}
+
+	/**
+	 * Return the activity type.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity type.
+	 */
+	function bp_get_activity_type() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity type.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $activity_type The activity type.
+		 */
+		return apply_filters( 'bp_get_activity_type', $activities_template->activity->type );
+	}
+
+	/**
+	 * Output the activity action name.
+	 *
+	 * Just a wrapper for bp_activity_type().
+	 *
+	 * @since 1.2.0
+	 * @deprecated 1.5.0
+	 *
+	 * @todo Properly deprecate in favor of bp_activity_type() and
+	 *       remove redundant echo
+	 *
+	 */
+	function bp_activity_action_name() { echo bp_activity_type(); }
+
+	/**
+	 * Return the activity type.
+	 *
+	 * Just a wrapper for bp_get_activity_type().
+	 *
+	 * @since 1.2.0
+	 * @deprecated 1.5.0
+	 *
+	 * @todo Properly deprecate in favor of bp_get_activity_type().
+	 *
+	 *
+	 * @return string The activity type.
+	 */
+	function bp_get_activity_action_name() { return bp_get_activity_type(); }
+
+/**
+ * Output the activity user ID.
+ *
+ * @since 1.1.0
+ *
+ */
+function bp_activity_user_id() {
+	echo bp_get_activity_user_id();
+}
+
+	/**
+	 * Return the activity user ID.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int The activity user ID.
+	 */
+	function bp_get_activity_user_id() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity user ID.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $user_id The activity user ID.
+		 */
+		return apply_filters( 'bp_get_activity_user_id', $activities_template->activity->user_id );
+	}
+
+/**
+ * Output the activity user link.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_user_link() {
+	echo bp_get_activity_user_link();
+}
+
+	/**
+	 * Return the activity user link.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string $link The activity user link.
+	 */
+	function bp_get_activity_user_link() {
+		global $activities_template;
+
+		if ( empty( $activities_template->activity->user_id ) || empty( $activities_template->activity->user_nicename ) || empty( $activities_template->activity->user_login ) ) {
+			$link = $activities_template->activity->primary_link;
+		} else {
+			$link = bp_core_get_user_domain( $activities_template->activity->user_id, $activities_template->activity->user_nicename, $activities_template->activity->user_login );
+		}
+
+		/**
+		 * Filters the activity user link.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $link The activity user link.
+		 */
+		return apply_filters( 'bp_get_activity_user_link', $link );
+	}
+
+/**
+ * Output the avatar of the user that performed the action.
+ *
+ * @since 1.1.0
+ *
+ * @see bp_get_activity_avatar() for description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_activity_avatar()} for description.
+ */
+function bp_activity_avatar( $args = '' ) {
+	echo bp_get_activity_avatar( $args );
+}
+	/**
+	 * Return the avatar of the user that performed the action.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @see bp_core_fetch_avatar() For a description of the arguments.
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @param array|string $args  {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see
+	 *     {@link bp_core_fetch_avatar()}.
+	 *     @type string      $alt     Default: 'Profile picture of [user name]' if
+	 *                                activity user name is available, otherwise 'Profile picture'.
+	 *     @type string      $class   Default: 'avatar'.
+	 *     @type string|bool $email   Default: Email of the activity's
+	 *                                associated user, if available. Otherwise false.
+	 *     @type string      $type    Default: 'full' when viewing a single activity
+	 *                                permalink page, otherwise 'thumb'.
+	 *     @type int|bool    $user_id Default: ID of the activity's user.
+	 * }
+	 * @return string User avatar string.
+	 */
+	function bp_get_activity_avatar( $args = '' ) {
+		global $activities_template;
+
+		$bp = buddypress();
+
+		// On activity permalink pages, default to the full-size avatar.
+		$type_default = bp_is_single_activity() ? 'full' : 'thumb';
+
+		// Within the activity comment loop, the current activity should be set
+		// to current_comment. Otherwise, just use activity.
+		$current_activity_item = isset( $activities_template->activity->current_comment ) ? $activities_template->activity->current_comment : $activities_template->activity;
+
+		// Activity user display name.
+		$dn_default  = isset( $current_activity_item->display_name ) ? $current_activity_item->display_name : '';
+
+		// Prepend some descriptive text to alt.
+		$alt_default = !empty( $dn_default ) ? sprintf( __( 'Profile picture of %s', 'buddypress' ), $dn_default ) : __( 'Profile picture', 'buddypress' );
+
+		$defaults = array(
+			'alt'     => $alt_default,
+			'class'   => 'avatar',
+			'email'   => false,
+			'type'    => $type_default,
+			'user_id' => false
+		);
+
+		$r = wp_parse_args( $args, $defaults );
+		extract( $r, EXTR_SKIP );
+
+		if ( !isset( $height ) && !isset( $width ) ) {
+
+			// Backpat.
+			if ( isset( $bp->avatar->full->height ) || isset( $bp->avatar->thumb->height ) ) {
+				$height = ( 'full' == $type ) ? $bp->avatar->full->height : $bp->avatar->thumb->height;
+			} else {
+				$height = 20;
+			}
+
+			// Backpat.
+			if ( isset( $bp->avatar->full->width ) || isset( $bp->avatar->thumb->width ) ) {
+				$width = ( 'full' == $type ) ? $bp->avatar->full->width : $bp->avatar->thumb->width;
+			} else {
+				$width = 20;
+			}
+		}
+
+		/**
+		 * Filters the activity avatar object based on current activity item component.
+		 *
+		 * This is a variable filter dependent on the component used.
+		 * Possible hooks are bp_get_activity_avatar_object_blog,
+		 * bp_get_activity_avatar_object_group, and bp_get_activity_avatar_object_user.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $component Component being displayed.
+		 */
+		$object  = apply_filters( 'bp_get_activity_avatar_object_' . $current_activity_item->component, 'user' );
+		$item_id = !empty( $user_id ) ? $user_id : $current_activity_item->user_id;
+
+		/**
+		 * Filters the activity avatar item ID.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param int $item_id Item ID for the activity avatar.
+		 */
+		$item_id = apply_filters( 'bp_get_activity_avatar_item_id', $item_id );
+
+		// If this is a user object pass the users' email address for Gravatar so we don't have to prefetch it.
+		if ( 'user' == $object && empty( $user_id ) && empty( $email ) && isset( $current_activity_item->user_email ) ) {
+			$email = $current_activity_item->user_email;
+		}
+
+		/**
+		 * Filters the value returned by bp_core_fetch_avatar.
+		 *
+		 * @since 1.1.3
+		 *
+		 * @param array $value HTML image element containing the activity avatar.
+		 */
+		return apply_filters( 'bp_get_activity_avatar', bp_core_fetch_avatar( array(
+			'item_id' => $item_id,
+			'object'  => $object,
+			'type'    => $type,
+			'alt'     => $alt,
+			'class'   => $class,
+			'width'   => $width,
+			'height'  => $height,
+			'email'   => $email
+		) ) );
+	}
+
+/**
+ * Output the avatar of the object that action was performed on.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_activity_secondary_avatar() for description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_activity_secondary_avatar} for description.
+ */
+function bp_activity_secondary_avatar( $args = '' ) {
+	echo bp_get_activity_secondary_avatar( $args );
+}
+
+	/**
+	 * Return the avatar of the object that action was performed on.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @see bp_core_fetch_avatar() for description of arguments.
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @param array|string $args  {
+	 *     For a complete description of arguments, see {@link bp_core_fetch_avatar()}.
+	 *     @type string      $alt     Default value varies based on current activity
+	 *                                item component.
+	 *     @type string      $type    Default: 'full' when viewing a single activity
+	 *                                permalink page, otherwise 'thumb'.
+	 *     @type string      $class   Default: 'avatar'.
+	 *     @type string|bool $email   Default: email of the activity's user.
+	 *     @type int|bool    $user_id Default: ID of the activity's user.
+	 * }
+	 * @return string The secondary avatar.
+	 */
+	function bp_get_activity_secondary_avatar( $args = '' ) {
+		global $activities_template;
+
+		$r = wp_parse_args( $args, array(
+			'alt'        => '',
+			'type'       => 'thumb',
+			'width'      => 20,
+			'height'     => 20,
+			'class'      => 'avatar',
+			'link_class' => '',
+			'linked'     => true,
+			'email'      => false
+		) );
+		extract( $r, EXTR_SKIP );
+
+		// Set item_id and object (default to user).
+		switch ( $activities_template->activity->component ) {
+			case 'groups' :
+				if ( bp_disable_group_avatar_uploads() ) {
+					return false;
+				}
+
+				$object  = 'group';
+				$item_id = $activities_template->activity->item_id;
+				$link    = '';
+				$name    = '';
+
+				// Only if groups is active.
+				if ( bp_is_active( 'groups' ) ) {
+					$group = groups_get_group( $item_id );
+					$link  = bp_get_group_permalink( $group );
+					$name  = $group->name;
+				}
+
+				if ( empty( $alt ) ) {
+					$alt = __( 'Group logo', 'buddypress' );
+
+					if ( ! empty( $name ) ) {
+						$alt = sprintf( __( 'Group logo of %s', 'buddypress' ), $name );
+					}
+				}
+
+				break;
+			case 'blogs' :
+				$object  = 'blog';
+				$item_id = $activities_template->activity->item_id;
+				$link    = home_url();
+
+				if ( empty( $alt ) ) {
+					$alt = sprintf( __( 'Profile picture of the author of the site %s', 'buddypress' ), get_blog_option( $item_id, 'blogname' ) );
+				}
+
+				break;
+			case 'friends' :
+				$object  = 'user';
+				$item_id = $activities_template->activity->secondary_item_id;
+				$link    = bp_core_get_userlink( $item_id, false, true );
+
+				if ( empty( $alt ) ) {
+					$alt = sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $activities_template->activity->secondary_item_id ) );
+				}
+
+				break;
+			default :
+				$object  = 'user';
+				$item_id = $activities_template->activity->user_id;
+				$email   = $activities_template->activity->user_email;
+				$link    = bp_core_get_userlink( $item_id, false, true );
+
+				if ( empty( $alt ) ) {
+					$alt = sprintf( __( 'Profile picture of %s', 'buddypress' ), $activities_template->activity->display_name );
+				}
+
+				break;
+		}
+
+		/**
+		 * Filters the activity secondary avatar object based on current activity item component.
+		 *
+		 * This is a variable filter dependent on the component used. Possible hooks are
+		 * bp_get_activity_secondary_avatar_object_blog, bp_get_activity_secondary_avatar_object_group,
+		 * and bp_get_activity_secondary_avatar_object_user.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param string $object Component being displayed.
+		 */
+		$object  = apply_filters( 'bp_get_activity_secondary_avatar_object_' . $activities_template->activity->component, $object );
+
+		/**
+		 * Filters the activity secondary avatar item ID.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param int $item_id ID for the secondary avatar item.
+		 */
+		$item_id = apply_filters( 'bp_get_activity_secondary_avatar_item_id', $item_id );
+
+		// If we have no item_id or object, there is no avatar to display.
+		if ( empty( $item_id ) || empty( $object ) ) {
+			return false;
+		}
+
+		// Get the avatar.
+		$avatar = bp_core_fetch_avatar( array(
+			'item_id' => $item_id,
+			'object'  => $object,
+			'type'    => $type,
+			'alt'     => $alt,
+			'class'   => $class,
+			'width'   => $width,
+			'height'  => $height,
+			'email'   => $email
+		) );
+
+		if ( !empty( $linked ) ) {
+
+			/**
+			 * Filters the secondary avatar link for current activity.
+			 *
+			 * @since 1.7.0
+			 *
+			 * @param string $link      Link to wrap the avatar image in.
+			 * @param string $component Activity component being acted on.
+			 */
+			$link = apply_filters( 'bp_get_activity_secondary_avatar_link', $link, $activities_template->activity->component );
+
+			/**
+			 * Filters the determined avatar for the secondary activity item.
+			 *
+			 * @since 1.2.10
+			 *
+			 * @param string $avatar Formatted HTML <img> element, or raw avatar URL.
+			 */
+			$avatar = apply_filters( 'bp_get_activity_secondary_avatar', $avatar );
+
+			return sprintf( '<a href="%s" class="%s">%s</a>',
+				$link,
+				$link_class,
+				$avatar
+			);
+		}
+
+		/** This filter is documented in bp-activity/bp-activity-template.php */
+		return apply_filters( 'bp_get_activity_secondary_avatar', $avatar );
+	}
+
+/**
+ * Output the activity action.
+ *
+ * @since 1.2.0
+ *
+ * @param array $args See bp_get_activity_action().
+ */
+function bp_activity_action( $args = array() ) {
+	echo bp_get_activity_action( $args );
+}
+
+	/**
+	 * Return the activity action.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @param array $args {
+	 *     @type bool $no_timestamp Whether to exclude the timestamp.
+	 * }
+	 *
+	 * @return string The activity action.
+	 */
+	function bp_get_activity_action( $args = array() ) {
+		global $activities_template;
+
+		$r = wp_parse_args( $args, array(
+			'no_timestamp' => false,
+		) );
+
+		/**
+		 * Filters the activity action before the action is inserted as meta.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param array $value Array containing the current action, the current activity, and the $args array passed into the function.
+		 */
+		$action = apply_filters_ref_array( 'bp_get_activity_action_pre_meta', array(
+			$activities_template->activity->action,
+			&$activities_template->activity,
+			$r
+		) );
+
+		// Prepend the activity action meta (link, time since, etc...).
+		if ( ! empty( $action ) && empty( $r['no_timestamp'] ) ) {
+			$action = bp_insert_activity_meta( $action );
+		}
+
+		/**
+		 * Filters the activity action after the action has been inserted as meta.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param array $value Array containing the current action, the current activity, and the $args array passed into the function.
+		 */
+		return apply_filters_ref_array( 'bp_get_activity_action', array(
+			$action,
+			&$activities_template->activity,
+			$r
+		) );
+	}
+
+/**
+ * Output the activity content body.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_content_body() {
+	echo bp_get_activity_content_body();
+}
+
+	/**
+	 * Return the activity content body.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity content body.
+	 */
+	function bp_get_activity_content_body() {
+		global $activities_template;
+
+		// Backwards compatibility if action is not being used.
+		if ( empty( $activities_template->activity->action ) && ! empty( $activities_template->activity->content ) ) {
+			$activities_template->activity->content = bp_insert_activity_meta( $activities_template->activity->content );
+		}
+
+		/**
+		 * Filters the activity content body.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $content  Content body.
+		 * @param object $activity Activity object. Passed by reference.
+		 */
+		return apply_filters_ref_array( 'bp_get_activity_content_body', array( $activities_template->activity->content, &$activities_template->activity ) );
+	}
+
+/**
+ * Does the activity have content?
+ *
+ * @since 1.2.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @return bool True if activity has content, false otherwise.
+ */
+function bp_activity_has_content() {
+	global $activities_template;
+
+	if ( ! empty( $activities_template->activity->content ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Output the activity content.
+ *
+ * @since 1.0.0
+ * @deprecated 1.5.0
+ *
+ * @todo properly deprecate this function.
+ *
+ */
+function bp_activity_content() {
+	echo bp_get_activity_content();
+}
+
+	/**
+	 * Return the activity content.
+	 *
+	 * @since 1.0.0
+	 * @deprecated 1.5.0
+	 *
+	 * @todo properly deprecate this function.
+	 *
+	 *
+	 * @return string The activity content.
+	 */
+	function bp_get_activity_content() {
+
+		/**
+		 * If you want to filter activity update content, please use
+		 * the filter 'bp_get_activity_content_body'.
+		 *
+		 * This function is mainly for backwards compatibility.
+		 */
+		$content = bp_get_activity_action() . ' ' . bp_get_activity_content_body();
+		return apply_filters( 'bp_get_activity_content', $content );
+	}
+
+/**
+ * Attach metadata about an activity item to the activity content.
+ *
+ * This metadata includes the time since the item was posted (which will appear
+ * as a link to the item's permalink).
+ *
+ * @since 1.2.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @param string $content The activity content.
+ * @return string The activity content with the metadata string attached.
+ */
+function bp_insert_activity_meta( $content = '' ) {
+	global $activities_template;
+
+	// Strip any legacy time since placeholders from BP 1.0-1.1.
+	$new_content = str_replace( '<span class="time-since">%s</span>', '', $content );
+
+	// Get the time since this activity was recorded.
+	$date_recorded  = bp_core_time_since( $activities_template->activity->date_recorded );
+
+	// Set up 'time-since' <span>.
+	$time_since = sprintf(
+		'<span class="time-since" data-livestamp="%1$s">%2$s</span>',
+		bp_core_get_iso8601_date( $activities_template->activity->date_recorded ),
+		$date_recorded
+	);
+
+	/**
+	 * Filters the activity item time since markup.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $value Array containing the time since markup and the current activity component.
+	 */
+	$time_since = apply_filters_ref_array( 'bp_activity_time_since', array(
+		$time_since,
+		&$activities_template->activity
+	) );
+
+	// Insert the permalink.
+	if ( ! bp_is_single_activity() ) {
+
+		// Setup variables for activity meta.
+		$activity_permalink = bp_activity_get_permalink( $activities_template->activity->id, $activities_template->activity );
+		$activity_meta      = sprintf( '%1$s <a href="%2$s" class="view activity-time-since bp-tooltip" data-bp-tooltip="%3$s">%4$s</a>',
+			$new_content,
+			$activity_permalink,
+			esc_attr__( 'View Discussion', 'buddypress' ),
+			$time_since
+		);
+
+		/**
+		 * Filters the activity permalink to be added to the activity content.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param array $value Array containing the html markup for the activity permalink, after being parsed by
+		 *                     sprintf and current activity component.
+		 */
+		$new_content = apply_filters_ref_array( 'bp_activity_permalink', array(
+			$activity_meta,
+			&$activities_template->activity
+		) );
+	} else {
+		$new_content .= str_pad( $time_since, strlen( $time_since ) + 2, ' ', STR_PAD_BOTH );
+	}
+
+	/**
+	 * Filters the activity content after activity metadata has been attached.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $content Activity content with the activity metadata added.
+	 */
+	return apply_filters( 'bp_insert_activity_meta', $new_content, $content );
+}
+
+/**
+ * Determine if the current user can delete an activity item.
+ *
+ * @since 1.2.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @param false|BP_Activity_Activity $activity Optional. Falls back on the current item in the loop.
+ * @return bool True if can delete, false otherwise.
+ */
+function bp_activity_user_can_delete( $activity = false ) {
+	global $activities_template;
+
+	// Try to use current activity if none was passed.
+	if ( empty( $activity ) && ! empty( $activities_template->activity ) ) {
+		$activity = $activities_template->activity;
+	}
+
+	// If current_comment is set, we'll use that in place of the main activity.
+	if ( isset( $activity->current_comment ) ) {
+		$activity = $activity->current_comment;
+	}
+
+	// Assume the user cannot delete the activity item.
+	$can_delete = false;
+
+	// Only logged in users can delete activity.
+	if ( is_user_logged_in() ) {
+
+		// Community moderators can always delete activity (at least for now).
+		if ( bp_current_user_can( 'bp_moderate' ) ) {
+			$can_delete = true;
+		}
+
+		// Users are allowed to delete their own activity. This is actually
+		// quite powerful, because doing so also deletes all comments to that
+		// activity item. We should revisit this eventually.
+		if ( isset( $activity->user_id ) && ( $activity->user_id === bp_loggedin_user_id() ) ) {
+			$can_delete = true;
+		}
+
+		// Viewing a single item, and this user is an admin of that item.
+		if ( bp_is_single_item() && bp_is_item_admin() ) {
+			$can_delete = true;
+		}
+	}
+
+	/**
+	 * Filters whether the current user can delete an activity item.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $can_delete Whether the user can delete the item.
+	 * @param object $activity   Current activity item object.
+	 */
+	return (bool) apply_filters( 'bp_activity_user_can_delete', $can_delete, $activity );
+}
+
+/**
+ * Output the activity parent content.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_activity_parent_content() for a description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_activity_parent_content} for description.
+ */
+function bp_activity_parent_content( $args = '' ) {
+	echo bp_get_activity_parent_content($args);
+}
+
+	/**
+	 * Return the activity content.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @param string $args Unused. Left over from an earlier implementation.
+	 * @return mixed False on failure, otherwise the activity parent content.
+	 */
+	function bp_get_activity_parent_content( $args = '' ) {
+		global $activities_template;
+
+		// Bail if no activity on no item ID.
+		if ( empty( $activities_template->activity ) || empty( $activities_template->activity->item_id ) ) {
+			return false;
+		}
+
+		// Get the ID of the parent activity content.
+		$parent_id = $activities_template->activity->item_id;
+
+		// Bail if no parent content.
+		if ( empty( $activities_template->activity_parents[ $parent_id ] ) ) {
+			return false;
+		}
+
+		// Bail if no action.
+		if ( empty( $activities_template->activity_parents[ $parent_id ]->action ) ) {
+			return false;
+		}
+
+		// Content always includes action.
+		$content = $activities_template->activity_parents[ $parent_id ]->action;
+
+		// Maybe append activity content, if it exists.
+		if ( ! empty( $activities_template->activity_parents[ $parent_id ]->content ) ) {
+			$content .= ' ' . $activities_template->activity_parents[ $parent_id ]->content;
+		}
+
+		// Remove the time since content for backwards compatibility.
+		$content = str_replace( '<span class="time-since">%s</span>', '', $content );
+
+		// Remove images.
+		$content = preg_replace( '/<img[^>]*>/Ui', '', $content );
+
+		/**
+		 * Filters the activity parent content.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $content Content set to be displayed as parent content.
+		 */
+		return apply_filters( 'bp_get_activity_parent_content', $content );
+	}
+
+/**
+ * Output the parent activity's user ID.
+ *
+ * @since 1.7.0
+ */
+function bp_activity_parent_user_id() {
+	echo bp_get_activity_parent_user_id();
+}
+
+	/**
+	 * Return the parent activity's user ID.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global BP_Activity_Template $activities_template
+	 *
+	 * @return bool|int False if parent activity can't be found, otherwise
+	 *                  the parent activity's user ID.
+	 */
+	function bp_get_activity_parent_user_id() {
+		global $activities_template;
+
+		// Bail if no activity on no item ID.
+		if ( empty( $activities_template->activity ) || empty( $activities_template->activity->item_id ) ) {
+			return false;
+		}
+
+		// Get the ID of the parent activity content.
+		$parent_id = $activities_template->activity->item_id;
+
+		// Bail if no parent item.
+		if ( empty( $activities_template->activity_parents[ $parent_id ] ) ) {
+			return false;
+		}
+
+		// Bail if no parent user ID.
+		if ( empty( $activities_template->activity_parents[ $parent_id ]->user_id ) ) {
+			return false;
+		}
+
+		$retval = $activities_template->activity_parents[ $parent_id ]->user_id;
+
+		/**
+		 * Filters the activity parent item's user ID.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param int $retval ID for the activity parent's user.
+		 */
+		return (int) apply_filters( 'bp_get_activity_parent_user_id', $retval );
+	}
+
+/**
+ * Output whether or not the current activity is in a current user's favorites.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_is_favorite() {
+	echo bp_get_activity_is_favorite();
+}
+
+	/**
+	 * Return whether the current activity is in a current user's favorites.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return bool True if user favorite, false otherwise.
+	 */
+	function bp_get_activity_is_favorite() {
+		global $activities_template;
+
+		/**
+		 * Filters whether the current activity item is in the current user's favorites.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param bool $value Whether or not the current activity item is in the current user's favorites.
+		 */
+		return (bool) apply_filters( 'bp_get_activity_is_favorite', in_array( $activities_template->activity->id, (array) $activities_template->my_favs ) );
+	}
+
+/**
+ * Output the comment markup for an activity item.
+ *
+ * @since 1.2.0
+ *
+ * @todo deprecate $args param
+ *
+ * @param array|string $args See {@link bp_activity_get_comments} for description.
+ */
+function bp_activity_comments( $args = '' ) {
+	echo bp_activity_get_comments( $args );
+}
+
+	/**
+	 * Get the comment markup for an activity item.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @todo deprecate $args param
+	 * @todo Given that checks for children already happen in bp_activity_recurse_comments(),
+	 *       this function can probably be streamlined or removed.
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @param string $args Unused. Left over from an earlier implementation.
+	 * @return bool
+	 */
+	function bp_activity_get_comments( $args = '' ) {
+		global $activities_template;
+
+		if ( empty( $activities_template->activity->children ) ) {
+			return false;
+		}
+
+		bp_activity_recurse_comments( $activities_template->activity );
+	}
+
+		/**
+		 * Loops through a level of activity comments and loads the template for each.
+		 *
+		 * Note: The recursion itself used to happen entirely in this function. Now it is
+		 * split between here and the comment.php template.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @global object $activities_template {@link BP_Activity_Template}
+		 *
+		 * @param object $comment The activity object currently being recursed.
+		 * @return bool|string
+		 */
+		function bp_activity_recurse_comments( $comment ) {
+			global $activities_template;
+
+			if ( empty( $comment ) ) {
+				return false;
+			}
+
+			if ( empty( $comment->children ) ) {
+				return false;
+			}
+
+			/**
+			 * Filters the opening tag for the template that lists activity comments.
+			 *
+			 * @since 1.6.0
+			 *
+			 * @param string $value Opening tag for the HTML markup to use.
+			 */
+			echo apply_filters( 'bp_activity_recurse_comments_start_ul', '<ul>' );
+			foreach ( (array) $comment->children as $comment_child ) {
+
+				// Put the comment into the global so it's available to filters.
+				$activities_template->activity->current_comment = $comment_child;
+
+				$template = bp_locate_template( 'activity/comment.php', false, false );
+
+				// Backward compatibility. In older versions of BP, the markup was
+				// generated in the PHP instead of a template. This ensures that
+				// older themes (which are not children of bp-default and won't
+				// have the new template) will still work.
+				if ( !$template ) {
+					$template = buddypress()->plugin_dir . '/bp-themes/bp-default/activity/comment.php';
+				}
+
+				load_template( $template, false );
+
+				unset( $activities_template->activity->current_comment );
+			}
+
+			/**
+			 * Filters the closing tag for the template that list activity comments.
+			 *
+			 * @since  1.6.0
+			 *
+			 * @param string $value Closing tag for the HTML markup to use.
+			 */
+			echo apply_filters( 'bp_activity_recurse_comments_end_ul', '</ul>' );
+		}
+
+/**
+ * Utility function that returns the comment currently being recursed.
+ *
+ * @since 1.5.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @return object|bool $current_comment The activity comment currently being
+ *                                      displayed. False on failure.
+ */
+function bp_activity_current_comment() {
+	global $activities_template;
+
+	$current_comment = !empty( $activities_template->activity->current_comment )
+		? $activities_template->activity->current_comment
+		: false;
+
+	/**
+	 * Filters the current comment being recursed.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param object|bool $current_comment The activity comment currently being displayed. False on failure.
+	 */
+	return apply_filters( 'bp_activity_current_comment', $current_comment );
+}
+
+
+/**
+ * Output the ID of the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_id() {
+	echo bp_get_activity_comment_id();
+}
+
+	/**
+	 * Return the ID of the activity comment currently being displayed.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int|bool $comment_id The ID of the activity comment currently
+	 *                              being displayed, false if none is found.
+	 */
+	function bp_get_activity_comment_id() {
+		global $activities_template;
+
+		$comment_id = isset( $activities_template->activity->current_comment->id ) ? $activities_template->activity->current_comment->id : false;
+
+		/**
+		 * Filters the ID of the activity comment currently being displayed.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int|bool $comment_id ID for the comment currently being displayed.
+		 */
+		return apply_filters( 'bp_activity_comment_id', $comment_id );
+	}
+
+/**
+ * Output the ID of the author of the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_user_id() {
+	echo bp_get_activity_comment_user_id();
+}
+
+	/**
+	 * Return the ID of the author of the activity comment currently being displayed.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return int|bool $user_id The user_id of the author of the displayed
+	 *                           activity comment. False on failure.
+	 */
+	function bp_get_activity_comment_user_id() {
+		global $activities_template;
+
+		$user_id = isset( $activities_template->activity->current_comment->user_id ) ? $activities_template->activity->current_comment->user_id : false;
+
+		/**
+		 * Filters the ID of the author of the activity comment currently being displayed.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int|bool $user_id ID for the author of the comment currently being displayed.
+		 */
+		return apply_filters( 'bp_activity_comment_user_id', $user_id );
+	}
+
+/**
+ * Output the author link for the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_user_link() {
+	echo bp_get_activity_comment_user_link();
+}
+
+	/**
+	 * Return the author link for the activity comment currently being displayed.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string $user_link The URL of the activity comment author's profile.
+	 */
+	function bp_get_activity_comment_user_link() {
+		$user_link = bp_core_get_user_domain( bp_get_activity_comment_user_id() );
+
+		/**
+		 * Filters the author link for the activity comment currently being displayed.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $user_link Link for the author of the activity comment currently being displayed.
+		 */
+		return apply_filters( 'bp_activity_comment_user_link', $user_link );
+	}
+
+/**
+ * Output the author name for the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_name() {
+	echo bp_get_activity_comment_name();
+}
+
+	/**
+	 * Return the author name for the activity comment currently being displayed.
+	 *
+	 * The use of the 'bp_acomment_name' filter is deprecated. Please use
+	 * 'bp_activity_comment_name'.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string $name The full name of the activity comment author.
+	 */
+	function bp_get_activity_comment_name() {
+		global $activities_template;
+
+		if ( isset( $activities_template->activity->current_comment->user_fullname ) ) {
+
+			$name = apply_filters( 'bp_acomment_name', $activities_template->activity->current_comment->user_fullname, $activities_template->activity->current_comment );  // Backward compatibility.
+		} else {
+			$name = $activities_template->activity->current_comment->display_name;
+		}
+
+		/**
+		 * Filters the name of the author for the activity comment.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $name Name to be displayed with the activity comment.
+		 */
+		return apply_filters( 'bp_activity_comment_name', $name );
+	}
+
+/**
+ * Output the formatted date_recorded of the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_date_recorded() {
+	echo bp_get_activity_comment_date_recorded();
+}
+
+	/**
+	 * Return the formatted date_recorded for the activity comment currently being displayed.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string|bool $date_recorded Time since the activity was recorded,
+	 *                                    in the form "%s ago". False on failure.
+	 */
+	function bp_get_activity_comment_date_recorded() {
+
+		/**
+		 * Filters the recorded date of the activity comment currently being displayed.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string|bool Date for the activity comment currently being displayed.
+		 */
+		return apply_filters( 'bp_activity_comment_date_recorded', bp_core_time_since( bp_get_activity_comment_date_recorded_raw() ) );
+	}
+
+/**
+ * Output the date_recorded of the activity comment currently being displayed.
+ *
+ * @since 2.3.0
+ *
+ */
+function bp_activity_comment_date_recorded_raw() {
+	echo bp_get_activity_comment_date_recorded_raw();
+}
+
+	/**
+	 * Return the date_recorded for the activity comment currently being displayed.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string|bool $date_recorded Time since the activity was recorded,
+	 *                                    in the form "%s ago". False on failure.
+	 */
+	function bp_get_activity_comment_date_recorded_raw() {
+		global $activities_template;
+
+		/**
+		 * Filters the raw recorded date of the activity comment currently being displayed.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string|bool Raw date for the activity comment currently being displayed.
+		 */
+		return apply_filters( 'bp_activity_comment_date_recorded', $activities_template->activity->current_comment->date_recorded );
+	}
+
+/**
+ * Output the 'delete' URL for the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_delete_link() {
+	echo bp_get_activity_comment_delete_link();
+}
+
+	/**
+	 * Gets the 'delete' URL for the activity comment currently being displayed.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string $link The nonced URL for deleting the current
+	 *                      activity comment.
+	 */
+	function bp_get_activity_comment_delete_link() {
+		$link = wp_nonce_url( trailingslashit( bp_get_activity_directory_permalink() . 'delete/' . bp_get_activity_comment_id() ) . '?cid=' . bp_get_activity_comment_id(), 'bp_activity_delete_link' );
+
+		/**
+		 * Filters the link used for deleting the activity comment currently being displayed.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $link Link to use for deleting the currently displayed activity comment.
+		 */
+		return apply_filters( 'bp_activity_comment_delete_link', $link );
+	}
+
+/**
+ * Output the content of the activity comment currently being displayed.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_comment_content() {
+	echo bp_get_activity_comment_content();
+}
+
+	/**
+	 * Return the content of the activity comment currently being displayed.
+	 *
+	 * The content is run through two filters. 'bp_get_activity_content'
+	 * will apply all filters applied to activity items in general. Use
+	 * 'bp_activity_comment_content' to modify the content of activity
+	 * comments only.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string $content The content of the current activity comment.
+	 */
+	function bp_get_activity_comment_content() {
+		global $activities_template;
+
+		/** This filter is documented in bp-activity/bp-activity-template.php */
+		$content = apply_filters( 'bp_get_activity_content', $activities_template->activity->current_comment->content );
+
+		/**
+		 * Filters the content of the current activity comment.
+		 *
+		 * @since 1.2.0
+		 * @since 3.0.0 Added $context parameter to disambiguate from bp_get_activity_comment_content().
+		 *
+		 * @param string $content The content of the current activity comment.
+		 * @param string $context This filter's context ("get").
+		 */
+		return apply_filters( 'bp_activity_comment_content', $content, 'get' );
+	}
+
+/**
+ * Output the activity comment count.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_comment_count() {
+	echo bp_activity_get_comment_count();
+}
+
+	/**
+	 * Return the comment count of an activity item.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @param array|null $deprecated Deprecated.
+	 * @return int $count The activity comment count.
+	 */
+	function bp_activity_get_comment_count( $deprecated = null ) {
+		global $activities_template;
+
+		// Deprecated notice about $args.
+		if ( ! empty( $deprecated ) ) {
+			_deprecated_argument( __FUNCTION__, '1.2', sprintf( __( '%1$s no longer accepts arguments. See the inline documentation at %2$s for more details.', 'buddypress' ), __FUNCTION__, __FILE__ ) );
+		}
+
+		// Get the count using the purpose-built recursive function.
+		$count = ! empty( $activities_template->activity->children )
+			? bp_activity_recurse_comment_count( $activities_template->activity )
+			: 0;
+
+		/**
+		 * Filters the activity comment count.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $count The activity comment count.
+		 */
+		return apply_filters( 'bp_activity_get_comment_count', (int) $count );
+	}
+
+		/**
+		 * Return the total number of comments to the current comment.
+		 *
+		 * This function recursively adds the total number of comments each
+		 * activity child has, and returns them.
+		 *
+		 * @since 1.2.0
+		 *
+		 *
+		 * @param object $comment Activity comment object.
+		 * @param int    $count The current iteration count.
+		 * @return int $count The activity comment count.
+		 */
+		function bp_activity_recurse_comment_count( $comment, $count = 0 ) {
+
+			// Copy the count.
+			$new_count = $count;
+
+			// Loop through children and recursively count comments.
+			if ( ! empty( $comment->children ) ) {
+				foreach ( (array) $comment->children as $comment ) {
+					$new_count++;
+					$new_count = bp_activity_recurse_comment_count( $comment, $new_count );
+				}
+			}
+
+			/**
+			 * Filters the total number of comments for the current comment.
+			 *
+			 * @since 2.1.0
+			 *
+			 * @param int    $new_count New total count for the current comment.
+			 * @param object $comment   Activity comment object.
+			 * @param int    $count     Current iteration count for the current comment.
+			 */
+			return apply_filters( 'bp_activity_recurse_comment_count', $new_count, $comment, $count );
+		}
+
+/**
+ * Output the depth of the current activity comment.
+ *
+ * @since 2.0.0
+ * @since 2.8.0 Added $comment as a parameter.
+ *
+ * @param object|int $comment Object of the activity comment or activity comment ID. Usually unnecessary
+ *                            when used in activity comment loop.
+ */
+function bp_activity_comment_depth( $comment = 0 ) {
+	echo bp_activity_get_comment_depth( $comment );
+}
+
+	/**
+	 * Return the current activity comment depth.
+	 *
+	 * @since 2.0.0
+	 * @since 2.8.0 Added $comment as a parameter.
+	 *
+	 * @param  object|int $comment Object of the activity comment or activity comment ID. Usually unnecessary
+	 *                             when used in activity comment loop.
+	 * @return int
+	 */
+	function bp_activity_get_comment_depth( $comment = 0 ) {
+		$depth = 0;
+
+		// Activity comment loop takes precedence.
+		if ( isset( $GLOBALS['activities_template']->activity->current_comment->depth ) ) {
+			$depth = $GLOBALS['activities_template']->activity->current_comment->depth;
+
+		// Get depth for activity comment manually.
+		} elseif ( ! empty( $comment ) ) {
+			// We passed an activity ID, so fetch the activity object.
+			if ( is_int( $comment ) ) {
+				$comment = new BP_Activity_Activity( $comment );
+			}
+
+			// Recurse through activity tree to find the depth.
+			if ( is_object( $comment ) && isset( $comment->type ) && 'activity_comment' === $comment->type ) {
+				// Fetch the entire root comment tree... ugh.
+				$comments = BP_Activity_Activity::get_activity_comments( $comment->item_id, 1, constant( 'PHP_INT_MAX' ) );
+
+				// Recursively find our comment object from the comment tree.
+				$iterator  = new RecursiveArrayIterator( $comments );
+				$recursive = new RecursiveIteratorIterator( $iterator, RecursiveIteratorIterator::SELF_FIRST );
+				foreach ( $recursive as $cid => $cobj ) {
+					// Skip items that are not a comment object.
+					if ( ! is_numeric( $cid ) || ! is_object( $cobj ) ) {
+						continue;
+					}
+
+					// We found the activity comment! Set the depth.
+					if ( $cid === $comment->id && isset( $cobj->depth ) ) {
+						$depth = $cobj->depth;
+						break;
+					}
+				}
+			}
+		}
+
+		/**
+		 * Filters the comment depth of the current activity comment.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int $depth Depth for the current activity comment.
+		 */
+		return apply_filters( 'bp_activity_get_comment_depth', $depth );
+	}
+
+/**
+ * Output the activity comment link.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_comment_link() {
+	echo bp_get_activity_comment_link();
+}
+
+	/**
+	 * Return the activity comment link.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity comment link.
+	 */
+	function bp_get_activity_comment_link() {
+		global $activities_template;
+
+		/**
+		 * Filters the comment link for the current activity comment.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Constructed URL parameters with activity IDs.
+		 */
+		return apply_filters( 'bp_get_activity_comment_link', '?ac=' . $activities_template->activity->id . '/#ac-form-' . $activities_template->activity->id );
+	}
+
+/**
+ * Output the activity comment form no JavaScript display CSS.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_comment_form_nojs_display() {
+	echo bp_get_activity_comment_form_nojs_display();
+}
+
+	/**
+	 * Return the activity comment form no JavaScript display CSS.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string|false The activity comment form no JavaScript
+	 *                      display CSS. False on failure.
+	 */
+	function bp_get_activity_comment_form_nojs_display() {
+		global $activities_template;
+
+		if ( isset( $_GET['ac'] ) && ( $_GET['ac'] === ( $activities_template->activity->id . '/' ) ) ) {
+			return 'style="display: block"';
+		}
+
+		return false;
+	}
+
+/**
+ * Output the activity comment form action.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_comment_form_action() {
+	echo bp_get_activity_comment_form_action();
+}
+
+	/**
+	 * Return the activity comment form action.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @return string The activity comment form action.
+	 */
+	function bp_get_activity_comment_form_action() {
+
+		/**
+		 * Filters the activity comment form action URL.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value URL to use in the comment form's action attribute.
+		 */
+		return apply_filters( 'bp_get_activity_comment_form_action', home_url( bp_get_activity_root_slug() . '/reply/' ) );
+	}
+
+/**
+ * Output the activity permalink ID.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_permalink_id() {
+	echo bp_get_activity_permalink_id();
+}
+
+	/**
+	 * Return the activity permalink ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @return string The activity permalink ID.
+	 */
+	function bp_get_activity_permalink_id() {
+
+		/**
+		 * Filters the activity action permalink ID.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Current action for the activity item.
+		 */
+		return apply_filters( 'bp_get_activity_permalink_id', bp_current_action() );
+	}
+
+/**
+ * Output the activity thread permalink.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_thread_permalink() {
+	echo esc_url( bp_get_activity_thread_permalink() );
+}
+
+	/**
+	 * Return the activity thread permalink.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @return string $link The activity thread permalink.
+	 */
+	function bp_get_activity_thread_permalink() {
+		global $activities_template;
+
+		$link = bp_activity_get_permalink( $activities_template->activity->id, $activities_template->activity );
+
+		/**
+		 * Filters the activity thread permalink.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $link The activity thread permalink.
+		 */
+		return apply_filters( 'bp_get_activity_thread_permalink', $link );
+	}
+
+/**
+ * Output the activity comment permalink.
+ *
+ * @since 1.8.0
+ *
+ */
+function bp_activity_comment_permalink() {
+	echo esc_url( bp_get_activity_comment_permalink() );
+}
+	/**
+	 * Return the activity comment permalink.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @return string $link The activity comment permalink.
+	 */
+	function bp_get_activity_comment_permalink() {
+		global $activities_template;
+
+		$link = bp_activity_get_permalink( $activities_template->activity->id, $activities_template->activity );
+
+		// Used for filter below.
+		$comment_id = isset( $activities_template->activity->current_comment->id )
+			? $activities_template->activity->current_comment->id
+			: 0;
+
+		/**
+		 * Filters the activity comment permalink.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param string $link       Activity comment permalink.
+		 * @param int    $comment_id ID for the current activity comment.
+		 */
+		return apply_filters( 'bp_get_activity_comment_permalink', $link, $comment_id );
+	}
+
+/**
+ * Output the activity favorite link.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_favorite_link() {
+	echo bp_get_activity_favorite_link();
+}
+
+	/**
+	 * Return the activity favorite link.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity favorite link.
+	 */
+	function bp_get_activity_favorite_link() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity favorite link.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Constructed link for favoriting the activity comment.
+		 */
+		return apply_filters( 'bp_get_activity_favorite_link', wp_nonce_url( home_url( bp_get_activity_root_slug() . '/favorite/' . $activities_template->activity->id . '/' ), 'mark_favorite' ) );
+	}
+
+/**
+ * Output the activity unfavorite link.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_unfavorite_link() {
+	echo bp_get_activity_unfavorite_link();
+}
+
+	/**
+	 * Return the activity unfavorite link.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity unfavorite link.
+	 */
+	function bp_get_activity_unfavorite_link() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity unfavorite link.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Constructed link for unfavoriting the activity comment.
+		 */
+		return apply_filters( 'bp_get_activity_unfavorite_link', wp_nonce_url( home_url( bp_get_activity_root_slug() . '/unfavorite/' . $activities_template->activity->id . '/' ), 'unmark_favorite' ) );
+	}
+
+/**
+ * Output the activity CSS class.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_css_class() {
+	echo bp_get_activity_css_class();
+}
+
+	/**
+	 * Return the current activity item's CSS class.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity item's CSS class.
+	 */
+	function bp_get_activity_css_class() {
+		global $activities_template;
+
+		/**
+		 * Filters the available mini activity actions available as CSS classes.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param array $value Array of classes used to determine classes applied to HTML element.
+		 */
+		$mini_activity_actions = apply_filters( 'bp_activity_mini_activity_types', array(
+			'friendship_accepted',
+			'friendship_created',
+			'new_blog',
+			'joined_group',
+			'created_group',
+			'new_member'
+		) );
+
+		$class = ' activity-item';
+
+		if ( in_array( $activities_template->activity->type, (array) $mini_activity_actions ) || empty( $activities_template->activity->content ) ) {
+			$class .= ' mini';
+		}
+
+		if ( bp_activity_get_comment_count() && bp_activity_can_comment() ) {
+			$class .= ' has-comments';
+		}
+
+		/**
+		 * Filters the determined classes to add to the HTML element.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Classes to be added to the HTML element.
+		 */
+		return apply_filters( 'bp_get_activity_css_class', $activities_template->activity->component . ' ' . $activities_template->activity->type . $class );
+	}
+
+/**
+ * Output the activity delete link.
+ *
+ * @since 1.1.0
+ *
+ */
+function bp_activity_delete_link() {
+	echo bp_get_activity_delete_link();
+}
+
+	/**
+	 * Return the activity delete link.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string $link Activity delete link. Contains $redirect_to arg
+	 *                      if on single activity page.
+	 */
+	function bp_get_activity_delete_link() {
+
+		$url   = bp_get_activity_delete_url();
+		$class = 'delete-activity';
+
+		// Determine if we're on a single activity page, and customize accordingly.
+		if ( bp_is_activity_component() && is_numeric( bp_current_action() ) ) {
+			$class = 'delete-activity-single';
+		}
+
+		$link = '<a href="' . esc_url( $url ) . '" class="button item-button bp-secondary-action ' . $class . ' confirm" rel="nofollow">' . __( 'Delete', 'buddypress' ) . '</a>';
+
+		/**
+		 * Filters the activity delete link.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $link Activity delete HTML link.
+		 */
+		return apply_filters( 'bp_get_activity_delete_link', $link );
+	}
+
+/**
+ * Output the URL to delete a single activity stream item.
+ *
+ * @since 2.1.0
+ *
+ */
+function bp_activity_delete_url() {
+	echo esc_url( bp_get_activity_delete_url() );
+}
+	/**
+	 * Return the URL to delete a single activity item.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string $link Activity delete link. Contains $redirect_to arg
+	 *                      if on single activity page.
+	 */
+	function bp_get_activity_delete_url() {
+		global $activities_template;
+
+		$url = trailingslashit( bp_get_root_domain() . '/' . bp_get_activity_root_slug() . '/delete/' . $activities_template->activity->id );
+
+		// Determine if we're on a single activity page, and customize accordingly.
+		if ( bp_is_activity_component() && is_numeric( bp_current_action() ) ) {
+			$url = add_query_arg( array( 'redirect_to' => wp_get_referer() ), $url );
+		}
+
+		$url = wp_nonce_url( $url, 'bp_activity_delete_link' );
+
+		/**
+		 * Filters the activity delete URL.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $url Activity delete URL.
+		 */
+		return apply_filters( 'bp_get_activity_delete_url', $url );
+	}
+
+/**
+ * Output the activity latest update link.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_activity_latest_update() for description of parameters.
+ *
+ * @param int $user_id See {@link bp_get_activity_latest_update()} for description.
+ */
+function bp_activity_latest_update( $user_id = 0 ) {
+	echo bp_get_activity_latest_update( $user_id );
+}
+
+	/**
+	 * Return the activity latest update link.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @param int $user_id If empty, will fall back on displayed user.
+	 * @return string|bool $latest_update The activity latest update link.
+	 *                                    False on failure.
+	 */
+	function bp_get_activity_latest_update( $user_id = 0 ) {
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_displayed_user_id();
+		}
+
+		if ( bp_is_user_inactive( $user_id ) ) {
+			return false;
+		}
+
+		if ( !$update = bp_get_user_meta( $user_id, 'bp_latest_update', true ) ) {
+			return false;
+		}
+
+		/**
+		 * Filters the latest update excerpt.
+		 *
+		 * @since 1.2.10
+		 * @since 2.6.0  Added the `$user_id` parameter.
+		 *
+		 * @param string $value   The excerpt for the latest update.
+		 * @param int    $user_id ID of the queried user.
+		 */
+		$latest_update = apply_filters( 'bp_get_activity_latest_update_excerpt', trim( strip_tags( bp_create_excerpt( $update['content'], bp_activity_get_excerpt_length() ) ) ), $user_id );
+
+		$latest_update = sprintf(
+			'%s <a href="%s">%s</a>',
+			$latest_update,
+			esc_url_raw( bp_activity_get_permalink( $update['id'] ) ),
+			esc_attr__( 'View', 'buddypress' )
+		);
+
+		/**
+		 * Filters the latest update excerpt with view link appended to the end.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param string $latest_update The latest update with "view" link appended to it.
+		 * @param int    $user_id       ID of the queried user.
+		 */
+		return apply_filters( 'bp_get_activity_latest_update', $latest_update, $user_id );
+	}
+
+/**
+ * Output the activity filter links.
+ *
+ * @since 1.1.0
+ *
+ * @see bp_get_activity_filter_links() for description of parameters.
+ *
+ * @param array|bool $args See {@link bp_get_activity_filter_links()} for description.
+ */
+function bp_activity_filter_links( $args = false ) {
+	echo bp_get_activity_filter_links( $args );
+}
+
+	/**
+	 * Return the activity filter links.
+	 *
+	 * @since 1.1.0
+	 *
+	 *
+	 * @param array|bool $args {
+	 *     @type string $style The type of markup to use for the links.
+	 *                         'list', 'paragraph', or 'span'. Default: 'list'.
+	 * }
+	 * @return string|bool $component_links The activity filter links.
+	 *         False on failure.
+	 */
+	function bp_get_activity_filter_links( $args = false ) {
+
+		$r = wp_parse_args( $args, array(
+			'style' => 'list'
+		) );
+
+		// Define local variable.
+		$component_links = array();
+
+		// Fetch the names of components that have activity recorded in the DB.
+		$components = BP_Activity_Activity::get_recorded_components();
+
+		if ( empty( $components ) ) {
+			return false;
+		}
+
+		foreach ( (array) $components as $component ) {
+
+			// Skip the activity comment filter.
+			if ( 'activity' == $component ) {
+				continue;
+			}
+
+			if ( isset( $_GET['afilter'] ) && $component == $_GET['afilter'] ) {
+				$selected = ' class="selected"';
+			} else {
+				$selected = '';
+			}
+
+			$component = esc_attr( $component );
+
+			switch ( $r['style'] ) {
+				case 'list':
+					$tag = 'li';
+					$before = '<li id="afilter-' . $component . '"' . $selected . '>';
+					$after = '</li>';
+				break;
+				case 'paragraph':
+					$tag = 'p';
+					$before = '<p id="afilter-' . $component . '"' . $selected . '>';
+					$after = '</p>';
+				break;
+				case 'span':
+					$tag = 'span';
+					$before = '<span id="afilter-' . $component . '"' . $selected . '>';
+					$after = '</span>';
+				break;
+			}
+
+			$link = add_query_arg( 'afilter', $component );
+			$link = remove_query_arg( 'acpage' , $link );
+
+			/**
+			 * Filters the activity filter link URL for the current activity component.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $link      The URL for the current component.
+			 * @param string $component The current component getting links constructed for.
+			 */
+			$link = apply_filters( 'bp_get_activity_filter_link_href', $link, $component );
+
+			$component_links[] = $before . '<a href="' . esc_url( $link ) . '">' . ucwords( $component ) . '</a>' . $after;
+		}
+
+		$link = remove_query_arg( 'afilter' , $link );
+
+		if ( isset( $_GET['afilter'] ) ) {
+			$component_links[] = '<' . $tag . ' id="afilter-clear"><a href="' . esc_url( $link ) . '">' . __( 'Clear Filter', 'buddypress' ) . '</a></' . $tag . '>';
+		}
+
+		/**
+		 * Filters all of the constructed filter links.
+		 *
+		 * @since 1.1.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $value All of the links to be displayed to the user.
+		 * @param array  $r     Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_activity_filter_links', implode( "\n", $component_links ), $r );
+	}
+
+/**
+ * Determine if a comment can be made on an activity item.
+ *
+ * @since 1.2.0
+ *
+ * @global object $activities_template {@link BP_Activity_Template}
+ *
+ * @return bool $can_comment True if item can receive comments.
+ */
+function bp_activity_can_comment() {
+	global $activities_template;
+	$bp = buddypress();
+
+	// Determine ability to comment based on activity type name.
+	$activity_type = bp_get_activity_type();
+
+	// Get the 'comment-reply' support for the current activity type.
+	$can_comment = bp_activity_type_supports( $activity_type, 'comment-reply' );
+
+	// Neutralize activity_comment.
+	if ( 'activity_comment' === $activity_type ) {
+		$can_comment = false;
+	}
+
+	/**
+	 * Filters whether a comment can be made on an activity item.
+	 *
+	 * @since 1.5.0
+	 * @since 2.5.0 Use $activity_type instead of $activity_name for the second parameter.
+	 *
+	 * @param bool   $can_comment     Status on if activity can be commented on.
+	 * @param string $activity_type   Current activity type being checked on.
+	 */
+	return apply_filters( 'bp_activity_can_comment', $can_comment, $activity_type );
+}
+
+/**
+ * Determine whether a comment can be made on an activity reply item.
+ *
+ * @since 1.5.0
+ *
+ * @param  bool|object $comment     Activity comment.
+ * @return bool        $can_comment True if comment can receive comments,
+ *                                  otherwise false.
+ */
+function bp_activity_can_comment_reply( $comment = false ) {
+
+	// Assume activity can be commented on.
+	$can_comment = true;
+
+	// Check that comment exists.
+	if ( empty( $comment ) ) {
+		$comment = bp_activity_current_comment();
+	}
+
+	if ( ! empty( $comment ) ) {
+
+		// Fall back on current comment in activity loop.
+		$comment_depth = isset( $comment->depth )
+			? intval( $comment->depth )
+			: bp_activity_get_comment_depth( $comment );
+
+		// Threading is turned on, so check the depth.
+		if ( get_option( 'thread_comments' ) ) {
+			$can_comment = (bool) ( $comment_depth < get_option( 'thread_comments_depth' ) );
+
+		// No threading for comment replies if no threading for comments.
+		} else {
+			$can_comment = false;
+		}
+	}
+
+	/**
+	 * Filters whether a comment can be made on an activity reply item.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $can_comment Status on if activity reply can be commented on.
+	 * @param object $comment     Current comment object being checked on.
+	 */
+	return (bool) apply_filters( 'bp_activity_can_comment_reply', $can_comment, $comment );
+}
+
+/**
+ * Determine whether favorites are allowed.
+ *
+ * Defaults to true, but can be modified by plugins.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if comment can receive comments.
+ */
+function bp_activity_can_favorite() {
+
+	/**
+	 * Filters whether or not users can favorite activity items.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $value Whether or not favoriting is enabled.
+	 */
+	return apply_filters( 'bp_activity_can_favorite', true );
+}
+
+/**
+ * Output the total favorite count for a specified user.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_total_favorite_count_for_user() for description of parameters.
+ *
+ * @param int $user_id See {@link bp_get_total_favorite_count_for_user()}.
+ */
+function bp_total_favorite_count_for_user( $user_id = 0 ) {
+	echo bp_get_total_favorite_count_for_user( $user_id );
+}
+
+	/**
+	 * Return the total favorite count for a specified user.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @param int $user_id ID of user being queried. Default: displayed user ID.
+	 * @return int The total favorite count for the specified user.
+	 */
+	function bp_get_total_favorite_count_for_user( $user_id = 0 ) {
+		$retval = false;
+
+		if ( bp_activity_can_favorite() ) {
+			// Default to displayed user if none is passed.
+			$user_id = empty( $user_id )
+				? bp_displayed_user_id()
+				: $user_id;
+
+			// Get user meta if user ID exists.
+			if ( ! empty( $user_id ) ) {
+				$retval = bp_activity_total_favorites_for_user( $user_id );
+			}
+		}
+
+		/**
+		 * Filters the total favorite count for a user.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param int|bool $retval  Total favorite count for a user. False on no favorites.
+		 * @param int      $user_id ID of the queried user.
+		 */
+		return apply_filters( 'bp_get_total_favorite_count_for_user', $retval, $user_id );
+	}
+
+
+/**
+ * Output the total mention count for a specified user.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_total_mention_count_for_user() for description of parameters.
+ *
+ * @param int $user_id See {@link bp_get_total_mention_count_for_user()}.
+ */
+function bp_total_mention_count_for_user( $user_id = 0 ) {
+	echo bp_get_total_mention_count_for_user( $user_id );
+}
+
+	/**
+	 * Return the total mention count for a specified user.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @param int $user_id ID of user being queried. Default: displayed user ID.
+	 * @return int The total mention count for the specified user.
+	 */
+	function bp_get_total_mention_count_for_user( $user_id = 0 ) {
+
+		// Default to displayed user if none is passed.
+		$user_id = empty( $user_id )
+			? bp_displayed_user_id()
+			: $user_id;
+
+		// Get user meta if user ID exists.
+		$retval = ! empty( $user_id )
+			? bp_get_user_meta( $user_id, 'bp_new_mention_count', true )
+			: false;
+
+		/**
+		 * Filters the total mention count for a user.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param int|bool $retval  Total mention count for a user. False on no mentions.
+		 * @param int      $user_id ID of the queried user.
+		 */
+		return apply_filters( 'bp_get_total_mention_count_for_user', $retval, $user_id );
+	}
+
+/**
+ * Output the public message link for displayed user.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_send_public_message_link() {
+	echo esc_url( bp_get_send_public_message_link() );
+}
+
+	/**
+	 * Return the public message link for the displayed user.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @return string The public message link for the displayed user.
+	 */
+	function bp_get_send_public_message_link() {
+
+		// No link if not logged in, not looking at someone else's profile.
+		if ( ! is_user_logged_in() || ! bp_is_user() || bp_is_my_profile() ) {
+			$retval = '';
+		} else {
+			$args   = array( 'r' => bp_get_displayed_user_mentionname() );
+			$url    = add_query_arg( $args, bp_get_activity_directory_permalink() );
+			$retval = wp_nonce_url( $url );
+		}
+
+		/**
+		 * Filters the public message link for the displayed user.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $retval The URL for the public message link.
+		 */
+		return apply_filters( 'bp_get_send_public_message_link', $retval );
+	}
+
+/**
+ * Recurse through all activity comments and return the activity comment IDs.
+ *
+ * @since 2.0.0
+ *
+ * @param array $activity Array of activities generated from {@link bp_activity_get()}.
+ * @param array $activity_ids Used for recursion purposes in this function.
+ * @return array
+ */
+function bp_activity_recurse_comments_activity_ids( $activity = array(), $activity_ids = array() ) {
+	if ( is_array( $activity ) && ! empty( $activity['activities'] ) ) {
+		$activity = $activity['activities'][0];
+	}
+
+	if ( ! empty( $activity->children ) ) {
+		foreach ($activity->children as $child ) {
+			$activity_ids[] = $child->id;
+
+			if( ! empty( $child->children ) ) {
+				$activity_ids = bp_activity_recurse_comments_activity_ids( $child, $activity_ids );
+			}
+		}
+	}
+
+	return $activity_ids;
+}
+
+/**
+ * Output the mentioned user display name.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_mentioned_user_display_name() for description of parameters.
+ *
+ * @param int|string|bool $user_id_or_username See {@link bp_get_mentioned_user_display_name()}.
+ */
+function bp_mentioned_user_display_name( $user_id_or_username = false ) {
+	echo bp_get_mentioned_user_display_name( $user_id_or_username );
+}
+
+	/**
+	 * Returns the mentioned user display name.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @param int|string|bool $user_id_or_username User ID or username.
+	 * @return string The mentioned user's display name.
+	 */
+	function bp_get_mentioned_user_display_name( $user_id_or_username = false ) {
+
+		// Get user display name.
+		$name = bp_core_get_user_displayname( $user_id_or_username );
+
+		// If user somehow has no name, return this really lame string.
+		if ( empty( $name ) ) {
+			$name = __( 'a user', 'buddypress' );
+		}
+
+		/**
+		 * Filters the mentioned user display name.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string     $name                Display name for the mentioned user.
+		 * @param int|string $user_id_or_username User ID or username use for query.
+		 */
+		return apply_filters( 'bp_get_mentioned_user_display_name', $name, $user_id_or_username );
+	}
+
+/**
+ * Output button for sending a public message (an @-mention).
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_send_public_message_button() for description of parameters.
+ *
+ * @param array|string $args See {@link bp_get_send_public_message_button()}.
+ */
+function bp_send_public_message_button( $args = '' ) {
+	echo bp_get_send_public_message_button( $args );
+}
+
+	/**
+	 * Return button for sending a public message (an @-mention).
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @param array|string $args {
+	 *     All arguments are optional. See {@link BP_Button} for complete
+	 *     descriptions.
+	 *     @type string $id                Default: 'public_message'.
+	 *     @type string $component         Default: 'activity'.
+	 *     @type bool   $must_be_logged_in Default: true.
+	 *     @type bool   $block_self        Default: true.
+	 *     @type string $wrapper_id        Default: 'post-mention'.
+	 *     @type string $link_href         Default: the public message link for
+	 *                                     the current member in the loop.
+	 *     @type string $link_text         Default: 'Public Message'.
+	 *     @type string $link_class        Default: 'activity-button mention'.
+	 * }
+	 * @return string The button for sending a public message.
+	 */
+	function bp_get_send_public_message_button( $args = '' ) {
+
+		$r = bp_parse_args( $args, array(
+			'id'                => 'public_message',
+			'component'         => 'activity',
+			'must_be_logged_in' => true,
+			'block_self'        => true,
+			'wrapper_id'        => 'post-mention',
+			'link_href'         => bp_get_send_public_message_link(),
+			'link_text'         => __( 'Public Message', 'buddypress' ),
+			'link_class'        => 'activity-button mention'
+		) );
+
+		/**
+		 * Filters the public message button HTML.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param array $r Array of arguments for the public message button HTML.
+		 */
+		return bp_get_button( apply_filters( 'bp_get_send_public_message_button', $r ) );
+	}
+
+/**
+ * Output the activity post form action.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_activity_post_form_action() {
+	echo bp_get_activity_post_form_action();
+}
+
+	/**
+	 * Return the activity post form action.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @return string The activity post form action.
+	 */
+	function bp_get_activity_post_form_action() {
+
+		/**
+		 * Filters the action url used for the activity post form.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value URL to be used for the activity post form.
+		 */
+		return apply_filters( 'bp_get_activity_post_form_action', home_url( bp_get_activity_root_slug() . '/post/' ) );
+	}
+
+/**
+ * Echo a list of linked avatars of users who have commented on the current activity item.
+ *
+ * Use this function to easily output activity comment authors' avatars.
+ *
+ * Avatars are wrapped in <li> elements, but you've got to provide your own
+ * <ul> or <ol> wrapper markup.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_core_fetch_avatar() for a description of arguments.
+ *
+ * @param array $args See {@link bp_core_fetch_avatar()}.
+ */
+function bp_activity_comments_user_avatars( $args = array() ) {
+
+	$r = bp_parse_args( $args, array(
+		'height' => false,
+		'html'   => true,
+		'type'   => 'thumb',
+		'width'  => false,
+	) );
+
+	// Get the user IDs of everyone who has left a comment to the current activity item.
+	$user_ids = bp_activity_get_comments_user_ids();
+	$output   = array();
+	$retval   = '';
+
+	if ( ! empty( $user_ids ) ) {
+		foreach ( (array) $user_ids as $user_id ) {
+
+			// Skip an empty user ID.
+			if ( empty( $user_id ) ) {
+				continue;
+			}
+
+			// Get profile link for this user.
+			$profile_link = bp_core_get_user_domain( $user_id );
+
+			// Get avatar for this user.
+			$image_html   = bp_core_fetch_avatar( array(
+				'item_id' => $user_id,
+				'height'  => $r['height'],
+				'html'    => $r['html'],
+				'type'    => $r['type'],
+				'width'   => $r['width']
+			) );
+
+			// If user has link & avatar, add them to the output array.
+			if ( ! empty( $profile_link ) && ! empty( $image_html ) ) {
+				$output[] = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $profile_link ), $image_html );
+			}
+		}
+
+		// If output array is not empty, wrap everything in some list items.
+		if ( ! empty( $output ) ) {
+			$retval = '<li>' . implode( '</li><li>', $output ) . '</li>';
+		}
+	}
+
+	/**
+	 * Filters the list of linked avatars for users who have commented on the current activity item.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $retval HTML markup for the list of avatars.
+	 * @param array  $r      Array of arguments used for each avatar.
+	 * @param array  $output Array of each avatar found, before imploded into single string.
+	 */
+	echo apply_filters( 'bp_activity_comments_user_avatars', $retval, $r, $output );
+}
+
+/**
+ * Return the IDs of every user who's left a comment on the current activity item.
+ *
+ * @since 1.7.0
+ *
+ * @return bool|array An array of IDs, or false if none are found.
+ */
+function bp_activity_get_comments_user_ids() {
+	global $activities_template;
+
+	$user_ids = ! empty( $activities_template->activity->children )
+		? (array) bp_activity_recurse_comments_user_ids( $activities_template->activity->children )
+		: array();
+
+	/**
+	 * Filters the list of user IDs for the current activity item.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $value Array of unique user IDs for the current activity item.
+	 */
+	return apply_filters( 'bp_activity_get_comments_user_ids', array_unique( $user_ids ) );
+}
+
+	/**
+	 * Recurse through all activity comments and collect the IDs of the users who wrote them.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $comments Array of {@link BP_Activity_Activity} items.
+	 * @return array Array of user IDs.
+	 */
+	function bp_activity_recurse_comments_user_ids( array $comments = array() ) {
+
+		// Default user ID's array.
+		$user_ids = array();
+
+		// Loop through comments and try to get user ID's.
+		if ( ! empty( $comments ) ) {
+			foreach ( $comments as $comment ) {
+
+				// If a user is a spammer, their activity items will have been
+				// automatically marked as spam. Skip these.
+				if ( ! empty( $comment->is_spam ) ) {
+					continue;
+				}
+
+				// Add user ID to array.
+				$user_ids[] = $comment->user_id;
+
+				// Check for commentception.
+				if ( ! empty( $comment->children ) ) {
+					$user_ids = array_merge( $user_ids, bp_activity_recurse_comments_user_ids( $comment->children ) );
+				}
+			}
+		}
+
+		/**
+		 * Filters the list of user IDs for the current activity comment item.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array $user_ids Array of user IDs for the current activity comment item.
+		 * @param array $comments Array of comments being checked for user IDs.
+		 */
+		return apply_filters( 'bp_activity_recurse_comments_user_ids', $user_ids, $comments );
+	}
+
+/**
+ * Output the mentionname for the displayed user.
+ *
+ * @since 1.9.0
+ */
+function bp_displayed_user_mentionname() {
+	echo bp_get_displayed_user_mentionname();
+}
+	/**
+	 * Get the mentionname for the displayed user.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string Mentionname for the displayed user, if available.
+	 */
+	function bp_get_displayed_user_mentionname() {
+
+		/**
+		 * Filters the mentionname for the displayed user.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $value The mentionanme for the displayed user.
+		 */
+		return apply_filters( 'bp_get_displayed_user_mentionname', bp_activity_get_user_mentionname( bp_displayed_user_id() ) );
+	}
+
+/**
+ * Echo a list of all registered activity types for use in dropdowns or checkbox lists.
+ *
+ * @since 1.7.0
+ *
+ * @param string       $output Optional. Either 'select' or 'checkbox'. Default: 'select'.
+ * @param array|string $args {
+ *     Optional extra arguments.
+ *     @type string       $checkbox_name When returning checkboxes, sets the 'name'
+ *                                       attribute.
+ *     @type array|string $selected      A list of types that should be checked/
+ *                                       selected.
+ * }
+ */
+function bp_activity_types_list( $output = 'select', $args = '' ) {
+
+	$args = bp_parse_args( $args, array(
+		'checkbox_name' => 'bp_activity_types',
+		'selected'      => array(),
+	) );
+
+	$activities = bp_activity_get_types();
+	natsort( $activities );
+
+	// Loop through the activity types and output markup.
+	foreach ( $activities as $type => $description ) {
+
+		// See if we need to preselect the current type.
+		$checked  = checked(  true, in_array( $type, (array) $args['selected'] ), false );
+		$selected = selected( true, in_array( $type, (array) $args['selected'] ), false );
+
+		// Switch output based on the element.
+		switch ( $output ) {
+			case 'select' :
+				printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $type ), $selected, esc_html( $description ) );
+				break;
+			case 'checkbox' :
+				printf( '<label style="" for="%1$s[]">%2$s<input type="checkbox" id="%1$s[]" name="%1$s[]" value="%3$s" %4$s/></label>', esc_attr( $args['checkbox_name'] ), esc_html( $description ), esc_attr( $args['checkbox_name'] ), esc_attr( $args['checkbox_name'] ), esc_attr( $type ), $checked );
+				break;
+		}
+
+		/**
+		 * Fires at the end of the listing of activity types.
+		 *
+		 * This is a variable action hook. The actual hook to use will depend on the output type specified.
+		 * Two default hooks are bp_activity_types_list_select and bp_activity_types_list_checkbox.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array  $args        Array of arguments passed into function.
+		 * @param string $type        Activity type being rendered in the output.
+		 * @param string $description Description of the activity type being rendered.
+		 */
+		do_action( 'bp_activity_types_list_' . $output, $args, $type, $description );
+	}
+
+	// Backpat with BP-Default for dropdown boxes only.
+	if ( 'select' === $output ) {
+		do_action( 'bp_activity_filter_options' );
+	}
+}
+
+
+/* RSS Feed Template Tags ****************************************************/
+
+/**
+ * Output the sitewide activity feed link.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_sitewide_activity_feed_link() {
+	echo bp_get_sitewide_activity_feed_link();
+}
+
+	/**
+	 * Returns the sitewide activity feed link.
+	 *
+	 * @since 1.0.0
+	 *
+	 *
+	 * @return string The sitewide activity feed link.
+	 */
+	function bp_get_sitewide_activity_feed_link() {
+
+		/**
+		 * Filters the sidewide activity feed link.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value The feed link for sitewide activity.
+		 */
+		return apply_filters( 'bp_get_sitewide_activity_feed_link', bp_get_root_domain() . '/' . bp_get_activity_root_slug() . '/feed/' );
+	}
+
+/**
+ * Output the member activity feed link.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_member_activity_feed_link() {
+	echo bp_get_member_activity_feed_link();
+}
+
+/**
+ * Output the member activity feed link.
+ *
+ * @since 1.0.0
+ * @deprecated 1.2.0
+ *
+ * @todo properly deprecate in favor of bp_member_activity_feed_link().
+ *
+ */
+function bp_activities_member_rss_link() { echo bp_get_member_activity_feed_link(); }
+
+	/**
+	 * Return the member activity feed link.
+	 *
+	 * @since 1.2.0
+	 *
+	 *
+	 * @return string $link The member activity feed link.
+	 */
+	function bp_get_member_activity_feed_link() {
+
+		// Single member activity feed link.
+		if ( bp_is_profile_component() || bp_is_current_action( 'just-me' ) ) {
+			$link = bp_displayed_user_domain() . bp_get_activity_slug() . '/feed/';
+
+		// Friend feed link.
+		} elseif ( bp_is_active( 'friends' ) && bp_is_current_action( bp_get_friends_slug() ) ) {
+			$link = bp_displayed_user_domain() . bp_get_activity_slug() . '/' . bp_get_friends_slug() . '/feed/';
+
+		// Group feed link.
+		} elseif ( bp_is_active( 'groups'  ) && bp_is_current_action( bp_get_groups_slug()  ) ) {
+			$link = bp_displayed_user_domain() . bp_get_activity_slug() . '/' . bp_get_groups_slug() . '/feed/';
+
+		// Favorites activity feed link.
+		} elseif ( 'favorites' === bp_current_action() ) {
+			$link = bp_displayed_user_domain() . bp_get_activity_slug() . '/favorites/feed/';
+
+		// Mentions activity feed link.
+		} elseif ( ( 'mentions' === bp_current_action() ) && bp_activity_do_mentions() ) {
+			$link = bp_displayed_user_domain() . bp_get_activity_slug() . '/mentions/feed/';
+
+		// No feed link.
+		} else {
+			$link = '';
+		}
+
+		/**
+		 * Filters the member activity feed link.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $link URL for the member activity feed.
+		 */
+		return apply_filters( 'bp_get_activities_member_rss_link', $link );
+	}
+
+	/**
+	 * Return the member activity feed link.
+	 *
+	 * @since 1.0.0
+	 * @deprecated 1.2.0
+	 *
+	 * @todo properly deprecate in favor of bp_get_member_activity_feed_link().
+	 *
+	 *
+	 * @return string The member activity feed link.
+	 */
+	function bp_get_activities_member_rss_link() { return bp_get_member_activity_feed_link(); }
+
+
+/** Template tags for RSS feed output ****************************************/
+
+/**
+ * Outputs the activity feed item guid.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_feed_item_guid() {
+	echo bp_get_activity_feed_item_guid();
+}
+
+	/**
+	 * Returns the activity feed item guid.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity feed item guid.
+	 */
+	function bp_get_activity_feed_item_guid() {
+		global $activities_template;
+
+		/**
+		 * Filters the activity feed item guid.
+		 *
+		 * @since 1.1.3
+		 *
+		 * @param string $value Calculated md5 value for the activity feed item.
+		 */
+		return apply_filters( 'bp_get_activity_feed_item_guid', md5( $activities_template->activity->date_recorded . '-' . $activities_template->activity->content ) );
+	}
+
+/**
+ * Output the activity feed item title.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_feed_item_title() {
+	echo bp_get_activity_feed_item_title();
+}
+
+	/**
+	 * Return the activity feed item title.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string $title The activity feed item title.
+	 */
+	function bp_get_activity_feed_item_title() {
+		global $activities_template;
+
+		if ( !empty( $activities_template->activity->action ) ) {
+			$content = $activities_template->activity->action;
+		} else {
+			$content = $activities_template->activity->content;
+		}
+
+		$content = explode( '<span', $content );
+		$title   = strip_tags( ent2ncr( trim( convert_chars( $content[0] ) ) ) );
+
+		if ( ':' === substr( $title, -1 ) ) {
+			$title = substr( $title, 0, -1 );
+		}
+
+		if ( 'activity_update' === $activities_template->activity->type ) {
+			$title .= ': ' . strip_tags( ent2ncr( trim( convert_chars( bp_create_excerpt( $activities_template->activity->content, 70, array( 'ending' => " [&#133;]" ) ) ) ) ) );
+		}
+
+		/**
+		 * Filters the activity feed item title.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $title The title for the activity feed item.
+		 */
+		return apply_filters( 'bp_get_activity_feed_item_title', $title );
+	}
+
+/**
+ * Output the activity feed item link.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_feed_item_link() {
+	echo bp_get_activity_feed_item_link();
+}
+
+	/**
+	 * Return the activity feed item link.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity feed item link.
+	 */
+	function bp_get_activity_feed_item_link() {
+		global $activities_template;
+
+		$retval = ! empty( $activities_template->activity->primary_link )
+			? $activities_template->activity->primary_link
+			: '';
+
+		/**
+		 * Filters the activity feed item link.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $retval The URL for the activity feed item.
+		 */
+		return apply_filters( 'bp_get_activity_feed_item_link', $retval );
+	}
+
+/**
+ * Output the activity feed item date.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_feed_item_date() {
+	echo bp_get_activity_feed_item_date();
+}
+
+	/**
+	 * Return the activity feed item date.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity feed item date.
+	 */
+	function bp_get_activity_feed_item_date() {
+		global $activities_template;
+
+		$retval = ! empty( $activities_template->activity->date_recorded )
+			? $activities_template->activity->date_recorded
+			: '';
+
+		/**
+		 * Filters the activity feed item date.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $retval The date for the activity feed item.
+		 */
+		return apply_filters( 'bp_get_activity_feed_item_date', $retval );
+	}
+
+/**
+ * Output the activity feed item description.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_activity_feed_item_description() {
+	echo bp_get_activity_feed_item_description();
+}
+
+	/**
+	 * Return the activity feed item description.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $activities_template {@link BP_Activity_Template}
+	 *
+	 * @return string The activity feed item description.
+	 */
+	function bp_get_activity_feed_item_description() {
+		global $activities_template;
+
+		// Get the content, if exists.
+		$content = ! empty( $activities_template->activity->content )
+			? $activities_template->activity->content
+			: '';
+
+		// Perform a few string conversions on the content, if it's not empty.
+		if ( ! empty( $content ) ) {
+			$content = ent2ncr( convert_chars( str_replace( '%s', '', $content ) ) );
+		}
+
+		/**
+		 * Filters the activity feed item description.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $content The description for the activity feed item.
+		 */
+		return apply_filters( 'bp_get_activity_feed_item_description', $content );
+	}
+
+/**
+ * Template tag so we can hook activity feed to <head>.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_sitewide_feed() {
+?>
+
+	<link rel="alternate" type="application/rss+xml" title="<?php bloginfo( 'name' ) ?> | <?php _e( 'Site Wide Activity RSS Feed', 'buddypress' ) ?>" href="<?php bp_sitewide_activity_feed_link() ?>" />
+
+<?php
+}
+add_action( 'bp_head', 'bp_activity_sitewide_feed' );
+
+/**
+ * Display available filters depending on the scope.
+ *
+ * @since 2.1.0
+ *
+ * @param string $context The current context. 'activity', 'member',
+ *                        'member_groups', 'group'.
+ */
+function bp_activity_show_filters( $context = '' ) {
+	echo bp_get_activity_show_filters( $context );
+}
+	/**
+	 * Get available filters depending on the scope.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param string $context The current context. 'activity', 'member',
+	 *                        'member_groups', 'group'.
+	 *
+	 * @return string HTML for <option> values.
+	 */
+	function bp_get_activity_show_filters( $context = '' ) {
+		$filters = array();
+		$actions = bp_activity_get_actions_for_context( $context );
+		foreach ( $actions as $action ) {
+			// Friends activity collapses two filters into one.
+			if ( in_array( $action['key'], array( 'friendship_accepted', 'friendship_created' ) ) ) {
+				$action['key'] = 'friendship_accepted,friendship_created';
+			}
+
+			$filters[ $action['key'] ] = $action['label'];
+		}
+
+		/**
+		 * Filters the options available in the activity filter dropdown.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param array  $filters Array of filter options for the given context, in the following format: $option_value => $option_name.
+		 * @param string $context Context for the filter. 'activity', 'member', 'member_groups', 'group'.
+		 */
+		$filters = apply_filters( 'bp_get_activity_show_filters_options', $filters, $context );
+
+		// Build the options output.
+		$output = '';
+
+		if ( ! empty( $filters ) ) {
+			foreach ( $filters as $value => $filter ) {
+				$output .= '<option value="' . esc_attr( $value ) . '">' . esc_html( $filter ) . '</option>' . "\n";
+			}
+		}
+
+		/**
+		 * Filters the HTML markup result for the activity filter dropdown.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $output  HTML output for the activity filter dropdown.
+		 * @param array  $filters Array of filter options for the given context, in the following format: $option_value => $option_name.
+		 * @param string $context Context for the filter. 'activity', 'member', 'member_groups', 'group'.
+		 */
+		return apply_filters( 'bp_get_activity_show_filters', $output, $filters, $context );
+	}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-activity.php	(revision 12265)
@@ -0,0 +1,1902 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @subpackage Activity
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Database interaction class for the BuddyPress activity component.
+ * Instance methods are available for creating/editing an activity,
+ * static methods for querying activities.
+ *
+ * @since 1.0.0
+ */
+class BP_Activity_Activity {
+
+	/** Properties ************************************************************/
+
+	/**
+	 * ID of the activity item.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	var $id;
+
+	/**
+	 * ID of the associated item.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	var $item_id;
+
+	/**
+	 * ID of the associated secondary item.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	var $secondary_item_id;
+
+	/**
+	 * ID of user associated with the activity item.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	var $user_id;
+
+	/**
+	 * The primary URL for the activity in RSS feeds.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	var $primary_link;
+
+	/**
+	 * BuddyPress component the activity item relates to.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	var $component;
+
+	/**
+	 * Activity type, eg 'new_blog_post'.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	var $type;
+
+	/**
+	 * Description of the activity, eg 'Alex updated his profile.'.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	var $action;
+
+	/**
+	 * The content of the activity item.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	var $content;
+
+	/**
+	 * The date the activity item was recorded, in 'Y-m-d h:i:s' format.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	var $date_recorded;
+
+	/**
+	 * Whether the item should be hidden in sitewide streams.
+	 *
+	 * @since 1.1.0
+	 * @var int
+	 */
+	var $hide_sitewide = 0;
+
+	/**
+	 * Node boundary start for activity or activity comment.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	var $mptt_left;
+
+	/**
+	 * Node boundary end for activity or activity comment.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	var $mptt_right;
+
+	/**
+	 * Whether this item is marked as spam.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $is_spam;
+
+	/**
+	 * Error holder.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var WP_Error
+	 */
+	public $errors;
+
+	/**
+	 * Error type to return. Either 'bool' or 'wp_error'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var string
+	 */
+	public $error_type = 'bool';
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|bool $id Optional. The ID of a specific activity item.
+	 */
+	public function __construct( $id = false ) {
+		// Instantiate errors object.
+		$this->errors = new WP_Error;
+
+		if ( !empty( $id ) ) {
+			$this->id = (int) $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Populate the object with data about the specific activity item.
+	 *
+	 * @since 1.0.0
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$row = wp_cache_get( $this->id, 'bp_activity' );
+
+		if ( false === $row ) {
+			$bp  = buddypress();
+			$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->activity->table_name} WHERE id = %d", $this->id ) );
+
+			wp_cache_set( $this->id, $row, 'bp_activity' );
+		}
+
+		if ( empty( $row ) ) {
+			$this->id = 0;
+			return;
+		}
+
+		$this->id                = (int) $row->id;
+		$this->item_id           = (int) $row->item_id;
+		$this->secondary_item_id = (int) $row->secondary_item_id;
+		$this->user_id           = (int) $row->user_id;
+		$this->primary_link      = $row->primary_link;
+		$this->component         = $row->component;
+		$this->type              = $row->type;
+		$this->action            = $row->action;
+		$this->content           = $row->content;
+		$this->date_recorded     = $row->date_recorded;
+		$this->hide_sitewide     = (int) $row->hide_sitewide;
+		$this->mptt_left         = (int) $row->mptt_left;
+		$this->mptt_right        = (int) $row->mptt_right;
+		$this->is_spam           = (int) $row->is_spam;
+
+		// Generate dynamic 'action' when possible.
+		$action = bp_activity_generate_action_string( $this );
+		if ( false !== $action ) {
+			$this->action = $action;
+
+			// If no callback is available, use the literal string from
+			// the database row.
+		} elseif ( ! empty( $row->action ) ) {
+			$this->action = $row->action;
+
+			// Provide a fallback to avoid PHP notices.
+		} else {
+			$this->action = '';
+		}
+	}
+
+	/**
+	 * Save the activity item to the database.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return WP_Error|bool True on success.
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->id                = apply_filters_ref_array( 'bp_activity_id_before_save',                array( $this->id,                &$this ) );
+		$this->item_id           = apply_filters_ref_array( 'bp_activity_item_id_before_save',           array( $this->item_id,           &$this ) );
+		$this->secondary_item_id = apply_filters_ref_array( 'bp_activity_secondary_item_id_before_save', array( $this->secondary_item_id, &$this ) );
+		$this->user_id           = apply_filters_ref_array( 'bp_activity_user_id_before_save',           array( $this->user_id,           &$this ) );
+		$this->primary_link      = apply_filters_ref_array( 'bp_activity_primary_link_before_save',      array( $this->primary_link,      &$this ) );
+		$this->component         = apply_filters_ref_array( 'bp_activity_component_before_save',         array( $this->component,         &$this ) );
+		$this->type              = apply_filters_ref_array( 'bp_activity_type_before_save',              array( $this->type,              &$this ) );
+		$this->action            = apply_filters_ref_array( 'bp_activity_action_before_save',            array( $this->action,            &$this ) );
+		$this->content           = apply_filters_ref_array( 'bp_activity_content_before_save',           array( $this->content,           &$this ) );
+		$this->date_recorded     = apply_filters_ref_array( 'bp_activity_date_recorded_before_save',     array( $this->date_recorded,     &$this ) );
+		$this->hide_sitewide     = apply_filters_ref_array( 'bp_activity_hide_sitewide_before_save',     array( $this->hide_sitewide,     &$this ) );
+		$this->mptt_left         = apply_filters_ref_array( 'bp_activity_mptt_left_before_save',         array( $this->mptt_left,         &$this ) );
+		$this->mptt_right        = apply_filters_ref_array( 'bp_activity_mptt_right_before_save',        array( $this->mptt_right,        &$this ) );
+		$this->is_spam           = apply_filters_ref_array( 'bp_activity_is_spam_before_save',           array( $this->is_spam,           &$this ) );
+
+		/**
+		 * Fires before the current activity item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Activity_Activity $this Current instance of the activity item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_activity_before_save', array( &$this ) );
+
+		if ( 'wp_error' === $this->error_type && $this->errors->get_error_code() ) {
+			return $this->errors;
+		}
+
+		if ( empty( $this->component ) || empty( $this->type ) ) {
+			if ( 'bool' === $this->error_type ) {
+				return false;
+			} else {
+				if ( empty( $this->component ) ) {
+					$this->errors->add( 'bp_activity_missing_component' );
+				} else {
+					$this->errors->add( 'bp_activity_missing_type' );
+				}
+
+				return $this->errors;
+			}
+		}
+
+		if ( empty( $this->primary_link ) ) {
+			$this->primary_link = bp_loggedin_user_domain();
+		}
+
+		// If we have an existing ID, update the activity item, otherwise insert it.
+		if ( ! empty( $this->id ) ) {
+			$q = $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET user_id = %d, component = %s, type = %s, action = %s, content = %s, primary_link = %s, date_recorded = %s, item_id = %d, secondary_item_id = %d, hide_sitewide = %d, is_spam = %d WHERE id = %d", $this->user_id, $this->component, $this->type, $this->action, $this->content, $this->primary_link, $this->date_recorded, $this->item_id, $this->secondary_item_id, $this->hide_sitewide, $this->is_spam, $this->id );
+		} else {
+			$q = $wpdb->prepare( "INSERT INTO {$bp->activity->table_name} ( user_id, component, type, action, content, primary_link, date_recorded, item_id, secondary_item_id, hide_sitewide, is_spam ) VALUES ( %d, %s, %s, %s, %s, %s, %s, %d, %d, %d, %d )", $this->user_id, $this->component, $this->type, $this->action, $this->content, $this->primary_link, $this->date_recorded, $this->item_id, $this->secondary_item_id, $this->hide_sitewide, $this->is_spam );
+		}
+
+		if ( false === $wpdb->query( $q ) ) {
+			return false;
+		}
+
+		// If this is a new activity item, set the $id property.
+		if ( empty( $this->id ) ) {
+			$this->id = $wpdb->insert_id;
+
+			// If an existing activity item, prevent any changes to the content generating new @mention notifications.
+		} else {
+			add_filter( 'bp_activity_at_name_do_notifications', '__return_false' );
+		}
+
+		/**
+		 * Fires after an activity item has been saved to the database.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Activity_Activity $this Current instance of activity item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_activity_after_save', array( &$this ) );
+
+		return true;
+	}
+
+	/** Static Methods ***************************************************/
+
+	/**
+	 * Get activity items, as specified by parameters.
+	 *
+	 * @since 1.2.0
+	 * @since 2.4.0 Introduced the `$fields` parameter.
+	 * @since 2.9.0 Introduced the `$order_by` parameter.
+	 *
+	 * @see BP_Activity_Activity::get_filter_sql() for a description of the
+	 *      'filter' parameter.
+	 * @see WP_Meta_Query::queries for a description of the 'meta_query'
+	 *      parameter format.
+	 *
+	 * @param array $args {
+	 *     An array of arguments. All items are optional.
+	 *     @type int          $page              Which page of results to fetch. Using page=1 without per_page will result
+	 *                                           in no pagination. Default: 1.
+	 *     @type int|bool     $per_page          Number of results per page. Default: 25.
+	 *     @type int|bool     $max               Maximum number of results to return. Default: false (unlimited).
+	 *     @type string       $fields            Activity fields to return. Pass 'ids' to get only the activity IDs.
+	 *                                           'all' returns full activity objects.
+	 *     @type string       $sort              ASC or DESC. Default: 'DESC'.
+	 *     @type string       $order_by          Column to order results by.
+	 *     @type array        $exclude           Array of activity IDs to exclude. Default: false.
+	 *     @type array        $in                Array of ids to limit query by (IN). Default: false.
+	 *     @type array        $meta_query        Array of meta_query conditions. See WP_Meta_Query::queries.
+	 *     @type array        $date_query        Array of date_query conditions. See first parameter of
+	 *                                           WP_Date_Query::__construct().
+	 *     @type array        $filter_query      Array of advanced query conditions. See BP_Activity_Query::__construct().
+	 *     @type string|array $scope             Pre-determined set of activity arguments.
+	 *     @type array        $filter            See BP_Activity_Activity::get_filter_sql().
+	 *     @type string       $search_terms      Limit results by a search term. Default: false.
+	 *     @type bool         $display_comments  Whether to include activity comments. Default: false.
+	 *     @type bool         $show_hidden       Whether to show items marked hide_sitewide. Default: false.
+	 *     @type string       $spam              Spam status. Default: 'ham_only'.
+	 *     @type bool         $update_meta_cache Whether to pre-fetch metadata for queried activity items. Default: true.
+	 *     @type string|bool  $count_total       If true, an additional DB query is run to count the total activity items
+	 *                                           for the query. Default: false.
+	 * }
+	 * @return array The array returned has two keys:
+	 *               - 'total' is the count of located activities
+	 *               - 'activities' is an array of the located activities
+	 */
+	public static function get( $args = array() ) {
+		global $wpdb;
+
+		// Backward compatibility with old method of passing arguments.
+		if ( !is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '1.6', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'max',
+				1 => 'page',
+				2 => 'per_page',
+				3 => 'sort',
+				4 => 'search_terms',
+				5 => 'filter',
+				6 => 'display_comments',
+				7 => 'show_hidden',
+				8 => 'exclude',
+				9 => 'in',
+				10 => 'spam'
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$bp = buddypress();
+		$r  = wp_parse_args( $args, array(
+			'page'              => 1,               // The current page.
+			'per_page'          => 25,              // Activity items per page.
+			'max'               => false,           // Max number of items to return.
+			'fields'            => 'all',           // Fields to include.
+			'sort'              => 'DESC',          // ASC or DESC.
+			'order_by'          => 'date_recorded', // Column to order by.
+			'exclude'           => false,           // Array of ids to exclude.
+			'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.
+			'spam'              => 'ham_only',      // Spam status.
+			'update_meta_cache' => true,            // Whether or not to update meta cache.
+			'count_total'       => false,           // Whether or not to use count_total.
+		) );
+
+		// Select conditions.
+		$select_sql = "SELECT DISTINCT a.id";
+
+		$from_sql   = " FROM {$bp->activity->table_name} a";
+
+		$join_sql   = '';
+
+		// Where conditions.
+		$where_conditions = array();
+
+		// Excluded types.
+		$excluded_types = array();
+
+		// Scope takes precedence.
+		if ( ! empty( $r['scope'] ) ) {
+			$scope_query = self::get_scope_query_sql( $r['scope'], $r );
+
+			// Add our SQL conditions if matches were found.
+			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'] );
+			$sql          = $filter_query->get_sql();
+			if ( ! empty( $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' == $r['spam'] ) {
+			$where_conditions['spam_sql'] = 'a.is_spam = 0';
+		} elseif ( 'spam_only' == $r['spam'] ) {
+			$where_conditions['spam_sql'] = 'a.is_spam = 1';
+		}
+
+		// Searching.
+		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 );
+
+			/**
+			 * Filters whether or not to include users for search parameters.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param bool $value Whether or not to include user search. Default false.
+			 */
+			if ( apply_filters( 'bp_activity_get_include_user_search', false ) ) {
+				$user_search = get_user_by( 'slug', $r['search_terms'] );
+				if ( false !== $user_search ) {
+					$user_id                         = $user_search->ID;
+					$where_conditions['search_sql'] .= $wpdb->prepare( ' OR a.user_id = %d', $user_id );
+				}
+			}
+		}
+
+		// Sorting.
+		$sort = $r['sort'];
+		if ( $sort != 'ASC' && $sort != 'DESC' ) {
+			$sort = 'DESC';
+		}
+
+		switch( $r['order_by'] ) {
+			case 'id' :
+			case 'user_id' :
+			case 'component' :
+			case 'type' :
+			case 'action' :
+			case 'content' :
+			case 'primary_link' :
+			case 'item_id' :
+			case 'secondary_item_id' :
+			case 'date_recorded' :
+			case 'hide_sitewide' :
+			case 'mptt_left' :
+			case 'mptt_right' :
+			case 'is_spam' :
+				break;
+
+			default :
+				$r['order_by'] = 'date_recorded';
+				break;
+		}
+		$order_by = 'a.' . $r['order_by'];
+
+		// Hide Hidden Items?
+		if ( ! $r['show_hidden'] ) {
+			$where_conditions['hidden_sql'] = "a.hide_sitewide = 0";
+		}
+
+		// Exclude specified items.
+		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( $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( $r['meta_query'] );
+
+		if ( ! empty( $meta_query_sql['join'] ) ) {
+			$join_sql .= $meta_query_sql['join'];
+		}
+
+		if ( ! empty( $meta_query_sql['where'] ) ) {
+			$where_conditions[] = $meta_query_sql['where'];
+		}
+
+		// Process date_query into SQL.
+		$date_query_sql = self::get_date_query_sql( $r['date_query'] );
+
+		if ( ! empty( $date_query_sql ) ) {
+			$where_conditions['date'] = $date_query_sql;
+		}
+
+		// 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 === $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( $r['filter']['object'] ) ) {
+			$excluded_types[] = 'last_activity';
+		}
+
+		// Build the excluded type sql part.
+		if ( ! empty( $excluded_types ) ) {
+			$not_in = "'" . implode( "', '", esc_sql( $excluded_types ) ) . "'";
+			$where_conditions['excluded_types'] = "a.type NOT IN ({$not_in})";
+		}
+
+		/**
+		 * Filters the MySQL WHERE conditions for the Activity items get method.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param array  $where_conditions Current conditions for MySQL WHERE statement.
+		 * @param array  $r                Parsed arguments passed into method.
+		 * @param string $select_sql       Current SELECT MySQL statement at point of execution.
+		 * @param string $from_sql         Current FROM MySQL statement at point of execution.
+		 * @param string $join_sql         Current INNER JOIN MySQL statement at point of execution.
+		 */
+		$where_conditions = apply_filters( 'bp_activity_get_where_conditions', $where_conditions, $r, $select_sql, $from_sql, $join_sql );
+
+		// Join the where conditions together.
+		$where_sql = 'WHERE ' . join( ' AND ', $where_conditions );
+
+		/**
+		 * Filter the MySQL JOIN clause for the main activity query.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $join_sql   JOIN clause.
+		 * @param array  $r          Method parameters.
+		 * @param string $select_sql Current SELECT MySQL statement.
+		 * @param string $from_sql   Current FROM MySQL statement.
+		 * @param string $where_sql  Current WHERE MySQL statement.
+		 */
+		$join_sql = apply_filters( 'bp_activity_get_join_sql', $join_sql, $r, $select_sql, $from_sql, $where_sql );
+
+		// Sanitize page and per_page parameters.
+		$page     = absint( $r['page']     );
+		$per_page = absint( $r['per_page'] );
+
+		$retval = array(
+			'activities'     => null,
+			'total'          => null,
+			'has_more_items' => null,
+		);
+
+		/**
+		 * Filters if BuddyPress should use legacy query structure over current structure for version 2.0+.
+		 *
+		 * It is not recommended to use the legacy structure, but allowed to if needed.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param bool                 $value Whether to use legacy structure or not.
+		 * @param BP_Activity_Activity $value Current method being called.
+		 * @param array                $r     Parsed arguments passed into method.
+		 */
+		if ( apply_filters( 'bp_use_legacy_activity_query', false, __METHOD__, $r ) ) {
+
+			// Legacy queries joined against the user table.
+			$select_sql = "SELECT DISTINCT a.*, u.user_email, u.user_nicename, u.user_login, u.display_name";
+			$from_sql   = " FROM {$bp->activity->table_name} a LEFT JOIN {$wpdb->users} u ON a.user_id = u.ID";
+
+			if ( ! empty( $page ) && ! empty( $per_page ) ) {
+				$pag_sql = $wpdb->prepare( "LIMIT %d, %d", absint( ( $page - 1 ) * $per_page ), $per_page );
+
+				/** This filter is documented in bp-activity/bp-activity-classes.php */
+				$activity_sql = apply_filters( 'bp_activity_get_user_join_filter', "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY a.date_recorded {$sort}, a.id {$sort} {$pag_sql}", $select_sql, $from_sql, $where_sql, $sort, $pag_sql );
+			} else {
+				$pag_sql = '';
+
+				/**
+				 * Filters the legacy MySQL query statement so plugins can alter before results are fetched.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string $value      Concatenated MySQL statement pieces to be query results with for legacy query.
+				 * @param string $select_sql Final SELECT MySQL statement portion for legacy query.
+				 * @param string $from_sql   Final FROM MySQL statement portion for legacy query.
+				 * @param string $where_sql  Final WHERE MySQL statement portion for legacy query.
+				 * @param string $sort       Final sort direction for legacy query.
+				 */
+				$activity_sql = apply_filters( 'bp_activity_get_user_join_filter', "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY a.date_recorded {$sort}, a.id {$sort}", $select_sql, $from_sql, $where_sql, $sort, $pag_sql );
+			}
+
+			/*
+			 * Queries that include 'last_activity' are cached separately,
+			 * since they are generally much less long-lived.
+			 */
+			if ( preg_match( '/a\.type NOT IN \([^\)]*\'last_activity\'[^\)]*\)/', $activity_sql ) ) {
+				$cache_group = 'bp_activity';
+			} else {
+				$cache_group = 'bp_activity_with_last_activity';
+			}
+
+			$activities = $wpdb->get_results( $activity_sql );
+
+			// Integer casting for legacy activity query.
+			foreach ( (array) $activities as $i => $ac ) {
+				$activities[ $i ]->id                = (int) $ac->id;
+				$activities[ $i ]->item_id           = (int) $ac->item_id;
+				$activities[ $i ]->secondary_item_id = (int) $ac->secondary_item_id;
+				$activities[ $i ]->user_id           = (int) $ac->user_id;
+				$activities[ $i ]->hide_sitewide     = (int) $ac->hide_sitewide;
+				$activities[ $i ]->mptt_left         = (int) $ac->mptt_left;
+				$activities[ $i ]->mptt_right        = (int) $ac->mptt_right;
+				$activities[ $i ]->is_spam           = (int) $ac->is_spam;
+			}
+
+		} else {
+			// Query first for activity IDs.
+			$activity_ids_sql = "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY {$order_by} {$sort}, a.id {$sort}";
+
+			if ( ! empty( $per_page ) && ! empty( $page ) ) {
+				// We query for $per_page + 1 items in order to
+				// populate the has_more_items flag.
+				$activity_ids_sql .= $wpdb->prepare( " LIMIT %d, %d", absint( ( $page - 1 ) * $per_page ), $per_page + 1 );
+			}
+
+			/**
+			 * Filters the paged activities MySQL statement.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param string $activity_ids_sql MySQL statement used to query for Activity IDs.
+			 * @param array  $r                Array of arguments passed into method.
+			 */
+			$activity_ids_sql = apply_filters( 'bp_activity_paged_activities_sql', $activity_ids_sql, $r );
+
+			/*
+			 * Queries that include 'last_activity' are cached separately,
+			 * since they are generally much less long-lived.
+			 */
+			if ( preg_match( '/a\.type NOT IN \([^\)]*\'last_activity\'[^\)]*\)/', $activity_ids_sql ) ) {
+				$cache_group = 'bp_activity';
+			} else {
+				$cache_group = 'bp_activity_with_last_activity';
+			}
+
+			$cached = bp_core_get_incremented_cache( $activity_ids_sql, $cache_group );
+			if ( false === $cached ) {
+				$activity_ids = $wpdb->get_col( $activity_ids_sql );
+				bp_core_set_incremented_cache( $activity_ids_sql, $cache_group, $activity_ids );
+			} else {
+				$activity_ids = $cached;
+			}
+
+			$retval['has_more_items'] = ! empty( $per_page ) && count( $activity_ids ) > $per_page;
+
+			// If we've fetched more than the $per_page value, we
+			// can discard the extra now.
+			if ( ! empty( $per_page ) && count( $activity_ids ) === $per_page + 1 ) {
+				array_pop( $activity_ids );
+			}
+
+			if ( 'ids' === $r['fields'] ) {
+				$activities = array_map( 'intval', $activity_ids );
+			} else {
+				$activities = self::get_activity_data( $activity_ids );
+			}
+		}
+
+		if ( 'ids' !== $r['fields'] ) {
+			// Get the fullnames of users so we don't have to query in the loop.
+			$activities = self::append_user_fullnames( $activities );
+
+			// Get activity meta.
+			$activity_ids = array();
+			foreach ( (array) $activities as $activity ) {
+				$activity_ids[] = $activity->id;
+			}
+
+			if ( ! empty( $activity_ids ) && $r['update_meta_cache'] ) {
+				bp_activity_update_meta_cache( $activity_ids );
+			}
+
+			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 );
+
+			// Generate action strings.
+			$activities = BP_Activity_Activity::generate_action_strings( $activities );
+		}
+
+		$retval['activities'] = $activities;
+
+		// If $max is set, only return up to the max results.
+		if ( ! empty( $r['count_total'] ) ) {
+
+			/**
+			 * Filters the total activities MySQL statement.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $value     MySQL statement used to query for total activities.
+			 * @param string $where_sql MySQL WHERE statement portion.
+			 * @param string $sort      Sort direction for query.
+			 */
+			$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 );
+			$cached = bp_core_get_incremented_cache( $total_activities_sql, $cache_group );
+			if ( false === $cached ) {
+				$total_activities = $wpdb->get_var( $total_activities_sql );
+				bp_core_set_incremented_cache( $total_activities_sql, $cache_group, $total_activities );
+			} else {
+				$total_activities = $cached;
+			}
+
+			if ( !empty( $r['max'] ) ) {
+				if ( (int) $total_activities > (int) $r['max'] ) {
+					$total_activities = $r['max'];
+				}
+			}
+
+			$retval['total'] = $total_activities;
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Convert activity IDs to activity objects, as expected in template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $activity_ids Array of activity IDs.
+	 * @return array
+	 */
+	protected static function get_activity_data( $activity_ids = array() ) {
+		global $wpdb;
+
+		// Bail if no activity ID's passed.
+		if ( empty( $activity_ids ) ) {
+			return array();
+		}
+
+		// Get BuddyPress.
+		$bp = buddypress();
+
+		$activities   = array();
+		$uncached_ids = bp_get_non_cached_ids( $activity_ids, 'bp_activity' );
+
+		// Prime caches as necessary.
+		if ( ! empty( $uncached_ids ) ) {
+			// Format the activity ID's for use in the query below.
+			$uncached_ids_sql = implode( ',', wp_parse_id_list( $uncached_ids ) );
+
+			// Fetch data from activity table, preserving order.
+			$queried_adata = $wpdb->get_results( "SELECT * FROM {$bp->activity->table_name} WHERE id IN ({$uncached_ids_sql})");
+
+			// Put that data into the placeholders created earlier,
+			// and add it to the cache.
+			foreach ( (array) $queried_adata as $adata ) {
+				wp_cache_set( $adata->id, $adata, 'bp_activity' );
+			}
+		}
+
+		// Now fetch data from the cache.
+		foreach ( $activity_ids as $activity_id ) {
+			// Integer casting.
+			$activity = wp_cache_get( $activity_id, 'bp_activity' );
+			if ( ! empty( $activity ) ) {
+				$activity->id                = (int) $activity->id;
+				$activity->user_id           = (int) $activity->user_id;
+				$activity->item_id           = (int) $activity->item_id;
+				$activity->secondary_item_id = (int) $activity->secondary_item_id;
+				$activity->hide_sitewide     = (int) $activity->hide_sitewide;
+				$activity->mptt_left         = (int) $activity->mptt_left;
+				$activity->mptt_right        = (int) $activity->mptt_right;
+				$activity->is_spam           = (int) $activity->is_spam;
+			}
+
+			$activities[] = $activity;
+		}
+
+		// Then fetch user data.
+		$user_query = new BP_User_Query( array(
+			'user_ids'        => wp_list_pluck( $activities, 'user_id' ),
+			'populate_extras' => false,
+		) );
+
+		// Associated located user data with activity items.
+		foreach ( $activities as $a_index => $a_item ) {
+			$a_user_id = intval( $a_item->user_id );
+			$a_user    = isset( $user_query->results[ $a_user_id ] ) ? $user_query->results[ $a_user_id ] : '';
+
+			if ( !empty( $a_user ) ) {
+				$activities[ $a_index ]->user_email    = $a_user->user_email;
+				$activities[ $a_index ]->user_nicename = $a_user->user_nicename;
+				$activities[ $a_index ]->user_login    = $a_user->user_login;
+				$activities[ $a_index ]->display_name  = $a_user->display_name;
+			}
+		}
+
+		return $activities;
+	}
+
+	/**
+	 * Append xProfile fullnames to an activity array.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $activities Activities array.
+	 * @return array
+	 */
+	protected static function append_user_fullnames( $activities ) {
+
+		if ( bp_is_active( 'xprofile' ) && ! empty( $activities ) ) {
+			$activity_user_ids = wp_list_pluck( $activities, 'user_id' );
+
+			if ( ! empty( $activity_user_ids ) ) {
+				$fullnames = bp_core_get_user_displaynames( $activity_user_ids );
+				if ( ! empty( $fullnames ) ) {
+					foreach ( (array) $activities as $i => $activity ) {
+						if ( ! empty( $fullnames[ $activity->user_id ] ) ) {
+							$activities[ $i ]->user_fullname = $fullnames[ $activity->user_id ];
+						}
+					}
+				}
+			}
+		}
+
+		return $activities;
+	}
+
+	/**
+	 * Pre-fetch data for objects associated with activity items.
+	 *
+	 * Activity items are associated with users, and often with other
+	 * BuddyPress data objects. Here, we pre-fetch data about these
+	 * associated objects, so that inline lookups - done primarily when
+	 * building action strings - do not result in excess database queries.
+	 *
+	 * The only object data required for activity component activity types
+	 * (activity_update and activity_comment) is related to users, and that
+	 * info is fetched separately in BP_Activity_Activity::get_activity_data().
+	 * So this method contains nothing but a filter that allows other
+	 * components, such as bp-friends and bp-groups, to hook in and prime
+	 * their own caches at the beginning of an activity loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $activities Array of activities.
+	 * @return array $activities Array of activities.
+	 */
+	protected static function prefetch_object_data( $activities ) {
+
+		/**
+		 * Filters inside prefetch_object_data method to aid in pre-fetching object data associated with activity item.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $activities Array of activities.
+		 */
+		return apply_filters( 'bp_activity_prefetch_object_data', $activities );
+	}
+
+	/**
+	 * Generate action strings for the activities located in BP_Activity_Activity::get().
+	 *
+	 * If no string can be dynamically generated for a given item
+	 * (typically because the activity type has not been properly
+	 * registered), the static 'action' value pulled from the database will
+	 * be left in place.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $activities Array of activities.
+	 * @return array
+	 */
+	protected static function generate_action_strings( $activities ) {
+		foreach ( $activities as $key => $activity ) {
+			$generated_action = bp_activity_generate_action_string( $activity );
+			if ( false !== $generated_action ) {
+				$activity->action = $generated_action;
+			}
+
+			$activities[ $key ] = $activity;
+		}
+
+		return $activities;
+	}
+
+	/**
+	 * Get the SQL for the 'meta_query' param in BP_Activity_Activity::get().
+	 *
+	 * We use WP_Meta_Query to do the heavy lifting of parsing the
+	 * meta_query array and creating the necessary SQL clauses. However,
+	 * since BP_Activity_Activity::get() builds its SQL differently than
+	 * WP_Query, we have to alter the return value (stripping the leading
+	 * AND keyword from the 'where' clause).
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $meta_query An array of meta_query filters. See the
+	 *                          documentation for WP_Meta_Query for details.
+	 * @return array $sql_array 'join' and 'where' clauses.
+	 */
+	public static function get_meta_query_sql( $meta_query = array() ) {
+		global $wpdb;
+
+		$sql_array = array(
+			'join'  => '',
+			'where' => '',
+		);
+
+		if ( ! empty( $meta_query ) ) {
+			$activity_meta_query = new WP_Meta_Query( $meta_query );
+
+			// WP_Meta_Query expects the table name at
+			// $wpdb->activitymeta.
+			$wpdb->activitymeta = buddypress()->activity->table_name_meta;
+
+			$meta_sql = $activity_meta_query->get_sql( 'activity', 'a', 'id' );
+
+			// Strip the leading AND - BP handles it in get().
+			$sql_array['where'] = preg_replace( '/^\sAND/', '', $meta_sql['where'] );
+			$sql_array['join']  = $meta_sql['join'];
+		}
+
+		return $sql_array;
+	}
+
+	/**
+	 * Get the SQL for the 'date_query' param in BP_Activity_Activity::get().
+	 *
+	 * We use BP_Date_Query, which extends WP_Date_Query, to do the heavy lifting
+	 * of parsing the date_query array and creating the necessary SQL clauses.
+	 * However, since BP_Activity_Activity::get() builds its SQL differently than
+	 * WP_Query, we have to alter the return value (stripping the leading AND
+	 * keyword from the query).
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $date_query An array of date_query parameters. See the
+	 *                          documentation for the first parameter of WP_Date_Query.
+	 * @return string
+	 */
+	public static function get_date_query_sql( $date_query = array() ) {
+		$sql = '';
+
+		// Date query.
+		if ( ! empty( $date_query ) && is_array( $date_query ) ) {
+			$date_query = new BP_Date_Query( $date_query, 'date_recorded' );
+			$sql = preg_replace( '/^\sAND/', '', $date_query->get_sql() );
+		}
+
+		return $sql;
+	}
+
+	/**
+	 * 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 multiple scopes.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param  mixed $scope  The activity scope. Accepts string or array of scopes.
+	 * @param  array $r      Current activity arguments. Same as those of BP_Activity_Activity::get(),
+	 *                       but merged with defaults.
+	 * @return false|array 'sql' WHERE SQL string and 'override' activity args.
+	 */
+	public static function get_scope_query_sql( $scope = false, $r = array() ) {
+
+		// Define arrays for future use.
+		$query_args = array();
+		$override   = array();
+		$retval     = array();
+
+		// Check for array of scopes.
+		if ( is_array( $scope ) ) {
+			$scopes = $scope;
+
+			// Explode a comma separated string of scopes.
+		} elseif ( is_string( $scope ) ) {
+			$scopes = explode( ',', $scope );
+		}
+
+		// Bail if no scope passed.
+		if ( empty( $scopes ) ) {
+			return false;
+		}
+
+		// Helper to easily grab the 'user_id'.
+		if ( ! empty( $r['filter']['user_id'] ) ) {
+			$r['user_id'] = $r['filter']['user_id'];
+		}
+
+		// Parse each scope; yes! we handle multiples!
+		foreach ( $scopes as $scope ) {
+			$scope_args = array();
+
+			/**
+			 * 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 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 );
+
+			if ( ! empty( $scope_args ) ) {
+				// 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 );
+			$sql   = $query->get_sql();
+			if ( ! empty( $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.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @deprecated 1.5
+	 * @deprecated Use BP_Activity_Activity::get() with an 'in' parameter instead.
+	 *
+	 * @param mixed    $activity_ids     Array or comma-separated string of activity IDs to retrieve.
+	 * @param int|bool $max              Maximum number of results to return. (Optional; default is no maximum).
+	 * @param int      $page             The set of results that the user is viewing. Used in pagination. (Optional; default is 1).
+	 * @param int      $per_page         Specifies how many results per page. Used in pagination. (Optional; default is 25).
+	 * @param string   $sort             MySQL column sort; ASC or DESC. (Optional; default is DESC).
+	 * @param bool     $display_comments Retrieve an activity item's associated comments or not. (Optional; default is false).
+	 * @return array
+	 */
+	public static function get_specific( $activity_ids, $max = false, $page = 1, $per_page = 25, $sort = 'DESC', $display_comments = false ) {
+		_deprecated_function( __FUNCTION__, '1.5', 'Use BP_Activity_Activity::get() with the "in" parameter instead.' );
+		return BP_Activity_Activity::get( $max, $page, $per_page, $sort, false, false, $display_comments, false, false, $activity_ids );
+	}
+
+	/**
+	 * Get the first activity ID that matches a set of criteria.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @todo Should parameters be optional?
+	 *
+	 * @param int    $user_id           User ID to filter by.
+	 * @param string $component         Component to filter by.
+	 * @param string $type              Activity type to filter by.
+	 * @param int    $item_id           Associated item to filter by.
+	 * @param int    $secondary_item_id Secondary associated item to filter by.
+	 * @param string $action            Action to filter by.
+	 * @param string $content           Content to filter by.
+	 * @param string $date_recorded     Date to filter by.
+	 * @return int|false Activity ID on success, false if none is found.
+	 */
+	public static function get_id( $user_id, $component, $type, $item_id, $secondary_item_id, $action, $content, $date_recorded ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$where_args = false;
+
+		if ( ! empty( $user_id ) ) {
+			$where_args[] = $wpdb->prepare( "user_id = %d", $user_id );
+		}
+
+		if ( ! empty( $component ) ) {
+			$where_args[] = $wpdb->prepare( "component = %s", $component );
+		}
+
+		if ( ! empty( $type ) ) {
+			$where_args[] = $wpdb->prepare( "type = %s", $type );
+		}
+
+		if ( ! empty( $item_id ) ) {
+			$where_args[] = $wpdb->prepare( "item_id = %d", $item_id );
+		}
+
+		if ( ! empty( $secondary_item_id ) ) {
+			$where_args[] = $wpdb->prepare( "secondary_item_id = %d", $secondary_item_id );
+		}
+
+		if ( ! empty( $action ) ) {
+			$where_args[] = $wpdb->prepare( "action = %s", $action );
+		}
+
+		if ( ! empty( $content ) ) {
+			$where_args[] = $wpdb->prepare( "content = %s", $content );
+		}
+
+		if ( ! empty( $date_recorded ) ) {
+			$where_args[] = $wpdb->prepare( "date_recorded = %s", $date_recorded );
+		}
+
+		if ( ! empty( $where_args ) ) {
+			$where_sql = 'WHERE ' . join( ' AND ', $where_args );
+			$result = $wpdb->get_var( "SELECT id FROM {$bp->activity->table_name} {$where_sql}" );
+
+			return is_numeric( $result ) ? (int) $result : false;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Delete activity items from the database.
+	 *
+	 * To delete a specific activity item, pass an 'id' parameter.
+	 * Otherwise use the filters.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $args {
+	 *     @int    $id                Optional. The ID of a specific item to delete.
+	 *     @string $action            Optional. The action to filter by.
+	 *     @string $content           Optional. The content to filter by.
+	 *     @string $component         Optional. The component name to filter by.
+	 *     @string $type              Optional. The activity type to filter by.
+	 *     @string $primary_link      Optional. The primary URL to filter by.
+	 *     @int    $user_id           Optional. The user ID to filter by.
+	 *     @int    $item_id           Optional. The associated item ID to filter by.
+	 *     @int    $secondary_item_id Optional. The secondary associated item ID to filter by.
+	 *     @string $date_recorded     Optional. The date to filter by.
+	 *     @int    $hide_sitewide     Optional. Default: false.
+	 * }
+	 * @return array|bool An array of deleted activity IDs on success, false on failure.
+	 */
+	public static function delete( $args = array() ) {
+		global $wpdb;
+
+		$bp = buddypress();
+		$r  = wp_parse_args( $args, array(
+			'id'                => false,
+			'action'            => false,
+			'content'           => false,
+			'component'         => false,
+			'type'              => false,
+			'primary_link'      => false,
+			'user_id'           => false,
+			'item_id'           => false,
+			'secondary_item_id' => false,
+			'date_recorded'     => false,
+			'hide_sitewide'     => false
+		) );
+
+		// Setup empty array from where query arguments.
+		$where_args = array();
+
+		// ID.
+		if ( ! empty( $r['id'] ) ) {
+			$where_args[] = $wpdb->prepare( "id = %d", $r['id'] );
+		}
+
+		// User ID.
+		if ( ! empty( $r['user_id'] ) ) {
+			$where_args[] = $wpdb->prepare( "user_id = %d", $r['user_id'] );
+		}
+
+		// Action.
+		if ( ! empty( $r['action'] ) ) {
+			$where_args[] = $wpdb->prepare( "action = %s", $r['action'] );
+		}
+
+		// Content.
+		if ( ! empty( $r['content'] ) ) {
+			$where_args[] = $wpdb->prepare( "content = %s", $r['content'] );
+		}
+
+		// Component.
+		if ( ! empty( $r['component'] ) ) {
+			$where_args[] = $wpdb->prepare( "component = %s", $r['component'] );
+		}
+
+		// Type.
+		if ( ! empty( $r['type'] ) ) {
+			$where_args[] = $wpdb->prepare( "type = %s", $r['type'] );
+		}
+
+		// Primary Link.
+		if ( ! empty( $r['primary_link'] ) ) {
+			$where_args[] = $wpdb->prepare( "primary_link = %s", $r['primary_link'] );
+		}
+
+		// Item ID.
+		if ( ! empty( $r['item_id'] ) ) {
+			$where_args[] = $wpdb->prepare( "item_id = %d", $r['item_id'] );
+		}
+
+		// Secondary item ID.
+		if ( ! empty( $r['secondary_item_id'] ) ) {
+			$where_args[] = $wpdb->prepare( "secondary_item_id = %d", $r['secondary_item_id'] );
+		}
+
+		// Date Recorded.
+		if ( ! empty( $r['date_recorded'] ) ) {
+			$where_args[] = $wpdb->prepare( "date_recorded = %s", $r['date_recorded'] );
+		}
+
+		// Hidden sitewide.
+		if ( ! empty( $r['hide_sitewide'] ) ) {
+			$where_args[] = $wpdb->prepare( "hide_sitewide = %d", $r['hide_sitewide'] );
+		}
+
+		// Bail if no where arguments.
+		if ( empty( $where_args ) ) {
+			return false;
+		}
+
+		// Join the where arguments for querying.
+		$where_sql = 'WHERE ' . join( ' AND ', $where_args );
+
+		// Fetch all activities being deleted so we can perform more actions.
+		$activities = $wpdb->get_results( "SELECT * FROM {$bp->activity->table_name} {$where_sql}" );
+
+		/**
+		 * Action to allow intercepting activity items to be deleted.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array $activities Array of activities.
+		 * @param array $r          Array of parsed arguments.
+		 */
+		do_action_ref_array( 'bp_activity_before_delete', array( $activities, $r ) );
+
+		// Attempt to delete activities from the database.
+		$deleted = $wpdb->query( "DELETE FROM {$bp->activity->table_name} {$where_sql}" );
+
+		// Bail if nothing was deleted.
+		if ( empty( $deleted ) ) {
+			return false;
+		}
+
+		/**
+		 * Action to allow intercepting activity items just deleted.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array $activities Array of activities.
+		 * @param array $r          Array of parsed arguments.
+		 */
+		do_action_ref_array( 'bp_activity_after_delete', array( $activities, $r ) );
+
+		// Pluck the activity IDs out of the $activities array.
+		$activity_ids = wp_parse_id_list( wp_list_pluck( $activities, 'id' ) );
+
+		// Handle accompanying activity comments and meta deletion.
+		if ( ! empty( $activity_ids ) ) {
+
+			// Delete all activity meta entries for activity items.
+			BP_Activity_Activity::delete_activity_meta_entries( $activity_ids );
+
+			// Setup empty array for comments.
+			$comment_ids = array();
+
+			// Loop through activity ids and attempt to delete comments.
+			foreach ( $activity_ids as $activity_id ) {
+
+				// Attempt to delete comments.
+				$comments = BP_Activity_Activity::delete( array(
+					'type'    => 'activity_comment',
+					'item_id' => $activity_id
+				) );
+
+				// Merge IDs together.
+				if ( ! empty( $comments ) ) {
+					$comment_ids = array_merge( $comment_ids, $comments );
+				}
+			}
+
+			// Merge activity IDs with any deleted comment IDs.
+			if ( ! empty( $comment_ids ) ) {
+				$activity_ids = array_unique( array_merge( $activity_ids, $comment_ids ) );
+			}
+		}
+
+		return $activity_ids;
+	}
+
+	/**
+	 * Delete the comments associated with a set of activity items.
+	 *
+	 * This method is no longer used by BuddyPress, and it is recommended not to
+	 * use it going forward, and use BP_Activity_Activity::delete() instead.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @deprecated 2.3.0
+	 *
+	 * @param array $activity_ids Activity IDs whose comments should be deleted.
+	 * @param bool  $delete_meta  Should we delete the activity meta items for these comments.
+	 * @return bool True on success.
+	 */
+	public static function delete_activity_item_comments( $activity_ids = array(), $delete_meta = true ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$delete_meta  = (bool) $delete_meta;
+		$activity_ids = implode( ',', wp_parse_id_list( $activity_ids ) );
+
+		if ( $delete_meta ) {
+			// Fetch the activity comment IDs for our deleted activity items.
+			$activity_comment_ids = $wpdb->get_col( "SELECT id FROM {$bp->activity->table_name} WHERE type = 'activity_comment' AND item_id IN ({$activity_ids})" );
+
+			if ( ! empty( $activity_comment_ids ) ) {
+				self::delete_activity_meta_entries( $activity_comment_ids );
+			}
+		}
+
+		return $wpdb->query( "DELETE FROM {$bp->activity->table_name} WHERE type = 'activity_comment' AND item_id IN ({$activity_ids})" );
+	}
+
+	/**
+	 * Delete the meta entries associated with a set of activity items.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $activity_ids Activity IDs whose meta should be deleted.
+	 * @return bool True on success.
+	 */
+	public static function delete_activity_meta_entries( $activity_ids = array() ) {
+		$activity_ids = wp_parse_id_list( $activity_ids );
+
+		foreach ( $activity_ids as $activity_id ) {
+			bp_activity_delete_meta( $activity_id );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Append activity comments to their associated activity items.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param array  $activities Activities to fetch comments for.
+	 * @param string $spam       Optional. 'ham_only' (default), 'spam_only' or 'all'.
+	 * @return array The updated activities with nested comments.
+	 */
+	public static function append_comments( $activities, $spam = 'ham_only' ) {
+		$activity_comments = array();
+
+		// Now fetch the activity comments and parse them into the correct position in the activities array.
+		foreach ( (array) $activities as $activity ) {
+			$top_level_parent_id = 'activity_comment' == $activity->type ? $activity->item_id : 0;
+			$activity_comments[$activity->id] = BP_Activity_Activity::get_activity_comments( $activity->id, $activity->mptt_left, $activity->mptt_right, $spam, $top_level_parent_id );
+		}
+
+		// Merge the comments with the activity items.
+		foreach ( (array) $activities as $key => $activity ) {
+			if ( isset( $activity_comments[$activity->id] ) ) {
+				$activities[$key]->children = $activity_comments[$activity->id];
+			}
+		}
+
+		return $activities;
+	}
+
+	/**
+	 * Get activity comments that are associated with a specific activity ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param int    $activity_id         Activity ID to fetch comments for.
+	 * @param int    $left                Left-most node boundary.
+	 * @param int    $right               Right-most node boundary.
+	 * @param string $spam                Optional. 'ham_only' (default), 'spam_only' or 'all'.
+	 * @param int    $top_level_parent_id Optional. The id of the root-level parent activity item.
+	 * @return array The updated activities with nested comments.
+	 */
+	public static function get_activity_comments( $activity_id, $left, $right, $spam = 'ham_only', $top_level_parent_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $top_level_parent_id ) ) {
+			$top_level_parent_id = $activity_id;
+		}
+
+		$comments = wp_cache_get( $activity_id, 'bp_activity_comments' );
+
+		// We store the string 'none' to cache the fact that the
+		// activity item has no comments.
+		if ( 'none' === $comments ) {
+			$comments = false;
+
+			// A true cache miss.
+		} elseif ( empty( $comments ) ) {
+
+			$bp = buddypress();
+
+			// Select the user's fullname with the query.
+			if ( bp_is_active( 'xprofile' ) ) {
+				$fullname_select = ", pd.value as user_fullname";
+				$fullname_from = ", {$bp->profile->table_name_data} pd ";
+				$fullname_where = "AND pd.user_id = a.user_id AND pd.field_id = 1";
+
+				// Prevent debug errors.
+			} else {
+				$fullname_select = $fullname_from = $fullname_where = '';
+			}
+
+			// Don't retrieve activity comments marked as spam.
+			if ( 'ham_only' == $spam ) {
+				$spam_sql = 'AND a.is_spam = 0';
+			} elseif ( 'spam_only' == $spam ) {
+				$spam_sql = 'AND a.is_spam = 1';
+			} else {
+				$spam_sql = '';
+			}
+
+			// Legacy query - not recommended.
+
+			/**
+			 * Filters if BuddyPress should use the legacy activity query.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param bool                 $value     Whether or not to use the legacy query.
+			 * @param BP_Activity_Activity $value     Magic method referring to currently called method.
+			 * @param array                $func_args Array of the method's argument list.
+			 */
+			if ( apply_filters( 'bp_use_legacy_activity_query', false, __METHOD__, func_get_args() ) ) {
+
+				/**
+				 * Filters the MySQL prepared statement for the legacy activity query.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string $value       Prepared statement for the activity query.
+				 * @param int    $activity_id Activity ID to fetch comments for.
+				 * @param int    $left        Left-most node boundary.
+				 * @param int    $right       Right-most node boundary.
+				 * @param string $spam_sql    SQL Statement portion to differentiate between ham or spam.
+				 */
+				$sql = apply_filters( 'bp_activity_comments_user_join_filter', $wpdb->prepare( "SELECT a.*, u.user_email, u.user_nicename, u.user_login, u.display_name{$fullname_select} FROM {$bp->activity->table_name} a, {$wpdb->users} u{$fullname_from} WHERE u.ID = a.user_id {$fullname_where} AND a.type = 'activity_comment' {$spam_sql} AND a.item_id = %d AND a.mptt_left > %d AND a.mptt_left < %d ORDER BY a.date_recorded ASC", $top_level_parent_id, $left, $right ), $activity_id, $left, $right, $spam_sql );
+
+				$descendants = $wpdb->get_results( $sql );
+
+				// We use the mptt BETWEEN clause to limit returned
+				// descendants to the correct part of the tree.
+			} else {
+				$sql = $wpdb->prepare( "SELECT id FROM {$bp->activity->table_name} a WHERE a.type = 'activity_comment' {$spam_sql} AND a.item_id = %d and a.mptt_left > %d AND a.mptt_left < %d ORDER BY a.date_recorded ASC", $top_level_parent_id, $left, $right );
+
+				$descendant_ids = $wpdb->get_col( $sql );
+				$descendants    = self::get_activity_data( $descendant_ids );
+				$descendants    = self::append_user_fullnames( $descendants );
+			}
+
+			$ref = array();
+
+			// Loop descendants and build an assoc array.
+			foreach ( (array) $descendants as $d ) {
+				$d->children = array();
+
+				// If we have a reference on the parent.
+				if ( isset( $ref[ $d->secondary_item_id ] ) ) {
+					$ref[ $d->secondary_item_id ]->children[ $d->id ] = $d;
+					$ref[ $d->id ] =& $ref[ $d->secondary_item_id ]->children[ $d->id ];
+
+					// If we don't have a reference on the parent, put in the root level.
+				} else {
+					$comments[ $d->id ] = $d;
+					$ref[ $d->id ] =& $comments[ $d->id ];
+				}
+			}
+
+			// Calculate depth for each item.
+			foreach ( $ref as &$r ) {
+				$depth = 1;
+				$parent_id = $r->secondary_item_id;
+
+				while ( $parent_id !== $r->item_id ) {
+					$depth++;
+
+					// When display_comments=stream, the parent comment may not be part of the
+					// returned results, so we manually fetch it.
+					if ( empty( $ref[ $parent_id ] ) ) {
+						$direct_parent = new BP_Activity_Activity( $parent_id );
+						if ( isset( $direct_parent->secondary_item_id ) ) {
+							// If the direct parent is not an activity update, that means we've reached
+							// the parent activity item (eg. new_blog_post).
+							if ( 'activity_update' !== $direct_parent->type ) {
+								$parent_id = $r->item_id;
+
+							} else {
+								$parent_id = $direct_parent->secondary_item_id;
+							}
+
+						} else {
+							// Something went wrong.  Short-circuit the depth calculation.
+							$parent_id = $r->item_id;
+						}
+					} else {
+						$parent_id = $ref[ $parent_id ]->secondary_item_id;
+					}
+				}
+				$r->depth = $depth;
+			}
+
+			// If we cache a value of false, it'll count as a cache
+			// miss the next time the activity comments are fetched.
+			// Storing the string 'none' is a hack workaround to
+			// avoid unnecessary queries.
+			if ( false === $comments ) {
+				$cache_value = 'none';
+			} else {
+				$cache_value = $comments;
+			}
+
+			wp_cache_set( $activity_id, $cache_value, 'bp_activity_comments' );
+		}
+
+		return $comments;
+	}
+
+	/**
+	 * Rebuild nested comment tree under an activity or activity comment.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param int $parent_id ID of an activity or activity comment.
+	 * @param int $left      Node boundary start for activity or activity comment.
+	 * @return int Right Node boundary of activity or activity comment.
+	 */
+	public static function rebuild_activity_comment_tree( $parent_id, $left = 1 ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// The right value of this node is the left value + 1.
+		$right = intval( $left + 1 );
+
+		// Get all descendants of this node.
+		$comments    = BP_Activity_Activity::get_child_comments( $parent_id );
+		$descendants = wp_list_pluck( $comments, 'id' );
+
+		// Loop the descendants and recalculate the left and right values.
+		foreach ( (array) $descendants as $descendant_id ) {
+			$right = BP_Activity_Activity::rebuild_activity_comment_tree( $descendant_id, $right );
+		}
+
+		// We've got the left value, and now that we've processed the children
+		// of this node we also know the right value.
+		if ( 1 === $left ) {
+			$wpdb->query( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET mptt_left = %d, mptt_right = %d WHERE id = %d", $left, $right, $parent_id ) );
+		} else {
+			$wpdb->query( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET mptt_left = %d, mptt_right = %d WHERE type = 'activity_comment' AND id = %d", $left, $right, $parent_id ) );
+		}
+
+		// Return the right value of this node + 1.
+		return intval( $right + 1 );
+	}
+
+	/**
+	 * Get child comments of an activity or activity comment.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param int $parent_id ID of an activity or activity comment.
+	 * @return object Numerically indexed array of child comments.
+	 */
+	public static function get_child_comments( $parent_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_results( $wpdb->prepare( "SELECT id FROM {$bp->activity->table_name} WHERE type = 'activity_comment' AND secondary_item_id = %d", $parent_id ) );
+	}
+
+	/**
+	 * Get a list of components that have recorded activity associated with them.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param bool $skip_last_activity If true, components will not be
+	 *                                 included if the only activity type associated with them is
+	 *                                 'last_activity'. (Since 2.0.0, 'last_activity' is stored in
+	 *                                 the activity table, but these items are not full-fledged
+	 *                                 activity items.) Default: true.
+	 * @return array List of component names.
+	 */
+	public static function get_recorded_components( $skip_last_activity = true ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( true === $skip_last_activity ) {
+			$components = $wpdb->get_col( "SELECT DISTINCT component FROM {$bp->activity->table_name} WHERE action != '' AND action != 'last_activity' ORDER BY component ASC" );
+		} else {
+			$components = $wpdb->get_col( "SELECT DISTINCT component FROM {$bp->activity->table_name} ORDER BY component ASC" );
+		}
+
+		return $components;
+	}
+
+	/**
+	 * Get sitewide activity items for use in an RSS feed.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $limit Optional. Number of items to fetch. Default: 35.
+	 * @return array $activity_feed List of activity items, with RSS data added.
+	 */
+	public static function get_sitewide_items_for_feed( $limit = 35 ) {
+		$activities    = bp_activity_get_sitewide( array( 'max' => $limit ) );
+		$activity_feed = array();
+
+		for ( $i = 0, $count = count( $activities ); $i < $count; ++$i ) {
+			$title                            = explode( '<span', $activities[$i]['content'] );
+			$activity_feed[$i]['title']       = trim( strip_tags( $title[0] ) );
+			$activity_feed[$i]['link']        = $activities[$i]['primary_link'];
+			$activity_feed[$i]['description'] = @sprintf( $activities[$i]['content'], '' );
+			$activity_feed[$i]['pubdate']     = $activities[$i]['date_recorded'];
+		}
+
+		return $activity_feed;
+	}
+
+	/**
+	 * Create SQL IN clause for filter queries.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Activity_Activity::get_filter_sql()
+	 *
+	 * @param string     $field The database field.
+	 * @param array|bool $items The values for the IN clause, or false when none are found.
+	 * @return string|false
+	 */
+	public static function get_in_operator_sql( $field, $items ) {
+		global $wpdb;
+
+		// Split items at the comma.
+		if ( ! is_array( $items ) ) {
+			$items = explode( ',', $items );
+		}
+
+		// Array of prepared integers or quoted strings.
+		$items_prepared = array();
+
+		// Clean up and format each item.
+		foreach ( $items as $item ) {
+			// Clean up the string.
+			$item = trim( $item );
+			// Pass everything through prepare for security and to safely quote strings.
+			$items_prepared[] = ( is_numeric( $item ) ) ? $wpdb->prepare( '%d', $item ) : $wpdb->prepare( '%s', $item );
+		}
+
+		// Build IN operator sql syntax.
+		if ( count( $items_prepared ) )
+			return sprintf( '%s IN ( %s )', trim( $field ), implode( ',', $items_prepared ) );
+		else
+			return false;
+	}
+
+	/**
+	 * Create filter SQL clauses.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $filter_array {
+	 *     Fields and values to filter by.
+	 *
+	 *     @type array|string|int $user_id      User ID(s).
+	 *     @type array|string     $object       Corresponds to the 'component'
+	 *                                          column in the database.
+	 *     @type array|string     $action       Corresponds to the 'type' column
+	 *                                          in the database.
+	 *     @type array|string|int $primary_id   Corresponds to the 'item_id'
+	 *                                          column in the database.
+	 *     @type array|string|int $secondary_id Corresponds to the
+	 *                                          'secondary_item_id' column in the database.
+	 *     @type int              $offset       Return only those items with an ID greater
+	 *                                          than the offset value.
+	 *     @type string           $since        Return only those items that have a
+	 *                                          date_recorded value greater than a
+	 *                                          given MySQL-formatted date.
+	 * }
+	 * @return string The filter clause, for use in a SQL query.
+	 */
+	public static function get_filter_sql( $filter_array ) {
+
+		$filter_sql = array();
+
+		if ( !empty( $filter_array['user_id'] ) ) {
+			$user_sql = BP_Activity_Activity::get_in_operator_sql( 'a.user_id', $filter_array['user_id'] );
+			if ( !empty( $user_sql ) )
+				$filter_sql[] = $user_sql;
+		}
+
+		if ( !empty( $filter_array['object'] ) ) {
+			$object_sql = BP_Activity_Activity::get_in_operator_sql( 'a.component', $filter_array['object'] );
+			if ( !empty( $object_sql ) )
+				$filter_sql[] = $object_sql;
+		}
+
+		if ( !empty( $filter_array['action'] ) ) {
+			$action_sql = BP_Activity_Activity::get_in_operator_sql( 'a.type', $filter_array['action'] );
+			if ( ! empty( $action_sql ) )
+				$filter_sql[] = $action_sql;
+		}
+
+		if ( !empty( $filter_array['primary_id'] ) ) {
+			$pid_sql = BP_Activity_Activity::get_in_operator_sql( 'a.item_id', $filter_array['primary_id'] );
+			if ( !empty( $pid_sql ) )
+				$filter_sql[] = $pid_sql;
+		}
+
+		if ( !empty( $filter_array['secondary_id'] ) ) {
+			$sid_sql = BP_Activity_Activity::get_in_operator_sql( 'a.secondary_item_id', $filter_array['secondary_id'] );
+			if ( !empty( $sid_sql ) )
+				$filter_sql[] = $sid_sql;
+		}
+
+		if ( ! empty( $filter_array['offset'] ) ) {
+			$sid_sql = absint( $filter_array['offset'] );
+			$filter_sql[] = "a.id >= {$sid_sql}";
+		}
+
+		if ( ! empty( $filter_array['since'] ) ) {
+			// Validate that this is a proper Y-m-d H:i:s date.
+			// Trick: parse to UNIX date then translate back.
+			$translated_date = date( 'Y-m-d H:i:s', strtotime( $filter_array['since'] ) );
+			if ( $translated_date === $filter_array['since'] ) {
+				$filter_sql[] = "a.date_recorded > '{$translated_date}'";
+			}
+		}
+
+		if ( empty( $filter_sql ) )
+			return false;
+
+		return join( ' AND ', $filter_sql );
+	}
+
+	/**
+	 * Get the date/time of last recorded activity.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string ISO timestamp.
+	 */
+	public static function get_last_updated() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( "SELECT date_recorded FROM {$bp->activity->table_name} ORDER BY date_recorded DESC LIMIT 1" );
+	}
+
+	/**
+	 * Get favorite count for a given user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id The ID of the user whose favorites you're counting.
+	 * @return int $value A count of the user's favorites.
+	 */
+	public static function total_favorite_count( $user_id ) {
+
+		// Get activities from user meta.
+		$favorite_activity_entries = bp_get_user_meta( $user_id, 'bp_favorite_activities', true );
+		if ( ! empty( $favorite_activity_entries ) ) {
+			return count( $favorite_activity_entries );
+		}
+
+		// No favorites.
+		return 0;
+	}
+
+	/**
+	 * Check whether an activity item exists with a given string content.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string $content The content to filter by.
+	 * @return int|false The ID of the first matching item if found, otherwise false.
+	 */
+	public static function check_exists_by_content( $content ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$result = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->activity->table_name} WHERE content = %s", $content ) );
+
+		return is_numeric( $result ) ? (int) $result : false;
+	}
+
+	/**
+	 * Hide all activity for a given user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id The ID of the user whose activity you want to mark hidden.
+	 * @return mixed
+	 */
+	public static function hide_all_for_user( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET hide_sitewide = 1 WHERE user_id = %d", $user_id ) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-component.php	(revision 12265)
@@ -0,0 +1,438 @@
+<?php
+/**
+ * BuddyPress Activity Streams Loader.
+ *
+ * An activity stream component, for users, groups, and site tracking.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityCore
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Main Activity Class.
+ *
+ * @since 1.5.0
+ */
+class BP_Activity_Component extends BP_Component {
+
+	/**
+	 * Start the activity component setup process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'activity',
+			__( 'Activity Streams', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 10,
+				'search_query_arg' => 'activity_search',
+				'features' => array( 'embeds' )
+			)
+		);
+	}
+
+	/**
+	 * Include component files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::includes() for a description of arguments.
+	 *
+	 * @param array $includes See BP_Component::includes() for a description.
+	 */
+	public function includes( $includes = array() ) {
+
+		// Files to include.
+		$includes = array(
+			'cssjs',
+			'filters',
+			'adminbar',
+			'template',
+			'functions',
+			'cache'
+		);
+
+		// Notifications support.
+		if ( bp_is_active( 'notifications' ) ) {
+			$includes[] = 'notifications';
+		}
+
+		// Load Akismet support if Akismet is configured.
+		$akismet_key = bp_get_option( 'wordpress_api_key' );
+
+		/** This filter is documented in bp-activity/bp-activity-akismet.php */
+		if ( defined( 'AKISMET_VERSION' ) && class_exists( 'Akismet' ) && ( ! empty( $akismet_key ) || defined( 'WPCOM_API_KEY' ) ) && apply_filters( 'bp_activity_use_akismet', bp_is_akismet_active() ) ) {
+			$includes[] = 'akismet';
+		}
+
+		// Embeds
+		if ( bp_is_active( $this->id, 'embeds' ) ) {
+			$includes[] = 'embeds';
+		}
+
+		if ( is_admin() ) {
+			$includes[] = 'admin';
+		}
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		/*
+		 * Load activity action and screen code if PHPUnit isn't running.
+		 *
+		 * For PHPUnit, we load these files in tests/phpunit/includes/install.php.
+		 */
+		if ( bp_is_current_component( 'activity' ) ) {
+			// Authenticated actions - Only fires when JS is disabled.
+			if ( is_user_logged_in() &&
+				in_array( bp_current_action(), array( 'delete', 'spam', 'post', 'reply', 'favorite', 'unfavorite' ), true )
+			) {
+				require $this->path . 'bp-activity/actions/' . bp_current_action() . '.php';
+			}
+
+			// RSS feeds.
+			if ( bp_is_current_action( 'feed' ) || bp_is_action_variable( 'feed', 0 ) ) {
+				require $this->path . 'bp-activity/actions/feeds.php';
+			}
+
+			// Screens - Directory.
+			if ( bp_is_activity_directory() ) {
+				require $this->path . 'bp-activity/screens/directory.php';
+			}
+
+			// Screens - User main nav.
+			if ( bp_is_user() ) {
+				require $this->path . 'bp-activity/screens/just-me.php';
+			}
+
+			// Screens - User secondary nav.
+			if ( bp_is_user() && in_array( bp_current_action(), array( 'friends', 'groups', 'favorites', 'mentions' ), true ) ) {
+				require $this->path . 'bp-activity/screens/' . bp_current_action() . '.php';
+			}
+
+			// Screens - Single permalink.
+			if ( bp_is_current_action( 'p' ) || is_numeric( bp_current_action() ) ) {
+				require $this->path . 'bp-activity/screens/permalink.php';
+			}
+
+			// Theme compatibility.
+			new BP_Activity_Theme_Compat();
+		}
+	}
+
+	/**
+	 * Set up component global variables.
+	 *
+	 * The BP_ACTIVITY_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_globals() for a description of arguments.
+	 *
+	 * @param array $args See BP_Component::setup_globals() for a description.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		// Define a slug, if necessary.
+		if ( ! defined( 'BP_ACTIVITY_SLUG' ) ) {
+			define( 'BP_ACTIVITY_SLUG', $this->id );
+		}
+
+		// Global tables for activity component.
+		$global_tables = array(
+			'table_name'      => $bp->table_prefix . 'bp_activity',
+			'table_name_meta' => $bp->table_prefix . 'bp_activity_meta',
+		);
+
+		// Metadata tables for groups component.
+		$meta_tables = array(
+			'activity' => $bp->table_prefix . 'bp_activity_meta',
+		);
+
+		// Fetch the default directory title.
+		$default_directory_titles = bp_core_get_directory_page_default_titles();
+		$default_directory_title  = $default_directory_titles[$this->id];
+
+		// All globals for activity component.
+		// Note that global_tables is included in this array.
+		$args = array(
+			'slug'                  => BP_ACTIVITY_SLUG,
+			'root_slug'             => isset( $bp->pages->activity->slug ) ? $bp->pages->activity->slug : BP_ACTIVITY_SLUG,
+			'has_directory'         => true,
+			'directory_title'       => isset( $bp->pages->activity->title ) ? $bp->pages->activity->title : $default_directory_title,
+			'notification_callback' => 'bp_activity_format_notifications',
+			'search_string'         => __( 'Search Activity...', 'buddypress' ),
+			'global_tables'         => $global_tables,
+			'meta_tables'           => $meta_tables,
+		);
+
+		parent::setup_globals( $args );
+	}
+
+	/**
+	 * Set up component navigation.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of arguments.
+	 *
+	 * @param array $main_nav Optional. See BP_Component::setup_nav() for description.
+	 * @param array $sub_nav  Optional. See BP_Component::setup_nav() for description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Stop if there is no user displayed or logged in.
+		if ( ! is_user_logged_in() && ! bp_displayed_user_id() ) {
+			return;
+		}
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$slug          = bp_get_activity_slug();
+		$activity_link = trailingslashit( $user_domain . $slug );
+
+		// Add 'Activity' to the main navigation.
+		$main_nav = array(
+			'name'                => _x( 'Activity', 'Profile activity screen nav', 'buddypress' ),
+			'slug'                => $slug,
+			'position'            => 10,
+			'screen_function'     => 'bp_activity_screen_my_activity',
+			'default_subnav_slug' => 'just-me',
+			'item_css_id'         => $this->id
+		);
+
+		// Add the subnav items to the activity nav item if we are using a theme that supports this.
+		$sub_nav[] = array(
+			'name'            => _x( 'Personal', 'Profile activity screen sub nav', 'buddypress' ),
+			'slug'            => 'just-me',
+			'parent_url'      => $activity_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_activity_screen_my_activity',
+			'position'        => 10
+		);
+
+		// Check @mentions.
+		if ( bp_activity_do_mentions() ) {
+			$sub_nav[] = array(
+				'name'            => _x( 'Mentions', 'Profile activity screen sub nav', 'buddypress' ),
+				'slug'            => 'mentions',
+				'parent_url'      => $activity_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_activity_screen_mentions',
+				'position'        => 20,
+				'item_css_id'     => 'activity-mentions'
+			);
+		}
+
+		// Favorite activity items.
+		if ( bp_activity_can_favorite() ) {
+			$sub_nav[] = array(
+				'name'            => _x( 'Favorites', 'Profile activity screen sub nav', 'buddypress' ),
+				'slug'            => 'favorites',
+				'parent_url'      => $activity_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_activity_screen_favorites',
+				'position'        => 30,
+				'item_css_id'     => 'activity-favs'
+			);
+		}
+
+		// Additional menu if friends is active.
+		if ( bp_is_active( 'friends' ) ) {
+			$sub_nav[] = array(
+				'name'            => _x( 'Friends', 'Profile activity screen sub nav', 'buddypress' ),
+				'slug'            => bp_get_friends_slug(),
+				'parent_url'      => $activity_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_activity_screen_friends',
+				'position'        => 40,
+				'item_css_id'     => 'activity-friends'
+			) ;
+		}
+
+		// Additional menu if groups is active.
+		if ( bp_is_active( 'groups' ) ) {
+			$sub_nav[] = array(
+				'name'            => _x( 'Groups', 'Profile activity screen sub nav', 'buddypress' ),
+				'slug'            => bp_get_groups_slug(),
+				'parent_url'      => $activity_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_activity_screen_groups',
+				'position'        => 50,
+				'item_css_id'     => 'activity-groups'
+			);
+		}
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up the component entries in the WordPress Admin Bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of the $wp_admin_nav
+	 *      parameter array.
+	 *
+	 * @param array $wp_admin_nav See BP_Component::setup_admin_bar() for a
+	 *                            description.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$activity_link = trailingslashit( bp_loggedin_user_domain() . bp_get_activity_slug() );
+
+			// Unread message count.
+			if ( bp_activity_do_mentions() ) {
+				$count = bp_get_total_mention_count_for_user( bp_loggedin_user_id() );
+				if ( !empty( $count ) ) {
+					$title = sprintf(
+						/* translators: %s: Unread mention count for the current user */
+						_x( 'Mentions %s', 'Toolbar Mention logged in user', 'buddypress' ),
+						'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+					);
+				} else {
+					$title = _x( 'Mentions', 'Toolbar Mention logged in user', 'buddypress' );
+				}
+			}
+
+			// Add the "Activity" sub menu.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => _x( 'Activity', 'My Account Activity sub nav', 'buddypress' ),
+				'href'   => $activity_link
+			);
+
+			// Personal.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-personal',
+				'title'    => _x( 'Personal', 'My Account Activity sub nav', 'buddypress' ),
+				'href'     => $activity_link,
+				'position' => 10
+			);
+
+			// Mentions.
+			if ( bp_activity_do_mentions() ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-mentions',
+					'title'    => $title,
+					'href'     => trailingslashit( $activity_link . 'mentions' ),
+					'position' => 20
+				);
+			}
+
+			// Favorite activity items.
+			if ( bp_activity_can_favorite() ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-favorites',
+					'title'    => _x( 'Favorites', 'My Account Activity sub nav', 'buddypress' ),
+					'href'     => trailingslashit( $activity_link . 'favorites' ),
+					'position' => 30
+				);
+			}
+
+			// Friends?
+			if ( bp_is_active( 'friends' ) ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-friends',
+					'title'    => _x( 'Friends', 'My Account Activity sub nav', 'buddypress' ),
+					'href'     => trailingslashit( $activity_link . bp_get_friends_slug() ),
+					'position' => 40
+				);
+			}
+
+			// Groups?
+			if ( bp_is_active( 'groups' ) ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-groups',
+					'title'    => _x( 'Groups', 'My Account Activity sub nav', 'buddypress' ),
+					'href'     => trailingslashit( $activity_link . bp_get_groups_slug() ),
+					'position' => 50
+				);
+			}
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function setup_title() {
+
+		// Adjust title based on view.
+		if ( bp_is_activity_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() ) {
+				$bp->bp_options_title = _x( 'My Activity', 'Page and <title>', 'buddypress' );
+			} else {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'	  => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title  = bp_get_displayed_user_fullname();
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups.
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_activity',
+			'bp_activity_comments',
+			'activity_meta'
+		) );
+
+		parent::setup_cache_groups();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-feed.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-feed.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-feed.php	(revision 12265)
@@ -0,0 +1,468 @@
+<?php
+/**
+ * BuddyPress Activity Classes.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityFeeds
+ * @since 1.8.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Create a RSS feed using the activity component.
+ *
+ * You should only construct a new feed when you've validated that you're on
+ * the appropriate screen.
+ *
+ * @since 1.8.0
+ *
+ * See {@link bp_activity_action_sitewide_feed()} as an example.
+ *
+ * @param array $args {
+ *   @type string $id               Required. Internal id for the feed; should be alphanumeric only.
+ *   @type string $title            Optional. RSS feed title.
+ *   @type string $link             Optional. Relevant link for the RSS feed.
+ *   @type string $description      Optional. RSS feed description.
+ *   @type string $ttl              Optional. Time-to-live. (see inline doc in constructor)
+ *   @type string $update_period    Optional. Part of the syndication module.
+ *                                            (see inline doc in constructor for more info)
+ *   @type string $update_frequency Optional. Part of the syndication module.
+ *                                            (see inline doc in constructor for more info)
+ *   @type string $max              Optional. Number of feed items to display.
+ *   @type array  $activity_args    Optional. Arguments passed to {@link bp_has_activities()}
+ * }
+ */
+class BP_Activity_Feed {
+
+	/**
+	 * Holds our custom class properties.
+	 *
+	 * These variables are stored in a protected array that is magically
+	 * updated using PHP 5.2+ methods.
+	 *
+	 * @see BP_Feed::__construct() This is where $data is added.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	protected $data;
+
+	/**
+	 * Magic method for checking the existence of a certain data variable.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $key Property to check.
+	 * @return bool Whether or not data variable exists.
+	 */
+	public function __isset( $key ) { return isset( $this->data[$key] ); }
+
+	/**
+	 * Magic method for getting a certain data variable.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $key Property to get.
+	 * @return mixed Data in variable if available or null.
+	 */
+	public function __get( $key ) { return isset( $this->data[$key] ) ? $this->data[$key] : null; }
+
+	/**
+	 * Magic method for setting a certain data variable.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $key   The property to set.
+	 * @param mixed  $value The value to set.
+	 */
+	public function __set( $key, $value ) { $this->data[$key] = $value; }
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $args Optional.
+	 */
+	public function __construct( $args = array() ) {
+
+		/**
+		 * Filters if BuddyPress should consider feeds enabled. If disabled, it will return early.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param bool true Default true aka feeds are enabled.
+		 */
+		if ( false === (bool) apply_filters( 'bp_activity_enable_feeds', true ) ) {
+			global $wp_query;
+
+			// Set feed flag to false.
+			$wp_query->is_feed = false;
+
+			return false;
+		}
+
+		// Setup data.
+		$this->data = wp_parse_args( $args, array(
+			// Internal identifier for the RSS feed - should be alphanumeric only.
+			'id'               => '',
+
+			// RSS title - should be plain-text.
+			'title'            => '',
+
+			// Relevant link for the RSS feed.
+			'link'             => '',
+
+			// RSS description - should be plain-text.
+			'description'      => '',
+
+			// Time-to-live - number of minutes to cache the data before an aggregator
+			// requests it again.  This is only acknowledged if the RSS client supports it
+			//
+			// See: http://www.rssboard.org/rss-profile#element-channel-ttl.
+			// See: http://www.kbcafe.com/rss/rssfeedstate.html#ttl.
+			'ttl'              => '30',
+
+			// Syndication module - similar to ttl, but not really supported by RSS
+			// clients
+			//
+			// See: http://web.resource.org/rss/1.0/modules/syndication/#description.
+			// See: http://www.kbcafe.com/rss/rssfeedstate.html#syndicationmodule.
+			'update_period'    => 'hourly',
+			'update_frequency' => 2,
+
+			// Number of items to display.
+			'max'              => 50,
+
+			// Activity arguments passed to bp_has_activities().
+			'activity_args'    => array()
+		) );
+
+		/**
+		 * Fires before the feed is setup so plugins can modify.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param BP_Activity_Feed $this Current instance of activity feed. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_activity_feed_prefetch', array( &$this ) );
+
+		// Setup class properties.
+		$this->setup_properties();
+
+		// Check if id is valid.
+		if ( empty( $this->id ) ) {
+			_doing_it_wrong( 'BP_Activity_Feed', __( "RSS feed 'id' must be defined", 'buddypress' ), 'BP 1.8' );
+			return false;
+		}
+
+		/**
+		 * Fires after the feed is setup so plugins can modify.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param BP_Activity_Feed $this Current instance of activity feed. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_activity_feed_postfetch', array( &$this ) );
+
+		// Setup feed hooks.
+		$this->setup_hooks();
+
+		// Output the feed.
+		$this->output();
+
+		// Kill the rest of the output.
+		die();
+	}
+
+	/** SETUP ****************************************************************/
+
+	/**
+	 * Setup and validate the class properties.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_properties() {
+		$this->id               = sanitize_title( $this->id );
+		$this->title            = strip_tags( $this->title );
+		$this->link             = esc_url_raw( $this->link );
+		$this->description      = strip_tags( $this->description );
+		$this->ttl              = (int) $this->ttl;
+		$this->update_period    = strip_tags( $this->update_period );
+		$this->update_frequency = (int) $this->update_frequency;
+
+		$this->activity_args    = wp_parse_args( $this->activity_args, array(
+			'max'              => $this->max,
+			'per_page'         => $this->max,
+			'display_comments' => 'stream'
+		) );
+
+	}
+
+	/**
+	 * Setup some hooks that are used in the feed.
+	 *
+	 * Currently, these hooks are used to maintain backwards compatibility with
+	 * the RSS feeds previous to BP 1.8.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_hooks() {
+		add_action( 'bp_activity_feed_rss_attributes',   array( $this, 'backpat_rss_attributes' ) );
+		add_action( 'bp_activity_feed_channel_elements', array( $this, 'backpat_channel_elements' ) );
+		add_action( 'bp_activity_feed_item_elements',    array( $this, 'backpat_item_elements' ) );
+	}
+
+	/** BACKPAT HOOKS ********************************************************/
+
+	/**
+	 * Fire a hook to ensure backward compatibility for RSS attributes.
+	 *
+	 * @since 1.8.0
+	 */
+	public function backpat_rss_attributes() {
+
+		/**
+		 * Fires inside backpat_rss_attributes method for backwards compatibility related to RSS attributes.
+		 *
+		 * This hook was originally separated out for individual components but has since been abstracted into the BP_Activity_Feed class.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'bp_activity_' . $this->id . '_feed' );
+	}
+
+	/**
+	 * Fire a hook to ensure backward compatibility for channel elements.
+	 *
+	 * @since 1.8.0
+	 */
+	public function backpat_channel_elements() {
+
+		/**
+		 * Fires inside backpat_channel_elements method for backwards compatibility related to RSS channel elements.
+		 *
+		 * This hook was originally separated out for individual components but has since been abstracted into the BP_Activity_Feed class.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'bp_activity_' . $this->id . '_feed_head' );
+	}
+
+	/**
+	 * Fire a hook to ensure backward compatibility for item elements.
+	 *
+	 * @since 1.8.0
+	 */
+	public function backpat_item_elements() {
+		switch ( $this->id ) {
+
+			// Sitewide and friends feeds use the 'personal' hook.
+			case 'sitewide' :
+			case 'friends' :
+				$id = 'personal';
+
+				break;
+
+			default :
+				$id = $this->id;
+
+				break;
+		}
+
+		/**
+		 * Fires inside backpat_item_elements method for backwards compatibility related to RSS item elements.
+		 *
+		 * This hook was originally separated out for individual components but has since been abstracted into the BP_Activity_Feed class.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'bp_activity_' . $id . '_feed_item' );
+	}
+
+	/** HELPERS **************************************************************/
+
+	/**
+	 * Output the feed's item content.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function feed_content() {
+		bp_activity_content_body();
+
+		switch ( $this->id ) {
+
+			// Also output parent activity item if we're on a specific feed.
+			case 'favorites' :
+			case 'friends' :
+			case 'mentions' :
+			case 'personal' :
+
+				if ( 'activity_comment' == bp_get_activity_action_name() ) :
+			?>
+				<strong><?php _e( 'In reply to', 'buddypress' ) ?></strong> -
+				<?php bp_activity_parent_content() ?>
+			<?php
+				endif;
+
+				break;
+		}
+	}
+
+	/**
+	 * Sets various HTTP headers related to Content-Type and browser caching.
+	 *
+	 * Most of this class method is derived from {@link WP::send_headers()}.
+	 *
+	 * @since 1.9.0
+	 */
+	protected function http_headers() {
+		// Set up some additional headers if not on a directory page
+		// this is done b/c BP uses pseudo-pages.
+		if ( ! bp_is_directory() ) {
+			global $wp_query;
+
+			$wp_query->is_404 = false;
+			status_header( 200 );
+		}
+
+		// Set content-type.
+		@header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true );
+		send_nosniff_header();
+
+		// Cache-related variables.
+		$last_modified      = mysql2date( 'D, d M Y H:i:s O', bp_activity_get_last_updated(), false );
+		$modified_timestamp = strtotime( $last_modified );
+		$etag               = md5( $last_modified );
+
+		// Set cache-related headers.
+		@header( 'Last-Modified: ' . $last_modified );
+		@header( 'Pragma: no-cache' );
+		@header( 'ETag: ' . '"' . $etag . '"' );
+
+		// First commit of BuddyPress! (Easter egg).
+		@header( 'Expires: Tue, 25 Mar 2008 17:13:55 GMT');
+
+		// Get ETag from supported user agents.
+		if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) {
+			$client_etag = wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] );
+
+			// Remove quotes from ETag.
+			$client_etag = trim( $client_etag, '"' );
+
+			// Strip suffixes from ETag if they exist (eg. "-gzip").
+			$etag_suffix_pos = strpos( $client_etag, '-' );
+			if ( ! empty( $etag_suffix_pos ) ) {
+				$client_etag = substr( $client_etag, 0, $etag_suffix_pos );
+			}
+
+		// No ETag found.
+		} else {
+			$client_etag = false;
+		}
+
+		// Get client last modified timestamp from supported user agents.
+		$client_last_modified      = empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ? '' : trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
+		$client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;
+
+		// Set 304 status if feed hasn't been updated since last fetch.
+		if ( ( $client_last_modified && $client_etag ) ?
+				 ( ( $client_modified_timestamp >= $modified_timestamp ) && ( $client_etag == $etag ) ) :
+				 ( ( $client_modified_timestamp >= $modified_timestamp ) || ( $client_etag == $etag ) ) ) {
+			$status = 304;
+		} else {
+			$status = false;
+		}
+
+		// If feed hasn't changed as reported by the user agent, set 304 status header.
+		if ( ! empty( $status ) ) {
+			status_header( $status );
+
+			// Cached response, so stop now!
+			if ( $status == 304 ) {
+				exit();
+			}
+		}
+	}
+
+	/** OUTPUT ***************************************************************/
+
+	/**
+	 * Output the RSS feed.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function output() {
+		$this->http_headers();
+		echo '<?xml version="1.0" encoding="' . get_option( 'blog_charset' ) . '"?'.'>';
+	?>
+
+<rss version="2.0"
+	xmlns:content="http://purl.org/rss/1.0/modules/content/"
+	xmlns:atom="http://www.w3.org/2005/Atom"
+	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
+	<?php
+
+	/**
+	 * Fires at the end of the opening RSS tag for feed output so plugins can add extra attributes.
+	 *
+	 * @since 1.8.0
+	 */
+	do_action( 'bp_activity_feed_rss_attributes' ); ?>
+>
+
+<channel>
+	<title><?php echo $this->title; ?></title>
+	<link><?php echo $this->link; ?></link>
+	<atom:link href="<?php self_link(); ?>" rel="self" type="application/rss+xml" />
+	<description><?php echo $this->description ?></description>
+	<lastBuildDate><?php echo mysql2date( 'D, d M Y H:i:s O', bp_activity_get_last_updated(), false ); ?></lastBuildDate>
+	<generator>https://buddypress.org/?v=<?php bp_version(); ?></generator>
+	<language><?php bloginfo_rss( 'language' ); ?></language>
+	<ttl><?php echo $this->ttl; ?></ttl>
+	<sy:updatePeriod><?php echo $this->update_period; ?></sy:updatePeriod>
+	<sy:updateFrequency><?php echo $this->update_frequency; ?></sy:updateFrequency>
+	<?php
+
+	/**
+	 * Fires at the end of channel elements list in RSS feed so plugins can add extra channel elements.
+	 *
+	 * @since 1.8.0
+	 */
+	do_action( 'bp_activity_feed_channel_elements' ); ?>
+
+	<?php if ( bp_has_activities( $this->activity_args ) ) : ?>
+		<?php while ( bp_activities() ) : bp_the_activity(); ?>
+			<item>
+				<guid isPermaLink="false"><?php bp_activity_feed_item_guid(); ?></guid>
+				<title><?php echo stripslashes( bp_get_activity_feed_item_title() ); ?></title>
+				<link><?php bp_activity_thread_permalink() ?></link>
+				<pubDate><?php echo mysql2date( 'D, d M Y H:i:s O', bp_get_activity_feed_item_date(), false ); ?></pubDate>
+
+				<?php if ( bp_get_activity_feed_item_description() ) : ?>
+					<content:encoded><![CDATA[<?php $this->feed_content(); ?>]]></content:encoded>
+				<?php endif; ?>
+
+				<?php if ( bp_activity_can_comment() ) : ?>
+					<slash:comments><?php bp_activity_comment_count(); ?></slash:comments>
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires at the end of the individual RSS Item list in RSS feed so plugins can add extra item elements.
+				 *
+				 * @since 1.8.0
+				 */
+				do_action( 'bp_activity_feed_item_elements' ); ?>
+			</item>
+		<?php endwhile; ?>
+
+	<?php endif; ?>
+</channel>
+</rss><?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-list-table.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-list-table.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-list-table.php	(revision 12265)
@@ -0,0 +1,857 @@
+<?php
+/**
+ * BuddyPress Activity component admin list table.
+ *
+ * Props to WordPress core for the Comments admin screen, and its contextual
+ * help text, on which this implementation is heavily based.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityAdmin
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * List table class for the Activity component admin page.
+ *
+ * @since 1.6.0
+ */
+class BP_Activity_List_Table extends WP_List_Table {
+
+	/**
+	 * What type of view is being displayed?
+	 *
+	 * E.g. "all", "pending", "approved", "spam"...
+	 *
+	 * @since 1.6.0
+	 * @var string $view
+	 */
+	public $view = 'all';
+
+	/**
+	 * How many activity items have been marked as spam.
+	 *
+	 * @since 1.6.0
+	 * @var int $spam_count
+	 */
+	public $spam_count = 0;
+
+	/**
+	 * Store activity-to-user-ID mappings for use in the In Response To column.
+	 *
+	 * @since 1.6.0
+	 * @var array $activity_user_id
+	 */
+	protected $activity_user_id = array();
+
+	/**
+	 * If users can comment on post and comment activity items.
+	 *
+	 * @link https://buddypress.trac.wordpress.org/ticket/6277
+	 *
+	 * @since 2.2.2
+	 * @var bool $disable_blogforum_comments
+	 */
+	public $disable_blogforum_comments = false;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.6.0
+	 */
+	public function __construct() {
+
+		// See if activity commenting is enabled for post/comment activity items.
+		$this->disable_blogforum_comments = bp_disable_blogforum_comments();
+
+		// Define singular and plural labels, as well as whether we support AJAX.
+		parent::__construct( array(
+			'ajax'     => false,
+			'plural'   => 'activities',
+			'singular' => 'activity',
+			'screen'   => get_current_screen(),
+		) );
+	}
+
+	/**
+	 * Handle filtering of data, sorting, pagination, and any other data manipulation prior to rendering.
+	 *
+	 * @since 1.6.0
+	 */
+	function prepare_items() {
+
+		// Option defaults.
+		$filter           = array();
+		$filter_query     = false;
+		$include_id       = false;
+		$search_terms     = false;
+		$sort             = 'DESC';
+		$spam             = 'ham_only';
+
+		// Set current page.
+		$page = $this->get_pagenum();
+
+		// Set per page from the screen options.
+		$per_page = $this->get_items_per_page( str_replace( '-', '_', "{$this->screen->id}_per_page" ) );
+
+		// Check if we're on the "Spam" view.
+		if ( !empty( $_REQUEST['activity_status'] ) && 'spam' == $_REQUEST['activity_status'] ) {
+			$spam       = 'spam_only';
+			$this->view = 'spam';
+		}
+
+		// Sort order.
+		if ( !empty( $_REQUEST['order'] ) && 'desc' != $_REQUEST['order'] )
+			$sort = 'ASC';
+
+		// Order by.
+		/*if ( !empty( $_REQUEST['orderby'] ) ) {
+		}*/
+
+		// Filter.
+		if ( ! empty( $_REQUEST['activity_type'] ) ) {
+			$filter = array( 'action' => $_REQUEST['activity_type'] );
+
+			/**
+			 * Filter here to override the filter with a filter query
+			 *
+			 * @since  2.5.0
+			 *
+			 * @param array $filter
+			 */
+			$has_filter_query = apply_filters( 'bp_activity_list_table_filter_activity_type_items', $filter );
+
+			if ( ! empty( $has_filter_query['filter_query'] ) ) {
+				// Reset the filter
+				$filter       = array();
+
+				// And use the filter query instead
+				$filter_query = $has_filter_query['filter_query'];
+			}
+		}
+
+		// Are we doing a search?
+		if ( !empty( $_REQUEST['s'] ) )
+			$search_terms = $_REQUEST['s'];
+
+		// Check if user has clicked on a specific activity (if so, fetch only that, and any related, activity).
+		if ( !empty( $_REQUEST['aid'] ) )
+			$include_id = (int) $_REQUEST['aid'];
+
+		// Get the spam total (ignoring any search query or filter).
+		$spams = bp_activity_get( array(
+			'display_comments' => 'stream',
+			'show_hidden'      => true,
+			'spam'             => 'spam_only',
+			'count_total'      => 'count_query',
+		) );
+		$this->spam_count = $spams['total'];
+		unset( $spams );
+
+		// Get the activities from the database.
+		$activities = bp_activity_get( array(
+			'display_comments' => 'stream',
+			'filter'           => $filter,
+			'in'               => $include_id,
+			'page'             => $page,
+			'per_page'         => $per_page,
+			'search_terms'     => $search_terms,
+			'filter_query'     => $filter_query,
+			'show_hidden'      => true,
+			// 'sort'             => $sort,
+			'spam'             => $spam,
+			'count_total'      => 'count_query',
+		) );
+
+		// If we're viewing a specific activity, flatten all activities into a single array.
+		if ( $include_id ) {
+			$activities['activities'] = BP_Activity_List_Table::flatten_activity_array( $activities['activities'] );
+			$activities['total']      = count( $activities['activities'] );
+
+			// Sort the array by the activity object's date_recorded value.
+			usort( $activities['activities'], function( $a, $b ) { return $a->date_recorded > $b->date_recorded; } );
+		}
+
+		// The bp_activity_get function returns an array of objects; cast these to arrays for WP_List_Table.
+		$new_activities = array();
+		foreach ( $activities['activities'] as $activity_item ) {
+			$new_activities[] = (array) $activity_item;
+
+			// Build an array of activity-to-user ID mappings for better efficiency in the In Response To column.
+			$this->activity_user_id[$activity_item->id] = $activity_item->user_id;
+		}
+
+		// Set raw data to display.
+		$this->items       = $new_activities;
+
+		// Store information needed for handling table pagination.
+		$this->set_pagination_args( array(
+			'per_page'    => $per_page,
+			'total_items' => $activities['total'],
+			'total_pages' => ceil( $activities['total'] / $per_page )
+		) );
+
+		// Don't truncate activity items; bp_activity_truncate_entry() needs to be used inside a BP_Activity_Template loop.
+		remove_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+	}
+
+	/**
+	 * Get an array of all the columns on the page.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return array Column headers.
+	 */
+	function get_column_info() {
+		$this->_column_headers = array(
+			$this->get_columns(),
+			array(),
+			$this->get_sortable_columns(),
+			$this->get_default_primary_column_name(),
+		);
+
+		return $this->_column_headers;
+	}
+
+	/**
+	 * Get name of default primary column
+	 *
+	 * @since 2.3.3
+	 *
+	 * @return string
+	 */
+	protected function get_default_primary_column_name() {
+		return 'author';
+	}
+
+	/**
+	 * Display a message on screen when no items are found (e.g. no search matches).
+	 *
+	 * @since 1.6.0
+	 */
+	function no_items() {
+		_e( 'No activities found.', 'buddypress' );
+	}
+
+	/**
+	 * Output the Activity data table.
+	 *
+	 * @since 1.6.0
+	 */
+	function display() {
+		$this->display_tablenav( 'top' ); ?>
+
+		<h2 class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Activities list', 'buddypress' );
+		?></h2>
+
+		<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>" cellspacing="0">
+			<thead>
+				<tr>
+					<?php $this->print_column_headers(); ?>
+				</tr>
+			</thead>
+
+			<tbody id="the-comment-list">
+				<?php $this->display_rows_or_placeholder(); ?>
+			</tbody>
+
+			<tfoot>
+				<tr>
+					<?php $this->print_column_headers( false ); ?>
+				</tr>
+			</tfoot>
+		</table>
+		<?php
+
+		$this->display_tablenav( 'bottom' );
+	}
+
+	/**
+	 * Generate content for a single row of the table.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param object $item The current item.
+	 */
+	function single_row( $item ) {
+		static $even = false;
+
+		if ( $even ) {
+			$row_class = ' class="even"';
+		} else {
+			$row_class = ' class="alternate odd"';
+		}
+
+		if ( 'activity_comment' === $item['type'] ) {
+			$root_id = $item['item_id'];
+		} else {
+			$root_id = $item['id'];
+		}
+
+		echo '<tr' . $row_class . ' id="activity-' . esc_attr( $item['id'] ) . '" data-parent_id="' . esc_attr( $item['id'] ) . '" data-root_id="' . esc_attr( $root_id ) . '">';
+		echo $this->single_row_columns( $item );
+		echo '</tr>';
+
+		$even = ! $even;
+	}
+
+	/**
+	 * Get the list of views available on this table (e.g. "all", "spam").
+	 *
+	 * @since 1.6.0
+	 */
+	function get_views() {
+		$url_base = add_query_arg( array( 'page' => 'bp-activity' ), bp_get_admin_url( 'admin.php' ) ); ?>
+
+		<h2 class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Filter activities list', 'buddypress' );
+		?></h2>
+
+		<ul class="subsubsub">
+			<li class="all"><a href="<?php echo esc_url( $url_base ); ?>" class="<?php if ( 'spam' != $this->view ) echo 'current'; ?>"><?php _e( 'All', 'buddypress' ); ?></a> |</li>
+			<li class="spam"><a href="<?php echo esc_url( add_query_arg( array( 'activity_status' => 'spam' ), $url_base ) ); ?>" class="<?php if ( 'spam' == $this->view ) echo 'current'; ?>"><?php printf( __( 'Spam <span class="count">(%s)</span>', 'buddypress' ), number_format_i18n( $this->spam_count ) ); ?></a></li>
+
+			<?php
+
+			/**
+			 * Fires inside listing of views so plugins can add their own.
+			 *
+			 * @since 1.6.0
+			 *
+			 * @param string $url_base Current URL base for view.
+			 * @param string $view     Current view being displayed.
+			 */
+			do_action( 'bp_activity_list_table_get_views', $url_base, $this->view ); ?>
+		</ul>
+	<?php
+	}
+
+	/**
+	 * Get bulk actions.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return array Key/value pairs for the bulk actions dropdown.
+	 */
+	function get_bulk_actions() {
+		$actions = array();
+		$actions['bulk_spam']   = __( 'Mark as Spam', 'buddypress' );
+		$actions['bulk_ham']    = __( 'Not Spam', 'buddypress' );
+		$actions['bulk_delete'] = __( 'Delete Permanently', 'buddypress' );
+
+		/**
+		 * Filters the default bulk actions so plugins can add custom actions.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $actions Default available actions for bulk operations.
+		 */
+		return apply_filters( 'bp_activity_list_table_get_bulk_actions', $actions );
+	}
+
+	/**
+	 * Get the table column titles.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @return array The columns to appear in the Activity list table.
+	 */
+	function get_columns() {
+		/**
+		 * Filters the titles for the columns for the activity list table.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param array $value Array of slugs and titles for the columns.
+		 */
+		return apply_filters( 'bp_activity_list_table_get_columns', array(
+			'cb'       => '<input name type="checkbox" />',
+			'author'   => _x('Author', 'Admin SWA column header', 'buddypress' ),
+			'comment'  => _x( 'Activity', 'Admin SWA column header', 'buddypress' ),
+			'action'   => _x( 'Action', 'Admin SWA column header', 'buddypress' ),
+			'response' => _x( 'In Response To', 'Admin SWA column header', 'buddypress' ),
+		) );
+	}
+
+	/**
+	 * Get the column names for sortable columns.
+	 *
+	 * Currently, returns an empty array (no columns are sortable).
+	 *
+	 * @since 1.6.0
+	 * @todo For this to work, BP_Activity_Activity::get() needs updating
+	 *       to support ordering by specific fields.
+	 *
+	 * @return array The columns that can be sorted on the Activity screen.
+	 */
+	function get_sortable_columns() {
+		return array();
+
+		/*return array(
+			'author' => array( 'activity_author', false ),  // Intentionally not using "=>"
+		);*/
+	}
+
+	/**
+	 * Markup for the "filter" part of the form (i.e. which activity type to display).
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $which 'top' or 'bottom'.
+	 */
+	function extra_tablenav( $which ) {
+
+		// Bail on bottom table nav.
+		if ( 'bottom' === $which ) {
+			return;
+		}
+
+		// Is any filter currently selected?
+		$selected = ( ! empty( $_REQUEST['activity_type'] ) ) ? $_REQUEST['activity_type'] : '';
+
+		// Get the actions.
+		$activity_actions = bp_activity_get_actions(); ?>
+
+		<div class="alignleft actions">
+			<label for="activity-type" class="screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'Filter by activity type', 'buddypress' );
+			?></label>
+			<select name="activity_type" id="activity-type">
+				<option value="" <?php selected( ! $selected ); ?>><?php _e( 'View all actions', 'buddypress' ); ?></option>
+
+				<?php foreach ( $activity_actions as $component => $actions ) : ?>
+					<?php
+					// Older avatar activity items use 'profile' for component. See r4273.
+					if ( $component === 'profile' ) {
+						$component = 'xprofile';
+					}
+
+					if ( bp_is_active( $component ) ) {
+						if ( $component === 'xprofile' ) {
+							$component_name = buddypress()->profile->name;
+						} else {
+							$component_name = buddypress()->$component->name;
+						}
+
+					} else {
+						// Prevent warnings by other plugins if a component is disabled but the activity type has been registered.
+						$component_name = ucfirst( $component );
+					}
+					?>
+
+					<optgroup label="<?php echo esc_html( $component_name ); ?>">
+
+						<?php foreach ( $actions as $action_key => $action_values ) : ?>
+
+							<?php
+
+							// Skip the incorrectly named pre-1.6 action.
+							if ( 'friends_register_activity_action' !== $action_key ) : ?>
+
+								<option value="<?php echo esc_attr( $action_key ); ?>" <?php selected( $action_key,  $selected ); ?>><?php echo esc_html( $action_values[ 'value' ] ); ?></option>
+
+							<?php endif; ?>
+
+						<?php endforeach; ?>
+
+					</optgroup>
+
+				<?php endforeach; ?>
+
+			</select>
+
+			<?php submit_button( __( 'Filter', 'buddypress' ), 'secondary', false, false, array( 'id' => 'post-query-submit' ) ); ?>
+		</div>
+
+	<?php
+	}
+
+	/**
+	 * Override WP_List_Table::row_actions().
+	 *
+	 * Basically a duplicate of the row_actions() method, but removes the
+	 * unnecessary <button> addition.
+	 *
+	 * @since 2.3.3
+	 * @since 2.3.4 Visibility set to public for compatibility with WP < 4.0.0.
+	 *
+	 * @param array $actions The list of actions.
+	 * @param bool  $always_visible Whether the actions should be always visible.
+	 * @return string
+	 */
+	public function row_actions( $actions, $always_visible = false ) {
+		$action_count = count( $actions );
+		$i = 0;
+
+		if ( !$action_count )
+			return '';
+
+		$out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
+		foreach ( $actions as $action => $link ) {
+			++$i;
+			( $i == $action_count ) ? $sep = '' : $sep = ' | ';
+			$out .= "<span class='$action'>$link$sep</span>";
+		}
+		$out .= '</div>';
+
+		return $out;
+	}
+
+	/**
+	 * Checkbox column markup.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	function column_cb( $item ) {
+		/* translators: accessibility text */
+		printf( '<label class="screen-reader-text" for="aid-%1$d">' . __( 'Select activity item %1$d', 'buddypress' ) . '</label><input type="checkbox" name="aid[]" value="%1$d" id="aid-%1$d" />', $item['id'] );
+	}
+
+	/**
+	 * Author column markup.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	function column_author( $item ) {
+		echo '<strong>' . get_avatar( $item['user_id'], '32' ) . ' ' . bp_core_get_userlink( $item['user_id'] ) . '</strong>';
+	}
+
+	/**
+	 * Action column markup.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	function column_action( $item ) {
+		$actions = bp_activity_admin_get_activity_actions();
+
+		if ( isset( $actions[ $item['type'] ] ) ) {
+			echo $actions[ $item['type'] ];
+		} else {
+			printf( __( 'Unregistered action - %s', 'buddypress' ), $item['type'] );
+		}
+	}
+
+	/**
+	 * Content column, and "quick admin" rollover actions.
+	 *
+	 * Called "comment" in the CSS so we can re-use some WP core CSS.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	function column_comment( $item ) {
+		// Determine what type of item (row) we're dealing with.
+		if ( $item['is_spam'] )
+			$item_status = 'spam';
+		else
+			$item_status = 'all';
+
+		// Preorder items: Reply | Edit | Spam | Delete Permanently.
+		$actions = array(
+			'reply'  => '',
+			'edit'   => '',
+			'spam'   => '', 'unspam' => '',
+			'delete' => '',
+		);
+
+		// Build actions URLs.
+		$base_url   = bp_get_admin_url( 'admin.php?page=bp-activity&amp;aid=' . $item['id'] );
+		$spam_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'spam-activity_' . $item['id'] ) );
+
+		$delete_url = $base_url . "&amp;action=delete&amp;$spam_nonce";
+		$edit_url   = $base_url . '&amp;action=edit';
+		$ham_url    = $base_url . "&amp;action=ham&amp;$spam_nonce";
+		$spam_url   = $base_url . "&amp;action=spam&amp;$spam_nonce";
+
+		// Rollover actions.
+		// Reply - JavaScript only; implemented by AJAX.
+		if ( 'spam' != $item_status ) {
+			if ( $this->can_comment( $item ) ) {
+				$actions['reply'] = sprintf( '<a href="#" class="reply hide-if-no-js">%s</a>', __( 'Reply', 'buddypress' ) );
+			} else {
+				$actions['reply'] = sprintf( '<span class="form-input-tip">%s</span>', __( 'Replies disabled', 'buddypress' ) );
+			}
+
+			// Edit.
+			$actions['edit'] = sprintf( '<a href="%s">%s</a>', $edit_url, __( 'Edit', 'buddypress' ) );
+		}
+
+		// Spam/unspam.
+		if ( 'spam' == $item_status )
+			$actions['unspam'] = sprintf( '<a href="%s">%s</a>', $ham_url, __( 'Not Spam', 'buddypress' ) );
+		else
+			$actions['spam'] = sprintf( '<a href="%s">%s</a>', $spam_url, __( 'Spam', 'buddypress' ) );
+
+		// Delete.
+		$actions['delete'] = sprintf( '<a href="%s" onclick="%s">%s</a>', $delete_url, "javascript:return confirm('" . esc_js( __( 'Are you sure?', 'buddypress' ) ) . "'); ", __( 'Delete Permanently', 'buddypress' ) );
+
+		// Start timestamp.
+		echo '<div class="submitted-on">';
+
+		/**
+		 * Filters available actions for plugins to alter.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $actions Array of available actions user could use.
+		 * @param array $item    Current item being added to page.
+		 */
+		$actions = apply_filters( 'bp_activity_admin_comment_row_actions', array_filter( $actions ), $item );
+
+		printf(
+			/* translators: %s: activity date and time */
+			__( 'Submitted on %s', 'buddypress' ),
+			sprintf(
+				'<a href="%1$s">%2$s</a>',
+				bp_activity_get_permalink( $item['id'] ),
+				sprintf(
+					/* translators: 1: activity date, 2: activity time */
+					__( '%1$s at %2$s', 'buddypress' ),
+					date_i18n( bp_get_option( 'date_format' ), strtotime( $item['date_recorded'] ) ),
+					get_date_from_gmt( $item['date_recorded'], bp_get_option( 'time_format' ) )
+				)
+			)
+		);
+
+		// End timestamp.
+		echo '</div>';
+
+		// Get activity content - if not set, use the action.
+		if ( ! empty( $item['content'] ) ) {
+			$activity = new BP_Activity_Activity( $item['id'] );
+
+			/** This filter is documented in bp-activity/bp-activity-template.php */
+			$content = apply_filters_ref_array( 'bp_get_activity_content_body', array( $item['content'], &$activity ) );
+		} else {
+			/**
+			 * Filters current activity item action.
+			 *
+			 * @since 1.2.0
+			 *
+			 * @var array $item Array index holding current activity item action.
+			 */
+			$content = apply_filters_ref_array( 'bp_get_activity_action', array( $item['action'] ) );
+		}
+
+		/**
+		 * Filter here to add extra output to the activity content into the Administration.
+		 *
+		 * @since  2.4.0
+		 *
+		 * @param  string $content The activity content.
+		 * @param  array  $item    The activity object converted into an array.
+		 */
+		echo apply_filters( 'bp_activity_admin_comment_content', $content, $item ) . ' ' . $this->row_actions( $actions );
+	}
+
+	/**
+	 * "In response to" column markup.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	function column_response( $item ) {
+
+		// Is $item is a root activity?
+		?>
+
+		<div class="response-links">
+
+		<?php
+		// Activity permalink.
+		$activity_permalink = '';
+		if ( ! $item['is_spam'] ) {
+			$activity_permalink = '<a href="' . bp_activity_get_permalink( $item['id'], (object) $item ) . '" class="comments-view-item-link">' . __( 'View Activity', 'buddypress' ) . '</a>';
+		}
+
+		/**
+		 * Filters default list of default root activity types.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $value Array of default activity types.
+		 * @param array $item  Current item being displayed.
+		 */
+		if ( empty( $item['item_id'] ) || ! in_array( $item['type'], apply_filters( 'bp_activity_admin_root_activity_types', array( 'activity_comment' ), $item ) ) ) {
+			echo $activity_permalink;
+
+			$comment_count     = !empty( $item['children'] ) ? bp_activity_recurse_comment_count( (object) $item ) : 0;
+			$root_activity_url = bp_get_admin_url( 'admin.php?page=bp-activity&amp;aid=' . $item['id'] );
+
+			// If the activity has comments, display a link to the activity's permalink, with its comment count in a speech bubble.
+			if ( $comment_count ) {
+				printf( '<a href="%1$s" class="post-com-count post-com-count-approved"><span class="comment-count comment-count-approved">%2$s</span></a>', esc_url( $root_activity_url ), number_format_i18n( $comment_count ) );
+			}
+
+		// For non-root activities, display a link to the replied-to activity's author's profile.
+		} else {
+			echo '<strong>' . get_avatar( $this->get_activity_user_id( $item['item_id'] ), '32' ) . ' ' . bp_core_get_userlink( $this->get_activity_user_id( $item['item_id'] ) ) . '</strong><br />';
+			echo $activity_permalink;
+		}
+		?>
+
+		</div>
+
+		<?php
+	}
+
+	/**
+	 * Allow plugins to add their custom column.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array  $item        Information about the current row.
+	 * @param string $column_name The column name.
+	 * @return string
+	 */
+	public function column_default( $item = array(), $column_name = '' ) {
+
+		/**
+		 * Filters a string to allow plugins to add custom column content.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param string $value       Empty string.
+		 * @param string $column_name Name of the column being rendered.
+		 * @param array  $item        The current activity item in the loop.
+		 */
+		return apply_filters( 'bp_activity_admin_get_custom_column', '', $column_name, $item );
+	}
+
+	/**
+	 * Get the user id associated with a given activity item.
+	 *
+	 * Wraps bp_activity_get_specific(), with some additional logic for
+	 * avoiding duplicate queries.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $activity_id Activity ID to retrieve User ID for.
+	 * @return int User ID of the activity item in question.
+	 */
+	protected function get_activity_user_id( $activity_id ) {
+		// If there is an existing activity/user ID mapping, just return the user ID.
+		if ( ! empty( $this->activity_user_id[$activity_id] ) ) {
+			return $this->activity_user_id[$activity_id];
+
+		/*
+		 * We don't have a mapping. This means the $activity_id is not on the current
+		 * page of results, so fetch its details from the database.
+		 */
+		} else {
+			$activity = bp_activity_get_specific( array( 'activity_ids' => $activity_id, 'show_hidden' => true, 'spam' => 'all', ) );
+
+			/*
+			 * If, somehow, the referenced activity has been deleted, leaving its associated
+			 * activities as orphans, use the logged in user's ID to avoid errors.
+			 */
+			if ( empty( $activity['activities'] ) )
+				return bp_loggedin_user_id();
+
+			// Store the new activity/user ID mapping for any later re-use.
+			$this->activity_user_id[ $activity['activities'][0]->id ] = $activity['activities'][0]->user_id;
+
+			// Return the user ID.
+			return $activity['activities'][0]->user_id;
+		}
+	}
+
+	/**
+	 * Checks if an activity item can be replied to.
+	 *
+	 * This method merges functionality from {@link bp_activity_can_comment()} and
+	 * {@link bp_blogs_disable_activity_commenting()}. This is done because the activity
+	 * list table doesn't use a BuddyPress activity loop, which prevents those
+	 * functions from working as intended.
+	 *
+	 * @since 2.0.0
+	 * @since 2.5.0 Include Post type activities types
+	 *
+	 * @param array $item An array version of the BP_Activity_Activity object.
+	 * @return bool $can_comment
+	 */
+	protected function can_comment( $item  ) {
+		$can_comment = bp_activity_type_supports( $item['type'], 'comment-reply' );
+
+		if ( ! $this->disable_blogforum_comments && bp_is_active( 'blogs' ) ) {
+			$parent_activity = false;
+
+			if ( bp_activity_type_supports( $item['type'], 'post-type-comment-tracking' ) ) {
+				$parent_activity = (object) $item;
+			} elseif ( 'activity_comment' === $item['type'] ) {
+				$parent_activity = new BP_Activity_Activity( $item['item_id'] );
+				$can_comment     = bp_activity_can_comment_reply( (object) $item );
+			}
+
+			if ( isset( $parent_activity->type ) && bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'post_type' ) ) {
+				// Fetch blog post comment depth and if the blog post's comments are open.
+				bp_blogs_setup_activity_loop_globals( $parent_activity );
+
+				$can_comment = bp_blogs_can_comment_reply( true, $item );
+			}
+		}
+
+		/**
+		 * Filters if an activity item can be commented on or not.
+		 *
+		 * @since 2.0.0
+		 * @since 2.5.0 Add a second parameter to include the activity item into the filter.
+		 *
+		 * @param bool  $can_comment Whether an activity item can be commented on or not.
+		 * @param array $item        An array version of the BP_Activity_Activity object.
+		 */
+		return apply_filters( 'bp_activity_list_table_can_comment', $can_comment, $item );
+	}
+
+	/**
+	 * Flatten the activity array.
+	 *
+	 * In some cases, BuddyPress gives us a structured tree of activity
+	 * items plus their comments. This method converts it to a flat array.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $tree Source array.
+	 * @return array Flattened array.
+	 */
+	public static function flatten_activity_array( $tree ){
+		foreach ( (array) $tree as $node ) {
+			if ( isset( $node->children ) ) {
+
+				foreach ( BP_Activity_List_Table::flatten_activity_array( $node->children ) as $child ) {
+					$tree[] = $child;
+				}
+
+				unset( $node->children );
+			}
+		}
+
+		return $tree;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-oembed-extension.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-oembed-extension.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-oembed-extension.php	(revision 12265)
@@ -0,0 +1,329 @@
+<?php
+/**
+ * BuddyPress Activity Classes.
+ *
+ * @package BuddyPress
+ * @subpackage Embeds
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * oEmbed handler to respond and render single activity items.
+ *
+ * @since 2.6.0
+ */
+class BP_Activity_oEmbed_Extension extends BP_Core_oEmbed_Extension {
+	/**
+	 * Custom oEmbed slug endpoint.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var string
+	 */
+	public $slug_endpoint = 'activity';
+
+	/**
+	 * Custom hooks.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function custom_hooks() {
+		add_action( 'oembed_dataparse',   array( $this, 'use_custom_iframe_sandbox_attribute' ), 20, 3 );
+		add_action( 'embed_content_meta', array( $this, 'embed_comments_button' ), 5 );
+		add_action( 'get_template_part_assets/embeds/header', array( $this, 'on_activity_header' ), 10, 2 );
+
+		add_filter( 'bp_activity_embed_html', array( $this, 'modify_iframe' ) );
+	}
+
+	/**
+	 * Add custom endpoint arguments.
+	 *
+	 * Currently, includes 'hide_media'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return array
+	 */
+	protected function set_route_args() {
+		return array(
+			'hide_media' => array(
+				'default' => false,
+				'sanitize_callback' => 'wp_validate_boolean'
+			)
+		);
+	}
+
+	/**
+	 * Output our custom embed template part.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function content() {
+		bp_get_asset_template_part( 'embeds/activity' );
+	}
+
+	/**
+	 * Check if we're on our single activity page.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return bool
+	 */
+	protected function is_page() {
+		return bp_is_single_activity();
+	}
+
+	/**
+	 * Validates the URL to determine if the activity item is valid.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string   $url The URL to check.
+	 * @return int|bool Activity ID on success; boolean false on failure.
+	 */
+	protected function validate_url_to_item_id( $url ) {
+		if ( bp_core_enable_root_profiles() ) {
+			$domain = bp_get_root_domain();
+		} else {
+			$domain = bp_get_members_directory_permalink();
+		}
+
+		// Check the URL to see if this is a single activity URL.
+		if ( 0 !== strpos( $url, $domain ) ) {
+			return false;
+		}
+
+		// Check for activity slug.
+		if ( false === strpos( $url, '/' . bp_get_activity_slug() . '/' ) ) {
+			return false;
+		}
+
+		// Do more checks.
+		$url = trim( untrailingslashit( $url ) );
+
+		// Grab the activity ID.
+		$activity_id = (int) substr(
+			$url,
+			strrpos( $url, '/' ) + 1
+		);
+
+		if ( ! empty( $activity_id ) ) {
+			// Check if activity item still exists.
+			$activity = new BP_Activity_Activity( $activity_id );
+
+			// Okay, we're good to go!
+			if ( ! empty( $activity->component ) && 0 === (int) $activity->is_spam ) {
+				return $activity_id;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Sets the oEmbed response data for our activity item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int $item_id The activity ID.
+	 * @return array
+	 */
+	protected function set_oembed_response_data( $item_id ) {
+		$activity = new BP_Activity_Activity( $item_id );
+
+		return array(
+			'content'      => $activity->content,
+			'title'        => __( 'Activity', 'buddypress' ),
+			'author_name'  => bp_core_get_user_displayname( $activity->user_id ),
+			'author_url'   => bp_core_get_user_domain( $activity->user_id ),
+
+			// Custom identifier.
+			'x_buddypress' => 'activity'
+		);
+	}
+
+	/**
+	 * Sets a custom <blockquote> for our oEmbed fallback HTML.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int $item_id The activity ID.
+	 * @return string
+	 */
+	protected function set_fallback_html( $item_id ) {
+		$activity    = new BP_Activity_Activity( $item_id );
+		$mentionname = bp_activity_do_mentions() ? ' (@' . bp_activity_get_user_mentionname( $activity->user_id ) . ')' : '';
+		$date        = date_i18n( get_option( 'date_format' ), strtotime( $activity->date_recorded ) );
+
+		// Make sure we can use some activity functions that depend on the loop.
+		$GLOBALS['activities_template'] = new stdClass;
+		$GLOBALS['activities_template']->activity = $activity;
+
+		// 'wp-embedded-content' CSS class is necessary due to how the embed JS works.
+		$blockquote = sprintf( '<blockquote class="wp-embedded-content bp-activity-item">%1$s%2$s %3$s</blockquote>',
+			bp_activity_get_embed_excerpt( $activity->content ),
+			'- ' . bp_core_get_user_displayname( $activity->user_id ) . $mentionname,
+			'<a href="' . esc_url( bp_activity_get_permalink( $item_id ) ) . '">' . $date . '</a>'
+		);
+
+		// Clean up.
+		unset( $GLOBALS['activities_template'] );
+
+		/**
+		 * Filters the fallback HTML used when embedding a BP activity item.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param string               $blockquote Current fallback HTML
+		 * @param BP_Activity_Activity $activity   Activity object
+		 */
+		return apply_filters( 'bp_activity_embed_fallback_html', $blockquote, $activity );
+	}
+
+	/**
+	 * Sets a custom <iframe> title for our oEmbed item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int $item_id The activity ID
+	 * @return string
+	 */
+	protected function set_iframe_title( $item_id ) {
+		return __( 'Embedded Activity Item', 'buddypress' );
+	}
+
+	/**
+	 * Use our custom <iframe> sandbox attribute in our oEmbed response.
+	 *
+	 * WordPress sets the <iframe> sandbox attribute to 'allow-scripts' regardless
+	 * of whatever the oEmbed response is in {@link wp_filter_oembed_result()}. We
+	 * need to add back our custom sandbox value so links will work.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @see BP_Activity_Component::modify_iframe() where our custom sandbox value is set.
+	 *
+	 * @param string $result The oEmbed HTML result.
+	 * @param object $data   A data object result from an oEmbed provider.
+	 * @param string $url    The URL of the content to be embedded.
+	 * @return string
+	 */
+	public function use_custom_iframe_sandbox_attribute( $result, $data, $url ) {
+		// Make sure we are on a BuddyPress activity oEmbed request.
+		if ( false === isset( $data->x_buddypress ) || 'activity' !== $data->x_buddypress ) {
+			return $result;
+		}
+
+		// Get unfiltered sandbox attribute from our own oEmbed response.
+		$sandbox_pos = strpos( $data->html, 'sandbox=' ) + 9;
+		$sandbox = substr( $data->html, $sandbox_pos, strpos( $data->html, '"', $sandbox_pos ) - $sandbox_pos );
+
+		// Replace only if our sandbox attribute contains 'allow-top-navigation'.
+		if ( false !== strpos( $sandbox, 'allow-top-navigation' ) ) {
+			$result = str_replace( ' sandbox="allow-scripts"', " sandbox=\"{$sandbox}\"", $result );
+
+			// Also remove 'security' attribute; this is only used for IE < 10.
+			$result = str_replace( 'security="restricted"', "", $result );
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Modify various IFRAME-related items if embeds are allowed.
+	 *
+	 * HTML modified:
+	 *  - Add sandbox="allow-top-navigation" attribute. This allows links to work
+	 *    within the iframe sandbox attribute.
+	 *
+	 * JS modified:
+	 *  - Remove IFRAME height restriction of 1000px. Fixes long embed items being
+	 *    truncated.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $retval Current embed HTML.
+	 * @return string
+	 */
+	public function modify_iframe( $retval ) {
+		// Add 'allow-top-navigation' to allow links to be clicked.
+		$retval = str_replace( 'sandbox="', 'sandbox="allow-top-navigation ', $retval );
+
+		// See /wp-includes/js/wp-embed.js.
+		if ( SCRIPT_DEBUG ) {
+			// Removes WP's hardcoded IFRAME height restriction.
+			$retval = str_replace( 'height = 1000;', 'height = height;', $retval );
+
+		// This is for the WP build minified version.
+		} else {
+			$retval = str_replace( 'g=1e3', 'g=g', $retval );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Do stuff when our oEmbed activity header template part is loading.
+	 *
+	 * Currently, removes wpautop() from the bp_activity_action() function.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $slug Template part slug requested.
+	 * @param string $name Template part name requested.
+	 */
+	public function on_activity_header( $slug, $name ) {
+		if ( false === $this->is_page() || 'activity' !== $name ) {
+			return;
+		}
+
+		remove_filter( 'bp_get_activity_action', 'wpautop' );
+	}
+
+	/**
+	 * Prints the markup for the activity embed comments button.
+	 *
+	 * Basically a copy of {@link print_embed_comments_button()}, but modified for
+	 * the BP activity component.
+	 *
+	 * @since 2.6.0
+	 */
+	public function embed_comments_button() {
+		if ( ! did_action( 'bp_embed_content' ) || ! bp_is_single_activity() ) {
+			return;
+		}
+
+		// Make sure our custom permalink shows up in the 'WordPress Embed' block.
+		add_filter( 'the_permalink', array( $this, 'filter_embed_url' ) );
+
+		// Only show comment bubble if we have some activity comments.
+		$count = bp_activity_get_comment_count();
+		if ( empty( $count ) ) {
+			return;
+		}
+	?>
+
+		<div class="wp-embed-comments">
+			<a href="<?php bp_activity_thread_permalink(); ?>">
+				<span class="dashicons dashicons-admin-comments"></span>
+				<?php
+				printf(
+					_n(
+						/* translators: accessibility text */
+						'%s <span class="screen-reader-text">Comment</span>',
+						/* translators: accessibility text */
+						'%s <span class="screen-reader-text">Comments</span>',
+						$count,
+						'buddypress'
+					),
+					number_format_i18n( $count )
+				);
+				?>
+			</a>
+		</div>
+
+	<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-query.php	(revision 12265)
@@ -0,0 +1,247 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @subpackage ActivityQuery
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * 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 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 2.2.0
+	 * @var array
+	 */
+	public $queries = array();
+
+	/**
+	 * Table alias.
+	 *
+	 * @since 2.2.0
+	 * @var string
+	 */
+	public $table_alias = '';
+
+	/**
+	 * Supported DB columns.
+	 *
+	 * See the 'wp_bp_activity' DB table schema.
+	 *
+	 * @since 2.2.0
+	 * @var array
+	 */
+	public $db_columns = array(
+		'id', 'user_id', 'component', 'type', 'action', 'content', 'primary_link',
+		'item_id', 'secondary_item_id', 'hide_sitewide', 'is_spam',
+	);
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 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 2.2.0
+	 *
+	 * @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 2.2.0
+	 *
+	 * @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'
+		) ) ) {
+			$clause['compare'] = '=';
+		}
+
+		$compare = $clause['compare'];
+
+		$alias = ! empty( $this->table_alias ) ? "{$this->table_alias}." : '';
+
+		// Next, Build the WHERE clause.
+		$where = '';
+
+		// Value.
+		if ( isset( $clause['value'] ) ) {
+			if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
+				if ( ! is_array( $value ) ) {
+					$value = preg_split( '/[,\s]+/', $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 ) {
+				$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 2.2.0
+	 *
+	 * @param array $query 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 2.2.0
+	 *
+	 * @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 '';
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-template.php	(revision 12265)
@@ -0,0 +1,408 @@
+<?php
+/**
+ * BuddyPress Activity Template.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main activity template loop class.
+ *
+ * This is responsible for loading a group of activity items and displaying them.
+ *
+ * @since 1.0.0
+ */
+class BP_Activity_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $current_activity = -1;
+
+	/**
+	 * The activity count.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $activity_count;
+
+	/**
+	 * The total activity count.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $total_activity_count;
+
+	/**
+	 * Array of activities located by the query.
+	 *
+	 * @since 1.0.0
+	 * @var array
+	 */
+	public $activities;
+
+	/**
+	 * The activity object currently being iterated on.
+	 *
+	 * @since 1.0.0
+	 * @var object
+	 */
+	public $activity;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @since 1.0.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * URL parameter key for activity pagination. Default: 'acpage'.
+	 *
+	 * @since 2.1.0
+	 * @var string
+	 */
+	public $pag_arg;
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $pag_page;
+
+	/**
+	 * The number of items being requested per page.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $pag_num;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	public $pag_links;
+
+	/**
+	 * The displayed user's full name.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	public $full_name;
+
+	/**
+	 * Constructor method.
+	 *
+	 * The arguments passed to this class constructor are of the same
+	 * format as {@link BP_Activity_Activity::get()}.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Activity_Activity::get() for a description of the argument
+	 *      structure, as well as default values.
+	 *
+	 * @param array $args {
+	 *     Array of arguments. Supports all arguments from
+	 *     BP_Activity_Activity::get(), as well as 'page_arg' and
+	 *     'include'. Default values for 'per_page' and 'display_comments'
+	 *     differ from the originating function, and are described below.
+	 *     @type string      $page_arg         The string used as a query parameter in
+	 *                                         pagination links. Default: 'acpage'.
+	 *     @type array|bool  $include          Pass an array of activity IDs to
+	 *                                         retrieve only those items, or false to noop the 'include'
+	 *                                         parameter. 'include' differs from 'in' in that 'in' forms
+	 *                                         an IN clause that works in conjunction with other filters
+	 *                                         passed to the function, while 'include' is interpreted as
+	 *                                         an exact list of items to retrieve, which skips all other
+	 *                                         filter-related parameters. Default: false.
+	 *     @type int|bool    $per_page         Default: 20.
+	 *     @type string|bool $display_comments Default: 'threaded'.
+	 * }
+	 */
+	public function __construct( $args ) {
+		$bp = buddypress();
+
+		// Backward compatibility with old method of passing arguments.
+		if ( !is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '1.6', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'page',
+				1 => 'per_page',
+				2 => 'max',
+				3 => 'include',
+				4 => 'sort',
+				5 => 'filter',
+				6 => 'search_terms',
+				7 => 'display_comments',
+				8 => 'show_hidden',
+				9 => 'exclude',
+				10 => 'in',
+				11 => 'spam',
+				12 => 'page_arg'
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$defaults = array(
+			'page'              => 1,
+			'per_page'          => 20,
+			'page_arg'          => 'acpage',
+			'max'               => false,
+			'fields'            => 'all',
+			'count_total'       => false,
+			'sort'              => false,
+			'include'           => false,
+			'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',
+			'update_meta_cache' => true,
+		);
+		$r = wp_parse_args( $args, $defaults );
+		extract( $r );
+
+		$this->pag_arg  = sanitize_key( $r['page_arg'] );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+
+		// Check if post/comment replies are disabled.
+		$this->disable_blogforum_replies = (bool) bp_core_get_root_option( 'bp-disable-blogforum-comments' );
+
+		// Get an array of the logged in user's favorite activities.
+		$this->my_favs = bp_get_user_meta( bp_loggedin_user_id(), 'bp_favorite_activities', true );
+
+		// Fetch specific activity items based on ID's.
+		if ( !empty( $include ) ) {
+			$this->activities = bp_activity_get_specific( array(
+				'activity_ids'      => explode( ',', $include ),
+				'max'               => $max,
+				'count_total'       => $count_total,
+				'page'              => $this->pag_page,
+				'per_page'          => $this->pag_num,
+				'sort'              => $sort,
+				'display_comments'  => $display_comments,
+				'show_hidden'       => $show_hidden,
+				'spam'              => $spam,
+				'update_meta_cache' => $update_meta_cache,
+			) );
+
+		// Fetch all activity items.
+		} else {
+			$this->activities = bp_activity_get( array(
+				'display_comments'  => $display_comments,
+				'max'               => $max,
+				'count_total'       => $count_total,
+				'per_page'          => $this->pag_num,
+				'page'              => $this->pag_page,
+				'sort'              => $sort,
+				'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,
+				'spam'              => $spam,
+				'update_meta_cache' => $update_meta_cache,
+			) );
+		}
+
+		// The total_activity_count property will be set only if a
+		// 'count_total' query has taken place.
+		if ( ! is_null( $this->activities['total'] ) ) {
+			if ( ! $max || $max >= (int) $this->activities['total'] ) {
+				$this->total_activity_count = (int) $this->activities['total'];
+			} else {
+				$this->total_activity_count = (int) $max;
+			}
+		}
+
+		$this->has_more_items = $this->activities['has_more_items'];
+
+		$this->activities = $this->activities['activities'];
+
+		if ( $max ) {
+			if ( $max >= count($this->activities) ) {
+				$this->activity_count = count( $this->activities );
+			} else {
+				$this->activity_count = (int) $max;
+			}
+		} else {
+			$this->activity_count = count( $this->activities );
+		}
+
+		$this->full_name = bp_get_displayed_user_fullname();
+
+		// Fetch parent content for activity comments so we do not have to query in the loop.
+		foreach ( (array) $this->activities as $activity ) {
+			if ( 'activity_comment' != $activity->type ) {
+				continue;
+			}
+
+			$parent_ids[] = $activity->item_id;
+		}
+
+		if ( !empty( $parent_ids ) ) {
+			$activity_parents = bp_activity_get_specific( array( 'activity_ids' => $parent_ids ) );
+		}
+
+		if ( !empty( $activity_parents['activities'] ) ) {
+			foreach( $activity_parents['activities'] as $parent ) {
+				$this->activity_parents[$parent->id] = $parent;
+			}
+
+			unset( $activity_parents );
+		}
+
+		if ( (int) $this->total_activity_count && (int) $this->pag_num ) {
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $this->pag_arg, '%#%' ),
+				'format'    => '',
+				'total'     => ceil( (int) $this->total_activity_count / (int) $this->pag_num ),
+				'current'   => (int) $this->pag_page,
+				'prev_text' => _x( '&larr;', 'Activity pagination previous text', 'buddypress' ),
+				'next_text' => _x( '&rarr;', 'Activity pagination next text', 'buddypress' ),
+				'mid_size'  => 1,
+				'add_args'  => array(),
+			) );
+		}
+	}
+
+	/**
+	 * Whether there are activity items available in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see bp_has_activities()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	function has_activities() {
+		if ( $this->activity_count ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set up the next activity item and iterate index.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return object The next activity item to iterate over.
+	 */
+	public function next_activity() {
+		$this->current_activity++;
+		$this->activity = $this->activities[ $this->current_activity ];
+
+		return $this->activity;
+	}
+
+	/**
+	 * Rewind the posts and reset post index.
+	 *
+	 * @since 1.0.0
+	 */
+	public function rewind_activities() {
+		$this->current_activity = -1;
+		if ( $this->activity_count > 0 ) {
+			$this->activity = $this->activities[0];
+		}
+	}
+
+	/**
+	 * Whether there are activity items left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_activities()} as part of the while loop
+	 * that controls iteration inside the activities loop, eg:
+	 *     while ( bp_activities() ) { ...
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see bp_activities()
+	 *
+	 * @return bool True if there are more activity items to show,
+	 *              otherwise false.
+	 */
+	public function user_activities() {
+		if ( ( $this->current_activity + 1 ) < $this->activity_count ) {
+			return true;
+		} elseif ( ( $this->current_activity + 1 ) == $this->activity_count ) {
+
+			/**
+			 * Fires right before the rewinding of activity posts.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'activity_loop_end' );
+
+			// Do some cleaning up after the loop.
+			$this->rewind_activities();
+		}
+
+		$this->in_the_loop = false;
+
+		return false;
+	}
+
+	/**
+	 * Set up the current activity item inside the loop.
+	 *
+	 * Used by {@link bp_the_activity()} to set up the current activity item
+	 * data while looping, so that template tags used during that iteration
+	 * make reference to the current activity item.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see bp_the_activity()
+	 */
+	public function the_activity() {
+
+		$this->in_the_loop = true;
+		$this->activity    = $this->next_activity();
+
+		if ( is_array( $this->activity ) ) {
+			$this->activity = (object) $this->activity;
+		}
+
+		// Loop has just started.
+		if ( $this->current_activity == 0 ) {
+
+			/**
+			 * Fires if the current activity item is the first in the activity loop.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action('activity_loop_start');
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-theme-compat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-theme-compat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-activity-theme-compat.php	(revision 12265)
@@ -0,0 +1,180 @@
+<?php
+/**
+ * BuddyPress Activity Theme Compatibility.
+ *
+ * @package BuddyPress
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main theme compat class for BuddyPress Activity.
+ *
+ * This class sets up the necessary theme compatibility actions to safely output
+ * activity template parts to the_title and the_content areas of a theme.
+ *
+ * @since 1.7.0
+ */
+class BP_Activity_Theme_Compat {
+
+	/**
+	 * Set up the activity component theme compatibility.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __construct() {
+		add_action( 'bp_setup_theme_compat', array( $this, 'is_activity' ) );
+	}
+
+	/**
+	 * Set up the theme compatibility hooks, if we're looking at an activity page.
+	 *
+	 * @since 1.7.0
+	 */
+	public function is_activity() {
+
+		// Bail if not looking at a group.
+		if ( ! bp_is_activity_component() )
+			return;
+
+		// Activity Directory.
+		if ( ! bp_displayed_user_id() && ! bp_current_action() ) {
+			bp_update_is_directory( true, 'activity' );
+
+			/** This action is documented in bp-activity/bp-activity-screens.php */
+			do_action( 'bp_activity_screen_index' );
+
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'directory_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'directory_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'directory_content'    ) );
+
+		// Single activity.
+		} elseif ( bp_is_single_activity() ) {
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'single_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'single_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'single_dummy_content'    ) );
+		}
+	}
+
+	/** Directory *************************************************************/
+
+	/**
+	 * Add template hierarchy to theme compat for the activity directory page.
+	 *
+	 * This is to mirror how WordPress has {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function directory_template_hierarchy( $templates ) {
+
+		/**
+		 * Filters the template hierarchy for the activity directory page.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $index-directory Array holding template names to be merged into template list.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_activity_directory', array(
+			'activity/index-directory.php'
+		) );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with directory data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_dummy_post() {
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => bp_get_directory_title( 'activity' ),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the groups index template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_content() {
+		return bp_buffer_template_part( 'activity/index', null, false );
+	}
+
+	/** Single ****************************************************************/
+
+	/**
+	 * Add custom template hierarchy to theme compat for activity permalink pages.
+	 *
+	 * This is to mirror how WordPress has {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function single_template_hierarchy( $templates ) {
+
+		/**
+		 * Filters the template hierarchy for the activity permalink pages.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $index Array holding template names to be merged into template list.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_activity_single_item', array(
+			'activity/single/index.php'
+		) );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with the displayed user's data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function single_dummy_post() {
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => __( 'Activity', 'buddypress' ),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the members' activity permalink template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function single_dummy_content() {
+		return bp_buffer_template_part( 'activity/single/home', null, false );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-akismet.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-akismet.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/classes/class-bp-akismet.php	(revision 12265)
@@ -0,0 +1,664 @@
+<?php
+/**
+ * Akismet support for BuddyPress' Activity Stream.
+ *
+ * @package BuddyPress
+ * @subpackage ActivityAkismet
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Akismet support for the Activity component.
+ *
+ * @since 1.6.0
+ * @since 2.3.0 We only support Akismet 3+.
+ */
+class BP_Akismet {
+
+	/**
+	 * The activity last marked as spam.
+	 *
+	 * @since 1.6.0
+	 * @var BP_Activity_Activity
+	 */
+	protected $last_activity = null;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.6.0
+	 */
+	public function __construct() {
+		$this->setup_actions();
+	}
+
+	/**
+	 * Hook Akismet into the activity stream.
+	 *
+	 * @since 1.6.0
+	 */
+	protected function setup_actions() {
+		// Add nonces to activity stream lists.
+		add_action( 'bp_after_activity_post_form', array( $this, 'add_activity_stream_nonce' ) );
+		add_action( 'bp_activity_entry_comments',  array( $this, 'add_activity_stream_nonce' ) );
+
+		// Add a "mark as spam" button to individual activity items.
+		add_action( 'bp_activity_entry_meta',      array( $this, 'add_activity_spam_button' ) );
+		add_action( 'bp_activity_comment_options', array( $this, 'add_activity_comment_spam_button' ) );
+
+		// Check activity for spam.
+		add_action( 'bp_activity_before_save',     array( $this, 'check_activity' ), 4, 1 );
+
+		// Tidy up member's latest (activity) update.
+		add_action( 'bp_activity_posted_update',   array( $this, 'check_member_activity_update' ), 1, 3 );
+
+		// Hooks to extend Activity core spam/ham functions for Akismet.
+		add_action( 'bp_activity_mark_as_spam',    array( $this, 'mark_as_spam' ), 10, 2 );
+		add_action( 'bp_activity_mark_as_ham',     array( $this, 'mark_as_ham' ),  10, 2 );
+
+		// Hook into the Activity wp-admin screen.
+		add_action( 'bp_activity_admin_comment_row_actions', array( $this, 'comment_row_action' ), 10, 2 );
+		add_action( 'bp_activity_admin_load',                array( $this, 'add_history_metabox' ) );
+	}
+
+	/**
+	 * Add a history item to the hover links in an activity's row.
+	 *
+	 * This function lifted with love from the Akismet WordPress plugin's
+	 * akismet_comment_row_action() function. Thanks!
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $actions  The hover links.
+	 * @param array $activity The activity for the current row being processed.
+	 * @return array The hover links.
+	 */
+	function comment_row_action( $actions, $activity ) {
+		$akismet_result = bp_activity_get_meta( $activity['id'], '_bp_akismet_result' );
+		$user_result    = bp_activity_get_meta( $activity['id'], '_bp_akismet_user_result' );
+		$desc           = '';
+
+		if ( !$user_result || $user_result == $akismet_result ) {
+			// Show the original Akismet result if the user hasn't overridden it, or if their decision was the same.
+			if ( 'true' == $akismet_result && $activity['is_spam'] )
+				$desc = __( 'Flagged as spam by Akismet', 'buddypress' );
+
+			elseif ( 'false' == $akismet_result && !$activity['is_spam'] )
+				$desc = __( 'Cleared by Akismet', 'buddypress' );
+
+		} else {
+			$who = bp_activity_get_meta( $activity['id'], '_bp_akismet_user' );
+
+			if ( 'true' == $user_result )
+				$desc = sprintf( __( 'Flagged as spam by %s', 'buddypress' ), $who );
+			else
+				$desc = sprintf( __( 'Un-spammed by %s', 'buddypress' ), $who );
+		}
+
+		// Add a History item to the hover links, just after Edit.
+		if ( $akismet_result ) {
+			$b = array();
+			foreach ( $actions as $k => $item ) {
+				$b[ $k ] = $item;
+				if ( $k == 'edit' )
+					$b['history'] = '<a href="' . esc_url( bp_get_admin_url( 'admin.php?page=bp-activity&amp;action=edit&aid=' . $activity['id'] ) ) . '#bp_activity_history"> '. __( 'History', 'buddypress' ) . '</a>';
+			}
+
+			$actions = $b;
+		}
+
+		if ( $desc )
+			echo '<span class="akismet-status"><a href="' . esc_url( bp_get_admin_url( 'admin.php?page=bp-activity&amp;action=edit&aid=' . $activity['id'] ) ) . '#bp_activity_history">' . htmlspecialchars( $desc ) . '</a></span>';
+
+		/**
+		 * Filters the list of actions for the current activity's row.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $actions Array of available actions for the current activity item's row.
+		 */
+		return apply_filters( 'bp_akismet_comment_row_action', $actions );
+	}
+
+	/**
+	 * Generate nonces for activity forms.
+	 *
+	 * These nonces appear in the member profile status form, as well as in
+	 * the reply form of each activity item. The nonces are, in turn, used
+	 * by Akismet to help detect spam activity.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see https://plugins.trac.wordpress.org/ticket/1232
+	 */
+	public function add_activity_stream_nonce() {
+		$form_id = '_bp_as_nonce';
+		$value   = '_bp_as_nonce_' . bp_loggedin_user_id();
+
+		// If we're in the activity stream loop, we can use the current item's ID to make the nonce unique.
+		if ( 'bp_activity_entry_comments' == current_filter() ) {
+			$form_id .= '_' . bp_get_activity_id();
+			$value   .= '_' . bp_get_activity_id();
+		}
+
+		wp_nonce_field( $value, $form_id, false );
+	}
+
+	/**
+	 * Clean up the bp_latest_update usermeta in case of spamming.
+	 *
+	 * Run just after an update is posted, this method check to see whether
+	 * the newly created update has been marked as spam by Akismet. If so,
+	 * the cached update is cleared from the user's 'bp_latest_update'
+	 * usermeta, ensuring that it won't appear in the member header and
+	 * elsewhere in the theme.
+	 *
+	 * This can't be done in BP_Akismet::check_activity() due to the
+	 * default AJAX implementation; see bp_dtheme_post_update().
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see bp_dtheme_post_update()
+	 *
+	 * @param string $content     Activity update text.
+	 * @param int    $user_id     User ID.
+	 * @param int    $activity_id Activity ID.
+	 */
+	public function check_member_activity_update( $content, $user_id, $activity_id ) {
+		// By default, only handle activity updates and activity comments.
+		if ( empty( $this->last_activity ) || !in_array( $this->last_activity->type, BP_Akismet::get_activity_types() ) )
+			return;
+
+		// Was this $activity_id just marked as spam? If not, bail out.
+		if ( !$this->last_activity->id || $activity_id != $this->last_activity->id || 'false' == $this->last_activity->akismet_submission['bp_as_result'] )
+			return;
+
+		// It was, so delete the member's latest activity update.
+		bp_delete_user_meta( $user_id, 'bp_latest_update' );
+	}
+
+	/**
+	 * Adds a "mark as spam" button to each activity item for site admins.
+	 *
+	 * This function is intended to be used inside the activity stream loop.
+	 *
+	 * @since 1.6.0
+	 */
+	public function add_activity_spam_button() {
+		if ( !bp_activity_user_can_mark_spam() )
+			return;
+
+		// By default, only handle activity updates and activity comments.
+		if ( !in_array( bp_get_activity_type(), BP_Akismet::get_activity_types() ) )
+			return;
+
+		bp_button(
+			array(
+				'block_self' => false,
+				'component'  => 'activity',
+				'id'         => 'activity_make_spam_' . bp_get_activity_id(),
+				'link_class' => 'bp-secondary-action spam-activity confirm button item-button',
+				'link_href'  => wp_nonce_url( bp_get_root_domain() . '/' . bp_get_activity_slug() . '/spam/' . bp_get_activity_id() . '/', 'bp_activity_akismet_spam_' . bp_get_activity_id() ),
+				'link_text'  => __( 'Spam', 'buddypress' ),
+				'wrapper'    => false,
+			)
+		);
+	}
+
+	/**
+	 * Adds a "mark as spam" button to each activity COMMENT item for site admins.
+	 *
+	 * This function is intended to be used inside the activity stream loop.
+	 *
+	 * @since 1.6.0
+	 */
+	public function add_activity_comment_spam_button() {
+		if ( !bp_activity_user_can_mark_spam() )
+			return;
+
+		// By default, only handle activity updates and activity comments.
+		$current_comment = bp_activity_current_comment();
+		if ( empty( $current_comment ) || !in_array( $current_comment->type, BP_Akismet::get_activity_types() ) )
+			return;
+
+		bp_button(
+			array(
+				'block_self' => false,
+				'component'  => 'activity',
+				'id'         => 'activity_make_spam_' . bp_get_activity_comment_id(),
+				'link_class' => 'bp-secondary-action spam-activity-comment confirm',
+				'link_href'  => wp_nonce_url( bp_get_root_domain() . '/' . bp_get_activity_slug() . '/spam/' . bp_get_activity_comment_id() . '/?cid=' . bp_get_activity_comment_id(), 'bp_activity_akismet_spam_' . bp_get_activity_comment_id() ),
+				'link_text'  => __( 'Spam', 'buddypress' ),
+				'wrapper'    => false,
+			)
+		);
+	}
+
+	/**
+	 * Get a filterable list of activity types that Akismet should automatically check for spam.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @static
+	 *
+	 * @return array $value List of activity types.
+	 */
+	public static function get_activity_types() {
+
+		/**
+		 * Filters the list of activity types that Akismet should automatically check for spam.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array $value Array of default activity types for Akismet to check.
+		 */
+		return apply_filters( 'bp_akismet_get_activity_types', array( 'activity_comment', 'activity_update' ) );
+	}
+
+	/**
+	 * Mark activity item as spam.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity Activity item being spammed.
+	 * @param string               $source   Either "by_a_person" (e.g. a person has
+	 *                                       manually marked the activity as spam) or
+	 *                                       "by_akismet" (automatically spammed).
+	 */
+	public function mark_as_spam( $activity, $source ) {
+		// Record this item so we can do some tidyup in BP_Akismet::check_member_activity_update().
+		$this->last_activity = $activity;
+
+		/**
+		 * Fires after marking an activity item has been marked as spam.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param BP_Activity_Activity $activity Activity object being marked as spam.
+		 * @param string               $source   Source of the whom marked as spam.
+		 *                                       Either "by_a_person" (e.g. a person has
+		 *                                       manually marked the activity as spam)
+		 *                                       or "by_akismet".
+		 */
+		do_action( 'bp_activity_akismet_mark_as_spam', $activity, $source );
+	}
+
+	/**
+	 * Mark activity item as ham.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity Activity item being hammed.
+	 * @param string               $source   Either "by_a_person" (e.g. a person has
+	 *                                       manually marked the activity as ham) or
+	 *                                       "by_akismet" (automatically hammed).
+	 */
+	public function mark_as_ham( $activity, $source ) {
+		// If the activity was, originally, automatically marked as spam by Akismet, run the @mentions filter as it would have been skipped.
+		if ( 'true' == bp_activity_get_meta( $activity->id, '_bp_akismet_result' ) && !bp_activity_get_meta( $activity->id, '_bp_akismet_user_result' ) )
+			$activity->content = bp_activity_at_name_filter( $activity->content, $activity->id );
+
+		/**
+		 * Fires after marking an activity item has been marked as ham.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param BP_Activity_Activity $activity Activity object being marked as ham.
+		 * @param string               $source   Source of the whom marked as ham.
+		 *                                       Either "by_a_person" (e.g. a person has
+		 *                                       manually marked the activity as ham) or
+		 *                                       "by_akismet" (automatically hammed).
+		 */
+		do_action( 'bp_activity_akismet_mark_as_ham', $activity, $source );
+	}
+
+	/**
+	 * Build a data package for the Akismet service to inspect.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see http://akismet.com/development/api/#comment-check
+	 * @static
+	 *
+	 * @param BP_Activity_Activity $activity Activity item data.
+	 * @return array $activity_data
+	 */
+	public static function build_akismet_data_package( $activity ) {
+		$userdata = get_userdata( $activity->user_id );
+
+		$activity_data                          = array();
+		$activity_data['akismet_comment_nonce'] = 'inactive';
+		$activity_data['comment_author']        = $userdata->display_name;
+		$activity_data['comment_author_email']  = $userdata->user_email;
+		$activity_data['comment_author_url']    = bp_core_get_userlink( $userdata->ID, false, true);
+		$activity_data['comment_content']       = $activity->content;
+		$activity_data['comment_type']          = $activity->type;
+		$activity_data['permalink']             = bp_activity_get_permalink( $activity->id, $activity );
+		$activity_data['user_ID']               = $userdata->ID;
+		$activity_data['user_role']             = Akismet::get_user_roles( $userdata->ID );
+
+		/**
+		 * Get the nonce if the new activity was submitted through the "what's up, Paul?" form.
+		 * This helps Akismet ensure that the update was a valid form submission.
+		 */
+		if ( !empty( $_POST['_bp_as_nonce'] ) )
+			$activity_data['akismet_comment_nonce'] = wp_verify_nonce( $_POST['_bp_as_nonce'], "_bp_as_nonce_{$userdata->ID}" ) ? 'passed' : 'failed';
+
+		/**
+		 * If the new activity was a reply to an existing item, check the nonce with the activity parent ID.
+		 * This helps Akismet ensure that the update was a valid form submission.
+		 */
+		elseif ( !empty( $activity->secondary_item_id ) && !empty( $_POST['_bp_as_nonce_' . $activity->secondary_item_id] ) )
+			$activity_data['akismet_comment_nonce'] = wp_verify_nonce( $_POST["_bp_as_nonce_{$activity->secondary_item_id}"], "_bp_as_nonce_{$userdata->ID}_{$activity->secondary_item_id}" ) ? 'passed' : 'failed';
+
+		/**
+		 * Filters activity data before being sent to Akismet to inspect.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param array                $activity_data Array of activity data for Akismet to inspect.
+		 * @param BP_Activity_Activity $activity      Activity item data.
+		 */
+		return apply_filters( 'bp_akismet_build_akismet_data_package', $activity_data, $activity );
+	}
+
+	/**
+	 * Check if the activity item is spam or ham.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see http://akismet.com/development/api/
+	 * @todo Spam counter?
+	 * @todo Auto-delete old spam?
+	 *
+	 * @param BP_Activity_Activity $activity The activity item to check.
+	 */
+	public function check_activity( $activity ) {
+		// By default, only handle activity updates and activity comments.
+		if ( !in_array( $activity->type, BP_Akismet::get_activity_types() ) )
+			return;
+
+		// Make sure last_activity is clear to avoid any confusion.
+		$this->last_activity = null;
+
+		// Build data package for Akismet.
+		$activity_data = BP_Akismet::build_akismet_data_package( $activity );
+
+		// Check with Akismet to see if this is spam.
+		$activity_data = $this->send_akismet_request( $activity_data, 'check', 'spam' );
+
+		// Record this item.
+		$this->last_activity = $activity;
+
+		// Store a copy of the data that was submitted to Akismet.
+		$this->last_activity->akismet_submission = $activity_data;
+
+		// Spam.
+		if ( 'true' == $activity_data['bp_as_result'] ) {
+			/**
+			 * Fires after an activity item has been proven to be spam, but before officially being marked as spam.
+			 *
+			 * @since 1.6.0
+			 *
+			 * @param BP_Activity_Activity $activity      The activity item proven to be spam.
+			 * @param array                $activity_data Array of activity data for item including
+			 *                                            Akismet check results data.
+			 */
+			do_action_ref_array( 'bp_activity_akismet_spam_caught', array( &$activity, $activity_data ) );
+
+			// Mark as spam.
+			bp_activity_mark_as_spam( $activity, 'by_akismet' );
+
+			if (
+				Akismet::allow_discard() &&
+				! empty( $activity_data['akismet_pro_tip'] ) &&
+				'discard' === $activity_data['akismet_pro_tip']
+			) {
+				// If this is so spammy it's not worth your time, let's just delete it.
+				if ( $activity->type === 'activity_comment' ) {
+					bp_activity_delete_comment( $activity->item_id, $activity->id );
+				} else {
+					bp_activity_delete( array( 'id' => $activity->id ) );
+				}
+			}
+		}
+
+		// Update activity meta after a spam check.
+		add_action( 'bp_activity_after_save', array( $this, 'update_activity_akismet_meta' ), 1, 1 );
+	}
+
+	/**
+	 * Update activity meta after a manual spam change (user-initiated).
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity The activity to check.
+	 */
+	public function update_activity_spam_meta( $activity ) {
+		// By default, only handle activity updates and activity comments.
+		if ( !in_array( $activity->type, BP_Akismet::get_activity_types() ) )
+			return;
+
+		$this->update_activity_history( $activity->id, sprintf( __( '%s reported this activity as spam', 'buddypress' ), bp_get_loggedin_user_username() ), 'report-spam' );
+		bp_activity_update_meta( $activity->id, '_bp_akismet_user_result', 'true' );
+		bp_activity_update_meta( $activity->id, '_bp_akismet_user', bp_get_loggedin_user_username() );
+	}
+
+	/**
+	 * Update activity meta after a manual ham change (user-initiated).
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity The activity to check.
+	 */
+	public function update_activity_ham_meta( $activity ) {
+		// By default, only handle activity updates and activity comments.
+		if ( !in_array( $activity->type, BP_Akismet::get_activity_types() ) )
+			return;
+
+		$this->update_activity_history( $activity->id, sprintf( __( '%s reported this activity as not spam', 'buddypress' ), bp_get_loggedin_user_username() ), 'report-ham' );
+		bp_activity_update_meta( $activity->id, '_bp_akismet_user_result', 'false' );
+		bp_activity_update_meta( $activity->id, '_bp_akismet_user', bp_get_loggedin_user_username() );
+	}
+
+	/**
+	 * Update activity meta after an automatic spam check (not user-initiated).
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param BP_Activity_Activity $activity The activity to check.
+	 */
+	public function update_activity_akismet_meta( $activity ) {
+		// Check we're dealing with what was last updated by Akismet.
+		if ( empty( $this->last_activity ) || !empty( $this->last_activity ) && $activity->id != $this->last_activity->id )
+			return;
+
+		// By default, only handle activity updates and activity comments.
+		if ( !in_array( $this->last_activity->type, BP_Akismet::get_activity_types() ) )
+			return;
+
+		// Spam.
+		if ( 'true' == $this->last_activity->akismet_submission['bp_as_result'] ) {
+			bp_activity_update_meta( $activity->id, '_bp_akismet_result', 'true' );
+			$this->update_activity_history( $activity->id, __( 'Akismet caught this item as spam', 'buddypress' ), 'check-spam' );
+
+		// Not spam.
+		} elseif ( 'false' == $this->last_activity->akismet_submission['bp_as_result'] ) {
+			bp_activity_update_meta( $activity->id, '_bp_akismet_result', 'false' );
+			$this->update_activity_history( $activity->id, __( 'Akismet cleared this item', 'buddypress' ), 'check-ham' );
+
+		// Uh oh, something's gone horribly wrong. Unexpected result.
+		} else {
+			bp_activity_update_meta( $activity->id, '_bp_akismet_error', bp_core_current_time() );
+			$this->update_activity_history( $activity->id, sprintf( __( 'Akismet was unable to check this item (response: %s), will automatically retry again later.', 'buddypress' ), $this->last_activity->akismet_submission['bp_as_result'] ), 'check-error' );
+		}
+
+		// Record the original data which was submitted to Akismet for checking.
+		bp_activity_update_meta( $activity->id, '_bp_akismet_submission', $this->last_activity->akismet_submission );
+	}
+
+	/**
+	 * Contact Akismet to check if this is spam or ham.
+	 *
+	 * Props to WordPress core Akismet plugin for a lot of this.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array  $activity_data Packet of information to submit to Akismet.
+	 * @param string $check         "check" or "submit".
+	 * @param string $spam          "spam" or "ham".
+	 * @return array $activity_data Activity data, with Akismet data added.
+	 */
+	public function send_akismet_request( $activity_data, $check = 'check', $spam = 'spam' ) {
+		$query_string = $path = '';
+
+		$activity_data['blog']         = bp_get_option( 'home' );
+		$activity_data['blog_charset'] = bp_get_option( 'blog_charset' );
+		$activity_data['blog_lang']    = get_locale();
+		$activity_data['referrer']     = $_SERVER['HTTP_REFERER'];
+		$activity_data['user_agent']   = bp_core_current_user_ua();
+		$activity_data['user_ip']      = bp_core_current_user_ip();
+
+		if ( Akismet::is_test_mode() )
+			$activity_data['is_test'] = 'true';
+
+		// Loop through _POST args and rekey strings.
+		foreach ( $_POST as $key => $value )
+			if ( is_string( $value ) && 'cookie' != $key )
+				$activity_data['POST_' . $key] = $value;
+
+		// Keys to ignore.
+		$ignore = array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'PHP_AUTH_PW' );
+
+		// Loop through _SERVER args and remove whitelisted keys.
+		foreach ( $_SERVER as $key => $value ) {
+
+			// Key should not be ignored.
+			if ( !in_array( $key, $ignore ) && is_string( $value ) ) {
+				$activity_data[$key] = $value;
+
+			// Key should be ignored.
+			} else {
+				$activity_data[$key] = '';
+			}
+		}
+
+		foreach ( $activity_data as $key => $data )
+			$query_string .= $key . '=' . urlencode( stripslashes( $data ) ) . '&';
+
+		if ( 'check' == $check )
+			$path = 'comment-check';
+		elseif ( 'submit' == $check )
+			$path = 'submit-' . $spam;
+
+		// Send to Akismet.
+		add_filter( 'akismet_ua', array( $this, 'buddypress_ua' ) );
+		$response = Akismet::http_post( $query_string, $path );
+		remove_filter( 'akismet_ua', array( $this, 'buddypress_ua' ) );
+
+		// Save response data.
+		$activity_data['bp_as_result'] = $response[1];
+		if ( isset( $response[0]['x-akismet-pro-tip'] ) ) {
+			$activity_data['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip'];
+		}
+
+		// Perform a daily tidy up.
+		if ( ! wp_next_scheduled( 'bp_activity_akismet_delete_old_metadata' ) )
+			wp_schedule_event( time(), 'daily', 'bp_activity_akismet_delete_old_metadata' );
+
+		return $activity_data;
+	}
+
+	/**
+	 * Filters user agent when sending to Akismet to add BuddyPress info.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $user_agent User agent string, as generated by Akismet.
+	 * @return string $user_agent Modified user agent string.
+	 */
+	public function buddypress_ua( $user_agent ) {
+		$user_agent = 'BuddyPress/' . bp_get_version() . ' | Akismet/'. constant( 'AKISMET_VERSION' );
+		return $user_agent;
+	}
+
+	/**
+	 * Adds a "History" meta box to the activity edit screen.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $screen_action The type of screen that has been requested.
+	 */
+	function add_history_metabox( $screen_action ) {
+		// Only proceed if we're on the edit screen.
+		if ( 'edit' != $screen_action )
+			return;
+
+		// Display meta box with a low priority (low position on screen by default).
+		add_meta_box( 'bp_activity_history',  __( 'Activity History', 'buddypress' ), array( $this, 'history_metabox' ), get_current_screen()->id, 'normal', 'low' );
+	}
+
+	/**
+	 * History meta box for the Activity admin edit screen.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see https://buddypress.trac.wordpress.org/ticket/3907
+	 * @todo Update activity meta to allow >1 record with the same key (iterate through $history).
+	 *
+	 * @param object $item Activity item.
+	 */
+	function history_metabox( $item ) {
+		$history = BP_Akismet::get_activity_history( $item->id );
+
+		if ( empty( $history ) )
+			return;
+
+		echo '<div class="akismet-history"><div>';
+		printf( _x( '%1$s &mdash; %2$s', 'x hours ago - akismet cleared this item', 'buddypress' ), '<span>' . bp_core_time_since( $history[2] ) . '</span>', esc_html( $history[1] ) );
+		echo '</div></div>';
+	}
+
+	/**
+	 * Update an activity item's Akismet history.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int    $activity_id Activity item ID.
+	 * @param string $message     Human-readable description of what's changed.
+	 * @param string $event       The type of check we were carrying out.
+	 */
+	public function update_activity_history( $activity_id = 0, $message = '', $event = '' ) {
+		$event = array(
+			'event'   => $event,
+			'message' => $message,
+			'time'    => Akismet::_get_microtime(),
+			'user'    => bp_loggedin_user_id(),
+		);
+
+		// Save the history data.
+		bp_activity_update_meta( $activity_id, '_bp_akismet_history', $event );
+	}
+
+	/**
+	 * Get an activity item's Akismet history.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $activity_id Activity item ID.
+	 * @return array The activity item's Akismet history.
+	 */
+	public function get_activity_history( $activity_id = 0 ) {
+		$history = bp_activity_get_meta( $activity_id, '_bp_akismet_history' );
+		if ( $history === false )
+			$history = array();
+
+		// Sort it by the time recorded.
+		usort( $history, 'akismet_cmp_time' );
+
+		return $history;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/css/mentions-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/css/mentions-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/css/mentions-rtl.css	(revision 12265)
@@ -0,0 +1,106 @@
+.atwho-view {
+	background: rgba(204, 204, 204, 0.8);
+	border-radius: 2px;
+	border: 1px solid rgb(204, 204, 204);
+	box-shadow: 0 0 5px rgba(204, 204, 204, 0.25), 0 0 1px #fff;
+	color: #d84800;
+	display: none;
+	font-family: sans-serif;
+	margin-top: 18px;
+	position: absolute;
+	top: 0;
+	z-index: 1000; /* >999 for wp-admin */
+}
+.atwho-view {
+	left: 0;
+}
+
+.atwho-view ul {
+	background: #fff;
+	list-style: none;
+	margin: auto;
+	padding: 0;
+}
+
+.atwho-view ul li {
+	border-bottom: 1px solid #efefef;
+	box-sizing: content-box;
+	cursor: pointer;
+	display: block;
+	font-size: 14px;
+	height: 20px;
+	line-height: 20px;
+	margin: 0;
+	overflow: hidden;
+	padding: 5px 10px;
+}
+
+.atwho-view img {
+	border-radius: 2px;
+	float: left;
+	height: 20px;
+	margin-top: 0;
+	width: 20px;
+}
+
+.atwho-view strong {
+	background: #efefef;
+	font-weight: 700;
+}
+
+.atwho-view .username strong {
+	color: #d54e21;
+}
+
+.atwho-view small {
+	color: #aaa;
+	float: left;
+	font-size: smaller;
+	font-weight: 400;
+	margin: 0 40px 0 10px;
+}
+
+.atwho-view .cur {
+	background: rgba(239, 239, 239, 0.5);
+}
+
+@media (max-width: 900px) {
+
+	.atwho-view img {
+		float: right;
+		margin: 0 0 0 10px;
+	}
+}
+
+@media (max-width: 400px) {
+
+	.atwho-view ul li {
+		font-size: 16px;
+		line-height: 23px;
+		padding: 13px;
+	}
+
+	.atwho-view ul li img {
+		height: 30px;
+		margin-top: -5px;
+		width: 30px;
+	}
+
+	.atwho-view {
+		border-radius: 0;
+		height: 100%;
+		right: 0 !important;
+		width: 100%;
+	}
+
+	.atwho-view ul li .username {
+		display: inline-block;
+		margin: -10px 0 0 0;
+		padding: 10px 0;
+	}
+
+	.atwho-view ul li small {
+		display: inline-block;
+		margin-right: 20px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/css/mentions.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/css/mentions.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/css/mentions.css	(revision 12265)
@@ -0,0 +1,108 @@
+.atwho-view {
+	background: rgba(204, 204, 204, 0.8);
+	border-radius: 2px;
+	border: 1px solid rgb(204, 204, 204);
+	box-shadow: 0 0 5px rgba(204, 204, 204, 0.25), 0 0 1px #fff;
+	color: #d84800;
+	display: none;
+	font-family: sans-serif;
+	margin-top: 18px;
+	position: absolute;
+	top: 0;
+	z-index: 1000; /* >999 for wp-admin */
+}
+
+/* rtl:ignore */
+.atwho-view {
+	left: 0;
+}
+
+.atwho-view ul {
+	background: #fff;
+	list-style: none;
+	margin: auto;
+	padding: 0;
+}
+
+.atwho-view ul li {
+	border-bottom: 1px solid #efefef;
+	box-sizing: content-box;
+	cursor: pointer;
+	display: block;
+	font-size: 14px;
+	height: 20px;
+	line-height: 20px;
+	margin: 0;
+	overflow: hidden;
+	padding: 5px 10px;
+}
+
+.atwho-view img {
+	border-radius: 2px;
+	float: right;
+	height: 20px;
+	margin-top: 0;
+	width: 20px;
+}
+
+.atwho-view strong {
+	background: #efefef;
+	font-weight: 700;
+}
+
+.atwho-view .username strong {
+	color: #d54e21;
+}
+
+.atwho-view small {
+	color: #aaa;
+	float: right;
+	font-size: smaller;
+	font-weight: 400;
+	margin: 0 10px 0 40px;
+}
+
+.atwho-view .cur {
+	background: rgba(239, 239, 239, 0.5);
+}
+
+@media (max-width: 900px) {
+
+	.atwho-view img {
+		float: left;
+		margin: 0 10px 0 0;
+	}
+}
+
+@media (max-width: 400px) {
+
+	.atwho-view ul li {
+		font-size: 16px;
+		line-height: 23px;
+		padding: 13px;
+	}
+
+	.atwho-view ul li img {
+		height: 30px;
+		margin-top: -5px;
+		width: 30px;
+	}
+
+	.atwho-view {
+		border-radius: 0;
+		height: 100%;
+		left: 0 !important;
+		width: 100%;
+	}
+
+	.atwho-view ul li .username {
+		display: inline-block;
+		margin: -10px 0 0 0;
+		padding: 10px 0;
+	}
+
+	.atwho-view ul li small {
+		display: inline-block;
+		margin-left: 20px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-activity/js/mentions.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/js/mentions.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/js/mentions.js	(revision 12265)
@@ -0,0 +1,256 @@
+/* global bp */
+
+window.bp = window.bp || {};
+
+( function( bp, $, undefined ) {
+	var mentionsQueryCache = [],
+		mentionsItem;
+
+	bp.mentions       = bp.mentions || {};
+	bp.mentions.users = window.bp.mentions.users || [];
+
+	if ( typeof window.BP_Suggestions === 'object' ) {
+		bp.mentions.users = window.BP_Suggestions.friends || bp.mentions.users;
+	}
+
+	/**
+	 * Adds BuddyPress @mentions to form inputs.
+	 *
+	 * @param {array|object} options If array, becomes the suggestions' data source. If object, passed as config to $.atwho().
+	 * @since 2.1.0
+	 */
+	$.fn.bp_mentions = function( options ) {
+		if ( $.isArray( options ) ) {
+			options = { data: options };
+		}
+
+		/**
+		 * Default options for at.js; see https://github.com/ichord/At.js/.
+		 */
+		var suggestionsDefaults = {
+			delay:             200,
+			hideWithoutSuffix: true,
+			insertTpl:         '@${ID}',
+			limit:             10,
+			startWithSpace:    false,
+			suffix:            '',
+
+			callbacks: {
+				/**
+				 * Custom filter to only match the start of spaced words.
+				 * Based on the core/default one.
+				 *
+				 * @param {string} query
+				 * @param {array} data
+				 * @param {string} search_key
+				 * @return {array}
+				 * @since 2.1.0
+				 */
+				filter: function( query, data, search_key ) {
+					var item, _i, _len, _results = [],
+					regxp = new RegExp( '^' + query + '| ' + query, 'ig' ); // start of string, or preceded by a space.
+
+					for ( _i = 0, _len = data.length; _i < _len; _i++ ) {
+						item = data[ _i ];
+						if ( item[ search_key ].toLowerCase().match( regxp ) ) {
+							_results.push( item );
+						}
+					}
+
+					return _results;
+				},
+
+				/**
+				 * Removes some spaces around highlighted string and tweaks regex to allow spaces
+				 * (to match display_name). Based on the core default.
+				 *
+				 * @param {unknown} li
+				 * @param {string} query
+				 * @return {string}
+				 * @since 2.1.0
+				 */
+				highlighter: function( li, query ) {
+					if ( ! query ) {
+						return li;
+					}
+
+					var regexp = new RegExp( '>(\\s*|[\\w\\s]*)(' + this.at.replace( '+', '\\+') + '?' + query.replace( '+', '\\+' ) + ')([\\w ]*)\\s*<', 'ig' );
+					return li.replace( regexp, function( str, $1, $2, $3 ) {
+						return '>' + $1 + '<strong>' + $2 + '</strong>' + $3 + '<';
+					});
+				},
+
+				/**
+				 * Reposition the suggestion list dynamically.
+				 *
+				 * @param {unknown} offset
+				 * @since 2.1.0
+				 */
+				before_reposition: function( offset ) {
+					// get the iframe, if any, already applied with atwho
+					var caret,
+							line,
+							iframeOffset,
+							move,
+							$view = $( '#atwho-ground-' + this.id + ' .atwho-view' ),
+							$body = $( 'body' ),
+							atwhoDataValue = this.$inputor.data( 'atwho' );
+
+					if ( 'undefined' !== atwhoDataValue && 'undefined' !== atwhoDataValue.iframe && null !== atwhoDataValue.iframe ) {
+						caret = this.$inputor.caret( 'offset', { iframe: atwhoDataValue.iframe } );
+						// Caret.js no longer calculates iframe caret position from the window (it's now just within the iframe).
+						// We need to get the iframe offset from the window and merge that into our object.
+						iframeOffset = $( atwhoDataValue.iframe ).offset();
+						if ( 'undefined' !== iframeOffset ) {
+							caret.left += iframeOffset.left;
+							caret.top  += iframeOffset.top;
+						}
+					} else {
+						caret = this.$inputor.caret( 'offset' );
+					}
+
+					// If the caret is past horizontal half, then flip it, yo
+					if ( caret.left > ( $body.width() / 2 ) ) {
+						$view.addClass( 'right' );
+						move = caret.left - offset.left - this.view.$el.width();
+					} else {
+						$view.removeClass( 'right' );
+						move = caret.left - offset.left + 1;
+					}
+
+					// If we're on a small screen, scroll to caret
+					if ( $body.width() <= 400 ) {
+						$( document ).scrollTop( caret.top - 6 );
+					}
+
+					// New position is under the caret (never above) and positioned to follow
+					// Dynamic sizing based on the input area (remove 'px' from end)
+					line = parseInt( this.$inputor.css( 'line-height' ).substr( 0, this.$inputor.css( 'line-height' ).length - 2 ), 10 );
+					if ( !line || line < 5 ) { // sanity check, and catch no line-height
+						line = 19;
+					}
+
+					offset.top   = caret.top + line;
+					offset.left += move;
+				},
+
+				/**
+				 * Override default behaviour which inserts junk tags in the WordPress Visual editor.
+				 *
+				 * @param {unknown} $inputor Element which we're inserting content into.
+				 * @param {string) content The content that will be inserted.
+				 * @param {string) suffix Applied to the end of the content string.
+				 * @return {string}
+				 * @since 2.1.0
+				 */
+				inserting_wrapper: function( $inputor, content, suffix ) {
+					return '' + content + suffix;
+				}
+			}
+		},
+
+		/**
+		 * Default options for our @mentions; see https://github.com/ichord/At.js/.
+		 */
+		mentionsDefaults = {
+			callbacks: {
+				/**
+				 * If there are no matches for the query in this.data, then query BuddyPress.
+				 *
+				 * @param {string} query Partial @mention to search for.
+				 * @param {function} render_view Render page callback function.
+				 * @since 2.1.0
+				 * @since 3.0.0. Renamed from "remote_filter" for at.js v1.5.4 support.
+				 */
+				remoteFilter: function( query, render_view ) {
+					var self = $( this ),
+						params = {};
+
+					mentionsItem = mentionsQueryCache[ query ];
+					if ( typeof mentionsItem === 'object' ) {
+						render_view( mentionsItem );
+						return;
+					}
+
+					if ( self.xhr ) {
+						self.xhr.abort();
+					}
+
+					params = { 'action': 'bp_get_suggestions', 'term': query, 'type': 'members' };
+
+					if ( $.isNumeric( this.$inputor.data( 'suggestions-group-id' ) ) ) {
+						params['group-id'] = parseInt( this.$inputor.data( 'suggestions-group-id' ), 10 );
+					}
+
+					self.xhr = $.getJSON( ajaxurl, params )
+						/**
+						 * Success callback for the @suggestions lookup.
+						 *
+						 * @param {object} response Details of users matching the query.
+						 * @since 2.1.0
+						 */
+						.done(function( response ) {
+							if ( ! response.success ) {
+								return;
+							}
+
+							var data = $.map( response.data,
+								/**
+								 * Create a composite index to determine ordering of results;
+								 * nicename matches will appear on top.
+								 *
+								 * @param {array} suggestion A suggestion's original data.
+								 * @return {array} A suggestion's new data.
+								 * @since 2.1.0
+								 */
+								function( suggestion ) {
+									suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
+									return suggestion;
+								}
+							);
+
+							mentionsQueryCache[ query ] = data;
+							render_view( data );
+						});
+				}
+			},
+
+			data: $.map( options.data,
+				/**
+				 * Create a composite index to search against of nicename + display name.
+				 * This will also determine ordering of results, so nicename matches will appear on top.
+				 *
+				 * @param {array} suggestion A suggestion's original data.
+				 * @return {array} A suggestion's new data.
+				 * @since 2.1.0
+				 */
+				function( suggestion ) {
+					suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
+					return suggestion;
+				}
+			),
+
+			at:         '@',
+			searchKey:  'search',
+			displayTpl: '<li data-value="@${ID}"><img src="${image}" /><span class="username">@${ID}</span><small>${name}</small></li>'
+		},
+
+		opts = $.extend( true, {}, suggestionsDefaults, mentionsDefaults, options );
+		return $.fn.atwho.call( this, opts );
+	};
+
+	$( document ).ready(function() {
+		// Activity/reply, post comments, dashboard post 'text' editor.
+		$( '.bp-suggestions, #comments form textarea, .wp-editor-area' ).bp_mentions( bp.mentions.users );
+	});
+
+	bp.mentions.tinyMCEinit = function() {
+		if ( typeof window.tinyMCE === 'undefined' || window.tinyMCE.activeEditor === null || typeof window.tinyMCE.activeEditor === 'undefined' ) {
+			return;
+		} else {
+			$( window.tinyMCE.activeEditor.contentDocument.activeElement )
+				.atwho( 'setIframe', $( '.wp-editor-wrap iframe' )[0] )
+				.bp_mentions( bp.mentions.users );
+		}
+	};
+})( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-activity/screens/directory.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/directory.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/directory.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Activity: Directory screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the Activity directory.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_screen_index() {
+	if ( bp_is_activity_directory() ) {
+		bp_update_is_directory( true, 'activity' );
+
+		/**
+		 * 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' );
Index: /tags/4.0.0-beta1/src/bp-activity/screens/favorites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/favorites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/favorites.php	(revision 12265)
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Activity: User's "Activity > Favorites" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the 'Favorites' activity page.
+ *
+ * @since 1.2.0
+ */
+function bp_activity_screen_favorites() {
+	bp_update_is_item_admin( bp_current_user_can( 'bp_moderate' ), 'activity' );
+
+	/**
+	 * Fires right before the loading of the "Favorites" screen template file.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_activity_screen_favorites' );
+
+	/**
+	 * Filters the template to load for the "Favorites" screen.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $template Path to the activity template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_activity_template_favorite_activity', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-activity/screens/friends.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/friends.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/friends.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Activity: User's "Activity > Friends" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the 'My Friends' activity page.
+ *
+ * @since 1.0.0
+ */
+function bp_activity_screen_friends() {
+	if ( !bp_is_active( 'friends' ) )
+		return false;
+
+	bp_update_is_item_admin( bp_current_user_can( 'bp_moderate' ), 'activity' );
+
+	/**
+	 * Fires right before the loading of the "My Friends" screen template file.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_activity_screen_friends' );
+
+	/**
+	 * Filters the template to load for the "My Friends" screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the activity template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_activity_template_friends_activity', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-activity/screens/groups.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/groups.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/groups.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Activity: User's "Activity > Groups" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the 'My Groups' activity page.
+ *
+ * @since 1.2.0
+ */
+function bp_activity_screen_groups() {
+	if ( !bp_is_active( 'groups' ) )
+		return false;
+
+	bp_update_is_item_admin( bp_current_user_can( 'bp_moderate' ), 'activity' );
+
+	/**
+	 * Fires right before the loading of the "My Groups" screen template file.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_activity_screen_groups' );
+
+	/**
+	 * Filters the template to load for the "My Groups" screen.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $template Path to the activity template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_activity_template_groups_activity', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-activity/screens/just-me.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/just-me.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/just-me.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Activity: User's "Activity" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the 'My Activity' page.
+ *
+ * @since 1.0.0
+ */
+function bp_activity_screen_my_activity() {
+
+	/**
+	 * Fires right before the loading of the "My Activity" screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_activity_screen_my_activity' );
+
+	/**
+	 * Filters the template to load for the "My Activity" screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the activity template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_activity_template_my_activity', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-activity/screens/mentions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/mentions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/mentions.php	(revision 12265)
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Activity: User's "Activity > Mentions" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the 'Mentions' activity page.
+ *
+ * @since 1.2.0
+ */
+function bp_activity_screen_mentions() {
+	bp_update_is_item_admin( bp_current_user_can( 'bp_moderate' ), 'activity' );
+
+	/**
+	 * Fires right before the loading of the "Mentions" screen template file.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_activity_screen_mentions' );
+
+	/**
+	 * Filters the template to load for the "Mentions" screen.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $template Path to the activity template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_activity_template_mention_activity', 'members/single/home' ) );
+}
+
+/**
+ * Reset the logged-in user's new mentions data when he visits his mentions screen.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activity_reset_my_new_mentions() {
+	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' );
Index: /tags/4.0.0-beta1/src/bp-activity/screens/permalink.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-activity/screens/permalink.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-activity/screens/permalink.php	(revision 12265)
@@ -0,0 +1,166 @@
+<?php
+/**
+ * Activity: Single permalink screen handler
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScreens
+ * @since 3.0.0
+ */
+
+/**
+ * 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();
+		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 );
+}
+add_action( 'bp_actions', 'bp_activity_action_permalink_router' );
+
+/**
+ * Load the page for a single activity item.
+ *
+ * @since 1.2.0
+ *
+ * @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();
+		return;
+
+	} else {
+		$activity = $activity['activities'][0];
+	}
+
+	/**
+	 * Check user access to the activity item.
+	 *
+	 * @since 3.0.0
+	 */
+	$has_access = bp_activity_user_can_read( $activity );
+
+	// 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 && bp_displayed_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 ) {
+		// If not logged in, prompt for login.
+		if ( ! is_user_logged_in() ) {
+			bp_core_no_access();
+
+		// Redirect away.
+		} else {
+			bp_core_add_message( __( 'You do not have access to this activity.', 'buddypress' ), 'error' );
+			bp_core_redirect( bp_loggedin_user_domain() );
+		}
+	}
+
+	/**
+	 * 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 );
+}
+add_action( 'bp_screens', 'bp_activity_screen_single_activity_permalink' );
Index: /tags/4.0.0-beta1/src/bp-blogs/actions/random.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/actions/random.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/actions/random.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Blogs: Random blog action handler
+ *
+ * @package BuddyPress
+ * @subpackage BlogsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Redirect to a random blog in the multisite network.
+ *
+ * @since 1.0.0
+ */
+function bp_blogs_redirect_to_random_blog() {
+
+	// Bail if not looking for a random blog.
+	if ( ! bp_is_blogs_component() || ! isset( $_GET['random-blog'] ) )
+		return;
+
+	// Multisite is active so find a random blog.
+	if ( is_multisite() ) {
+		$blog = bp_blogs_get_random_blogs( 1, 1 );
+		bp_core_redirect( get_home_url( $blog['blogs'][0]->blog_id ) );
+
+	// No multisite and still called, always redirect to root.
+	} else {
+		bp_core_redirect( bp_core_get_root_domain() );
+	}
+}
+add_action( 'bp_actions', 'bp_blogs_redirect_to_random_blog' );
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-activity.php	(revision 12265)
@@ -0,0 +1,1470 @@
+<?php
+/**
+ * BuddyPress Blogs Activity.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsActivity
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register activity actions for the blogs component.
+ *
+ * @since 1.0.0
+ *
+ * @return bool|null Returns false if activity component is not active.
+ */
+function bp_blogs_register_activity_actions() {
+	if ( is_multisite() ) {
+		bp_activity_set_action(
+			buddypress()->blogs->id,
+			'new_blog',
+			__( 'New site created', 'buddypress' ),
+			'bp_blogs_format_activity_action_new_blog',
+			__( 'New Sites', 'buddypress' ),
+			array( 'activity', 'member' ),
+			0
+		);
+	}
+
+	/**
+	 * Fires after the registry of the default blog component activity actions.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_blogs_register_activity_actions' );
+}
+add_action( 'bp_register_activity_actions', 'bp_blogs_register_activity_actions' );
+
+/**
+ * Set up the tracking arguments for the 'post' post type.
+ *
+ * @since 2.5.0 This was moved out of the BP_Blogs_Component class.
+ *
+ * @see bp_activity_get_post_type_tracking_args() for information on parameters.
+ *
+ * @param object|null $params    Tracking arguments.
+ * @param string|int  $post_type Post type to track.
+ * @return object|null
+ */
+function bp_blogs_register_post_tracking_args( $params = null, $post_type = 0 ) {
+
+	/**
+	 * Filters the post types to track for the Blogs component.
+	 *
+	 * @since 1.5.0
+	 * @deprecated 2.3.0
+	 *
+	 * Make sure plugins still using 'bp_blogs_record_post_post_types'
+	 * to track their post types will generate new_blog_post activities
+	 * See https://buddypress.trac.wordpress.org/ticket/6306
+	 *
+	 * @param array $value Array of post types to track.
+	 */
+	$post_types = apply_filters( 'bp_blogs_record_post_post_types', array( 'post' ) );
+	$post_types_array = array_flip( $post_types );
+
+	if ( ! isset( $post_types_array[ $post_type ] ) ) {
+		return $params;
+	}
+
+	// Set specific params for the 'post' post type.
+	$params->component_id    = buddypress()->blogs->id;
+	$params->action_id       = 'new_blog_post';
+	$params->admin_filter    = __( 'New post published', 'buddypress' );
+	$params->format_callback = 'bp_blogs_format_activity_action_new_blog_post';
+	$params->front_filter    = __( 'Posts', 'buddypress' );
+	$params->contexts        = array( 'activity', 'member' );
+	$params->position        = 5;
+
+	if ( post_type_supports( $post_type, 'comments' ) ) {
+		$params->comment_action_id = 'new_blog_comment';
+
+		/**
+		 * Filters the post types to track for the Blogs component.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0
+		 *
+		 * Make sure plugins still using 'bp_blogs_record_comment_post_types'
+		 * to track comment about their post types will generate new_blog_comment activities
+		 * See https://buddypress.trac.wordpress.org/ticket/6306
+		 *
+		 * @param array $value Array of post types to track.
+		 */
+		$comment_post_types = apply_filters( 'bp_blogs_record_comment_post_types', array( 'post' ) );
+		$comment_post_types_array = array_flip( $comment_post_types );
+
+		if ( isset( $comment_post_types_array[ $post_type ] ) ) {
+			$params->comments_tracking = new stdClass();
+			$params->comments_tracking->component_id    = buddypress()->blogs->id;
+			$params->comments_tracking->action_id       = 'new_blog_comment';
+			$params->comments_tracking->admin_filter    = __( 'New post comment posted', 'buddypress' );
+			$params->comments_tracking->format_callback = 'bp_blogs_format_activity_action_new_blog_comment';
+			$params->comments_tracking->front_filter    = __( 'Comments', 'buddypress' );
+			$params->comments_tracking->contexts        = array( 'activity', 'member' );
+			$params->comments_tracking->position        = 10;
+		}
+	}
+
+	return $params;
+}
+add_filter( 'bp_activity_get_post_type_tracking_args', 'bp_blogs_register_post_tracking_args', 10, 2 );
+
+/**
+ * Format 'new_blog' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string
+ */
+function bp_blogs_format_activity_action_new_blog( $action, $activity ) {
+	$blog_url  = bp_blogs_get_blogmeta( $activity->item_id, 'url' );
+	$blog_name = bp_blogs_get_blogmeta( $activity->item_id, 'name' );
+
+	$action = sprintf( __( '%s created the site %s', 'buddypress' ), bp_core_get_userlink( $activity->user_id ), '<a href="' . esc_url( $blog_url ) . '">' . esc_html( $blog_name ) . '</a>' );
+
+	// Legacy filter - requires the BP_Blogs_Blog object.
+	if ( has_filter( 'bp_blogs_activity_created_blog_action' ) ) {
+		$user_blog = BP_Blogs_Blog::get_user_blog( $activity->user_id, $activity->item_id );
+		if ( $user_blog ) {
+			$recorded_blog = new BP_Blogs_Blog( $user_blog );
+		}
+
+		if ( isset( $recorded_blog ) ) {
+			$action = apply_filters( 'bp_blogs_activity_created_blog_action', $action, $recorded_blog, $blog_name, bp_blogs_get_blogmeta( $activity->item_id, 'description' ) );
+		}
+	}
+
+	/**
+	 * Filters the new blog activity action for the new blog.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   Constructed activity action.
+	 * @param object $activity Activity data object.
+	 */
+	return apply_filters( 'bp_blogs_format_activity_action_new_blog', $action, $activity );
+}
+
+/**
+ * Format 'new_blog_post' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string Constructed activity action.
+ */
+function bp_blogs_format_activity_action_new_blog_post( $action, $activity ) {
+	$blog_url  = bp_blogs_get_blogmeta( $activity->item_id, 'url' );
+	$blog_name = bp_blogs_get_blogmeta( $activity->item_id, 'name' );
+
+	if ( empty( $blog_url ) || empty( $blog_name ) ) {
+		$blog_url  = get_home_url( $activity->item_id );
+		$blog_name = get_blog_option( $activity->item_id, 'blogname' );
+
+		bp_blogs_update_blogmeta( $activity->item_id, 'url', $blog_url );
+		bp_blogs_update_blogmeta( $activity->item_id, 'name', $blog_name );
+	}
+
+	/**
+	 * When the post is published we are faking an activity object
+	 * to which we add 2 properties :
+	 * - the post url
+	 * - the post title
+	 * This is done to build the 'post link' part of the activity
+	 * action string.
+	 * NB: in this case the activity has not yet been created.
+	 */
+	if ( isset( $activity->post_url ) ) {
+		$post_url = $activity->post_url;
+
+	/**
+	 * The post_url property is not set, we need to build the url
+	 * thanks to the post id which is also saved as the secondary
+	 * item id property of the activity object.
+	 */
+	} else {
+		$post_url = add_query_arg( 'p', $activity->secondary_item_id, trailingslashit( $blog_url ) );
+	}
+
+	// Should be the case when the post has just been published.
+	if ( isset( $activity->post_title ) ) {
+		$post_title = $activity->post_title;
+
+	// If activity already exists try to get the post title from activity meta.
+	} else if ( ! empty( $activity->id ) ) {
+		$post_title = bp_activity_get_meta( $activity->id, 'post_title' );
+	}
+
+	/**
+	 * In case the post was published without a title
+	 * or the activity meta was not found.
+	 */
+	if ( empty( $post_title ) ) {
+		// Defaults to no title.
+		$post_title = esc_html__( '(no title)', 'buddypress' );
+
+		switch_to_blog( $activity->item_id );
+
+		$post = get_post( $activity->secondary_item_id );
+		if ( is_a( $post, 'WP_Post' ) ) {
+			// Does the post have a title ?
+			if ( ! empty( $post->post_title ) ) {
+				$post_title = $post->post_title;
+			}
+
+			// Make sure the activity exists before saving the post title in activity meta.
+			if ( ! empty( $activity->id ) ) {
+				bp_activity_update_meta( $activity->id, 'post_title', $post_title );
+			}
+		}
+
+		restore_current_blog();
+	}
+
+	// Build the 'post link' part of the activity action string.
+	$post_link  = '<a href="' . esc_url( $post_url ) . '">' . $post_title . '</a>';
+
+	$user_link = bp_core_get_userlink( $activity->user_id );
+
+	// Build the complete activity action string.
+	if ( is_multisite() ) {
+		$action  = sprintf( __( '%1$s wrote a new post, %2$s, on the site %3$s', 'buddypress' ), $user_link, $post_link, '<a href="' . esc_url( $blog_url ) . '">' . esc_html( $blog_name ) . '</a>' );
+	} else {
+		$action  = sprintf( __( '%1$s wrote a new post, %2$s', 'buddypress' ), $user_link, $post_link );
+	}
+
+	// Legacy filter - requires the post object.
+	if ( has_filter( 'bp_blogs_activity_new_post_action' ) ) {
+		switch_to_blog( $activity->item_id );
+		$post = get_post( $activity->secondary_item_id );
+		restore_current_blog();
+
+		if ( ! empty( $post ) && ! is_wp_error( $post ) ) {
+			$action = apply_filters( 'bp_blogs_activity_new_post_action', $action, $post, $post_url );
+		}
+	}
+
+	/**
+	 * Filters the new blog post action for the new blog.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   Constructed activity action.
+	 * @param object $activity Activity data object.
+	 */
+	return apply_filters( 'bp_blogs_format_activity_action_new_blog_post', $action, $activity );
+}
+
+/**
+ * Format 'new_blog_comment' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string Constructed activity action.
+ */
+function bp_blogs_format_activity_action_new_blog_comment( $action, $activity ) {
+	/**
+	 * When the comment is published we are faking an activity object
+	 * to which we add 4 properties :
+	 * - the post url
+	 * - the post title
+	 * - the blog url
+	 * - the blog name
+	 * This is done to build the 'post link' part of the activity
+	 * action string.
+	 * NB: in this case the activity has not yet been created.
+	 */
+
+	$blog_url = false;
+
+	// Try to get the blog url from the activity object
+	if ( isset( $activity->blog_url ) ) {
+		$blog_url = $activity->blog_url;
+	} else {
+		$blog_url = bp_blogs_get_blogmeta( $activity->item_id, 'url' );
+	}
+
+	$blog_name = false;
+
+	// Try to get the blog name from the activity object
+	if ( isset( $activity->blog_name ) ) {
+		$blog_name = $activity->blog_name;
+	} else {
+		$blog_name = bp_blogs_get_blogmeta( $activity->item_id, 'name' );
+	}
+
+	if ( empty( $blog_url ) || empty( $blog_name ) ) {
+		$blog_url  = get_home_url( $activity->item_id );
+		$blog_name = get_blog_option( $activity->item_id, 'blogname' );
+
+		bp_blogs_update_blogmeta( $activity->item_id, 'url', $blog_url );
+		bp_blogs_update_blogmeta( $activity->item_id, 'name', $blog_name );
+	}
+
+	$post_url = false;
+
+	// Try to get the post url from the activity object
+	if ( isset( $activity->post_url ) ) {
+		$post_url = $activity->post_url;
+
+	/**
+	 * The post_url property is not set, we need to build the url
+	 * thanks to the post id which is also saved as the secondary
+	 * item id property of the activity object.
+	 */
+	} elseif ( ! empty( $activity->id ) ) {
+		$post_url = bp_activity_get_meta( $activity->id, 'post_url' );
+	}
+
+	$post_title = false;
+
+	// Should be the case when the comment has just been published
+	if ( isset( $activity->post_title ) ) {
+		$post_title = $activity->post_title;
+
+	// If activity already exists try to get the post title from activity meta
+	} elseif ( ! empty( $activity->id ) ) {
+		$post_title = bp_activity_get_meta( $activity->id, 'post_title' );
+	}
+
+	// Should only be empty at the time of post creation.
+	if ( empty( $post_url ) || empty( $post_title ) ) {
+		switch_to_blog( $activity->item_id );
+
+		$comment = get_comment( $activity->secondary_item_id );
+
+		if ( ! empty( $comment->comment_post_ID ) ) {
+			$post_url = add_query_arg( 'p', $comment->comment_post_ID, trailingslashit( $blog_url ) );
+			bp_activity_update_meta( $activity->id, 'post_url', $post_url );
+
+			$post = get_post( $comment->comment_post_ID );
+
+			if ( is_a( $post, 'WP_Post' ) ) {
+				$post_title = $post->post_title;
+				bp_activity_update_meta( $activity->id, 'post_title', $post_title );
+			}
+		}
+
+		restore_current_blog();
+	}
+
+	$post_link = '<a href="' . esc_url( $post_url ) . '">' . $post_title . '</a>';
+	$user_link = bp_core_get_userlink( $activity->user_id );
+
+	if ( is_multisite() ) {
+		$action  = sprintf( __( '%1$s commented on the post, %2$s, on the site %3$s', 'buddypress' ), $user_link, $post_link, '<a href="' . esc_url( $blog_url ) . '">' . esc_html( $blog_name ) . '</a>' );
+	} else {
+		$action  = sprintf( __( '%1$s commented on the post, %2$s', 'buddypress' ), $user_link, $post_link );
+	}
+
+	// Legacy filter - requires the comment object.
+	if ( has_filter( 'bp_blogs_activity_new_comment_action' ) ) {
+		switch_to_blog( $activity->item_id );
+		$comment = get_comment( $activity->secondary_item_id );
+		restore_current_blog();
+
+		if ( ! empty( $comment ) && ! is_wp_error( $comment ) ) {
+			$action = apply_filters( 'bp_blogs_activity_new_comment_action', $action, $comment, $post_url . '#' . $activity->secondary_item_id );
+		}
+	}
+
+	/**
+	 * Filters the new blog comment action for the new blog.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   Constructed activity action.
+	 * @param object $activity Activity data object.
+	 */
+	return apply_filters( 'bp_blogs_format_activity_action_new_blog_comment', $action, $activity );
+}
+
+/**
+ * Fetch data related to blogs at the beginning of an activity loop.
+ *
+ * This reduces database overhead during the activity loop.
+ *
+ * @since 2.0.0
+ *
+ * @param array $activities Array of activity items.
+ * @return array
+ */
+function bp_blogs_prefetch_activity_object_data( $activities ) {
+	if ( empty( $activities ) ) {
+		return $activities;
+	}
+
+	$blog_ids = array();
+
+	foreach ( $activities as $activity ) {
+		if ( buddypress()->blogs->id !== $activity->component ) {
+			continue;
+		}
+
+		$blog_ids[] = $activity->item_id;
+	}
+
+	if ( ! empty( $blog_ids ) ) {
+		bp_blogs_update_meta_cache( $blog_ids );
+	}
+
+	return $activities;
+}
+add_filter( 'bp_activity_prefetch_object_data', 'bp_blogs_prefetch_activity_object_data' );
+
+/**
+ * Record blog-related activity to the activity stream.
+ *
+ * @since 1.0.0
+ *
+ * @see bp_activity_add() for description of parameters.
+ *
+ * @param array|string $args {
+ *     See {@link bp_activity_add()} for complete description of arguments.
+ *     The arguments listed here have different default values from
+ *     bp_activity_add().
+ *     @type string $component Default: 'blogs'.
+ * }
+ * @return WP_Error|bool|int On success, returns the activity ID. False on failure.
+ */
+function bp_blogs_record_activity( $args = '' ) {
+	$defaults = array(
+		'user_id'           => bp_loggedin_user_id(),
+		'action'            => '',
+		'content'           => '',
+		'primary_link'      => '',
+		'component'         => buddypress()->blogs->id,
+		'type'              => false,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'recorded_time'     => bp_core_current_time(),
+		'hide_sitewide'     => false
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+
+	if ( ! empty( $r['action'] ) ) {
+
+		/**
+		 * Filters the action associated with activity for activity stream.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Action for the activity stream.
+		 */
+		$r['action'] = apply_filters( 'bp_blogs_record_activity_action', $r['action'] );
+	}
+
+	if ( ! empty( $r['content'] ) ) {
+
+		/**
+		 * Filters the content associated with activity for activity stream.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Generated summary from content for the activity stream.
+		 * @param string $value Content for the activity stream.
+		 * @param array  $r     Array of arguments used for the activity stream item.
+		 */
+		$r['content'] = apply_filters( 'bp_blogs_record_activity_content', bp_activity_create_summary( $r['content'], $r ), $r['content'], $r );
+	}
+
+	// Check for an existing entry and update if one exists.
+	$id = bp_activity_get_activity_id( array(
+		'user_id'           => $r['user_id'],
+		'component'         => $r['component'],
+		'type'              => $r['type'],
+		'item_id'           => $r['item_id'],
+		'secondary_item_id' => $r['secondary_item_id'],
+	) );
+
+	return bp_activity_add( array( 'id' => $id, 'user_id' => $r['user_id'], 'action' => $r['action'], 'content' => $r['content'], 'primary_link' => $r['primary_link'], 'component' => $r['component'], 'type' => $r['type'], 'item_id' => $r['item_id'], 'secondary_item_id' => $r['secondary_item_id'], 'recorded_time' => $r['recorded_time'], 'hide_sitewide' => $r['hide_sitewide'] ) );
+}
+
+/**
+ * Delete a blog-related activity stream item.
+ *
+ * @since 1.0.0
+ *
+ * @see bp_activity_delete() for description of parameters.
+ *
+ * @param array|string $args {
+ *     See {@link bp_activity_delete()} for complete description of arguments.
+ *     The arguments listed here have different default values from
+ *     bp_activity_add().
+ *     @type string $component Default: 'blogs'.
+ * }
+ * @return bool True on success, false on failure.
+ */
+function bp_blogs_delete_activity( $args = '' ) {
+	$r = bp_parse_args( $args, array(
+		'item_id'           => false,
+		'component'         => buddypress()->blogs->id,
+		'type'              => false,
+		'user_id'           => false,
+		'secondary_item_id' => false
+	) );
+
+	bp_activity_delete_by_item_id( $r );
+}
+
+/**
+ * Check if a blog post's activity item should be closed from commenting.
+ *
+ * This mirrors the {@link comments_open()} and {@link _close_comments_for_old_post()}
+ * functions, but for use with the BuddyPress activity stream to be as
+ * lightweight as possible.
+ *
+ * By lightweight, we actually mirror a few of the blog's commenting settings
+ * to blogmeta and checks the values in blogmeta instead.  This is to prevent
+ * multiple {@link switch_to_blog()} calls in the activity stream.
+ *
+ * @since 2.0.0
+ *
+ * @param object $activity The BP_Activity_Activity object.
+ * @return bool
+ */
+function bp_blogs_comments_open( $activity ) {
+	$open = true;
+
+	$blog_id = $activity->item_id;
+
+	// See if we've mirrored the close comments option before.
+	$days_old = bp_blogs_get_blogmeta( $blog_id, 'close_comments_days_old' );
+
+	// We've never cached these items before, so do it now.
+	if ( '' === $days_old ) {
+		switch_to_blog( $blog_id );
+
+		// Use comments_open().
+		remove_filter( 'comments_open', 'bp_comments_open', 10 );
+		$open = comments_open( $activity->secondary_item_id );
+		add_filter( 'comments_open', 'bp_comments_open', 10, 2 );
+
+		// Might as well mirror values to blogmeta since we're here!
+		$thread_depth = get_option( 'thread_comments' );
+		if ( ! empty( $thread_depth ) ) {
+			$thread_depth = get_option( 'thread_comments_depth' );
+		} else {
+			// Perhaps filter this?
+			$thread_depth = 1;
+		}
+
+		bp_blogs_update_blogmeta( $blog_id, 'close_comments_for_old_posts', get_option( 'close_comments_for_old_posts' ) );
+		bp_blogs_update_blogmeta( $blog_id, 'close_comments_days_old',      get_option( 'close_comments_days_old' ) );
+		bp_blogs_update_blogmeta( $blog_id, 'thread_comments_depth',        $thread_depth );
+
+		restore_current_blog();
+
+	// Check blogmeta and manually check activity item.
+	// Basically a copy of _close_comments_for_old_post().
+	} else {
+
+		// Comments are closed.
+		if ( 'closed' == bp_activity_get_meta( $activity->id, 'post_comment_status' ) ) {
+			return false;
+		}
+
+		if ( ! bp_blogs_get_blogmeta( $blog_id, 'close_comments_for_old_posts' ) ) {
+			return $open;
+		}
+
+		$days_old = (int) $days_old;
+		if ( ! $days_old ) {
+			return $open;
+		}
+
+		/*
+		   Commenting out for now - needs some more thought...
+		   should we add the post type to activity meta?
+
+		$post = get_post($post_id);
+
+		// This filter is documented in wp-includes/comment.php
+		$post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) );
+		if ( ! in_array( $post->post_type, $post_types ) )
+			return $open;
+		*/
+
+		if ( time() - strtotime( $activity->date_recorded ) > ( $days_old * DAY_IN_SECONDS ) ) {
+			return false;
+		}
+
+		return $open;
+	}
+
+	return $open;
+}
+
+/** SITE TRACKING *******************************************************/
+
+/**
+ * Add an activity entry for a newly-created site.
+ *
+ * Hooked to the 'bp_blogs_new_blog' action.
+ *
+ * @since 2.6.0
+ *
+ * @param BP_Blogs_Blog $recorded_blog Current site being recorded. Passed by reference.
+ * @param bool          $is_private    Whether the current site being recorded is private.
+ * @param bool          $is_recorded   Whether the current site was recorded.
+ */
+function bp_blogs_record_activity_on_site_creation( $recorded_blog, $is_private, $is_recorded, $no_activity ) {
+	// Only record this activity if the blog is public.
+	if ( ! $is_private && ! $no_activity && bp_blogs_is_blog_trackable( $recorded_blog->blog_id, $recorded_blog->user_id ) ) {
+		bp_blogs_record_activity( array(
+			'user_id'      => $recorded_blog->user_id,
+
+			/**
+			 * Filters the activity created blog primary link.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $value Blog primary link.
+			 * @param int    $value Blog ID.
+			 */
+			'primary_link' => apply_filters( 'bp_blogs_activity_created_blog_primary_link', bp_blogs_get_blogmeta( $recorded_blog->blog_id, 'url' ), $recorded_blog->blog_id ),
+			'type'         => 'new_blog',
+			'item_id'      => $recorded_blog->blog_id
+		) );
+	}
+}
+add_action( 'bp_blogs_new_blog', 'bp_blogs_record_activity_on_site_creation', 10, 4 );
+
+/**
+ * Deletes the 'new_blog' activity entry when a site is deleted.
+ *
+ * @since 2.6.0
+ *
+ * @param int $blog_id Site ID.
+ */
+function bp_blogs_delete_new_blog_activity_for_site( $blog_id, $user_id = 0 ) {
+	$args = array(
+		'item_id'   => $blog_id,
+		'component' => buddypress()->blogs->id,
+		'type'      => 'new_blog'
+	);
+
+	/**
+	 * In the case a user is removed, make sure he is the author of the 'new_blog' activity
+	 * when trying to delete it.
+	 */
+	if ( ! empty( $user_id ) ) {
+		$args['user_id'] = $user_id;
+	}
+
+	bp_blogs_delete_activity( $args );
+}
+add_action( 'bp_blogs_remove_blog',          'bp_blogs_delete_new_blog_activity_for_site', 10, 1 );
+add_action( 'bp_blogs_remove_blog_for_user', 'bp_blogs_delete_new_blog_activity_for_site', 10, 2 );
+
+/**
+ * Delete all 'blogs' activity items for a site when the site is deleted.
+ *
+ * @since 2.6.0
+ *
+ * @param int $blog_id Site ID.
+ */
+function bp_blogs_delete_activity_for_site( $blog_id ) {
+	bp_blogs_delete_activity( array(
+		'item_id'   => $blog_id,
+		'component' => buddypress()->blogs->id,
+		'type'      => false
+	) );
+}
+add_action( 'bp_blogs_remove_data_for_blog', 'bp_blogs_delete_activity_for_site' );
+
+/**
+ * Remove a blog post activity item from the activity stream.
+ *
+ * @since 1.0.0
+ *
+ * @param int $post_id ID of the post to be removed.
+ * @param int $blog_id Optional. Defaults to current blog ID.
+ * @param int $user_id Optional. Defaults to the logged-in user ID. This param
+ *                     is currently unused in the function (but is passed to hooks).
+ * @return bool
+ */
+function bp_blogs_remove_post( $post_id, $blog_id = 0, $user_id = 0 ) {
+	global $wpdb;
+
+	if ( empty( $wpdb->blogid ) ) {
+		return false;
+	}
+
+	$post_id = (int) $post_id;
+
+	if ( ! $blog_id ) {
+		$blog_id = (int) $wpdb->blogid;
+	}
+
+	if ( ! $user_id ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	/**
+	 * Fires before removal of a blog post activity item from the activity stream.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $blog_id ID of the blog associated with the post that was removed.
+	 * @param int $post_id ID of the post that was removed.
+	 * @param int $user_id ID of the user having the blog removed for.
+	 */
+	do_action( 'bp_blogs_before_remove_post', $blog_id, $post_id, $user_id );
+
+	bp_blogs_delete_activity( array(
+		'item_id'           => $blog_id,
+		'secondary_item_id' => $post_id,
+		'component'         => buddypress()->blogs->id,
+		'type'              => 'new_blog_post'
+	) );
+
+	/**
+	 * Fires after removal of a blog post activity item from the activity stream.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $blog_id ID of the blog associated with the post that was removed.
+	 * @param int $post_id ID of the post that was removed.
+	 * @param int $user_id ID of the user having the blog removed for.
+	 */
+	do_action( 'bp_blogs_remove_post', $blog_id, $post_id, $user_id );
+}
+add_action( 'delete_post', 'bp_blogs_remove_post' );
+
+/** POST COMMENT SYNCHRONIZATION ****************************************/
+
+/**
+ * Syncs activity comments and posts them back as blog comments.
+ *
+ * Note: This is only a one-way sync - activity comments -> blog comment.
+ *
+ * For blog post -> activity comment, see {@link bp_activity_post_type_comment()}.
+ *
+ * @since 2.0.0
+ * @since 2.5.0 Allow custom post types to sync their comments with activity ones
+ *
+ * @param int    $comment_id      The activity ID for the posted activity comment.
+ * @param array  $params          Parameters for the activity comment.
+ * @param object $parent_activity Parameters of the parent activity item (in this case, the blog post).
+ */
+function bp_blogs_sync_add_from_activity_comment( $comment_id, $params, $parent_activity ) {
+	// if parent activity isn't a post type having the buddypress-activity support, stop now!
+	if ( ! bp_activity_type_supports( $parent_activity->type, 'post-type-comment-tracking' ) ) {
+		return;
+	}
+
+	// If activity comments are disabled for blog posts, stop now!
+	if ( bp_disable_blogforum_comments() ) {
+		return;
+	}
+
+	// Do not sync if the activity comment was marked as spam.
+	$activity = new BP_Activity_Activity( $comment_id );
+	if ( $activity->is_spam ) {
+		return;
+	}
+
+	// Check if comments are still open for parent item.
+	$comments_open = bp_blogs_comments_open( $parent_activity );
+	if ( ! $comments_open ) {
+		return;
+	}
+
+	// Get userdata.
+	if ( $params['user_id'] == bp_loggedin_user_id() ) {
+		$user = buddypress()->loggedin_user->userdata;
+	} else {
+		$user = bp_core_get_core_userdata( $params['user_id'] );
+	}
+
+	// Get associated post type and set default comment parent
+	$post_type      = bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'post_type' );
+	$comment_parent = 0;
+
+	// See if a parent WP comment ID exists.
+	if ( ! empty( $params['parent_id'] ) && ! empty( $post_type ) ) {
+		$comment_parent = bp_activity_get_meta( $params['parent_id'], "bp_blogs_{$post_type}_comment_id" );
+	}
+
+	// Comment args.
+	$args = array(
+		'comment_post_ID'      => $parent_activity->secondary_item_id,
+		'comment_author'       => bp_core_get_user_displayname( $params['user_id'] ),
+		'comment_author_email' => $user->user_email,
+		'comment_author_url'   => bp_core_get_user_domain( $params['user_id'], $user->user_nicename, $user->user_login ),
+		'comment_content'      => $params['content'],
+		'comment_type'         => '', // Could be interesting to add 'buddypress' here...
+		'comment_parent'       => (int) $comment_parent,
+		'user_id'              => $params['user_id'],
+		'comment_approved'     => 1
+	);
+
+	// Prevent separate activity entry being made.
+	remove_action( 'comment_post', 'bp_activity_post_type_comment', 10 );
+
+	// Handle multisite.
+	switch_to_blog( $parent_activity->item_id );
+
+	// Handle timestamps for the WP comment after we've switched to the blog.
+	$args['comment_date']     = current_time( 'mysql' );
+	$args['comment_date_gmt'] = current_time( 'mysql', 1 );
+
+	// Post the comment.
+	$post_comment_id = wp_insert_comment( $args );
+
+	// Add meta to comment.
+	add_comment_meta( $post_comment_id, 'bp_activity_comment_id', $comment_id );
+
+	// Add meta to activity comment.
+	if ( ! empty( $post_type ) ) {
+		bp_activity_update_meta( $comment_id, "bp_blogs_{$post_type}_comment_id", $post_comment_id );
+	}
+
+	// Resave activity comment with WP comment permalink.
+	//
+	// in bp_blogs_activity_comment_permalink(), we change activity comment
+	// permalinks to use the post comment link
+	//
+	// @todo since this is done after AJAX posting, the activity comment permalink
+	// doesn't change on the front end until the next page refresh.
+	$resave_activity = new BP_Activity_Activity( $comment_id );
+	$resave_activity->primary_link = get_comment_link( $post_comment_id );
+
+	/**
+	 * Now that the activity id exists and the post comment was created, we don't need to update
+	 * the content of the comment as there are no chances it has evolved.
+	 */
+	remove_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
+
+	$resave_activity->save();
+
+	// Add the edit activity comment hook back.
+	add_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
+
+	// Multisite again!
+	restore_current_blog();
+
+	// Add the comment hook back.
+	add_action( 'comment_post', 'bp_activity_post_type_comment', 10, 2 );
+
+	/**
+	 * Fires after activity comments have been synced and posted as blog comments.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int    $comment_id      The activity ID for the posted activity comment.
+	 * @param array  $args            Array of args used for the comment syncing.
+	 * @param object $parent_activity Parameters of the blog post parent activity item.
+	 * @param object $user            User data object for the blog comment.
+	 */
+	do_action( 'bp_blogs_sync_add_from_activity_comment', $comment_id, $args, $parent_activity, $user );
+}
+add_action( 'bp_activity_comment_posted', 'bp_blogs_sync_add_from_activity_comment', 10, 3 );
+
+/**
+ * Deletes the blog comment when the associated activity comment is deleted.
+ *
+ * Note: This is hooked on the 'bp_activity_delete_comment_pre' filter instead
+ * of the 'bp_activity_delete_comment' action because we need to fetch the
+ * activity comment children before they are deleted.
+ *
+ * @since 2.0.0
+ * @since 2.5.0 Add the $delected parameter
+ *
+ * @param bool $retval             Whether BuddyPress should continue or not.
+ * @param int  $parent_activity_id The parent activity ID for the activity comment.
+ * @param int  $activity_id        The activity ID for the pending deleted activity comment.
+ * @param bool $deleted            Whether the comment was deleted or not.
+ * @return bool
+ */
+function bp_blogs_sync_delete_from_activity_comment( $retval, $parent_activity_id, $activity_id, &$deleted ) {
+	// Check if parent activity is a blog post.
+	$parent_activity = new BP_Activity_Activity( $parent_activity_id );
+
+	// if parent activity isn't a post type having the buddypress-activity support, stop now!
+	if ( ! bp_activity_type_supports( $parent_activity->type, 'post-type-comment-tracking' ) ) {
+		return $retval;
+	}
+
+	// Fetch the activity comments for the activity item.
+	$activity = bp_activity_get( array(
+		'in'               => $activity_id,
+		'display_comments' => 'stream',
+		'spam'             => 'all',
+	) );
+
+	// Get all activity comment IDs for the pending deleted item.
+	$activity_ids   = bp_activity_recurse_comments_activity_ids( $activity );
+	$activity_ids[] = $activity_id;
+
+	// Handle multisite
+	// switch to the blog where the comment was made.
+	switch_to_blog( $parent_activity->item_id );
+
+	// Remove associated blog comments.
+	bp_blogs_remove_associated_blog_comments( $activity_ids, current_user_can( 'moderate_comments' ) );
+
+	// Multisite again!
+	restore_current_blog();
+
+	// Rebuild activity comment tree
+	// emulate bp_activity_delete_comment().
+	BP_Activity_Activity::rebuild_activity_comment_tree( $parent_activity_id );
+
+	// Avoid the error message although the comments were successfully deleted
+	$deleted = true;
+
+	// We're overriding the default bp_activity_delete_comment() functionality
+	// so we need to return false.
+	return false;
+}
+add_filter( 'bp_activity_delete_comment_pre', 'bp_blogs_sync_delete_from_activity_comment', 10, 4 );
+
+/**
+ * Updates the blog comment when the associated activity comment is edited.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_Activity_Activity $activity The activity object.
+ */
+function bp_blogs_sync_activity_edit_to_post_comment( BP_Activity_Activity $activity ) {
+	// This is a new entry, so stop!
+	// We only want edits!
+	if ( empty( $activity->id ) || bp_disable_blogforum_comments() ) {
+		return;
+	}
+
+	// fetch parent activity item
+	$parent_activity = new BP_Activity_Activity( $activity->item_id );
+
+	// if parent activity isn't a post type having the buddypress-activity support for comments, stop now!
+	if ( ! bp_activity_type_supports( $parent_activity->type, 'post-type-comment-tracking' ) ) {
+		return;
+	}
+
+	$post_type = bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'post_type' );
+
+	// No associated post type for this activity comment, stop.
+	if ( ! $post_type ) {
+		return;
+	}
+
+	// Try to see if a corresponding blog comment exists.
+	$post_comment_id = bp_activity_get_meta( $activity->id, "bp_blogs_{$post_type}_comment_id" );
+
+	if ( empty( $post_comment_id ) ) {
+		return;
+	}
+
+	// Handle multisite.
+	switch_to_blog( $parent_activity->item_id );
+
+	// Get the comment status
+	$post_comment_status = wp_get_comment_status( $post_comment_id );
+	$old_comment_status  = $post_comment_status;
+
+	// No need to edit the activity, as it's the activity who's updating the comment
+	remove_action( 'transition_comment_status', 'bp_activity_transition_post_type_comment_status', 10 );
+	remove_action( 'bp_activity_post_type_comment', 'bp_blogs_comment_sync_activity_comment', 10 );
+
+	if ( 1 === $activity->is_spam && 'spam' !== $post_comment_status ) {
+		wp_spam_comment( $post_comment_id );
+	} elseif ( ! $activity->is_spam ) {
+		if ( 'spam' === $post_comment_status  ) {
+			wp_unspam_comment( $post_comment_id );
+		} elseif ( 'trash' === $post_comment_status ) {
+			wp_untrash_comment( $post_comment_id );
+		} else {
+			// Update the blog post comment.
+			wp_update_comment( array(
+				'comment_ID'       => $post_comment_id,
+				'comment_content'  => $activity->content,
+			) );
+		}
+	}
+
+	// Restore actions
+	add_action( 'transition_comment_status',     'bp_activity_transition_post_type_comment_status', 10, 3 );
+	add_action( 'bp_activity_post_type_comment', 'bp_blogs_comment_sync_activity_comment',          10, 4 );
+
+	restore_current_blog();
+}
+add_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
+
+/**
+ * When a post is trashed, remove each comment's associated activity meta.
+ *
+ * When a post is trashed and later untrashed, we currently don't reinstate
+ * activity items for these comments since their activity entries are already
+ * deleted when initially trashed.
+ *
+ * Since these activity entries are deleted, we need to remove the deleted
+ * activity comment IDs from each comment's meta when a post is trashed.
+ *
+ * @since 2.0.0
+ *
+ * @param int   $post_id  The post ID.
+ * @param array $comments Array of comment statuses. The key is comment ID, the
+ *                        value is the $comment->comment_approved value.
+ */
+function bp_blogs_remove_activity_meta_for_trashed_comments( $post_id = 0, $comments = array() ) {
+	if ( ! empty( $comments ) ) {
+		foreach ( array_keys( $comments ) as $comment_id ) {
+			delete_comment_meta( $comment_id, 'bp_activity_comment_id' );
+		}
+	}
+}
+add_action( 'trashed_post_comments', 'bp_blogs_remove_activity_meta_for_trashed_comments', 10, 2 );
+
+/**
+ * Filter 'new_blog_comment' bp_has_activities() loop to include new- and old-style blog activity comment items.
+ *
+ * In BuddyPress 2.0, the schema for storing activity items related to blog
+ * posts changed. Instead creating new top-level 'new_blog_comment' activity
+ * items, blog comments are recorded in the activity stream as comments on the
+ * 'new_blog_post' activity items corresponding to the parent post. This filter
+ * ensures that the 'new_blog_comment' filter in bp_has_activities() (which
+ * powers the 'Comments' filter in the activity directory dropdown) includes
+ * both old-style and new-style activity comments.
+ *
+ * @since 2.1.0
+ * @since 2.5.0 Used for any synced Post type comments, in wp-admin or front-end contexts.
+ *
+ * @param array $args Arguments passed from bp_parse_args() in bp_has_activities().
+ * @return array $args
+ */
+function bp_blogs_new_blog_comment_query_backpat( $args ) {
+	global $wpdb;
+	$bp = buddypress();
+
+	// If activity comments are disabled for blog posts, stop now!
+	if ( bp_disable_blogforum_comments() ) {
+		return $args;
+	}
+
+	// Get the associated post type
+	$post_type = bp_activity_post_type_get_tracking_arg( $args['action'], 'post_type' );
+
+	// Bail if this is not an activity associated with a post type
+	if ( empty( $post_type ) ) {
+		return $args;
+	}
+
+	// Bail if this is an activity about posts and not comments
+	if ( bp_activity_post_type_get_tracking_arg( $args['action'], 'comment_action_id' ) ) {
+		return $args;
+	}
+
+	// Comment synced ?
+	$activity_ids = $wpdb->get_col( $wpdb->prepare( "SELECT activity_id FROM {$bp->activity->table_name_meta} WHERE meta_key = %s", "bp_blogs_{$post_type}_comment_id" ) );
+
+	if ( empty( $activity_ids ) ) {
+		return $args;
+	}
+
+	// Init the filter query.
+	$filter_query = array();
+
+	if ( ! isset( $args['scope'] ) || 'null' === $args['scope'] ) {
+		$args['scope'] = '';
+	} elseif ( 'just-me' === $args['scope'] ) {
+		$filter_query = array(
+			'relation' => 'AND',
+			array(
+				'column' => 'user_id',
+				'value'  => bp_displayed_user_id(),
+			),
+		);
+		$args['scope'] = '';
+	}
+
+	$filter_query[] = array(
+		'relation' => 'OR',
+		array(
+			'column' => 'type',
+			'value'  => $args['action'],
+		),
+		array(
+			'column'  => 'id',
+			'value'   =>  $activity_ids,
+			'compare' => 'IN'
+		),
+	);
+
+	$args['filter_query'] = $filter_query;
+
+	// Make sure to have comment in stream mode && avoid duplicate content.
+	$args['display_comments'] = 'stream';
+
+	// Finally reset the action.
+	$args['action'] = '';
+	$args['type']   = '';
+
+	// Return the original arguments.
+	return $args;
+}
+add_filter( 'bp_after_has_activities_parse_args',                'bp_blogs_new_blog_comment_query_backpat' );
+add_filter( 'bp_activity_list_table_filter_activity_type_items', 'bp_blogs_new_blog_comment_query_backpat' );
+
+/**
+ * Utility function to set up some variables for use in the activity loop.
+ *
+ * Grabs the blog's comment depth and the post's open comment status options
+ * for later use in the activity and activity comment loops.
+ *
+ * This is to prevent having to requery these items later on.
+ *
+ * @since 2.0.0
+ *
+ * @see bp_blogs_disable_activity_commenting()
+ * @see bp_blogs_setup_comment_loop_globals_on_ajax()
+ *
+ * @param object $activity The BP_Activity_Activity object.
+ */
+function bp_blogs_setup_activity_loop_globals( $activity ) {
+	if ( ! is_object( $activity ) ) {
+		return;
+	}
+
+	// The activity type does not support comments or replies ? stop now!
+	if ( ! bp_activity_type_supports( $activity->type, 'post-type-comment-reply' ) ) {
+		return;
+	}
+
+	if ( empty( $activity->id ) ) {
+		return;
+	}
+
+	// If we've already done this before, stop now!
+	if ( isset( buddypress()->blogs->allow_comments[ $activity->id ] ) ) {
+		return;
+	}
+
+	$allow_comments = bp_blogs_comments_open( $activity );
+	$thread_depth   = bp_blogs_get_blogmeta( $activity->item_id, 'thread_comments_depth' );
+	$moderation     = bp_blogs_get_blogmeta( $activity->item_id, 'comment_moderation' );
+
+	// Initialize a local object so we won't have to query this again in the
+	// comment loop.
+	if ( empty( buddypress()->blogs->allow_comments ) ) {
+		buddypress()->blogs->allow_comments = array();
+	}
+	if ( empty( buddypress()->blogs->thread_depth ) ) {
+		buddypress()->blogs->thread_depth   = array();
+	}
+	if ( empty( buddypress()->blogs->comment_moderation ) ) {
+		buddypress()->blogs->comment_moderation = array();
+	}
+
+	/*
+	 * Cache comment settings in the buddypress() singleton for later reference.
+	 *
+	 * See bp_blogs_disable_activity_commenting() / bp_blogs_can_comment_reply().
+	 *
+	 * thread_depth is keyed by activity ID instead of blog ID because when we're
+	 * in an actvity comment loop, we don't have access to the blog ID...
+	 *
+	 * Should probably object cache these values instead...
+	 */
+	buddypress()->blogs->allow_comments[ $activity->id ]     = $allow_comments;
+	buddypress()->blogs->thread_depth[ $activity->id ]       = $thread_depth;
+	buddypress()->blogs->comment_moderation[ $activity->id ] = $moderation;
+}
+
+/**
+ * Set up some globals used in the activity comment loop when AJAX is used.
+ *
+ * @since 2.0.0
+ *
+ * @see bp_blogs_setup_activity_loop_globals()
+ */
+function bp_blogs_setup_comment_loop_globals_on_ajax() {
+	// Not AJAX? stop now!
+	if ( ! defined( 'DOING_AJAX' ) ) {
+		return;
+	}
+	if ( false === (bool) constant( 'DOING_AJAX' ) ) {
+		return;
+	}
+
+	// Get the parent activity item.
+	$comment         = bp_activity_current_comment();
+	$parent_activity = new BP_Activity_Activity( $comment->item_id );
+
+	// Setup the globals.
+	bp_blogs_setup_activity_loop_globals( $parent_activity );
+}
+add_action( 'bp_before_activity_comment', 'bp_blogs_setup_comment_loop_globals_on_ajax' );
+
+/**
+ * Disable activity commenting for blog posts based on certain criteria.
+ *
+ * If activity commenting is enabled for blog posts, we still need to disable
+ * commenting if:
+ *  - comments are disabled for the WP blog post from the admin dashboard
+ *  - the WP blog post is supposed to be automatically closed from comments
+ *    based on a certain age
+ *  - the activity entry is a 'new_blog_comment' type
+ *
+ * @since 2.0.0
+ *
+ * @param bool $retval Is activity commenting enabled for this activity entry.
+ * @return bool
+ */
+function bp_blogs_disable_activity_commenting( $retval ) {
+	global $activities_template;
+
+	// If activity commenting is disabled, return current value.
+	if ( bp_disable_blogforum_comments() || ! isset( $activities_template->in_the_loop ) ) {
+		return $retval;
+	}
+
+	$type = bp_get_activity_type();
+
+	// It's a post type supporting comment tracking.
+	if ( bp_activity_type_supports( $type, 'post-type-comment-tracking' ) ) {
+		// The activity type is supporting comments or replies
+		if ( bp_activity_type_supports( $type, 'post-type-comment-reply' ) ) {
+			// Setup some globals we'll need to reference later.
+			bp_blogs_setup_activity_loop_globals( $activities_template->activity );
+
+			// If comments are closed for the WP blog post, we should disable
+			// activity comments for this activity entry.
+			if ( empty( buddypress()->blogs->allow_comments[ bp_get_activity_id() ] ) ) {
+				$retval = false;
+			}
+
+			// If comments need moderation, disable activity commenting.
+			if ( ! empty( buddypress()->blogs->comment_moderation[ bp_get_activity_id() ] ) ) {
+				$retval = false;
+			}
+		// The activity type does not support comments or replies
+		} else {
+			$retval = false;
+		}
+	}
+
+	return $retval;
+}
+add_filter( 'bp_activity_can_comment', 'bp_blogs_disable_activity_commenting' );
+
+/**
+ * Limit the display of post type synced comments.
+ *
+ * @since  2.5.0
+ *
+ * When viewing the synced comments in stream mode, this prevents comments to
+ * be displayed twice, and avoids a Javascript error as the form to add replies
+ * is not available.
+ *
+ * @param  int $retval  The comment count for the activity.
+ * @return int          The comment count, or 0 to hide activity comment replies.
+ */
+function bp_blogs_post_type_comments_avoid_duplicates( $retval ) {
+	/**
+	 * Only limit the display when Post type comments are synced with
+	 * activity comments.
+	 */
+	if ( bp_disable_blogforum_comments() ) {
+		return $retval;
+	}
+
+	if ( 'activity_comment' !== bp_get_activity_type() ) {
+		return $retval;
+	}
+
+	// Check the parent activity
+	$parent_activity = new BP_Activity_Activity( bp_get_activity_item_id() );
+
+	if ( isset( $parent_activity->type ) && bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'post_type' ) ) {
+		$retval = 0;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_activity_get_comment_count', 'bp_blogs_post_type_comments_avoid_duplicates' );
+
+/**
+ * Check if an activity comment associated with a blog post can be replied to.
+ *
+ * By default, disables replying to activity comments if the corresponding WP
+ * blog post no longer accepts comments.
+ *
+ * This check uses a locally-cached value set in {@link bp_blogs_disable_activity_commenting()}
+ * via {@link bp_blogs_setup_activity_loop_globals()}.
+ *
+ * @since 2.0.0
+ *
+ * @param bool         $retval  Are replies allowed for this activity reply.
+ * @param object|array $comment The activity comment object.
+ *
+ * @return bool
+ */
+function bp_blogs_can_comment_reply( $retval, $comment ) {
+	if ( is_array( $comment ) ) {
+		$comment = (object) $comment;
+	}
+
+	// Check comment depth and disable if depth is too large.
+	if ( isset( buddypress()->blogs->thread_depth[$comment->item_id] ) ){
+		if ( bp_activity_get_comment_depth( $comment ) >= buddypress()->blogs->thread_depth[$comment->item_id] ) {
+			$retval = false;
+		}
+	}
+
+	// Check if we should disable activity replies based on the parent activity.
+	if ( isset( buddypress()->blogs->allow_comments[$comment->item_id] ) ){
+		// The blog post has closed off commenting, so we should disable all activity
+		// comments under the parent 'new_blog_post' activity entry.
+		if ( empty( buddypress()->blogs->allow_comments[$comment->item_id] ) ) {
+			$retval = false;
+		}
+	}
+
+	// If comments need moderation, disable activity commenting.
+	if ( ! empty( buddypress()->blogs->comment_moderation[$comment->item_id] ) ) {
+		$retval = false;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_activity_can_comment_reply', 'bp_blogs_can_comment_reply', 10, 2 );
+
+/**
+ * Changes activity comment permalinks to use the blog comment permalink
+ * instead of the activity permalink.
+ *
+ * This is only done if activity commenting is allowed and whether the parent
+ * activity item is a 'new_blog_post' entry.
+ *
+ * @since 2.0.0
+ *
+ * @param string $retval The activity comment permalink.
+ * @return string
+ */
+function bp_blogs_activity_comment_permalink( $retval = '' ) {
+	global $activities_template;
+
+	// Get the current comment ID.
+	$item_id = isset( $activities_template->activity->current_comment->item_id )
+		? $activities_template->activity->current_comment->item_id
+		: false;
+
+	// Maybe adjust the link if item ID exists.
+	if ( ( false !== $item_id ) && isset( buddypress()->blogs->allow_comments[ $item_id ] ) ) {
+		$retval = $activities_template->activity->current_comment->primary_link;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_get_activity_comment_permalink', 'bp_blogs_activity_comment_permalink' );
+
+/**
+ * Changes single activity comment entries to use the blog comment permalink.
+ *
+ * This is only done if the activity comment is associated with a blog comment.
+ *
+ * @since 2.0.1
+ *
+ * @param string               $retval   The activity permalink.
+ * @param BP_Activity_Activity $activity Activity object.
+ * @return string
+ */
+function bp_blogs_activity_comment_single_permalink( $retval, $activity ) {
+	if ( 'activity_comment' !== $activity->type ) {
+		return $retval;
+	}
+
+	if ( bp_disable_blogforum_comments() ) {
+		return $retval;
+	}
+
+	$parent_activity = new BP_Activity_Activity( $activity->item_id );
+
+	if ( isset( $parent_activity->type ) && bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'post_type' ) ) {
+		$retval = $activity->primary_link;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_activity_get_permalink', 'bp_blogs_activity_comment_single_permalink', 10, 2 );
+
+/**
+ * Formats single activity comment entries to use the blog comment action.
+ *
+ * This is only done if the activity comment is associated with a blog comment.
+ *
+ * @since 2.0.1
+ *
+ * @param string               $retval   The activity action.
+ * @param BP_Activity_Activity $activity Activity object.
+ * @return string
+ */
+function bp_blogs_activity_comment_single_action( $retval, $activity ) {
+	if ( 'activity_comment' !== $activity->type ) {
+		return $retval;
+	}
+
+	if ( bp_disable_blogforum_comments() ) {
+		return $retval;
+	}
+
+	$parent_activity = new BP_Activity_Activity( $activity->item_id );
+
+	if ( ! isset( $parent_activity->type ) ) {
+		return $retval;
+	}
+
+	$post_type = bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'post_type' );
+
+	if ( ! $post_type ) {
+		return $retval;
+	}
+
+	$blog_comment_id = bp_activity_get_meta( $activity->id, "bp_blogs_{$post_type}_comment_id" );
+
+	if ( ! empty( $blog_comment_id ) ) {
+		$bp = buddypress();
+
+		// Check if a comment action id is set for the parent activity
+		$comment_action_id = bp_activity_post_type_get_tracking_arg( $parent_activity->type, 'comment_action_id' );
+
+		// Use the action string callback for the activity type
+		if ( ! empty( $comment_action_id ) ) {
+			// Fake a 'new_{post_type}_comment' by cloning the activity object.
+			$object = clone $activity;
+
+			// Set the type of the activity to be a comment about a post type
+			$object->type = $comment_action_id;
+
+			// Use the blog ID as the item_id.
+			$object->item_id = $parent_activity->item_id;
+
+			// Use comment ID as the secondary_item_id.
+			$object->secondary_item_id = $blog_comment_id;
+
+			// Get the format callback for this activity comment
+			$format_callback = bp_activity_post_type_get_tracking_arg( $comment_action_id, 'format_callback' );
+
+			// now format the activity action using the 'new_{post_type}_comment' action callback
+			if ( is_callable( $format_callback ) ) {
+				$retval = call_user_func_array( $format_callback, array( '', $object ) );
+			}
+		}
+	}
+
+	return $retval;
+}
+add_filter( 'bp_get_activity_action_pre_meta', 'bp_blogs_activity_comment_single_action', 10, 2 );
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-cache.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * BuddyPress Blogs Caching.
+ *
+ * Caching functions handle the clearing of cached objects and pages on specific
+ * actions throughout BuddyPress.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsCache
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Slurp up blogmeta for a specified set of blogs.
+ *
+ * It grabs all blogmeta associated with all of the blogs passed
+ * in $blog_ids and adds it to the WP cache. This improves efficiency when
+ * using querying blogmeta inline.
+ *
+ * @param int|string|array|bool $blog_ids Accepts a single blog ID, or a comma-
+ *                                        separated list or array of blog IDs.
+ */
+function bp_blogs_update_meta_cache( $blog_ids = false ) {
+	$cache_args = array(
+		'object_ids'    => $blog_ids,
+		'object_type'   => buddypress()->blogs->id,
+		'object_column' => 'blog_id',
+		'cache_group'   => 'bp_blog_meta',
+		'meta_table'    => buddypress()->blogs->table_name_blogmeta,
+	);
+
+	bp_update_meta_cache( $cache_args );
+}
+/**
+ * Clear the blog object cache.
+ *
+ * @since 1.0.0
+ *
+ * @param int $blog_id ID of the current blog.
+ * @param int $user_id ID of the user whose blog cache should be cleared.
+ */
+function bp_blogs_clear_blog_object_cache( $blog_id = 0, $user_id = 0 ) {
+	if ( ! empty( $user_id ) ) {
+		wp_cache_delete( 'bp_blogs_of_user_'        . $user_id, 'bp' );
+		wp_cache_delete( 'bp_total_blogs_for_user_' . $user_id, 'bp' );
+	}
+
+	wp_cache_delete( 'bp_total_blogs', 'bp' );
+}
+
+// List actions to clear object caches on.
+add_action( 'bp_blogs_remove_blog_for_user', 'bp_blogs_clear_blog_object_cache', 10, 2 );
+add_action( 'wpmu_new_blog',                 'bp_blogs_clear_blog_object_cache', 10, 2 );
+add_action( 'bp_blogs_remove_blog',          'bp_blogs_clear_blog_object_cache' );
+
+// List actions to clear super cached pages on, if super cache is installed.
+add_action( 'bp_blogs_remove_data_for_blog', 'bp_core_clear_cache' );
+add_action( 'bp_blogs_remove_comment',       'bp_core_clear_cache' );
+add_action( 'bp_blogs_remove_post',          'bp_core_clear_cache' );
+add_action( 'bp_blogs_remove_blog_for_user', 'bp_core_clear_cache' );
+add_action( 'bp_blogs_remove_blog',          'bp_core_clear_cache' );
+add_action( 'bp_blogs_new_blog_comment',     'bp_core_clear_cache' );
+add_action( 'bp_blogs_new_blog_post',        'bp_core_clear_cache' );
+add_action( 'bp_blogs_new_blog',             'bp_core_clear_cache' );
+add_action( 'bp_blogs_remove_data',          'bp_core_clear_cache' );
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-filters.php	(revision 12265)
@@ -0,0 +1,163 @@
+<?php
+/**
+ * Filters related to the Blogs component.
+ *
+ * @package BuddyPress
+ * @subpackage BlogFilters
+ * @since 1.6.0
+ */
+
+/** Display Filters **********************************************************/
+
+add_filter( 'bp_get_blog_latest_post_title', 'wptexturize'   );
+add_filter( 'bp_get_blog_latest_post_title', 'convert_chars' );
+add_filter( 'bp_get_blog_latest_post_title', 'trim'          );
+
+add_filter( 'bp_blog_latest_post_content', 'wptexturize'        );
+add_filter( 'bp_blog_latest_post_content', 'convert_smilies'    );
+add_filter( 'bp_blog_latest_post_content', 'convert_chars'      );
+add_filter( 'bp_blog_latest_post_content', 'wpautop'            );
+add_filter( 'bp_blog_latest_post_content', 'shortcode_unautop'  );
+add_filter( 'bp_blog_latest_post_content', 'prepend_attachment' );
+
+/**
+ * Ensure that the 'Create a new site' link at wp-admin/my-sites.php points to the BP blog signup.
+ *
+ * @since 1.6.0
+ *
+ *       returned value.
+ *
+ * @param string $url The original URL (points to wp-signup.php by default).
+ * @return string The new URL.
+ */
+function bp_blogs_creation_location( $url ) {
+
+	/**
+	 * Filters the 'Create a new site' link URL.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value URL for the 'Create a new site' signup page.
+	 */
+	return apply_filters( 'bp_blogs_creation_location', trailingslashit( bp_get_blogs_directory_permalink() . 'create' ), $url );
+}
+add_filter( 'wp_signup_location', 'bp_blogs_creation_location' );
+
+/**
+ * Only select comments by ID instead of all fields when using get_comments().
+ *
+ * @since 2.1.0
+ *
+ * @see bp_blogs_update_post_activity_meta()
+ *
+ * @param array $retval Current SQL clauses in array format.
+ * @return array
+ */
+function bp_blogs_comments_clauses_select_by_id( $retval ) {
+	$retval['fields'] = 'comment_ID';
+
+	return $retval;
+}
+
+/**
+ * Check whether the current activity about a post or a comment can be published.
+ *
+ * Abstracted from the deprecated `bp_blogs_record_post()`.
+ *
+ * @since 2.2.0
+ *
+ * @param bool $return  Whether the post should be published.
+ * @param int  $blog_id ID of the blog.
+ * @param int  $post_id ID of the post.
+ * @param int  $user_id ID of the post author.
+ * @return bool True to authorize the post to be published, otherwise false.
+ */
+function bp_blogs_post_pre_publish( $return = true, $blog_id = 0, $post_id = 0, $user_id = 0 ) {
+	$bp = buddypress();
+
+	// If blog is not trackable, do not record the activity.
+	if ( ! bp_blogs_is_blog_trackable( $blog_id, $user_id ) ) {
+		return false;
+	}
+
+	/*
+	 * Stop infinite loops with WordPress MU Sitewide Tags.
+	 * That plugin changed the way its settings were stored at some point. Thus the dual check.
+	 */
+	$sitewide_tags_blog_settings = bp_core_get_root_option( 'sitewide_tags_blog' );
+	if ( ! empty( $sitewide_tags_blog_settings ) ) {
+		$st_options = maybe_unserialize( $sitewide_tags_blog_settings );
+		$tags_blog_id = isset( $st_options['tags_blog_id'] ) ? $st_options['tags_blog_id'] : 0;
+	} else {
+		$tags_blog_id = bp_core_get_root_option( 'sitewide_tags_blog' );
+		$tags_blog_id = intval( $tags_blog_id );
+	}
+
+	/**
+	 * Filters whether or not BuddyPress should block sitewide tags activity.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param bool $value Current status of the sitewide tags activity.
+	 */
+	if ( (int) $blog_id == $tags_blog_id && apply_filters( 'bp_blogs_block_sitewide_tags_activity', true ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters whether or not the current blog is public.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param int $value Value from the blog_public option for the current blog.
+	 */
+	$is_blog_public = apply_filters( 'bp_is_blog_public', (int) get_blog_option( $blog_id, 'blog_public' ) );
+
+	if ( 0 === $is_blog_public && is_multisite() ) {
+		return false;
+	}
+
+	return $return;
+}
+add_filter( 'bp_activity_post_pre_publish', 'bp_blogs_post_pre_publish', 10, 4 );
+add_filter( 'bp_activity_post_pre_comment', 'bp_blogs_post_pre_publish', 10, 4 );
+
+/**
+ * Registers our custom thumb size with WP's Site Icon feature.
+ *
+ * @since 2.7.0
+ *
+ * @param  array $sizes Current array of custom site icon sizes.
+ * @return array
+ */
+function bp_blogs_register_custom_site_icon_size( $sizes ) {
+	$sizes[] = bp_core_avatar_thumb_width();
+	return $sizes;
+}
+add_filter( 'site_icon_image_sizes', 'bp_blogs_register_custom_site_icon_size' );
+
+/**
+ * Filters the column name during blog metadata queries.
+ *
+ * This filters 'sanitize_key', which is used during various core metadata
+ * API functions: {@link https://core.trac.wordpress.org/browser/branches/4.9/src/wp-includes/meta.php?lines=47,160,324}.
+ * Due to how we are passing our meta type, we need to ensure that the correct
+ * DB column is referenced during blogmeta queries.
+ *
+ * @since 4.0.0
+ *
+ * @see bp_blogs_delete_blogmeta()
+ * @see bp_blogs_get_blogmeta()
+ * @see bp_blogs_update_blogmeta()
+ * @see bp_blogs_add_blogmeta()
+ *
+ * @param string $retval
+ *
+ * @return string
+ */
+function bp_blogs_filter_meta_column_name( $retval ) {
+	if ( 'bp_blog_id' === $retval ) {
+		$retval = 'blog_id';
+	}
+	return $retval;
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-functions.php	(revision 12265)
@@ -0,0 +1,1462 @@
+<?php
+/**
+ * Blogs component functions.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Check whether the $bp global lists an activity directory page.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if set, false if empty.
+ */
+function bp_blogs_has_directory() {
+	$bp = buddypress();
+
+	return (bool) !empty( $bp->pages->blogs->id );
+}
+
+/**
+ * Retrieve a set of blogs.
+ *
+ * @see BP_Blogs_Blog::get() for a description of arguments and return value.
+ *
+ * @param array|string $args {
+ *     Arguments are listed here with their default values. For more
+ *     information about the arguments, see {@link BP_Blogs_Blog::get()}.
+ *     @type string      $type              Default: 'active'.
+ *     @type int|bool    $user_id           Default: false.
+ *     @type array       $include_blog_ids  Default: false.
+ *     @type string|bool $search_terms      Default: false.
+ *     @type int         $per_page          Default: 20.
+ *     @type int         $page              Default: 1.
+ *     @type bool        $update_meta_cache Whether to pre-fetch blogmeta. Default: true.
+ * }
+ * @return array See {@link BP_Blogs_Blog::get()}.
+ */
+function bp_blogs_get_blogs( $args = '' ) {
+
+	// Parse query arguments.
+	$r = bp_parse_args( $args, array(
+		'type'              => 'active', // 'active', 'alphabetical', 'newest', or 'random'
+		'include_blog_ids'  => false,    // Array of blog IDs to include
+		'user_id'           => false,    // Limit to blogs this user can post to
+		'search_terms'      => false,    // Limit to blogs matching these search terms
+		'per_page'          => 20,       // The number of results to return per page
+		'page'              => 1,        // The page to return if limiting per page
+		'update_meta_cache' => true      // Whether to pre-fetch blogmeta
+	), 'blogs_get_blogs' );
+
+	// Get the blogs.
+	$blogs = BP_Blogs_Blog::get(
+		$r['type'],
+		$r['per_page'],
+		$r['page'],
+		$r['user_id'],
+		$r['search_terms'],
+		$r['update_meta_cache'],
+		$r['include_blog_ids']
+	);
+
+	/**
+	 * Filters a set of blogs.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $blogs Array of blog data.
+	 * @param array $r     Parsed query arguments.
+	 */
+	return apply_filters( 'bp_blogs_get_blogs', $blogs, $r );
+}
+
+/**
+ * Populate the BP blogs table with existing blogs.
+ *
+ * Warning: By default, this will remove all existing records from the BP
+ * blogs and blogmeta tables before re-populating the tables.
+ *
+ * @since 1.0.0
+ * @since 2.6.0 Accepts $args as a parameter.
+ *
+ * @param array $args {
+ *     Array of arguments.
+ *     @type int   $offset   The offset to use.
+ *     @type int   $limit    The number of blogs to record at one time.
+ *     @type array $blog_ids Blog IDs to record. If empty, all blogs will be recorded.
+ *     @type array $site_id  The network site ID to use.
+ * }
+ *
+ * @return bool
+ */
+function bp_blogs_record_existing_blogs( $args = array() ) {
+	global $wpdb;
+
+	// Query for all sites in network.
+	$r = bp_parse_args( $args, array(
+		'offset'   => (int) bp_get_option( '_bp_record_blogs_offset' ),
+		'limit'    => 50,
+		'blog_ids' => array(),
+		'site_id'  => $wpdb->siteid
+	), 'record_existing_blogs' );
+
+	// Truncate all BP blogs tables if starting fresh
+	if ( empty( $r['offset'] ) && empty( $r['blog_ids'] ) ) {
+		$bp = buddypress();
+
+		// Truncate user blogs table
+		$truncate = $wpdb->query( "TRUNCATE {$bp->blogs->table_name}" );
+		if ( is_wp_error( $truncate ) ) {
+			return false;
+		}
+
+		// Truncate user blogmeta table
+		$truncate = $wpdb->query( "TRUNCATE {$bp->blogs->table_name_blogmeta}" );
+		if ( is_wp_error( $truncate ) ) {
+			return false;
+		}
+	}
+
+	// Multisite
+	if ( is_multisite() ) {
+		$sql = array();
+		$sql['select'] = $wpdb->prepare( "SELECT blog_id, last_updated FROM {$wpdb->base_prefix}blogs WHERE mature = 0 AND spam = 0 AND deleted = 0 AND site_id = %d", $r['site_id'] );
+
+		// Omit root blog if large network
+		if ( wp_is_large_network( 'users' ) ) {
+			$sql['omit_root_blog'] = $wpdb->prepare( "AND blog_id != %d", bp_get_root_blog_id() );
+		}
+
+		// Filter by selected blog IDs
+		if ( ! empty( $r['blog_ids'] ) ) {
+			$in        = implode( ',', wp_parse_id_list( $r['blog_ids'] ) );
+			$sql['in'] = "AND blog_id IN ({$in})";
+		}
+
+		$sql['orderby'] = 'ORDER BY blog_id ASC';
+
+		$sql['limit'] = $wpdb->prepare( "LIMIT %d", $r['limit'] );
+
+		if ( ! empty( $r['offset'] ) ) {
+			$sql['offset'] = $wpdb->prepare( "OFFSET %d", $r['offset'] );
+		}
+
+		$blogs = $wpdb->get_results( implode( ' ', $sql ) );
+
+	// Record a single site.
+	} else {
+		// Just record blog for the current user only.
+		$record = bp_blogs_record_blog( $wpdb->blogid, get_current_user_id(), true );
+
+		if ( false === $record ) {
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	 // Bail if there are no blogs
+	 if ( empty( $blogs ) ) {
+		// Make sure we remove our offset marker
+		if ( is_multisite() ) {
+			bp_delete_option( '_bp_record_blogs_offset' );
+		}
+
+		return false;
+	}
+
+	// Loop through users of blogs and record the relationship.
+	foreach ( (array) $blogs as $blog ) {
+
+		// Ensure that the cache is clear after the table TRUNCATE above.
+		wp_cache_delete( $blog->blog_id, 'bp_blog_meta' );
+
+		// Get all users.
+		$users = get_users( array(
+			'blog_id' => $blog->blog_id,
+			'fields'  => 'ID'
+		) );
+
+		// Continue on if no users exist for this site (how did this happen?).
+		if ( empty( $users ) ) {
+			continue;
+		}
+
+		// Loop through users and record their relationship to this blog.
+		foreach ( (array) $users as $user_id ) {
+			bp_blogs_add_user_to_blog( $user_id, false, $blog->blog_id );
+
+			// Clear cache
+			bp_blogs_clear_blog_object_cache( $blog->blog_id, $user_id );
+		}
+
+		// Update blog last activity timestamp
+		if ( ! empty( $blog->last_updated ) && false !== strtotime( $blog->last_updated ) ) {
+			bp_blogs_update_blogmeta( $blog->blog_id, 'last_activity', $blog->last_updated );
+		}
+	}
+
+	// See if we need to do this again
+	if ( is_multisite() && empty( $r['blog_ids'] ) ) {
+		$sql['offset'] = $wpdb->prepare( " OFFSET %d", $r['limit'] + $r['offset'] );
+
+		// Check if there are more blogs to record
+		$blog_ids = $wpdb->get_results( implode( ' ', $sql ) );
+
+		// We have more blogs; record offset and re-run function
+		if ( ! empty( $blog_ids  ) ) {
+			bp_update_option( '_bp_record_blogs_offset', $r['limit'] + $r['offset'] );
+			bp_blogs_record_existing_blogs( array(
+				'offset'   => $r['limit'] + $r['offset'],
+				'limit'    => $r['limit'],
+				'blog_ids' => $r['blog_ids'],
+				'site_id'  => $r['site_id']
+			) );
+
+			// Bail since we have more blogs to record.
+			return;
+
+		// No more blogs; delete offset marker
+		} else {
+			bp_delete_option( '_bp_record_blogs_offset' );
+		}
+	}
+
+	/**
+	 * Fires after the BP blogs tables have been populated with existing blogs.
+	 *
+	 * @since 2.4.0
+	 */
+	do_action( 'bp_blogs_recorded_existing_blogs' );
+
+	// No errors.
+	return true;
+}
+
+/**
+ * Check whether a given blog should be recorded in activity streams.
+ *
+ * If $user_id is provided, you can restrict site from being recordable
+ * only to particular users.
+ *
+ * @since 1.7.0
+ *
+ * @param int $blog_id ID of the blog being checked.
+ * @param int $user_id Optional. ID of the user for whom access is being checked.
+ * @return bool True if blog is recordable, otherwise false.
+ */
+function bp_blogs_is_blog_recordable( $blog_id, $user_id = 0 ) {
+
+	/**
+	 * Filters whether or not a blog is globally activity stream recordable.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool $value   Whether or not recordable. Default true.
+	 * @param int  $blog_id Current blog ID.
+	 */
+	$recordable_globally = apply_filters( 'bp_blogs_is_blog_recordable', true, $blog_id );
+
+	if ( !empty( $user_id ) ) {
+		/**
+		 * Filters whether or not a blog is globally activity stream recordable for user.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param bool $recordable_globally Whether or not recordable.
+		 * @param int  $blog_id             Current blog ID.
+		 * @param int  $user_id             Current user ID.
+		 */
+		$recordable_for_user = apply_filters( 'bp_blogs_is_blog_recordable_for_user', $recordable_globally, $blog_id, $user_id );
+	} else {
+		$recordable_for_user = $recordable_globally;
+	}
+
+	if ( !empty( $recordable_for_user ) ) {
+		return true;
+	}
+
+	return $recordable_globally;
+}
+
+/**
+ * Check whether a given blog should be tracked by the Blogs component.
+ *
+ * If $user_id is provided, the developer can restrict site from
+ * being trackable only to particular users.
+ *
+ * @since 1.7.0
+ *
+ * @param int $blog_id ID of the blog being checked.
+ * @param int $user_id Optional. ID of the user for whom access is being checked.
+ * @return bool True if blog is trackable, otherwise false.
+ */
+function bp_blogs_is_blog_trackable( $blog_id, $user_id = 0 ) {
+
+	/**
+	 * Filters whether or not a blog is globally trackable.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool $value Whether or not trackable.
+	 * @param int  $blog_id Current blog ID.
+	 */
+	$trackable_globally = apply_filters( 'bp_blogs_is_blog_trackable', bp_blogs_is_blog_recordable( $blog_id, $user_id ), $blog_id );
+
+	if ( !empty( $user_id ) ) {
+
+		/**
+		 * Filters whether or not a blog is globally trackable for user.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param bool $value   Whether or not trackable.
+		 * @param int  $blog_id Current blog ID.
+		 * @param int  $user_id Current user ID.
+		 */
+		$trackable_for_user = apply_filters( 'bp_blogs_is_blog_trackable_for_user', $trackable_globally, $blog_id, $user_id );
+	} else {
+		$trackable_for_user = $trackable_globally;
+	}
+
+	if ( !empty( $trackable_for_user ) ) {
+		return $trackable_for_user;
+	}
+
+	return $trackable_globally;
+}
+
+/**
+ * Make BuddyPress aware of a new site so that it can track its activity.
+ *
+ * @since 1.0.0
+ *
+ * @param int  $blog_id     ID of the blog being recorded.
+ * @param int  $user_id     ID of the user for whom the blog is being recorded.
+ * @param bool $no_activity Optional. Whether to skip recording an activity
+ *                          item about this blog creation. Default: false.
+ * @return false|null Returns false on failure.
+ */
+function bp_blogs_record_blog( $blog_id, $user_id, $no_activity = false ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_loggedin_user_id();
+
+	// If blog is not recordable, do not record the activity.
+	if ( !bp_blogs_is_blog_recordable( $blog_id, $user_id ) )
+		return false;
+
+	$name = get_blog_option( $blog_id, 'blogname' );
+	$url  = get_home_url( $blog_id );
+
+	if ( empty( $name ) ) {
+		$name = $url;
+	}
+
+	$description     = get_blog_option( $blog_id, 'blogdescription' );
+	$close_old_posts = get_blog_option( $blog_id, 'close_comments_for_old_posts' );
+	$close_days_old  = get_blog_option( $blog_id, 'close_comments_days_old' );
+	$moderation      = get_blog_option( $blog_id, 'comment_moderation' );
+
+	$thread_depth = get_blog_option( $blog_id, 'thread_comments' );
+	if ( ! empty( $thread_depth ) ) {
+		$thread_depth = get_blog_option( $blog_id, 'thread_comments_depth' );
+	} else {
+		// Perhaps filter this?
+		$thread_depth = 1;
+	}
+
+	$recorded_blog          = new BP_Blogs_Blog;
+	$recorded_blog->user_id = $user_id;
+	$recorded_blog->blog_id = $blog_id;
+	$recorded_blog_id       = $recorded_blog->save();
+	$is_recorded            = !empty( $recorded_blog_id ) ? true : false;
+
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'url', $url );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'name', $name );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'description', $description );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'last_activity', bp_core_current_time() );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'close_comments_for_old_posts', $close_old_posts );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'close_comments_days_old', $close_days_old );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'thread_comments_depth', $thread_depth );
+	bp_blogs_update_blogmeta( $recorded_blog->blog_id, 'comment_moderation', $moderation );
+
+	$is_private = !empty( $_POST['blog_public'] ) && (int) $_POST['blog_public'] ? false : true;
+
+	/**
+	 * Filters whether or not a new blog is public.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $is_private Whether or not blog is public.
+	 */
+	$is_private = !apply_filters( 'bp_is_new_blog_public', !$is_private );
+
+	/**
+	 * Fires after BuddyPress has been made aware of a new site for activity tracking.
+	 *
+	 * @since 1.0.0
+	 * @since 2.6.0 Added $no_activity as a parameter.
+	 *
+	 * @param BP_Blogs_Blog $recorded_blog Current blog being recorded. Passed by reference.
+	 * @param bool          $is_private    Whether or not the current blog being recorded is private.
+	 * @param bool          $is_recorded   Whether or not the current blog was recorded.
+	 * @param bool          $no_activity   Whether to skip recording an activity item for this blog creation.
+	 */
+	do_action_ref_array( 'bp_blogs_new_blog', array( &$recorded_blog, $is_private, $is_recorded, $no_activity ) );
+}
+add_action( 'wpmu_new_blog', 'bp_blogs_record_blog', 10, 2 );
+
+/**
+ * Update blog name in BuddyPress blogmeta table.
+ *
+ * @global object $wpdb DB Layer.
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but
+ *                         unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_blogname( $oldvalue, $newvalue ) {
+	global $wpdb;
+
+	bp_blogs_update_blogmeta( $wpdb->blogid, 'name', $newvalue );
+}
+add_action( 'update_option_blogname', 'bp_blogs_update_option_blogname', 10, 2 );
+
+/**
+ * Update blog description in BuddyPress blogmeta table.
+ *
+ * @global object $wpdb DB Layer.
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but
+ *                         unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_blogdescription( $oldvalue, $newvalue ) {
+	global $wpdb;
+
+	bp_blogs_update_blogmeta( $wpdb->blogid, 'description', $newvalue );
+}
+add_action( 'update_option_blogdescription', 'bp_blogs_update_option_blogdescription', 10, 2 );
+
+/**
+ * Update "Close comments for old posts" option in BuddyPress blogmeta table.
+ *
+ * @since 2.0.0
+ *
+ * @global object $wpdb DB Layer.
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but
+ *                         unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_close_comments_for_old_posts( $oldvalue, $newvalue ) {
+	global $wpdb;
+
+	bp_blogs_update_blogmeta( $wpdb->blogid, 'close_comments_for_old_posts', $newvalue );
+}
+add_action( 'update_option_close_comments_for_old_posts', 'bp_blogs_update_option_close_comments_for_old_posts', 10, 2 );
+
+/**
+ * Update "Close comments after days old" option in BuddyPress blogmeta table.
+ *
+ * @since 2.0.0
+ *
+ * @global object $wpdb DB Layer.
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but
+ *                         unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_close_comments_days_old( $oldvalue, $newvalue ) {
+	global $wpdb;
+
+	bp_blogs_update_blogmeta( $wpdb->blogid, 'close_comments_days_old', $newvalue );
+}
+add_action( 'update_option_close_comments_days_old', 'bp_blogs_update_option_close_comments_days_old', 10, 2 );
+
+/**
+ * When toggling threaded comments, update thread depth in blogmeta table.
+ *
+ * @since 2.0.0
+ *
+ * @global object $wpdb DB Layer.
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but
+ *                         unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_thread_comments( $oldvalue, $newvalue ) {
+	global $wpdb;
+
+	if ( empty( $newvalue ) ) {
+		$thread_depth = 1;
+	} else {
+		$thread_depth = get_option( 'thread_comments_depth' );
+	}
+
+	bp_blogs_update_blogmeta( $wpdb->blogid, 'thread_comments_depth', $thread_depth );
+}
+add_action( 'update_option_thread_comments', 'bp_blogs_update_option_thread_comments', 10, 2 );
+
+/**
+ * When updating comment depth, update thread depth in blogmeta table.
+ *
+ * @since 2.0.0
+ *
+ * @global object $wpdb DB Layer.
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but
+ *                         unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_thread_comments_depth( $oldvalue, $newvalue ) {
+	global $wpdb;
+
+	$comments_enabled = get_option( 'thread_comments' );
+
+	if (  $comments_enabled ) {
+		bp_blogs_update_blogmeta( $wpdb->blogid, 'thread_comments_depth', $newvalue );
+	}
+}
+add_action( 'update_option_thread_comments_depth', 'bp_blogs_update_option_thread_comments_depth', 10, 2 );
+
+/**
+ * When updating comment moderation, mirror value in blogmeta table.
+ *
+ * @since 3.0.0
+ *
+ * @param string $oldvalue Value before save. Passed by do_action() but unused here.
+ * @param string $newvalue Value to change meta to.
+ */
+function bp_blogs_update_option_comment_moderation( $oldvalue, $newvalue ) {
+	bp_blogs_update_blogmeta( $GLOBALS['wpdb']->blogid, 'comment_moderation', $newvalue );
+}
+add_action( 'update_option_comment_moderation', 'bp_blogs_update_option_comment_moderation', 10, 2 );
+
+/**
+ * Syncs site icon URLs to blogmeta.
+ *
+ * @since 2.7.0
+ *
+ * @param int|string $old_value Old value
+ * @param int|string $new_value New value
+ */
+function bp_blogs_update_option_site_icon( $old_value, $new_value ) {
+	if ( 0 === $new_value ) {
+		bp_blogs_update_blogmeta( get_current_blog_id(), 'site_icon_url_thumb', 0 );
+		bp_blogs_update_blogmeta( get_current_blog_id(), 'site_icon_url_full',  0 );
+	} else {
+		// Save site icon URL as blogmeta.
+		bp_blogs_update_blogmeta( get_current_blog_id(), 'site_icon_url_thumb', get_site_icon_url( bp_core_avatar_thumb_width() ) );
+		bp_blogs_update_blogmeta( get_current_blog_id(), 'site_icon_url_full',  get_site_icon_url( bp_core_avatar_full_width()  ) );
+	}
+}
+add_action( 'update_option_site_icon', 'bp_blogs_update_option_site_icon', 10, 2 );
+
+/**
+ * Deletes the 'url' blogmeta for a site.
+ *
+ * Fires when a site's details are updated, which generally happens when
+ * editing a site under "Network Admin > Sites". Prior to WP 4.9, the
+ * correct hook was 'refresh_blog_details'; afterward, 'clean_site_cache'.
+ *
+ * @since 2.3.0
+ *
+ * @param int $site_id The site ID.
+ */
+function bp_blogs_delete_url_blogmeta( $site_id = 0 ) {
+	bp_blogs_delete_blogmeta( (int) $site_id, 'url' );
+}
+
+if ( function_exists( 'wp_switch_roles_and_user' ) ) {
+	add_action( 'clean_site_cache', 'bp_blogs_delete_url_blogmeta' );
+} else {
+	add_action( 'refresh_blog_details', 'bp_blogs_delete_url_blogmeta' );
+}
+
+/**
+ * Record activity metadata about a published blog post.
+ *
+ * @since 2.2.0
+ *
+ * @param int     $activity_id ID of the activity item.
+ * @param WP_Post $post        Post object.
+ * @param array   $args        Array of arguments.
+ */
+function bp_blogs_publish_post_activity_meta( $activity_id, $post, $args ) {
+	if ( empty( $activity_id ) || 'post' != $post->post_type ) {
+		return;
+	}
+
+	bp_activity_update_meta( $activity_id, 'post_title', $post->post_title );
+
+	if ( ! empty( $args['post_url'] ) ) {
+		$post_permalink = $args['post_url'];
+	} else {
+		$post_permalink = $post->guid;
+	}
+
+	bp_activity_update_meta( $activity_id, 'post_url',   $post_permalink );
+
+	// Update the blog's last activity.
+	bp_blogs_update_blogmeta( $args['item_id'], 'last_activity', bp_core_current_time() );
+
+	/**
+	 * Fires after BuddyPress has recorded metadata about a published blog post.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int     $ID    ID of the blog post being recorded.
+	 * @param WP_Post $post  WP_Post object for the current blog post.
+	 * @param string  $value ID of the user associated with the current blog post.
+	 */
+	do_action( 'bp_blogs_new_blog_post', $post->ID, $post, $args['user_id'] );
+}
+add_action( 'bp_activity_post_type_published', 'bp_blogs_publish_post_activity_meta', 10, 3 );
+
+/**
+ * Updates a blog post's activity meta entry during a post edit.
+ *
+ * @since 2.2.0
+ * @since 2.5.0 Add the post type tracking args object parameter
+ *
+ * @param WP_Post              $post                 Post object.
+ * @param BP_Activity_Activity $activity             Activity object.
+ * @param object               $activity_post_object The post type tracking args object.
+ */
+function bp_blogs_update_post_activity_meta( $post, $activity, $activity_post_object ) {
+	if ( empty( $activity->id ) || empty( $activity_post_object->action_id ) ) {
+		return;
+	}
+
+	// Update post title in activity meta.
+	$existing_title = bp_activity_get_meta( $activity->id, 'post_title' );
+	if ( $post->post_title !== $existing_title ) {
+		bp_activity_update_meta( $activity->id, 'post_title', $post->post_title );
+
+		if ( ! empty( $activity_post_object->comments_tracking->action_id ) ) {
+			// Now update activity meta for post comments... sigh.
+			add_filter( 'comments_clauses', 'bp_blogs_comments_clauses_select_by_id' );
+			$comments = get_comments( array( 'post_id' => $post->ID ) );
+			remove_filter( 'comments_clauses', 'bp_blogs_comments_clauses_select_by_id' );
+
+			if ( ! empty( $comments ) ) {
+				$activity_ids = array();
+				$comment_ids  = wp_list_pluck( $comments, 'comment_ID' );
+
+				// Set up activity args.
+				$args = array(
+					'update_meta_cache' => false,
+					'show_hidden'       => true,
+					'per_page'          => 99999,
+				);
+
+				// Query for old-style "new_blog_comment" activity items.
+				$args['filter'] = array(
+					'object'       => $activity_post_object->comments_tracking->component_id,
+					'action'       => $activity_post_object->comments_tracking->action_id,
+					'secondary_id' => implode( ',', $comment_ids ),
+				);
+
+				$activities = bp_activity_get( $args );
+				if ( ! empty( $activities['activities'] ) ) {
+					$activity_ids = (array) wp_list_pluck( $activities['activities'], 'id' );
+				}
+
+				// Query for activity comments connected to a blog post.
+				unset( $args['filter'] );
+				$args['meta_query'] = array( array(
+					'key'     => 'bp_blogs_' . $post->post_type . '_comment_id',
+					'value'   => $comment_ids,
+					'compare' => 'IN',
+				) );
+				$args['type'] = 'activity_comment';
+				$args['display_comments'] = 'stream';
+
+				$activities = bp_activity_get( $args );
+				if ( ! empty( $activities['activities'] ) ) {
+					$activity_ids = array_merge( $activity_ids, (array) wp_list_pluck( $activities['activities'], 'id' ) );
+				}
+
+				// Update activity meta for all found activity items.
+				if ( ! empty( $activity_ids ) ) {
+					foreach ( $activity_ids as $aid ) {
+						bp_activity_update_meta( $aid, 'post_title', $post->post_title );
+					}
+				}
+
+				unset( $activities, $activity_ids, $comment_ids, $comments );
+			}
+		}
+	}
+
+	// Add post comment status to activity meta if closed.
+	if( 'closed' == $post->comment_status ) {
+		bp_activity_update_meta( $activity->id, 'post_comment_status', $post->comment_status );
+	} else {
+		bp_activity_delete_meta( $activity->id, 'post_comment_status' );
+	}
+}
+add_action( 'bp_activity_post_type_updated', 'bp_blogs_update_post_activity_meta', 10, 3 );
+
+/**
+ * Update Activity and blogs meta and eventually sync comment with activity comment
+ *
+ * @since  2.5.0
+ *
+ * @param  int|bool        $activity_id          ID of recorded activity, or false if sync is active.
+ * @param  WP_Comment|null $comment              The comment object.
+ * @param  array           $activity_args        Array of activity arguments.
+ * @param  object|null     $activity_post_object The post type tracking args object.
+ * @return WP_Error|bool|int Returns false if no activity, the activity id otherwise.
+ */
+function bp_blogs_comment_sync_activity_comment( &$activity_id, $comment = null, $activity_args = array(), $activity_post_object = null ) {
+	if ( empty( $activity_args ) || empty( $comment->post->ID ) || empty( $activity_post_object->comment_action_id ) ) {
+		return false;
+	}
+
+	// Set the current blog id.
+	$blog_id = get_current_blog_id();
+
+	// These activity metadatas are used to build the new_blog_comment action string
+	if ( ! empty( $activity_id ) && ! empty( $activity_args['item_id'] ) && 'new_blog_comment' === $activity_post_object->comment_action_id ) {
+		// add some post info in activity meta
+		bp_activity_update_meta( $activity_id, 'post_title', $comment->post->post_title );
+		bp_activity_update_meta( $activity_id, 'post_url',   esc_url_raw( add_query_arg( 'p', $comment->post->ID, home_url( '/' ) ) ) );
+	}
+
+	// Sync comment - activity comment
+	if ( ! bp_disable_blogforum_comments() ) {
+
+		if ( ! empty( $_REQUEST['action'] ) ) {
+			$existing_activity_id = get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true );
+
+			if ( ! empty( $existing_activity_id ) ) {
+				$activity_args['id'] = $existing_activity_id;
+			}
+		}
+
+		if ( empty( $activity_post_object ) ) {
+			$activity_post_object = bp_activity_get_post_type_tracking_args( $comment->post->post_type );
+		}
+
+		if ( isset( $activity_post_object->action_id ) && isset( $activity_post_object->component_id ) ) {
+			// find the parent 'new_post_type' activity entry
+			$parent_activity_id = bp_activity_get_activity_id( array(
+				'component'         => $activity_post_object->component_id,
+				'type'              => $activity_post_object->action_id,
+				'item_id'           => $blog_id,
+				'secondary_item_id' => $comment->comment_post_ID
+			) );
+
+			// Try to create a new activity item for the parent blog post.
+			if ( empty( $parent_activity_id ) ) {
+				$parent_activity_id = bp_activity_post_type_publish( $comment->post->ID, $comment->post );
+			}
+		}
+
+		// we found the parent activity entry
+		// so let's go ahead and reconfigure some activity args
+		if ( ! empty( $parent_activity_id ) ) {
+			// set the parent activity entry ID
+			$activity_args['activity_id'] = $parent_activity_id;
+
+			// now see if the WP parent comment has a BP activity ID
+			$comment_parent = 0;
+			if ( ! empty( $comment->comment_parent ) ) {
+				$comment_parent = get_comment_meta( $comment->comment_parent, 'bp_activity_comment_id', true );
+			}
+
+			// WP parent comment does not have a BP activity ID
+			// so set to 'new_' . post_type activity ID
+			if ( empty( $comment_parent ) ) {
+				$comment_parent = $parent_activity_id;
+			}
+
+			$activity_args['parent_id']         = $comment_parent;
+			$activity_args['skip_notification'] = true;
+
+		// could not find corresponding parent activity entry
+		// so wipe out $args array
+		} else {
+			$activity_args = array();
+		}
+
+		// Record in activity streams
+		if ( ! empty( $activity_args ) ) {
+			$activity_id = bp_activity_new_comment( $activity_args );
+
+			if ( empty( $activity_args['id'] ) ) {
+				// The activity metadata to inform about the corresponding comment ID
+				bp_activity_update_meta( $activity_id, "bp_blogs_{$comment->post->post_type}_comment_id", $comment->comment_ID );
+
+				// The comment metadata to inform about the corresponding activity ID
+				add_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', $activity_id );
+
+				// These activity metadatas are used to build the new_blog_comment action string
+				if ( 'new_blog_comment' === $activity_post_object->comment_action_id ) {
+					bp_activity_update_meta( $activity_id, 'post_title', $comment->post->post_title );
+					bp_activity_update_meta( $activity_id, 'post_url', esc_url_raw( add_query_arg( 'p', $comment->post->ID, home_url( '/' ) ) ) );
+				}
+			}
+
+			/**
+			 * Fires after an activity comment is added from a WP post comment.
+			 *
+			 * @since 2.6.0
+			 *
+			 * @param int        $activity_id          The activity comment ID.
+			 * @param WP_Comment $post_type_comment    WP Comment object.
+			 * @param array      $activity_args        Activity comment arguments.
+			 * @param object     $activity_post_object The post type tracking args object.
+			 */
+			do_action( 'bp_blogs_comment_sync_activity_comment', $activity_id, $comment, $activity_args, $activity_post_object );
+		}
+	}
+
+	// Update the blogs last active date
+	bp_blogs_update_blogmeta( $blog_id, 'last_activity', bp_core_current_time() );
+
+	if ( 'new_blog_comment' === $activity_post_object->comment_action_id ) {
+		/**
+		 * Fires after BuddyPress has recorded metadata about a published blog post comment.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param int     $value    Comment ID of the blog post comment being recorded.
+		 * @param WP_Post $post  WP_Comment object for the current blog post.
+		 * @param string  $value ID of the user associated with the current blog post comment.
+		 */
+		do_action( 'bp_blogs_new_blog_comment', $comment->comment_ID, $comment, bp_loggedin_user_id() );
+	}
+
+	return $activity_id;
+}
+add_action( 'bp_activity_post_type_comment', 'bp_blogs_comment_sync_activity_comment', 10, 4 );
+
+/**
+ * Record a user's association with a blog.
+ *
+ * This function is hooked to several WordPress actions where blog roles are
+ * set/changed ('add_user_to_blog', 'profile_update', 'user_register'). It
+ * parses the changes, and records them as necessary in the BP blog tracker.
+ *
+ * BuddyPress does not track blogs for users with the 'subscriber' role by
+ * default, though as of 2.1.0 you can filter 'bp_blogs_get_allowed_roles' to
+ * modify this behavior.
+ *
+ * @param int         $user_id The ID of the user.
+ * @param string|bool $role    User's WordPress role for this blog ID.
+ * @param int         $blog_id Blog ID user is being added to.
+ * @return false|null False on failure.
+ */
+function bp_blogs_add_user_to_blog( $user_id, $role = false, $blog_id = 0 ) {
+	global $wpdb;
+
+	// If no blog ID was passed, use the root blog ID.
+	if ( empty( $blog_id ) ) {
+		$blog_id = isset( $wpdb->blogid ) ? $wpdb->blogid : bp_get_root_blog_id();
+	}
+
+	// If no role was passed, try to find the blog role.
+	if ( empty( $role ) ) {
+
+		// Get user capabilities.
+		$key        = $wpdb->get_blog_prefix( $blog_id ). 'capabilities';
+		$user_roles = array_keys( (array) bp_get_user_meta( $user_id, $key, true ) );
+
+		// User has roles so lets.
+		if ( ! empty( $user_roles ) ) {
+
+			// Get blog roles.
+			$blog_roles      = array_keys( bp_get_current_blog_roles() );
+
+			// Look for blog only roles of the user.
+			$intersect_roles = array_intersect( $user_roles, $blog_roles );
+
+			// If there's a role in the array, use the first one. This isn't
+			// very smart, but since roles aren't exactly hierarchical, and
+			// WordPress does not yet have a UI for multiple user roles, it's
+			// fine for now.
+			if ( ! empty( $intersect_roles ) ) {
+				$role = array_shift( $intersect_roles );
+			}
+		}
+	}
+
+	// Bail if no role was found or role is not in the allowed roles array.
+	if ( empty( $role ) || ! in_array( $role, bp_blogs_get_allowed_roles() ) ) {
+		return false;
+	}
+
+	// Record the blog activity for this user being added to this blog.
+	bp_blogs_record_blog( $blog_id, $user_id, true );
+}
+add_action( 'add_user_to_blog', 'bp_blogs_add_user_to_blog', 10, 3 );
+add_action( 'profile_update',   'bp_blogs_add_user_to_blog'        );
+add_action( 'user_register',    'bp_blogs_add_user_to_blog'        );
+
+/**
+ * The allowed blog roles a member must have to be recorded into the
+ * `bp_user_blogs` pointer table.
+ *
+ * This added and was made filterable in BuddyPress 2.1.0 to make it easier
+ * to extend the functionality of the Blogs component.
+ *
+ * @since 2.1.0
+ *
+ * @return string
+ */
+function bp_blogs_get_allowed_roles() {
+
+	/**
+	 * Filters the allowed roles a member must have to be recorded into bp_user_blogs pointer table.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $value Array of potential roles user needs.
+	 */
+	return apply_filters( 'bp_blogs_get_allowed_roles', array( 'contributor', 'author', 'editor', 'administrator' ) );
+}
+
+/**
+ * Remove a blog-user pair from BP's blog tracker.
+ *
+ * @param int $user_id ID of the user whose blog is being removed.
+ * @param int $blog_id Optional. ID of the blog being removed. Default: current blog ID.
+ */
+function bp_blogs_remove_user_from_blog( $user_id, $blog_id = 0 ) {
+	global $wpdb;
+
+	if ( empty( $blog_id ) ) {
+		$blog_id = $wpdb->blogid;
+	}
+
+	bp_blogs_remove_blog_for_user( $user_id, $blog_id );
+}
+add_action( 'remove_user_from_blog', 'bp_blogs_remove_user_from_blog', 10, 2 );
+
+/**
+ * Rehook WP's maybe_add_existing_user_to_blog with a later priority.
+ *
+ * WordPress catches add-user-to-blog requests at init:10. In some cases, this
+ * can precede BP's Blogs component. This function bumps the priority of the
+ * core function, so that we can be sure that the Blogs component is loaded
+ * first. See https://buddypress.trac.wordpress.org/ticket/3916.
+ *
+ * @since 1.6.0
+ */
+function bp_blogs_maybe_add_user_to_blog() {
+	if ( ! is_multisite() )
+		return;
+
+	remove_action( 'init', 'maybe_add_existing_user_to_blog' );
+	add_action( 'init', 'maybe_add_existing_user_to_blog', 20 );
+}
+add_action( 'init', 'bp_blogs_maybe_add_user_to_blog', 1 );
+
+/**
+ * Remove the "blog created" item from the BP blogs tracker and activity stream.
+ *
+ * @param int $blog_id ID of the blog being removed.
+ */
+function bp_blogs_remove_blog( $blog_id ) {
+
+	$blog_id = (int) $blog_id;
+
+	/**
+	 * Fires before a "blog created" item is removed from blogs
+	 * tracker and activity stream.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $blog_id ID of the blog having its item removed.
+	 */
+	do_action( 'bp_blogs_before_remove_blog', $blog_id );
+
+	BP_Blogs_Blog::delete_blog_for_all( $blog_id );
+
+	/**
+	 * Fires after a "blog created" item has been removed from blogs
+	 * tracker and activity stream.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $blog_id ID of the blog who had its item removed.
+	 */
+	do_action( 'bp_blogs_remove_blog', $blog_id );
+}
+add_action( 'delete_blog', 'bp_blogs_remove_blog' );
+
+/**
+ * Remove a blog from the tracker for a specific user.
+ *
+ * @param int $user_id ID of the user for whom the blog is being removed.
+ * @param int $blog_id ID of the blog being removed.
+ */
+function bp_blogs_remove_blog_for_user( $user_id, $blog_id ) {
+
+	$blog_id = (int) $blog_id;
+	$user_id = (int) $user_id;
+
+	/**
+	 * Fires before a blog is removed from the tracker for a specific user.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $blog_id ID of the blog being removed.
+	 * @param int $user_id ID of the user having the blog removed for.
+	 */
+	do_action( 'bp_blogs_before_remove_blog_for_user', $blog_id, $user_id );
+
+	BP_Blogs_Blog::delete_blog_for_user( $blog_id, $user_id );
+
+	/**
+	 * Fires after a blog has been removed from the tracker for a specific user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $blog_id ID of the blog that was removed.
+	 * @param int $user_id ID of the user having the blog removed for.
+	 */
+	do_action( 'bp_blogs_remove_blog_for_user', $blog_id, $user_id );
+}
+add_action( 'remove_user_from_blog', 'bp_blogs_remove_blog_for_user', 10, 2 );
+
+/**
+ * Remove a synced activity comment from the activity stream.
+ *
+ * @since 2.5.0
+ *
+ * @param bool   $deleted              True when a comment post type activity was successfully removed.
+ * @param int    $comment_id           ID of the comment to be removed.
+ * @param object $activity_post_object The post type tracking args object.
+ * @param string $activity_type        The post type comment activity type.
+ *
+ * @return bool True on success. False on error.
+ */
+function bp_blogs_post_type_remove_comment( $deleted, $comment_id, $activity_post_object, $activity_type = '' ) {
+	// Remove synced activity comments, if needed.
+	if ( ! bp_disable_blogforum_comments() ) {
+		// Get associated activity ID from comment meta
+		$activity_id = get_comment_meta( $comment_id, 'bp_activity_comment_id', true );
+
+		/**
+		 * Delete the associated activity comment & also remove
+		 * child post comments and associated activity comments.
+		 */
+		if ( ! empty( $activity_id ) ) {
+			// fetch the activity comments for the activity item
+			$activity = bp_activity_get( array(
+				'in'               => $activity_id,
+				'display_comments' => 'stream',
+				'spam'             => 'all',
+			) );
+
+			// get all activity comment IDs for the pending deleted item
+			if ( ! empty( $activity['activities'] ) ) {
+				$activity_ids   = bp_activity_recurse_comments_activity_ids( $activity );
+				$activity_ids[] = $activity_id;
+
+				// delete activity items
+				foreach ( $activity_ids as $activity_id ) {
+					bp_activity_delete( array(
+						'id' => $activity_id
+					) );
+				}
+
+				// remove associated blog comments
+				bp_blogs_remove_associated_blog_comments( $activity_ids );
+
+				// rebuild activity comment tree
+				BP_Activity_Activity::rebuild_activity_comment_tree( $activity['activities'][0]->item_id );
+
+				// Set the result
+				$deleted = true;
+			}
+		}
+	}
+
+	// Backcompat for comments about the 'post' post type.
+	if ( 'new_blog_comment' === $activity_type ) {
+		/**
+		 * Fires after a blog comment activity item was removed from activity stream.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $value      ID for the blog associated with the removed comment.
+		 * @param int $comment_id ID of the comment being removed.
+		 * @param int $value      ID of the current logged in user.
+		 */
+		do_action( 'bp_blogs_remove_comment', get_current_blog_id(), $comment_id, bp_loggedin_user_id() );
+	}
+
+	return $deleted;
+}
+add_action( 'bp_activity_post_type_remove_comment', 'bp_blogs_post_type_remove_comment', 10, 4 );
+
+/**
+ * Removes blog comments that are associated with activity comments.
+ *
+ * @since 2.0.0
+ *
+ * @see bp_blogs_remove_synced_comment()
+ * @see bp_blogs_sync_delete_from_activity_comment()
+ *
+ * @param array $activity_ids The activity IDs to check association with blog
+ *                            comments.
+ * @param bool  $force_delete  Whether to force delete the comments. If false,
+ *                            comments are trashed instead.
+ */
+function bp_blogs_remove_associated_blog_comments( $activity_ids = array(), $force_delete = true ) {
+	// Query args.
+	$query_args = array(
+		'meta_query' => array(
+			array(
+				'key'     => 'bp_activity_comment_id',
+				'value'   => implode( ',', (array) $activity_ids ),
+				'compare' => 'IN',
+			)
+		)
+	);
+
+	// Get comment.
+	$comment_query = new WP_Comment_Query;
+	$comments = $comment_query->query( $query_args );
+
+	// Found the corresponding comments
+	// let's delete them!
+	foreach ( $comments as $comment ) {
+		wp_delete_comment( $comment->comment_ID, $force_delete );
+
+		// If we're trashing the comment, remove the meta key as well.
+		if ( empty( $force_delete ) ) {
+			delete_comment_meta( $comment->comment_ID, 'bp_activity_comment_id' );
+		}
+	}
+}
+
+/**
+ * Get the total number of blogs being tracked by BuddyPress.
+ *
+ * @return int $count Total blog count.
+ */
+function bp_blogs_total_blogs() {
+	$count = wp_cache_get( 'bp_total_blogs', 'bp' );
+
+	if ( false === $count ) {
+		$blogs = BP_Blogs_Blog::get_all();
+		$count = $blogs['total'];
+		wp_cache_set( 'bp_total_blogs', $count, 'bp' );
+	}
+	return $count;
+}
+
+/**
+ * Get the total number of blogs being tracked by BP for a specific user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id ID of the user being queried. Default: on a user page,
+ *                     the displayed user. Otherwise, the logged-in user.
+ * @return int $count Total blog count for the user.
+ */
+function bp_blogs_total_blogs_for_user( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+	}
+
+	// No user ID? do not attempt to look at cache.
+	if ( empty( $user_id ) ) {
+		return 0;
+	}
+
+	$count = wp_cache_get( 'bp_total_blogs_for_user_' . $user_id, 'bp' );
+	if ( false === $count ) {
+		$count = BP_Blogs_Blog::total_blog_count_for_user( $user_id );
+		wp_cache_set( 'bp_total_blogs_for_user_' . $user_id, $count, 'bp' );
+	}
+
+	return $count;
+}
+
+/**
+ * Remove the all data related to a given blog from the BP blogs tracker and activity stream.
+ *
+ * @param int $blog_id The ID of the blog to expunge.
+ */
+function bp_blogs_remove_data_for_blog( $blog_id ) {
+
+	/**
+	 * Fires before all data related to a given blog is removed from blogs tracker
+	 * and activity stream.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $blog_id ID of the blog whose data is being removed.
+	 */
+	do_action( 'bp_blogs_before_remove_data_for_blog', $blog_id );
+
+	// If this is regular blog, delete all data for that blog.
+	BP_Blogs_Blog::delete_blog_for_all( $blog_id );
+
+	/**
+	 * Fires after all data related to a given blog has been removed from blogs tracker
+	 * and activity stream.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $blog_id ID of the blog whose data is being removed.
+	 */
+	do_action( 'bp_blogs_remove_data_for_blog', $blog_id );
+}
+add_action( 'delete_blog', 'bp_blogs_remove_data_for_blog', 1 );
+
+/**
+ * Get all of a user's blogs, as tracked by BuddyPress.
+ *
+ * @see BP_Blogs_Blog::get_blogs_for_user() for a description of parameters
+ *      and return values.
+ *
+ * @param int  $user_id     See {@BP_Blogs_Blog::get_blogs_for_user()}.
+ * @param bool $show_hidden See {@BP_Blogs_Blog::get_blogs_for_user()}.
+ * @return array See {@BP_Blogs_Blog::get_blogs_for_user()}.
+ */
+function bp_blogs_get_blogs_for_user( $user_id, $show_hidden = false ) {
+	return BP_Blogs_Blog::get_blogs_for_user( $user_id, $show_hidden );
+}
+
+/**
+ * Retrieve a list of all blogs.
+ *
+ * @see BP_Blogs_Blog::get_all() for a description of parameters and return values.
+ *
+ * @param int|null $limit See {@BP_Blogs_Blog::get_all()}.
+ * @param int|null $page  See {@BP_Blogs_Blog::get_all()}.
+ * @return array See {@BP_Blogs_Blog::get_all()}.
+ */
+function bp_blogs_get_all_blogs( $limit = null, $page = null ) {
+	return BP_Blogs_Blog::get_all( $limit, $page );
+}
+
+/**
+ * Retrieve a random list of blogs.
+ *
+ * @see BP_Blogs_Blog::get() for a description of parameters and return values.
+ *
+ * @param int|null $limit See {@BP_Blogs_Blog::get()}.
+ * @param int|null $page  See {@BP_Blogs_Blog::get()}.
+ * @return array See {@BP_Blogs_Blog::get()}.
+ */
+function bp_blogs_get_random_blogs( $limit = null, $page = null ) {
+	return BP_Blogs_Blog::get( 'random', $limit, $page );
+}
+
+/**
+ * Check whether a given blog is hidden.
+ *
+ * @see BP_Blogs_Blog::is_hidden() for a description of parameters and return values.
+ *
+ * @param int $blog_id See {@BP_Blogs_Blog::is_hidden()}.
+ * @return bool See {@BP_Blogs_Blog::is_hidden()}.
+ */
+function bp_blogs_is_blog_hidden( $blog_id ) {
+	return BP_Blogs_Blog::is_hidden( $blog_id );
+}
+
+/*
+ * Blog meta functions
+ *
+ * These functions are used to store specific blogmeta in one global table,
+ * rather than in each blog's options table. Significantly speeds up global blog
+ * queries. By default each blog's name, description and last updated time are
+ * stored and synced here.
+ */
+
+/**
+ * Delete a metadata from the DB for a blog.
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int         $blog_id    ID of the blog whose metadata is being deleted.
+ * @param string|bool $meta_key   Optional. The key of the metadata being deleted. If
+ *                                omitted, all BP metadata associated with the blog will
+ *                                be deleted.
+ * @param string|bool $meta_value Optional. If present, the metadata will only be
+ *                                deleted if the meta_value matches this parameter.
+ * @param bool        $delete_all Optional. If true, delete matching metadata entries for
+ *                                all objects, ignoring the specified blog_id. Otherwise, only
+ *                                delete matching metadata entries for the specified blog.
+ *                                Default: false.
+ * @return bool True on success, false on failure.
+ */
+function bp_blogs_delete_blogmeta( $blog_id, $meta_key = false, $meta_value = false, $delete_all = false ) {
+	global $wpdb;
+
+	// Legacy - if no meta_key is passed, delete all for the blog_id.
+	if ( empty( $meta_key ) ) {
+		$table_name = buddypress()->blogs->table_name_blogmeta;
+		$sql        = "SELECT meta_key FROM {$table_name} WHERE blog_id = %d";
+		$query      = $wpdb->prepare( $sql, $blog_id );
+		$keys       = $wpdb->get_col( $query );
+
+		// With no meta_key, ignore $delete_all.
+		$delete_all = false;
+	} else {
+		$keys = array( $meta_key );
+	}
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+
+	$retval = false;
+	foreach ( $keys as $key ) {
+		$retval = delete_metadata( 'bp_blog', $blog_id, $key, $meta_value, $delete_all );
+	}
+
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+	remove_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Get metadata for a given blog.
+ *
+ * @since 1.2.0
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int    $blog_id  ID of the blog whose metadata is being requested.
+ * @param string $meta_key Optional. If present, only the metadata matching
+ *                         that meta key will be returned. Otherwise, all
+ *                         metadata for the blog will be fetched.
+ * @param bool   $single   Optional. If true, return only the first value of the
+ *                         specified meta_key. This parameter has no effect if
+ *                         meta_key is not specified. Default: true.
+ * @return mixed The meta value(s) being requested.
+ */
+function bp_blogs_get_blogmeta( $blog_id, $meta_key = '', $single = true ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+	$retval = get_metadata( 'bp_blog', $blog_id, $meta_key, $single );
+	remove_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Update a piece of blog meta.
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int    $blog_id    ID of the blog whose metadata is being updated.
+ * @param string $meta_key   Key of the metadata being updated.
+ * @param mixed  $meta_value Value to be set.
+ * @param mixed  $prev_value Optional. If specified, only update existing
+ *                           metadata entries with the specified value.
+ *                           Otherwise, update all entries.
+ * @return bool|int Returns false on failure. On successful update of existing
+ *                  metadata, returns true. On successful creation of new metadata,
+ *                  returns the integer ID of the new metadata row.
+ */
+function bp_blogs_update_blogmeta( $blog_id, $meta_key, $meta_value, $prev_value = '' ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+	$retval = update_metadata( 'bp_blog', $blog_id, $meta_key, $meta_value, $prev_value );
+	remove_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Add a piece of blog metadata.
+ *
+ * @since 2.0.0
+ *
+ * @param int    $blog_id    ID of the blog.
+ * @param string $meta_key   Metadata key.
+ * @param mixed  $meta_value Metadata value.
+ * @param bool   $unique     Optional. Whether to enforce a single metadata value
+ *                           for the given key. If true, and the object already has a value for
+ *                           the key, no change will be made. Default: false.
+ * @return int|bool The meta ID on successful update, false on failure.
+ */
+function bp_blogs_add_blogmeta( $blog_id, $meta_key, $meta_value, $unique = false ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+	$retval = add_metadata( 'bp_blog', $blog_id, $meta_key, $meta_value, $unique );
+	remove_filter( 'sanitize_key', 'bp_blogs_filter_meta_column_name' );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+/**
+ * Remove all blog associations for a given user.
+ *
+ * @param int $user_id ID whose blog data should be removed.
+ * @return bool Returns false on failure.
+ */
+function bp_blogs_remove_data( $user_id ) {
+	if ( !is_multisite() )
+		return false;
+
+	/**
+	 * Fires before all blog associations are removed for a given user.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $user_id ID of the user whose blog associations are being removed.
+	 */
+	do_action( 'bp_blogs_before_remove_data', $user_id );
+
+	// If this is regular blog, delete all data for that blog.
+	BP_Blogs_Blog::delete_blogs_for_user( $user_id );
+
+	/**
+	 * Fires after all blog associations are removed for a given user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id ID of the user whose blog associations were removed.
+	 */
+	do_action( 'bp_blogs_remove_data', $user_id );
+}
+add_action( 'wpmu_delete_user',  'bp_blogs_remove_data' );
+add_action( 'delete_user',       'bp_blogs_remove_data' );
+add_action( 'bp_make_spam_user', 'bp_blogs_remove_data' );
+
+/**
+ * Restore all blog associations for a given user.
+ *
+ * @since 2.2.0
+ *
+ * @param int $user_id ID whose blog data should be restored.
+ */
+function bp_blogs_restore_data( $user_id = 0 ) {
+	if ( ! is_multisite() ) {
+		return;
+	}
+
+	// Get the user's blogs.
+	$user_blogs = get_blogs_of_user( $user_id );
+	if ( empty( $user_blogs ) ) {
+		return;
+	}
+
+	$blogs = array_keys( $user_blogs );
+
+	foreach ( $blogs as $blog_id ) {
+		bp_blogs_add_user_to_blog( $user_id, false, $blog_id );
+	}
+}
+add_action( 'bp_make_ham_user', 'bp_blogs_restore_data', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-loader.php	(revision 12265)
@@ -0,0 +1,25 @@
+<?php
+/**
+ * BuddyPress Blogs Loader
+ *
+ * The blogs component tracks posts and comments to member activity streams,
+ * shows blogs the member can post to in their profiles, and caches useful
+ * information from those blogs to make querying blogs in bulk more performant.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsCore
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-blogs component.
+ *
+ * @since 1.5.0
+ */
+function bp_setup_blogs() {
+	buddypress()->blogs = new BP_Blogs_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_blogs', 6 );
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-template.php	(revision 12265)
@@ -0,0 +1,1544 @@
+<?php
+/**
+ * BuddyPress Blogs Template Tags.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the blogs component slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_blogs_slug() {
+	echo bp_get_blogs_slug();
+}
+	/**
+	 * Return the blogs component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string The 'blogs' slug.
+	 */
+	function bp_get_blogs_slug() {
+
+		/**
+		 * Filters the blogs component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Slug for the blogs component.
+		 */
+		return apply_filters( 'bp_get_blogs_slug', buddypress()->blogs->slug );
+	}
+
+/**
+ * Output the blogs component root slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_blogs_root_slug() {
+	echo bp_get_blogs_root_slug();
+}
+	/**
+	 * Return the blogs component root slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string The 'blogs' root slug.
+	 */
+	function bp_get_blogs_root_slug() {
+
+		/**
+		 * Filters the blogs component root slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $root_slug Root slug for the blogs component.
+		 */
+		return apply_filters( 'bp_get_blogs_root_slug', buddypress()->blogs->root_slug );
+	}
+
+/**
+ * Output blog directory permalink.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_blogs_directory_permalink() {
+	echo esc_url( bp_get_blogs_directory_permalink() );
+}
+	/**
+	 * Return blog directory permalink.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @return string The URL of the Blogs directory.
+	 */
+	function bp_get_blogs_directory_permalink() {
+
+		/**
+		 * Filters the blog directory permalink.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Permalink URL for the blog directory.
+		 */
+		return apply_filters( 'bp_get_blogs_directory_permalink', trailingslashit( bp_get_root_domain() . '/' . bp_get_blogs_root_slug() ) );
+	}
+
+/**
+ * Rewind the blogs and reset blog index.
+ */
+function bp_rewind_blogs() {
+	global $blogs_template;
+
+	$blogs_template->rewind_blogs();
+}
+
+/**
+ * Initialize the blogs loop.
+ *
+ * Based on the $args passed, bp_has_blogs() populates the $blogs_template
+ * global, enabling the use of BuddyPress templates and template functions to
+ * display a list of activity items.
+ *
+ * @global object $blogs_template {@link BP_Blogs_Template}
+ *
+ * @param array|string $args {
+ *     Arguments for limiting the contents of the blogs loop. Most arguments
+ *     are in the same format as {@link BP_Blogs_Blog::get()}. However, because
+ *     the format of the arguments accepted here differs in a number of ways,
+ *     and because bp_has_blogs() determines some default arguments in a
+ *     dynamic fashion, we list all accepted arguments here as well.
+ *
+ *     Arguments can be passed as an associative array, or as a URL query
+ *     string (eg, 'user_id=4&per_page=3').
+ *
+ *     @type int      $page             Which page of results to fetch. Using page=1 without
+ *                                      per_page will result in no pagination. Default: 1.
+ *     @type int|bool $per_page         Number of results per page. Default: 20.
+ *     @type string   $page_arg         The string used as a query parameter in
+ *                                      pagination links. Default: 'bpage'.
+ *     @type int|bool $max              Maximum number of results to return.
+ *                                      Default: false (unlimited).
+ *     @type string   $type             The order in which results should be fetched.
+ *                                      'active', 'alphabetical', 'newest', or 'random'.
+ *     @type array    $include_blog_ids Array of blog IDs to limit results to.
+ *     @type string   $sort             'ASC' or 'DESC'. Default: 'DESC'.
+ *     @type string   $search_terms     Limit results by a search term. Default: the value of `$_REQUEST['s']` or
+ *                                      `$_REQUEST['sites_search']`, if present.
+ *     @type int      $user_id          The ID of the user whose blogs should be retrieved.
+ *                                      When viewing a user profile page, 'user_id' defaults to the
+ *                                      ID of the displayed user. Otherwise the default is false.
+ * }
+ * @return bool Returns true when blogs are found, otherwise false.
+ */
+function bp_has_blogs( $args = '' ) {
+	global $blogs_template;
+
+	// Check for and use search terms.
+	$search_terms_default = false;
+	$search_query_arg = bp_core_get_component_search_query_arg( 'blogs' );
+	if ( ! empty( $_REQUEST[ $search_query_arg ] ) ) {
+		$search_terms_default = stripslashes( $_REQUEST[ $search_query_arg ] );
+	} elseif ( ! empty( $_REQUEST['s'] ) ) {
+		$search_terms_default = stripslashes( $_REQUEST['s'] );
+	}
+
+	// Parse arguments.
+	$r = bp_parse_args( $args, array(
+		'type'              => 'active',
+		'page_arg'          => 'bpage', // See https://buddypress.trac.wordpress.org/ticket/3679.
+		'page'              => 1,
+		'per_page'          => 20,
+		'max'               => false,
+		'user_id'           => bp_displayed_user_id(), // Pass a user_id to limit to only blogs this user is a member of.
+		'include_blog_ids'  => false,
+		'search_terms'      => $search_terms_default,
+		'update_meta_cache' => true
+	), 'has_blogs' );
+
+	// Set per_page to maximum if max is enforced.
+	if ( ! empty( $r['max'] ) && ( (int) $r['per_page'] > (int) $r['max'] ) ) {
+		$r['per_page'] = (int) $r['max'];
+	}
+
+	// Get the blogs.
+	$blogs_template = new BP_Blogs_Template( $r['type'], $r['page'], $r['per_page'], $r['max'], $r['user_id'], $r['search_terms'], $r['page_arg'], $r['update_meta_cache'], $r['include_blog_ids'] );
+
+	/**
+	 * Filters whether or not there are blogs to list.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool              $value          Whether or not there are blogs to list.
+	 * @param BP_Blogs_Template $blogs_template Current blogs template object.
+	 * @param array             $r              Parsed arguments used in blogs template query.
+	 */
+	return apply_filters( 'bp_has_blogs', $blogs_template->has_blogs(), $blogs_template, $r );
+}
+
+/**
+ * Determine if there are still blogs left in the loop.
+ *
+ * @global object $blogs_template {@link BP_Blogs_Template}
+ *
+ * @return bool Returns true when blogs are found.
+ */
+function bp_blogs() {
+	global $blogs_template;
+
+	return $blogs_template->blogs();
+}
+
+/**
+ * Get the current blog object in the loop.
+ *
+ * @global object $blogs_template {@link BP_Blogs_Template}
+ *
+ * @return object The current blog within the loop.
+ */
+function bp_the_blog() {
+	global $blogs_template;
+
+	return $blogs_template->the_blog();
+}
+
+/**
+ * Output the blogs pagination count.
+ *
+ * @since 1.0.0
+ */
+function bp_blogs_pagination_count() {
+	echo bp_get_blogs_pagination_count();
+}
+
+/**
+ * Get the blogs pagination count.
+ *
+ * @since 2.7.0
+ *
+ * @global object $blogs_template {@link BP_Blogs_Template}
+ *
+ * @return string
+ */
+function bp_get_blogs_pagination_count() {
+	global $blogs_template;
+
+	$start_num = intval( ( $blogs_template->pag_page - 1 ) * $blogs_template->pag_num ) + 1;
+	$from_num  = bp_core_number_format( $start_num );
+	$to_num    = bp_core_number_format( ( $start_num + ( $blogs_template->pag_num - 1 ) > $blogs_template->total_blog_count ) ? $blogs_template->total_blog_count : $start_num + ( $blogs_template->pag_num - 1 ) );
+	$total     = bp_core_number_format( $blogs_template->total_blog_count );
+
+	if ( 1 == $blogs_template->total_blog_count ) {
+		$message = __( 'Viewing 1 site', 'buddypress' );
+	} else {
+		$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s site', 'Viewing %1$s - %2$s of %3$s sites', $blogs_template->total_blog_count, 'buddypress' ), $from_num, $to_num, $total );
+	}
+
+	/**
+	 * Filters the "Viewing x-y of z blogs" pagination message.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $message  "Viewing x-y of z blogs" text.
+	 * @param string $from_num Total amount for the low value in the range.
+	 * @param string $to_num   Total amount for the high value in the range.
+	 * @param string $total    Total amount of blogs found.
+	 */
+	return apply_filters( 'bp_get_blogs_pagination_count', $message, $from_num, $to_num, $total );
+}
+
+/**
+ * Output the blogs pagination links.
+ */
+function bp_blogs_pagination_links() {
+	echo bp_get_blogs_pagination_links();
+}
+	/**
+	 * Return the blogs pagination links.
+	 *
+	 * @global object $blogs_template {@link BP_Blogs_Template}
+	 *
+	 * @return string HTML pagination links.
+	 */
+	function bp_get_blogs_pagination_links() {
+		global $blogs_template;
+
+		/**
+		 * Filters the blogs pagination links.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $pag_links HTML pagination links.
+		 */
+		return apply_filters( 'bp_get_blogs_pagination_links', $blogs_template->pag_links );
+	}
+
+/**
+ * Output a blog's avatar.
+ *
+ * @see bp_get_blog_avatar() for description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_blog_avatar()}.
+ */
+function bp_blog_avatar( $args = '' ) {
+	echo bp_get_blog_avatar( $args );
+}
+	/**
+	 * Get a blog's avatar.
+	 *
+	 * At the moment, blog avatars are simply the user avatars of the blog
+	 * admin. Filter 'bp_get_blog_avatar_' . $blog_id to customize.
+	 *
+	 * @since 2.4.0 Introduced `$title` argument.
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and
+	 *      return values.
+	 *
+	 * @param array|string $args  {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see
+	 *     {@link bp_core_fetch_avatar()}.
+	 *     @type string   $alt     Default: 'Profile picture of site author [user name]'.
+	 *     @type string   $class   Default: 'avatar'.
+	 *     @type string   $type    Default: 'full'.
+	 *     @type int|bool $width   Default: false.
+	 *     @type int|bool $height  Default: false.
+	 *     @type bool     $id      Currently unused.
+	 *     @type bool     $no_grav Default: true.
+	 * }
+	 * @return string User avatar string.
+	 */
+	function bp_get_blog_avatar( $args = '' ) {
+		global $blogs_template;
+
+		// Bail if avatars are turned off
+		// @todo Should we maybe still filter this?
+		if ( ! buddypress()->avatar->show_avatars ) {
+			return false;
+		}
+
+		$author_displayname = bp_core_get_user_displayname( $blogs_template->blog->admin_user_id );
+
+		// Parse the arguments.
+		$r = bp_parse_args( $args, array(
+			'type'    => 'full',
+			'width'   => false,
+			'height'  => false,
+			'class'   => 'avatar',
+			'id'      => false,
+			'alt'     => sprintf( __( 'Profile picture of site author %s', 'buddypress' ), esc_attr( $author_displayname ) ),
+			'no_grav' => true,
+		) );
+
+		// Use site icon if available.
+		$avatar = '';
+		if ( bp_is_active( 'blogs', 'site-icon' ) && function_exists( 'has_site_icon' ) ) {
+			$site_icon = bp_blogs_get_blogmeta( bp_get_blog_id(), "site_icon_url_{$r['type']}" );
+
+			// Never attempted to fetch site icon before; do it now!
+			if ( '' === $site_icon ) {
+				switch_to_blog( bp_get_blog_id() );
+
+				// Fetch the other size first.
+				if ( 'full' === $r['type'] ) {
+					$size      = bp_core_avatar_thumb_width();
+					$save_size = 'thumb';
+				} else {
+					$size      = bp_core_avatar_full_width();
+					$save_size = 'full';
+				}
+
+				$site_icon = get_site_icon_url( $size );
+				// Empty site icons get saved as integer 0.
+				if ( empty( $site_icon ) ) {
+					$site_icon = 0;
+				}
+
+				// Sync site icon for other size to blogmeta.
+				bp_blogs_update_blogmeta( bp_get_blog_id(), "site_icon_url_{$save_size}", $site_icon );
+
+				// Now, fetch the size we want.
+				if ( 0 !== $site_icon ) {
+					$size      = 'full' === $r['type'] ? bp_core_avatar_full_width() : bp_core_avatar_thumb_width();
+					$site_icon = get_site_icon_url( $size );
+				}
+
+				// Sync site icon to blogmeta.
+				bp_blogs_update_blogmeta( bp_get_blog_id(), "site_icon_url_{$r['type']}", $site_icon );
+
+				restore_current_blog();
+			}
+
+			// We have a site icon.
+			if ( ! is_numeric( $site_icon ) ) {
+				if ( empty( $r['width'] ) && ! isset( $size ) ) {
+					$size = 'full' === $r['type'] ? bp_core_avatar_full_width() : bp_core_avatar_thumb_width();
+				} else {
+					$size = (int) $r['width'];
+				}
+
+				$avatar = sprintf( '<img src="%1$s" class="%2$s" width="%3$s" height="%3$s" alt="%4$s" />',
+					esc_url( $site_icon ),
+					esc_attr( "{$r['class']} avatar-{$size}" ),
+					esc_attr( $size ),
+					sprintf( esc_attr__( 'Site icon for %s', 'buddypress' ), bp_get_blog_name() )
+				);
+			}
+		}
+
+		// Fallback to user ID avatar.
+		if ( '' === $avatar ) {
+			$avatar = bp_core_fetch_avatar( array(
+				'item_id'    => $blogs_template->blog->admin_user_id,
+				// 'avatar_dir' => 'blog-avatars',
+				// 'object'     => 'blog',
+				'type'       => $r['type'],
+				'alt'        => $r['alt'],
+				'css_id'     => $r['id'],
+				'class'      => $r['class'],
+				'width'      => $r['width'],
+				'height'     => $r['height']
+			) );
+		}
+
+		/**
+		 * In future BuddyPress versions you will be able to set the avatar for a blog.
+		 * Right now you can use a filter with the ID of the blog to change it if you wish.
+		 * By default it will return the avatar for the primary blog admin.
+		 *
+		 * This filter is deprecated as of BuddyPress 1.5 and may be removed in a future version.
+		 * Use the 'bp_get_blog_avatar' filter instead.
+		 */
+		$avatar = apply_filters( 'bp_get_blog_avatar_' . $blogs_template->blog->blog_id, $avatar );
+
+		/**
+		 * Filters a blog's avatar.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $avatar  Formatted HTML <img> element, or raw avatar
+		 *                        URL based on $html arg.
+		 * @param int    $blog_id ID of the blog whose avatar is being displayed.
+		 * @param array  $r       Array of arguments used when fetching avatar.
+		 */
+		return apply_filters( 'bp_get_blog_avatar', $avatar, $blogs_template->blog->blog_id, $r );
+	}
+
+function bp_blog_permalink() {
+	echo bp_get_blog_permalink();
+}
+	function bp_get_blog_permalink() {
+		global $blogs_template;
+
+		if ( empty( $blogs_template->blog->domain ) )
+			$permalink = bp_get_root_domain() . $blogs_template->blog->path;
+		else {
+			$protocol = 'http://';
+			if ( is_ssl() )
+				$protocol = 'https://';
+
+			$permalink = $protocol . $blogs_template->blog->domain . $blogs_template->blog->path;
+		}
+
+		/**
+		 * Filters the blog permalink.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $permalink Permalink URL for the blog.
+		 */
+		return apply_filters( 'bp_get_blog_permalink', $permalink );
+	}
+
+/**
+ * Output the name of the current blog in the loop.
+ */
+function bp_blog_name() {
+	echo bp_get_blog_name();
+}
+	/**
+	 * Return the name of the current blog in the loop.
+	 *
+	 * @return string The name of the current blog in the loop.
+	 */
+	function bp_get_blog_name() {
+		global $blogs_template;
+
+		/**
+		 * Filters the name of the current blog in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $name Name of the current blog in the loop.
+		 */
+		return apply_filters( 'bp_get_blog_name', $blogs_template->blog->name );
+	}
+
+/**
+ * Output the ID of the current blog in the loop.
+ *
+ * @since 1.7.0
+ */
+function bp_blog_id() {
+	echo bp_get_blog_id();
+}
+	/**
+	 * Return the ID of the current blog in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @return int ID of the current blog in the loop.
+	 */
+	function bp_get_blog_id() {
+		global $blogs_template;
+
+		/**
+		 * Filters the ID of the current blog in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param int $blog_id ID of the current blog in the loop.
+		 */
+		return apply_filters( 'bp_get_blog_id', $blogs_template->blog->blog_id );
+	}
+
+/**
+ * Output the description of the current blog in the loop.
+ */
+function bp_blog_description() {
+
+	/**
+	 * Filters the description of the current blog in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $value Description of the current blog in the loop.
+	 */
+	echo apply_filters( 'bp_blog_description', bp_get_blog_description() );
+}
+	/**
+	 * Return the description of the current blog in the loop.
+	 *
+	 * @return string Description of the current blog in the loop.
+	 */
+	function bp_get_blog_description() {
+		global $blogs_template;
+
+		/**
+		 * Filters the description of the current blog in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Description of the current blog in the loop.
+		 */
+		return apply_filters( 'bp_get_blog_description', $blogs_template->blog->description );
+	}
+
+/**
+ * Output the row class of the current blog in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param array $classes Array of custom classes.
+ */
+function bp_blog_class( $classes = array() ) {
+	echo bp_get_blog_class( $classes );
+}
+	/**
+	 * Return the row class of the current blog in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global BP_Blogs_Template $blogs_template
+	 *
+	 * @param array $classes Array of custom classes.
+	 * @return string Row class of the site.
+	 */
+	function bp_get_blog_class( $classes = array() ) {
+		global $blogs_template;
+
+		// Add even/odd classes, but only if there's more than 1 group.
+		if ( $blogs_template->blog_count > 1 ) {
+			$pos_in_loop = (int) $blogs_template->current_blog;
+			$classes[]   = ( $pos_in_loop % 2 ) ? 'even' : 'odd';
+
+		// If we've only one site in the loop, don't bother with odd and even.
+		} else {
+			$classes[] = 'bp-single-blog';
+		}
+
+		/**
+		 * Filters the row class of the current blog in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array $classes Array of classes to be applied to row.
+		 */
+		$classes = apply_filters( 'bp_get_blog_class', $classes );
+		$classes = array_merge( $classes, array() );
+		$retval  = 'class="' . join( ' ', $classes ) . '"';
+
+		return $retval;
+	}
+
+/**
+ * Output the last active date of the current blog in the loop.
+ *
+ * @param array $args See {@link bp_get_blog_last_active()}.
+ */
+function bp_blog_last_active( $args = array() ) {
+	echo bp_get_blog_last_active( $args );
+}
+	/**
+	 * Return the last active date of the current blog in the loop.
+	 *
+	 * @param array $args {
+	 *     Array of optional arguments.
+	 *     @type bool $active_format If true, formatted "Active 5 minutes ago".
+	 *                               If false, formatted "5 minutes ago".
+	 *                               Default: true.
+	 * }
+	 * @return string Last active date.
+	 */
+	function bp_get_blog_last_active( $args = array() ) {
+		global $blogs_template;
+
+		// Parse the activity format.
+		$r = bp_parse_args( $args, array(
+			'active_format' => true
+		) );
+
+		// Backwards compatibility for anyone forcing a 'true' active_format.
+		if ( true === $r['active_format'] ) {
+			$r['active_format'] = __( 'active %s', 'buddypress' );
+		}
+
+		// Blog has been posted to at least once.
+		if ( isset( $blogs_template->blog->last_activity ) ) {
+
+			// Backwards compatibility for pre 1.5 'ago' strings.
+			$last_activity = ! empty( $r['active_format'] )
+				? bp_core_get_last_activity( $blogs_template->blog->last_activity, $r['active_format'] )
+				: bp_core_time_since( $blogs_template->blog->last_activity );
+
+		// Blog has never been posted to.
+		} else {
+			$last_activity = __( 'Never active', 'buddypress' );
+		}
+
+		/**
+		 * Filters the last active date of the current blog in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $last_activity Last active date.
+		 * @param array  $r             Array of parsed args used to determine formatting.
+		 */
+		return apply_filters( 'bp_blog_last_active', $last_activity, $r );
+	}
+
+/**
+ * Output the latest post from the current blog in the loop.
+ *
+ * @param array $args See {@link bp_get_blog_latest_post()}.
+ */
+function bp_blog_latest_post( $args = array() ) {
+	echo bp_get_blog_latest_post( $args );
+}
+	/**
+	 * Return the latest post from the current blog in the loop.
+	 *
+	 * @param array $args {
+	 *     Array of optional arguments.
+	 *     @type bool $latest_format If true, formatted "Latest post: [link to post]".
+	 *                               If false, formatted "[link to post]".
+	 *                               Default: true.
+	 * }
+	 * @return string $retval String of the form 'Latest Post: [link to post]'.
+	 */
+	function bp_get_blog_latest_post( $args = array() ) {
+		global $blogs_template;
+
+		$r = wp_parse_args( $args, array(
+			'latest_format' => true,
+		) );
+
+		$retval = bp_get_blog_latest_post_title();
+
+		if ( ! empty( $retval ) ) {
+			if ( ! empty( $r['latest_format'] ) ) {
+
+				/**
+				 * Filters the title text of the latest post for the current blog in loop.
+				 *
+				 * @since 1.0.0
+				 *
+				 * @param string $retval Title of the latest post.
+				 */
+				$retval = sprintf( __( 'Latest Post: %s', 'buddypress' ), '<a href="' . $blogs_template->blog->latest_post->guid . '">' . apply_filters( 'the_title', $retval ) . '</a>' );
+			} else {
+
+				/** This filter is documented in bp-blogs/bp-blogs-template.php */
+				$retval = '<a href="' . $blogs_template->blog->latest_post->guid . '">' . apply_filters( 'the_title', $retval ) . '</a>';
+			}
+		}
+
+		/**
+		 * Filters the HTML markup result for the latest blog post in loop.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $retval HTML markup for the latest post.
+		 * @param array  $r      Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_blog_latest_post', $retval, $r );
+	}
+
+/**
+ * Output the title of the latest post on the current blog in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_get_blog_latest_post_title()
+ */
+function bp_blog_latest_post_title() {
+	echo bp_get_blog_latest_post_title();
+}
+	/**
+	 * Return the title of the latest post on the current blog in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global BP_Blogs_Template
+	 *
+	 * @return string Post title.
+	 */
+	function bp_get_blog_latest_post_title() {
+		global $blogs_template;
+
+		$retval = '';
+
+		if ( ! empty( $blogs_template->blog->latest_post ) && ! empty( $blogs_template->blog->latest_post->post_title ) )
+			$retval = $blogs_template->blog->latest_post->post_title;
+
+		/**
+		 * Filters the title text of the latest post on the current blog in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $retval Title text for the latest post.
+		 */
+		return apply_filters( 'bp_get_blog_latest_post_title', $retval );
+	}
+
+/**
+ * Output the permalink of the latest post on the current blog in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_get_blog_latest_post_title()
+ */
+function bp_blog_latest_post_permalink() {
+	echo esc_url( bp_get_blog_latest_post_permalink() );
+}
+	/**
+	 * Return the permalink of the latest post on the current blog in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global BP_Blogs_Template
+	 *
+	 * @return string URL of the blog's latest post.
+	 */
+	function bp_get_blog_latest_post_permalink() {
+		global $blogs_template;
+
+		$retval = '';
+
+		if ( ! empty( $blogs_template->blog->latest_post ) && ! empty( $blogs_template->blog->latest_post->ID ) )
+			$retval = add_query_arg( 'p', $blogs_template->blog->latest_post->ID, bp_get_blog_permalink() );
+
+		/**
+		 * Filters the permalink of the latest post on the current blog in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $retval Permalink URL of the latest post.
+		 */
+		return apply_filters( 'bp_get_blog_latest_post_permalink', $retval );
+	}
+
+/**
+ * Output the content of the latest post on the current blog in the loop.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_blog_latest_post_content() {
+	echo bp_get_blog_latest_post_content();
+}
+	/**
+	 * Return the content of the latest post on the current blog in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global BP_Blogs_Template
+	 *
+	 * @return string Content of the blog's latest post.
+	 */
+	function bp_get_blog_latest_post_content() {
+		global $blogs_template;
+
+		$retval = '';
+
+		if ( ! empty( $blogs_template->blog->latest_post ) && ! empty( $blogs_template->blog->latest_post->post_content ) )
+			$retval = $blogs_template->blog->latest_post->post_content;
+
+		/**
+		 * Filters the content of the latest post on the current blog in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $retval Content of the latest post on the current blog in the loop.
+		 */
+		return apply_filters( 'bp_get_blog_latest_post_content', $retval );
+	}
+
+/**
+ * Output the featured image of the latest post on the current blog in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_get_blog_latest_post_content() For description of parameters.
+ *
+ * @param string $size See {@link bp_get_blog_latest_post_featured_image()}.
+ */
+function bp_blog_latest_post_featured_image( $size = 'thumbnail' ) {
+	echo bp_get_blog_latest_post_featured_image( $size );
+}
+	/**
+	 * Return the featured image of the latest post on the current blog in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global BP_Blogs_Template
+	 *
+	 * @param string $size Image version to return. 'thumbnail', 'medium',
+	 *                     'large', or 'post-thumbnail'. Default: 'thumbnail'.
+	 * @return string URL of the image.
+	 */
+	function bp_get_blog_latest_post_featured_image( $size = 'thumbnail' ) {
+		global $blogs_template;
+
+		$retval = '';
+
+		if ( ! empty( $blogs_template->blog->latest_post ) && ! empty( $blogs_template->blog->latest_post->images[$size] ) )
+			$retval = $blogs_template->blog->latest_post->images[$size];
+
+		/**
+		 * Filters the featured image of the latest post on the current blog in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $retval The featured image of the latest post on the current blog in the loop.
+		 */
+		return apply_filters( 'bp_get_blog_latest_post_featured_image', $retval );
+	}
+
+/**
+ * Does the latest blog post have a featured image?
+ *
+ * @since 1.7.0
+ *
+ * @param string $thumbnail Image version to return. 'thumbnail', 'medium', 'large',
+ *                          or 'post-thumbnail'. Default: 'thumbnail'.
+ * @return bool True if the latest blog post from the current blog has a
+ *              featured image of the given size.
+ */
+function bp_blog_latest_post_has_featured_image( $thumbnail = 'thumbnail' ) {
+	$image  = bp_get_blog_latest_post_featured_image( $thumbnail );
+
+	/**
+	 * Filters whether or not the latest blog post has a featured image.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool   $value     Whether or not the latest blog post has a featured image.
+	 * @param string $thumbnail Image version to return.
+	 * @param string $image     Returned value from bp_get_blog_latest_post_featured_image.
+	 */
+	return apply_filters( 'bp_blog_latest_post_has_featured_image', ! empty( $image ), $thumbnail, $image );
+}
+
+/**
+ * Output hidden fields to help with form submissions in Sites directory.
+ *
+ * This function detects whether 's', 'letter', or 'blogs_search' requests are
+ * currently being made (as in a URL parameter), and creates corresponding
+ * hidden fields.
+ */
+function bp_blog_hidden_fields() {
+	if ( isset( $_REQUEST['s'] ) )
+		echo '<input type="hidden" id="search_terms" value="' . esc_attr( $_REQUEST['s'] ). '" name="search_terms" />';
+
+	if ( isset( $_REQUEST['letter'] ) )
+		echo '<input type="hidden" id="selected_letter" value="' . esc_attr( $_REQUEST['letter'] ) . '" name="selected_letter" />';
+
+	if ( isset( $_REQUEST['blogs_search'] ) )
+		echo '<input type="hidden" id="search_terms" value="' . esc_attr( $_REQUEST['blogs_search'] ) . '" name="search_terms" />';
+}
+
+/**
+ * Output the total number of blogs on the site.
+ */
+function bp_total_blog_count() {
+	echo bp_get_total_blog_count();
+}
+	/**
+	 * Return the total number of blogs on the site.
+	 *
+	 * @return int Total number of blogs.
+	 */
+	function bp_get_total_blog_count() {
+
+		/**
+		 * Filters the total number of blogs on the site.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $value Total number of blogs on the site.
+		 */
+		return apply_filters( 'bp_get_total_blog_count', bp_blogs_total_blogs() );
+	}
+	add_filter( 'bp_get_total_blog_count', 'bp_core_number_format' );
+
+/**
+ * Output the total number of blogs for a given user.
+ *
+ * @param int $user_id ID of the user.
+ */
+function bp_total_blog_count_for_user( $user_id = 0 ) {
+	echo bp_get_total_blog_count_for_user( $user_id );
+}
+	/**
+	 * Return the total number of blogs for a given user.
+	 *
+	 * @param int $user_id ID of the user.
+	 * @return int Total number of blogs for the user.
+	 */
+	function bp_get_total_blog_count_for_user( $user_id = 0 ) {
+
+		/**
+		 * Filters the total number of blogs for a given user.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param int $value   Total number of blogs for a given user.
+		 * @param int $user_id ID of the queried user.
+		 */
+		return apply_filters( 'bp_get_total_blog_count_for_user', bp_blogs_total_blogs_for_user( $user_id ), $user_id );
+	}
+	add_filter( 'bp_get_total_blog_count_for_user', 'bp_core_number_format' );
+
+
+/** Blog Registration ********************************************************/
+
+/**
+ * Checks whether blog creation is enabled.
+ *
+ * Returns true when blog creation is enabled for logged-in users only, or
+ * when it's enabled for new registrations.
+ *
+ * @return bool True if blog registration is enabled.
+ */
+function bp_blog_signup_enabled() {
+	$bp = buddypress();
+
+	$active_signup = isset( $bp->site_options['registration'] )
+		? $bp->site_options['registration']
+		: 'all';
+
+	/**
+	 * Filters whether or not blog creation is enabled.
+	 *
+	 * Return "all", "none", "blog" or "user".
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $active_signup Value of the registration site option creation status.
+	 */
+	$active_signup = apply_filters( 'wpmu_active_signup', $active_signup );
+
+	if ( 'none' == $active_signup || 'user' == $active_signup )
+		return false;
+
+	return true;
+}
+
+/**
+ * Output the wrapper markup for the blog signup form.
+ *
+ * @param string          $blogname   Optional. The default blog name (path or domain).
+ * @param string          $blog_title Optional. The default blog title.
+ * @param string|WP_Error $errors     Optional. The WP_Error object returned by a previous
+ *                                    submission attempt.
+ */
+function bp_show_blog_signup_form($blogname = '', $blog_title = '', $errors = '') {
+	global $current_user;
+
+	if ( isset($_POST['submit']) ) {
+		bp_blogs_validate_blog_signup();
+	} else {
+		if ( ! is_wp_error($errors) ) {
+			$errors = new WP_Error();
+		}
+
+		/**
+		 * Filters the default values for Blog name, title, and any current errors.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param array $value {
+		 *      string   $blogname   Default blog name provided.
+		 *      string   $blog_title Default blog title provided.
+		 *      WP_Error $errors     WP_Error object.
+		 * }
+		 */
+		$filtered_results = apply_filters('signup_another_blog_init', array('blogname' => $blogname, 'blog_title' => $blog_title, 'errors' => $errors ));
+		$blogname = $filtered_results['blogname'];
+		$blog_title = $filtered_results['blog_title'];
+		$errors = $filtered_results['errors'];
+
+		if ( $errors->get_error_code() ) {
+			echo "<p>" . __('There was a problem; please correct the form below and try again.', 'buddypress') . "</p>";
+		}
+		?>
+		<p><?php printf(__("By filling out the form below, you can <strong>add a site to your account</strong>. There is no limit to the number of sites that you can have, so create to your heart's content, but blog responsibly!", 'buddypress'), $current_user->display_name) ?></p>
+
+		<p><?php _e("If you&#8217;re not going to use a great domain, leave it for a new user. Now have at it!", 'buddypress') ?></p>
+
+		<form class="standard-form" id="setupform" method="post" action="">
+
+			<input type="hidden" name="stage" value="gimmeanotherblog" />
+			<?php
+
+			/**
+			 * Fires after the default hidden fields in blog signup form markup.
+			 *
+			 * @since 1.0.0
+			 */
+			do_action( 'signup_hidden_fields' ); ?>
+
+			<?php bp_blogs_signup_blog($blogname, $blog_title, $errors); ?>
+			<p>
+				<input id="submit" type="submit" name="submit" class="submit" value="<?php esc_attr_e('Create Site', 'buddypress') ?>" />
+			</p>
+
+			<?php wp_nonce_field( 'bp_blog_signup_form' ) ?>
+		</form>
+		<?php
+	}
+}
+
+/**
+ * Output the input fields for the blog creation form.
+ *
+ * @param string          $blogname   Optional. The default blog name (path or domain).
+ * @param string          $blog_title Optional. The default blog title.
+ * @param string|WP_Error $errors     Optional. The WP_Error object returned by a previous
+ *                                    submission attempt.
+ */
+function bp_blogs_signup_blog( $blogname = '', $blog_title = '', $errors = '' ) {
+	global $current_site;
+
+	// Blog name.
+	if( !is_subdomain_install() )
+		echo '<label for="blogname">' . __('Site Name:', 'buddypress') . '</label>';
+	else
+		echo '<label for="blogname">' . __('Site Domain:', 'buddypress') . '</label>';
+
+	if ( $errmsg = $errors->get_error_message('blogname') ) { ?>
+
+		<p class="error"><?php echo $errmsg ?></p>
+
+	<?php }
+
+	if ( !is_subdomain_install() )
+		echo '<span class="prefix_address">' . $current_site->domain . $current_site->path . '</span> <input name="blogname" type="text" id="blogname" value="'.$blogname.'" maxlength="63" /><br />';
+	else
+		echo '<input name="blogname" type="text" id="blogname" value="'.$blogname.'" maxlength="63" ' . bp_get_form_field_attributes( 'blogname' ) . '/> <span class="suffix_address">.' . bp_signup_get_subdomain_base() . '</span><br />';
+
+	if ( !is_user_logged_in() ) {
+		print '(<strong>' . __( 'Your address will be ' , 'buddypress');
+
+		if ( !is_subdomain_install() ) {
+			print $current_site->domain . $current_site->path . __( 'blogname' , 'buddypress');
+		} else {
+			print __( 'domain.' , 'buddypress') . $current_site->domain . $current_site->path;
+		}
+
+		echo '.</strong> ' . __( 'Must be at least 4 characters, letters and numbers only. It cannot be changed so choose carefully!)' , 'buddypress') . '</p>';
+	}
+
+	// Blog Title.
+	?>
+
+	<label for="blog_title"><?php _e('Site Title:', 'buddypress') ?></label>
+
+	<?php if ( $errmsg = $errors->get_error_message('blog_title') ) { ?>
+
+		<p class="error"><?php echo $errmsg ?></p>
+
+	<?php }
+	echo '<input name="blog_title" type="text" id="blog_title" value="'.esc_html($blog_title, 1).'" /></p>';
+	?>
+
+	<fieldset class="create-site">
+		<legend class="label"><?php _e('Privacy: I would like my site to appear in search engines, and in public listings around this network', 'buddypress') ?></legend>
+
+		<label class="checkbox" for="blog_public_on">
+			<input type="radio" id="blog_public_on" name="blog_public" value="1" <?php if( !isset( $_POST['blog_public'] ) || '1' == $_POST['blog_public'] ) { ?>checked="checked"<?php } ?> />
+			<strong><?php _e( 'Yes' , 'buddypress'); ?></strong>
+		</label>
+		<label class="checkbox" for="blog_public_off">
+			<input type="radio" id="blog_public_off" name="blog_public" value="0" <?php if( isset( $_POST['blog_public'] ) && '0' == $_POST['blog_public'] ) { ?>checked="checked"<?php } ?> />
+			<strong><?php _e( 'No' , 'buddypress'); ?></strong>
+		</label>
+	</fieldset>
+
+	<?php
+
+	/**
+	 * Fires at the end of all of the default input fields for blog creation form.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param WP_Error $errors WP_Error object if any present.
+	 */
+	do_action('signup_blogform', $errors);
+}
+
+/**
+ * Process a blog registration submission.
+ *
+ * Passes submitted values to {@link wpmu_create_blog()}.
+ *
+ * @return bool True on success, false on failure.
+ */
+function bp_blogs_validate_blog_signup() {
+	global $wpdb, $current_user, $blogname, $blog_title, $errors, $domain, $path, $current_site;
+
+	if ( !check_admin_referer( 'bp_blog_signup_form' ) )
+		return false;
+
+	$current_user = wp_get_current_user();
+
+	if( !is_user_logged_in() )
+		die();
+
+	$result = bp_blogs_validate_blog_form();
+	extract($result);
+
+	if ( $errors->get_error_code() ) {
+		unset($_POST['submit']);
+		bp_show_blog_signup_form( $blogname, $blog_title, $errors );
+		return false;
+	}
+
+	$public = (int) $_POST['blog_public'];
+
+	// Depreciated.
+	$meta = apply_filters( 'signup_create_blog_meta', array( 'lang_id' => 1, 'public' => $public ) );
+
+	/**
+	 * Filters the default values for Blog meta.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $meta {
+	 *      string $value  Default blog language ID.
+	 *      string $public Default public status.
+	 * }
+	 */
+	$meta = apply_filters( 'add_signup_meta', $meta );
+
+	// If this is a subdomain install, set up the site inside the root domain.
+	if ( is_subdomain_install() )
+		$domain = $blogname . '.' . preg_replace( '|^www\.|', '', $current_site->domain );
+
+	$blog_id = wpmu_create_blog( $domain, $path, $blog_title, $current_user->ID, $meta, $wpdb->siteid );
+	bp_blogs_confirm_blog_signup( $domain, $path, $blog_title, $current_user->user_login, $current_user->user_email, $meta, $blog_id );
+	return true;
+}
+
+/**
+ * Validate a blog creation submission.
+ *
+ * Essentially, a wrapper for {@link wpmu_validate_blog_signup()}.
+ *
+ * @return array Contains the new site data and error messages.
+ */
+function bp_blogs_validate_blog_form() {
+	$user = '';
+	if ( is_user_logged_in() )
+		$user = wp_get_current_user();
+
+	return wpmu_validate_blog_signup($_POST['blogname'], $_POST['blog_title'], $user);
+}
+
+/**
+ * Display a message after successful blog registration.
+ *
+ * @since 2.6.0 Introduced `$blog_id` parameter.
+ *
+ * @param string       $domain     The new blog's domain.
+ * @param string       $path       The new blog's path.
+ * @param string       $blog_title The new blog's title.
+ * @param string       $user_name  The user name of the user who created the blog. Unused.
+ * @param string       $user_email The email of the user who created the blog. Unused.
+ * @param string|array $meta       Meta values associated with the new blog. Unused.
+ * @param int|null     $blog_id    ID of the newly created blog.
+ */
+function bp_blogs_confirm_blog_signup( $domain, $path, $blog_title, $user_name, $user_email = '', $meta = '', $blog_id = null ) {
+	switch_to_blog( $blog_id );
+	$blog_url  = set_url_scheme( home_url() );
+	$login_url = set_url_scheme( wp_login_url() );
+	restore_current_blog();
+
+	?>
+	<p><?php _e( 'Congratulations! You have successfully registered a new site.', 'buddypress' ) ?></p>
+	<p>
+		<?php printf(
+			'%s %s',
+			sprintf(
+				__( '%s is your new site.', 'buddypress' ),
+				sprintf( '<a href="%s">%s</a>', esc_url( $blog_url ), esc_url( $blog_url ) )
+			),
+			sprintf(
+				/* translators: 1: Login URL, 2: User name */
+				__( '<a href="%1$s">Log in</a> as "%2$s" using your existing password.', 'buddypress' ),
+				esc_url( $login_url ),
+				esc_html( $user_name )
+			)
+		); ?>
+	</p>
+
+<?php
+
+	/**
+	 * Fires after the default successful blog registration message markup.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action('signup_finished');
+}
+
+/**
+ * Output a "Create a Site" link for users viewing their own profiles.
+ *
+ * This function is not used by BuddyPress as of 1.2, but is kept here for older
+ * themes that may still be using it.
+ */
+function bp_create_blog_link() {
+
+	// Don't show this link when not on your own profile.
+	if ( ! bp_is_my_profile() ) {
+		return;
+	}
+
+	/**
+	 * Filters "Create a Site" links for users viewing their own profiles.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value HTML link for creating a site.
+	 */
+	echo apply_filters( 'bp_create_blog_link', '<a href="' . trailingslashit( bp_get_blogs_directory_permalink() . 'create' ) . '">' . __( 'Create a Site', 'buddypress' ) . '</a>' );
+}
+
+/**
+ * Output navigation tabs for a user Blogs page.
+ *
+ * Currently unused by BuddyPress.
+ */
+function bp_blogs_blog_tabs() {
+
+	// Don't show these tabs on a user's own profile.
+	if ( bp_is_my_profile() ) {
+		return false;
+	} ?>
+
+	<ul class="content-header-nav">
+		<li<?php if ( bp_is_current_action( 'my-blogs'        ) || !bp_current_action() ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_blogs_slug() . '/my-blogs'        ); ?>"><?php printf( __( "%s's Sites", 'buddypress' ),           bp_get_displayed_user_fullname() ); ?></a></li>
+		<li<?php if ( bp_is_current_action( 'recent-posts'    )                         ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_blogs_slug() . '/recent-posts'    ); ?>"><?php printf( __( "%s's Recent Posts", 'buddypress' ),    bp_get_displayed_user_fullname() ); ?></a></li>
+		<li<?php if ( bp_is_current_action( 'recent-comments' )                         ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_blogs_slug() . '/recent-comments' ); ?>"><?php printf( __( "%s's Recent Comments", 'buddypress' ), bp_get_displayed_user_fullname() ); ?></a></li>
+	</ul>
+
+<?php
+
+	/**
+	 * Fires after the markup for the navigation tabs for a user Blogs page.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_blogs_blog_tabs' );
+}
+
+/**
+ * Output the blog directory search form.
+ */
+function bp_directory_blogs_search_form() {
+
+	$query_arg = bp_core_get_component_search_query_arg( 'blogs' );
+
+	if ( ! empty( $_REQUEST[ $query_arg ] ) ) {
+		$search_value = stripslashes( $_REQUEST[ $query_arg ] );
+	} else {
+		$search_value = bp_get_search_default_text( 'blogs' );
+	}
+
+	$search_form_html = '<form action="" method="get" id="search-blogs-form">
+		<label for="blogs_search"><input type="text" name="' . esc_attr( $query_arg ) . '" id="blogs_search" placeholder="'. esc_attr( $search_value ) .'" /></label>
+		<input type="submit" id="blogs_search_submit" name="blogs_search_submit" value="' . __( 'Search', 'buddypress' ) . '" />
+	</form>';
+
+	/**
+	 * Filters the output for the blog directory search form.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $search_form_html HTML markup for blog directory search form.
+	 */
+	echo apply_filters( 'bp_directory_blogs_search_form', $search_form_html );
+}
+
+/**
+ * Output the Create a Site button.
+ *
+ * @since 2.0.0
+ */
+function bp_blog_create_button() {
+	echo bp_get_blog_create_button();
+}
+	/**
+	 * Get the Create a Site button.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return false|string
+	 */
+	function bp_get_blog_create_button() {
+		if ( ! is_user_logged_in() ) {
+			return false;
+		}
+
+		if ( ! bp_blog_signup_enabled() ) {
+			return false;
+		}
+
+		$button_args = array(
+			'id'         => 'create_blog',
+			'component'  => 'blogs',
+			'link_text'  => __( 'Create a Site', 'buddypress' ),
+			'link_class' => 'blog-create no-ajax',
+			'link_href'  => trailingslashit( bp_get_blogs_directory_permalink() . 'create' ),
+			'wrapper'    => false,
+			'block_self' => false,
+		);
+
+		/**
+		 * Filters the Create a Site button.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $button_args Array of arguments to be used for the Create a Site button.
+		 */
+		return bp_get_button( apply_filters( 'bp_get_blog_create_button', $button_args ) );
+	}
+
+/**
+ * Output the Create a Site nav item.
+ *
+ * @since 2.2.0
+ */
+function bp_blog_create_nav_item() {
+	echo bp_get_blog_create_nav_item();
+}
+
+	/**
+	 * Get the Create a Site nav item.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_blog_create_nav_item() {
+		// Get the create a site button.
+		$create_blog_button = bp_get_blog_create_button();
+
+		// Make sure the button is available.
+		if ( empty( $create_blog_button ) ) {
+			return;
+		}
+
+		$output = '<li id="blog-create-nav">' . $create_blog_button . '</li>';
+
+		/**
+		 * Filters the Create A Site nav item output.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param string $output Nav item output.
+		 */
+		return apply_filters( 'bp_get_blog_create_nav_item', $output );
+	}
+
+/**
+ * Checks if a specific theme is still filtering the Blogs directory title
+ * if so, transform the title button into a Blogs directory nav item.
+ *
+ * @since 2.2.0
+ *
+ * @return string|null HTML Output
+ */
+function bp_blog_backcompat_create_nav_item() {
+	// Bail if Blogs nav item is already used by bp-legacy.
+	if ( has_action( 'bp_blogs_directory_blog_types', 'bp_legacy_theme_blog_create_nav', 999 ) ) {
+		return;
+	}
+
+	// Bail if the theme is not filtering the Blogs directory title.
+	if ( ! has_filter( 'bp_blogs_directory_header' ) ) {
+		return;
+	}
+
+	bp_blog_create_nav_item();
+}
+add_action( 'bp_blogs_directory_blog_types', 'bp_blog_backcompat_create_nav_item', 1000 );
+
+/**
+ * Output button for visiting a blog in a loop.
+ *
+ * @see bp_get_blogs_visit_blog_button() for description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_blogs_visit_blog_button()}.
+ */
+function bp_blogs_visit_blog_button( $args = '' ) {
+	echo bp_get_blogs_visit_blog_button( $args );
+}
+	/**
+	 * Return button for visiting a blog in a loop.
+	 *
+	 * @see BP_Button for a complete description of arguments and return
+	 *      value.
+	 *
+	 * @param array|string $args {
+	 *     Arguments are listed below, with their default values. For a
+	 *     complete description of arguments, see {@link BP_Button}.
+	 *     @type string $id                Default: 'visit_blog'.
+	 *     @type string $component         Default: 'blogs'.
+	 *     @type bool   $must_be_logged_in Default: false.
+	 *     @type bool   $block_self        Default: false.
+	 *     @type string $wrapper_class     Default: 'blog-button visit'.
+	 *     @type string $link_href         Permalink of the current blog in the loop.
+	 *     @type string $link_class        Default: 'blog-button visit'.
+	 *     @type string $link_text         Default: 'Visit Site'.
+	 * }
+	 * @return string The HTML for the Visit button.
+	 */
+	function bp_get_blogs_visit_blog_button( $args = '' ) {
+		$defaults = array(
+			'id'                => 'visit_blog',
+			'component'         => 'blogs',
+			'must_be_logged_in' => false,
+			'block_self'        => false,
+			'wrapper_class'     => 'blog-button visit',
+			'link_href'         => bp_get_blog_permalink(),
+			'link_class'        => 'blog-button visit',
+			'link_text'         => __( 'Visit Site', 'buddypress' ),
+		);
+
+		$button = wp_parse_args( $args, $defaults );
+
+		/**
+		 * Filters the button for visiting a blog in a loop.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param array $button Array of arguments to be used for the button to visit a blog.
+		 */
+		return bp_get_button( apply_filters( 'bp_get_blogs_visit_blog_button', $button ) );
+	}
+
+/** Stats **********************************************************************/
+
+/**
+ * Display the number of blogs in user's profile.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args Before|after|user_id.
+ */
+function bp_blogs_profile_stats( $args = '' ) {
+	echo bp_blogs_get_profile_stats( $args );
+}
+add_action( 'bp_members_admin_user_stats', 'bp_blogs_profile_stats', 9, 1 );
+
+/**
+ * Return the number of blogs in user's profile.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args Before|after|user_id.
+ * @return string HTML for stats output.
+ */
+function bp_blogs_get_profile_stats( $args = '' ) {
+
+	// Parse the args.
+	$r = bp_parse_args( $args, array(
+		'before'  => '<li class="bp-blogs-profile-stats">',
+		'after'   => '</li>',
+		'user_id' => bp_displayed_user_id(),
+		'blogs'   => 0,
+		'output'  => ''
+	), 'blogs_get_profile_stats' );
+
+	// Allow completely overloaded output.
+	if ( is_multisite() && empty( $r['output'] ) ) {
+
+		// Only proceed if a user ID was passed.
+		if ( ! empty( $r['user_id'] ) ) {
+
+			// Get the user's blogs.
+			if ( empty( $r['blogs'] ) ) {
+				$r['blogs'] = absint( bp_blogs_total_blogs_for_user( $r['user_id'] ) );
+			}
+
+			// If blogs exist, show some formatted output.
+			$r['output'] = $r['before'] . sprintf( _n( '%s site', '%s sites', $r['blogs'], 'buddypress' ), '<strong>' . $r['blogs'] . '</strong>' ) . $r['after'];
+		}
+	}
+
+	/**
+	 * Filters the number of blogs in user's profile.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $value Output determined for the profile stats.
+	 * @param array  $r     Array of arguments used for default output if none provided.
+	 */
+	return apply_filters( 'bp_blogs_get_profile_stats', $r['output'], $r );
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/bp-blogs-widgets.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BuddyPress Blogs Widgets.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsWidgets
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register the widgets for the Blogs component.
+ */
+function bp_blogs_register_widgets() {
+	global $wpdb;
+
+	if ( bp_is_active( 'activity' ) && bp_is_root_blog( $wpdb->blogid ) ) {
+		add_action( 'widgets_init', function() { register_widget( 'BP_Blogs_Recent_Posts_Widget' ); } );
+	}
+}
+add_action( 'bp_register_widgets', 'bp_blogs_register_widgets' );
Index: /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-blog.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-blog.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-blog.php	(revision 12265)
@@ -0,0 +1,665 @@
+<?php
+/**
+ * BuddyPress Blogs Classes.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main BuddyPress blog class.
+ *
+ * A BP_Blogs_Object represents a link between a specific WordPress blog on a
+ * network and a specific user on that blog.
+ *
+ * @since 1.0.0
+ */
+class BP_Blogs_Blog {
+
+	/**
+	 * Site ID.
+	 *
+	 * @var int|null
+	 */
+	public $id;
+
+	/**
+	 * User ID.
+	 *
+	 * @var int
+	 */
+	public $user_id;
+
+	/**
+	 * Blog ID.
+	 *
+	 * @var int
+	 */
+	public $blog_id;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @param int|null $id Optional. The ID of the blog.
+	 */
+	public function __construct( $id = null ) {
+		if ( !empty( $id ) ) {
+			$this->id = (int) $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Populate the object with data about the specific activity item.
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$blog = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->blogs->table_name} WHERE id = %d", $this->id ) );
+
+		$this->user_id = (int) $blog->user_id;
+		$this->blog_id = (int) $blog->blog_id;
+	}
+
+	/**
+	 * Save the BP blog data to the database.
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function save() {
+		global $wpdb;
+
+		/**
+		 * Filters the blog user ID before save.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $value User ID.
+		 * @param int $value Site ID.
+		 */
+		$this->user_id = apply_filters( 'bp_blogs_blog_user_id_before_save', $this->user_id, $this->id );
+
+		/**
+		 * Filters the blog blog ID before save.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $value Blog ID.
+		 * @param int $value Site ID.
+		 */
+		$this->blog_id = apply_filters( 'bp_blogs_blog_id_before_save', $this->blog_id, $this->id );
+
+		/**
+		 * Fires before the current blog item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Blogs_Blog $this Current instance of the blog item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_blogs_blog_before_save', array( &$this ) );
+
+		// Don't try and save if there is no user ID or blog ID set.
+		if ( !$this->user_id || !$this->blog_id )
+			return false;
+
+		// Don't save if this blog has already been recorded for the user.
+		if ( !$this->id && $this->exists() )
+			return false;
+
+		$bp = buddypress();
+
+		if ( $this->id ) {
+			// Update.
+			$sql = $wpdb->prepare( "UPDATE {$bp->blogs->table_name} SET user_id = %d, blog_id = %d WHERE id = %d", $this->user_id, $this->blog_id, $this->id );
+		} else {
+			// Save.
+			$sql = $wpdb->prepare( "INSERT INTO {$bp->blogs->table_name} ( user_id, blog_id ) VALUES ( %d, %d )", $this->user_id, $this->blog_id );
+		}
+
+		if ( !$wpdb->query($sql) )
+			return false;
+
+		/**
+		 * Fires after the current blog item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Blogs_Blog $this Current instance of the blog item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_blogs_blog_after_save', array( &$this ) );
+
+		if ( $this->id )
+			return $this->id;
+		else
+			return $wpdb->insert_id;
+	}
+
+	/**
+	 * Check whether an association between this user and this blog exists.
+	 *
+	 * @return int $value The number of associations between the user and blog
+	 *                    saved in the blog component tables.
+	 */
+	public function exists() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->blogs->table_name} WHERE user_id = %d AND blog_id = %d", $this->user_id, $this->blog_id ) );
+	}
+
+	/** Static Methods ***************************************************/
+
+	/**
+	 * Retrieve a set of blog-user associations.
+	 *
+	 * @param string      $type              The order in which results should be returned.
+	 *                                       'active', 'alphabetical', 'newest', or 'random'.
+	 * @param int|bool    $limit             Optional. The maximum records to return.
+	 *                                       Default: false.
+	 * @param int|bool    $page              Optional. The page of records to return.
+	 *                                       Default: false (unlimited results).
+	 * @param int         $user_id           Optional. ID of the user whose blogs are being
+	 *                                       retrieved. Default: 0.
+	 * @param string|bool $search_terms      Optional. Search by text stored in
+	 *                                       blogmeta (such as the blog name). Default: false.
+	 * @param bool        $update_meta_cache Whether to pre-fetch metadata for
+	 *                                       blogs. Default: true.
+	 * @param array|bool  $include_blog_ids  Array of blog IDs to include.
+	 * @return array Multidimensional results array, structured as follows:
+	 *               'blogs' - Array of located blog objects
+	 *               'total' - A count of the total blogs matching the filter params
+	 */
+	public static function get( $type, $limit = false, $page = false, $user_id = 0, $search_terms = false, $update_meta_cache = true, $include_blog_ids = false ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
+			$hidden_sql = "AND wb.public = 1";
+		else
+			$hidden_sql = '';
+
+		$pag_sql = ( $limit && $page ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';
+
+		$user_sql = !empty( $user_id ) ? $wpdb->prepare( " AND b.user_id = %d", $user_id ) : '';
+
+		switch ( $type ) {
+			case 'active': default:
+				$order_sql = "ORDER BY bm.meta_value DESC";
+				break;
+			case 'alphabetical':
+				$order_sql = "ORDER BY bm_name.meta_value ASC";
+				break;
+			case 'newest':
+				$order_sql = "ORDER BY wb.registered DESC";
+				break;
+			case 'random':
+				$order_sql = "ORDER BY RAND()";
+				break;
+		}
+
+		$include_sql = '';
+		$include_blog_ids = array_filter( wp_parse_id_list( $include_blog_ids ) );
+		if ( ! empty( $include_blog_ids ) ) {
+			$blog_ids_sql = implode( ',', $include_blog_ids );
+			$include_sql  = " AND b.blog_id IN ({$blog_ids_sql})";
+		}
+
+		if ( ! empty( $search_terms ) ) {
+			$search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
+			$search_terms_sql  = $wpdb->prepare( 'AND (bm_name.meta_value LIKE %s OR bm_description.meta_value LIKE %s)', $search_terms_like, $search_terms_like );
+		} else {
+			$search_terms_sql = '';
+		}
+
+		$paged_blogs = $wpdb->get_results( "
+			SELECT b.blog_id, b.user_id as admin_user_id, u.user_email as admin_user_email, wb.domain, wb.path, bm.meta_value as last_activity, bm_name.meta_value as name
+			FROM
+			  {$bp->blogs->table_name} b
+			  LEFT JOIN {$bp->blogs->table_name_blogmeta} bm ON (b.blog_id = bm.blog_id)
+			  LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_name ON (b.blog_id = bm_name.blog_id)
+			  LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_description ON (b.blog_id = bm_description.blog_id)
+			  LEFT JOIN {$wpdb->base_prefix}blogs wb ON (b.blog_id = wb.blog_id)
+			  LEFT JOIN {$wpdb->users} u ON (b.user_id = u.ID)
+			WHERE
+			  wb.archived = '0' AND wb.spam = 0 AND wb.mature = 0 AND wb.deleted = 0 {$hidden_sql}
+			  AND bm.meta_key = 'last_activity' AND bm_name.meta_key = 'name' AND bm_description.meta_key = 'description'
+			  {$search_terms_sql} {$user_sql} {$include_sql}
+			GROUP BY b.blog_id {$order_sql} {$pag_sql}
+		" );
+
+		$total_blogs = $wpdb->get_var( "
+			SELECT COUNT(DISTINCT b.blog_id)
+			FROM
+			  {$bp->blogs->table_name} b
+			  LEFT JOIN {$wpdb->base_prefix}blogs wb ON (b.blog_id = wb.blog_id)
+			  LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_name ON (b.blog_id = bm_name.blog_id)
+			  LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_description ON (b.blog_id = bm_description.blog_id)
+			WHERE
+			  wb.archived = '0' AND wb.spam = 0 AND wb.mature = 0 AND wb.deleted = 0 {$hidden_sql}
+			  AND
+			  bm_name.meta_key = 'name' AND bm_description.meta_key = 'description'
+			  {$search_terms_sql} {$user_sql} {$include_sql}
+		" );
+
+		$blog_ids = array();
+		foreach ( (array) $paged_blogs as $blog ) {
+			$blog_ids[] = (int) $blog->blog_id;
+		}
+
+		$paged_blogs = BP_Blogs_Blog::get_blog_extras( $paged_blogs, $blog_ids, $type );
+
+		// Integer casting.
+		foreach ( (array) $paged_blogs as $key => $data ) {
+			$paged_blogs[ $key ]->blog_id       = (int) $paged_blogs[ $key ]->blog_id;
+			$paged_blogs[ $key ]->admin_user_id = (int) $paged_blogs[ $key ]->admin_user_id;
+		}
+
+		if ( $update_meta_cache ) {
+			bp_blogs_update_meta_cache( $blog_ids );
+		}
+
+		return array( 'blogs' => $paged_blogs, 'total' => $total_blogs );
+	}
+
+	/**
+	 * Delete the record of a given blog for all users.
+	 *
+	 * @param int $blog_id The blog being removed from all users.
+	 * @return int|bool Number of rows deleted on success, false on failure.
+	 */
+	public static function delete_blog_for_all( $blog_id ) {
+		global $wpdb;
+
+		bp_blogs_delete_blogmeta( $blog_id );
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->blogs->table_name} WHERE blog_id = %d", $blog_id ) );
+	}
+
+	/**
+	 * Delete the record of a given blog for a specific user.
+	 *
+	 * @param int      $blog_id The blog being removed.
+	 * @param int|null $user_id Optional. The ID of the user from whom the blog is
+	 *                          being removed. If absent, defaults to the logged-in user ID.
+	 * @return int|bool Number of rows deleted on success, false on failure.
+	 */
+	public static function delete_blog_for_user( $blog_id, $user_id = null ) {
+		global $wpdb;
+
+		if ( !$user_id )
+			$user_id = bp_loggedin_user_id();
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->blogs->table_name} WHERE user_id = %d AND blog_id = %d", $user_id, $blog_id ) );
+	}
+
+	/**
+	 * Delete all of a user's blog associations in the BP tables.
+	 *
+	 * @param int|null $user_id Optional. The ID of the user whose blog associations
+	 *                          are being deleted. If absent, defaults to logged-in user ID.
+	 * @return int|bool Number of rows deleted on success, false on failure.
+	 */
+	public static function delete_blogs_for_user( $user_id = null ) {
+		global $wpdb;
+
+		if ( !$user_id )
+			$user_id = bp_loggedin_user_id();
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->blogs->table_name} WHERE user_id = %d", $user_id ) );
+	}
+
+	/**
+	 * Get all of a user's blogs, as tracked by BuddyPress.
+	 *
+	 * Note that this is different from the WordPress function
+	 * {@link get_blogs_of_user()}; the current method returns only those
+	 * blogs that have been recorded by BuddyPress, while the WP function
+	 * does a true query of a user's blog capabilities.
+	 *
+	 * @param int  $user_id     Optional. ID of the user whose blogs are being
+	 *                          queried. Defaults to logged-in user.
+	 * @param bool $show_hidden Optional. Whether to include blogs that are not marked
+	 *                          public. Defaults to true when viewing one's own profile.
+	 * @return array Multidimensional results array, structured as follows:
+	 *               'blogs' - Array of located blog objects.
+	 *               'total' - A count of the total blogs for the user.
+	 */
+	public static function get_blogs_for_user( $user_id = 0, $show_hidden = false ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( !$user_id )
+			$user_id = bp_displayed_user_id();
+
+		// Show logged in users their hidden blogs.
+		if ( !bp_is_my_profile() && !$show_hidden )
+			$blogs = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT b.blog_id, b.id, bm1.meta_value as name, wb.domain, wb.path FROM {$bp->blogs->table_name} b, {$wpdb->base_prefix}blogs wb, {$bp->blogs->table_name_blogmeta} bm1 WHERE b.blog_id = wb.blog_id AND b.blog_id = bm1.blog_id AND bm1.meta_key = 'name' AND wb.public = 1 AND wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND b.user_id = %d ORDER BY b.blog_id", $user_id ) );
+		else
+			$blogs = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT b.blog_id, b.id, bm1.meta_value as name, wb.domain, wb.path FROM {$bp->blogs->table_name} b, {$wpdb->base_prefix}blogs wb, {$bp->blogs->table_name_blogmeta} bm1 WHERE b.blog_id = wb.blog_id AND b.blog_id = bm1.blog_id AND bm1.meta_key = 'name' AND wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND b.user_id = %d ORDER BY b.blog_id", $user_id ) );
+
+		$total_blog_count = BP_Blogs_Blog::total_blog_count_for_user( $user_id );
+
+		$user_blogs = array();
+		foreach ( (array) $blogs as $blog ) {
+			$user_blogs[$blog->blog_id] = new stdClass;
+			$user_blogs[$blog->blog_id]->id = (int) $blog->id;
+			$user_blogs[$blog->blog_id]->blog_id = (int) $blog->blog_id;
+			$user_blogs[$blog->blog_id]->siteurl = ( is_ssl() ) ? 'https://' . $blog->domain . $blog->path : 'http://' . $blog->domain . $blog->path;
+			$user_blogs[$blog->blog_id]->name = $blog->name;
+		}
+
+		return array( 'blogs' => $user_blogs, 'count' => $total_blog_count );
+	}
+
+	/**
+	 * Get IDs of all of a user's blogs, as tracked by BuddyPress.
+	 *
+	 * This method always includes hidden blogs.
+	 *
+	 * @param int $user_id Optional. ID of the user whose blogs are being
+	 *                     queried. Defaults to logged-in user.
+	 * @return int The number of blogs associated with the user.
+	 */
+	public static function get_blog_ids_for_user( $user_id = 0 ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( !$user_id )
+			$user_id = bp_displayed_user_id();
+
+		return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM {$bp->blogs->table_name} WHERE user_id = %d", $user_id ) ) );
+	}
+
+	/**
+	 * Check whether a blog has been recorded by BuddyPress.
+	 *
+	 * @param int $blog_id ID of the blog being queried.
+	 * @return int|null The ID of the first located entry in the BP table
+	 *                  on success, otherwise null.
+	 */
+	public static function is_recorded( $blog_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$query = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->blogs->table_name} WHERE blog_id = %d", $blog_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+
+	/**
+	 * Return a count of associated blogs for a given user.
+	 *
+	 * Includes hidden blogs when the logged-in user is the same as the
+	 * $user_id parameter, or when the logged-in user has the bp_moderate
+	 * cap.
+	 *
+	 * @param int|null $user_id Optional. ID of the user whose blogs are being
+	 *                          queried. Defaults to logged-in user.
+	 * @return int Blog count for the user.
+	 */
+	public static function total_blog_count_for_user( $user_id = null ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( !$user_id )
+			$user_id = bp_displayed_user_id();
+
+		// If the user is logged in return the blog count including their hidden blogs.
+		if ( ( is_user_logged_in() && $user_id == bp_loggedin_user_id() ) || bp_current_user_can( 'bp_moderate' ) ) {
+			return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT b.blog_id) FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND user_id = %d", $user_id ) );
+		} else {
+			return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT b.blog_id) FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.public = 1 AND wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND user_id = %d", $user_id ) );
+		}
+	}
+
+	/**
+	 * Return a list of blogs matching a search term.
+	 *
+	 * Matches against blog names and descriptions, as stored in the BP
+	 * blogmeta table.
+	 *
+	 * @param string   $filter The search term.
+	 * @param int|null $limit  Optional. The maximum number of items to return.
+	 *                         Default: null (no limit).
+	 * @param int|null $page   Optional. The page of results to return. Default:
+	 *                         null (no limit).
+	 * @return array Multidimensional results array, structured as follows:
+	 *               'blogs' - Array of located blog objects.
+	 *               'total' - A count of the total blogs matching the query.
+	 */
+	public static function search_blogs( $filter, $limit = null, $page = null ) {
+		global $wpdb;
+
+		$search_terms_like = '%' . bp_esc_like( $filter ) . '%';
+		$search_terms_sql  = $wpdb->prepare( 'bm.meta_value LIKE %s', $search_terms_like );
+
+		$hidden_sql = '';
+		if ( !bp_current_user_can( 'bp_moderate' ) )
+			$hidden_sql = "AND wb.public = 1";
+
+		$pag_sql = '';
+		if ( $limit && $page ) {
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+		}
+
+		$bp = buddypress();
+
+		$paged_blogs = $wpdb->get_results( "SELECT DISTINCT bm.blog_id FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE ( ( bm.meta_key = 'name' OR bm.meta_key = 'description' ) AND {$search_terms_sql} ) {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY meta_value ASC{$pag_sql}" );
+		$total_blogs = $wpdb->get_var( "SELECT COUNT(DISTINCT bm.blog_id) FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE ( ( bm.meta_key = 'name' OR bm.meta_key = 'description' ) AND {$search_terms_sql} ) {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY meta_value ASC" );
+
+		// Integer casting.
+		foreach ( (array) $paged_blogs as $key => $data ) {
+			$paged_blogs[ $key ]->blog_id = (int) $paged_blogs[ $key ]->blog_id;
+		}
+
+		return array( 'blogs' => $paged_blogs, 'total' => (int) $total_blogs );
+	}
+
+	/**
+	 * Retrieve a list of all blogs.
+	 *
+	 * Query will include hidden blogs if the logged-in user has the
+	 * 'bp_moderate' cap.
+	 *
+	 * @param int|null $limit Optional. The maximum number of items to return.
+	 *                        Default: null (no limit).
+	 * @param int|null $page  Optional. The page of results to return. Default:
+	 *                        null (no limit).
+	 * @return array Multidimensional results array, structured as follows:
+	 *               'blogs' - Array of located blog objects.
+	 *               'total' - A count of the total blogs.
+	 */
+	public static function get_all( $limit = null, $page = null ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$hidden_sql = !bp_current_user_can( 'bp_moderate' ) ? "AND wb.public = 1" : '';
+		$pag_sql    = ( $limit && $page ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';
+
+		$paged_blogs = $wpdb->get_results( "SELECT DISTINCT b.blog_id FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 {$hidden_sql} {$pag_sql}" );
+		$total_blogs = $wpdb->get_var( "SELECT COUNT(DISTINCT b.blog_id) FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 {$hidden_sql}" );
+
+		// Integer casting.
+		foreach ( (array) $paged_blogs as $key => $data ) {
+			$paged_blogs[ $key ]->blog_id = (int) $paged_blogs[ $key ]->blog_id;
+		}
+
+		return array( 'blogs' => $paged_blogs, 'total' => (int) $total_blogs );
+	}
+
+	/**
+	 * Retrieve a list of blogs whose names start with a given letter.
+	 *
+	 * Query will include hidden blogs if the logged-in user has the
+	 * 'bp_moderate' cap.
+	 *
+	 * @param string   $letter The letter you're looking for.
+	 * @param int|null $limit  Optional. The maximum number of items to return.
+	 *                         Default: null (no limit).
+	 * @param int|null $page   Optional. The page of results to return. Default:
+	 *                         null (no limit).
+	 * @return array Multidimensional results array, structured as follows:
+	 *               'blogs' - Array of located blog objects.
+	 *               'total' - A count of the total blogs matching the query.
+	 */
+	public static function get_by_letter( $letter, $limit = null, $page = null ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$letter_like = '%' . bp_esc_like( $letter ) . '%';
+		$letter_sql  = $wpdb->prepare( 'bm.meta_value LIKE %s', $letter_like );
+
+		$hidden_sql = '';
+		if ( !bp_current_user_can( 'bp_moderate' ) )
+			$hidden_sql = "AND wb.public = 1";
+
+		$pag_sql = '';
+		if ( $limit && $page )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		$paged_blogs = $wpdb->get_results( "SELECT DISTINCT bm.blog_id FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE bm.meta_key = 'name' AND {$letter_sql} {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY bm.meta_value ASC{$pag_sql}" );
+		$total_blogs = $wpdb->get_var( "SELECT COUNT(DISTINCT bm.blog_id) FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE bm.meta_key = 'name' AND {$letter_sql} {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY bm.meta_value ASC" );
+
+		// Integer casting.
+		foreach ( (array) $paged_blogs as $key => $data ) {
+			$paged_blogs[ $key ]->blog_id = (int) $paged_blogs[ $key ]->blog_id;
+		}
+
+		return array( 'blogs' => $paged_blogs, 'total' => (int) $total_blogs );
+	}
+
+	/**
+	 * Fetch blog data not caught in the main query and append it to results array.
+	 *
+	 * Gets the following information, which is either unavailable at the
+	 * time of the original query, or is more efficient to look up in one
+	 * fell swoop:
+	 *   - The latest post for each blog, include Featured Image data
+	 *   - The blog description
+	 *
+	 * @param array       $paged_blogs Array of results from the original query.
+	 * @param array       $blog_ids    Array of IDs returned from the original query.
+	 * @param string|bool $type        Not currently used. Default: false.
+	 * @return array $paged_blogs The located blogs array, with the extras added.
+	 */
+	public static function get_blog_extras( &$paged_blogs, &$blog_ids, $type = false ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( empty( $blog_ids ) )
+			return $paged_blogs;
+
+		$blog_ids = implode( ',', wp_parse_id_list( $blog_ids ) );
+
+		for ( $i = 0, $count = count( $paged_blogs ); $i < $count; ++$i ) {
+			$blog_prefix = $wpdb->get_blog_prefix( $paged_blogs[$i]->blog_id );
+			$paged_blogs[$i]->latest_post = $wpdb->get_row( "SELECT ID, post_content, post_title, post_excerpt, guid FROM {$blog_prefix}posts WHERE post_status = 'publish' AND post_type = 'post' AND id != 1 ORDER BY id DESC LIMIT 1" );
+			$images = array();
+
+			// Add URLs to any Featured Image this post might have.
+			if ( ! empty( $paged_blogs[$i]->latest_post ) && has_post_thumbnail( $paged_blogs[$i]->latest_post->ID ) ) {
+
+				// Grab 4 sizes of the image. Thumbnail.
+				$image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'thumbnail', false );
+				if ( ! empty( $image ) )
+					$images['thumbnail'] = $image[0];
+
+				// Medium.
+				$image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'medium', false );
+				if ( ! empty( $image ) )
+					$images['medium'] = $image[0];
+
+				// Large.
+				$image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'large', false );
+				if ( ! empty( $image ) )
+					$images['large'] = $image[0];
+
+				// Post thumbnail.
+				$image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'post-thumbnail', false );
+				if ( ! empty( $image ) )
+					$images['post-thumbnail'] = $image[0];
+
+				// Add the images to the latest_post object.
+				$paged_blogs[$i]->latest_post->images = $images;
+			}
+		}
+
+		/* Fetch the blog description for each blog (as it may be empty we can't fetch it in the main query). */
+		$blog_descs = $wpdb->get_results( "SELECT blog_id, meta_value as description FROM {$bp->blogs->table_name_blogmeta} WHERE meta_key = 'description' AND blog_id IN ( {$blog_ids} )" );
+
+		for ( $i = 0, $count = count( $paged_blogs ); $i < $count; ++$i ) {
+			foreach ( (array) $blog_descs as $desc ) {
+				if ( $desc->blog_id == $paged_blogs[$i]->blog_id )
+					$paged_blogs[$i]->description = $desc->description;
+			}
+		}
+
+		return $paged_blogs;
+	}
+
+	/**
+	 * Check whether a given blog is hidden.
+	 *
+	 * Checks the 'public' column in the wp_blogs table.
+	 *
+	 * @param int $blog_id The ID of the blog being checked.
+	 * @return bool True if hidden (public = 0), false otherwise.
+	 */
+	public static function is_hidden( $blog_id ) {
+		global $wpdb;
+
+		if ( !(int) $wpdb->get_var( $wpdb->prepare( "SELECT public FROM {$wpdb->base_prefix}blogs WHERE blog_id = %d", $blog_id ) ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get ID of user-blog link.
+	 *
+	 * @param int $user_id ID of user.
+	 * @param int $blog_id ID of blog.
+	 * @return int|bool ID of user-blog link, or false if not found.
+	 */
+	public static function get_user_blog( $user_id, $blog_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$user_blog = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->blogs->table_name} WHERE user_id = %d AND blog_id = %d", $user_id, $blog_id ) );
+
+		if ( empty( $user_blog ) ) {
+			$user_blog = false;
+		} else {
+			$user_blog = intval( $user_blog );
+		}
+
+		return $user_blog;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-component.php	(revision 12265)
@@ -0,0 +1,355 @@
+<?php
+/**
+ * BuddyPress Blogs Loader
+ *
+ * The blogs component tracks posts and comments to member activity streams,
+ * shows blogs the member can post to in their profiles, and caches useful
+ * information from those blogs to make querying blogs in bulk more performant.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsCore
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Creates our Blogs component.
+ */
+class BP_Blogs_Component extends BP_Component {
+
+	/**
+	 * Start the blogs component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'blogs',
+			__( 'Site Directory', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 30,
+				'search_query_arg' => 'sites_search',
+				'features' => array( 'site-icon' )
+			)
+		);
+	}
+
+	/**
+	 * Set up global settings for the blogs component.
+	 *
+	 * The BP_BLOGS_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_globals() for description of parameters.
+	 *
+	 * @param array $args See {@link BP_Component::setup_globals()}.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		if ( ! defined( 'BP_BLOGS_SLUG' ) ) {
+			define ( 'BP_BLOGS_SLUG', $this->id );
+		}
+
+		// Global tables for messaging component.
+		$global_tables = array(
+			'table_name'          => $bp->table_prefix . 'bp_user_blogs',
+			'table_name_blogmeta' => $bp->table_prefix . 'bp_user_blogs_blogmeta',
+		);
+
+		$meta_tables = array(
+			'bp_blog' => $bp->table_prefix . 'bp_user_blogs_blogmeta',
+		);
+
+		// Fetch the default directory title.
+		$default_directory_titles = bp_core_get_directory_page_default_titles();
+		$default_directory_title  = $default_directory_titles[$this->id];
+
+		// All globals for blogs component.
+		$args = array(
+			'slug'                  => BP_BLOGS_SLUG,
+			'root_slug'             => isset( $bp->pages->blogs->slug ) ? $bp->pages->blogs->slug : BP_BLOGS_SLUG,
+			'has_directory'         => is_multisite(), // Non-multisite installs don't need a top-level Sites directory, since there's only one site.
+			'directory_title'       => isset( $bp->pages->blogs->title ) ? $bp->pages->blogs->title : $default_directory_title,
+			'notification_callback' => 'bp_blogs_format_notifications',
+			'search_string'         => __( 'Search sites...', 'buddypress' ),
+			'autocomplete_all'      => defined( 'BP_MESSAGES_AUTOCOMPLETE_ALL' ),
+			'global_tables'         => $global_tables,
+			'meta_tables'           => $meta_tables,
+		);
+
+		// Setup the globals.
+		parent::setup_globals( $args );
+
+		/**
+		 * Filters if a blog is public.
+		 *
+		 * In case the config is not multisite, the blog_public option is ignored.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param int $value Whether or not the blog is public.
+		 */
+		if ( 0 !== apply_filters( 'bp_is_blog_public', (int) get_option( 'blog_public' ) ) || ! is_multisite() ) {
+
+			/**
+			 * Filters the post types to track for the Blogs component.
+			 *
+			 * @since 1.5.0
+			 * @deprecated 2.3.0
+			 *
+			 * @param array $value Array of post types to track.
+			 */
+			$post_types = apply_filters( 'bp_blogs_record_post_post_types', array( 'post' ) );
+
+			foreach ( $post_types as $post_type ) {
+				add_post_type_support( $post_type, 'buddypress-activity' );
+			}
+		}
+	}
+
+	/**
+	 * Include bp-blogs files.
+	 *
+	 * @see BP_Component::includes() for description of parameters.
+	 *
+	 * @param array $includes See {@link BP_Component::includes()}.
+	 */
+	public function includes( $includes = array() ) {
+
+		// Files to include.
+		$includes = array(
+			'cache',
+			'template',
+			'filters',
+			'functions',
+		);
+
+		if ( bp_is_active( 'activity' ) ) {
+			$includes[] = 'activity';
+		}
+
+		if ( is_multisite() ) {
+			$includes[] = 'widgets';
+		}
+
+		// Include the files.
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		// Bail if not on a blogs page or not multisite.
+		if ( ! bp_is_blogs_component() || ! is_multisite() ) {
+			return;
+		}
+
+		// Actions.
+		if ( isset( $_GET['random-blog'] ) ) {
+			require $this->path . 'bp-blogs/actions/random.php';
+		}
+
+		// Screens.
+		if ( bp_is_user() ) {
+			require $this->path . 'bp-blogs/screens/my-blogs.php';
+		} else {
+			if ( bp_is_blogs_directory() ) {
+				require $this->path . 'bp-blogs/screens/directory.php';
+			}
+
+			if ( is_user_logged_in() && bp_is_current_action( 'create' ) ) {
+				require $this->path . 'bp-blogs/screens/create.php';
+			}
+
+			// Theme compatibility.
+			new BP_Blogs_Theme_Compat();
+		}
+	}
+
+	/**
+	 * Set up component navigation for bp-blogs.
+	 *
+	 * @see BP_Component::setup_nav() for a description of arguments.
+	 *
+	 * @param array $main_nav Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 * @param array $sub_nav  Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		/**
+		 * Blog/post/comment menus should not appear on single WordPress setups.
+		 * Although comments and posts made by users will still show on their
+		 * activity stream.
+		 */
+		if ( ! is_multisite() ) {
+			return false;
+		}
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$slug       = bp_get_blogs_slug();
+		$parent_url = trailingslashit( $user_domain . $slug );
+
+		// Add 'Sites' to the main navigation.
+		$count    = (int) bp_get_total_blog_count_for_user();
+		$class    = ( 0 === $count ) ? 'no-count' : 'count';
+		$nav_text = sprintf(
+			/* translators: %s: Site count for the current user */
+			__( 'Sites %s', 'buddypress' ),
+			sprintf(
+				'<span class="%s">%s</span>',
+				esc_attr( $class ),
+				bp_core_number_format( $count )
+			)
+		);
+		$main_nav = array(
+			'name'                => $nav_text,
+			'slug'                => $slug,
+			'position'            => 30,
+			'screen_function'     => 'bp_blogs_screen_my_blogs',
+			'default_subnav_slug' => 'my-sites',
+			'item_css_id'         => $this->id
+		);
+
+		$sub_nav[] = array(
+			'name'            => __( 'My Sites', 'buddypress' ),
+			'slug'            => 'my-sites',
+			'parent_url'      => $parent_url,
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_blogs_screen_my_blogs',
+			'position'        => 10
+		);
+
+		// Setup navigation.
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up bp-blogs integration with the WordPress admin bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_admin_bar() for a description of arguments.
+	 *
+	 * @param array $wp_admin_nav See BP_Component::setup_admin_bar()
+	 *                            for description.
+	 * @return bool
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		/**
+		 * Site/post/comment menus should not appear on single WordPress setups.
+		 *
+		 * Comments and posts made by users will still show in their activity.
+		 */
+		if ( ! is_multisite() ) {
+			return false;
+		}
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$blogs_link = trailingslashit( bp_loggedin_user_domain() . bp_get_blogs_slug() );
+
+			// Add the "Sites" sub menu.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => __( 'Sites', 'buddypress' ),
+				'href'   => $blogs_link
+			);
+
+			// My Sites.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-my-sites',
+				'title'    => __( 'My Sites', 'buddypress' ),
+				'href'     => $blogs_link,
+				'position' => 10
+			);
+
+			// Create a Site.
+			if ( bp_blog_signup_enabled() ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-create',
+					'title'    => __( 'Create a Site', 'buddypress' ),
+					'href'     => trailingslashit( bp_get_blogs_directory_permalink() . 'create' ),
+					'position' => 99
+				);
+			}
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 */
+	public function setup_title() {
+
+		// Set up the component options navigation for Site.
+		if ( bp_is_blogs_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() ) {
+				if ( bp_is_active( 'xprofile' ) ) {
+					$bp->bp_options_title = __( 'My Sites', 'buddypress' );
+				}
+
+			// If we are not viewing the logged in user, set up the current
+			// users avatar and name.
+			} else {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title = bp_get_displayed_user_fullname();
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_blog_meta'
+		) );
+
+		parent::setup_cache_groups();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-recent-posts-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-recent-posts-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-recent-posts-widget.php	(revision 12265)
@@ -0,0 +1,164 @@
+<?php
+/**
+ * BuddyPress Blogs Recent Posts Widget.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsWidgets
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The Recent Networkwide Posts widget.
+ */
+class BP_Blogs_Recent_Posts_Widget extends WP_Widget {
+
+	/**
+	 * Constructor method.
+	 */
+	public function __construct() {
+		$widget_ops = array(
+			'description'                 => __( 'A list of recently published posts from across your network.', 'buddypress' ),
+			'classname'                   => 'widget_bp_blogs_widget buddypress widget',
+			'customize_selective_refresh' => true,
+		);
+		parent::__construct( false, $name = _x( '(BuddyPress) Recent Networkwide Posts', 'widget name', 'buddypress' ), $widget_ops );
+	}
+
+	/**
+	 * Display the networkwide posts widget.
+	 *
+	 * @see WP_Widget::widget() for description of parameters.
+	 *
+	 * @param array $args     Widget arguments.
+	 * @param array $instance Widget settings, as saved by the user.
+	 */
+	public function widget( $args, $instance ) {
+		global $activities_template;
+
+		$title = ! empty( $instance['title'] )
+			? esc_html( $instance['title'] )
+			: __( 'Recent Networkwide Posts', 'buddypress' );
+
+		if ( ! empty( $instance['link_title'] ) ) {
+			$title = '<a href="' . bp_get_blogs_directory_permalink() . '">' . esc_html( $title ) . '</a>';
+		}
+
+		/**
+		 * Filters the Blogs Recent Posts widget title.
+		 *
+		 * @since 2.2.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
+
+		echo $args['before_widget'];
+		echo $args['before_title'] . $title . $args['after_title'];
+
+		if ( empty( $instance['max_posts'] ) || empty( $instance['max_posts'] ) ) {
+			$instance['max_posts'] = 10;
+		}
+
+		$after_widget = $args['after_widget'];
+
+		// Override some of the contextually set parameters for bp_has_activities().
+		$args = array(
+			'action'     => 'new_blog_post',
+			'max'        => $instance['max_posts'],
+			'per_page'   => $instance['max_posts'],
+			'user_id'    => 0,
+			'scope'      => false,
+			'object'     => false,
+			'primary_id' => false
+		);
+
+		// Back up global.
+		$old_activities_template = $activities_template;
+
+		?>
+
+		<?php if ( bp_has_activities( $args ) ) : ?>
+
+			<ul id="blog-post-list" class="activity-list item-list">
+
+				<?php while ( bp_activities() ) : bp_the_activity(); ?>
+
+					<li>
+						<div class="activity-content" style="margin: 0">
+							<div class="activity-header"><?php bp_activity_action(); ?></div>
+
+							<?php if ( bp_get_activity_content_body() ) : ?>
+
+								<div class="activity-inner"><?php bp_activity_content_body(); ?></div>
+
+							<?php endif; ?>
+
+						</div>
+					</li>
+
+				<?php endwhile; ?>
+
+			</ul>
+
+		<?php else : ?>
+
+			<div id="message" class="info">
+				<p><?php _e( 'Sorry, there were no posts found. Why not write one?', 'buddypress' ); ?></p>
+			</div>
+
+		<?php endif; ?>
+
+		<?php echo $after_widget;
+
+		// Restore the global.
+		$activities_template = $old_activities_template;
+	}
+
+	/**
+	 * Update the networkwide posts widget options.
+	 *
+	 * @param array $new_instance The new instance options.
+	 * @param array $old_instance The old instance options.
+	 * @return array $instance The parsed options to be saved.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance               = $old_instance;
+		$instance['title']      = strip_tags( $new_instance['title'] );
+		$instance['max_posts']  = strip_tags( $new_instance['max_posts'] );
+		$instance['link_title'] = (bool) $new_instance['link_title'];
+
+		return $instance;
+	}
+
+	/**
+	 * Output the networkwide posts widget options form.
+	 *
+	 * @param array $instance Settings for this widget.
+	 *
+	 * @return void
+	 */
+	public function form( $instance ) {
+		$instance = wp_parse_args( (array) $instance, array(
+			'title'      => __( 'Recent Networkwide Posts', 'buddypress' ),
+			'max_posts'  => 10,
+			'link_title' => false,
+		) );
+
+		$title      = strip_tags( $instance['title'] );
+		$max_posts  = strip_tags( $instance['max_posts'] );
+		$link_title = (bool) $instance['link_title'];
+
+		?>
+
+		<p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _ex( 'Title:', 'Label for the Title field of the Recent Networkwide Posts widget', 'buddypress' ); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" style="width: 100%;" /></label></p>
+		<p><label for="<?php echo $this->get_field_id( 'link_title' ); ?>"><input type="checkbox" name="<?php echo $this->get_field_name( 'link_title' ); ?>" value="1" <?php checked( $link_title ); ?> /> <?php _e( 'Link widget title to Blogs directory', 'buddypress' ); ?></label></p>
+		<p><label for="<?php echo $this->get_field_id( 'max_posts' ); ?>"><?php _e( 'Max posts to show:', 'buddypress' ); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'max_posts' ); ?>" name="<?php echo $this->get_field_name( 'max_posts' ); ?>" type="text" value="<?php echo esc_attr( $max_posts ); ?>" style="width: 30%" /></label></p>
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-template.php	(revision 12265)
@@ -0,0 +1,246 @@
+<?php
+/**
+ * BuddyPress Blogs Template Class.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main blog template loop class.
+ *
+ * Responsible for loading a group of blogs into a loop for display.
+ */
+class BP_Blogs_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @var int
+	 */
+	public $current_blog = -1;
+
+	/**
+	 * The number of blogs returned by the paged query.
+	 *
+	 * @var int
+	 */
+	public $blog_count = 0;
+
+	/**
+	 * Array of blogs located by the query..
+	 *
+	 * @var array
+	 */
+	public $blogs = array();
+
+	/**
+	 * The blog object currently being iterated on.
+	 *
+	 * @var object
+	 */
+	public $blog;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @var bool
+	 */
+	public $in_the_loop = false;
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @var int
+	 */
+	public $pag_page = 1;
+
+	/**
+	 * The number of items being requested per page.
+	 *
+	 * @var int
+	 */
+	public $pag_num = 20;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @var string
+	 */
+	public $pag_links = '';
+
+	/**
+	 * The total number of blogs matching the query parameters.
+	 *
+	 * @var int
+	 */
+	public $total_blog_count = 0;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @see BP_Blogs_Blog::get() for a description of parameters.
+	 *
+	 * @param string     $type              See {@link BP_Blogs_Blog::get()}.
+	 * @param string     $page              See {@link BP_Blogs_Blog::get()}.
+	 * @param string     $per_page          See {@link BP_Blogs_Blog::get()}.
+	 * @param string     $max               See {@link BP_Blogs_Blog::get()}.
+	 * @param string     $user_id           See {@link BP_Blogs_Blog::get()}.
+	 * @param string     $search_terms      See {@link BP_Blogs_Blog::get()}.
+	 * @param string     $page_arg          The string used as a query parameter in
+	 *                                      pagination links. Default: 'bpage'.
+	 * @param bool       $update_meta_cache Whether to pre-fetch metadata for
+	 *                                      queried blogs.
+	 * @param array|bool $include_blog_ids  Array of blog IDs to include.
+	 */
+	public function __construct( $type, $page, $per_page, $max, $user_id, $search_terms, $page_arg = 'bpage', $update_meta_cache = true, $include_blog_ids = false ) {
+
+		$this->pag_arg  = sanitize_key( $page_arg );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $page     );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $per_page );
+
+		// Backwards compatibility support for blogs by first letter.
+		if ( ! empty( $_REQUEST['letter'] ) ) {
+			$this->blogs = BP_Blogs_Blog::get_by_letter( $_REQUEST['letter'], $this->pag_num, $this->pag_page );
+
+		// Typical blogs query.
+		} else {
+			$this->blogs = bp_blogs_get_blogs( array(
+				'type'              => $type,
+				'per_page'          => $this->pag_num,
+				'page'              => $this->pag_page,
+				'user_id'           => $user_id,
+				'search_terms'      => $search_terms,
+				'update_meta_cache' => $update_meta_cache,
+				'include_blog_ids'  => $include_blog_ids,
+			) );
+		}
+
+		// Set the total blog count.
+		if ( empty( $max ) || ( $max >= (int) $this->blogs['total'] ) ) {
+			$this->total_blog_count = (int) $this->blogs['total'];
+		} else {
+			$this->total_blog_count = (int) $max;
+		}
+
+		// Set the blogs array (to loop through later.
+		$this->blogs = $this->blogs['blogs'];
+
+		// Get the current blog count to compare maximum against.
+		$blog_count = count( $this->blogs );
+
+		// Set the current blog count.
+		if ( empty( $max ) || ( $max >= (int) $blog_count ) ) {
+			$this->blog_count = (int) $blog_count;
+		} else {
+			$this->blog_count = (int) $max;
+		}
+
+		// Build pagination links based on total blogs and current page number.
+		if ( ! empty( $this->total_blog_count ) && ! empty( $this->pag_num ) ) {
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $this->pag_arg, '%#%' ),
+				'format'    => '',
+				'total'     => ceil( (int) $this->total_blog_count / (int) $this->pag_num ),
+				'current'   => (int) $this->pag_page,
+				'prev_text' => _x( '&larr;', 'Blog pagination previous text', 'buddypress' ),
+				'next_text' => _x( '&rarr;', 'Blog pagination next text',     'buddypress' ),
+				'mid_size'  => 1,
+				'add_args'  => array(),
+			) );
+		}
+	}
+
+	/**
+	 * Whether there are blogs available in the loop.
+	 *
+	 * @see bp_has_blogs()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	public function has_blogs() {
+		return (bool) ! empty( $this->blog_count );
+	}
+
+	/**
+	 * Set up the next blog and iterate index.
+	 *
+	 * @return object The next blog to iterate over.
+	 */
+	public function next_blog() {
+		$this->current_blog++;
+		$this->blog = $this->blogs[ $this->current_blog ];
+
+		return $this->blog;
+	}
+
+	/**
+	 * Rewind the blogs and reset blog index.
+	 */
+	public function rewind_blogs() {
+		$this->current_blog = -1;
+		if ( $this->blog_count > 0 ) {
+			$this->blog = $this->blogs[0];
+		}
+	}
+
+	/**
+	 * Whether there are blogs left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_blogs()} as part of the while loop
+	 * that controls iteration inside the blogs loop, eg:
+	 *     while ( bp_blogs() ) { ...
+	 *
+	 * @see bp_blogs()
+	 *
+	 * @return bool True if there are more blogs to show, otherwise false.
+	 */
+	public function blogs() {
+		if ( ( $this->current_blog + 1 ) < $this->blog_count ) {
+			return true;
+		} elseif ( ( $this->current_blog + 1 ) === $this->blog_count ) {
+
+			/**
+			 * Fires right before the rewinding of blogs listing after all are shown.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'blog_loop_end' );
+			// Do some cleaning up after the loop.
+			$this->rewind_blogs();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Set up the current blog inside the loop.
+	 *
+	 * Used by {@link bp_the_blog()} to set up the current blog data while
+	 * looping, so that template tags used during that iteration make
+	 * reference to the current blog.
+	 *
+	 * @see bp_the_blog()
+	 */
+	public function the_blog() {
+
+		$this->in_the_loop = true;
+		$this->blog        = $this->next_blog();
+
+		// Loop has just started.
+		if ( 0 === $this->current_blog ) {
+
+			/**
+			 * Fires if on the first blog in the loop.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'blog_loop_start' );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-theme-compat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-theme-compat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/classes/class-bp-blogs-theme-compat.php	(revision 12265)
@@ -0,0 +1,201 @@
+<?php
+/**
+ * BuddyPress Blogs Theme Compatibility.
+ *
+ * @package BuddyPress
+ * @subpackage BlogsScreens
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main theme compat class for BuddyPress Blogs.
+ *
+ * This class sets up the necessary theme compatibility actions to safely output
+ * group template parts to the_title and the_content areas of a theme.
+ *
+ * @since 1.7.0
+ */
+class BP_Blogs_Theme_Compat {
+
+	/**
+	 * Set up theme compatibility for the Blogs component.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __construct() {
+		add_action( 'bp_setup_theme_compat', array( $this, 'is_blogs' ) );
+	}
+
+	/**
+	 * Are we looking at something that needs Blogs theme compatibility?
+	 *
+	 * @since 1.7.0
+	 */
+	public function is_blogs() {
+
+		// Bail if not looking at a group.
+		if ( ! bp_is_blogs_component() )
+			return;
+
+		// Bail if looking at a users sites.
+		if ( bp_is_user() )
+			return;
+
+		// Blog Directory.
+		if ( is_multisite() && ! bp_current_action() ) {
+			bp_update_is_directory( true, 'blogs' );
+
+			/**
+			 * Fires if in the blog directory and BuddyPress needs Blog theme compatibility,
+			 * before the actions and filters are added.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_blogs_screen_index' );
+
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'directory_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'directory_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'directory_content'    ) );
+
+		// Create blog.
+		} elseif ( is_user_logged_in() && bp_blog_signup_enabled() ) {
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'create_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'create_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'create_content'    ) );
+		}
+	}
+
+	/** Directory *************************************************************/
+
+	/**
+	 * Add template hierarchy to theme compat for the blog directory page.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function directory_template_hierarchy( $templates ) {
+
+		/**
+		 * Filters the custom templates used for theme compat with the blog directory page.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of template paths to add to template list to look for.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_blogs_create', array(
+			'blogs/index-directory.php'
+		) );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with directory data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_dummy_post() {
+
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => __( 'Sites', 'buddypress' ),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the groups index template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_content() {
+		return bp_buffer_template_part( 'blogs/index', null, false );
+	}
+
+	/** Create ****************************************************************/
+
+	/**
+	 * Add custom template hierarchy to theme compat for the blog create page.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function create_template_hierarchy( $templates ) {
+
+		/**
+		 * Filters the custom templates used for theme compat with the blog create page.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of template paths to add to template list to look for.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_blogs_create', array(
+			'blogs/index-create.php'
+		) );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with create screen data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function create_dummy_post() {
+
+		// Title based on ability to create blogs.
+		if ( is_user_logged_in() && bp_blog_signup_enabled() ) {
+			$title = __( 'Create a Site', 'buddypress' );
+		} else {
+			$title = __( 'Sites', 'buddypress' );
+		}
+
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => $title,
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the create screen template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function create_content() {
+		return bp_buffer_template_part( 'blogs/create', null, false );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-blogs/screens/create.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/screens/create.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/screens/create.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Blogs: Create screen handler
+ *
+ * @package BuddyPress
+ * @subpackage BlogsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the "Create a Blog" screen.
+ *
+ * @since 1.0.0
+ */
+function bp_blogs_screen_create_a_blog() {
+
+	if ( !is_multisite() ||  !bp_is_blogs_component() || !bp_is_current_action( 'create' ) )
+		return false;
+
+	if ( !is_user_logged_in() || !bp_blog_signup_enabled() )
+		return false;
+
+	/**
+	 * Fires right before the loading of the Create A Blog screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_blogs_screen_create_a_blog' );
+
+	bp_core_load_template( apply_filters( 'bp_blogs_template_create_a_blog', 'blogs/create' ) );
+}
+add_action( 'bp_screens', 'bp_blogs_screen_create_a_blog', 3 );
Index: /tags/4.0.0-beta1/src/bp-blogs/screens/directory.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/screens/directory.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/screens/directory.php	(revision 12265)
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Blogs: Directory screen handler
+ *
+ * @package BuddyPress
+ * @subpackage BlogsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the top-level Blogs directory.
+ *
+ * @since 1.5-beta-1
+ */
+function bp_blogs_screen_index() {
+	if ( bp_is_blogs_directory() ) {
+		bp_update_is_directory( true, 'blogs' );
+
+		/**
+		 * Fires right before the loading of the top-level Blogs screen template file.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'bp_blogs_screen_index' );
+
+		bp_core_load_template( apply_filters( 'bp_blogs_screen_index', 'blogs/index' ) );
+	}
+}
+add_action( 'bp_screens', 'bp_blogs_screen_index', 2 );
Index: /tags/4.0.0-beta1/src/bp-blogs/screens/my-blogs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-blogs/screens/my-blogs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-blogs/screens/my-blogs.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Blogs: User's "Sites" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage BlogsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the "My Blogs" screen.
+ *
+ * @since 1.0.0
+ */
+function bp_blogs_screen_my_blogs() {
+	if ( !is_multisite() )
+		return false;
+
+	/**
+	 * Fires right before the loading of the My Blogs screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_blogs_screen_my_blogs' );
+
+	bp_core_load_template( apply_filters( 'bp_blogs_template_my_blogs', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-actions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-actions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-actions.php	(revision 12265)
@@ -0,0 +1,236 @@
+<?php
+/**
+ * BuddyPress Admin Actions.
+ *
+ * This file contains the actions that are used through-out BuddyPress Admin. They
+ * are consolidated here to make searching for them easier, and to help developers
+ * understand at a glance the order in which things occur.
+ *
+ * There are a few common places that additional actions can currently be found.
+ *
+ *  - BuddyPress: In {@link BuddyPress::setup_actions()} in BuddyPress.php
+ *  - Admin: More in {@link bp_Admin::setup_actions()} in admin.php
+ *
+ * @package BuddyPress
+ * @subpackage Admin
+ * @since 2.3.0
+ * @see bp-core-actions.php
+ * @see bp-core-filters.php
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Attach BuddyPress to WordPress.
+ *
+ * BuddyPress uses its own internal actions to help aid in third-party plugin
+ * development, and to limit the amount of potential future code changes when
+ * updates to WordPress core occur.
+ *
+ * These actions exist to create the concept of 'plugin dependencies'. They
+ * provide a safe way for plugins to execute code *only* when BuddyPress is
+ * installed and activated, without needing to do complicated guesswork.
+ *
+ * For more information on how this works, see the 'Plugin Dependency' section
+ * near the bottom of this file.
+ *
+ *          v--WordPress Actions       v--BuddyPress Sub-actions
+ */
+add_action( 'admin_menu',                         'bp_admin_menu'                    );
+add_action( 'admin_init',                         'bp_admin_init'                    );
+add_action( 'admin_head',                         'bp_admin_head'                    );
+add_action( 'admin_notices',                      'bp_admin_notices'                 );
+add_action( 'admin_enqueue_scripts',              'bp_admin_enqueue_scripts'         );
+add_action( 'customize_controls_enqueue_scripts', 'bp_admin_enqueue_scripts', 8      );
+add_action( 'network_admin_menu',                 'bp_admin_menu'                    );
+add_action( 'custom_menu_order',                  'bp_admin_custom_menu_order'       );
+add_action( 'menu_order',                         'bp_admin_menu_order'              );
+add_action( 'wpmu_new_blog',                      'bp_new_site',               10, 6 );
+
+// Hook on to admin_init.
+add_action( 'bp_admin_init', 'bp_setup_updater',          1000 );
+add_action( 'bp_admin_init', 'bp_core_activation_notice', 1010 );
+add_action( 'bp_admin_init', 'bp_register_importers'           );
+add_action( 'bp_admin_init', 'bp_register_admin_style'         );
+add_action( 'bp_admin_init', 'bp_register_admin_settings'      );
+add_action( 'bp_admin_init', 'bp_do_activation_redirect', 1    );
+
+// Add a new separator.
+add_action( 'bp_admin_menu', 'bp_admin_separator' );
+
+/**
+ * When a new site is created in a multisite installation, run the activation
+ * routine on that site.
+ *
+ * @since 1.7.0
+ *
+ * @param int    $blog_id ID of the blog being installed to.
+ * @param int    $user_id ID of the user the install is for.
+ * @param string $domain  Domain to use with the install.
+ * @param string $path    Path to use with the install.
+ * @param int    $site_id ID of the site being installed to.
+ * @param array  $meta    Metadata to use with the site creation.
+ */
+function bp_new_site( $blog_id, $user_id, $domain, $path, $site_id, $meta ) {
+
+	// Bail if plugin is not network activated.
+	if ( ! is_plugin_active_for_network( buddypress()->basename ) )
+		return;
+
+	// Switch to the new blog.
+	switch_to_blog( $blog_id );
+
+	/**
+	 * Fires the activation routine for a new site created in a multisite installation.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param int    $blog_id ID of the blog being installed to.
+	 * @param int    $user_id ID of the user the install is for.
+	 * @param string $domain  Domain to use with the install.
+	 * @param string $path    Path to use with the install.
+	 * @param int    $site_id ID of the site being installed to.
+	 * @param array  $meta    Metadata to use with the site creation.
+	 */
+	do_action( 'bp_new_site', $blog_id, $user_id, $domain, $path, $site_id, $meta );
+
+	// Restore original blog.
+	restore_current_blog();
+}
+
+/** Sub-Actions ***************************************************************/
+
+/**
+ * Piggy back admin_init action.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_admin_init() {
+
+	/**
+	 * Fires inside the bp_admin_init function.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_admin_init' );
+}
+
+/**
+ * Piggy back admin_menu action.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_admin_menu() {
+
+	/**
+	 * Fires inside the bp_admin_menu function.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_admin_menu' );
+}
+
+/**
+ * Piggy back admin_head action.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_admin_head() {
+
+	/**
+	 * Fires inside the bp_admin_head function.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_admin_head' );
+}
+
+/**
+ * Piggy back admin_notices action.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_admin_notices() {
+
+	/**
+	 * Fires inside the bp_admin_notices function.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_admin_notices' );
+}
+
+/**
+ * Piggy back admin_enqueue_scripts action.
+ *
+ * @since 1.7.0
+ *
+ * @param string $hook_suffix The current admin page, passed to
+ *                            'admin_enqueue_scripts'.
+ */
+function bp_admin_enqueue_scripts( $hook_suffix = '' ) {
+
+	/**
+	 * Fires inside the bp_admin_enqueue_scripts function.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $hook_suffix The current admin page, passed to admin_enqueue_scripts.
+	 */
+	do_action( 'bp_admin_enqueue_scripts', $hook_suffix );
+}
+
+/**
+ * Dedicated action to register BuddyPress importers.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_register_importers() {
+
+	/**
+	 * Fires inside the bp_register_importers function.
+	 *
+	 * Used to register a BuddyPress importer.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_register_importers' );
+}
+
+/**
+ * Dedicated action to register admin styles.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_register_admin_style() {
+
+	/**
+	 * Fires inside the bp_register_admin_style function.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_register_admin_style' );
+}
+
+/**
+ * Dedicated action to register admin settings.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_register_admin_settings() {
+
+	/**
+	 * Fires inside the bp_register_admin_settings function.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_register_admin_settings' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-components.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-components.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-components.php	(revision 12265)
@@ -0,0 +1,376 @@
+<?php
+/**
+ * BuddyPress Admin Component Functions.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Renders the Component Setup admin panel.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_core_admin_components_settings() {
+?>
+
+	<div class="wrap">
+
+		<h1><?php _e( 'BuddyPress Settings', 'buddypress' ); ?> </h1>
+
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Components', 'buddypress' ) ); ?></h2>
+		<form action="" method="post" id="bp-admin-component-form">
+
+			<?php bp_core_admin_components_options(); ?>
+
+			<p class="submit clear">
+				<input class="button-primary" type="submit" name="bp-admin-component-submit" id="bp-admin-component-submit" value="<?php esc_attr_e( 'Save Settings', 'buddypress' ) ?>"/>
+			</p>
+
+			<?php wp_nonce_field( 'bp-admin-component-setup' ); ?>
+
+		</form>
+	</div>
+
+<?php
+}
+
+/**
+ * Creates reusable markup for component setup on the Components and Pages dashboard panel.
+ *
+ * @since 1.6.0
+ *
+ * @todo Use settings API
+ */
+function bp_core_admin_components_options() {
+
+	// Declare local variables.
+	$deactivated_components = array();
+
+	/**
+	 * Filters the array of available components.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param mixed $value Active components.
+	 */
+	$active_components      = apply_filters( 'bp_active_components', bp_get_option( 'bp-active-components' ) );
+
+	// The default components (if none are previously selected).
+	$default_components = array(
+		'xprofile' => array(
+			'title'       => __( 'Extended Profiles', 'buddypress' ),
+			'description' => __( 'Customize your community with fully editable profile fields that allow your users to describe themselves.', 'buddypress' )
+		),
+		'settings' => array(
+			'title'       => __( 'Account Settings', 'buddypress' ),
+			'description' => __( 'Allow your users to modify their account and notification settings directly from within their profiles.', 'buddypress' )
+		),
+		'notifications' => array(
+			'title'       => __( 'Notifications', 'buddypress' ),
+			'description' => __( 'Notify members of relevant activity with a toolbar bubble and/or via email, and allow them to customize their notification settings.', 'buddypress' )
+		),
+	);
+
+	$optional_components = bp_core_admin_get_components( 'optional' );
+	$required_components = bp_core_admin_get_components( 'required' );
+	$retired_components  = bp_core_admin_get_components( 'retired'  );
+
+	// Merge optional and required together.
+	$all_components = $optional_components + $required_components;
+
+	// If this is an upgrade from before BuddyPress 1.5, we'll have to convert
+	// deactivated components into activated ones.
+	if ( empty( $active_components ) ) {
+		$deactivated_components = bp_get_option( 'bp-deactivated-components' );
+		if ( !empty( $deactivated_components ) ) {
+
+			// Trim off namespace and filename.
+			$trimmed = array();
+			foreach ( array_keys( (array) $deactivated_components ) as $component ) {
+				$trimmed[] = str_replace( '.php', '', str_replace( 'bp-', '', $component ) );
+			}
+
+			// Loop through the optional components to create an active component array.
+			foreach ( array_keys( (array) $optional_components ) as $ocomponent ) {
+				if ( !in_array( $ocomponent, $trimmed ) ) {
+					$active_components[$ocomponent] = 1;
+				}
+			}
+		}
+	}
+
+	// On new install, set active components to default.
+	if ( empty( $active_components ) ) {
+		$active_components = $default_components;
+	}
+
+	// Core component is always active.
+	$active_components['core'] = $all_components['core'];
+	$inactive_components       = array_diff( array_keys( $all_components ) , array_keys( $active_components ) );
+
+	/** Display **************************************************************
+	 */
+
+	// Get the total count of all plugins.
+	$all_count = count( $all_components );
+	$page      = bp_core_do_network_admin()  ? 'settings.php' : 'options-general.php';
+	$action    = !empty( $_GET['action'] ) ? $_GET['action'] : 'all';
+
+	switch( $action ) {
+		case 'all' :
+			$current_components = $all_components;
+			break;
+		case 'active' :
+			foreach ( array_keys( $active_components ) as $component ) {
+				$current_components[$component] = $all_components[$component];
+			}
+			break;
+		case 'inactive' :
+			foreach ( $inactive_components as $component ) {
+				$current_components[$component] = $all_components[$component];
+			}
+			break;
+		case 'mustuse' :
+			$current_components = $required_components;
+			break;
+		case 'retired' :
+			$current_components = $retired_components;
+			break;
+	} ?>
+
+	<h3 class="screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Filter components list', 'buddypress' );
+	?></h3>
+
+	<ul class="subsubsub">
+		<li><a href="<?php echo esc_url( add_query_arg( array( 'page' => 'bp-components', 'action' => 'all'      ), bp_get_admin_url( $page ) ) ); ?>" <?php if ( $action === 'all'      ) : ?>class="current"<?php endif; ?>><?php printf( _nx( 'All <span class="count">(%s)</span>',      'All <span class="count">(%s)</span>',      $all_count,         'plugins', 'buddypress' ), number_format_i18n( $all_count                    ) ); ?></a> | </li>
+		<li><a href="<?php echo esc_url( add_query_arg( array( 'page' => 'bp-components', 'action' => 'active'   ), bp_get_admin_url( $page ) ) ); ?>" <?php if ( $action === 'active'   ) : ?>class="current"<?php endif; ?>><?php printf( _n(  'Active <span class="count">(%s)</span>',   'Active <span class="count">(%s)</span>',   count( $active_components   ), 'buddypress' ), number_format_i18n( count( $active_components   ) ) ); ?></a> | </li>
+		<li><a href="<?php echo esc_url( add_query_arg( array( 'page' => 'bp-components', 'action' => 'inactive' ), bp_get_admin_url( $page ) ) ); ?>" <?php if ( $action === 'inactive' ) : ?>class="current"<?php endif; ?>><?php printf( _n(  'Inactive <span class="count">(%s)</span>', 'Inactive <span class="count">(%s)</span>', count( $inactive_components ), 'buddypress' ), number_format_i18n( count( $inactive_components ) ) ); ?></a> | </li>
+		<li><a href="<?php echo esc_url( add_query_arg( array( 'page' => 'bp-components', 'action' => 'mustuse'  ), bp_get_admin_url( $page ) ) ); ?>" <?php if ( $action === 'mustuse'  ) : ?>class="current"<?php endif; ?>><?php printf( _n(  'Must-Use <span class="count">(%s)</span>', 'Must-Use <span class="count">(%s)</span>', count( $required_components ), 'buddypress' ), number_format_i18n( count( $required_components ) ) ); ?></a> | </li>
+		<li><a href="<?php echo esc_url( add_query_arg( array( 'page' => 'bp-components', 'action' => 'retired'  ), bp_get_admin_url( $page ) ) ); ?>" <?php if ( $action === 'retired'  ) : ?>class="current"<?php endif; ?>><?php printf( _n(  'Retired <span class="count">(%s)</span>',  'Retired <span class="count">(%s)</span>',  count( $retired_components ),  'buddypress' ), number_format_i18n( count( $retired_components  ) ) ); ?></a></li>
+	</ul>
+
+	<h3 class="screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Components list', 'buddypress' );
+	?></h3>
+
+	<table class="wp-list-table widefat plugins">
+		<thead>
+			<tr>
+				<td id="cb" class="manage-column column-cb check-column"><input id="cb-select-all-1" type="checkbox" <?php checked( empty( $inactive_components ) ); ?>>
+					<label class="screen-reader-text" for="cb-select-all-1"><?php
+					/* translators: accessibility text */
+					_e( 'Enable or disable all optional components in bulk', 'buddypress' );
+				?></label></td>
+				<th scope="col" id="name" class="manage-column column-title column-primary"><?php _e( 'Component', 'buddypress' ); ?></th>
+				<th scope="col" id="description" class="manage-column column-description"><?php _e( 'Description', 'buddypress' ); ?></th>
+			</tr>
+		</thead>
+
+		<tbody id="the-list">
+
+			<?php if ( !empty( $current_components ) ) : ?>
+
+				<?php foreach ( $current_components as $name => $labels ) : ?>
+
+					<?php if ( !in_array( $name, array( 'core', 'members' ) ) ) :
+						$class = isset( $active_components[esc_attr( $name )] ) ? 'active' : 'inactive';
+					else :
+						$class = 'active';
+					endif; ?>
+
+					<tr id="<?php echo esc_attr( $name ); ?>" class="<?php echo esc_attr( $name ) . ' ' . esc_attr( $class ); ?>">
+						<th scope="row" class="check-column">
+
+							<?php if ( !in_array( $name, array( 'core', 'members' ) ) ) : ?>
+
+								<input type="checkbox" id="<?php echo esc_attr( "bp_components[$name]" ); ?>" name="<?php echo esc_attr( "bp_components[$name]" ); ?>" value="1"<?php checked( isset( $active_components[esc_attr( $name )] ) ); ?> /><label for="<?php echo esc_attr( "bp_components[$name]" ); ?>" class="screen-reader-text"><?php
+									/* translators: accessibility text */
+									printf( __( 'Select %s', 'buddypress' ), esc_html( $labels['title'] ) ); ?></label>
+
+							<?php endif; ?>
+
+						</th>
+						<td class="plugin-title column-primary">
+							<label for="<?php echo esc_attr( "bp_components[$name]" ); ?>">
+								<span aria-hidden="true"></span>
+								<strong><?php echo esc_html( $labels['title'] ); ?></strong>
+							</label>
+						</td>
+
+						<td class="column-description desc">
+							<div class="plugin-description">
+								<p><?php echo $labels['description']; ?></p>
+							</div>
+
+						</td>
+					</tr>
+
+				<?php endforeach ?>
+
+			<?php else : ?>
+
+				<tr class="no-items">
+					<td class="colspanchange" colspan="3"><?php _e( 'No components found.', 'buddypress' ); ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+		</tbody>
+
+		<tfoot>
+			<tr>
+				<td class="manage-column column-cb check-column"><input id="cb-select-all-2" type="checkbox" <?php checked( empty( $inactive_components ) ); ?>>
+					<label class="screen-reader-text" for="cb-select-all-2"><?php
+					/* translators: accessibility text */
+					_e( 'Enable or disable all optional components in bulk', 'buddypress' );
+				?></label></td>
+				<th class="manage-column column-title column-primary"><?php _e( 'Component', 'buddypress' ); ?></th>
+				<th class="manage-column column-description"><?php _e( 'Description', 'buddypress' ); ?></th>
+			</tr>
+		</tfoot>
+
+	</table>
+
+	<input type="hidden" name="bp_components[members]" value="1" />
+
+	<?php
+}
+
+/**
+ * Handle saving the Component settings.
+ *
+ * @since 1.6.0
+ *
+ * @todo Use settings API when it supports saving network settings
+ */
+function bp_core_admin_components_settings_handler() {
+
+	// Bail if not saving settings.
+	if ( ! isset( $_POST['bp-admin-component-submit'] ) )
+		return;
+
+	// Bail if nonce fails.
+	if ( ! check_admin_referer( 'bp-admin-component-setup' ) )
+		return;
+
+	// Settings form submitted, now save the settings. First, set active components.
+	if ( isset( $_POST['bp_components'] ) ) {
+
+		// Load up BuddyPress.
+		$bp = buddypress();
+
+		// Save settings and upgrade schema.
+		require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
+		require_once( $bp->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
+
+		$submitted = stripslashes_deep( $_POST['bp_components'] );
+		$bp->active_components = bp_core_admin_get_active_components_from_submitted_settings( $submitted );
+
+		bp_core_install( $bp->active_components );
+		bp_core_add_page_mappings( $bp->active_components );
+		bp_update_option( 'bp-active-components', $bp->active_components );
+	}
+
+	// Where are we redirecting to?
+	$base_url = bp_get_admin_url( add_query_arg( array( 'page' => 'bp-components', 'updated' => 'true' ), 'admin.php' ) );
+
+	// Redirect.
+	wp_redirect( $base_url );
+	die();
+}
+add_action( 'bp_admin_init', 'bp_core_admin_components_settings_handler' );
+
+/**
+ * Calculates the components that should be active after save, based on submitted settings.
+ *
+ * The way that active components must be set after saving your settings must
+ * be calculated differently depending on which of the Components subtabs you
+ * are coming from:
+ * - When coming from All or Active, the submitted checkboxes accurately
+ *   reflect the desired active components, so we simply pass them through
+ * - When coming from Inactive, components can only be activated - already
+ *   active components will not be passed in the $_POST global. Thus, we must
+ *   parse the newly activated components with the already active components
+ *   saved in the $bp global
+ * - When activating a Retired component, the situation is similar to Inactive.
+ * - When deactivating a Retired component, no value is passed in the $_POST
+ *   global (because the component settings are checkboxes). So, in order to
+ *   determine whether a retired component is being deactivated, we retrieve a
+ *   list of retired components, and check each one to ensure that its checkbox
+ *   is not present, before merging the submitted components with the active
+ *   ones.
+ *
+ * @since 1.7.0
+ *
+ * @param array $submitted This is the array of component settings coming from the POST
+ *                         global. You should stripslashes_deep() before passing to this function.
+ * @return array The calculated list of component settings
+ */
+function bp_core_admin_get_active_components_from_submitted_settings( $submitted ) {
+	$current_action = 'all';
+
+	if ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'active', 'inactive', 'retired' ) ) ) {
+		$current_action = $_GET['action'];
+	}
+
+	$current_components = buddypress()->active_components;
+
+	switch ( $current_action ) {
+		case 'retired' :
+			$retired_components = bp_core_admin_get_components( 'retired' );
+			foreach ( array_keys( $retired_components ) as $retired_component ) {
+				if ( ! isset( $submitted[ $retired_component ] ) ) {
+					unset( $current_components[ $retired_component ] );
+				}
+			} // Fall through.
+
+
+		case 'inactive' :
+			$components = array_merge( $submitted, $current_components );
+			break;
+
+		case 'all' :
+		case 'active' :
+		default :
+			$components = $submitted;
+			break;
+	}
+
+	return $components;
+}
+
+/**
+ * Return a list of component information.
+ *
+ * We use this information both to build the markup for the admin screens, as
+ * well as to do some processing on settings data submitted from those screens.
+ *
+ * @since 1.7.0
+ *
+ * @param string $type Optional; component type to fetch. Default value is 'all', or 'optional', 'retired', 'required'.
+ * @return array Requested components' data.
+ */
+function bp_core_admin_get_components( $type = 'all' ) {
+	$components = bp_core_get_components( $type );
+
+	/**
+	 * Filters the list of component information.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array  $components Array of component information.
+	 * @param string $type       Type of component list requested.
+	 *                           Possible values include 'all', 'optional',
+	 *                           'retired', 'required'.
+	 */
+	return apply_filters( 'bp_core_admin_get_components', $components, $type );
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-functions.php	(revision 12265)
@@ -0,0 +1,1137 @@
+<?php
+/**
+ * BuddyPress Common Admin Functions.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Menu **********************************************************************/
+
+/**
+ * Initializes the wp-admin area "BuddyPress" menus and sub menus.
+ *
+ */
+function bp_core_admin_menu_init() {
+	add_action( bp_core_admin_hook(), 'bp_core_add_admin_menu', 9 );
+}
+
+/**
+ * In BP 1.6, the top-level admin menu was removed. For backpat, this function
+ * keeps the top-level menu if a plugin has registered a menu into the old
+ * 'bp-general-settings' menu.
+ *
+ * The old "bp-general-settings" page was renamed "bp-components".
+ *
+ * @global array $_parent_pages
+ * @global array $_registered_pages
+ * @global array $submenu
+ *
+ * @since 1.6.0
+ */
+function bp_core_admin_backpat_menu() {
+	global $_parent_pages, $_registered_pages, $submenu;
+
+	// If there's no bp-general-settings menu (perhaps because the current
+	// user is not an Administrator), there's nothing to do here.
+	if ( ! isset( $submenu['bp-general-settings'] ) ) {
+		return;
+	}
+
+	/**
+	 * By default, only the core "Help" submenu is added under the top-level BuddyPress menu.
+	 * This means that if no third-party plugins have registered their admin pages into the
+	 * 'bp-general-settings' menu, it will only contain one item. Kill it.
+	 */
+	if ( 1 != count( $submenu['bp-general-settings'] ) ) {
+		return;
+	}
+
+	// This removes the top-level menu.
+	remove_submenu_page( 'bp-general-settings', 'bp-general-settings' );
+	remove_menu_page( 'bp-general-settings' );
+
+	// These stop people accessing the URL directly.
+	unset( $_parent_pages['bp-general-settings'] );
+	unset( $_registered_pages['toplevel_page_bp-general-settings'] );
+}
+add_action( bp_core_admin_hook(), 'bp_core_admin_backpat_menu', 999 );
+
+/**
+ * This tells WP to highlight the Settings > BuddyPress menu item,
+ * regardless of which actual BuddyPress admin screen we are on.
+ *
+ * The conditional prevents the behaviour when the user is viewing the
+ * backpat "Help" page, the Activity page, or any third-party plugins.
+ *
+ * @global string $plugin_page
+ * @global array $submenu
+ *
+ * @since 1.6.0
+ */
+function bp_core_modify_admin_menu_highlight() {
+	global $plugin_page, $submenu_file;
+
+	// This tweaks the Settings subnav menu to show only one BuddyPress menu item.
+	if ( ! in_array( $plugin_page, array( 'bp-activity', 'bp-general-settings', ) ) ) {
+		$submenu_file = 'bp-components';
+	}
+
+	// Network Admin > Tools.
+	if ( in_array( $plugin_page, array( 'bp-tools', 'available-tools' ) ) ) {
+		$submenu_file = $plugin_page;
+	}
+}
+
+/**
+ * Generates markup for a fallback top-level BuddyPress menu page, if the site is running
+ * a legacy plugin which hasn't been updated. If the site is up to date, this page
+ * will never appear.
+ *
+ * @see bp_core_admin_backpat_menu()
+ *
+ * @since 1.6.0
+ *
+ * @todo Add convenience links into the markup once new positions are finalised.
+ */
+function bp_core_admin_backpat_page() {
+	$url          = bp_core_do_network_admin() ? network_admin_url( 'settings.php' ) : admin_url( 'options-general.php' );
+	$settings_url = add_query_arg( 'page', 'bp-components', $url ); ?>
+
+	<div class="wrap">
+		<h2><?php _e( 'Why have all my BuddyPress menus disappeared?', 'buddypress' ); ?></h2>
+
+		<p><?php _e( "Don't worry! We've moved the BuddyPress options into more convenient and easier to find locations. You're seeing this page because you are running a legacy BuddyPress plugin which has not been updated.", 'buddypress' ); ?></p>
+		<p><?php printf( __( 'Components, Pages, Settings, and Forums, have been moved to <a href="%s">Settings &gt; BuddyPress</a>. Profile Fields has been moved into the <a href="%s">Users</a> menu.', 'buddypress' ), esc_url( $settings_url ), bp_get_admin_url( 'users.php?page=bp-profile-setup' ) ); ?></p>
+	</div>
+
+	<?php
+}
+
+/** Notices *******************************************************************/
+
+/**
+ * Print admin messages to admin_notices or network_admin_notices.
+ *
+ * BuddyPress combines all its messages into a single notice, to avoid a preponderance of yellow
+ * boxes.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_core_print_admin_notices() {
+
+	// Only the super admin should see messages.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return;
+	}
+
+	// On multisite installs, don't show on a non-root blog, unless
+	// 'do_network_admin' is overridden.
+	if ( is_multisite() && bp_core_do_network_admin() && ! bp_is_root_blog() ) {
+		return;
+	}
+
+	$notice_types = array();
+	foreach ( buddypress()->admin->notices as $notice ) {
+		$notice_types[] = $notice['type'];
+	}
+	$notice_types = array_unique( $notice_types );
+
+	foreach ( $notice_types as $type ) {
+		$notices = wp_list_filter( buddypress()->admin->notices, array( 'type' => $type ) );
+		printf( '<div id="message" class="fade %s">', sanitize_html_class( $type ) );
+
+		foreach ( $notices as $notice ) {
+			printf( '<p>%s</p>', $notice['message'] );
+		}
+
+		printf( '</div>' );
+	}
+}
+add_action( 'admin_notices',         'bp_core_print_admin_notices' );
+add_action( 'network_admin_notices', 'bp_core_print_admin_notices' );
+
+/**
+ * Add an admin notice to the BP queue.
+ *
+ * Messages added with this function are displayed in BuddyPress's general purpose admin notices
+ * box. It is recommended that you hook this function to admin_init, so that your messages are
+ * loaded in time.
+ *
+ * @since 1.5.0
+ *
+ * @param string $notice The notice you are adding to the queue.
+ * @param string $type   The notice type; optional. Usually either "updated" or "error".
+ */
+function bp_core_add_admin_notice( $notice = '', $type = 'updated' ) {
+
+	// Do not add if the notice is empty.
+	if ( empty( $notice ) ) {
+		return;
+	}
+
+	// Double check the object before referencing it.
+	if ( ! isset( buddypress()->admin->notices ) ) {
+		buddypress()->admin->notices = array();
+	}
+
+	// Add the notice.
+	buddypress()->admin->notices[] = array(
+		'message' => $notice,
+		'type'    => $type,
+	);
+}
+
+/**
+ * Verify that some BP prerequisites are set up properly, and notify the admin if not.
+ *
+ * On every Dashboard page, this function checks the following:
+ *   - that pretty permalinks are enabled.
+ *   - that every BP component that needs a WP page for a directory has one.
+ *   - that no WP page has multiple BP components associated with it.
+ * The administrator will be shown a notice for each check that fails.
+ *
+ * @global WPDB $wpdb WordPress DB object
+ * @global WP_Rewrite $wp_rewrite
+ *
+ * @since 1.2.0
+ */
+function bp_core_activation_notice() {
+	global $wp_rewrite, $wpdb;
+
+	// Only the super admin gets warnings.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return;
+	}
+
+	// Bail in user admin.
+	if ( is_user_admin() ) {
+		return;
+	}
+
+	// On multisite installs, don't load on a non-root blog, unless do_network_admin is overridden.
+	if ( is_multisite() && bp_core_do_network_admin() && ! bp_is_root_blog() ) {
+		return;
+	}
+
+	// Bail if in network admin, and BuddyPress is not network activated.
+	if ( is_network_admin() && ! bp_is_network_activated() ) {
+		return;
+	}
+
+	/**
+	 * Check to make sure that the blog setup routine has run. This can't
+	 * happen during the wizard because of the order which the components
+	 * are loaded.
+	 */
+	if ( bp_is_active( 'blogs' ) ) {
+		$bp    = buddypress();
+		$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$bp->blogs->table_name}" );
+
+		if ( empty( $count ) ) {
+			bp_blogs_record_existing_blogs();
+		}
+	}
+
+	// Add notice if no rewrite rules are enabled.
+	if ( empty( $wp_rewrite->permalink_structure ) ) {
+		bp_core_add_admin_notice( sprintf( __( '<strong>BuddyPress is almost ready</strong>. You must <a href="%s">update your permalink structure</a> to something other than the default for it to work.', 'buddypress' ), admin_url( 'options-permalink.php' ) ), 'error' );
+	}
+
+	// Get BuddyPress instance.
+	$bp = buddypress();
+
+	/**
+	 * Check for orphaned BP components (BP component is enabled, no WP page exists).
+	 */
+	$orphaned_components = array();
+	$wp_page_components  = array();
+
+	// Only components with 'has_directory' require a WP page to function.
+	foreach( array_keys( $bp->loaded_components ) as $component_id ) {
+		if ( !empty( $bp->{$component_id}->has_directory ) ) {
+			$wp_page_components[] = array(
+				'id'   => $component_id,
+				'name' => isset( $bp->{$component_id}->name ) ? $bp->{$component_id}->name : ucwords( $bp->{$component_id}->id )
+			);
+		}
+	}
+
+	// Activate and Register are special cases. They are not components but they need WP pages.
+	// If user registration is disabled, we can skip this step.
+	if ( bp_get_signup_allowed() ) {
+		$wp_page_components[] = array(
+			'id'   => 'activate',
+			'name' => __( 'Activate', 'buddypress' )
+		);
+
+		$wp_page_components[] = array(
+			'id'   => 'register',
+			'name' => __( 'Register', 'buddypress' )
+		);
+	}
+
+	// On the first admin screen after a new installation, this isn't set, so grab it to suppress
+	// a misleading error message.
+	if ( empty( $bp->pages->members ) ) {
+		$bp->pages = bp_core_get_directory_pages();
+	}
+
+	foreach( $wp_page_components as $component ) {
+		if ( !isset( $bp->pages->{$component['id']} ) ) {
+			$orphaned_components[] = $component['name'];
+		}
+	}
+
+	if ( !empty( $orphaned_components ) ) {
+		$admin_url = bp_get_admin_url( add_query_arg( array( 'page' => 'bp-page-settings' ), 'admin.php' ) );
+		$notice    = sprintf(
+			'%1$s <a href="%2$s">%3$s</a>',
+			sprintf(
+				__( 'The following active BuddyPress Components do not have associated WordPress Pages: %s.', 'buddypress' ),
+				'<strong>' . implode( '</strong>, <strong>', array_map( 'esc_html', $orphaned_components ) ) . '</strong>'
+			),
+			esc_url( $admin_url ),
+			__( 'Repair', 'buddypress' )
+		);
+
+		bp_core_add_admin_notice( $notice );
+	}
+
+	// BP components cannot share a single WP page. Check for duplicate assignments, and post a message if found.
+	$dupe_names = array();
+	$page_ids   = bp_core_get_directory_page_ids();
+	$dupes      = array_diff_assoc( $page_ids, array_unique( $page_ids ) );
+
+	if ( !empty( $dupes ) ) {
+		foreach( array_keys( $dupes ) as $dupe_component ) {
+			$dupe_names[] = $bp->pages->{$dupe_component}->title;
+		}
+
+		// Make sure that there are no duplicate duplicates :).
+		$dupe_names = array_unique( $dupe_names );
+	}
+
+	// If there are duplicates, post a message about them.
+	if ( !empty( $dupe_names ) ) {
+		$admin_url = bp_get_admin_url( add_query_arg( array( 'page' => 'bp-page-settings' ), 'admin.php' ) );
+		$notice    = sprintf(
+			'%1$s <a href="%2$s">%3$s</a>',
+			sprintf(
+				__( 'Each BuddyPress Component needs its own WordPress page. The following WordPress Pages have more than one component associated with them: %s.', 'buddypress' ),
+				'<strong>' . implode( '</strong>, <strong>', array_map( 'esc_html', $dupe_names ) ) . '</strong>'
+			),
+			esc_url( $admin_url ),
+			__( 'Repair', 'buddypress' )
+		);
+
+		bp_core_add_admin_notice( $notice );
+	}
+}
+
+/**
+ * Redirect user to BuddyPress's What's New page on activation.
+ *
+ * @since 1.7.0
+ *
+ * @internal Used internally to redirect BuddyPress to the about page on activation.
+ *
+ */
+function bp_do_activation_redirect() {
+
+	// Bail if no activation redirect.
+	if ( ! get_transient( '_bp_activation_redirect' ) ) {
+		return;
+	}
+
+	// Delete the redirect transient.
+	delete_transient( '_bp_activation_redirect' );
+
+	// Bail if activating from network, or bulk.
+	if ( isset( $_GET['activate-multi'] ) ) {
+		return;
+	}
+
+	$query_args = array();
+	if ( get_transient( '_bp_is_new_install' ) ) {
+		$query_args['is_new_install'] = '1';
+		delete_transient( '_bp_is_new_install' );
+	}
+
+	// Redirect to dashboard and trigger the Hello screen.
+	wp_safe_redirect( add_query_arg( $query_args, bp_get_admin_url( '?hello=buddypress' ) ) );
+}
+
+/** UI/Styling ****************************************************************/
+
+/**
+ * Output the tabs in the admin area.
+ *
+ * @since 1.5.0
+ *
+ * @param string $active_tab Name of the tab that is active. Optional.
+ */
+function bp_core_admin_tabs( $active_tab = '' ) {
+	$tabs_html    = '';
+	$idle_class   = 'nav-tab';
+	$active_class = 'nav-tab nav-tab-active';
+
+	/**
+	 * Filters the admin tabs to be displayed.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $value Array of tabs to output to the admin area.
+	 */
+	$tabs         = apply_filters( 'bp_core_admin_tabs', bp_core_get_admin_tabs( $active_tab ) );
+
+	// Loop through tabs and build navigation.
+	foreach ( array_values( $tabs ) as $tab_data ) {
+		$is_current = (bool) ( $tab_data['name'] == $active_tab );
+		$tab_class  = $is_current ? $active_class : $idle_class;
+		$tabs_html .= '<a href="' . esc_url( $tab_data['href'] ) . '" class="' . esc_attr( $tab_class ) . '">' . esc_html( $tab_data['name'] ) . '</a>';
+	}
+
+	echo $tabs_html;
+
+	/**
+	 * Fires after the output of tabs for the admin area.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_admin_tabs' );
+}
+
+/**
+ * Get the data for the tabs in the admin area.
+ *
+ * @since 2.2.0
+ *
+ * @param string $active_tab Name of the tab that is active. Optional.
+ * @return string
+ */
+function bp_core_get_admin_tabs( $active_tab = '' ) {
+	$tabs = array(
+		'0' => array(
+			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-components' ), 'admin.php' ) ),
+			'name' => __( 'Components', 'buddypress' )
+		),
+		'2' => array(
+			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-settings' ), 'admin.php' ) ),
+			'name' => __( 'Options', 'buddypress' )
+		),
+		'1' => array(
+			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-page-settings' ), 'admin.php' ) ),
+			'name' => __( 'Pages', 'buddypress' )
+		),
+		'3' => array(
+			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-credits' ), 'admin.php' ) ),
+			'name' => __( 'Credits', 'buddypress' )
+		),
+	);
+
+	/**
+	 * Filters the tab data used in our wp-admin screens.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $tabs Tab data.
+	 */
+	return apply_filters( 'bp_core_get_admin_tabs', $tabs );
+}
+
+/** Help **********************************************************************/
+
+/**
+ * Adds contextual help to BuddyPress admin pages.
+ *
+ * @since 1.7.0
+ * @todo Make this part of the BP_Component class and split into each component.
+ *
+ * @param string $screen Current screen.
+ */
+function bp_core_add_contextual_help( $screen = '' ) {
+
+	$screen = get_current_screen();
+
+	switch ( $screen->id ) {
+
+		// Component page.
+		case 'settings_page_bp-components' :
+
+			// Help tabs.
+			$screen->add_help_tab( array(
+				'id'      => 'bp-comp-overview',
+				'title'   => __( 'Overview', 'buddypress' ),
+				'content' => bp_core_add_contextual_help_content( 'bp-comp-overview' ),
+			) );
+
+			// Help panel - sidebar links.
+			$screen->set_help_sidebar(
+				'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+				'<p>' . __( '<a href="https://codex.buddypress.org/getting-started/configure-components/">Managing Components</a>', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+			);
+			break;
+
+		// Pages page.
+		case 'settings_page_bp-page-settings' :
+
+			// Help tabs.
+			$screen->add_help_tab( array(
+				'id' => 'bp-page-overview',
+				'title' => __( 'Overview', 'buddypress' ),
+				'content' => bp_core_add_contextual_help_content( 'bp-page-overview' ),
+			) );
+
+			// Help panel - sidebar links.
+			$screen->set_help_sidebar(
+				'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+				'<p>' . __( '<a href="https://codex.buddypress.org/getting-started/configure-components/#settings-buddypress-pages">Managing Pages</a>', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+			);
+
+			break;
+
+		// Settings page.
+		case 'settings_page_bp-settings' :
+
+			// Help tabs.
+			$screen->add_help_tab( array(
+				'id'      => 'bp-settings-overview',
+				'title'   => __( 'Overview', 'buddypress' ),
+				'content' => bp_core_add_contextual_help_content( 'bp-settings-overview' ),
+			) );
+
+			// Help panel - sidebar links.
+			$screen->set_help_sidebar(
+				'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+				'<p>' . __( '<a href="https://codex.buddypress.org/getting-started/configure-components/#settings-buddypress-settings">Managing Settings</a>', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+			);
+
+			break;
+
+		// Profile fields page.
+		case 'users_page_bp-profile-setup' :
+
+			// Help tabs.
+			$screen->add_help_tab( array(
+				'id'      => 'bp-profile-overview',
+				'title'   => __( 'Overview', 'buddypress' ),
+				'content' => bp_core_add_contextual_help_content( 'bp-profile-overview' ),
+			) );
+
+			// Help panel - sidebar links.
+			$screen->set_help_sidebar(
+				'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+				'<p>' . __( '<a href="https://codex.buddypress.org/administrator-guide/extended-profiles/">Managing Profile Fields</a>', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+			);
+
+			break;
+	}
+}
+add_action( 'load-settings_page_bp-components', 'bp_core_add_contextual_help' );
+add_action( 'load-settings_page_bp-page-settings', 'bp_core_add_contextual_help' );
+add_action( 'load-settings_page_bp-settings', 'bp_core_add_contextual_help' );
+add_action( 'load-users_page_bp-profile-setup', 'bp_core_add_contextual_help' );
+
+/**
+ * Renders contextual help content to contextual help tabs.
+ *
+ * @since 1.7.0
+ *
+ * @param string $tab Current help content tab.
+ * @return string
+ */
+function bp_core_add_contextual_help_content( $tab = '' ) {
+
+	switch ( $tab ) {
+		case 'bp-comp-overview' :
+			$retval = __( 'By default, all but four of the BuddyPress components are enabled. You can selectively enable or disable any of the components by using the form below. Your BuddyPress installation will continue to function. However, the features of the disabled components will no longer be accessible to anyone using the site.', 'buddypress' );
+			break;
+
+		case 'bp-page-overview' :
+			$retval = __( 'BuddyPress Components use WordPress Pages for their root directory/archive pages. You can change the page associations for each active component by using the form below.', 'buddypress' );
+			break;
+
+		case 'bp-settings-overview' :
+			$retval = __( 'Extra configuration settings are provided and activated. You can selectively enable or disable any setting by using the form on this screen.', 'buddypress' );
+			break;
+
+		case 'bp-profile-overview' :
+			$retval = __( 'Your users will distinguish themselves through their profile page. Create relevant profile fields that will show on each users profile.', 'buddypress' ) . '<br /><br />' . __( 'Note: Any fields in the first group will appear on the signup page.', 'buddypress' );
+			break;
+
+		default:
+			$retval = false;
+			break;
+	}
+
+	// Wrap text in a paragraph tag.
+	if ( !empty( $retval ) ) {
+		$retval = '<p>' . $retval . '</p>';
+	}
+
+	return $retval;
+}
+
+/** Separator *****************************************************************/
+
+/**
+ * Add a separator to the WordPress admin menus.
+ *
+ * @since 1.7.0
+ *
+ */
+function bp_admin_separator() {
+
+	// Bail if BuddyPress is not network activated and viewing network admin.
+	if ( is_network_admin() && ! bp_is_network_activated() ) {
+		return;
+	}
+
+	// Bail if BuddyPress is network activated and viewing site admin.
+	if ( ! is_network_admin() && bp_is_network_activated() ) {
+		return;
+	}
+
+	// Prevent duplicate separators when no core menu items exist.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return;
+	}
+
+	// Bail if there are no components with admin UI's. Hardcoded for now, until
+	// there's a real API for determining this later.
+	if ( ! bp_is_active( 'activity' ) && ! bp_is_active( 'groups' ) ) {
+		return;
+	}
+
+	global $menu;
+
+	$menu[] = array( '', 'read', 'separator-buddypress', '', 'wp-menu-separator buddypress' );
+}
+
+/**
+ * Tell WordPress we have a custom menu order.
+ *
+ * @since 1.7.0
+ *
+ * @param bool $menu_order Menu order.
+ * @return bool Always true.
+ */
+function bp_admin_custom_menu_order( $menu_order = false ) {
+
+	// Bail if user cannot see admin pages.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return $menu_order;
+	}
+
+	return true;
+}
+
+/**
+ * Move our custom separator above our custom post types.
+ *
+ * @since 1.7.0
+ *
+ * @param array $menu_order Menu Order.
+ * @return array Modified menu order.
+ */
+function bp_admin_menu_order( $menu_order = array() ) {
+
+	// Bail if user cannot see admin pages.
+	if ( empty( $menu_order ) || ! bp_current_user_can( 'bp_moderate' ) ) {
+		return $menu_order;
+	}
+
+	// Initialize our custom order array.
+	$bp_menu_order = array();
+
+	// Menu values.
+	$last_sep     = is_network_admin() ? 'separator1' : 'separator2';
+
+	/**
+	 * Filters the custom admin menus.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $value Empty array.
+	 */
+	$custom_menus = (array) apply_filters( 'bp_admin_menu_order', array() );
+
+	// Bail if no components have top level admin pages.
+	if ( empty( $custom_menus ) ) {
+		return $menu_order;
+	}
+
+	// Add our separator to beginning of array.
+	array_unshift( $custom_menus, 'separator-buddypress' );
+
+	// Loop through menu order and do some rearranging.
+	foreach ( (array) $menu_order as $item ) {
+
+		// Position BuddyPress menus above appearance.
+		if ( $last_sep == $item ) {
+
+			// Add our custom menus.
+			foreach( (array) $custom_menus as $custom_menu ) {
+				if ( array_search( $custom_menu, $menu_order ) ) {
+					$bp_menu_order[] = $custom_menu;
+				}
+			}
+
+			// Add the appearance separator.
+			$bp_menu_order[] = $last_sep;
+
+		// Skip our menu items.
+		} elseif ( ! in_array( $item, $custom_menus ) ) {
+			$bp_menu_order[] = $item;
+		}
+	}
+
+	// Return our custom order.
+	return $bp_menu_order;
+}
+
+/** Utility  *****************************************************************/
+
+/**
+ * When using a WP_List_Table, get the currently selected bulk action.
+ *
+ * WP_List_Tables have bulk actions at the top and at the bottom of the tables,
+ * and the inputs have different keys in the $_REQUEST array. This function
+ * reconciles the two values and returns a single action being performed.
+ *
+ * @since 1.7.0
+ *
+ * @return string
+ */
+function bp_admin_list_table_current_bulk_action() {
+
+	$action = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
+
+	// If the bottom is set, let it override the action.
+	if ( ! empty( $_REQUEST['action2'] ) && $_REQUEST['action2'] != "-1" ) {
+		$action = $_REQUEST['action2'];
+	}
+
+	return $action;
+}
+
+/** Menus *********************************************************************/
+
+/**
+ * Register meta box and associated JS for BuddyPress WP Nav Menu.
+ *
+ * @since 1.9.0
+ */
+function bp_admin_wp_nav_menu_meta_box() {
+	if ( ! bp_is_root_blog() ) {
+		return;
+	}
+
+	add_meta_box( 'add-buddypress-nav-menu', __( 'BuddyPress', 'buddypress' ), 'bp_admin_do_wp_nav_menu_meta_box', 'nav-menus', 'side', 'default' );
+
+	add_action( 'admin_print_footer_scripts', 'bp_admin_wp_nav_menu_restrict_items' );
+}
+
+/**
+ * Build and populate the BuddyPress accordion on Appearance > Menus.
+ *
+ * @since 1.9.0
+ *
+ * @global $nav_menu_selected_id
+ */
+function bp_admin_do_wp_nav_menu_meta_box() {
+	global $nav_menu_selected_id;
+
+	$walker = new BP_Walker_Nav_Menu_Checklist( false );
+	$args   = array( 'walker' => $walker );
+
+	$post_type_name = 'buddypress';
+
+	$tabs = array();
+
+	$tabs['loggedin']['label']  = __( 'Logged-In', 'buddypress' );
+	$tabs['loggedin']['pages']  = bp_nav_menu_get_loggedin_pages();
+
+	$tabs['loggedout']['label'] = __( 'Logged-Out', 'buddypress' );
+	$tabs['loggedout']['pages'] = bp_nav_menu_get_loggedout_pages();
+
+	?>
+
+	<div id="buddypress-menu" class="posttypediv">
+		<h4><?php _e( 'Logged-In', 'buddypress' ) ?></h4>
+		<p><?php _e( '<em>Logged-In</em> links are relative to the current user, and are not visible to visitors who are not logged in.', 'buddypress' ) ?></p>
+
+		<div id="tabs-panel-posttype-<?php echo $post_type_name; ?>-loggedin" class="tabs-panel tabs-panel-active">
+			<ul id="buddypress-menu-checklist-loggedin" class="categorychecklist form-no-clear">
+				<?php echo walk_nav_menu_tree( array_map( 'wp_setup_nav_menu_item', $tabs['loggedin']['pages'] ), 0, (object) $args );?>
+			</ul>
+		</div>
+
+		<h4><?php _e( 'Logged-Out', 'buddypress' ) ?></h4>
+		<p><?php _e( '<em>Logged-Out</em> links are not visible to users who are logged in.', 'buddypress' ) ?></p>
+
+		<div id="tabs-panel-posttype-<?php echo $post_type_name; ?>-loggedout" class="tabs-panel tabs-panel-active">
+			<ul id="buddypress-menu-checklist-loggedout" class="categorychecklist form-no-clear">
+				<?php echo walk_nav_menu_tree( array_map( 'wp_setup_nav_menu_item', $tabs['loggedout']['pages'] ), 0, (object) $args );?>
+			</ul>
+		</div>
+
+		<?php
+		$removed_args = array(
+			'action',
+			'customlink-tab',
+			'edit-menu-item',
+			'menu-item',
+			'page-tab',
+			'_wpnonce',
+		);
+		?>
+
+		<p class="button-controls">
+			<span class="list-controls">
+				<a href="<?php
+				echo esc_url( add_query_arg(
+					array(
+						$post_type_name . '-tab' => 'all',
+						'selectall'              => 1,
+					),
+					remove_query_arg( $removed_args )
+				) );
+				?>#buddypress-menu" class="select-all"><?php _e( 'Select All', 'buddypress' ); ?></a>
+			</span>
+			<span class="add-to-menu">
+				<input type="submit"<?php if ( function_exists( 'wp_nav_menu_disabled_check' ) ) : wp_nav_menu_disabled_check( $nav_menu_selected_id ); endif; ?> class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu', 'buddypress' ); ?>" name="add-custom-menu-item" id="submit-buddypress-menu" />
+				<span class="spinner"></span>
+			</span>
+		</p>
+	</div><!-- /#buddypress-menu -->
+
+	<?php
+}
+
+/**
+ * In admin emails list, for non-en_US locales, add notice explaining how to reinstall emails.
+ *
+ * If BuddyPress installs before its translations are in place, tell people how to reinstall
+ * the emails so they have their contents in their site's language.
+ *
+ * @since 2.5.0
+ */
+function bp_admin_email_maybe_add_translation_notice() {
+	if ( get_current_screen()->post_type !== bp_get_email_post_type() || get_locale() === 'en_US' ) {
+		return;
+	}
+
+	// If user can't access BP Tools, there's no point showing the message.
+	if ( ! current_user_can( buddypress()->admin->capability ) ) {
+		return;
+	}
+
+	if ( bp_core_do_network_admin() ) {
+		$admin_page = 'admin.php';
+	} else {
+		$admin_page = 'tools.php';
+	}
+
+	bp_core_add_admin_notice(
+		sprintf(
+			__( 'Are these emails not written in your site\'s language? Go to <a href="%s">BuddyPress Tools and try the "reinstall emails"</a> tool.', 'buddypress' ),
+			esc_url( add_query_arg( 'page', 'bp-tools', bp_get_admin_url( $admin_page ) ) )
+		),
+		'updated'
+	);
+}
+add_action( 'admin_head-edit.php', 'bp_admin_email_maybe_add_translation_notice' );
+
+/**
+ * In emails editor, add notice linking to token documentation on Codex.
+ *
+ * @since 2.5.0
+ */
+function bp_admin_email_add_codex_notice() {
+	if ( get_current_screen()->post_type !== bp_get_email_post_type() ) {
+		return;
+	}
+
+	bp_core_add_admin_notice(
+		sprintf(
+			__( 'Phrases wrapped in braces <code>{{ }}</code> are email tokens. <a href="%s">Learn about tokens on the BuddyPress Codex</a>.', 'buddypress' ),
+			esc_url( 'https://codex.buddypress.org/emails/email-tokens/' )
+		),
+		'error'
+	);
+}
+add_action( 'admin_head-post.php', 'bp_admin_email_add_codex_notice' );
+
+/**
+ * Display metabox for email taxonomy type.
+ *
+ * Shows the term description in a list, rather than the term name itself.
+ *
+ * @since 2.5.0
+ *
+ * @param WP_Post $post Post object.
+ * @param array   $box {
+ *     Tags meta box arguments.
+ *
+ *     @type string   $id       Meta box ID.
+ *     @type string   $title    Meta box title.
+ *     @type callable $callback Meta box display callback.
+ * }
+ */
+function bp_email_tax_type_metabox( $post, $box ) {
+	$r = array(
+		'taxonomy' => bp_get_email_tax_type()
+	);
+
+	$tax_name = esc_attr( $r['taxonomy'] );
+	$taxonomy = get_taxonomy( $r['taxonomy'] );
+	?>
+	<div id="taxonomy-<?php echo $tax_name; ?>" class="categorydiv">
+		<div id="<?php echo $tax_name; ?>-all" class="tabs-panel">
+			<?php
+			$name = ( $tax_name == 'category' ) ? 'post_category' : 'tax_input[' . $tax_name . ']';
+			echo "<input type='hidden' name='{$name}[]' value='0' />"; // Allows for an empty term set to be sent. 0 is an invalid Term ID and will be ignored by empty() checks.
+			?>
+			<ul id="<?php echo $tax_name; ?>checklist" data-wp-lists="list:<?php echo $tax_name; ?>" class="categorychecklist form-no-clear">
+				<?php wp_terms_checklist( $post->ID, array( 'taxonomy' => $tax_name, 'walker' => new BP_Walker_Category_Checklist ) ); ?>
+			</ul>
+		</div>
+
+		<p><?php esc_html_e( 'Choose when this email will be sent.', 'buddypress' ); ?></p>
+	</div>
+	<?php
+}
+
+/**
+ * Custom metaboxes used by our 'bp-email' post type.
+ *
+ * @since 2.5.0
+ */
+function bp_email_custom_metaboxes() {
+	// Remove default 'Excerpt' metabox and replace with our own.
+	remove_meta_box( 'postexcerpt', null, 'normal' );
+	add_meta_box( 'postexcerpt', __( 'Plain text email content', 'buddypress' ), 'bp_email_plaintext_metabox', null, 'normal', 'high' );
+}
+add_action( 'add_meta_boxes_' . bp_get_email_post_type(), 'bp_email_custom_metaboxes' );
+
+/**
+ * Customized version of the 'Excerpt' metabox for our 'bp-email' post type.
+ *
+ * We are using the 'Excerpt' metabox as our plain-text email content editor.
+ *
+ * @since 2.5.0
+ *
+ * @param WP_Post $post
+ */
+function bp_email_plaintext_metabox( $post ) {
+?>
+
+	<label class="screen-reader-text" for="excerpt"><?php
+		/* translators: accessibility text */
+		_e( 'Plain text email content', 'buddypress' );
+	?></label><textarea rows="5" cols="40" name="excerpt" id="excerpt"><?php echo $post->post_excerpt; // textarea_escaped ?></textarea>
+
+	<p><?php _e( 'Most email clients support HTML email. However, some people prefer to receive plain text email. Enter a plain text alternative version of your email here.', 'buddypress' ); ?></p>
+
+<?php
+}
+
+/**
+ * Restrict various items from view if editing a BuddyPress menu.
+ *
+ * If a person is editing a BP menu item, that person should not be able to
+ * see or edit the following fields:
+ *
+ * - CSS Classes - We use the 'bp-menu' CSS class to determine if the
+ *   menu item belongs to BP, so we cannot allow manipulation of this field to
+ *   occur.
+ * - URL - This field is automatically generated by BP on output, so this
+ *   field is useless and can cause confusion.
+ *
+ * Note: These restrictions are only enforced if JavaScript is enabled.
+ *
+ * @since 1.9.0
+ */
+function bp_admin_wp_nav_menu_restrict_items() {
+?>
+	<script type="text/javascript">
+	jQuery( '#menu-to-edit').on( 'click', 'a.item-edit', function() {
+		var settings  = jQuery(this).closest( '.menu-item-bar' ).next( '.menu-item-settings' );
+		var css_class = settings.find( '.edit-menu-item-classes' );
+
+		if( css_class.val().indexOf( 'bp-menu' ) === 0 ) {
+			css_class.attr( 'readonly', 'readonly' );
+			settings.find( '.field-url' ).css( 'display', 'none' );
+		}
+	});
+	</script>
+<?php
+}
+
+/**
+ * Add "Mark as Spam/Ham" button to user row actions.
+ *
+ * @since 2.0.0
+ *
+ * @param array  $actions     User row action links.
+ * @param object $user_object Current user information.
+ * @return array $actions User row action links.
+ */
+function bp_core_admin_user_row_actions( $actions, $user_object ) {
+
+	// Setup the $user_id variable from the current user object.
+	$user_id = 0;
+	if ( !empty( $user_object->ID ) ) {
+		$user_id = absint( $user_object->ID );
+	}
+
+	// Bail early if user cannot perform this action, or is looking at themselves.
+	if ( current_user_can( 'edit_user', $user_id ) && ( bp_loggedin_user_id() !== $user_id ) ) {
+
+		// Admin URL could be single site or network.
+		$url = bp_get_admin_url( 'users.php' );
+
+		// If spammed, create unspam link.
+		if ( bp_is_user_spammer( $user_id ) ) {
+			$url             = add_query_arg( array( 'action' => 'ham', 'user' => $user_id ), $url );
+			$unspam_link     = wp_nonce_url( $url, 'bp-spam-user' );
+			$actions['ham']  = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $unspam_link ), esc_html__( 'Not Spam', 'buddypress' ) );
+
+		// If not already spammed, create spam link.
+		} else {
+			$url             = add_query_arg( array( 'action' => 'spam', 'user' => $user_id ), $url );
+			$spam_link       = wp_nonce_url( $url, 'bp-spam-user' );
+			$actions['spam'] = sprintf( '<a class="submitdelete" href="%1$s">%2$s</a>', esc_url( $spam_link ), esc_html__( 'Spam', 'buddypress' ) );
+		}
+	}
+
+	// Create a "View" link.
+	$url             = bp_core_get_user_domain( $user_id );
+	$actions['view'] = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $url ), esc_html__( 'View', 'buddypress' ) );
+
+	// Return new actions.
+	return $actions;
+}
+
+/**
+ * Catch requests to mark individual users as spam/ham from users.php.
+ *
+ * @since 2.0.0
+ */
+function bp_core_admin_user_manage_spammers() {
+
+	// Print our inline scripts on non-Multisite.
+	add_action( 'admin_footer', 'bp_core_admin_user_spammed_js' );
+
+	$action  = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : false;
+	$updated = isset( $_REQUEST['updated'] ) ? $_REQUEST['updated'] : false;
+	$mode    = isset( $_POST['mode'] ) ? $_POST['mode'] : false;
+
+	// If this is a multisite, bulk request, stop now!
+	if ( 'list' == $mode ) {
+		return;
+	}
+
+	// Process a spam/ham request.
+	if ( ! empty( $action ) && in_array( $action, array( 'spam', 'ham' ) ) ) {
+
+		check_admin_referer( 'bp-spam-user' );
+
+		$user_id = ! empty( $_REQUEST['user'] ) ? intval( $_REQUEST['user'] ) : false;
+
+		if ( empty( $user_id ) ) {
+			return;
+		}
+
+		$redirect = wp_get_referer();
+
+		$status = ( $action == 'spam' ) ? 'spam' : 'ham';
+
+		// Process the user.
+		bp_core_process_spammer_status( $user_id, $status );
+
+		$redirect = add_query_arg( array( 'updated' => 'marked-' . $status ), $redirect );
+
+		wp_redirect( $redirect );
+	}
+
+	// Display feedback.
+	if ( ! empty( $updated ) && in_array( $updated, array( 'marked-spam', 'marked-ham' ) ) ) {
+
+		if ( 'marked-spam' === $updated ) {
+			$notice = __( 'User marked as spammer. Spam users are visible only to site admins.', 'buddypress' );
+		} else {
+			$notice = __( 'User removed from spam.', 'buddypress' );
+		}
+
+		bp_core_add_admin_notice( $notice );
+	}
+}
+
+/**
+ * Inline script that adds the 'site-spammed' class to spammed users.
+ *
+ * @since 2.0.0
+ */
+function bp_core_admin_user_spammed_js() {
+	?>
+	<script type="text/javascript">
+		jQuery( document ).ready( function($) {
+			$( '.row-actions .ham' ).each( function() {
+				$( this ).closest( 'tr' ).addClass( 'site-spammed' );
+			});
+		});
+	</script>
+	<?php
+}
+
+/**
+ * Catch and process an admin notice dismissal.
+ *
+ * @since 2.7.0
+ */
+function bp_core_admin_notice_dismiss_callback() {
+	if ( ! current_user_can( 'install_plugins' ) ) {
+		wp_send_json_error();
+	}
+
+	if ( empty( $_POST['nonce'] ) || empty( $_POST['notice_id'] ) ) {
+		wp_send_json_error();
+	}
+
+	$notice_id = wp_unslash( $_POST['notice_id'] );
+
+	if ( ! wp_verify_nonce( $_POST['nonce'], 'bp-dismissible-notice-' . $notice_id ) ) {
+		wp_send_json_error();
+	}
+
+	bp_update_option( "bp-dismissed-notice-$notice_id", 1 );
+
+	wp_send_json_success();
+}
+add_action( 'wp_ajax_bp_dismiss_notice', 'bp_core_admin_notice_dismiss_callback' );
+
+/**
+ * Add a "buddypress" class to body element of wp-admin.
+ *
+ * @since 2.8.0
+ *
+ * @param string $classes CSS classes for the body tag in the admin, a comma separated string.
+ *
+ * @return string
+ */
+function bp_core_admin_body_classes( $classes ) {
+	return $classes . ' buddypress';
+}
+add_filter( 'admin_body_class', 'bp_core_admin_body_classes' );
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-schema.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-schema.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-schema.php	(revision 12265)
@@ -0,0 +1,537 @@
+<?php
+/**
+ * BuddyPress DB schema.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Main installer.
+ *
+ * Can be passed an optional array of components to explicitly run installation
+ * routines on, typically the first time a component is activated in Settings.
+ *
+ * @since 1.0.0
+ *
+ * @param array|bool $active_components Components to install.
+ */
+function bp_core_install( $active_components = false ) {
+
+	bp_pre_schema_upgrade();
+
+	// If no components passed, get all the active components from the main site.
+	if ( empty( $active_components ) ) {
+
+		/** This filter is documented in bp-core/admin/bp-core-admin-components.php */
+		$active_components = apply_filters( 'bp_active_components', bp_get_option( 'bp-active-components' ) );
+	}
+
+	// Install Activity Streams even when inactive (to store last_activity data).
+	bp_core_install_activity_streams();
+
+	// Install the signups table.
+	bp_core_maybe_install_signups();
+
+	// Notifications.
+	if ( !empty( $active_components['notifications'] ) ) {
+		bp_core_install_notifications();
+	}
+
+	// Friend Connections.
+	if ( !empty( $active_components['friends'] ) ) {
+		bp_core_install_friends();
+	}
+
+	// Extensible Groups.
+	if ( !empty( $active_components['groups'] ) ) {
+		bp_core_install_groups();
+	}
+
+	// Private Messaging.
+	if ( !empty( $active_components['messages'] ) ) {
+		bp_core_install_private_messaging();
+	}
+
+	// Extended Profiles.
+	if ( !empty( $active_components['xprofile'] ) ) {
+		bp_core_install_extended_profiles();
+	}
+
+	// Blog tracking.
+	if ( !empty( $active_components['blogs'] ) ) {
+		bp_core_install_blog_tracking();
+	}
+}
+
+/**
+ * Install database tables for the Notifications component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_notifications() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_notifications (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				user_id bigint(20) NOT NULL,
+				item_id bigint(20) NOT NULL,
+				secondary_item_id bigint(20),
+				component_name varchar(75) NOT NULL,
+				component_action varchar(75) NOT NULL,
+				date_notified datetime NOT NULL,
+				is_new bool NOT NULL DEFAULT 0,
+				KEY item_id (item_id),
+				KEY secondary_item_id (secondary_item_id),
+				KEY user_id (user_id),
+				KEY is_new (is_new),
+				KEY component_name (component_name),
+				KEY component_action (component_action),
+				KEY useritem (user_id,is_new)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_notifications_meta (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				notification_id bigint(20) NOT NULL,
+				meta_key varchar(255) DEFAULT NULL,
+				meta_value longtext DEFAULT NULL,
+				KEY notification_id (notification_id),
+				KEY meta_key (meta_key(191))
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+}
+
+/**
+ * Install database tables for the Activity component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_activity_streams() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_activity (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				user_id bigint(20) NOT NULL,
+				component varchar(75) NOT NULL,
+				type varchar(75) NOT NULL,
+				action text NOT NULL,
+				content longtext NOT NULL,
+				primary_link text NOT NULL,
+				item_id bigint(20) NOT NULL,
+				secondary_item_id bigint(20) DEFAULT NULL,
+				date_recorded datetime NOT NULL,
+				hide_sitewide bool DEFAULT 0,
+				mptt_left int(11) NOT NULL DEFAULT 0,
+				mptt_right int(11) NOT NULL DEFAULT 0,
+				is_spam tinyint(1) NOT NULL DEFAULT 0,
+				KEY date_recorded (date_recorded),
+				KEY user_id (user_id),
+				KEY item_id (item_id),
+				KEY secondary_item_id (secondary_item_id),
+				KEY component (component),
+				KEY type (type),
+				KEY mptt_left (mptt_left),
+				KEY mptt_right (mptt_right),
+				KEY hide_sitewide (hide_sitewide),
+				KEY is_spam (is_spam)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_activity_meta (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				activity_id bigint(20) NOT NULL,
+				meta_key varchar(255) DEFAULT NULL,
+				meta_value longtext DEFAULT NULL,
+				KEY activity_id (activity_id),
+				KEY meta_key (meta_key(191))
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+}
+
+/**
+ * Install database tables for the Notifications component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_friends() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_friends (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				initiator_user_id bigint(20) NOT NULL,
+				friend_user_id bigint(20) NOT NULL,
+				is_confirmed bool DEFAULT 0,
+				is_limited bool DEFAULT 0,
+				date_created datetime NOT NULL,
+				KEY initiator_user_id (initiator_user_id),
+				KEY friend_user_id (friend_user_id)
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+}
+
+/**
+ * Install database tables for the Groups component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_groups() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_groups (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				creator_id bigint(20) NOT NULL,
+				name varchar(100) NOT NULL,
+				slug varchar(200) NOT NULL,
+				description longtext NOT NULL,
+				status varchar(10) NOT NULL DEFAULT 'public',
+				parent_id bigint(20) NOT NULL DEFAULT 0,
+				enable_forum tinyint(1) NOT NULL DEFAULT '1',
+				date_created datetime NOT NULL,
+				KEY creator_id (creator_id),
+				KEY status (status),
+				KEY parent_id (parent_id)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_groups_members (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				group_id bigint(20) NOT NULL,
+				user_id bigint(20) NOT NULL,
+				inviter_id bigint(20) NOT NULL,
+				is_admin tinyint(1) NOT NULL DEFAULT '0',
+				is_mod tinyint(1) NOT NULL DEFAULT '0',
+				user_title varchar(100) NOT NULL,
+				date_modified datetime NOT NULL,
+				comments longtext NOT NULL,
+				is_confirmed tinyint(1) NOT NULL DEFAULT '0',
+				is_banned tinyint(1) NOT NULL DEFAULT '0',
+				invite_sent tinyint(1) NOT NULL DEFAULT '0',
+				KEY group_id (group_id),
+				KEY is_admin (is_admin),
+				KEY is_mod (is_mod),
+				KEY user_id (user_id),
+				KEY inviter_id (inviter_id),
+				KEY is_confirmed (is_confirmed)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_groups_groupmeta (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				group_id bigint(20) NOT NULL,
+				meta_key varchar(255) DEFAULT NULL,
+				meta_value longtext DEFAULT NULL,
+				KEY group_id (group_id),
+				KEY meta_key (meta_key(191))
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+}
+
+/**
+ * Install database tables for the Messages component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_private_messaging() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_messages_messages (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				thread_id bigint(20) NOT NULL,
+				sender_id bigint(20) NOT NULL,
+				subject varchar(200) NOT NULL,
+				message longtext NOT NULL,
+				date_sent datetime NOT NULL,
+				KEY sender_id (sender_id),
+				KEY thread_id (thread_id)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_messages_recipients (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				user_id bigint(20) NOT NULL,
+				thread_id bigint(20) NOT NULL,
+				unread_count int(10) NOT NULL DEFAULT '0',
+				sender_only tinyint(1) NOT NULL DEFAULT '0',
+				is_deleted tinyint(1) NOT NULL DEFAULT '0',
+				KEY user_id (user_id),
+				KEY thread_id (thread_id),
+				KEY is_deleted (is_deleted),
+				KEY sender_only (sender_only),
+				KEY unread_count (unread_count)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_messages_notices (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				subject varchar(200) NOT NULL,
+				message longtext NOT NULL,
+				date_sent datetime NOT NULL,
+				is_active tinyint(1) NOT NULL DEFAULT '0',
+				KEY is_active (is_active)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_messages_meta (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				message_id bigint(20) NOT NULL,
+				meta_key varchar(255) DEFAULT NULL,
+				meta_value longtext DEFAULT NULL,
+				KEY message_id (message_id),
+				KEY meta_key (meta_key(191))
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+}
+
+/**
+ * Install database tables for the Profiles component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_extended_profiles() {
+	global $wpdb;
+
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	// These values should only be updated if they are not already present.
+	if ( ! bp_get_option( 'bp-xprofile-base-group-name' ) ) {
+		bp_update_option( 'bp-xprofile-base-group-name', _x( 'General', 'First field-group name', 'buddypress' ) );
+	}
+
+	if ( ! bp_get_option( 'bp-xprofile-fullname-field-name' ) ) {
+		bp_update_option( 'bp-xprofile-fullname-field-name', _x( 'Display Name', 'Display name field', 'buddypress' ) );
+	}
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_xprofile_groups (
+				id bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				name varchar(150) NOT NULL,
+				description mediumtext NOT NULL,
+				group_order bigint(20) NOT NULL DEFAULT '0',
+				can_delete tinyint(1) NOT NULL,
+				KEY can_delete (can_delete)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_xprofile_fields (
+				id bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				group_id bigint(20) unsigned NOT NULL,
+				parent_id bigint(20) unsigned NOT NULL,
+				type varchar(150) NOT NULL,
+				name varchar(150) NOT NULL,
+				description longtext NOT NULL,
+				is_required tinyint(1) NOT NULL DEFAULT '0',
+				is_default_option tinyint(1) NOT NULL DEFAULT '0',
+				field_order bigint(20) NOT NULL DEFAULT '0',
+				option_order bigint(20) NOT NULL DEFAULT '0',
+				order_by varchar(15) NOT NULL DEFAULT '',
+				can_delete tinyint(1) NOT NULL DEFAULT '1',
+				KEY group_id (group_id),
+				KEY parent_id (parent_id),
+				KEY field_order (field_order),
+				KEY can_delete (can_delete),
+				KEY is_required (is_required)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_xprofile_data (
+				id bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				field_id bigint(20) unsigned NOT NULL,
+				user_id bigint(20) unsigned NOT NULL,
+				value longtext NOT NULL,
+				last_updated datetime NOT NULL,
+				KEY field_id (field_id),
+				KEY user_id (user_id)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_xprofile_meta (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				object_id bigint(20) NOT NULL,
+				object_type varchar(150) NOT NULL,
+				meta_key varchar(255) DEFAULT NULL,
+				meta_value longtext DEFAULT NULL,
+				KEY object_id (object_id),
+				KEY meta_key (meta_key(191))
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+
+	// Insert the default group and fields.
+	$insert_sql = array();
+
+	if ( ! $wpdb->get_var( "SELECT id FROM {$bp_prefix}bp_xprofile_groups WHERE id = 1" ) ) {
+		$insert_sql[] = "INSERT INTO {$bp_prefix}bp_xprofile_groups ( name, description, can_delete ) VALUES ( " . $wpdb->prepare( '%s', stripslashes( bp_get_option( 'bp-xprofile-base-group-name' ) ) ) . ", '', 0 );";
+	}
+
+	if ( ! $wpdb->get_var( "SELECT id FROM {$bp_prefix}bp_xprofile_fields WHERE id = 1" ) ) {
+		$insert_sql[] = "INSERT INTO {$bp_prefix}bp_xprofile_fields ( group_id, parent_id, type, name, description, is_required, can_delete ) VALUES ( 1, 0, 'textbox', " . $wpdb->prepare( '%s', stripslashes( bp_get_option( 'bp-xprofile-fullname-field-name' ) ) ) . ", '', 1, 0 );";
+	}
+
+	dbDelta( $insert_sql );
+}
+
+/**
+ * Install database tables for the Sites component.
+ *
+ * @since 1.0.0
+ *
+ */
+function bp_core_install_blog_tracking() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_user_blogs (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				user_id bigint(20) NOT NULL,
+				blog_id bigint(20) NOT NULL,
+				KEY user_id (user_id),
+				KEY blog_id (blog_id)
+			) {$charset_collate};";
+
+	$sql[] = "CREATE TABLE {$bp_prefix}bp_user_blogs_blogmeta (
+				id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+				blog_id bigint(20) NOT NULL,
+				meta_key varchar(255) DEFAULT NULL,
+				meta_value longtext DEFAULT NULL,
+				KEY blog_id (blog_id),
+				KEY meta_key (meta_key(191))
+			) {$charset_collate};";
+
+	dbDelta( $sql );
+}
+
+/** Signups *******************************************************************/
+
+/**
+ * Install the signups table.
+ *
+ * @since 2.0.0
+ *
+ * @global $wpdb
+ */
+function bp_core_install_signups() {
+	global $wpdb;
+
+	// Signups is not there and we need it so let's create it.
+	require_once( buddypress()->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
+	require_once( ABSPATH                  . 'wp-admin/includes/upgrade.php'     );
+
+	// Never use bp_core_get_table_prefix() for any global users tables.
+	$wpdb->signups = $wpdb->base_prefix . 'signups';
+
+	// Use WP's core CREATE TABLE query.
+	$create_queries = wp_get_db_schema( 'ms_global' );
+	if ( ! is_array( $create_queries ) ) {
+		$create_queries = explode( ';', $create_queries );
+		$create_queries = array_filter( $create_queries );
+	}
+
+	// Filter out all the queries except wp_signups.
+	foreach ( $create_queries as $key => $query ) {
+		if ( preg_match( "|CREATE TABLE ([^ ]*)|", $query, $matches ) ) {
+			if ( trim( $matches[1], '`' ) !== $wpdb->signups ) {
+				unset( $create_queries[ $key ] );
+			}
+		}
+	}
+
+	// Run WordPress's database upgrader.
+	if ( ! empty( $create_queries ) ) {
+		dbDelta( $create_queries );
+	}
+}
+
+/**
+ * Update the signups table, adding `signup_id` column and drop `domain` index.
+ *
+ * This is necessary because WordPress's `pre_schema_upgrade()` function wraps
+ * table ALTER's in multisite checks, and other plugins may have installed their
+ * own sign-ups table; Eg: Gravity Forms User Registration Add On.
+ *
+ * @since 2.0.1
+ *
+ * @see pre_schema_upgrade()
+ * @link https://core.trac.wordpress.org/ticket/27855 WordPress Trac Ticket
+ * @link https://buddypress.trac.wordpress.org/ticket/5563 BuddyPress Trac Ticket
+ *
+ * @global WPDB $wpdb
+ */
+function bp_core_upgrade_signups() {
+	global $wpdb;
+
+	// Bail if global tables should not be upgraded.
+	if ( defined( 'DO_NOT_UPGRADE_GLOBAL_TABLES' ) ) {
+		return;
+	}
+
+	// Never use bp_core_get_table_prefix() for any global users tables.
+	$wpdb->signups = $wpdb->base_prefix . 'signups';
+
+	// Attempt to alter the signups table.
+	$wpdb->query( "ALTER TABLE {$wpdb->signups} ADD signup_id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST" );
+	$wpdb->query( "ALTER TABLE {$wpdb->signups} DROP INDEX domain" );
+}
+
+/**
+ * Add default emails.
+ *
+ * @since 2.5.0
+ */
+function bp_core_install_emails() {
+	$defaults = array(
+		'post_status' => 'publish',
+		'post_type'   => bp_get_email_post_type(),
+	);
+
+	$emails       = bp_email_get_schema();
+	$descriptions = bp_email_get_type_schema( 'description' );
+
+	// Add these emails to the database.
+	foreach ( $emails as $id => $email ) {
+
+		// Some emails are multisite-only.
+		if ( ! is_multisite() && isset( $email['args'] ) && ! empty( $email['args']['multisite'] ) ) {
+			continue;
+		}
+
+		$post_id = wp_insert_post( bp_parse_args( $email, $defaults, 'install_email_' . $id ) );
+		if ( ! $post_id ) {
+			continue;
+		}
+
+		$tt_ids = wp_set_object_terms( $post_id, $id, bp_get_email_tax_type() );
+		foreach ( $tt_ids as $tt_id ) {
+			$term = get_term_by( 'term_taxonomy_id', (int) $tt_id, bp_get_email_tax_type() );
+			wp_update_term( (int) $term->term_id, bp_get_email_tax_type(), array(
+				'description' => $descriptions[ $id ],
+			) );
+		}
+	}
+
+	bp_update_option( 'bp-emails-unsubscribe-salt', base64_encode( wp_generate_password( 64, true, true ) ) );
+
+	/**
+	 * Fires after BuddyPress adds the posts for its emails.
+	 *
+	 * @since 2.5.0
+	 */
+	do_action( 'bp_core_install_emails' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-settings.php	(revision 12265)
@@ -0,0 +1,391 @@
+<?php
+/**
+ * BuddyPress Admin Settings.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Main settings section description for the settings page.
+ *
+ * @since 1.6.0
+ */
+function bp_admin_setting_callback_main_section() { }
+
+/**
+ * Admin bar for logged out users setting field.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin_setting_callback_admin_bar() {
+?>
+
+	<input id="hide-loggedout-adminbar" name="hide-loggedout-adminbar" type="checkbox" value="1" <?php checked( !bp_hide_loggedout_adminbar( false ) ); ?> />
+	<label for="hide-loggedout-adminbar"><?php _e( 'Show the Toolbar for logged out users', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Allow members to delete their accounts setting field.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin_setting_callback_account_deletion() {
+?>
+
+	<input id="bp-disable-account-deletion" name="bp-disable-account-deletion" type="checkbox" value="1" <?php checked( !bp_disable_account_deletion( false ) ); ?> />
+	<label for="bp-disable-account-deletion"><?php _e( 'Allow registered members to delete their own accounts', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Form element to change the active template pack.
+ */
+function bp_admin_setting_callback_theme_package_id() {
+	$options = '';
+
+	/*
+	 * Note: This should never be empty. /bp-templates/ is the
+	 * canonical backup if no other packages exist. If there's an error here,
+	 * something else is wrong.
+	 *
+	 * See BuddyPress::register_theme_packages()
+	 */
+	foreach ( (array) buddypress()->theme_compat->packages as $id => $theme ) {
+		$options .= sprintf(
+			'<option value="%1$s" %2$s>%3$s</option>',
+			esc_attr( $id ),
+			selected( $theme->id, bp_get_theme_package_id(), false ),
+			esc_html( $theme->name )
+		);
+	}
+
+	if ( $options ) : ?>
+		<select name="_bp_theme_package_id" id="_bp_theme_package_id" aria-describedby="_bp_theme_package_description"><?php echo $options; ?></select>
+		<p id="_bp_theme_package_description" class="description"><?php esc_html_e( 'The selected Template Pack will serve all BuddyPress templates.', 'buddypress' ); ?></p>
+
+	<?php else : ?>
+		<p><?php esc_html_e( 'No template packages available.', 'buddypress' ); ?></p>
+
+	<?php endif;
+}
+
+/** Activity *******************************************************************/
+
+/**
+ * Groups settings section description for the settings page.
+ *
+ * @since 1.6.0
+ */
+function bp_admin_setting_callback_activity_section() { }
+
+/**
+ * Allow Akismet setting field.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin_setting_callback_activity_akismet() {
+?>
+
+	<input id="_bp_enable_akismet" name="_bp_enable_akismet" type="checkbox" value="1" <?php checked( bp_is_akismet_active( true ) ); ?> />
+	<label for="_bp_enable_akismet"><?php _e( 'Allow Akismet to scan for activity stream spam', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Allow activity comments on posts and comments.
+ *
+ * @since 1.6.0
+ */
+function bp_admin_setting_callback_blogforum_comments() {
+?>
+
+	<input id="bp-disable-blogforum-comments" name="bp-disable-blogforum-comments" type="checkbox" value="1" <?php checked( !bp_disable_blogforum_comments( false ) ); ?> />
+	<label for="bp-disable-blogforum-comments"><?php _e( 'Allow activity stream commenting on posts and comments', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Allow Heartbeat to refresh activity stream.
+ *
+ * @since 2.0.0
+ */
+function bp_admin_setting_callback_heartbeat() {
+?>
+
+	<input id="_bp_enable_heartbeat_refresh" name="_bp_enable_heartbeat_refresh" type="checkbox" value="1" <?php checked( bp_is_activity_heartbeat_active( true ) ); ?> />
+	<label for="_bp_enable_heartbeat_refresh"><?php _e( 'Automatically check for new items while viewing the activity stream', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Sanitization for bp-disable-blogforum-comments setting.
+ *
+ * In the UI, a checkbox asks whether you'd like to *enable* post/comment activity comments. For
+ * legacy reasons, the option that we store is 1 if these comments are *disabled*. So we use this
+ * function to flip the boolean before saving the intval.
+ *
+ * @since 1.6.0
+ *
+ * @param bool $value Whether or not to sanitize.
+ * @return bool
+ */
+function bp_admin_sanitize_callback_blogforum_comments( $value = false ) {
+	return $value ? 0 : 1;
+}
+
+/** XProfile ******************************************************************/
+
+/**
+ * Profile settings section description for the settings page.
+ *
+ * @since 1.6.0
+ */
+function bp_admin_setting_callback_xprofile_section() { }
+
+/**
+ * Enable BP->WP profile syncing field.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin_setting_callback_profile_sync() {
+?>
+
+	<input id="bp-disable-profile-sync" name="bp-disable-profile-sync" type="checkbox" value="1" <?php checked( !bp_disable_profile_sync( false ) ); ?> />
+	<label for="bp-disable-profile-sync"><?php _e( 'Enable BuddyPress to WordPress profile syncing', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Allow members to upload avatars field.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin_setting_callback_avatar_uploads() {
+?>
+
+	<input id="bp-disable-avatar-uploads" name="bp-disable-avatar-uploads" type="checkbox" value="1" <?php checked( !bp_disable_avatar_uploads( false ) ); ?> />
+	<label for="bp-disable-avatar-uploads"><?php _e( 'Allow registered members to upload avatars', 'buddypress' ); ?></label>
+
+<?php
+}
+
+/**
+ * Allow members to upload cover images field.
+ *
+ * @since 2.4.0
+ */
+function bp_admin_setting_callback_cover_image_uploads() {
+?>
+	<input id="bp-disable-cover-image-uploads" name="bp-disable-cover-image-uploads" type="checkbox" value="1" <?php checked( ! bp_disable_cover_image_uploads() ); ?> />
+	<label for="bp-disable-cover-image-uploads"><?php _e( 'Allow registered members to upload cover images', 'buddypress' ); ?></label>
+<?php
+}
+
+/** Groups Section ************************************************************/
+
+/**
+ * Groups settings section description for the settings page.
+ *
+ * @since 1.6.0
+ */
+function bp_admin_setting_callback_groups_section() { }
+
+/**
+ * Allow all users to create groups field.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin_setting_callback_group_creation() {
+?>
+
+	<input id="bp_restrict_group_creation" name="bp_restrict_group_creation" type="checkbox" aria-describedby="bp_group_creation_description" value="1" <?php checked( !bp_restrict_group_creation( false ) ); ?> />
+	<label for="bp_restrict_group_creation"><?php _e( 'Enable group creation for all users', 'buddypress' ); ?></label>
+	<p class="description" id="bp_group_creation_description"><?php _e( 'Administrators can always create groups, regardless of this setting.', 'buddypress' ); ?></p>
+
+<?php
+}
+
+/**
+ * 'Enable group avatars' field markup.
+ *
+ * @since 2.3.0
+ */
+function bp_admin_setting_callback_group_avatar_uploads() {
+?>
+	<input id="bp-disable-group-avatar-uploads" name="bp-disable-group-avatar-uploads" type="checkbox" value="1" <?php checked( ! bp_disable_group_avatar_uploads() ); ?> />
+	<label for="bp-disable-group-avatar-uploads"><?php _e( 'Allow customizable avatars for groups', 'buddypress' ); ?></label>
+<?php
+}
+
+/**
+ * 'Enable group cover images' field markup.
+ *
+ * @since 2.4.0
+ */
+function bp_admin_setting_callback_group_cover_image_uploads() {
+?>
+	<input id="bp-disable-group-cover-image-uploads" name="bp-disable-group-cover-image-uploads" type="checkbox" value="1" <?php checked( ! bp_disable_group_cover_image_uploads() ); ?> />
+	<label for="bp-disable-group-cover-image-uploads"><?php _e( 'Allow customizable cover images for groups', 'buddypress' ); ?></label>
+<?php
+}
+
+/** Settings Page *************************************************************/
+
+/**
+ * The main settings page
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_core_admin_settings() {
+
+	// We're saving our own options, until the WP Settings API is updated to work with Multisite.
+	$form_action = add_query_arg( 'page', 'bp-settings', bp_get_admin_url( 'admin.php' ) );
+
+	?>
+
+	<div class="wrap">
+
+		<h1><?php _e( 'BuddyPress Settings', 'buddypress' ); ?> </h1>
+
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Options', 'buddypress' ) ); ?></h2>
+
+		<form action="<?php echo esc_url( $form_action ) ?>" method="post">
+
+			<?php settings_fields( 'buddypress' ); ?>
+
+			<?php do_settings_sections( 'buddypress' ); ?>
+
+			<p class="submit">
+				<input type="submit" name="submit" class="button-primary" value="<?php esc_attr_e( 'Save Settings', 'buddypress' ); ?>" />
+			</p>
+		</form>
+	</div>
+
+<?php
+}
+
+/**
+ * Save our settings.
+ *
+ * @since 1.6.0
+ */
+function bp_core_admin_settings_save() {
+	global $wp_settings_fields;
+
+	if ( isset( $_GET['page'] ) && 'bp-settings' == $_GET['page'] && !empty( $_POST['submit'] ) ) {
+		check_admin_referer( 'buddypress-options' );
+
+		// Because many settings are saved with checkboxes, and thus will have no values
+		// in the $_POST array when unchecked, we loop through the registered settings.
+		if ( isset( $wp_settings_fields['buddypress'] ) ) {
+			foreach( (array) $wp_settings_fields['buddypress'] as $section => $settings ) {
+				foreach( $settings as $setting_name => $setting ) {
+					$value = isset( $_POST[$setting_name] ) ? $_POST[$setting_name] : '';
+
+					bp_update_option( $setting_name, $value );
+				}
+			}
+		}
+
+		// Some legacy options are not registered with the Settings API, or are reversed in the UI.
+		$legacy_options = array(
+			'bp-disable-account-deletion',
+			'bp-disable-avatar-uploads',
+			'bp-disable-cover-image-uploads',
+			'bp-disable-group-avatar-uploads',
+			'bp-disable-group-cover-image-uploads',
+			'bp_disable_blogforum_comments',
+			'bp-disable-profile-sync',
+			'bp_restrict_group_creation',
+			'hide-loggedout-adminbar',
+		);
+
+		foreach( $legacy_options as $legacy_option ) {
+			// Note: Each of these options is represented by its opposite in the UI
+			// Ie, the Profile Syncing option reads "Enable Sync", so when it's checked,
+			// the corresponding option should be unset.
+			$value = isset( $_POST[$legacy_option] ) ? '' : 1;
+			bp_update_option( $legacy_option, $value );
+		}
+
+		bp_core_redirect( add_query_arg( array( 'page' => 'bp-settings', 'updated' => 'true' ), bp_get_admin_url( 'admin.php' ) ) );
+	}
+}
+add_action( 'bp_admin_init', 'bp_core_admin_settings_save', 100 );
+
+/**
+ * Output settings API option.
+ *
+ * @since 1.6.0
+ *
+ * @param string $option  Form option to echo.
+ * @param string $default Form option default.
+ * @param bool   $slug    Form option slug.
+ */
+function bp_form_option( $option, $default = '' , $slug = false ) {
+	echo bp_get_form_option( $option, $default, $slug );
+}
+	/**
+	 * Return settings API option
+	 *
+	 * @since 1.6.0
+	 *
+	 *
+	 * @param string $option  Form option to return.
+	 * @param string $default Form option default.
+	 * @param bool   $slug    Form option slug.
+	 * @return string
+	 */
+	function bp_get_form_option( $option, $default = '', $slug = false ) {
+
+		// Get the option and sanitize it.
+		$value = bp_get_option( $option, $default );
+
+		// Slug?
+		if ( true === $slug ) {
+
+			/**
+			 * Filters the slug value in the form field.
+			 *
+			 * @since 1.6.0
+			 *
+			 * @param string $value Value being returned for the requested option.
+			 */
+			$value = esc_attr( apply_filters( 'editable_slug', $value ) );
+		} else { // Not a slug.
+			$value = esc_attr( $value );
+		}
+
+		// Fallback to default.
+		if ( empty( $value ) )
+			$value = $default;
+
+		/**
+		 * Filters the settings API option.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $value  Value being returned for the requested option.
+		 * @param string $option Option whose value is being requested.
+		 */
+		return apply_filters( 'bp_get_form_option', $value, $option );
+	}
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-slugs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-slugs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-slugs.php	(revision 12265)
@@ -0,0 +1,276 @@
+<?php
+/**
+ * BuddyPress Admin Slug Functions.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Renders the page mapping admin panel.
+ *
+ * @since 1.6.0
+ * @todo Use settings API
+ */
+function bp_core_admin_slugs_settings() {
+?>
+
+	<div class="wrap">
+
+		<h1><?php _e( 'BuddyPress Settings', 'buddypress' ); ?> </h1>
+
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Pages', 'buddypress' ) ); ?></h2>
+		<form action="" method="post" id="bp-admin-page-form">
+
+			<?php bp_core_admin_slugs_options(); ?>
+
+			<p class="submit clear">
+				<input class="button-primary" type="submit" name="bp-admin-pages-submit" id="bp-admin-pages-submit" value="<?php esc_attr_e( 'Save Settings', 'buddypress' ) ?>"/>
+			</p>
+
+			<?php wp_nonce_field( 'bp-admin-pages-setup' ); ?>
+
+		</form>
+	</div>
+
+<?php
+}
+
+/**
+ * Generate a list of directory pages, for use when building Components panel markup.
+ *
+ * @since 2.4.1
+ *
+ * @return array
+ */
+function bp_core_admin_get_directory_pages() {
+	$bp = buddypress();
+	$directory_pages = array();
+
+	// Loop through loaded components and collect directories.
+	if ( is_array( $bp->loaded_components ) ) {
+		foreach( $bp->loaded_components as $component_slug => $component_id ) {
+
+			// Only components that need directories should be listed here.
+			if ( isset( $bp->{$component_id} ) && !empty( $bp->{$component_id}->has_directory ) ) {
+
+				// The component->name property was introduced in BP 1.5, so we must provide a fallback.
+				$directory_pages[$component_id] = !empty( $bp->{$component_id}->name ) ? $bp->{$component_id}->name : ucwords( $component_id );
+			}
+		}
+	}
+
+	/** Directory Display *****************************************************/
+
+	/**
+	 * Filters the loaded components needing directory page association to a WordPress page.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $directory_pages Array of available components to set associations for.
+	 */
+	return apply_filters( 'bp_directory_pages', $directory_pages );
+}
+
+/**
+ * Generate a list of static pages, for use when building Components panel markup.
+ *
+ * By default, this list contains 'register' and 'activate'.
+ *
+ * @since 2.4.1
+ *
+ * @return array
+ */
+function bp_core_admin_get_static_pages() {
+	$static_pages = array(
+		'register' => __( 'Register', 'buddypress' ),
+		'activate' => __( 'Activate', 'buddypress' ),
+	);
+
+	/**
+	 * Filters the default static pages for BuddyPress setup.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $static_pages Array of static default static pages.
+	 */
+	return apply_filters( 'bp_static_pages', $static_pages );
+}
+
+/**
+ * Creates reusable markup for page setup on the Components and Pages dashboard panel.
+ *
+ * @package BuddyPress
+ * @since 1.6.0
+ * @todo Use settings API
+ */
+function bp_core_admin_slugs_options() {
+
+	// Get the existing WP pages
+	$existing_pages = bp_core_get_directory_page_ids();
+
+	// Set up an array of components (along with component names) that have directory pages.
+	$directory_pages = bp_core_admin_get_directory_pages();
+
+	if ( !empty( $directory_pages ) ) : ?>
+
+		<h3><?php _e( 'Directories', 'buddypress' ); ?></h3>
+
+		<p><?php _e( 'Associate a WordPress Page with each BuddyPress component directory.', 'buddypress' ); ?></p>
+
+		<table class="form-table">
+			<tbody>
+
+				<?php foreach ( $directory_pages as $name => $label ) : ?>
+
+					<tr valign="top">
+						<th scope="row">
+							<label for="bp_pages[<?php echo esc_attr( $name ) ?>]"><?php echo esc_html( $label ) ?></label>
+						</th>
+
+						<td>
+
+							<?php if ( ! bp_is_root_blog() ) switch_to_blog( bp_get_root_blog_id() ); ?>
+
+							<?php echo wp_dropdown_pages( array(
+								'name'             => 'bp_pages[' . esc_attr( $name ) . ']',
+								'echo'             => false,
+								'show_option_none' => __( '- None -', 'buddypress' ),
+								'selected'         => !empty( $existing_pages[$name] ) ? $existing_pages[$name] : false
+							) ); ?>
+
+							<?php if ( !empty( $existing_pages[$name] ) ) : ?>
+
+								<a href="<?php echo get_permalink( $existing_pages[$name] ); ?>" class="button-secondary" target="_bp"><?php _e( 'View', 'buddypress' ); ?></a>
+
+							<?php endif; ?>
+
+							<?php if ( ! bp_is_root_blog() ) restore_current_blog(); ?>
+
+						</td>
+					</tr>
+
+
+				<?php endforeach ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of default directories.
+				 *
+				 * Allows plugins to add their own directory associations.
+				 *
+				 * @since 1.5.0
+				 */
+				do_action( 'bp_active_external_directories' ); ?>
+
+			</tbody>
+		</table>
+
+	<?php
+
+	endif;
+
+	/** Static Display ********************************************************/
+
+	$static_pages = bp_core_admin_get_static_pages();
+
+	if ( !empty( $static_pages ) ) : ?>
+
+		<h3><?php _e( 'Registration', 'buddypress' ); ?></h3>
+
+		<?php if ( bp_get_signup_allowed() ) : ?>
+			<p><?php _e( 'Associate WordPress Pages with the following BuddyPress Registration pages.', 'buddypress' ); ?></p>
+		<?php else : ?>
+			<?php if ( is_multisite() ) : ?>
+				<p><?php printf( __( 'Registration is currently disabled.  Before associating a page is allowed, please enable registration by selecting either the "User accounts may be registered" or "Both sites and user accounts can be registered" option on <a href="%s">this page</a>.', 'buddypress' ), network_admin_url( 'settings.php' ) ); ?></p>
+			<?php else : ?>
+				<p><?php printf( __( 'Registration is currently disabled.  Before associating a page is allowed, please enable registration by clicking on the "Anyone can register" checkbox on <a href="%s">this page</a>.', 'buddypress' ), admin_url( 'options-general.php' ) ); ?></p>
+			<?php endif; ?>
+		<?php endif; ?>
+
+		<table class="form-table">
+			<tbody>
+
+				<?php if ( bp_get_signup_allowed() ) : foreach ( $static_pages as $name => $label ) : ?>
+
+					<tr valign="top">
+						<th scope="row">
+							<label for="bp_pages[<?php echo esc_attr( $name ) ?>]"><?php echo esc_html( $label ) ?></label>
+						</th>
+
+						<td>
+
+							<?php if ( ! bp_is_root_blog() ) switch_to_blog( bp_get_root_blog_id() ); ?>
+
+							<?php echo wp_dropdown_pages( array(
+								'name'             => 'bp_pages[' . esc_attr( $name ) . ']',
+								'echo'             => false,
+								'show_option_none' => __( '- None -', 'buddypress' ),
+								'selected'         => !empty( $existing_pages[$name] ) ? $existing_pages[$name] : false
+							) ) ?>
+
+							<?php if ( !empty( $existing_pages[$name] ) ) : ?>
+
+								<a href="<?php echo get_permalink( $existing_pages[$name] ); ?>" class="button-secondary" target="_bp"><?php _e( 'View', 'buddypress' ); ?></a>
+
+							<?php endif; ?>
+
+							<?php if ( ! bp_is_root_blog() ) restore_current_blog(); ?>
+
+						</td>
+					</tr>
+
+				<?php endforeach; endif; ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of default static pages for BuddyPress setup.
+				 *
+				 * @since 1.5.0
+				 */
+				do_action( 'bp_active_external_pages' ); ?>
+
+			</tbody>
+		</table>
+
+		<?php
+	endif;
+}
+
+/**
+ * Handle saving of the BuddyPress slugs.
+ *
+ * @since 1.6.0
+ * @todo Use settings API
+ */
+function bp_core_admin_slugs_setup_handler() {
+
+	if ( isset( $_POST['bp-admin-pages-submit'] ) ) {
+		if ( !check_admin_referer( 'bp-admin-pages-setup' ) )
+			return false;
+
+		// Then, update the directory pages.
+		if ( isset( $_POST['bp_pages'] ) ) {
+			$valid_pages = array_merge( bp_core_admin_get_directory_pages(), bp_core_admin_get_static_pages() );
+
+			$new_directory_pages = array();
+			foreach ( (array) $_POST['bp_pages'] as $key => $value ) {
+				if ( isset( $valid_pages[ $key ] ) ) {
+					$new_directory_pages[ $key ] = (int) $value;
+				}
+			}
+			bp_core_update_directory_page_ids( $new_directory_pages );
+		}
+
+		$base_url = bp_get_admin_url( add_query_arg( array( 'page' => 'bp-page-settings', 'updated' => 'true' ), 'admin.php' ) );
+
+		wp_redirect( $base_url );
+	}
+}
+add_action( 'bp_admin_init', 'bp_core_admin_slugs_setup_handler' );
Index: /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-tools.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-tools.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/bp-core-admin-tools.php	(revision 12265)
@@ -0,0 +1,490 @@
+<?php
+/**
+ * BuddyPress Tools panel.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Render the BuddyPress Tools page.
+ *
+ * @since 2.0.0
+ */
+function bp_core_admin_tools() {
+	?>
+	<div class="wrap">
+
+		<h1><?php esc_html_e( 'BuddyPress Tools', 'buddypress' ) ?></h1>
+
+		<p><?php esc_html_e( 'BuddyPress keeps track of various relationships between members, groups, and activity items. Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?></p>
+		<p><?php esc_html_e( 'Use the tools below to manually recalculate these relationships.', 'buddypress' ); ?>
+		</p>
+		<p class="description"><?php esc_html_e( 'Some of these tools create substantial database overhead. Avoid running more than one repair job at a time.', 'buddypress' ); ?></p>
+
+		<form class="settings" method="post" action="">
+
+			<fieldset>
+				<legend><?php esc_html_e( 'Repair tools', 'buddypress' ) ?></legend>
+
+				<div class="checkbox">
+				<?php foreach ( bp_admin_repair_list() as $item ) : ?>
+					<label for="<?php echo esc_attr( str_replace( '_', '-', $item[0] ) ); ?>"><input type="checkbox" class="checkbox" name="<?php echo esc_attr( $item[0] ) . '" id="' . esc_attr( str_replace( '_', '-', $item[0] ) ); ?>" value="1" /> <?php echo esc_html( $item[1] ); ?></label>
+				<?php endforeach; ?>
+				</div>
+
+				<p class="submit">
+					<input class="button-primary" type="submit" name="bp-tools-submit" value="<?php esc_attr_e( 'Repair Items', 'buddypress' ); ?>" />
+					<?php wp_nonce_field( 'bp-do-counts' ); ?>
+				</p>
+
+			</fieldset>
+
+		</form>
+
+	</div>
+
+	<?php
+}
+
+/**
+ * Handle the processing and feedback of the admin tools page.
+ *
+ * @since 2.0.0
+ */
+function bp_admin_repair_handler() {
+	if ( ! bp_is_post_request() || empty( $_POST['bp-tools-submit'] ) ) {
+		return;
+	}
+
+	check_admin_referer( 'bp-do-counts' );
+
+	// Bail if user cannot moderate.
+	$capability = bp_core_do_network_admin() ? 'manage_network_options' : 'manage_options';
+	if ( ! bp_current_user_can( $capability ) ) {
+		return;
+	}
+
+	wp_cache_flush();
+	$messages = array();
+
+	foreach ( (array) bp_admin_repair_list() as $item ) {
+		if ( isset( $item[2] ) && isset( $_POST[$item[0]] ) && 1 === absint( $_POST[$item[0]] ) && is_callable( $item[2] ) ) {
+			$messages[] = call_user_func( $item[2] );
+		}
+	}
+
+	if ( count( $messages ) ) {
+		foreach ( $messages as $message ) {
+			bp_admin_tools_feedback( $message[1] );
+		}
+	}
+}
+add_action( bp_core_admin_hook(), 'bp_admin_repair_handler' );
+
+/**
+ * Get the array of the repair list.
+ *
+ * @return array
+ */
+function bp_admin_repair_list() {
+	$repair_list = array();
+
+	// Members:
+	// - member count
+	// - last_activity migration (2.0).
+	$repair_list[20] = array(
+		'bp-total-member-count',
+		__( 'Repair total members count.', 'buddypress' ),
+		'bp_admin_repair_count_members',
+	);
+
+	$repair_list[25] = array(
+		'bp-last-activity',
+		__( 'Repair member "last activity" data.', 'buddypress' ),
+		'bp_admin_repair_last_activity',
+	);
+
+	// Friends:
+	// - user friend count.
+	if ( bp_is_active( 'friends' ) ) {
+		$repair_list[0] = array(
+			'bp-user-friends',
+			__( 'Repair total friends count for each member.', 'buddypress' ),
+			'bp_admin_repair_friend_count',
+		);
+	}
+
+	// Groups:
+	// - user group count.
+	if ( bp_is_active( 'groups' ) ) {
+		$repair_list[10] = array(
+			'bp-group-count',
+			__( 'Repair total groups count for each member.', 'buddypress' ),
+			'bp_admin_repair_group_count',
+		);
+	}
+
+	// Blogs:
+	// - user blog count.
+	if ( bp_is_active( 'blogs' ) ) {
+		$repair_list[90] = array(
+			'bp-blog-records',
+			__( 'Repopulate site tracking records.', 'buddypress' ),
+			'bp_admin_repair_blog_records',
+		);
+	}
+
+	// Emails:
+	// - reinstall emails.
+	$repair_list[100] = array(
+		'bp-reinstall-emails',
+		__( 'Reinstall emails (delete and restore from defaults).', 'buddypress' ),
+		'bp_admin_reinstall_emails',
+	);
+
+	ksort( $repair_list );
+
+	/**
+	 * Filters the array of the repair list.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $repair_list Array of values for the Repair list options.
+	 */
+	return (array) apply_filters( 'bp_repair_list', $repair_list );
+}
+
+/**
+ * Recalculate friend counts for each user.
+ *
+ * @since 2.0.0
+ *
+ * @return array
+ */
+function bp_admin_repair_friend_count() {
+	global $wpdb;
+
+	if ( ! bp_is_active( 'friends' ) ) {
+		return;
+	}
+
+	$statement = __( 'Counting the number of friends for each user&hellip; %s', 'buddypress' );
+	$result    = __( 'Failed!', 'buddypress' );
+
+	$sql_delete = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN ( 'total_friend_count' );";
+	if ( is_wp_error( $wpdb->query( $sql_delete ) ) ) {
+		return array( 1, sprintf( $statement, $result ) );
+	}
+
+	$bp = buddypress();
+
+	// Walk through all users on the site.
+	$total_users = $wpdb->get_row( "SELECT count(ID) as c FROM {$wpdb->users}" )->c;
+
+	$updated = array();
+	if ( $total_users > 0 ) {
+		$per_query = 500;
+		$offset = 0;
+		while ( $offset < $total_users ) {
+			// Only bother updating counts for users who actually have friendships.
+			$friendships = $wpdb->get_results( $wpdb->prepare( "SELECT initiator_user_id, friend_user_id FROM {$bp->friends->table_name} WHERE is_confirmed = 1 AND ( ( initiator_user_id > %d AND initiator_user_id <= %d ) OR ( friend_user_id > %d AND friend_user_id <= %d ) )", $offset, $offset + $per_query, $offset, $offset + $per_query ) );
+
+			// The previous query will turn up duplicates, so we
+			// filter them here.
+			foreach ( $friendships as $friendship ) {
+				if ( ! isset( $updated[ $friendship->initiator_user_id ] ) ) {
+					BP_Friends_Friendship::total_friend_count( $friendship->initiator_user_id );
+					$updated[ $friendship->initiator_user_id ] = 1;
+				}
+
+				if ( ! isset( $updated[ $friendship->friend_user_id ] ) ) {
+					BP_Friends_Friendship::total_friend_count( $friendship->friend_user_id );
+					$updated[ $friendship->friend_user_id ] = 1;
+				}
+			}
+
+			$offset += $per_query;
+		}
+	} else {
+		return array( 2, sprintf( $statement, $result ) );
+	}
+
+	return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
+}
+
+/**
+ * Recalculate group counts for each user.
+ *
+ * @since 2.0.0
+ *
+ * @return array
+ */
+function bp_admin_repair_group_count() {
+	global $wpdb;
+
+	if ( ! bp_is_active( 'groups' ) ) {
+		return;
+	}
+
+	$statement = __( 'Counting the number of groups for each user&hellip; %s', 'buddypress' );
+	$result    = __( 'Failed!', 'buddypress' );
+
+	$sql_delete = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN ( 'total_group_count' );";
+	if ( is_wp_error( $wpdb->query( $sql_delete ) ) ) {
+		return array( 1, sprintf( $statement, $result ) );
+	}
+
+	$bp = buddypress();
+
+	// Walk through all users on the site.
+	$total_users = $wpdb->get_row( "SELECT count(ID) as c FROM {$wpdb->users}" )->c;
+
+	if ( $total_users > 0 ) {
+		$per_query = 500;
+		$offset = 0;
+		while ( $offset < $total_users ) {
+			// But only bother to update counts for users that have groups.
+			$users = $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE is_confirmed = 1 AND is_banned = 0 AND user_id > %d AND user_id <= %d", $offset, $offset + $per_query ) );
+
+			foreach ( $users as $user ) {
+				BP_Groups_Member::refresh_total_group_count_for_user( $user );
+			}
+
+			$offset += $per_query;
+		}
+	} else {
+		return array( 2, sprintf( $statement, $result ) );
+	}
+
+	return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
+}
+
+/**
+ * Recalculate user-to-blog relationships and useful blog meta data.
+ *
+ * @since 2.1.0
+ *
+ * @return array
+ */
+function bp_admin_repair_blog_records() {
+
+	// Description of this tool, displayed to the user.
+	$statement = __( 'Repopulating Blogs records&hellip; %s', 'buddypress' );
+
+	// Default to failure text.
+	$result    = __( 'Failed!',   'buddypress' );
+
+	// Default to unrepaired.
+	$repair    = false;
+
+	// Run function if blogs component is active.
+	if ( bp_is_active( 'blogs' ) ) {
+		$repair = bp_blogs_record_existing_blogs();
+	}
+
+	// Setup success/fail messaging.
+	if ( true === $repair ) {
+		$result = __( 'Complete!', 'buddypress' );
+	}
+
+	// All done!
+	return array( 0, sprintf( $statement, $result ) );
+}
+
+/**
+ * Recalculate the total number of active site members.
+ *
+ * @since 2.0.0
+ */
+function bp_admin_repair_count_members() {
+	$statement = __( 'Counting the number of active members on the site&hellip; %s', 'buddypress' );
+	delete_transient( 'bp_active_member_count' );
+	bp_core_get_active_member_count();
+	return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
+}
+
+/**
+ * Repair user last_activity data.
+ *
+ * Re-runs the migration from usermeta introduced in BP 2.0.
+ *
+ * @since 2.0.0
+ */
+function bp_admin_repair_last_activity() {
+	$statement = __( 'Determining last activity dates for each user&hellip; %s', 'buddypress' );
+	bp_last_activity_migrate();
+	return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
+}
+
+/**
+ * Assemble admin notices relating success/failure of repair processes.
+ *
+ * @since 2.0.0
+ *
+ * @param string      $message Feedback message.
+ * @param string|bool $class   Unused.
+ * @return false|Closure
+ */
+function bp_admin_tools_feedback( $message, $class = false ) {
+	if ( is_string( $message ) ) {
+		$message = '<p>' . $message . '</p>';
+		$class = $class ? $class : 'updated';
+	} elseif ( is_wp_error( $message ) ) {
+		$errors = $message->get_error_messages();
+
+		switch ( count( $errors ) ) {
+			case 0:
+				return false;
+
+			case 1:
+				$message = '<p>' . $errors[0] . '</p>';
+				break;
+
+			default:
+				$message = '<ul>' . "\n\t" . '<li>' . implode( '</li>' . "\n\t" . '<li>', $errors ) . '</li>' . "\n" . '</ul>';
+				break;
+		}
+
+		$class = $class ? $class : 'error';
+	} else {
+		return false;
+	}
+
+	$message = '<div id="message" class="' . esc_attr( $class ) . '">' . $message . '</div>';
+	$message = str_replace( "'", "\'", $message );
+	$lambda  = function() use ( $message ) { echo $message; };
+
+	add_action( bp_core_do_network_admin() ? 'network_admin_notices' : 'admin_notices', $lambda );
+
+	return $lambda;
+}
+
+/**
+ * Render the Available Tools page.
+ *
+ * We register this page on Network Admin as a top-level home for our
+ * BuddyPress tools. This displays the default content.
+ *
+ * @since 2.0.0
+ */
+function bp_core_admin_available_tools_page() {
+	?>
+	<div class="wrap">
+		<h1><?php esc_attr_e( 'Tools', 'buddypress' ) ?></h1>
+
+		<?php
+
+		/**
+		 * Fires inside the markup used to display the Available Tools page.
+		 *
+		 * @since 2.0.0
+		 */
+		do_action( 'bp_network_tool_box' ); ?>
+
+	</div>
+	<?php
+}
+
+/**
+ * Render an introduction of BuddyPress tools on Available Tools page.
+ *
+ * @since 2.0.0
+ */
+function bp_core_admin_available_tools_intro() {
+	$query_arg = array(
+		'page' => 'bp-tools'
+	);
+
+	$page = bp_core_do_network_admin() ? 'admin.php' : 'tools.php' ;
+	$url  = add_query_arg( $query_arg, bp_get_admin_url( $page ) );
+	?>
+	<div class="card tool-box">
+		<h2><?php esc_html_e( 'BuddyPress Tools', 'buddypress' ) ?></h2>
+		<p>
+			<?php esc_html_e( 'BuddyPress keeps track of various relationships between users, groups, and activity items. Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?>
+			<?php printf( esc_html_x( 'Use the %s to repair these relationships.', 'buddypress tools intro', 'buddypress' ), '<a href="' . esc_url( $url ) . '">' . esc_html__( 'BuddyPress Tools', 'buddypress' ) . '</a>' ); ?>
+		</p>
+	</div>
+	<?php
+}
+
+/**
+ * Delete emails and restore from defaults.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_admin_reinstall_emails() {
+	$switched = false;
+
+	// Switch to the root blog, where the email posts live.
+	if ( ! bp_is_root_blog() ) {
+		switch_to_blog( bp_get_root_blog_id() );
+		bp_register_taxonomies();
+
+		$switched = true;
+	}
+
+	$emails = get_posts( array(
+		'fields'           => 'ids',
+		'post_status'      => 'publish',
+		'post_type'        => bp_get_email_post_type(),
+		'posts_per_page'   => 200,
+		'suppress_filters' => false,
+	) );
+
+	if ( $emails ) {
+		foreach ( $emails as $email_id ) {
+			wp_trash_post( $email_id );
+		}
+	}
+
+	// Make sure we have no orphaned email type terms.
+	$email_types = get_terms( bp_get_email_tax_type(), array(
+		'fields'                 => 'ids',
+		'hide_empty'             => false,
+		'update_term_meta_cache' => false,
+	) );
+
+	if ( $email_types ) {
+		foreach ( $email_types as $term_id ) {
+			wp_delete_term( (int) $term_id, bp_get_email_tax_type() );
+		}
+	}
+
+	require_once( buddypress()->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
+	bp_core_install_emails();
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	return array( 0, __( 'Emails have been successfully reinstalled.', 'buddypress' ) );
+}
+
+/**
+ * Add notice on the "Tools > BuddyPress" page if more sites need recording.
+ *
+ * This notice only shows up in the network admin dashboard.
+ *
+ * @since 2.6.0
+ */
+function bp_core_admin_notice_repopulate_blogs_resume() {
+	$screen = get_current_screen();
+	if ( 'tools_page_bp-tools-network' !== $screen->id ) {
+		return;
+	}
+
+	if ( '' === bp_get_option( '_bp_record_blogs_offset' ) ) {
+		return;
+	}
+
+	echo '<div class="error"><p>' . __( 'It looks like you have more sites to record. Resume recording by checking the "Repopulate site tracking records" option.', 'buddypress' ) . '</p></div>';
+}
+add_action( 'network_admin_notices', 'bp_core_admin_notice_repopulate_blogs_resume' );
Index: /tags/4.0.0-beta1/src/bp-core/admin/css/common-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/css/common-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/css/common-rtl.css	(revision 12265)
@@ -0,0 +1,578 @@
+/*------------------------------------------------------------------------------
+
+This stylesheet is always loaded in wp-admin.
+
+@since 1.6.0
+@since 2.5.0
+
+--------------------------------------------------------------------------------
+TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Welcome Screen
+	1.1 Version Badge
+	1.2 ---
+	1.3 Credits Panel
+2.0 Dashicons
+	2.1 Top level menus
+	2.2 Settings - Components
+	2.3 Tools
+	2.4 Tooltips
+3.0 Users
+	3.1 Users List
+	3.2 Site Notices
+4.0 Emails - Edit page
+5.0 Tools - BuddyPress
+6.0 Plugins page
+------------------------------------------------------------------------------*/
+
+/*------------------------------------------------------------------------------
+ * 1.0 Welcome Screen
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 1.1 Version Badge
+ */
+.bp-badge {
+	color: #d84800;
+	display: inline-block;
+	font: 400 150px/1 dashicons !important;
+}
+
+.bp-badge:before {
+	content: "\f448";
+}
+
+.settings_page_bp-credits code,
+.index_page_bp-about code {
+	background-color: #e0e0e0;
+	color: #636363;
+	font-size: 1em;
+}
+
+/*
+ * 1.3 Credits Panel
+ *
+ * Taken from WP 4.9.5.
+ */
+.bp-about-wrap {
+	position: relative;
+	max-width: 1050px;
+	font-size: 15px;
+}
+
+.bp-about-wrap img {
+	margin: 0;
+	max-width: 100%;
+	height: auto;
+	vertical-align: middle;
+}
+
+.bp-about-wrap p {
+	line-height: 1.5;
+	font-size: 14px;
+}
+
+.bp-about-wrap h2 {
+	margin: 40px 0 0.6em;
+	font-size: 2.7em;
+	line-height: 1.3;
+	font-weight: 300;
+	text-align: center;
+}
+
+.bp-about-wrap h3 {
+	margin: 1.25em 0 0.6em;
+	font-size: 1.4em;
+	line-height: 1.5;
+}
+
+.bp-about-wrap code {
+	font-size: 14px;
+	font-weight: 400;
+}
+
+.bp-about-wrap .about-description {
+	margin-top: 1.4em;
+	font-weight: 400;
+	line-height: 1.6;
+	font-size: 19px;
+}
+
+.bp-about-wrap h3.wp-people-group {
+	margin: 2.6em 0 1.33em;
+	padding: 0;
+	font-size: 16px;
+	line-height: inherit;
+}
+
+.bp-about-wrap .wp-people-group {
+	padding: 0 5px;
+	margin: 0 -5px 0 -15px;
+}
+
+.bp-about-wrap .compact {
+	margin-bottom: 0;
+}
+
+.bp-about-wrap .wp-person {
+	display: inline-block;
+	vertical-align: top;
+	margin-left: 10px;
+	padding-bottom: 15px;
+	height: 70px;
+	width: 280px;
+}
+
+.bp-about-wrap .compact .wp-person {
+	height: auto;
+	width: 180px;
+	padding-bottom: 0;
+	margin-bottom: 0;
+}
+
+.bp-about-wrap .wp-person .gravatar {
+	float: right;
+	margin: 0 0 10px 10px;
+	padding: 1px;
+	width: 60px;
+	height: 60px;
+}
+
+.bp-about-wrap .compact .wp-person .gravatar {
+	width: 30px;
+	height: 30px;
+}
+
+.bp-about-wrap .wp-person .web {
+	margin: 6px 0 2px;
+	font-size: 16px;
+	font-weight: 400;
+	line-height: 2;
+	text-decoration: none;
+}
+
+.bp-about-wrap .wp-person .title {
+	display: block;
+}
+
+.bp-about-wrap p.wp-credits-list a {
+	white-space: nowrap;
+}
+
+@media only screen and (max-width: 500px) {
+
+	.bp-about-wrap {
+		margin-left: 20px;
+		margin-right: 10px;
+	}
+
+	.bp-about-wrap .bp-about-wrap h1 {
+		margin-left: 0;
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 2.0 Dashicons
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 2.1 Top level menus
+ */
+#adminmenu #toplevel_page_bp-activity .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-activity_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-activity_network .wp-menu-image:before {
+	content: "\f452";
+}
+
+#adminmenu #toplevel_page_bp-groups .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-groups_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-groups_network .wp-menu-image:before {
+	content: "\f456";
+}
+
+#adminmenu #toplevel_page_bp-notifications .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-notifications_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-notifications_network .wp-menu-image:before {
+	content: "\f439";
+}
+
+#adminmenu #toplevel_page_bp-messages .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-messages_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-messages_network .wp-menu-image:before {
+	content: "\f457";
+}
+
+#adminmenu #toplevel_page_bp-friends .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-friends_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-friends_network .wp-menu-image:before {
+	content: "\f454";
+}
+
+#adminmenu #toplevel_page_bp-settings .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-settings_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-settings_network .wp-menu-image:before {
+	content: "\f108";
+}
+
+#adminmenu li.toplevel_page_bp-components .wp-menu-image,
+#adminmenu li.toplevel_page_bp-general-settings .wp-menu-image {
+	content: "\f448";
+}
+
+/*
+ * 2.2 Settings - Components
+ */
+.settings_page_bp-components td.plugin-title span {
+	float: right;
+	width: 18px;
+	height: 18px;
+	margin-left: 5px;
+}
+
+.settings_page_bp-components td.plugin-title span:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.settings_page_bp-components tr.activity td.plugin-title span:before {
+	content: "\f452";
+}
+
+.settings_page_bp-components tr.notifications td.plugin-title span:before {
+	content: "\f339";
+}
+
+.settings_page_bp-components tr.xprofile td.plugin-title span:before {
+	content: "\f336";
+}
+
+.settings_page_bp-components tr.settings td.plugin-title span:before {
+	content: "\f108";
+}
+
+.settings_page_bp-components tr.groups td.plugin-title span:before {
+	content: "\f456";
+}
+
+.settings_page_bp-components tr.messages td.plugin-title span:before {
+	content: "\f457";
+}
+
+.settings_page_bp-components tr.blogs td.plugin-title span:before {
+	content: "\f120";
+}
+
+.settings_page_bp-components tr.friends td.plugin-title span:before {
+	content: "\f454";
+}
+
+.settings_page_bp-components tr.core td.plugin-title span:before {
+	content: "\f448";
+}
+
+.settings_page_bp-components tr.members td.plugin-title span:before {
+	content: "\f307";
+}
+
+#bp-admin-component-form .wp-list-table.plugins .plugin-title {
+	width: 25%;
+}
+
+@media screen and ( max-width: 782px ) {
+
+	.settings_page_bp-components td.plugin-title span {
+		margin-top: 5px;
+	}
+
+	#bp-admin-component-form .wp-list-table.plugins .plugin-title {
+		display: block;
+		width: auto;
+	}
+
+	#bp-admin-component-form .subsubsub {
+		margin-bottom: 0;
+		padding-bottom: 35px;
+	}
+}
+
+/*
+ * 2.3 Tools
+ */
+#adminmenu .toplevel_page_network-tools div.wp-menu-image:before {
+	content: "";
+}
+
+/*
+ * 2.4 Tooltips
+ */
+.bp-tooltip {
+	position: relative;
+}
+
+.bp-tooltip:after {
+	background: #fff;
+	border: 1px solid #aaa;
+	border-collapse: separate;
+	border-radius: 1px;
+	box-shadow: -1px 1px 0 1px rgba(132, 132, 132, 0.3);
+	color: #000;
+	content: attr(data-bp-tooltip);
+	display: none;
+	font-family: sans-serif;
+	font-size: 11px;
+	font-weight: 400;
+	letter-spacing: normal;
+	line-height: 1.5;
+	margin-top: 10px;
+	max-width: 240px;
+	opacity: 0;
+	padding: 3px 6px;
+	position: absolute;
+	left: 50%;
+	text-align: center;
+	text-decoration: none;
+	text-shadow: none;
+	text-transform: none;
+	top: 100%;
+	transform: translateX(-50%);
+	transition: opacity 2s ease-out;
+	white-space: pre;
+	word-wrap: break-word;
+	z-index: 998;
+}
+
+.bp-hello-close .bp-tooltip:after {
+	left: 0;
+	text-align: left;
+	transform: translateX(0);
+}
+
+.bp-hello-social .bp-tooltip:after {
+	bottom: 120%;
+	margin-bottom: 20px;
+	margin-top: 0;
+	top: auto;
+	transform: translateX(-15%);
+}
+
+.bp-tooltip:hover:after,
+.bp-tooltip:active:after,
+.bp-tooltip:focus:after {
+	display: inline-block;
+	opacity: 1;
+	overflow: visible;
+	text-decoration: none;
+	z-index: 999;
+}
+
+/*------------------------------------------------------------------------------
+ * 3.0 Users
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 3.1 Users List
+ */
+body.site-users-php th#role,
+body.users-php th#role,
+body.users_page_bp-signups th#count_sent {
+	width: 10%;
+}
+
+body.site-users-php th#name,
+body.site-users-php th#email,
+body.users-php th#name,
+body.users-php th#registered,
+body.users-php th#email,
+body.users_page_bp-signups th#name,
+body.users_page_bp-signups th#registered,
+body.users_page_bp-signups th#email,
+body.users_page_bp-signups th#date_sent {
+	width: 15%;
+}
+
+body.users-php th#blogs,
+body.users_page_bp-signups th#blogs {
+	width: 20%;
+}
+
+body.users_page_bp-signups th.column-count_sent,
+body.users_page_bp-signups td.count_sent {
+	text-align: center;
+}
+
+.bp-signups-list table {
+	margin: 1em 0;
+}
+
+.bp-signups-list .column-fields {
+	font-weight: 700;
+}
+
+/*
+ * 3.2 Site Notices
+ */
+.bp-new-notice-panel {
+	background: #fff;
+	border: 1px solid #e5e5e5;
+	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+	font-size: 13px;
+	line-height: 2.1;
+	margin: 1.5em 0 3em;
+	overflow: auto;
+	padding: 10px 25px 25px;
+	position: relative;
+}
+
+.bp-new-notice-panel label {
+	clear: both;
+	float: right;
+	margin-left: 3%;
+	width: 20%;
+}
+
+.bp-new-notice-panel input,
+.bp-new-notice-panel textarea {
+	clear: none;
+	margin-bottom: 1em;
+	width: 75%;
+}
+
+.bp-new-notice-panel input[type="text"]:after,
+.bp-new-notice-panel textarea:after {
+	clear: both;
+	content: " ";
+	display: table;
+}
+
+.bp-new-notice-panel .button-primary {
+	margin-right: 23%;
+	width: auto;
+}
+
+.bp-notice-about {
+	font-size: 1em;
+	margin-bottom: 1em;
+}
+
+.bp-new-notice {
+	margin-bottom: 1em;
+	margin-top: 0;
+}
+
+.bp-notices-list {
+	margin-bottom: 0;
+}
+
+@media screen and (max-width: 782px) {
+
+	.bp-new-notice-panel {
+		margin-bottom: 1.5em;
+	}
+
+	.bp-new-notice-panel input,
+	.bp-new-notice-panel textarea {
+		margin-right: 0;
+		width: 100%;
+	}
+
+	.bp-new-notice-panel .button-primary {
+		margin-right: 0;
+		width: auto;
+	}
+
+	.bp-new-notice-panel .button {
+		max-width: 45%;
+		word-wrap: break-word;
+	}
+
+	.bp-notice-about {
+		margin-top: 0;
+		margin-bottom: 1em;
+	}
+
+	.bp-new-notice {
+		margin-bottom: 0.5em;
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 4.0 Emails - Edit Page
+ *----------------------------------------------------------------------------*/
+body.post-type-bp-email #excerpt {
+	height: auto;
+}
+
+body.post-type-bp-email th#situation {
+	width: 20%;
+}
+
+body.post-type-bp-email td.column-situation ul {
+	margin: 0;
+}
+
+body.post-type-bp-email .categorydiv label {
+	display: block;
+	float: right;
+	padding-right: 25px;
+	text-indent: -25px;
+}
+
+/*------------------------------------------------------------------------------
+ * 5.0 Tools - BuddyPress
+ *----------------------------------------------------------------------------*/
+.tools_page_bp-tools .wrap {
+	max-width: 950px;
+}
+
+.tools_page_bp-tools p {
+	line-height: 2;
+}
+
+.tools_page_bp-tools fieldset {
+	margin: 2em 0 0;
+}
+
+.tools_page_bp-tools legend {
+	color: #23282d;
+	font-size: 1.3em;
+	font-weight: 600;
+	margin: 1em 0;
+}
+
+.tools_page_bp-tools label {
+	clear: right;
+	display: block;
+	line-height: 1.5;
+	margin: 0 0 1em;
+	vertical-align: middle;
+}
+
+@media screen and (max-width: 782px) {
+
+	.tools_page_bp-tools p {
+		line-height: 1.5;
+	}
+
+	.tools_page_bp-tools label {
+		margin-bottom: 1em;
+		padding-left: 25px;
+		text-indent: -33px;
+	}
+
+	.tools_page_bp-tools .checkbox {
+		padding: 0 30px 0 0;
+	}
+}
+
+
+/*------------------------------------------------------------------------------
+ * 6.0 Plugins page
+ *----------------------------------------------------------------------------*/
+#buddypress-update.not-shiny .update-message {
+	border-right: 0;
+	padding-right: 36px;
+}
+
+#buddypress-update.not-shiny .update-message:before {
+	content: "\f534";
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/css/common.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/css/common.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/css/common.css	(revision 12265)
@@ -0,0 +1,578 @@
+/*------------------------------------------------------------------------------
+
+This stylesheet is always loaded in wp-admin.
+
+@since 1.6.0
+@since 2.5.0
+
+--------------------------------------------------------------------------------
+TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Welcome Screen
+	1.1 Version Badge
+	1.2 ---
+	1.3 Credits Panel
+2.0 Dashicons
+	2.1 Top level menus
+	2.2 Settings - Components
+	2.3 Tools
+	2.4 Tooltips
+3.0 Users
+	3.1 Users List
+	3.2 Site Notices
+4.0 Emails - Edit page
+5.0 Tools - BuddyPress
+6.0 Plugins page
+------------------------------------------------------------------------------*/
+
+/*------------------------------------------------------------------------------
+ * 1.0 Welcome Screen
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 1.1 Version Badge
+ */
+.bp-badge {
+	color: #d84800;
+	display: inline-block;
+	font: 400 150px/1 dashicons !important;
+}
+
+.bp-badge:before {
+	content: "\f448";
+}
+
+.settings_page_bp-credits code,
+.index_page_bp-about code {
+	background-color: #e0e0e0;
+	color: #636363;
+	font-size: 1em;
+}
+
+/*
+ * 1.3 Credits Panel
+ *
+ * Taken from WP 4.9.5.
+ */
+.bp-about-wrap {
+	position: relative;
+	max-width: 1050px;
+	font-size: 15px;
+}
+
+.bp-about-wrap img {
+	margin: 0;
+	max-width: 100%;
+	height: auto;
+	vertical-align: middle;
+}
+
+.bp-about-wrap p {
+	line-height: 1.5;
+	font-size: 14px;
+}
+
+.bp-about-wrap h2 {
+	margin: 40px 0 0.6em;
+	font-size: 2.7em;
+	line-height: 1.3;
+	font-weight: 300;
+	text-align: center;
+}
+
+.bp-about-wrap h3 {
+	margin: 1.25em 0 0.6em;
+	font-size: 1.4em;
+	line-height: 1.5;
+}
+
+.bp-about-wrap code {
+	font-size: 14px;
+	font-weight: 400;
+}
+
+.bp-about-wrap .about-description {
+	margin-top: 1.4em;
+	font-weight: 400;
+	line-height: 1.6;
+	font-size: 19px;
+}
+
+.bp-about-wrap h3.wp-people-group {
+	margin: 2.6em 0 1.33em;
+	padding: 0;
+	font-size: 16px;
+	line-height: inherit;
+}
+
+.bp-about-wrap .wp-people-group {
+	padding: 0 5px;
+	margin: 0 -15px 0 -5px;
+}
+
+.bp-about-wrap .compact {
+	margin-bottom: 0;
+}
+
+.bp-about-wrap .wp-person {
+	display: inline-block;
+	vertical-align: top;
+	margin-right: 10px;
+	padding-bottom: 15px;
+	height: 70px;
+	width: 280px;
+}
+
+.bp-about-wrap .compact .wp-person {
+	height: auto;
+	width: 180px;
+	padding-bottom: 0;
+	margin-bottom: 0;
+}
+
+.bp-about-wrap .wp-person .gravatar {
+	float: left;
+	margin: 0 10px 10px 0;
+	padding: 1px;
+	width: 60px;
+	height: 60px;
+}
+
+.bp-about-wrap .compact .wp-person .gravatar {
+	width: 30px;
+	height: 30px;
+}
+
+.bp-about-wrap .wp-person .web {
+	margin: 6px 0 2px;
+	font-size: 16px;
+	font-weight: 400;
+	line-height: 2;
+	text-decoration: none;
+}
+
+.bp-about-wrap .wp-person .title {
+	display: block;
+}
+
+.bp-about-wrap p.wp-credits-list a {
+	white-space: nowrap;
+}
+
+@media only screen and (max-width: 500px) {
+
+	.bp-about-wrap {
+		margin-right: 20px;
+		margin-left: 10px;
+	}
+
+	.bp-about-wrap .bp-about-wrap h1 {
+		margin-right: 0;
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 2.0 Dashicons
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 2.1 Top level menus
+ */
+#adminmenu #toplevel_page_bp-activity .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-activity_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-activity_network .wp-menu-image:before {
+	content: "\f452";
+}
+
+#adminmenu #toplevel_page_bp-groups .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-groups_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-groups_network .wp-menu-image:before {
+	content: "\f456";
+}
+
+#adminmenu #toplevel_page_bp-notifications .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-notifications_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-notifications_network .wp-menu-image:before {
+	content: "\f439";
+}
+
+#adminmenu #toplevel_page_bp-messages .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-messages_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-messages_network .wp-menu-image:before {
+	content: "\f457";
+}
+
+#adminmenu #toplevel_page_bp-friends .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-friends_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-friends_network .wp-menu-image:before {
+	content: "\f454";
+}
+
+#adminmenu #toplevel_page_bp-settings .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-settings_user .wp-menu-image:before,
+#adminmenu #toplevel_page_bp-settings_network .wp-menu-image:before {
+	content: "\f108";
+}
+
+#adminmenu li.toplevel_page_bp-components .wp-menu-image,
+#adminmenu li.toplevel_page_bp-general-settings .wp-menu-image {
+	content: "\f448";
+}
+
+/*
+ * 2.2 Settings - Components
+ */
+.settings_page_bp-components td.plugin-title span {
+	float: left;
+	width: 18px;
+	height: 18px;
+	margin-right: 5px;
+}
+
+.settings_page_bp-components td.plugin-title span:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.settings_page_bp-components tr.activity td.plugin-title span:before {
+	content: "\f452";
+}
+
+.settings_page_bp-components tr.notifications td.plugin-title span:before {
+	content: "\f339";
+}
+
+.settings_page_bp-components tr.xprofile td.plugin-title span:before {
+	content: "\f336";
+}
+
+.settings_page_bp-components tr.settings td.plugin-title span:before {
+	content: "\f108";
+}
+
+.settings_page_bp-components tr.groups td.plugin-title span:before {
+	content: "\f456";
+}
+
+.settings_page_bp-components tr.messages td.plugin-title span:before {
+	content: "\f457";
+}
+
+.settings_page_bp-components tr.blogs td.plugin-title span:before {
+	content: "\f120";
+}
+
+.settings_page_bp-components tr.friends td.plugin-title span:before {
+	content: "\f454";
+}
+
+.settings_page_bp-components tr.core td.plugin-title span:before {
+	content: "\f448";
+}
+
+.settings_page_bp-components tr.members td.plugin-title span:before {
+	content: "\f307";
+}
+
+#bp-admin-component-form .wp-list-table.plugins .plugin-title {
+	width: 25%;
+}
+
+@media screen and ( max-width: 782px ) {
+
+	.settings_page_bp-components td.plugin-title span {
+		margin-top: 5px;
+	}
+
+	#bp-admin-component-form .wp-list-table.plugins .plugin-title {
+		display: block;
+		width: auto;
+	}
+
+	#bp-admin-component-form .subsubsub {
+		margin-bottom: 0;
+		padding-bottom: 35px;
+	}
+}
+
+/*
+ * 2.3 Tools
+ */
+#adminmenu .toplevel_page_network-tools div.wp-menu-image:before {
+	content: "";
+}
+
+/*
+ * 2.4 Tooltips
+ */
+.bp-tooltip {
+	position: relative;
+}
+
+.bp-tooltip:after {
+	background: #fff;
+	border: 1px solid #aaa;
+	border-collapse: separate;
+	border-radius: 1px;
+	box-shadow: 1px 1px 0 1px rgba(132, 132, 132, 0.3);
+	color: #000;
+	content: attr(data-bp-tooltip);
+	display: none;
+	font-family: sans-serif;
+	font-size: 11px;
+	font-weight: 400;
+	letter-spacing: normal;
+	line-height: 1.5;
+	margin-top: 10px;
+	max-width: 240px;
+	opacity: 0;
+	padding: 3px 6px;
+	position: absolute;
+	right: 50%;
+	text-align: center;
+	text-decoration: none;
+	text-shadow: none;
+	text-transform: none;
+	top: 100%;
+	transform: translateX(50%);
+	transition: opacity 2s ease-out;
+	white-space: pre;
+	word-wrap: break-word;
+	z-index: 998;
+}
+
+.bp-hello-close .bp-tooltip:after {
+	right: 0;
+	text-align: right;
+	transform: translateX(0);
+}
+
+.bp-hello-social .bp-tooltip:after {
+	bottom: 120%;
+	margin-bottom: 20px;
+	margin-top: 0;
+	top: auto;
+	transform: translateX(15%);
+}
+
+.bp-tooltip:hover:after,
+.bp-tooltip:active:after,
+.bp-tooltip:focus:after {
+	display: inline-block;
+	opacity: 1;
+	overflow: visible;
+	text-decoration: none;
+	z-index: 999;
+}
+
+/*------------------------------------------------------------------------------
+ * 3.0 Users
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 3.1 Users List
+ */
+body.site-users-php th#role,
+body.users-php th#role,
+body.users_page_bp-signups th#count_sent {
+	width: 10%;
+}
+
+body.site-users-php th#name,
+body.site-users-php th#email,
+body.users-php th#name,
+body.users-php th#registered,
+body.users-php th#email,
+body.users_page_bp-signups th#name,
+body.users_page_bp-signups th#registered,
+body.users_page_bp-signups th#email,
+body.users_page_bp-signups th#date_sent {
+	width: 15%;
+}
+
+body.users-php th#blogs,
+body.users_page_bp-signups th#blogs {
+	width: 20%;
+}
+
+body.users_page_bp-signups th.column-count_sent,
+body.users_page_bp-signups td.count_sent {
+	text-align: center;
+}
+
+.bp-signups-list table {
+	margin: 1em 0;
+}
+
+.bp-signups-list .column-fields {
+	font-weight: 700;
+}
+
+/*
+ * 3.2 Site Notices
+ */
+.bp-new-notice-panel {
+	background: #fff;
+	border: 1px solid #e5e5e5;
+	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+	font-size: 13px;
+	line-height: 2.1;
+	margin: 1.5em 0 3em;
+	overflow: auto;
+	padding: 10px 25px 25px;
+	position: relative;
+}
+
+.bp-new-notice-panel label {
+	clear: both;
+	float: left;
+	margin-right: 3%;
+	width: 20%;
+}
+
+.bp-new-notice-panel input,
+.bp-new-notice-panel textarea {
+	clear: none;
+	margin-bottom: 1em;
+	width: 75%;
+}
+
+.bp-new-notice-panel input[type="text"]:after,
+.bp-new-notice-panel textarea:after {
+	clear: both;
+	content: " ";
+	display: table;
+}
+
+.bp-new-notice-panel .button-primary {
+	margin-left: 23%;
+	width: auto;
+}
+
+.bp-notice-about {
+	font-size: 1em;
+	margin-bottom: 1em;
+}
+
+.bp-new-notice {
+	margin-bottom: 1em;
+	margin-top: 0;
+}
+
+.bp-notices-list {
+	margin-bottom: 0;
+}
+
+@media screen and (max-width: 782px) {
+
+	.bp-new-notice-panel {
+		margin-bottom: 1.5em;
+	}
+
+	.bp-new-notice-panel input,
+	.bp-new-notice-panel textarea {
+		margin-left: 0;
+		width: 100%;
+	}
+
+	.bp-new-notice-panel .button-primary {
+		margin-left: 0;
+		width: auto;
+	}
+
+	.bp-new-notice-panel .button {
+		max-width: 45%;
+		word-wrap: break-word;
+	}
+
+	.bp-notice-about {
+		margin-top: 0;
+		margin-bottom: 1em;
+	}
+
+	.bp-new-notice {
+		margin-bottom: 0.5em;
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 4.0 Emails - Edit Page
+ *----------------------------------------------------------------------------*/
+body.post-type-bp-email #excerpt {
+	height: auto;
+}
+
+body.post-type-bp-email th#situation {
+	width: 20%;
+}
+
+body.post-type-bp-email td.column-situation ul {
+	margin: 0;
+}
+
+body.post-type-bp-email .categorydiv label {
+	display: block;
+	float: left;
+	padding-left: 25px;
+	text-indent: -25px;
+}
+
+/*------------------------------------------------------------------------------
+ * 5.0 Tools - BuddyPress
+ *----------------------------------------------------------------------------*/
+.tools_page_bp-tools .wrap {
+	max-width: 950px;
+}
+
+.tools_page_bp-tools p {
+	line-height: 2;
+}
+
+.tools_page_bp-tools fieldset {
+	margin: 2em 0 0;
+}
+
+.tools_page_bp-tools legend {
+	color: #23282d;
+	font-size: 1.3em;
+	font-weight: 600;
+	margin: 1em 0;
+}
+
+.tools_page_bp-tools label {
+	clear: left;
+	display: block;
+	line-height: 1.5;
+	margin: 0 0 1em;
+	vertical-align: middle;
+}
+
+@media screen and (max-width: 782px) {
+
+	.tools_page_bp-tools p {
+		line-height: 1.5;
+	}
+
+	.tools_page_bp-tools label {
+		margin-bottom: 1em;
+		padding-right: 25px;
+		text-indent: -33px;
+	}
+
+	.tools_page_bp-tools .checkbox {
+		padding: 0 0 0 30px;
+	}
+}
+
+
+/*------------------------------------------------------------------------------
+ * 6.0 Plugins page
+ *----------------------------------------------------------------------------*/
+#buddypress-update.not-shiny .update-message {
+	border-left: 0;
+	padding-left: 36px;
+}
+
+#buddypress-update.not-shiny .update-message:before {
+	content: "\f534";
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/css/customizer-controls-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/css/customizer-controls-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/css/customizer-controls-rtl.css	(revision 12265)
@@ -0,0 +1,10 @@
+.customize-control-range output {
+	background-color: #fff;
+	border: 1px solid #ddd;
+	box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07);
+	float: right;
+	margin-left: 10px;
+	margin-top: -4px;
+	min-width: 16px;
+	padding: 5px;
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/css/customizer-controls.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/css/customizer-controls.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/css/customizer-controls.css	(revision 12265)
@@ -0,0 +1,10 @@
+.customize-control-range output {
+	background-color: #fff;
+	border: 1px solid #ddd;
+	box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07);
+	float: left;
+	margin-right: 10px;
+	margin-top: -4px;
+	min-width: 16px;
+	padding: 5px;
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/css/hello-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/css/hello-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/css/hello-rtl.css	(revision 12265)
@@ -0,0 +1,323 @@
+/*------------------------------------------------------------------------------
+Loaded in wp-admin for query string `hello=buddypress`.
+
+@since 3.0.0
+
+--------------------------------------------------------------------------------
+TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 - Typography and colour
+2.0 - Dashicons
+3.0 - Elements
+	3.1 - Backdrop and container
+	3.2 - Modal footer
+	3.3 - Modal header
+	3.4 - Modal content
+4.0 - Content styles
+	4.1 - Backdrop and container
+	4.2 - Footer content
+	4.3 - Header content
+	4.4 - Content content
+5.0 - Media
+6.0 - Media Queries
+	6.1 - Desktop Medium
+	6.2 - Desktop Large
+------------------------------------------------------------------------------*/
+/*------------------------------------------------------------------------------
+ * 1.0 - Typography and colour
+ *----------------------------------------------------------------------------*/
+:root {
+	--bp-hello-color-primary: #d34600;
+	--bp-hello-color-secondary: #e5e5e5;
+	--bp-hello-container-size: 15%;
+}
+
+#bp-hello-container a {
+	color: var(--bp-hello-color-primary);
+}
+
+#bp-hello-container a:hover {
+	transition: all 0.1s ease-in-out;
+	color: black;
+}
+
+#bp-hello-container .bp-hello-header h1 {
+	line-height: 1.7;
+	font-size: 21px;
+	font-weight: 400;
+}
+
+.bp-hello-content p {
+	font-size: 16px;
+}
+
+/*------------------------------------------------------------------------------
+ * 2.0 - Dashicons
+ *----------------------------------------------------------------------------*/
+.bp-hello-close .button {
+	padding: 5px !important;
+}
+
+.bp-hello-close .close-modal:before {
+	content: "\f158";
+	color: #23282d;
+	/* wp toolbar */
+	font: 400 1.5em/1 dashicons;
+	speak: none;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	background-image: none !important;
+}
+
+.bp-hello-close .close-modal:focus:before, .bp-hello-close .close-modal:hover:before {
+	transition: all 0.1s ease-in-out;
+	color: var(--bp-hello-color-primary);
+}
+
+.bp-hello-social li a:before {
+	color: #23282d;
+	/* wp toolbar */
+	font: 400 30px/0.6 dashicons;
+	speak: none;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	background-image: none !important;
+}
+
+.bp-hello-social li a:hover:before {
+	transition: all 0.1s ease-in-out;
+	color: var(--bp-hello-color-primary);
+}
+
+.bp-hello-social li a.support:before {
+	content: "\f448";
+}
+
+.bp-hello-social li a.twitter:before {
+	content: "\f301";
+}
+
+/*------------------------------------------------------------------------------
+ * 3.0 - Elements
+ *----------------------------------------------------------------------------*/
+/*
+ * 3.1 - Backdrop and container
+ */
+#bp-hello-backdrop {
+	position: fixed;
+	top: 0;
+	right: 0;
+	left: 0;
+	bottom: 0;
+	z-index: 9998;
+	display: none;
+}
+
+#bp-hello-container {
+	position: fixed;
+	top: 0;
+	bottom: 80px;
+	z-index: 99999;
+}
+
+.bp-disable-scroll {
+	overflow: hidden;
+}
+
+/*
+ * 3.2 - Modal footer
+ */
+.bp-hello-footer {
+	display: flex;
+	flex-wrap: wrap;
+	height: 58px;
+	max-height: 58px;
+}
+
+.bp-hello-social-cta,
+.bp-hello-social-links {
+	flex-basis: 50%;
+}
+
+.bp-hello-social-links ul {
+	display: inline-block;
+}
+
+.bp-hello-social li {
+	position: relative;
+	bottom: -5px;
+	display: inline-block;
+	list-style-type: none;
+	margin-bottom: 0;
+}
+
+.bp-hello-social li:last-child a {
+	margin-right: 4px;
+}
+
+/*
+ * 3.3 - Modal header
+ */
+.bp-hello-header {
+	height: 58px;
+	max-height: 58px;
+}
+
+/*
+ * 3.4 - Modal content
+ */
+.bp-hello-content {
+	padding: 0 25px;
+	height: calc(100% - 58px);
+	overflow-y: auto;
+	-webkit-overflow-scrolling: touch;
+}
+
+/*------------------------------------------------------------------------------
+ * 4.0 - Content styles
+ *----------------------------------------------------------------------------*/
+/*
+ * 4.1 - Backdrop and container
+ */
+#bp-hello-backdrop {
+	background-color: rgba(0, 0, 0, 0.8);
+	transition: opacity 0.15s ease-out;
+}
+
+#bp-hello-container {
+	background-color: white;
+}
+
+/*
+ * 4.2 - Footer content
+ */
+.bp-hello-footer {
+	border-radius: 0 0 3px 3px;
+	background-color: white;
+	border-top: 1px solid var(--bp-hello-color-secondary);
+	padding: 6px 25px;
+}
+
+.bp-hello-footer p {
+	font-size: 15px;
+}
+
+.bp-hello-social-cta {
+	text-align: right;
+}
+
+.bp-hello-social-links {
+	text-align: left;
+}
+
+.bp-hello-social li a {
+	text-decoration: none;
+}
+
+/*
+ * 4.3 - Header content
+ */
+.bp-hello-header {
+	padding: 6px 25px;
+}
+
+.bp-hello-header h1 {
+	width: calc(100% - 51px);
+}
+
+#bp-hello-container .bp-hello-header {
+	border-bottom: 1px solid var(--bp-hello-color-secondary);
+}
+
+.bp-hello-title {
+	text-align: right;
+}
+
+.bp-hello-close {
+	position: absolute;
+	top: 20px;
+	left: 25px;
+	text-align: left;
+}
+
+/*
+ * 4.4 - Content content
+ */
+.bp-hello-content {
+	background-color: white;
+}
+
+/*------------------------------------------------------------------------------
+ * 5.0 - Media
+ *----------------------------------------------------------------------------*/
+.bp-hello-content img {
+	border-radius: 2px;
+	max-width: 100%;
+}
+
+.bp-hello-content iframe {
+	width: 100%;
+}
+
+/*------------------------------------------------------------------------------
+ * 6.0 - Media Queries
+ *----------------------------------------------------------------------------*/
+/*
+ * 6.1 - Desktop Medium
+ */
+@media only screen and (min-width: 1024px) {
+	#bp-hello-backdrop {
+		display: block;
+	}
+	#bp-hello-container {
+		position: fixed;
+		top: 60px;
+		right: var(--bp-hello-container-size);
+		left: var(--bp-hello-container-size);
+		bottom: 30px;
+		z-index: 9999;
+		border-radius: 3px;
+	}
+	#bp-hello-container .bp-hello-header h1 {
+		line-height: inherit;
+	}
+	.bp-hello-header {
+		height: auto;
+		max-height: inherit;
+		padding: 6px 30px;
+	}
+	.bp-hello-close {
+		left: 30px;
+	}
+	.bp-hello-close .close-modal:before {
+		line-height: 0.7;
+	}
+	.bp-hello-footer {
+		position: fixed;
+		right: var(--bp-hello-container-size);
+		left: var(--bp-hello-container-size);
+		bottom: 30px;
+		z-index: 10000;
+		height: auto;
+		max-height: inherit;
+		padding: 6px 30px;
+	}
+	.bp-hello-content {
+		height: calc(100% - 90px);
+		padding: 0 30px;
+	}
+	.bp-hello-content p {
+		font-size: 14px;
+	}
+}
+
+/**
+ * 6.2 - Desktop Large
+ */
+@media screen and (min-width: 1280px) {
+	#bp-hello-container,
+	.bp-hello-footer {
+		right: calc((100% - 896px) / 2);
+		left: calc((100% - 896px) / 2);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/css/hello.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/css/hello.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/css/hello.css	(revision 12265)
@@ -0,0 +1,323 @@
+/*------------------------------------------------------------------------------
+Loaded in wp-admin for query string `hello=buddypress`.
+
+@since 3.0.0
+
+--------------------------------------------------------------------------------
+TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 - Typography and colour
+2.0 - Dashicons
+3.0 - Elements
+	3.1 - Backdrop and container
+	3.2 - Modal footer
+	3.3 - Modal header
+	3.4 - Modal content
+4.0 - Content styles
+	4.1 - Backdrop and container
+	4.2 - Footer content
+	4.3 - Header content
+	4.4 - Content content
+5.0 - Media
+6.0 - Media Queries
+	6.1 - Desktop Medium
+	6.2 - Desktop Large
+------------------------------------------------------------------------------*/
+/*------------------------------------------------------------------------------
+ * 1.0 - Typography and colour
+ *----------------------------------------------------------------------------*/
+:root {
+	--bp-hello-color-primary: #d34600;
+	--bp-hello-color-secondary: #e5e5e5;
+	--bp-hello-container-size: 15%;
+}
+
+#bp-hello-container a {
+	color: var(--bp-hello-color-primary);
+}
+
+#bp-hello-container a:hover {
+	transition: all 0.1s ease-in-out;
+	color: black;
+}
+
+#bp-hello-container .bp-hello-header h1 {
+	line-height: 1.7;
+	font-size: 21px;
+	font-weight: 400;
+}
+
+.bp-hello-content p {
+	font-size: 16px;
+}
+
+/*------------------------------------------------------------------------------
+ * 2.0 - Dashicons
+ *----------------------------------------------------------------------------*/
+.bp-hello-close .button {
+	padding: 5px !important;
+}
+
+.bp-hello-close .close-modal:before {
+	content: "\f158";
+	color: #23282d;
+	/* wp toolbar */
+	font: 400 1.5em/1 dashicons;
+	speak: none;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	background-image: none !important;
+}
+
+.bp-hello-close .close-modal:focus:before, .bp-hello-close .close-modal:hover:before {
+	transition: all 0.1s ease-in-out;
+	color: var(--bp-hello-color-primary);
+}
+
+.bp-hello-social li a:before {
+	color: #23282d;
+	/* wp toolbar */
+	font: 400 30px/0.6 dashicons;
+	speak: none;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	background-image: none !important;
+}
+
+.bp-hello-social li a:hover:before {
+	transition: all 0.1s ease-in-out;
+	color: var(--bp-hello-color-primary);
+}
+
+.bp-hello-social li a.support:before {
+	content: "\f448";
+}
+
+.bp-hello-social li a.twitter:before {
+	content: "\f301";
+}
+
+/*------------------------------------------------------------------------------
+ * 3.0 - Elements
+ *----------------------------------------------------------------------------*/
+/*
+ * 3.1 - Backdrop and container
+ */
+#bp-hello-backdrop {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 9998;
+	display: none;
+}
+
+#bp-hello-container {
+	position: fixed;
+	top: 0;
+	bottom: 80px;
+	z-index: 99999;
+}
+
+.bp-disable-scroll {
+	overflow: hidden;
+}
+
+/*
+ * 3.2 - Modal footer
+ */
+.bp-hello-footer {
+	display: flex;
+	flex-wrap: wrap;
+	height: 58px;
+	max-height: 58px;
+}
+
+.bp-hello-social-cta,
+.bp-hello-social-links {
+	flex-basis: 50%;
+}
+
+.bp-hello-social-links ul {
+	display: inline-block;
+}
+
+.bp-hello-social li {
+	position: relative;
+	bottom: -5px;
+	display: inline-block;
+	list-style-type: none;
+	margin-bottom: 0;
+}
+
+.bp-hello-social li:last-child a {
+	margin-left: 4px;
+}
+
+/*
+ * 3.3 - Modal header
+ */
+.bp-hello-header {
+	height: 58px;
+	max-height: 58px;
+}
+
+/*
+ * 3.4 - Modal content
+ */
+.bp-hello-content {
+	padding: 0 25px;
+	height: calc(100% - 58px);
+	overflow-y: auto;
+	-webkit-overflow-scrolling: touch;
+}
+
+/*------------------------------------------------------------------------------
+ * 4.0 - Content styles
+ *----------------------------------------------------------------------------*/
+/*
+ * 4.1 - Backdrop and container
+ */
+#bp-hello-backdrop {
+	background-color: rgba(0, 0, 0, 0.8);
+	transition: opacity 0.15s ease-out;
+}
+
+#bp-hello-container {
+	background-color: white;
+}
+
+/*
+ * 4.2 - Footer content
+ */
+.bp-hello-footer {
+	border-radius: 0 0 3px 3px;
+	background-color: white;
+	border-top: 1px solid var(--bp-hello-color-secondary);
+	padding: 6px 25px;
+}
+
+.bp-hello-footer p {
+	font-size: 15px;
+}
+
+.bp-hello-social-cta {
+	text-align: left;
+}
+
+.bp-hello-social-links {
+	text-align: right;
+}
+
+.bp-hello-social li a {
+	text-decoration: none;
+}
+
+/*
+ * 4.3 - Header content
+ */
+.bp-hello-header {
+	padding: 6px 25px;
+}
+
+.bp-hello-header h1 {
+	width: calc(100% - 51px);
+}
+
+#bp-hello-container .bp-hello-header {
+	border-bottom: 1px solid var(--bp-hello-color-secondary);
+}
+
+.bp-hello-title {
+	text-align: left;
+}
+
+.bp-hello-close {
+	position: absolute;
+	top: 20px;
+	right: 25px;
+	text-align: right;
+}
+
+/*
+ * 4.4 - Content content
+ */
+.bp-hello-content {
+	background-color: white;
+}
+
+/*------------------------------------------------------------------------------
+ * 5.0 - Media
+ *----------------------------------------------------------------------------*/
+.bp-hello-content img {
+	border-radius: 2px;
+	max-width: 100%;
+}
+
+.bp-hello-content iframe {
+	width: 100%;
+}
+
+/*------------------------------------------------------------------------------
+ * 6.0 - Media Queries
+ *----------------------------------------------------------------------------*/
+/*
+ * 6.1 - Desktop Medium
+ */
+@media only screen and (min-width: 1024px) {
+	#bp-hello-backdrop {
+		display: block;
+	}
+	#bp-hello-container {
+		position: fixed;
+		top: 60px;
+		left: var(--bp-hello-container-size);
+		right: var(--bp-hello-container-size);
+		bottom: 30px;
+		z-index: 9999;
+		border-radius: 3px;
+	}
+	#bp-hello-container .bp-hello-header h1 {
+		line-height: inherit;
+	}
+	.bp-hello-header {
+		height: auto;
+		max-height: inherit;
+		padding: 6px 30px;
+	}
+	.bp-hello-close {
+		right: 30px;
+	}
+	.bp-hello-close .close-modal:before {
+		line-height: 0.7;
+	}
+	.bp-hello-footer {
+		position: fixed;
+		left: var(--bp-hello-container-size);
+		right: var(--bp-hello-container-size);
+		bottom: 30px;
+		z-index: 10000;
+		height: auto;
+		max-height: inherit;
+		padding: 6px 30px;
+	}
+	.bp-hello-content {
+		height: calc(100% - 90px);
+		padding: 0 30px;
+	}
+	.bp-hello-content p {
+		font-size: 14px;
+	}
+}
+
+/**
+ * 6.2 - Desktop Large
+ */
+@media screen and (min-width: 1280px) {
+	#bp-hello-container,
+	.bp-hello-footer {
+		left: calc((100% - 896px) / 2);
+		right: calc((100% - 896px) / 2);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/admin/js/customizer-controls.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/js/customizer-controls.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/js/customizer-controls.js	(revision 12265)
@@ -0,0 +1,22 @@
+/**
+ * Customizer controls implementation.
+ *
+ * If you're looking to add JS for a specific panel or control, don't add it here.
+ * The file only implements generic Customizer control implementations.
+ *
+ * @since 2.5.0
+ */
+
+(function( $ ) {
+	$( window ).on( 'load', function() {
+		/**
+		 * <range> element: update label when value changes.
+		 *
+		 * @since 2.5.0
+		 */
+		$( '.customize-control-range input' ).on( 'input', function() {
+			var $this = $( this );
+			$this.siblings( 'output' ).text( $this.val() );
+		});
+	});
+})( jQuery );
Index: /tags/4.0.0-beta1/src/bp-core/admin/js/customizer-receiver-emails.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/js/customizer-receiver-emails.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/js/customizer-receiver-emails.js	(revision 12265)
@@ -0,0 +1,115 @@
+/**
+ * Customizer implementation for Email.
+ *
+ * If you're looking to add JS for every instance of a control, don't add it here.
+ * The file only implements the Customizer controls for Emails.
+ *
+ * @since 2.5.0
+ */
+
+(function( $ ) {
+	wp.customize( 'bp_email_options[email_bg]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.email_bg' ).attr( 'bgcolor', newval );
+				$( 'hr' ).attr( 'color', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[header_bg]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.header_bg' ).attr( 'bgcolor', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[header_text_size]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.header_text_size' ).css( 'font-size', newval + 'px' );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[header_text_color]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.header_text_color' ).css( 'color', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[highlight_color]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.header_bg' ).css( 'border-top-color', newval );
+				$( 'a' ).css( 'color', newval );
+				$( 'hr' ).attr( 'color', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[body_bg]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.body_bg' ).attr( 'bgcolor', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[body_text_size]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				// 1.618 = golden mean.
+				$( '.body_text_size' )
+					.css( 'font-size', newval + 'px' )
+					.css( 'line-height', Math.floor( newval * 1.618 ) + 'px' );
+
+				// 1.35 = default body_text_size multipler. Gives default heading of 20px.
+				$( '.welcome' ).css( 'font-size', Math.floor( newval * 1.35 ) + 'px' );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[body_text_color]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.body_text_color' ).css( 'color', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[footer_bg]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.footer_bg' ).attr( 'bgcolor', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[footer_text_size]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.footer_text_size' )
+					.css( 'font-size', newval + 'px' )
+					.css( 'line-height', Math.floor( newval * 1.618 ) + 'px' );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[footer_text_color]', function( value ) {
+		value.bind(function( newval ) {
+			if ( newval.length ) {
+				$( '.footer_text_color' ).css( 'color', newval );
+			}
+		});
+	});
+
+	wp.customize( 'bp_email_options[footer_text]', function( value ) {
+		value.bind(function( newval ) {
+			$( '.footer_text' ).html( newval );
+		});
+	});
+})( jQuery );
Index: /tags/4.0.0-beta1/src/bp-core/admin/js/dismissible-admin-notices.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/js/dismissible-admin-notices.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/js/dismissible-admin-notices.js	(revision 12265)
@@ -0,0 +1,16 @@
+(function($){
+	$(document).ready(function() {
+		$( '.bp-is-dismissible .notice-dismiss' ).click( function() {
+			var $notice = $( this ).closest( '.notice' );
+			var notice_id = $notice.data( 'noticeid' );
+			$.post( {
+				url: ajaxurl,
+				data: {
+					action: 'bp_dismiss_notice',
+					nonce: $( '#bp-dismissible-nonce-' + notice_id ).val(),
+					notice_id: $notice.data( 'noticeid' )
+				}
+			} );
+		} );
+	});
+}(jQuery));
Index: /tags/4.0.0-beta1/src/bp-core/admin/js/hello.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/js/hello.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/js/hello.js	(revision 12265)
@@ -0,0 +1,116 @@
+/**
+ * Loads for BuddyPress Hello in wp-admin for query string `hello=buddypress`.
+ *
+ * @since 3.0.0
+ */
+(function() {
+	/**
+	 * Open the BuddyPress Hello modal.
+	 */
+	var bp_hello_open_modal = function() {
+		var backdrop = document.getElementById( 'bp-hello-backdrop' ),
+			modal = document.getElementById( 'bp-hello-container' );
+
+		document.body.classList.add( 'bp-disable-scroll' );
+
+		// Show modal and overlay.
+		backdrop.style.display = '';
+		modal.style.display    = '';
+
+		// Focus the "X" so bp_hello_handle_keyboard_events() works.
+		var focus_target = modal.querySelectorAll( 'a[href], button' );
+		focus_target     = Array.prototype.slice.call( focus_target );
+		focus_target[0].focus();
+
+		// Events.
+		modal.addEventListener( 'keydown', bp_hello_handle_keyboard_events );
+		backdrop.addEventListener( 'click', bp_hello_close_modal );
+	};
+
+	/**
+	 * Close modal if "X" or background is touched.
+	 *
+	 * @param {Event} event - A click event.
+	 */
+	document.addEventListener( 'click', function( event ) {
+		var backdrop = document.getElementById( 'bp-hello-backdrop' );
+		if ( ! backdrop || ! document.getElementById( 'bp-hello-container' ) ) {
+			return;
+		}
+
+		var backdrop_click    = backdrop.contains( event.target ),
+			modal_close_click = event.target.classList.contains( 'close-modal' );
+
+		if ( ! modal_close_click && ! backdrop_click ) {
+			return;
+		}
+
+		bp_hello_close_modal();
+	}, false );
+
+	/**
+	 * Close the Hello modal.
+	 */
+	var bp_hello_close_modal = function() {
+		var backdrop = document.getElementById( 'bp-hello-backdrop' ),
+			modal = document.getElementById( 'bp-hello-container' );
+
+		document.body.classList.remove( 'bp-disable-scroll' );
+
+		// Remove modal and overlay.
+		modal.parentNode.removeChild( modal );
+		backdrop.parentNode.removeChild( backdrop );
+	};
+
+	/**
+	 * Restrict keyboard focus to elements within the BuddyPress Hello modal.
+	 *
+	 * @param {Event} event - A keyboard focus event.
+	 */
+	var bp_hello_handle_keyboard_events = function( event ) {
+		var modal = document.getElementById( 'bp-hello-container' ),
+			focus_targets = Array.prototype.slice.call(
+				modal.querySelectorAll( 'a[href], button' )
+			),
+			first_tab_stop = focus_targets[0],
+			last_tab_stop  = focus_targets[ focus_targets.length - 1 ];
+
+		// Check for TAB key press.
+		if ( event.keyCode !== 9 ) {
+			return;
+		}
+
+		// When SHIFT+TAB on first tab stop, go to last tab stop in modal.
+		if ( event.shiftKey && document.activeElement === first_tab_stop ) {
+			event.preventDefault();
+			last_tab_stop.focus();
+
+		// When TAB reaches last tab stop, go to first tab stop in modal.
+		} else if ( document.activeElement === last_tab_stop ) {
+			event.preventDefault();
+			first_tab_stop.focus();
+		}
+	};
+
+	/**
+	 * Close modal if escape key is presssed.
+	 *
+	 * @param {Event} event - A keyboard focus event.
+	 */
+	document.addEventListener( 'keyup', function( event ) {
+		if ( event.keyCode === 27 ) {
+			if ( ! document.getElementById( 'bp-hello-backdrop' ) || ! document.getElementById( 'bp-hello-container' ) ) {
+				return;
+			}
+
+			bp_hello_close_modal();
+		}
+	}, false );
+
+	// Init modal after the screen's loaded.
+	if ( document.attachEvent ? document.readyState === 'complete' : document.readyState !== 'loading' ) {
+		bp_hello_open_modal();
+	} else {
+		document.addEventListener( 'DOMContentLoaded', bp_hello_open_modal );
+	}
+}());
Index: /tags/4.0.0-beta1/src/bp-core/admin/sass/hello.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/admin/sass/hello.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/admin/sass/hello.scss	(revision 12265)
@@ -0,0 +1,399 @@
+/*------------------------------------------------------------------------------
+Loaded in wp-admin for query string `hello=buddypress`.
+
+@since 3.0.0
+
+--------------------------------------------------------------------------------
+TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 - Typography and colour
+2.0 - Dashicons
+3.0 - Elements
+	3.1 - Backdrop and container
+	3.2 - Modal footer
+	3.3 - Modal header
+	3.4 - Modal content
+4.0 - Content styles
+	4.1 - Backdrop and container
+	4.2 - Footer content
+	4.3 - Header content
+	4.4 - Content content
+5.0 - Media
+6.0 - Media Queries
+	6.1 - Desktop Medium
+	6.2 - Desktop Large
+------------------------------------------------------------------------------*/
+
+/*------------------------------------------------------------------------------
+ * 1.0 - Typography and colour
+ *----------------------------------------------------------------------------*/
+:root {
+	--bp-hello-color-primary: #d34600;
+	--bp-hello-color-secondary: #e5e5e5;
+	--bp-hello-container-size: 15%;
+}
+
+#bp-hello-container {
+
+	a {
+		color: var(--bp-hello-color-primary);
+
+		&:hover {
+			-webkit-transition: all 0.1s ease-in-out;
+			-o-transition: all 0.1s ease-in-out;
+			transition: all 0.1s ease-in-out;
+			color: rgb(0, 0, 0);
+		}
+	}
+
+	.bp-hello-header {
+
+		h1 {
+			line-height: 1.7;
+			font-size: 21px;
+			font-weight: 400;
+		}
+	}
+}
+
+.bp-hello-content {
+
+	p {
+		font-size: 16px;
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 2.0 - Dashicons
+ *----------------------------------------------------------------------------*/
+.bp-hello-close {
+
+	.button {
+		padding: 5px !important;
+	}
+
+	.close-modal {
+
+		&:before {
+			content: "\f158";
+			color: #23282d; /* wp toolbar */
+			font: 400 1.5em/1 dashicons;
+			speak: none;
+			-webkit-font-smoothing: antialiased;
+			-moz-osx-font-smoothing: grayscale;
+			background-image: none !important;
+		}
+
+		&:focus:before,
+		&:hover:before {
+			-webkit-transition: all 0.1s ease-in-out;
+			-o-transition: all 0.1s ease-in-out;
+			transition: all 0.1s ease-in-out;
+			color: var(--bp-hello-color-primary);
+		}
+	}
+}
+
+.bp-hello-social {
+
+	li a {
+
+		&:before {
+			color: #23282d; /* wp toolbar */
+			font: 400 30px/0.6 dashicons;
+			speak: none;
+			-webkit-font-smoothing: antialiased;
+			-moz-osx-font-smoothing: grayscale;
+			background-image: none !important;
+		}
+
+		&:hover:before {
+			-webkit-transition: all 0.1s ease-in-out;
+			-o-transition: all 0.1s ease-in-out;
+			transition: all 0.1s ease-in-out;
+			color: var(--bp-hello-color-primary);
+		}
+
+		&.support:before {
+			content: "\f448";
+		}
+
+		&.twitter:before {
+			content: "\f301";
+		}
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 3.0 - Elements
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 3.1 - Backdrop and container
+ */
+#bp-hello-backdrop {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 9998;
+
+	display: none;
+}
+
+#bp-hello-container {
+	position: fixed;
+	top: 0;
+	bottom: 80px;
+	z-index: 99999;
+}
+
+.bp-disable-scroll {
+	overflow: hidden;
+}
+
+/*
+ * 3.2 - Modal footer
+ */
+.bp-hello-footer {
+	display: -webkit-box;
+	display: -ms-flexbox;
+	display: flex;
+	-ms-flex-wrap: wrap;
+	flex-wrap: wrap;
+
+	height: 58px;
+	max-height: 58px;
+}
+
+.bp-hello-social-cta,
+.bp-hello-social-links {
+	-ms-flex-preferred-size: 50%;
+	flex-basis: 50%;
+}
+
+.bp-hello-social-links {
+
+	ul {
+		display: inline-block;
+	}
+}
+
+.bp-hello-social {
+
+	li {
+		position: relative;
+		bottom: -5px;
+		display: inline-block;
+		list-style-type: none;
+		margin-bottom: 0;
+
+		&:last-child a {
+			margin-left: 4px;
+		}
+	}
+}
+
+/*
+ * 3.3 - Modal header
+ */
+.bp-hello-header {
+	height: 58px;
+	max-height: 58px;
+}
+
+/*
+ * 3.4 - Modal content
+ */
+.bp-hello-content {
+	padding: 0 25px;
+
+	// Force scrolling.
+	height: calc(100% - 58px);
+	overflow-y: auto;
+	-webkit-overflow-scrolling: touch;
+}
+
+/*------------------------------------------------------------------------------
+ * 4.0 - Content styles
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 4.1 - Backdrop and container
+ */
+#bp-hello-backdrop {
+	background-color: rgba(0, 0, 0, 0.8);
+	-webkit-transition: opacity 0.15s ease-out;
+	-o-transition: opacity 0.15s ease-out;
+	transition: opacity 0.15s ease-out;
+}
+
+#bp-hello-container {
+	background-color: rgb(255, 255, 255);
+}
+
+/*
+ * 4.2 - Footer content
+ */
+.bp-hello-footer {
+	border-radius: 0 0 3px 3px;
+	background-color: rgb(255, 255, 255);
+	border-top: 1px solid var(--bp-hello-color-secondary);
+	padding: 6px 25px;
+
+	p {
+		font-size: 15px;
+	}
+}
+
+.bp-hello-social-cta {
+	text-align: left;
+}
+
+.bp-hello-social-links {
+	text-align: right;
+}
+
+.bp-hello-social {
+
+	li {
+
+		a {
+			text-decoration: none;
+		}
+	}
+}
+
+/*
+ * 4.3 - Header content
+ */
+.bp-hello-header {
+	padding: 6px 25px;
+
+	h1 {
+		width: calc(100% - 51px);  // Approx. width of "X" button block.
+	}
+}
+
+#bp-hello-container {
+
+	.bp-hello-header {
+		border-bottom: 1px solid var(--bp-hello-color-secondary);
+	}
+}
+
+.bp-hello-title {
+	text-align: left;
+}
+
+.bp-hello-close {
+	position: absolute;
+	top: 20px;
+	right: 25px;
+	text-align: right;
+}
+
+/*
+ * 4.4 - Content content
+ */
+.bp-hello-content {
+	background-color: rgb(255, 255, 255);
+}
+
+/*------------------------------------------------------------------------------
+ * 5.0 - Media
+ *----------------------------------------------------------------------------*/
+.bp-hello-content {
+
+	img {
+		border-radius: 2px;
+		max-width: 100%;
+	}
+
+	iframe {
+		width: 100%;
+	}
+}
+
+/*------------------------------------------------------------------------------
+ * 6.0 - Media Queries
+ *----------------------------------------------------------------------------*/
+
+/*
+ * 6.1 - Desktop Medium
+ */
+@media only screen and (min-width: 1024px) {
+
+	#bp-hello-backdrop {
+		display: block;
+	}
+
+	#bp-hello-container {
+		position: fixed;
+		top: 60px;
+		left: var(--bp-hello-container-size);
+		right: var(--bp-hello-container-size);
+		bottom: 30px;
+		z-index: 9999;
+
+		border-radius: 3px;
+
+		.bp-hello-header {
+
+			h1 {
+				line-height: inherit;
+			}
+		}
+	}
+
+	.bp-hello-header {
+		height: auto;
+		max-height: inherit;
+		padding: 6px 30px;
+	}
+
+	.bp-hello-close {
+		right: 30px;
+
+		.close-modal:before {
+			line-height: 0.7;
+		}
+	}
+
+	.bp-hello-footer {
+		position: fixed; // Fixed position above "content" div.
+		left: var(--bp-hello-container-size);
+		right: var(--bp-hello-container-size);
+		bottom: 30px;
+		z-index: 10000; // See #bp-hello-backdrop
+
+		height: auto;
+		max-height: inherit;
+
+		padding: 6px 30px;
+	}
+
+	.bp-hello-content {
+		// Very very approx. height of header and footer.
+		height: calc(100% - 90px);
+		padding: 0 30px;
+
+		p {
+			font-size: 14px;
+		}
+	}
+}
+
+/**
+ * 6.2 - Desktop Large
+ */
+@media screen and (min-width: 1280px) {
+
+	#bp-hello-container,
+	.bp-hello-footer {
+		// Approx. max-width of modal at Desktop Medium size.
+		left: calc((100% - 896px) / 2);
+		right: calc((100% - 896px) / 2);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-actions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-actions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-actions.php	(revision 12265)
@@ -0,0 +1,129 @@
+<?php
+/**
+ * BuddyPress Filters & Actions.
+ *
+ * This file contains the actions and filters that are used through-out BuddyPress.
+ * They are consolidated here to make searching for them easier, and to help
+ * developers understand at a glance the order in which things occur.
+ *
+ * @package BuddyPress
+ * @subpackage Hooks
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Attach BuddyPress to WordPress.
+ *
+ * BuddyPress uses its own internal actions to help aid in third-party plugin
+ * development, and to limit the amount of potential future code changes when
+ * updates to WordPress core occur.
+ *
+ * These actions exist to create the concept of 'plugin dependencies'. They
+ * provide a safe way for plugins to execute code *only* when BuddyPress is
+ * installed and activated, without needing to do complicated guesswork.
+ *
+ * For more information on how this works, see the 'Plugin Dependency' section
+ * near the bottom of this file.
+ *
+ *           v--WordPress Actions       v--BuddyPress Sub-actions
+ */
+add_action( 'plugins_loaded',          'bp_loaded',                 10    );
+add_action( 'init',                    'bp_init',                   10    );
+add_action( 'rest_api_init',           'bp_rest_api_init',          20    ); // After WP core.
+add_action( 'customize_register',      'bp_customize_register',     20    ); // After WP core.
+add_action( 'parse_query',             'bp_parse_query',            2     ); // Early for overrides.
+add_action( 'wp',                      'bp_ready',                  10    );
+add_action( 'set_current_user',        'bp_setup_current_user',     10    );
+add_action( 'setup_theme',             'bp_setup_theme',            10    );
+add_action( 'after_setup_theme',       'bp_after_setup_theme',      100   ); // After WP themes.
+add_action( 'wp_enqueue_scripts',      'bp_enqueue_scripts',        10    );
+add_action( 'enqueue_embed_scripts',   'bp_enqueue_embed_scripts',  10    );
+add_action( 'admin_bar_menu',          'bp_setup_admin_bar',        20    ); // After WP core.
+add_action( 'template_redirect',       'bp_template_redirect',      10    );
+add_action( 'widgets_init',            'bp_widgets_init',           10    );
+add_action( 'generate_rewrite_rules',  'bp_generate_rewrite_rules', 10    );
+
+/**
+ * The bp_loaded hook - Attached to 'plugins_loaded' above.
+ *
+ * Attach various loader actions to the bp_loaded action.
+ * The load order helps to execute code at the correct time.
+ *                                                      v---Load order
+ */
+add_action( 'bp_loaded', 'bp_setup_components',         2  );
+add_action( 'bp_loaded', 'bp_include',                  4  );
+add_action( 'bp_loaded', 'bp_setup_option_filters',     5  );
+add_action( 'bp_loaded', 'bp_setup_cache_groups',       5  );
+add_action( 'bp_loaded', 'bp_setup_widgets',            6  );
+add_action( 'bp_loaded', 'bp_register_theme_packages',  12 );
+add_action( 'bp_loaded', 'bp_register_theme_directory', 14 );
+
+/**
+ * The bp_init hook - Attached to 'init' above.
+ *
+ * Attach various initialization actions to the bp_init action.
+ * The load order helps to execute code at the correct time.
+ *                                                   v---Load order
+ */
+add_action( 'bp_init', 'bp_register_post_types',     2  );
+add_action( 'bp_init', 'bp_register_taxonomies',     2  );
+add_action( 'bp_init', 'bp_core_set_uri_globals',    2  );
+add_action( 'bp_init', 'bp_setup_globals',           4  );
+add_action( 'bp_init', 'bp_setup_canonical_stack',   5  );
+add_action( 'bp_init', 'bp_setup_nav',               6  );
+add_action( 'bp_init', 'bp_setup_title',             8  );
+add_action( 'bp_init', 'bp_core_load_admin_bar_css', 12 );
+add_action( 'bp_init', 'bp_add_rewrite_tags',        20 );
+add_action( 'bp_init', 'bp_add_rewrite_rules',       30 );
+add_action( 'bp_init', 'bp_add_permastructs',        40 );
+
+/**
+ * The bp_register_taxonomies hook - Attached to 'bp_init' @ priority 2 above.
+ */
+add_action( 'bp_register_taxonomies', 'bp_register_member_types' );
+
+/**
+ * Late includes.
+ *
+ * Run after the canonical stack is setup to allow for conditional includes
+ * on certain pages.
+ */
+add_action( 'bp_setup_canonical_stack', 'bp_late_include', 20 );
+
+/**
+ * The bp_template_redirect hook - Attached to 'template_redirect' above.
+ *
+ * Attach various template actions to the bp_template_redirect action.
+ * The load order helps to execute code at the correct time.
+ *
+ * Note that we currently use template_redirect versus template include because
+ * BuddyPress is a bully and overrides the existing themes output in many
+ * places. This won't always be this way, we promise.
+ *                                                           v---Load order
+ */
+add_action( 'bp_template_redirect', 'bp_redirect_canonical', 2  );
+add_action( 'bp_template_redirect', 'bp_actions',            4  );
+add_action( 'bp_template_redirect', 'bp_screens',            6  );
+add_action( 'bp_template_redirect', 'bp_post_request',       10 );
+add_action( 'bp_template_redirect', 'bp_get_request',        10 );
+
+/**
+ * Add the BuddyPress functions file and the Theme Compat Default features.
+ */
+add_action( 'bp_after_setup_theme', 'bp_check_theme_template_pack_dependency',   -10 );
+add_action( 'bp_after_setup_theme', 'bp_load_theme_functions',                    1  );
+add_action( 'bp_after_setup_theme', 'bp_register_theme_compat_default_features',  10 );
+
+// Load the admin.
+if ( is_admin() ) {
+	add_action( 'bp_loaded', 'bp_admin' );
+}
+
+// Activation redirect.
+add_action( 'bp_activation', 'bp_add_activation_redirect' );
+
+// Email unsubscribe.
+add_action( 'bp_get_request_unsubscribe', 'bp_email_unsubscribe_handler' );
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-admin.php	(revision 12265)
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Main BuddyPress Admin Class.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Setup BuddyPress Admin.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_admin() {
+	buddypress()->admin = new BP_Admin();
+	return;
+
+
+	// These are strings we may use to describe maintenance/security releases, where we aim for no new strings.
+	_n_noop( 'Maintenance Release', 'Maintenance Releases', 'buddypress' );
+	_n_noop( 'Security Release', 'Security Releases', 'buddypress' );
+	_n_noop( 'Maintenance and Security Release', 'Maintenance and Security Releases', 'buddypress' );
+
+	/* translators: 1: BuddyPress version number. */
+	_n_noop(
+		'<strong>Version %1$s</strong> addressed a security issue.',
+		'<strong>Version %1$s</strong> addressed some security issues.',
+		'buddypress'
+	);
+
+	/* translators: 1: BuddyPress version number, 2: plural number of bugs. */
+	_n_noop(
+		'<strong>Version %1$s</strong> addressed %2$s bug.',
+		'<strong>Version %1$s</strong> addressed %2$s bugs.',
+		'buddypress'
+	);
+
+	/* translators: 1: BuddyPress version number, 2: plural number of bugs. Singular security issue. */
+	_n_noop(
+		'<strong>Version %1$s</strong> addressed a security issue and fixed %2$s bug.',
+		'<strong>Version %1$s</strong> addressed a security issue and fixed %2$s bugs.',
+		'buddypress'
+	);
+
+	/* translators: 1: BuddyPress version number, 2: plural number of bugs. More than one security issue. */
+	_n_noop(
+		'<strong>Version %1$s</strong> addressed some security issues and fixed %2$s bug.',
+		'<strong>Version %1$s</strong> addressed some security issues and fixed %2$s bugs.',
+		'buddypress'
+	);
+
+	__( 'For more information, see <a href="%s">the release notes</a>.', 'buddypress' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-adminbar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-adminbar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-adminbar.php	(revision 12265)
@@ -0,0 +1,116 @@
+<?php
+/**
+ * BuddyPress Core Toolbar.
+ *
+ * Handles the core functions related to the WordPress Toolbar.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add the secondary BuddyPress area to the my-account menu.
+ *
+ * @since 1.6.0
+ *
+ * @global WP_Admin_Bar $wp_admin_bar.
+ */
+function bp_admin_bar_my_account_root() {
+	global $wp_admin_bar;
+
+	// Bail if this is an ajax request.
+	if ( !bp_use_wp_admin_bar() || defined( 'DOING_AJAX' ) )
+		return;
+
+	// Only add menu for logged in user.
+	if ( is_user_logged_in() ) {
+
+		// Add secondary parent item for all BuddyPress components.
+		$wp_admin_bar->add_menu( array(
+			'parent'    => 'my-account',
+			'id'        => 'my-account-buddypress',
+			'title'     => __( 'My Account', 'buddypress' ),
+			'group'     => true,
+			'meta'      => array(
+				'class' => 'ab-sub-secondary'
+			)
+		) );
+	}
+}
+add_action( 'admin_bar_menu', 'bp_admin_bar_my_account_root', 100 );
+
+/**
+ * Handle the Toolbar/BuddyBar business.
+ *
+ * @since 1.2.0
+ */
+function bp_core_load_admin_bar() {
+
+	// Show the Toolbar for logged out users.
+	if ( ! is_user_logged_in() && (int) bp_get_option( 'hide-loggedout-adminbar' ) != 1 ) {
+		show_admin_bar( true );
+	}
+
+	// Hide the WordPress Toolbar and show the BuddyBar.
+	if ( ! bp_use_wp_admin_bar() ) {
+		_doing_it_wrong( __FUNCTION__, __( 'The BuddyBar is no longer supported. Please migrate to the WordPress toolbar as soon as possible.', 'buddypress' ), '2.1.0' );
+
+		// Load deprecated code if not available.
+		if ( ! function_exists( 'bp_core_admin_bar' ) ) {
+			require buddypress()->plugin_dir . 'bp-core/deprecated/2.1.php';
+		}
+
+		// Keep the WP Toolbar from loading.
+		show_admin_bar( false );
+
+		// Actions used to build the BP Toolbar.
+		add_action( 'bp_adminbar_logo',  'bp_adminbar_logo'               );
+		add_action( 'bp_adminbar_menus', 'bp_adminbar_login_menu',    2   );
+		add_action( 'bp_adminbar_menus', 'bp_adminbar_account_menu',  4   );
+		add_action( 'bp_adminbar_menus', 'bp_adminbar_thisblog_menu', 6   );
+		add_action( 'bp_adminbar_menus', 'bp_adminbar_random_menu',   100 );
+
+		// Actions used to append BP Toolbar to footer.
+		add_action( 'wp_footer',    'bp_core_admin_bar', 8 );
+		add_action( 'admin_footer', 'bp_core_admin_bar'    );
+	}
+}
+add_action( 'init', 'bp_core_load_admin_bar', 9 );
+
+/**
+ * Handle the enqueueing of toolbar CSS.
+ *
+ * This function exists mostly for backwards compatibility reasons, so anyone
+ * previously unhooking this function can continue to do so. It's hooked to
+ * the `bp_init` action in `bp-core-actions.php`.
+ *
+ * @since 1.5.0
+ */
+function bp_core_load_admin_bar_css() {
+	add_action( 'bp_enqueue_scripts',       'bp_core_enqueue_admin_bar_css', 1 );
+	add_action( 'bp_admin_enqueue_scripts', 'bp_core_enqueue_admin_bar_css', 1 );
+}
+
+/**
+ * Enqueue supplemental WordPress Toolbar styling.
+ *
+ * @since 2.1.0
+ *
+ * @see bp_core_register_common_styles()
+ * @see bp_core_load_admin_bar_css()
+ */
+function bp_core_enqueue_admin_bar_css() {
+
+	// Bail if not using WordPress's admin bar or it's not showing on this
+	// page request.
+	if ( ! bp_use_wp_admin_bar() || ! is_admin_bar_showing() ) {
+		return;
+	}
+
+	// Enqueue the additional adminbar css.
+	wp_enqueue_style( 'bp-admin-bar' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-attachments.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-attachments.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-attachments.php	(revision 12265)
@@ -0,0 +1,1515 @@
+<?php
+/**
+ * BuddyPress Attachments functions.
+ *
+ * @package BuddyPress
+ * @subpackage Attachments
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Check if the current WordPress version is using Plupload 2.1.1
+ *
+ * Plupload 2.1.1 was introduced in WordPress 3.9. Our bp-plupload.js
+ * script requires it. So we need to make sure the current WordPress
+ * match with our needs.
+ *
+ * @since 2.3.0
+ * @since 3.0.0 This is always true.
+ *
+ * @return bool Always true.
+ */
+function bp_attachments_is_wp_version_supported() {
+	return true;
+}
+
+/**
+ * Get the Attachments Uploads dir data.
+ *
+ * @since 2.4.0
+ *
+ * @param string $data The data to get. Possible values are: 'dir', 'basedir' & 'baseurl'.
+ *                     Leave empty to get all datas.
+ * @return string|array The needed Upload dir data.
+ */
+function bp_attachments_uploads_dir_get( $data = '' ) {
+	$attachments_dir = 'buddypress';
+	$retval          = '';
+
+	if ( 'dir' === $data ) {
+		$retval = $attachments_dir;
+	} else {
+		$upload_data = bp_upload_dir();
+
+		// Return empty string, if Uploads data are not available.
+		if ( ! $upload_data ) {
+			return $retval;
+		}
+
+		// Build the Upload data array for BuddyPress attachments.
+		foreach ( $upload_data as $key => $value ) {
+			if ( 'basedir' === $key || 'baseurl' === $key ) {
+				$upload_data[ $key ] = trailingslashit( $value ) . $attachments_dir;
+
+				// Fix for HTTPS.
+				if ( 'baseurl' === $key && is_ssl() ) {
+					$upload_data[ $key ] = str_replace( 'http://', 'https://', $upload_data[ $key ] );
+				}
+			} else {
+				unset( $upload_data[ $key ] );
+			}
+		}
+
+		// Add the dir to the array.
+		$upload_data['dir'] = $attachments_dir;
+
+		if ( empty( $data ) ) {
+			$retval = $upload_data;
+		} elseif ( isset( $upload_data[ $data ] ) ) {
+			$retval = $upload_data[ $data ];
+		}
+	}
+
+	/**
+	 * Filter here to edit the Attachments upload dir data.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string|array $retval The needed Upload dir data or the full array of data
+	 * @param string       $data   The data requested
+	 */
+	return apply_filters( 'bp_attachments_uploads_dir_get', $retval, $data );
+}
+
+/**
+ * Gets the upload dir array for cover images.
+ *
+ * @since 3.0.0
+ *
+ * @return array See wp_upload_dir().
+ */
+function bp_attachments_cover_image_upload_dir( $args = array() ) {
+	// Default values are for profiles.
+	$object_id = bp_displayed_user_id();
+
+	if ( empty( $object_id ) ) {
+		$object_id = bp_loggedin_user_id();
+	}
+
+	$object_directory = 'members';
+
+	// We're in a group, edit default values.
+	if ( bp_is_group() || bp_is_group_create() ) {
+		$object_id        = bp_get_current_group_id();
+		$object_directory = 'groups';
+	}
+
+	$r = bp_parse_args( $args, array(
+		'object_id' => $object_id,
+		'object_directory' => $object_directory,
+	), 'cover_image_upload_dir' );
+
+
+	// Set the subdir.
+	$subdir  = '/' . $r['object_directory'] . '/' . $r['object_id'] . '/cover-image';
+
+	$upload_dir = bp_attachments_uploads_dir_get();
+
+	/**
+	 * Filters the cover image upload directory.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $value      Array containing the path, URL, and other helpful settings.
+	 * @param array $upload_dir The original Uploads dir.
+	 */
+	return apply_filters( 'bp_attachments_cover_image_upload_dir', array(
+		'path'    => $upload_dir['basedir'] . $subdir,
+		'url'     => set_url_scheme( $upload_dir['baseurl'] ) . $subdir,
+		'subdir'  => $subdir,
+		'basedir' => $upload_dir['basedir'],
+		'baseurl' => set_url_scheme( $upload_dir['baseurl'] ),
+		'error'   => false,
+	), $upload_dir );
+}
+
+/**
+ * Get the max upload file size for any attachment.
+ *
+ * @since 2.4.0
+ *
+ * @param string $type A string to inform about the type of attachment
+ *                     we wish to get the max upload file size for.
+ * @return int Max upload file size for any attachment.
+ */
+function bp_attachments_get_max_upload_file_size( $type = '' ) {
+	$fileupload_maxk = bp_core_get_root_option( 'fileupload_maxk' );
+
+	if ( '' === $fileupload_maxk ) {
+		$fileupload_maxk = 5120000; // 5mb;
+	} else {
+		$fileupload_maxk = $fileupload_maxk * 1024;
+	}
+
+	/**
+	 * Filter here to edit the max upload file size.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param int    $fileupload_maxk Max upload file size for any attachment.
+	 * @param string $type            The attachment type (eg: 'avatar' or 'cover_image').
+	 */
+	return apply_filters( 'bp_attachments_get_max_upload_file_size', $fileupload_maxk, $type );
+}
+
+/**
+ * Get allowed types for any attachment.
+ *
+ * @since 2.4.0
+ *
+ * @param string $type The extension types to get.
+ *                     Default: 'avatar'.
+ * @return array The list of allowed extensions for attachments.
+ */
+function bp_attachments_get_allowed_types( $type = 'avatar' ) {
+	// Defaults to BuddyPress supported image extensions.
+	$exts = array( 'jpeg', 'gif', 'png' );
+
+	/**
+	 * It's not a BuddyPress feature, get the allowed extensions
+	 * matching the $type requested.
+	 */
+	if ( 'avatar' !== $type && 'cover_image' !== $type ) {
+		// Reset the default exts.
+		$exts = array();
+
+		switch ( $type ) {
+			case 'video' :
+				$exts = wp_get_video_extensions();
+			break;
+
+			case 'audio' :
+				$exts = wp_get_video_extensions();
+			break;
+
+			default:
+				$allowed_mimes = get_allowed_mime_types();
+
+				/**
+				 * Search for allowed mimes matching the type.
+				 *
+				 * Eg: using 'application/vnd.oasis' as the $type
+				 * parameter will get all OpenOffice extensions supported
+				 * by WordPress and allowed for the current user.
+				 */
+				if ( '' !== $type ) {
+					$allowed_mimes = preg_grep( '/' . addcslashes( $type, '/.+-' ) . '/', $allowed_mimes );
+				}
+
+				$allowed_types = array_keys( $allowed_mimes );
+
+				// Loop to explode keys using '|'.
+				foreach ( $allowed_types as $allowed_type ) {
+					$t = explode( '|', $allowed_type );
+					$exts = array_merge( $exts, (array) $t );
+				}
+			break;
+		}
+	}
+
+	/**
+	 * Filter here to edit the allowed extensions by attachment type.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array  $exts List of allowed extensions.
+	 * @param string $type The requested file type.
+	 */
+	return apply_filters( 'bp_attachments_get_allowed_types', $exts, $type );
+}
+
+/**
+ * Get allowed attachment mime types.
+ *
+ * @since 2.4.0
+ *
+ * @param string $type          The extension types to get (Optional).
+ * @param array  $allowed_types List of allowed extensions.
+ * @return array List of allowed mime types.
+ */
+function bp_attachments_get_allowed_mimes( $type = '', $allowed_types = array() ) {
+	if ( empty( $allowed_types ) ) {
+		$allowed_types = bp_attachments_get_allowed_types( $type );
+	}
+
+	$validate_mimes = wp_match_mime_types( join( ',', $allowed_types ), wp_get_mime_types() );
+	$allowed_mimes  = array_map( 'implode', $validate_mimes );
+
+	/**
+	 * Include jpg type if jpeg is set
+	 */
+	if ( isset( $allowed_mimes['jpeg'] ) && ! isset( $allowed_mimes['jpg'] ) ) {
+		$allowed_mimes['jpg'] = $allowed_mimes['jpeg'];
+	}
+
+	return $allowed_mimes;
+}
+
+/**
+ * Check the uploaded attachment type is allowed.
+ *
+ * @since 2.4.0
+ *
+ * @param string $file          Full path to the file.
+ * @param string $filename      The name of the file (may differ from $file due to $file being
+ *                              in a tmp directory).
+ * @param array  $allowed_mimes The attachment allowed mimes (Required).
+ * @return bool True if the attachment type is allowed. False otherwise
+ */
+function bp_attachments_check_filetype( $file, $filename, $allowed_mimes ) {
+	$filetype = wp_check_filetype_and_ext( $file, $filename, $allowed_mimes );
+
+	if ( ! empty( $filetype['ext'] ) && ! empty( $filetype['type'] ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Use the absolute path to an image to set an attachment type for a given item.
+ *
+ * @since 2.4.0
+ *
+ * @param string $type The attachment type to create (avatar or cover_image). Default: avatar.
+ * @param array  $args {
+ *     @type int    $item_id   The ID of the object (Required). Default: 0.
+ *     @type string $object    The object type (eg: group, user, blog) (Required). Default: 'user'.
+ *     @type string $component The component for the object (eg: groups, xprofile, blogs). Default: ''.
+ *     @type string $image     The absolute path to the image (Required). Default: ''.
+ *     @type int    $crop_w    Crop width. Default: 0.
+ *     @type int    $crop_h    Crop height. Default: 0.
+ *     @type int    $crop_x    The horizontal starting point of the crop. Default: 0.
+ *     @type int    $crop_y    The vertical starting point of the crop. Default: 0.
+ * }
+ * @return bool True on success, false otherwise.
+ */
+function bp_attachments_create_item_type( $type = 'avatar', $args = array() ) {
+	if ( empty( $type ) || ( $type !== 'avatar' && $type !== 'cover_image' ) ) {
+		return false;
+	}
+
+	$r = bp_parse_args( $args, array(
+		'item_id'   => 0,
+		'object'    => 'user',
+		'component' => '',
+		'image'     => '',
+		'crop_w'    => 0,
+		'crop_h'    => 0,
+		'crop_x'    => 0,
+		'crop_y'    => 0
+	), 'create_item_' . $type );
+
+	if ( empty( $r['item_id'] ) || empty( $r['object'] ) || ! file_exists( $r['image'] ) || ! @getimagesize( $r['image'] ) ) {
+		return false;
+	}
+
+	// Make sure the file path is safe.
+	if ( 1 === validate_file( $r['image'] ) ) {
+		return false;
+	}
+
+	// Set the component if not already done.
+	if ( empty( $r['component'] ) ) {
+		if ( 'user' === $r['object'] ) {
+			$r['component'] = 'xprofile';
+		} else {
+			$r['component'] = $r['object'] . 's';
+		}
+	}
+
+	// Get allowed mimes for the Attachment type and check the image one is.
+	$allowed_mimes = bp_attachments_get_allowed_mimes( $type );
+	$is_allowed    = wp_check_filetype( $r['image'], $allowed_mimes );
+
+	// It's not an image.
+	if ( ! $is_allowed['ext'] ) {
+		return false;
+	}
+
+	// Init the Attachment data.
+	$attachment_data = array();
+
+	if ( 'avatar' === $type ) {
+		// Set crop width for the avatar if not given.
+		if ( empty( $r['crop_w'] ) ) {
+			$r['crop_w'] = bp_core_avatar_full_width();
+		}
+
+		// Set crop height for the avatar if not given.
+		if ( empty( $r['crop_h'] ) ) {
+			$r['crop_h'] = bp_core_avatar_full_height();
+		}
+
+		if ( is_callable( $r['component'] . '_avatar_upload_dir' ) ) {
+			$dir_args = array( $r['item_id'] );
+
+			// In case  of xprofile, we need an extra argument.
+			if ( 'xprofile' === $r['component'] ) {
+				$dir_args = array( false, $r['item_id'] );
+			}
+
+			$attachment_data = call_user_func_array( $r['component'] . '_avatar_upload_dir', $dir_args );
+		}
+	} elseif ( 'cover_image' === $type ) {
+		$attachment_data = bp_attachments_cover_image_upload_dir();
+
+		// The BP Attachments Uploads Dir is not set, stop.
+		if ( ! $attachment_data ) {
+			return false;
+		}
+
+		// Default to members for xProfile.
+		$object_subdir = 'members';
+
+		if ( 'xprofile' !== $r['component'] ) {
+			$object_subdir = sanitize_key( $r['component'] );
+		}
+
+		// Set Subdir.
+		$attachment_data['subdir'] = $object_subdir . '/' . $r['item_id'] . '/cover-image';
+
+		// Set Path.
+		$attachment_data['path'] = trailingslashit( $attachment_data['basedir'] ) . $attachment_data['subdir'];
+	}
+
+	if ( ! isset( $attachment_data['path'] ) || ! isset( $attachment_data['subdir'] ) ) {
+		return false;
+	}
+
+	// It's not a regular upload, we may need to create some folders.
+	if ( ! is_dir( $attachment_data['path'] ) ) {
+		if ( ! wp_mkdir_p( $attachment_data['path'] ) ) {
+			return false;
+		}
+	}
+
+	// Set the image name and path.
+	$image_file_name = wp_unique_filename( $attachment_data['path'], basename( $r['image'] ) );
+	$image_file_path = $attachment_data['path'] . '/' . $image_file_name;
+
+	// Copy the image file into the avatar dir.
+	if ( ! copy( $r['image'], $image_file_path ) ) {
+		return false;
+	}
+
+	// Init the response.
+	$created = false;
+
+	// It's an avatar, we need to crop it.
+	if ( 'avatar' === $type ) {
+		$created = bp_core_avatar_handle_crop( array(
+			'object'        => $r['object'],
+			'avatar_dir'    => trim( dirname( $attachment_data['subdir'] ), '/' ),
+			'item_id'       => (int) $r['item_id'],
+			'original_file' => trailingslashit( $attachment_data['subdir'] ) . $image_file_name,
+			'crop_w'        => $r['crop_w'],
+			'crop_h'        => $r['crop_h'],
+			'crop_x'        => $r['crop_x'],
+			'crop_y'        => $r['crop_y']
+		) );
+
+	// It's a cover image we need to fit it to feature's dimensions.
+	} elseif ( 'cover_image' === $type ) {
+		$cover_image = bp_attachments_cover_image_generate_file( array(
+			'file'            => $image_file_path,
+			'component'       => $r['component'],
+			'cover_image_dir' => $attachment_data['path']
+		) );
+
+		$created = ! empty( $cover_image['cover_file'] );
+	}
+
+	// Remove copied file if it fails.
+	if ( ! $created ) {
+		@unlink( $image_file_path );
+	}
+
+	// Return the response.
+	return $created;
+}
+
+/**
+ * Get the url or the path for a type of attachment.
+ *
+ * @since 2.4.0
+ *
+ * @param string $data whether to get the url or the path.
+ * @param array  $args {
+ *     @type string $object_dir  The object dir (eg: members/groups). Defaults to members.
+ *     @type int    $item_id     The object id (eg: a user or a group id). Defaults to current user.
+ *     @type string $type        The type of the attachment which is also the subdir where files are saved.
+ *                               Defaults to 'cover-image'
+ *     @type string $file        The name of the file.
+ * }
+ * @return string|bool The url or the path to the attachment, false otherwise
+ */
+function bp_attachments_get_attachment( $data = 'url', $args = array() ) {
+	// Default value.
+	$attachment_data = false;
+
+	$r = bp_parse_args( $args, array(
+		'object_dir' => 'members',
+		'item_id'    => bp_loggedin_user_id(),
+		'type'       => 'cover-image',
+		'file'       => '',
+	), 'attachments_get_attachment_src' );
+
+	/**
+	 * Filters whether or not to handle fetching a BuddyPress image attachment.
+	 *
+	 * If you want to override this function, make sure you return false.
+	 *
+	 * @since 2.5.1
+	 *
+	 * @param null|string $value If null is returned, proceed with default behaviour. Otherwise, value returned verbatim.
+	 * @param array $r {
+	 *     @type string $object_dir The object dir (eg: members/groups). Defaults to members.
+	 *     @type int    $item_id    The object id (eg: a user or a group id). Defaults to current user.
+	 *     @type string $type       The type of the attachment which is also the subdir where files are saved.
+	 *                              Defaults to 'cover-image'
+	 *     @type string $file       The name of the file.
+	 * }
+	 */
+	$pre_filter = apply_filters( 'bp_attachments_pre_get_attachment', null, $r );
+	if ( $pre_filter !== null ) {
+		return $pre_filter;
+	}
+
+	// Get BuddyPress Attachments Uploads Dir datas.
+	$bp_attachments_uploads_dir = bp_attachments_uploads_dir_get();
+
+	// The BP Attachments Uploads Dir is not set, stop.
+	if ( ! $bp_attachments_uploads_dir ) {
+		return $attachment_data;
+	}
+
+	$type_subdir = $r['object_dir'] . '/' . $r['item_id'] . '/' . $r['type'];
+	$type_dir    = trailingslashit( $bp_attachments_uploads_dir['basedir'] ) . $type_subdir;
+
+	if ( 1 === validate_file( $type_dir ) || ! is_dir( $type_dir ) ) {
+		return $attachment_data;
+	}
+
+	if ( ! empty( $r['file'] ) ) {
+		if ( ! file_exists( trailingslashit( $type_dir ) . $r['file'] ) ) {
+			return $attachment_data;
+		}
+
+		if ( 'url' === $data ) {
+			$attachment_data = trailingslashit( $bp_attachments_uploads_dir['baseurl'] ) . $type_subdir . '/' . $r['file'];
+		} else {
+			$attachment_data = trailingslashit( $type_dir ) . $r['file'];
+		}
+
+	} else {
+		$file = false;
+
+		// Open the directory and get the first file.
+		if ( $att_dir = opendir( $type_dir ) ) {
+
+			while ( false !== ( $attachment_file = readdir( $att_dir ) ) ) {
+				// Look for the first file having the type in its name.
+				if ( false !== strpos( $attachment_file, $r['type'] ) && empty( $file ) ) {
+					$file = $attachment_file;
+					break;
+				}
+			}
+		}
+
+		if ( empty( $file ) ) {
+			return $attachment_data;
+		}
+
+		if ( 'url' === $data ) {
+			$attachment_data = trailingslashit( $bp_attachments_uploads_dir['baseurl'] ) . $type_subdir . '/' . $file;
+		} else {
+			$attachment_data = trailingslashit( $type_dir ) . $file;
+		}
+	}
+
+	return $attachment_data;
+}
+
+/**
+ * Delete an attachment for the given arguments
+ *
+ * @since 2.4.0
+ *
+ * @see bp_attachments_get_attachment() For more information on accepted arguments.
+ *
+ * @param array $args Array of arguments for the attachment deletion.
+ * @return bool True if the attachment was deleted, false otherwise.
+ */
+function bp_attachments_delete_file( $args = array() ) {
+	$attachment_path = bp_attachments_get_attachment( 'path', $args );
+
+	/**
+	 * Filters whether or not to handle deleting an existing BuddyPress attachment.
+	 *
+	 * If you want to override this function, make sure you return false.
+	 *
+	 * @since 2.5.1
+	 *
+	 * @param bool $value Whether or not to delete the BuddyPress attachment.
+	 * @param array $args Array of arguments for the attachment deletion.
+	 */
+	if ( ! apply_filters( 'bp_attachments_pre_delete_file', true, $args ) ) {
+		return true;
+	}
+
+	if ( empty( $attachment_path ) ) {
+		return false;
+	}
+
+	@unlink( $attachment_path );
+	return true;
+}
+
+/**
+ * Get the BuddyPress Plupload settings.
+ *
+ * @since 2.3.0
+ *
+ * @return array List of BuddyPress Plupload settings.
+ */
+function bp_attachments_get_plupload_default_settings() {
+
+	$max_upload_size = wp_max_upload_size();
+
+	if ( ! $max_upload_size ) {
+		$max_upload_size = 0;
+	}
+
+	$defaults = array(
+		'runtimes'            => 'html5,flash,silverlight,html4',
+		'file_data_name'      => 'file',
+		'multipart_params'    => array(
+			'action'          => 'bp_upload_attachment',
+			'_wpnonce'        => wp_create_nonce( 'bp-uploader' ),
+		),
+		'url'                 => admin_url( 'admin-ajax.php', 'relative' ),
+		'flash_swf_url'       => includes_url( 'js/plupload/plupload.flash.swf' ),
+		'silverlight_xap_url' => includes_url( 'js/plupload/plupload.silverlight.xap' ),
+		'filters' => array(
+			'max_file_size'   => $max_upload_size . 'b',
+		),
+		'multipart'           => true,
+		'urlstream_upload'    => true,
+	);
+
+	// WordPress is not allowing multi selection for iOs 7 device.. See #29602.
+	if ( wp_is_mobile() && strpos( $_SERVER['HTTP_USER_AGENT'], 'OS 7_' ) !== false &&
+		strpos( $_SERVER['HTTP_USER_AGENT'], 'like Mac OS X' ) !== false ) {
+
+		$defaults['multi_selection'] = false;
+	}
+
+	$settings = array(
+		'defaults' => $defaults,
+		'browser'  => array(
+			'mobile'    => wp_is_mobile(),
+			'supported' => _device_can_upload(),
+		),
+		'limitExceeded' => is_multisite() && ! is_upload_space_available(),
+	);
+
+	/**
+	 * Filter the BuddyPress Plupload default settings.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $settings Default Plupload parameters array.
+	 */
+	return apply_filters( 'bp_attachments_get_plupload_default_settings', $settings );
+}
+
+/**
+ * Builds localization strings for the BuddyPress Uploader scripts.
+ *
+ * @since 2.3.0
+ *
+ * @return array Plupload default localization strings.
+ */
+function bp_attachments_get_plupload_l10n() {
+	// Localization strings.
+	return apply_filters( 'bp_attachments_get_plupload_l10n', array(
+			'queue_limit_exceeded'      => __( 'You have attempted to queue too many files.', 'buddypress' ),
+			'file_exceeds_size_limit'   => __( '%s exceeds the maximum upload size for this site.', 'buddypress' ),
+			'zero_byte_file'            => __( 'This file is empty. Please try another.', 'buddypress' ),
+			'invalid_filetype'          => __( 'This file type is not allowed. Please try another.', 'buddypress' ),
+			'not_an_image'              => __( 'This file is not an image. Please try another.', 'buddypress' ),
+			'image_memory_exceeded'     => __( 'Memory exceeded. Please try another smaller file.', 'buddypress' ),
+			'image_dimensions_exceeded' => __( 'This is larger than the maximum size. Please try another.', 'buddypress' ),
+			'default_error'             => __( 'An error occurred. Please try again later.', 'buddypress' ),
+			'missing_upload_url'        => __( 'There was a configuration error. Please contact the server administrator.', 'buddypress' ),
+			'upload_limit_exceeded'     => __( 'You may only upload 1 file.', 'buddypress' ),
+			'http_error'                => __( 'HTTP error.', 'buddypress' ),
+			'upload_failed'             => __( 'Upload failed.', 'buddypress' ),
+			'big_upload_failed'         => __( 'Please try uploading this file with the %1$sbrowser uploader%2$s.', 'buddypress' ),
+			'big_upload_queued'         => __( '%s exceeds the maximum upload size for the multi-file uploader when used in your browser.', 'buddypress' ),
+			'io_error'                  => __( 'IO error.', 'buddypress' ),
+			'security_error'            => __( 'Security error.', 'buddypress' ),
+			'file_cancelled'            => __( 'File canceled.', 'buddypress' ),
+			'upload_stopped'            => __( 'Upload stopped.', 'buddypress' ),
+			'dismiss'                   => __( 'Dismiss', 'buddypress' ),
+			'crunching'                 => __( 'Crunching&hellip;', 'buddypress' ),
+			'unique_file_warning'       => __( 'Make sure to upload a unique file', 'buddypress' ),
+			'error_uploading'           => __( '&#8220;%s&#8221; has failed to upload.', 'buddypress' ),
+			'has_avatar_warning'        => __( 'If you&#39;d like to delete the existing profile photo but not upload a new one, please use the delete tab.', 'buddypress' )
+	) );
+}
+
+/**
+ * Enqueues the script needed for the Uploader UI.
+ *
+ * @since 2.3.0
+ *
+ * @see BP_Attachment::script_data() && BP_Attachment_Avatar::script_data() for examples showing how
+ * to set specific script data.
+ *
+ * @param string $class Name of the class extending BP_Attachment (eg: BP_Attachment_Avatar).
+ * @return null|WP_Error
+ */
+function bp_attachments_enqueue_scripts( $class = '' ) {
+	// Enqueue me just once per page, please.
+	if ( did_action( 'bp_attachments_enqueue_scripts' ) ) {
+		return;
+	}
+
+	if ( ! $class || ! class_exists( $class ) ) {
+		return new WP_Error( 'missing_parameter' );
+	}
+
+	// Get an instance of the class and get the script data.
+	$attachment = new $class;
+	$script_data  = $attachment->script_data();
+
+	$args = bp_parse_args( $script_data, array(
+		'action'            => '',
+		'file_data_name'    => '',
+		'max_file_size'     => 0,
+		'browse_button'     => 'bp-browse-button',
+		'container'         => 'bp-upload-ui',
+		'drop_element'      => 'drag-drop-area',
+		'bp_params'         => array(),
+		'extra_css'         => array(),
+		'extra_js'          => array(),
+		'feedback_messages' => array(),
+	), 'attachments_enqueue_scripts' );
+
+	if ( empty( $args['action'] ) || empty( $args['file_data_name'] ) ) {
+		return new WP_Error( 'missing_parameter' );
+	}
+
+	// Get the BuddyPress uploader strings.
+	$strings = bp_attachments_get_plupload_l10n();
+
+	// Get the BuddyPress uploader settings.
+	$settings = bp_attachments_get_plupload_default_settings();
+
+	// Set feedback messages.
+	if ( ! empty( $args['feedback_messages'] ) ) {
+		$strings['feedback_messages'] = $args['feedback_messages'];
+	}
+
+	// Use a temporary var to ease manipulation.
+	$defaults = $settings['defaults'];
+
+	// Set the upload action.
+	$defaults['multipart_params']['action'] = $args['action'];
+
+	// Set BuddyPress upload parameters if provided.
+	if ( ! empty( $args['bp_params'] ) ) {
+		$defaults['multipart_params']['bp_params'] = $args['bp_params'];
+	}
+
+	// Merge other arguments.
+	$ui_args = array_intersect_key( $args, array(
+		'file_data_name' => true,
+		'browse_button'  => true,
+		'container'      => true,
+		'drop_element'   => true,
+	) );
+
+	$defaults = array_merge( $defaults, $ui_args );
+
+	if ( ! empty( $args['max_file_size'] ) ) {
+		$defaults['filters']['max_file_size'] = $args['max_file_size'] . 'b';
+	}
+
+	// Specific to BuddyPress Avatars.
+	if ( 'bp_avatar_upload' === $defaults['multipart_params']['action'] ) {
+
+		// Include the cropping informations for avatars.
+		$settings['crop'] = array(
+			'full_h'  => bp_core_avatar_full_height(),
+			'full_w'  => bp_core_avatar_full_width(),
+		);
+
+		// Avatar only need 1 file and 1 only!
+		$defaults['multi_selection'] = false;
+
+		// Does the object already has an avatar set.
+		$has_avatar = $defaults['multipart_params']['bp_params']['has_avatar'];
+
+		// What is the object the avatar belongs to.
+		$object = $defaults['multipart_params']['bp_params']['object'];
+
+		// Init the Avatar nav.
+		$avatar_nav = array(
+			'upload' => array( 'id' => 'upload', 'caption' => __( 'Upload', 'buddypress' ), 'order' => 0  ),
+
+			// The delete view will only show if the object has an avatar.
+			'delete' => array( 'id' => 'delete', 'caption' => __( 'Delete', 'buddypress' ), 'order' => 100, 'hide' => (int) ! $has_avatar ),
+		);
+
+		// Create the Camera Nav if the WebCam capture feature is enabled.
+		if ( bp_avatar_use_webcam() && 'user' === $object ) {
+			$avatar_nav['camera'] = array( 'id' => 'camera', 'caption' => __( 'Take Photo', 'buddypress' ), 'order' => 10 );
+
+			// Set warning messages.
+			$strings['camera_warnings'] = array(
+				'requesting'  => __( 'Please allow us to access to your camera.', 'buddypress'),
+				'loading'     => __( 'Please wait as we access your camera.', 'buddypress' ),
+				'loaded'      => __( 'Camera loaded. Click on the "Capture" button to take your photo.', 'buddypress' ),
+				'noaccess'    => __( 'It looks like you do not have a webcam or we were unable to get permission to use your webcam. Please upload a photo instead.', 'buddypress' ),
+				'errormsg'    => __( 'Your browser is not supported. Please upload a photo instead.', 'buddypress' ),
+				'videoerror'  => __( 'Video error. Please upload a photo instead.', 'buddypress' ),
+				'ready'       => __( 'Your profile photo is ready. Click on the "Save" button to use this photo.', 'buddypress' ),
+				'nocapture'   => __( 'No photo was captured. Click on the "Capture" button to take your photo.', 'buddypress' ),
+			);
+		}
+
+		/**
+		 * Use this filter to add a navigation to a custom tool to set the object's avatar.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $avatar_nav {
+		 *     An associative array of available nav items where each item is an array organized this way:
+		 *     $avatar_nav[ $nav_item_id ].
+		 *     @type string $nav_item_id The nav item id in lower case without special characters or space.
+		 *     @type string $caption     The name of the item nav that will be displayed in the nav.
+		 *     @type int    $order       An integer to specify the priority of the item nav, choose one.
+		 *                               between 1 and 99 to be after the uploader nav item and before the delete nav item.
+		 *     @type int    $hide        If set to 1 the item nav will be hidden
+		 *                               (only used for the delete nav item).
+		 * }
+		 * @param string $object The object the avatar belongs to (eg: user or group).
+		 */
+		$settings['nav'] = bp_sort_by_key( apply_filters( 'bp_attachments_avatar_nav', $avatar_nav, $object ), 'order', 'num' );
+
+	// Specific to BuddyPress cover images.
+	} elseif ( 'bp_cover_image_upload' === $defaults['multipart_params']['action'] ) {
+
+		// Cover images only need 1 file and 1 only!
+		$defaults['multi_selection'] = false;
+
+		// Default cover component is xprofile.
+		$cover_component = 'xprofile';
+
+		// Get the object we're editing the cover image of.
+		$object = $defaults['multipart_params']['bp_params']['object'];
+
+		// Set the cover component according to the object.
+		if ( 'group' === $object ) {
+			$cover_component = 'groups';
+		} elseif ( 'user' !== $object ) {
+			$cover_component = apply_filters( 'bp_attachments_cover_image_ui_component', $cover_component );
+		}
+		// Get cover image advised dimensions.
+		$cover_dimensions = bp_attachments_get_cover_image_dimensions( $cover_component );
+
+		// Set warning messages.
+		$strings['cover_image_warnings'] = apply_filters( 'bp_attachments_cover_image_ui_warnings', array(
+			'dimensions'  => sprintf(
+					__( 'For better results, make sure to upload an image that is larger than %1$spx wide, and %2$spx tall.', 'buddypress' ),
+					(int) $cover_dimensions['width'],
+					(int) $cover_dimensions['height']
+				),
+		) );
+	}
+
+	// Set Plupload settings.
+	$settings['defaults'] = $defaults;
+
+	/**
+	 * Enqueue some extra styles if required
+	 *
+	 * Extra styles need to be registered.
+	 */
+	if ( ! empty( $args['extra_css'] ) ) {
+		foreach ( (array) $args['extra_css'] as $css ) {
+			if ( empty( $css ) ) {
+				continue;
+			}
+
+			wp_enqueue_style( $css );
+		}
+	}
+
+	wp_enqueue_script ( 'bp-plupload' );
+	wp_localize_script( 'bp-plupload', 'BP_Uploader', array( 'strings' => $strings, 'settings' => $settings ) );
+
+	/**
+	 * Enqueue some extra scripts if required
+	 *
+	 * Extra scripts need to be registered.
+	 */
+	if ( ! empty( $args['extra_js'] ) ) {
+		foreach ( (array) $args['extra_js'] as $js ) {
+			if ( empty( $js ) ) {
+				continue;
+			}
+
+			wp_enqueue_script( $js );
+		}
+	}
+
+	/**
+	 * Fires at the conclusion of bp_attachments_enqueue_scripts()
+	 * to avoid the scripts to be loaded more than once.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_attachments_enqueue_scripts' );
+}
+
+/**
+ * Check the current user's capability to edit an avatar for a given object.
+ *
+ * @since 2.3.0
+ *
+ * @param string $capability The capability to check.
+ * @param array  $args       An array containing the item_id and the object to check.
+ * @return bool
+ */
+function bp_attachments_current_user_can( $capability, $args = array() ) {
+	$can = false;
+
+	if ( 'edit_avatar' === $capability || 'edit_cover_image' === $capability ) {
+		/**
+		 * Needed avatar arguments are set.
+		 */
+		if ( isset( $args['item_id'] ) && isset( $args['object'] ) ) {
+			// Group profile photo.
+			if ( bp_is_active( 'groups' ) && 'group' === $args['object'] ) {
+				if ( bp_is_group_create() ) {
+					$can = (bool) groups_is_user_creator( bp_loggedin_user_id(), $args['item_id'] ) || bp_current_user_can( 'bp_moderate' );
+				} else {
+					$can = (bool) groups_is_user_admin( bp_loggedin_user_id(), $args['item_id'] ) || bp_current_user_can( 'bp_moderate' );
+				}
+			// User profile photo.
+			} elseif ( bp_is_active( 'xprofile' ) && 'user' === $args['object'] ) {
+				$can = bp_loggedin_user_id() === (int) $args['item_id'] || bp_current_user_can( 'bp_moderate' );
+			}
+		/**
+		 * No avatar arguments, fallback to bp_user_can_create_groups()
+		 * or bp_is_item_admin()
+		 */
+		} else {
+			if ( bp_is_group_create() ) {
+				$can = bp_user_can_create_groups();
+			} else {
+				$can = bp_is_item_admin();
+			}
+		}
+	}
+
+	return apply_filters( 'bp_attachments_current_user_can', $can, $capability, $args );
+}
+
+/**
+ * Send a JSON response back to an Ajax upload request.
+ *
+ * @since 2.3.0
+ *
+ * @param bool  $success  True for a success, false otherwise.
+ * @param bool  $is_html4 True if the Plupload runtime used is html4, false otherwise.
+ * @param mixed $data     Data to encode as JSON, then print and die.
+ */
+function bp_attachments_json_response( $success, $is_html4 = false, $data = null ) {
+	$response = array( 'success' => $success );
+
+	if ( isset( $data ) ) {
+		$response['data'] = $data;
+	}
+
+	// Send regular json response.
+	if ( ! $is_html4 ) {
+		wp_send_json( $response );
+
+	/**
+	 * Send specific json response
+	 * the html4 Plupload handler requires a text/html content-type for older IE.
+	 * See https://core.trac.wordpress.org/ticket/31037
+	 */
+	} else {
+		echo wp_json_encode( $response );
+
+		wp_die();
+	}
+}
+
+/**
+ * Get an Attachment template part.
+ *
+ * @since 2.3.0
+ *
+ * @param string $slug Template part slug. eg 'uploader' for 'uploader.php'.
+ * @return bool
+ */
+function bp_attachments_get_template_part( $slug ) {
+	$switched = false;
+
+	/*
+	 * Use bp-legacy attachment template part for older bp-default themes or if in
+	 * admin area.
+	 */
+	if ( ! bp_use_theme_compat_with_current_theme() || ( is_admin() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) ) {
+		$current = bp_get_theme_compat_id();
+		if ( 'legacy' !== $current ) {
+			$switched = true;
+			bp_setup_theme_compat( 'legacy' );
+		}
+	}
+
+	// Load the template part.
+	bp_get_template_part( 'assets/_attachments/' . $slug );
+
+	if ( $switched ) {
+		bp_setup_theme_compat( $current );
+	}
+}
+
+/** Cover Image ***************************************************************/
+
+/**
+ * Get the cover image settings
+ *
+ * @since 2.4.0
+ *
+ * @param string $component The component to get the settings for ("xprofile" for user or "groups").
+ * @return false|array The cover image settings in array, false on failure.
+ */
+function bp_attachments_get_cover_image_settings( $component = 'xprofile' ) {
+	// Default parameters.
+	$args = array();
+
+	// First look in BP Theme Compat.
+	$cover_image = bp_get_theme_compat_feature( 'cover_image' );
+
+	if ( ! empty( $cover_image ) ) {
+		$args = (array) $cover_image;
+	}
+
+	/**
+	 * Then let people override/set the feature using this dynamic filter
+	 *
+	 * Eg: for the user's profile cover image use:
+	 * add_filter( 'bp_before_xprofile_cover_image_settings_parse_args', 'your_filter', 10, 1 );
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $settings The cover image settings
+	 */
+	$settings = bp_parse_args( $args, array(
+		'components'    => array(),
+		'width'         => 1300,
+		'height'        => 225,
+		'callback'      => '',
+		'theme_handle'  => '',
+		'default_cover' => '',
+	), $component . '_cover_image_settings' );
+
+	if ( empty( $settings['components'] ) || empty( $settings['callback'] ) || empty( $settings['theme_handle'] ) ) {
+		return false;
+	}
+
+	// Current component is not supported.
+	if ( ! in_array( $component, $settings['components'] ) ) {
+		return false;
+	}
+
+	// Finally return the settings.
+	return $settings;
+}
+
+/**
+ * Get cover image Width and Height.
+ *
+ * @since 2.4.0
+ *
+ * @param string $component The BuddyPress component concerned ("xprofile" for user or "groups").
+ * @return array|bool An associative array containing the advised width and height for the cover image. False if settings are empty.
+ */
+function bp_attachments_get_cover_image_dimensions( $component = 'xprofile' ) {
+	// Let's prevent notices when setting the warning strings.
+	$default = array( 'width' => 0, 'height' => 0 );
+
+	$settings = bp_attachments_get_cover_image_settings( $component );
+
+	if ( empty( $settings ) ) {
+		return false;
+	}
+
+	// Get width and height.
+	$wh = array_intersect_key( $settings, $default );
+
+	/**
+	 * Filter here to edit the cover image dimensions if needed.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array  $wh       An associative array containing the width and height values.
+	 * @param array  $settings An associative array containing all the feature settings.
+	 * @param string $compnent The requested component.
+	 */
+	return apply_filters( 'bp_attachments_get_cover_image_dimensions', $wh, $settings, $component );
+}
+
+/**
+ * Are we on a page to edit a cover image?
+ *
+ * @since 2.4.0
+ *
+ * @return bool True if on a page to edit a cover image, false otherwise.
+ */
+function bp_attachments_cover_image_is_edit() {
+	$retval = false;
+
+	$current_component = bp_current_component();
+	if ( bp_is_active( 'xprofile' ) && bp_is_current_component( 'xprofile' ) ) {
+		$current_component = 'xprofile';
+	}
+
+	if ( ! bp_is_active( $current_component, 'cover_image' ) ) {
+		return $retval;
+	}
+
+	if ( bp_is_user_change_cover_image() ) {
+		$retval = ! bp_disable_cover_image_uploads();
+	}
+
+	if ( ( bp_is_group_admin_page() && 'group-cover-image' == bp_get_group_current_admin_tab() )
+		|| ( bp_is_group_create() && bp_is_group_creation_step( 'group-cover-image' ) ) ) {
+		$retval = ! bp_disable_group_cover_image_uploads();
+	}
+
+	return apply_filters( 'bp_attachments_cover_image_is_edit', $retval, $current_component );
+}
+
+/**
+ * Does the user has a cover image?
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id User ID to retrieve cover image for.
+ * @return bool True if the user has a cover image, false otherwise.
+ */
+function bp_attachments_get_user_has_cover_image( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	$cover_src = bp_attachments_get_attachment( 'url', array(
+		'item_id'   => $user_id,
+	) );
+
+	return (bool) apply_filters( 'bp_attachments_get_user_has_cover_image', $cover_src, $user_id );
+}
+
+/**
+ * Does the group has a cover image?
+ *
+ * @since 2.4.0
+ *
+ * @param int $group_id Group ID to check cover image existence for.
+ * @return bool True if the group has a cover image, false otherwise.
+ */
+function bp_attachments_get_group_has_cover_image( $group_id = 0 ) {
+	if ( empty( $group_id ) ) {
+		$group_id = bp_get_current_group_id();
+	}
+
+	$cover_src = bp_attachments_get_attachment( 'url', array(
+		'object_dir' => 'groups',
+		'item_id'    => $group_id,
+	) );
+
+	return (bool) apply_filters( 'bp_attachments_get_user_has_cover_image', $cover_src, $group_id );
+}
+
+/**
+ * Generate the cover image file.
+ *
+ * @since 2.4.0
+ *
+ * @param array                          $args {
+ *     @type string $file            The absolute path to the image. Required.
+ *     @type string $component       The component for the object (eg: groups, xprofile). Required.
+ *     @type string $cover_image_dir The Cover image dir to write the image into. Required.
+ * }
+ * @param BP_Attachment_Cover_Image|null $cover_image_class The class to use to fit the cover image.
+ * @return false|array An array containing cover image data on success, false otherwise.
+ */
+function bp_attachments_cover_image_generate_file( $args = array(), $cover_image_class = null ) {
+	// Bail if an argument is missing.
+	if ( empty( $args['file'] ) || empty( $args['component'] ) || empty( $args['cover_image_dir'] ) ) {
+		return false;
+	}
+
+	// Get advised dimensions for the cover image.
+	$dimensions = bp_attachments_get_cover_image_dimensions( $args['component'] );
+
+	// No dimensions or the file does not match with the cover image dir, stop!
+	if ( false === $dimensions || $args['file'] !== $args['cover_image_dir'] . '/' . wp_basename( $args['file'] ) ) {
+		return false;
+	}
+
+	if ( ! is_a( $cover_image_class, 'BP_Attachment_Cover_Image' ) ) {
+		$cover_image_class = new BP_Attachment_Cover_Image();
+	}
+
+	$upload_dir = bp_attachments_cover_image_upload_dir();
+
+	// Make sure the file is inside the Cover Image Upload path.
+	if ( false === strpos( $args['file'], $upload_dir['basedir'] ) ) {
+		return false;
+	}
+
+	// Resize the image so that it fit with the cover image dimensions.
+	$cover_image  = $cover_image_class->fit( $args['file'], $dimensions );
+	$is_too_small = false;
+
+	// Image is too small in width and height.
+	if ( empty( $cover_image ) ) {
+		$cover_file = $cover_image_class->generate_filename( $args['file'] );
+		@rename( $args['file'], $cover_file );
+
+		// It's too small!
+		$is_too_small = true;
+	} elseif ( ! empty( $cover_image['path'] ) ) {
+		$cover_file = $cover_image['path'];
+
+		// Image is too small in width or height.
+		if ( $cover_image['width'] < $dimensions['width'] || $cover_image['height'] < $dimensions['height'] ) {
+			$is_too_small = true;
+		}
+	}
+
+	// We were not able to generate the cover image file.
+	if ( empty( $cover_file ) ) {
+		return false;
+	}
+
+	// Do some clean up with old cover image, now a new one is set.
+	$cover_basename = wp_basename( $cover_file );
+
+	if ( $att_dir = opendir( $args['cover_image_dir'] ) ) {
+		while ( false !== ( $attachment_file = readdir( $att_dir ) ) ) {
+			// Skip directories and the new cover image.
+			if ( 2 < strlen( $attachment_file ) && 0 !== strpos( $attachment_file, '.' ) && $cover_basename !== $attachment_file ) {
+				@unlink( $args['cover_image_dir'] . '/' . $attachment_file );
+			}
+		}
+	}
+
+	// Finally return needed data.
+	return array(
+		'cover_file'     => $cover_file,
+		'cover_basename' => $cover_basename,
+		'is_too_small'   => $is_too_small
+	);
+}
+
+/**
+ * Ajax Upload and set a cover image
+ *
+ * @since 2.4.0
+ *
+ * @return string|null A json object containing success data if the upload succeeded,
+ *                     error message otherwise.
+ */
+function bp_attachments_cover_image_ajax_upload() {
+	if ( ! bp_is_post_request() ) {
+		wp_die();
+	}
+
+	check_admin_referer( 'bp-uploader' );
+
+	// Sending the json response will be different if the current Plupload runtime is html4.
+	$is_html4 = ! empty( $_POST['html4' ] );
+
+	if ( empty( $_POST['bp_params'] ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	$bp_params = bp_parse_args( $_POST['bp_params'], array(
+		'object'  => 'user',
+		'item_id' => bp_loggedin_user_id(),
+	), 'attachments_cover_image_ajax_upload' );
+
+	$bp_params['item_id'] = (int) $bp_params['item_id'];
+	$bp_params['object']  = sanitize_text_field( $bp_params['object'] );
+
+	// We need the object to set the uploads dir filter.
+	if ( empty( $bp_params['object'] ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	// Capability check.
+	if ( ! bp_attachments_current_user_can( 'edit_cover_image', $bp_params ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	$bp          = buddypress();
+	$needs_reset = array();
+
+	// Member's cover image.
+	if ( 'user' === $bp_params['object'] ) {
+		$object_data = array( 'dir' => 'members', 'component' => 'xprofile' );
+
+		if ( ! bp_displayed_user_id() && ! empty( $bp_params['item_id'] ) ) {
+			$needs_reset = array( 'key' => 'displayed_user', 'value' => $bp->displayed_user );
+			$bp->displayed_user->id = $bp_params['item_id'];
+		}
+
+	// Group's cover image.
+	} elseif ( 'group' === $bp_params['object'] ) {
+		$object_data = array( 'dir' => 'groups', 'component' => 'groups' );
+
+		if ( ! bp_get_current_group_id() && ! empty( $bp_params['item_id'] ) ) {
+			$needs_reset = array( 'component' => 'groups', 'key' => 'current_group', 'value' => $bp->groups->current_group );
+			$bp->groups->current_group = groups_get_group( $bp_params['item_id'] );
+		}
+
+	// Other object's cover image.
+	} else {
+		$object_data = apply_filters( 'bp_attachments_cover_image_object_dir', array(), $bp_params['object'] );
+	}
+
+	// Stop here in case of a missing parameter for the object.
+	if ( empty( $object_data['dir'] ) || empty( $object_data['component'] ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	/**
+	 * Filters whether or not to handle cover image uploading.
+	 *
+	 * If you want to override this function, make sure you return an array with the 'result' key set.
+	 *
+	 * @since 2.5.1
+	 *
+	 * @param array $value
+	 * @param array $bp_params
+	 * @param array $needs_reset Stores original value of certain globals we need to revert to later.
+	 * @param array $object_data
+	 */
+	$pre_filter = apply_filters( 'bp_attachments_pre_cover_image_ajax_upload', array(), $bp_params, $needs_reset, $object_data );
+	if ( isset( $pre_filter['result'] ) ) {
+		bp_attachments_json_response( $pre_filter['result'], $is_html4, $pre_filter );
+	}
+
+	$cover_image_attachment = new BP_Attachment_Cover_Image();
+	$uploaded = $cover_image_attachment->upload( $_FILES );
+
+	// Reset objects.
+	if ( ! empty( $needs_reset ) ) {
+		if ( ! empty( $needs_reset['component'] ) ) {
+			$bp->{$needs_reset['component']}->{$needs_reset['key']} = $needs_reset['value'];
+		} else {
+			$bp->{$needs_reset['key']} = $needs_reset['value'];
+		}
+	}
+
+	if ( ! empty( $uploaded['error'] ) ) {
+		// Upload error response.
+		bp_attachments_json_response( false, $is_html4, array(
+			'type'    => 'upload_error',
+			'message' => sprintf( __( 'Upload Failed! Error was: %s', 'buddypress' ), $uploaded['error'] ),
+		) );
+	}
+
+	$error_message = __( 'There was a problem uploading the cover image.', 'buddypress' );
+
+	$bp_attachments_uploads_dir = bp_attachments_cover_image_upload_dir();
+
+	// The BP Attachments Uploads Dir is not set, stop.
+	if ( ! $bp_attachments_uploads_dir ) {
+		bp_attachments_json_response( false, $is_html4, array(
+			'type'    => 'upload_error',
+			'message' => $error_message,
+		) );
+	}
+
+	$cover_subdir = $object_data['dir'] . '/' . $bp_params['item_id'] . '/cover-image';
+	$cover_dir    = trailingslashit( $bp_attachments_uploads_dir['basedir'] ) . $cover_subdir;
+
+	if ( 1 === validate_file( $cover_dir ) || ! is_dir( $cover_dir ) ) {
+		// Upload error response.
+		bp_attachments_json_response( false, $is_html4, array(
+			'type'    => 'upload_error',
+			'message' => $error_message,
+		) );
+	}
+
+	/*
+	 * Generate the cover image so that it fit to feature's dimensions
+	 *
+	 * Unlike the avatar, uploading and generating the cover image is happening during
+	 * the same Ajax request, as we already instantiated the BP_Attachment_Cover_Image
+	 * class, let's use it.
+	 */
+	$cover = bp_attachments_cover_image_generate_file( array(
+		'file'            => $uploaded['file'],
+		'component'       => $object_data['component'],
+		'cover_image_dir' => $cover_dir
+	), $cover_image_attachment );
+
+	if ( ! $cover ) {
+		bp_attachments_json_response( false, $is_html4, array(
+			'type'    => 'upload_error',
+			'message' => $error_message,
+		) );
+	}
+
+	$cover_url = trailingslashit( $bp_attachments_uploads_dir['baseurl'] ) . $cover_subdir . '/' . $cover['cover_basename'];
+
+	// 1 is success.
+	$feedback_code = 1;
+
+	// 0 is the size warning.
+	if ( $cover['is_too_small'] ) {
+		$feedback_code = 0;
+	}
+
+	// Set the name of the file.
+	$name = $_FILES['file']['name'];
+	$name_parts = pathinfo( $name );
+	$name = trim( substr( $name, 0, - ( 1 + strlen( $name_parts['extension'] ) ) ) );
+
+	/**
+	 * Fires if the new cover image was successfully uploaded.
+	 *
+	 * The dynamic portion of the hook will be xprofile in case of a user's
+	 * cover image, groups in case of a group's cover image. For instance:
+	 * Use add_action( 'xprofile_cover_image_uploaded' ) to run your specific
+	 * code once the user has set his cover image.
+	 *
+	 * @since 2.4.0
+	 * @since 3.0.0 Added $cover_url, $name, $feedback_code arguments.
+	 *
+	 * @param int    $item_id       Inform about the item id the cover image was set for.
+	 * @param string $name          Filename.
+	 * @param string $cover_url     URL to the image.
+	 * @param int    $feedback_code If value not 1, an error occured.
+	 */
+	do_action(
+		$object_data['component'] . '_cover_image_uploaded',
+		(int) $bp_params['item_id'],
+		$name,
+		$cover_url,
+		$feedback_code
+	);
+
+	// Finally return the cover image url to the UI.
+	bp_attachments_json_response( true, $is_html4, array(
+		'name'          => $name,
+		'url'           => $cover_url,
+		'feedback_code' => $feedback_code,
+	) );
+}
+add_action( 'wp_ajax_bp_cover_image_upload', 'bp_attachments_cover_image_ajax_upload' );
+
+/**
+ * Ajax delete a cover image for a given object and item id.
+ *
+ * @since 2.4.0
+ *
+ * @return string|null A json object containing success data if the cover image was deleted
+ *                     error message otherwise.
+ */
+function bp_attachments_cover_image_ajax_delete() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	if ( empty( $_POST['object'] ) || empty( $_POST['item_id'] ) ) {
+		wp_send_json_error();
+	}
+
+	$args = array(
+		'object'  => sanitize_text_field( $_POST['object'] ),
+		'item_id' => (int) $_POST['item_id'],
+	);
+
+	// Check permissions.
+	check_admin_referer( 'bp_delete_cover_image', 'nonce' );
+	if ( ! bp_attachments_current_user_can( 'edit_cover_image', $args ) ) {
+		wp_send_json_error();
+	}
+
+	// Set object for the user's case.
+	if ( 'user' === $args['object'] ) {
+		$component = 'xprofile';
+		$dir       = 'members';
+
+	// Set it for any other cases.
+	} else {
+		$component = $args['object'] . 's';
+		$dir       = $component;
+	}
+
+	// Handle delete.
+	if ( bp_attachments_delete_file( array( 'item_id' => $args['item_id'], 'object_dir' => $dir, 'type' => 'cover-image' ) ) ) {
+		/**
+		 * Fires if the cover image was successfully deleted.
+		 *
+		 * The dynamic portion of the hook will be xprofile in case of a user's
+		 * cover image, groups in case of a group's cover image. For instance:
+		 * Use add_action( 'xprofile_cover_image_deleted' ) to run your specific
+		 * code once the user has deleted his cover image.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param int $item_id Inform about the item id the cover image was deleted for.
+		 */
+		do_action( "{$component}_cover_image_deleted", (int) $args['item_id'] );
+
+		$response = array(
+			'reset_url'     => '',
+			'feedback_code' => 3,
+		);
+
+		// Get cover image settings in case there's a default header.
+		$cover_params = bp_attachments_get_cover_image_settings( $component );
+
+		// Check if there's a default cover.
+		if ( ! empty( $cover_params['default_cover'] ) ) {
+			$response['reset_url'] = $cover_params['default_cover'];
+		}
+
+		wp_send_json_success( $response );
+
+	} else {
+		wp_send_json_error( array(
+			'feedback_code' => 2,
+		) );
+	}
+}
+add_action( 'wp_ajax_bp_cover_image_delete', 'bp_attachments_cover_image_ajax_delete' );
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-avatars.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-avatars.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-avatars.php	(revision 12265)
@@ -0,0 +1,2027 @@
+<?php
+/**
+ * BuddyPress Avatars.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the constants we need for avatar support.
+ *
+ * @since 1.2.0
+ */
+function bp_core_set_avatar_constants() {
+
+	$bp = buddypress();
+
+	if ( !defined( 'BP_AVATAR_THUMB_WIDTH' ) )
+		define( 'BP_AVATAR_THUMB_WIDTH', 50 );
+
+	if ( !defined( 'BP_AVATAR_THUMB_HEIGHT' ) )
+		define( 'BP_AVATAR_THUMB_HEIGHT', 50 );
+
+	if ( !defined( 'BP_AVATAR_FULL_WIDTH' ) )
+		define( 'BP_AVATAR_FULL_WIDTH', 150 );
+
+	if ( !defined( 'BP_AVATAR_FULL_HEIGHT' ) )
+		define( 'BP_AVATAR_FULL_HEIGHT', 150 );
+
+	if ( !defined( 'BP_AVATAR_ORIGINAL_MAX_WIDTH' ) )
+		define( 'BP_AVATAR_ORIGINAL_MAX_WIDTH', 450 );
+
+	if ( !defined( 'BP_AVATAR_ORIGINAL_MAX_FILESIZE' ) ) {
+		define( 'BP_AVATAR_ORIGINAL_MAX_FILESIZE', bp_attachments_get_max_upload_file_size( 'avatar' ) );
+	}
+
+	if ( ! defined( 'BP_SHOW_AVATARS' ) ) {
+		define( 'BP_SHOW_AVATARS', bp_get_option( 'show_avatars' ) );
+	}
+}
+add_action( 'bp_init', 'bp_core_set_avatar_constants', 3 );
+
+/**
+ * Set up global variables related to avatars.
+ *
+ * @since 1.5.0
+ */
+function bp_core_set_avatar_globals() {
+	$bp = buddypress();
+
+	$bp->avatar        = new stdClass;
+	$bp->avatar->thumb = new stdClass;
+	$bp->avatar->full  = new stdClass;
+
+	// Dimensions.
+	$bp->avatar->thumb->width  = BP_AVATAR_THUMB_WIDTH;
+	$bp->avatar->thumb->height = BP_AVATAR_THUMB_HEIGHT;
+	$bp->avatar->full->width   = BP_AVATAR_FULL_WIDTH;
+	$bp->avatar->full->height  = BP_AVATAR_FULL_HEIGHT;
+
+	// Upload maximums.
+	$bp->avatar->original_max_width    = BP_AVATAR_ORIGINAL_MAX_WIDTH;
+	$bp->avatar->original_max_filesize = BP_AVATAR_ORIGINAL_MAX_FILESIZE;
+
+	// Defaults.
+	$bp->avatar->thumb->default = bp_core_avatar_default_thumb();
+	$bp->avatar->full->default  = bp_core_avatar_default();
+
+	// These have to be set on page load in order to avoid infinite filter loops at runtime.
+	$bp->avatar->upload_path = bp_core_avatar_upload_path();
+	$bp->avatar->url = bp_core_avatar_url();
+
+	// Cache the root blog's show_avatars setting, to avoid unnecessary
+	// calls to switch_to_blog().
+	$bp->avatar->show_avatars = (bool) BP_SHOW_AVATARS;
+
+	// Backpat for pre-1.5.
+	if ( ! defined( 'BP_AVATAR_UPLOAD_PATH' ) )
+		define( 'BP_AVATAR_UPLOAD_PATH', $bp->avatar->upload_path );
+
+	// Backpat for pre-1.5.
+	if ( ! defined( 'BP_AVATAR_URL' ) )
+		define( 'BP_AVATAR_URL', $bp->avatar->url );
+
+	/**
+	 * Fires at the end of the core avatar globals setup.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_core_set_avatar_globals' );
+}
+add_action( 'bp_setup_globals', 'bp_core_set_avatar_globals' );
+
+/**
+ * Get an avatar for a BuddyPress object.
+ *
+ * Supports avatars for users, groups, and blogs by default, but can be
+ * extended to support custom components as well.
+ *
+ * This function gives precedence to locally-uploaded avatars. When a local
+ * avatar is not found, Gravatar is queried. To disable Gravatar fallbacks
+ * locally:
+ *    add_filter( 'bp_core_fetch_avatar_no_grav', '__return_true' );
+ *
+ * @since 1.1.0
+ * @since 2.4.0 Added 'extra_attr', 'scheme', 'rating' and 'force_default' for $args.
+ *              These are inherited from WordPress 4.2.0. See {@link get_avatar()}.
+ *
+ * @param array|string $args {
+ *     An array of arguments. All arguments are technically optional; some
+ *     will, if not provided, be auto-detected by bp_core_fetch_avatar(). This
+ *     auto-detection is described more below, when discussing specific
+ *     arguments.
+ *
+ *     @type int|bool    $item_id    The numeric ID of the item for which you're requesting
+ *                                   an avatar (eg, a user ID). If no 'item_id' is present,
+ *                                   the function attempts to infer an ID from the 'object' + the
+ *                                   current context: if 'object' is 'user' and the current page is a
+ *                                   user page, 'item_id' will default to the displayed user ID; if
+ *                                   'group' and on a group page, to the current group ID; if 'blog',
+ *                                   to the current blog's ID. If no 'item_id' can be determined in
+ *                                   this way, the function returns false. Default: false.
+ *     @type string      $object     The kind of object for which you're getting an
+ *                                   avatar. BuddyPress natively supports three options: 'user',
+ *                                   'group', 'blog'; a plugin may register more.  Default: 'user'.
+ *     @type string      $type       When a new avatar is uploaded to BP, 'thumb' and
+ *                                   'full' versions are saved. This parameter specifies whether you'd
+ *                                   like the 'full' or smaller 'thumb' avatar. Default: 'thumb'.
+ *     @type string|bool $avatar_dir The name of the subdirectory where the
+ *                                   requested avatar should be found. If no value is passed,
+ *                                   'avatar_dir' is inferred from 'object': 'user' becomes 'avatars',
+ *                                   'group' becomes 'group-avatars', 'blog' becomes 'blog-avatars'.
+ *                                   Remember that this string denotes a subdirectory of BP's main
+ *                                   avatar directory (usually based on {@link wp_upload_dir()}); it's a
+ *                                   string like 'group-avatars' rather than the full directory path.
+ *                                   Generally, it'll only be necessary to override the default value if
+ *                                   storing avatars in a non-default location. Defaults to false
+ *                                   (auto-detected).
+ *     @type int|bool    $width      Requested avatar width. The unit is px. This value
+ *                                   is used to build the 'width' attribute for the <img> element. If
+ *                                   no value is passed, BP uses the global avatar width for this
+ *                                   avatar type. Default: false (auto-detected).
+ *     @type int|bool    $height     Requested avatar height. The unit is px. This
+ *                                   value is used to build the 'height' attribute for the <img>
+ *                                   element. If no value is passed, BP uses the global avatar height
+ *                                   for this avatar type. Default: false (auto-detected).
+ *     @type string      $class      The CSS class for the <img> element. Note that BP
+ *                                   uses the 'avatar' class fairly extensively in its default styling,
+ *                                   so if you plan to pass a custom value, consider appending it to
+ *                                   'avatar' (eg 'avatar foo') rather than replacing it altogether.
+ *                                   Default: 'avatar'.
+ *     @type string|bool $css_id     The CSS id for the <img> element.
+ *                                   Default: false.
+ *     @type string      $title      The title attribute for the <img> element.
+ *                                   Default: false.
+ *     @type string      $alt        The alt attribute for the <img> element. In BP, this
+ *                                   value is generally passed by the wrapper functions, where the data
+ *                                   necessary for concatenating the string is at hand; see
+ *                                   {@link bp_get_activity_avatar()} for an example. Default: ''.
+ *     @type string|bool $email      An email to use in Gravatar queries. Unless
+ *                                   otherwise configured, BP uses Gravatar as a fallback for avatars
+ *                                   that are not provided locally. Gravatar's API requires using a hash
+ *                                   of the user's email address; this argument provides it. If not
+ *                                   provided, the function will infer it: for users, by getting the
+ *                                   user's email from the database, for groups/blogs, by concatenating
+ *                                   "{$item_id}-{$object}@{bp_get_root_domain()}". The user query adds
+ *                                   overhead, so it's recommended that wrapper functions provide a
+ *                                   value for 'email' when querying user IDs. Default: false.
+ *     @type bool       $no_grav     Whether to disable the default Gravatar fallback.
+ *                                   By default, BP will fall back on Gravatar when it cannot find a
+ *                                   local avatar. In some cases, this may be undesirable, in which
+ *                                   case 'no_grav' should be set to true. To disable Gravatar
+ *                                   fallbacks globally, see the 'bp_core_fetch_avatar_no_grav' filter.
+ *                                   Default: true for groups, otherwise false.
+ *     @type bool       $html        Whether to return an <img> HTML element, vs a raw URL
+ *                                   to an avatar. If false, <img>-specific arguments (like 'css_id')
+ *                                   will be ignored. Default: true.
+ *     @type string     $extra_attr  HTML attributes to insert in the IMG element. Not sanitized. Default: ''.
+ *     @type string     $scheme      URL scheme to use. See set_url_scheme() for accepted values.
+ *                                   Default null.
+ *     @type string     $rating      What rating to display Gravatars for. Accepts 'G', 'PG', 'R', 'X'.
+ *                                   Default is the value of the 'avatar_rating' option.
+ *     @type bool       $force_default Used when creating the Gravatar URL. Whether to force the default
+ *                                     image regardless if the Gravatar exists. Default: false.
+ * }
+ * @return string Formatted HTML <img> element, or raw avatar URL based on $html arg.
+ */
+function bp_core_fetch_avatar( $args = '' ) {
+	$bp = buddypress();
+
+	// If avatars are disabled for the root site, obey that request and bail.
+	if ( ! $bp->avatar->show_avatars ) {
+		return;
+	}
+
+	global $current_blog;
+
+	// Set the default variables array and parse it against incoming $args array.
+	$params = wp_parse_args( $args, array(
+		'item_id'       => false,
+		'object'        => 'user',
+		'type'          => 'thumb',
+		'avatar_dir'    => false,
+		'width'         => false,
+		'height'        => false,
+		'class'         => 'avatar',
+		'css_id'        => false,
+		'alt'           => '',
+		'email'         => false,
+		'no_grav'       => null,
+		'html'          => true,
+		'title'         => '',
+		'extra_attr'    => '',
+		'scheme'        => null,
+		'rating'        => get_option( 'avatar_rating' ),
+		'force_default' => false,
+	) );
+
+	/* Set item_id ***********************************************************/
+
+	if ( empty( $params['item_id'] ) ) {
+
+		switch ( $params['object'] ) {
+
+			case 'blog'  :
+				$params['item_id'] = $current_blog->id;
+				break;
+
+			case 'group' :
+				if ( bp_is_active( 'groups' ) ) {
+					$params['item_id'] = $bp->groups->current_group->id;
+				} else {
+					$params['item_id'] = false;
+				}
+
+				break;
+
+			case 'user'  :
+			default      :
+				$params['item_id'] = bp_displayed_user_id();
+				break;
+		}
+
+		/**
+		 * Filters the ID of the item being requested.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value  ID of avatar item being requested.
+		 * @param string $value  Avatar type being requested.
+		 * @param array  $params Array of parameters for the request.
+		 */
+		$params['item_id'] = apply_filters( 'bp_core_avatar_item_id', $params['item_id'], $params['object'], $params );
+
+		if ( empty( $params['item_id'] ) ) {
+			return false;
+		}
+	}
+
+	/* Set avatar_dir ********************************************************/
+
+	if ( empty( $params['avatar_dir'] ) ) {
+
+		switch ( $params['object'] ) {
+
+			case 'blog'  :
+				$params['avatar_dir'] = 'blog-avatars';
+				break;
+
+			case 'group' :
+				if ( bp_is_active( 'groups' ) ) {
+					$params['avatar_dir'] = 'group-avatars';
+				} else {
+					$params['avatar_dir'] = false;
+				}
+
+				break;
+
+			case 'user'  :
+			default      :
+				$params['avatar_dir'] = 'avatars';
+				break;
+		}
+
+		/**
+		 * Filters the avatar directory to use.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value  Name of the subdirectory where the requested avatar should be found.
+		 * @param string $value  Avatar type being requested.
+		 * @param array  $params Array of parameters for the request.
+		 */
+		$params['avatar_dir'] = apply_filters( 'bp_core_avatar_dir', $params['avatar_dir'], $params['object'], $params );
+
+		if ( empty( $params['avatar_dir'] ) ) {
+			return false;
+		}
+	}
+
+	/* <img> alt *************************************************************/
+
+	if ( false !== strpos( $params['alt'], '%s' ) || false !== strpos( $params['alt'], '%1$s' ) ) {
+
+		switch ( $params['object'] ) {
+
+			case 'blog'  :
+				$item_name = get_blog_option( $params['item_id'], 'blogname' );
+				break;
+
+			case 'group' :
+				$item_name = bp_get_group_name( groups_get_group( $params['item_id'] ) );
+				break;
+
+			case 'user'  :
+			default :
+				$item_name = bp_core_get_user_displayname( $params['item_id'] );
+				break;
+		}
+
+		/**
+		 * Filters the alt attribute value to be applied to avatar.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value  alt to be applied to avatar.
+		 * @param string $value  ID of avatar item being requested.
+		 * @param string $value  Avatar type being requested.
+		 * @param array  $params Array of parameters for the request.
+		 */
+		$item_name = apply_filters( 'bp_core_avatar_alt', $item_name, $params['item_id'], $params['object'], $params );
+		$params['alt'] = sprintf( $params['alt'], $item_name );
+	}
+
+	/* Sanity Checks *********************************************************/
+
+	// Get a fallback for the 'alt' parameter, create html output.
+	if ( empty( $params['alt'] ) ) {
+		$params['alt'] = __( 'Profile Photo', 'buddypress' );
+	}
+	$html_alt = ' alt="' . esc_attr( $params['alt'] ) . '"';
+
+	// Filter image title and create html string.
+	$html_title = '';
+
+	/**
+	 * Filters the title attribute value to be applied to avatar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value  Title to be applied to avatar.
+	 * @param string $value  ID of avatar item being requested.
+	 * @param string $value  Avatar type being requested.
+	 * @param array  $params Array of parameters for the request.
+	 */
+	$params['title'] = apply_filters( 'bp_core_avatar_title', $params['title'], $params['item_id'], $params['object'], $params );
+
+	if ( ! empty( $params['title'] ) ) {
+		$html_title = ' title="' . esc_attr( $params['title'] ) . '"';
+	}
+
+	// Extra attributes.
+	$extra_attr = ! empty( $args['extra_attr'] ) ? ' ' . $args['extra_attr'] : '';
+
+	// Set CSS ID and create html string.
+	$html_css_id = '';
+
+	/**
+	 * Filters the ID attribute to be applied to avatar.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $value  ID to be applied to avatar.
+	 * @param string $value  ID of avatar item being requested.
+	 * @param string $value  Avatar type being requested.
+	 * @param array  $params Array of parameters for the request.
+	 */
+	$params['css_id'] = apply_filters( 'bp_core_css_id', $params['css_id'], $params['item_id'], $params['object'], $params );
+
+	if ( ! empty( $params['css_id'] ) ) {
+		$html_css_id = ' id="' . esc_attr( $params['css_id'] ) . '"';
+	}
+
+	// Set image width.
+	if ( false !== $params['width'] ) {
+		// Width has been specified. No modification necessary.
+	} elseif ( 'thumb' == $params['type'] ) {
+		$params['width'] = bp_core_avatar_thumb_width();
+	} else {
+		$params['width'] = bp_core_avatar_full_width();
+	}
+	$html_width = ' width="' . $params['width'] . '"';
+
+	// Set image height.
+	if ( false !== $params['height'] ) {
+		// Height has been specified. No modification necessary.
+	} elseif ( 'thumb' == $params['type'] ) {
+		$params['height'] = bp_core_avatar_thumb_height();
+	} else {
+		$params['height'] = bp_core_avatar_full_height();
+	}
+	$html_height = ' height="' . $params['height'] . '"';
+
+	/**
+	 * Filters the classes to be applied to the avatar.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array|string $value  Class(es) to be applied to the avatar.
+	 * @param string       $value  ID of the avatar item being requested.
+	 * @param string       $value  Avatar type being requested.
+	 * @param array        $params Array of parameters for the request.
+	 */
+	$params['class'] = apply_filters( 'bp_core_avatar_class', $params['class'], $params['item_id'], $params['object'], $params );
+
+	// Use an alias to leave the param unchanged.
+	$avatar_classes = $params['class'];
+	if ( ! is_array( $avatar_classes ) ) {
+		$avatar_classes = explode( ' ', $avatar_classes );
+	}
+
+	// Merge classes.
+	$avatar_classes = array_merge( $avatar_classes, array(
+		$params['object'] . '-' . $params['item_id'] . '-avatar',
+		'avatar-' . $params['width'],
+	) );
+
+	// Sanitize each class.
+	$avatar_classes = array_map( 'sanitize_html_class', $avatar_classes );
+
+	// Populate the class attribute.
+	$html_class = ' class="' . join( ' ', $avatar_classes ) . ' photo"';
+
+	// Set img URL and DIR based on prepopulated constants.
+	$avatar_loc        = new stdClass();
+	$avatar_loc->path  = trailingslashit( bp_core_avatar_upload_path() );
+	$avatar_loc->url   = trailingslashit( bp_core_avatar_url() );
+
+	$avatar_loc->dir   = trailingslashit( $params['avatar_dir'] );
+
+	/**
+	 * Filters the avatar folder directory URL.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string $value Path to the avatar folder URL.
+	 * @param int    $value ID of the avatar item being requested.
+	 * @param string $value Avatar type being requested.
+	 * @param string $value Subdirectory where the requested avatar should be found.
+	 */
+	$avatar_folder_url = apply_filters( 'bp_core_avatar_folder_url', ( $avatar_loc->url  . $avatar_loc->dir . $params['item_id'] ), $params['item_id'], $params['object'], $params['avatar_dir'] );
+
+	/**
+	 * Filters the avatar folder directory path.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string $value Path to the avatar folder directory.
+	 * @param int    $value ID of the avatar item being requested.
+	 * @param string $value Avatar type being requested.
+	 * @param string $value Subdirectory where the requested avatar should be found.
+	 */
+	$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', ( $avatar_loc->path . $avatar_loc->dir . $params['item_id'] ), $params['item_id'], $params['object'], $params['avatar_dir'] );
+
+	/**
+	 * Look for uploaded avatar first. Use it if it exists.
+	 * Set the file names to search for, to select the full size
+	 * or thumbnail image.
+	 */
+	$avatar_size              = ( 'full' == $params['type'] ) ? '-bpfull' : '-bpthumb';
+	$legacy_user_avatar_name  = ( 'full' == $params['type'] ) ? '-avatar2' : '-avatar1';
+	$legacy_group_avatar_name = ( 'full' == $params['type'] ) ? '-groupavatar-full' : '-groupavatar-thumb';
+
+	// Check for directory.
+	if ( file_exists( $avatar_folder_dir ) ) {
+
+		// Open directory.
+		if ( $av_dir = opendir( $avatar_folder_dir ) ) {
+
+			// Stash files in an array once to check for one that matches.
+			$avatar_files = array();
+			while ( false !== ( $avatar_file = readdir( $av_dir ) ) ) {
+				// Only add files to the array (skip directories).
+				if ( 2 < strlen( $avatar_file ) ) {
+					$avatar_files[] = $avatar_file;
+				}
+			}
+
+			// Check for array.
+			if ( 0 < count( $avatar_files ) ) {
+
+				// Check for current avatar.
+				foreach( $avatar_files as $key => $value ) {
+					if ( strpos ( $value, $avatar_size )!== false ) {
+						$avatar_url = $avatar_folder_url . '/' . $avatar_files[$key];
+					}
+				}
+
+				// Legacy avatar check.
+				if ( !isset( $avatar_url ) ) {
+					foreach( $avatar_files as $key => $value ) {
+						if ( strpos ( $value, $legacy_user_avatar_name )!== false ) {
+							$avatar_url = $avatar_folder_url . '/' . $avatar_files[$key];
+						}
+					}
+
+					// Legacy group avatar check.
+					if ( !isset( $avatar_url ) ) {
+						foreach( $avatar_files as $key => $value ) {
+							if ( strpos ( $value, $legacy_group_avatar_name )!== false ) {
+								$avatar_url = $avatar_folder_url . '/' . $avatar_files[$key];
+							}
+						}
+					}
+				}
+			}
+		}
+
+		// Close the avatar directory.
+		closedir( $av_dir );
+
+		// If we found a locally uploaded avatar.
+		if ( isset( $avatar_url ) ) {
+			// Support custom scheme.
+			$avatar_url = set_url_scheme( $avatar_url, $params['scheme'] );
+
+			// Return it wrapped in an <img> element.
+			if ( true === $params['html'] ) {
+
+				/**
+				 * Filters an avatar URL wrapped in an <img> element.
+				 *
+				 * @since 1.1.0
+				 *
+				 * @param string $value             Full <img> element for an avatar.
+				 * @param array  $params            Array of parameters for the request.
+				 * @param string $value             ID of the item requested.
+				 * @param string $value             Subdirectory where the requested avatar should be found.
+				 * @param string $html_css_id       ID attribute for avatar.
+				 * @param string $html_width        Width attribute for avatar.
+				 * @param string $html_height       Height attribute for avatar.
+				 * @param string $avatar_folder_url Avatar URL path.
+				 * @param string $avatar_folder_dir Avatar DIR path.
+				 */
+				return apply_filters( 'bp_core_fetch_avatar', '<img src="' . $avatar_url . '"' . $html_class . $html_css_id  . $html_width . $html_height . $html_alt . $html_title . $extra_attr . ' />', $params, $params['item_id'], $params['avatar_dir'], $html_css_id, $html_width, $html_height, $avatar_folder_url, $avatar_folder_dir );
+
+			// ...or only the URL
+			} else {
+
+				/**
+				 * Filters a locally uploaded avatar URL.
+				 *
+				 * @since 1.2.5
+				 *
+				 * @param string $avatar_url URL for a locally uploaded avatar.
+				 * @param array  $params     Array of parameters for the request.
+				 */
+				return apply_filters( 'bp_core_fetch_avatar_url', $avatar_url, $params );
+			}
+		}
+	}
+
+	// By default, Gravatar is not pinged for groups.
+	if ( null === $params['no_grav'] ) {
+		$params['no_grav'] = 'group' === $params['object'];
+	}
+
+	/**
+	 * Filters whether or not to skip Gravatar check.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool  $value  Whether or not to skip Gravatar.
+	 * @param array $params Array of parameters for the avatar request.
+	 */
+	if ( ! apply_filters( 'bp_core_fetch_avatar_no_grav', $params['no_grav'], $params ) ) {
+
+		// Set gravatar type.
+		if ( empty( $bp->grav_default->{$params['object']} ) ) {
+			$default_grav = 'wavatar';
+		} elseif ( 'mystery' == $bp->grav_default->{$params['object']} ) {
+
+			/**
+			 * Filters the Mystery person avatar src value.
+			 *
+			 * @since 1.2.0
+			 *
+			 * @param string $value Avatar value.
+			 * @param string $value Width to display avatar at.
+			 */
+			$default_grav = apply_filters( 'bp_core_mysteryman_src', 'mm', $params['width'] );
+		} else {
+			$default_grav = $bp->grav_default->{$params['object']};
+		}
+
+		// Set gravatar object.
+		if ( empty( $params['email'] ) ) {
+			if ( 'user' == $params['object'] ) {
+				$params['email'] = bp_core_get_user_email( $params['item_id'] );
+			} elseif ( 'group' == $params['object'] || 'blog' == $params['object'] ) {
+				$params['email'] = $params['item_id'] . '-' . $params['object'] . '@' . bp_get_root_domain();
+			}
+		}
+
+		/**
+		 * Filters the Gravatar email to use.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Email to use in Gravatar request.
+		 * @param string $value ID of the item being requested.
+		 * @param string $value Object type being requested.
+		 */
+		$params['email'] = apply_filters( 'bp_core_gravatar_email', $params['email'], $params['item_id'], $params['object'] );
+
+		/**
+		 * Filters the Gravatar URL host.
+		 *
+		 * @since 1.0.2
+		 *
+		 * @param string $value Gravatar URL host.
+		 */
+		$gravatar = apply_filters( 'bp_gravatar_url', '//www.gravatar.com/avatar/' );
+
+		// Append email hash to Gravatar.
+		$gravatar .=  md5( strtolower( $params['email'] ) );
+
+		// Main Gravatar URL args.
+		$url_args = array(
+			's' => $params['width']
+		);
+
+		// Custom Gravatar URL args.
+		if ( ! empty( $params['force_default'] ) ) {
+			$url_args['f'] = 'y';
+		}
+		if ( ! empty( $params['rating'] ) ) {
+			$url_args['r'] = strtolower( $params['rating'] );
+		}
+
+		/**
+		 * Filters the Gravatar "d" parameter.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param string $default_grav The avatar default.
+		 * @param array  $params       The avatar's data.
+		 */
+		$default_grav = apply_filters( 'bp_core_avatar_default', $default_grav, $params );
+
+		// Only set default image if 'Gravatar Logo' is not requested.
+		if ( 'gravatar_default' !== $default_grav ) {
+			$url_args['d'] = $default_grav;
+		}
+
+		// Set up the Gravatar URL.
+		$gravatar = esc_url( add_query_arg(
+			rawurlencode_deep( array_filter( $url_args ) ),
+			$gravatar
+		) );
+
+	// No avatar was found, and we've been told not to use a gravatar.
+	} else {
+
+		/**
+		 * Filters the avatar default when Gravatar is not used.
+		 *
+		 * This is a variable filter dependent on the avatar type being requested.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value  Default avatar for non-gravatar requests.
+		 * @param array  $params Array of parameters for the avatar request.
+		 */
+		$gravatar = apply_filters( 'bp_core_default_avatar_' . $params['object'], bp_core_avatar_default( 'local', $params ), $params );
+	}
+
+	if ( true === $params['html'] ) {
+
+		/** This filter is documented in bp-core/bp-core-avatars.php */
+		return apply_filters( 'bp_core_fetch_avatar', '<img src="' . $gravatar . '"' . $html_css_id . $html_class . $html_width . $html_height . $html_alt . $html_title . $extra_attr . ' />', $params, $params['item_id'], $params['avatar_dir'], $html_css_id, $html_width, $html_height, $avatar_folder_url, $avatar_folder_dir );
+	} else {
+
+		/** This filter is documented in bp-core/bp-core-avatars.php */
+		return apply_filters( 'bp_core_fetch_avatar_url', $gravatar, $params );
+	}
+}
+
+/**
+ * Delete an existing avatar.
+ *
+ * @since 1.1.0
+ *
+ * @param array|string $args {
+ *     Array of function parameters.
+ *     @type bool|int    $item_id    ID of the item whose avatar you're deleting.
+ *                                   Defaults to the current item of type $object.
+ *     @type string      $object     Object type of the item whose avatar you're
+ *                                   deleting. 'user', 'group', 'blog', or custom.
+ *                                   Default: 'user'.
+ *     @type bool|string $avatar_dir Subdirectory where avatar is located.
+ *                                   Default: false, which falls back on the default location
+ *                                   corresponding to the $object.
+ * }
+ * @return bool True on success, false on failure.
+ */
+function bp_core_delete_existing_avatar( $args = '' ) {
+
+	$defaults = array(
+		'item_id'    => false,
+		'object'     => 'user', // User OR group OR blog OR custom type (if you use filters).
+		'avatar_dir' => false
+	);
+
+	$args = wp_parse_args( $args, $defaults );
+	extract( $args, EXTR_SKIP );
+
+	/**
+	 * Filters whether or not to handle deleting an existing avatar.
+	 *
+	 * If you want to override this function, make sure you return false.
+	 *
+	 * @since 2.5.1
+	 *
+	 * @param bool  $value Whether or not to delete the avatar.
+	 * @param array $args {
+	 *     Array of function parameters.
+	 *
+	 *     @type bool|int    $item_id    ID of the item whose avatar you're deleting.
+	 *                                   Defaults to the current item of type $object.
+	 *     @type string      $object     Object type of the item whose avatar you're
+	 *                                   deleting. 'user', 'group', 'blog', or custom.
+	 *                                   Default: 'user'.
+	 *     @type bool|string $avatar_dir Subdirectory where avatar is located.
+	 *                                   Default: false, which falls back on the default location
+	 *                                   corresponding to the $object.
+	 * }
+	 */
+	if ( ! apply_filters( 'bp_core_pre_delete_existing_avatar', true, $args ) ) {
+		return true;
+	}
+
+	if ( empty( $item_id ) ) {
+		if ( 'user' == $object )
+			$item_id = bp_displayed_user_id();
+		elseif ( 'group' == $object )
+			$item_id = buddypress()->groups->current_group->id;
+		elseif ( 'blog' == $object )
+			$item_id = $current_blog->id;
+
+		/** This filter is documented in bp-core/bp-core-avatars.php */
+		$item_id = apply_filters( 'bp_core_avatar_item_id', $item_id, $object );
+
+		if ( !$item_id ) return false;
+	}
+
+	if ( empty( $avatar_dir ) ) {
+		if ( 'user' == $object )
+			$avatar_dir = 'avatars';
+		elseif ( 'group' == $object )
+			$avatar_dir = 'group-avatars';
+		elseif ( 'blog' == $object )
+			$avatar_dir = 'blog-avatars';
+
+		/** This filter is documented in bp-core/bp-core-avatars.php */
+		$avatar_dir = apply_filters( 'bp_core_avatar_dir', $avatar_dir, $object );
+
+		if ( !$avatar_dir ) return false;
+	}
+
+	/** This filter is documented in bp-core/bp-core-avatars.php */
+	$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', bp_core_avatar_upload_path() . '/' . $avatar_dir . '/' . $item_id, $item_id, $object, $avatar_dir );
+
+	if ( !file_exists( $avatar_folder_dir ) )
+		return false;
+
+	if ( $av_dir = opendir( $avatar_folder_dir ) ) {
+		while ( false !== ( $avatar_file = readdir($av_dir) ) ) {
+			if ( ( preg_match( "/-bpfull/", $avatar_file ) || preg_match( "/-bpthumb/", $avatar_file ) ) && '.' != $avatar_file && '..' != $avatar_file )
+				@unlink( $avatar_folder_dir . '/' . $avatar_file );
+		}
+	}
+	closedir($av_dir);
+
+	@rmdir( $avatar_folder_dir );
+
+	/**
+	 * Fires after deleting an existing avatar.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array $args Array of arguments used for avatar deletion.
+	 */
+	do_action( 'bp_core_delete_existing_avatar', $args );
+
+	return true;
+}
+
+/**
+ * Ajax delete an avatar for a given object and item id.
+ *
+ * @since 2.3.0
+ *
+ * @return string|null A JSON object containing success data if the avatar was deleted,
+ *                     error message otherwise.
+ */
+function bp_avatar_ajax_delete() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	$avatar_data = $_POST;
+
+	if ( empty( $avatar_data['object'] ) || empty( $avatar_data['item_id'] ) ) {
+		wp_send_json_error();
+	}
+
+	$nonce = 'bp_delete_avatar_link';
+	if ( 'group' === $avatar_data['object'] ) {
+		$nonce = 'bp_group_avatar_delete';
+	}
+
+	// Check the nonce.
+	check_admin_referer( $nonce, 'nonce' );
+
+	// Capability check.
+	if ( ! bp_attachments_current_user_can( 'edit_avatar', $avatar_data ) ) {
+		wp_send_json_error();
+	}
+
+	// Handle delete.
+	if ( bp_core_delete_existing_avatar( array( 'item_id' => $avatar_data['item_id'], 'object' => $avatar_data['object'] ) ) ) {
+		$return = array(
+			'avatar' => html_entity_decode( bp_core_fetch_avatar( array(
+				'object'  => $avatar_data['object'],
+				'item_id' => $avatar_data['item_id'],
+				'html'    => false,
+				'type'    => 'full',
+			) ) ),
+			'feedback_code' => 4,
+			'item_id'       => $avatar_data['item_id'],
+		);
+
+		wp_send_json_success( $return );
+	} else {
+		wp_send_json_error( array(
+			'feedback_code' => 3,
+		) );
+	}
+}
+add_action( 'wp_ajax_bp_avatar_delete', 'bp_avatar_ajax_delete' );
+
+/**
+ * Handle avatar uploading.
+ *
+ * The functions starts off by checking that the file has been uploaded
+ * properly using bp_core_check_avatar_upload(). It then checks that the file
+ * size is within limits, and that it has an accepted file extension (jpg, gif,
+ * png). If everything checks out, crop the image and move it to its real
+ * location.
+ *
+ * @since 1.1.0
+ *
+ * @see bp_core_check_avatar_upload()
+ * @see bp_core_check_avatar_type()
+ *
+ * @param array  $file              The appropriate entry the from $_FILES superglobal.
+ * @param string $upload_dir_filter A filter to be applied to 'upload_dir'.
+ * @return bool True on success, false on failure.
+ */
+function bp_core_avatar_handle_upload( $file, $upload_dir_filter ) {
+
+	/**
+	 * Filters whether or not to handle uploading.
+	 *
+	 * If you want to override this function, make sure you return false.
+	 *
+	 * @since 1.2.4
+	 *
+	 * @param bool   $value             Whether or not to crop.
+	 * @param array  $file              Appropriate entry from $_FILES superglobal.
+	 * @parma string $upload_dir_filter A filter to be applied to 'upload_dir'.
+	 */
+	if ( ! apply_filters( 'bp_core_pre_avatar_handle_upload', true, $file, $upload_dir_filter ) ) {
+		return true;
+	}
+
+	// Setup some variables.
+	$bp          = buddypress();
+	$upload_path = bp_core_avatar_upload_path();
+
+	// Upload the file.
+	$avatar_attachment = new BP_Attachment_Avatar();
+	$bp->avatar_admin->original = $avatar_attachment->upload( $file, $upload_dir_filter );
+
+	// In case of an error, stop the process and display a feedback to the user.
+	if ( ! empty( $bp->avatar_admin->original['error'] ) ) {
+		bp_core_add_message( sprintf( __( 'Upload Failed! Error was: %s', 'buddypress' ), $bp->avatar_admin->original['error'] ), 'error' );
+		return false;
+	}
+
+	// The Avatar UI available width.
+	$ui_available_width = 0;
+
+	// Try to set the ui_available_width using the avatar_admin global.
+	if ( isset( $bp->avatar_admin->ui_available_width ) ) {
+		$ui_available_width = $bp->avatar_admin->ui_available_width;
+	}
+
+	// Maybe resize.
+	$bp->avatar_admin->resized = $avatar_attachment->shrink( $bp->avatar_admin->original['file'], $ui_available_width );
+	$bp->avatar_admin->image   = new stdClass();
+
+	// We only want to handle one image after resize.
+	if ( empty( $bp->avatar_admin->resized ) ) {
+		$bp->avatar_admin->image->file = $bp->avatar_admin->original['file'];
+		$bp->avatar_admin->image->dir  = str_replace( $upload_path, '', $bp->avatar_admin->original['file'] );
+	} else {
+		$bp->avatar_admin->image->file = $bp->avatar_admin->resized['path'];
+		$bp->avatar_admin->image->dir  = str_replace( $upload_path, '', $bp->avatar_admin->resized['path'] );
+		@unlink( $bp->avatar_admin->original['file'] );
+	}
+
+	// Check for WP_Error on what should be an image.
+	if ( is_wp_error( $bp->avatar_admin->image->dir ) ) {
+		bp_core_add_message( sprintf( __( 'Upload failed! Error was: %s', 'buddypress' ), $bp->avatar_admin->image->dir->get_error_message() ), 'error' );
+		return false;
+	}
+
+	// If the uploaded image is smaller than the "full" dimensions, throw a warning.
+	if ( $avatar_attachment->is_too_small( $bp->avatar_admin->image->file ) ) {
+		bp_core_add_message( sprintf( __( 'You have selected an image that is smaller than recommended. For best results, upload a picture larger than %d x %d pixels.', 'buddypress' ), bp_core_avatar_full_width(), bp_core_avatar_full_height() ), 'error' );
+	}
+
+	// Set the url value for the image.
+	$bp->avatar_admin->image->url = bp_core_avatar_url() . $bp->avatar_admin->image->dir;
+
+	return true;
+}
+
+/**
+ * Ajax upload an avatar.
+ *
+ * @since 2.3.0
+ *
+ * @return string|null A JSON object containing success data if the upload succeeded
+ *                     error message otherwise.
+ */
+function bp_avatar_ajax_upload() {
+	if ( ! bp_is_post_request() ) {
+		wp_die();
+	}
+
+	/**
+	 * Sending the json response will be different if
+	 * the current Plupload runtime is html4.
+	 */
+	$is_html4 = false;
+	if ( ! empty( $_POST['html4' ] ) ) {
+		$is_html4 = true;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'bp-uploader' );
+
+	// Init the BuddyPress parameters.
+	$bp_params = array();
+
+	// We need it to carry on.
+	if ( ! empty( $_POST['bp_params' ] ) ) {
+		$bp_params = $_POST['bp_params' ];
+	} else {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	// We need the object to set the uploads dir filter.
+	if ( empty( $bp_params['object'] ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	// Capability check.
+	if ( ! bp_attachments_current_user_can( 'edit_avatar', $bp_params ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	$bp = buddypress();
+	$bp_params['upload_dir_filter'] = '';
+	$needs_reset = array();
+
+	if ( 'user' === $bp_params['object'] && bp_is_active( 'xprofile' ) ) {
+		$bp_params['upload_dir_filter'] = 'xprofile_avatar_upload_dir';
+
+		if ( ! bp_displayed_user_id() && ! empty( $bp_params['item_id'] ) ) {
+			$needs_reset = array( 'key' => 'displayed_user', 'value' => $bp->displayed_user );
+			$bp->displayed_user->id = $bp_params['item_id'];
+		}
+	} elseif ( 'group' === $bp_params['object'] && bp_is_active( 'groups' ) ) {
+		$bp_params['upload_dir_filter'] = 'groups_avatar_upload_dir';
+
+		if ( ! bp_get_current_group_id() && ! empty( $bp_params['item_id'] ) ) {
+			$needs_reset = array( 'component' => 'groups', 'key' => 'current_group', 'value' => $bp->groups->current_group );
+			$bp->groups->current_group = groups_get_group( $bp_params['item_id'] );
+		}
+	} else {
+		/**
+		 * Filter here to deal with other components.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @var array $bp_params the BuddyPress Ajax parameters.
+		 */
+		$bp_params = apply_filters( 'bp_core_avatar_ajax_upload_params', $bp_params );
+	}
+
+	if ( ! isset( $bp->avatar_admin ) ) {
+		$bp->avatar_admin = new stdClass();
+	}
+
+	/**
+	 * The BuddyPress upload parameters is including the Avatar UI Available width,
+	 * add it to the avatar_admin global for a later use.
+	 */
+	if ( isset( $bp_params['ui_available_width'] ) ) {
+		$bp->avatar_admin->ui_available_width =  (int) $bp_params['ui_available_width'];
+	}
+
+	// Upload the avatar.
+	$avatar = bp_core_avatar_handle_upload( $_FILES, $bp_params['upload_dir_filter'] );
+
+	// Reset objects.
+	if ( ! empty( $needs_reset ) ) {
+		if ( ! empty( $needs_reset['component'] ) ) {
+			$bp->{$needs_reset['component']}->{$needs_reset['key']} = $needs_reset['value'];
+		} else {
+			$bp->{$needs_reset['key']} = $needs_reset['value'];
+		}
+	}
+
+	// Init the feedback message.
+	$feedback_message = false;
+
+	if ( ! empty( $bp->template_message ) ) {
+		$feedback_message = $bp->template_message;
+
+		// Remove template message.
+		$bp->template_message      = false;
+		$bp->template_message_type = false;
+
+		@setcookie( 'bp-message', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+		@setcookie( 'bp-message-type', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	}
+
+	if ( empty( $avatar ) ) {
+		// Default upload error.
+		$message = __( 'Upload failed.', 'buddypress' );
+
+		// Use the template message if set.
+		if ( ! empty( $feedback_message ) ) {
+			$message = $feedback_message;
+		}
+
+		// Upload error reply.
+		bp_attachments_json_response( false, $is_html4, array(
+			'type'    => 'upload_error',
+			'message' => $message,
+		) );
+	}
+
+	if ( empty( $bp->avatar_admin->image->file ) ) {
+		bp_attachments_json_response( false, $is_html4 );
+	}
+
+	$uploaded_image = @getimagesize( $bp->avatar_admin->image->file );
+
+	// Set the name of the file.
+	$name = $_FILES['file']['name'];
+	$name_parts = pathinfo( $name );
+	$name = trim( substr( $name, 0, - ( 1 + strlen( $name_parts['extension'] ) ) ) );
+
+	// Finally return the avatar to the editor.
+	bp_attachments_json_response( true, $is_html4, array(
+		'name'      => $name,
+		'url'       => $bp->avatar_admin->image->url,
+		'width'     => $uploaded_image[0],
+		'height'    => $uploaded_image[1],
+		'feedback'  => $feedback_message,
+	) );
+}
+add_action( 'wp_ajax_bp_avatar_upload', 'bp_avatar_ajax_upload' );
+
+/**
+ * Handle avatar webcam capture.
+ *
+ * @since 2.3.0
+ *
+ * @param string $data    Base64 encoded image.
+ * @param int    $item_id Item to associate.
+ * @return bool True on success, false on failure.
+ */
+function bp_avatar_handle_capture( $data = '', $item_id = 0 ) {
+	if ( empty( $data ) || empty( $item_id ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters whether or not to handle avatar webcam capture.
+	 *
+	 * If you want to override this function, make sure you return false.
+	 *
+	 * @since 2.5.1
+	 *
+	 * @param bool   $value   Whether or not to crop.
+	 * @param string $data    Base64 encoded image.
+	 * @param int    $item_id Item to associate.
+	 */
+	if ( ! apply_filters( 'bp_avatar_pre_handle_capture', true, $data, $item_id ) ) {
+		return true;
+	}
+
+	$avatar_dir = bp_core_avatar_upload_path() . '/avatars';
+
+	// It's not a regular upload, we may need to create this folder.
+	if ( ! file_exists( $avatar_dir ) ) {
+		if ( ! wp_mkdir_p( $avatar_dir ) ) {
+			return false;
+		}
+	}
+
+	/**
+	 * Filters the Avatar folder directory.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $avatar_dir Directory for storing avatars.
+	 * @param int    $item_id    ID of the item being acted on.
+	 * @param string $value      Avatar type.
+	 * @param string $value      Avatars word.
+	 */
+	$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', $avatar_dir . '/' . $item_id, $item_id, 'user', 'avatars' );
+
+	// It's not a regular upload, we may need to create this folder.
+	if( ! is_dir( $avatar_folder_dir ) ) {
+		if ( ! wp_mkdir_p( $avatar_folder_dir ) ) {
+			return false;
+		}
+	}
+
+	$original_file = $avatar_folder_dir . '/webcam-capture-' . $item_id . '.png';
+
+	if ( file_put_contents( $original_file, $data ) ) {
+		$avatar_to_crop = str_replace( bp_core_avatar_upload_path(), '', $original_file );
+
+		// Crop to default values.
+		$crop_args = array( 'item_id' => $item_id, 'original_file' => $avatar_to_crop, 'crop_x' => 0, 'crop_y' => 0 );
+
+		return bp_core_avatar_handle_crop( $crop_args );
+	} else {
+		return false;
+	}
+}
+
+/**
+ * Crop an uploaded avatar.
+ *
+ * @since 1.1.0
+ *
+ * @param array|string $args {
+ *     Array of function parameters.
+ *
+ *     @type string      $object        Object type of the item whose avatar you're
+ *                                      handling. 'user', 'group', 'blog', or custom.
+ *                                      Default: 'user'.
+ *     @type string      $avatar_dir    Subdirectory where avatar should be stored.
+ *                                      Default: 'avatars'.
+ *     @type bool|int    $item_id       ID of the item that the avatar belongs to.
+ *     @type bool|string $original_file Absolute path to the original avatar file.
+ *     @type int         $crop_w        Crop width. Default: the global 'full' avatar width,
+ *                                      as retrieved by bp_core_avatar_full_width().
+ *     @type int         $crop_h        Crop height. Default: the global 'full' avatar height,
+ *                                      as retrieved by bp_core_avatar_full_height().
+ *     @type int         $crop_x        The horizontal starting point of the crop. Default: 0.
+ *     @type int         $crop_y        The vertical starting point of the crop. Default: 0.
+ * }
+ * @return bool True on success, false on failure.
+ */
+function bp_core_avatar_handle_crop( $args = '' ) {
+
+	$r = wp_parse_args( $args, array(
+		'object'        => 'user',
+		'avatar_dir'    => 'avatars',
+		'item_id'       => false,
+		'original_file' => false,
+		'crop_w'        => bp_core_avatar_full_width(),
+		'crop_h'        => bp_core_avatar_full_height(),
+		'crop_x'        => 0,
+		'crop_y'        => 0
+	) );
+
+	/**
+	 * Filters whether or not to handle cropping.
+	 *
+	 * If you want to override this function, make sure you return false.
+	 *
+	 * @since 1.2.4
+	 *
+	 * @param bool  $value Whether or not to crop.
+	 * @param array $r     Array of parsed arguments for function.
+	 */
+	if ( ! apply_filters( 'bp_core_pre_avatar_handle_crop', true, $r ) ) {
+		return true;
+	}
+
+	// Crop the file.
+	$avatar_attachment = new BP_Attachment_Avatar();
+	$cropped           = $avatar_attachment->crop( $r );
+
+	// Check for errors.
+	if ( empty( $cropped['full'] ) || empty( $cropped['thumb'] ) || is_wp_error( $cropped['full'] ) || is_wp_error( $cropped['thumb'] ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Ajax set an avatar for a given object and item id.
+ *
+ * @since 2.3.0
+ *
+ * @return string|null A JSON object containing success data if the crop/capture succeeded
+ *                     error message otherwise.
+ */
+function bp_avatar_ajax_set() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'bp_avatar_cropstore', 'nonce' );
+
+	$avatar_data = wp_parse_args( $_POST, array(
+		'crop_w' => bp_core_avatar_full_width(),
+		'crop_h' => bp_core_avatar_full_height(),
+		'crop_x' => 0,
+		'crop_y' => 0
+	) );
+
+	if ( empty( $avatar_data['object'] ) || empty( $avatar_data['item_id'] ) || empty( $avatar_data['original_file'] ) ) {
+		wp_send_json_error();
+	}
+
+	// Capability check.
+	if ( ! bp_attachments_current_user_can( 'edit_avatar', $avatar_data ) ) {
+		wp_send_json_error();
+	}
+
+	if ( ! empty( $avatar_data['type'] ) && 'camera' === $avatar_data['type'] && 'user' === $avatar_data['object'] ) {
+		$webcam_avatar = false;
+
+		if ( ! empty( $avatar_data['original_file'] ) ) {
+			$webcam_avatar = str_replace( array( 'data:image/png;base64,', ' ' ), array( '', '+' ), $avatar_data['original_file'] );
+			$webcam_avatar = base64_decode( $webcam_avatar );
+		}
+
+		if ( ! bp_avatar_handle_capture( $webcam_avatar, $avatar_data['item_id'] ) ) {
+			wp_send_json_error( array(
+				'feedback_code' => 1
+			) );
+
+		} else {
+			$return = array(
+				'avatar' => html_entity_decode( bp_core_fetch_avatar( array(
+					'object'  => $avatar_data['object'],
+					'item_id' => $avatar_data['item_id'],
+					'html'    => false,
+					'type'    => 'full',
+				) ) ),
+				'feedback_code' => 2,
+				'item_id'       => $avatar_data['item_id'],
+			);
+
+			/**
+			 * Fires if the new avatar was successfully captured.
+			 *
+			 * @since 1.1.0 Used to inform the avatar was successfully cropped
+			 * @since 2.3.4 Add two new parameters to inform about the user id and
+			 *              about the way the avatar was set (eg: 'crop' or 'camera')
+			 *              Move the action at the right place, once the avatar is set
+			 * @since 2.8.0 Added the `$avatar_data` parameter.
+			 *
+			 * @param string $item_id     Inform about the user id the avatar was set for.
+			 * @param string $type        Inform about the way the avatar was set ('camera').
+			 * @param array  $avatar_data Array of parameters passed to the avatar handler.
+			 */
+			do_action( 'xprofile_avatar_uploaded', (int) $avatar_data['item_id'], $avatar_data['type'], $avatar_data );
+
+			wp_send_json_success( $return );
+		}
+
+		return;
+	}
+
+	$original_file = str_replace( bp_core_avatar_url(), '', $avatar_data['original_file'] );
+
+	// Set avatars dir & feedback part.
+	if ( 'user' === $avatar_data['object'] ) {
+		$avatar_dir = 'avatars';
+
+	// Defaults to object-avatars dir.
+	} else {
+		$avatar_dir = sanitize_key( $avatar_data['object'] ) . '-avatars';
+	}
+
+	// Crop args.
+	$r = array(
+		'item_id'       => $avatar_data['item_id'],
+		'object'        => $avatar_data['object'],
+		'avatar_dir'    => $avatar_dir,
+		'original_file' => $original_file,
+		'crop_w'        => $avatar_data['crop_w'],
+		'crop_h'        => $avatar_data['crop_h'],
+		'crop_x'        => $avatar_data['crop_x'],
+		'crop_y'        => $avatar_data['crop_y']
+	);
+
+	// Handle crop.
+	if ( bp_core_avatar_handle_crop( $r ) ) {
+		$return = array(
+			'avatar' => html_entity_decode( bp_core_fetch_avatar( array(
+				'object'  => $avatar_data['object'],
+				'item_id' => $avatar_data['item_id'],
+				'html'    => false,
+				'type'    => 'full',
+			) ) ),
+			'feedback_code' => 2,
+			'item_id'       => $avatar_data['item_id'],
+		);
+
+		if ( 'user' === $avatar_data['object'] ) {
+			/** This action is documented in bp-core/bp-core-avatars.php */
+			do_action( 'xprofile_avatar_uploaded', (int) $avatar_data['item_id'], $avatar_data['type'], $r );
+		} elseif ( 'group' === $avatar_data['object'] ) {
+			/** This action is documented in bp-groups/bp-groups-screens.php */
+			do_action( 'groups_avatar_uploaded', (int) $avatar_data['item_id'], $avatar_data['type'], $r );
+		}
+
+		wp_send_json_success( $return );
+	} else {
+		wp_send_json_error( array(
+			'feedback_code' => 1,
+		) );
+	}
+}
+add_action( 'wp_ajax_bp_avatar_set', 'bp_avatar_ajax_set' );
+
+/**
+ * Filter {@link get_avatar_url()} to use the BuddyPress user avatar URL.
+ *
+ * @since 2.9.0
+ *
+ * @param  string $retval      The URL of the avatar.
+ * @param  mixed  $id_or_email The Gravatar to retrieve. Accepts a user_id, gravatar md5 hash,
+ *                             user email, WP_User object, WP_Post object, or WP_Comment object.
+ * @param  array  $args        Arguments passed to get_avatar_data(), after processing.
+ * @return string
+ */
+function bp_core_get_avatar_data_url_filter( $retval, $id_or_email, $args ) {
+	$user = null;
+
+	// Ugh, hate duplicating code; process the user identifier.
+	if ( is_numeric( $id_or_email ) ) {
+		$user = get_user_by( 'id', absint( $id_or_email ) );
+	} elseif ( $id_or_email instanceof WP_User ) {
+		// User Object
+		$user = $id_or_email;
+	} elseif ( $id_or_email instanceof WP_Post ) {
+		// Post Object
+		$user = get_user_by( 'id', (int) $id_or_email->post_author );
+	} elseif ( $id_or_email instanceof WP_Comment ) {
+		if ( ! empty( $id_or_email->user_id ) ) {
+			$user = get_user_by( 'id', (int) $id_or_email->user_id );
+		}
+	} elseif ( is_email( $id_or_email ) ) {
+		$user = get_user_by( 'email', $id_or_email );
+	}
+
+	// No user, so bail.
+	if ( false === $user instanceof WP_User ) {
+		return $retval;
+	}
+
+	// Set BuddyPress-specific avatar args.
+	$args['item_id'] = $user->ID;
+	$args['html']    = false;
+
+	// Use the 'full' type if size is larger than BP's thumb width.
+	if ( (int) $args['size'] > bp_core_avatar_thumb_width() ) {
+		$args['type'] = 'full';
+	}
+
+	// Get the BuddyPress avatar URL.
+	if ( $bp_avatar = bp_core_fetch_avatar( $args ) ) {
+		return $bp_avatar;
+	}
+
+	return $retval;
+}
+add_filter( 'get_avatar_url', 'bp_core_get_avatar_data_url_filter', 10, 3 );
+
+/**
+ * Is the current avatar upload error-free?
+ *
+ * @since 1.0.0
+ *
+ * @param array $file The $_FILES array.
+ * @return bool True if no errors are found. False if there are errors.
+ */
+function bp_core_check_avatar_upload( $file ) {
+	if ( isset( $file['error'] ) && $file['error'] )
+		return false;
+
+	return true;
+}
+
+/**
+ * Is the file size of the current avatar upload permitted?
+ *
+ * @since 1.0.0
+ *
+ * @param array $file The $_FILES array.
+ * @return bool True if the avatar is under the size limit, otherwise false.
+ */
+function bp_core_check_avatar_size( $file ) {
+	if ( $file['file']['size'] > bp_core_avatar_original_max_filesize() )
+		return false;
+
+	return true;
+}
+
+/**
+ * Get allowed avatar types.
+ *
+ * @since 2.3.0
+ *
+ * @return array
+ */
+function bp_core_get_allowed_avatar_types() {
+	$allowed_types = bp_attachments_get_allowed_types( 'avatar' );
+
+	/**
+	 * Filters the list of allowed image types.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $allowed_types List of image types.
+	 */
+	$avatar_types = (array) apply_filters( 'bp_core_get_allowed_avatar_types', $allowed_types );
+
+	if ( empty( $avatar_types ) ) {
+		$avatar_types = $allowed_types;
+	} else {
+		$avatar_types = array_intersect( $allowed_types, $avatar_types );
+	}
+
+	return array_values( $avatar_types );
+}
+
+/**
+ * Get allowed avatar mime types.
+ *
+ * @since 2.3.0
+ *
+ * @return array
+ */
+function bp_core_get_allowed_avatar_mimes() {
+	$allowed_types  = bp_core_get_allowed_avatar_types();
+
+	return bp_attachments_get_allowed_mimes( 'avatar', $allowed_types );
+}
+
+/**
+ * Does the current avatar upload have an allowed file type?
+ *
+ * Permitted file types are JPG, GIF and PNG.
+ *
+ * @since 1.0.0
+ *
+ * @param array $file The $_FILES array.
+ * @return bool True if the file extension is permitted, otherwise false.
+ */
+function bp_core_check_avatar_type( $file ) {
+	return bp_attachments_check_filetype( $file['file']['tmp_name'], $file['file']['name'], bp_core_get_allowed_avatar_mimes() );
+}
+
+/**
+ * Fetch data from the BP root blog's upload directory.
+ *
+ * @since 1.8.0
+ *
+ * @param string $type The variable we want to return from the $bp->avatars object.
+ *                     Only 'upload_path' and 'url' are supported. Default: 'upload_path'.
+ * @return string The avatar upload directory path.
+ */
+function bp_core_get_upload_dir( $type = 'upload_path' ) {
+	$bp = buddypress();
+
+	switch ( $type ) {
+		case 'upload_path' :
+			$constant = 'BP_AVATAR_UPLOAD_PATH';
+			$key      = 'basedir';
+
+			break;
+
+		case 'url' :
+			$constant = 'BP_AVATAR_URL';
+			$key      = 'baseurl';
+
+			break;
+
+		default :
+			return false;
+
+			break;
+	}
+
+	// See if the value has already been calculated and stashed in the $bp global.
+	if ( isset( $bp->avatar->$type ) ) {
+		$retval = $bp->avatar->$type;
+	} else {
+		// If this value has been set in a constant, just use that.
+		if ( defined( $constant ) ) {
+			$retval = constant( $constant );
+		} else {
+
+			// Use cached upload dir data if available.
+			if ( ! empty( $bp->avatar->upload_dir ) ) {
+				$upload_dir = $bp->avatar->upload_dir;
+
+			// No cache, so query for it.
+			} else {
+
+				// Get upload directory information from current site.
+				$upload_dir = bp_upload_dir();
+
+				// Stash upload directory data for later use.
+				$bp->avatar->upload_dir = $upload_dir;
+			}
+
+			// Directory does not exist and cannot be created.
+			if ( ! empty( $upload_dir['error'] ) ) {
+				$retval = '';
+
+			} else {
+				$retval = $upload_dir[$key];
+
+				// If $key is 'baseurl', check to see if we're on SSL
+				// Workaround for WP13941, WP15928, WP19037.
+				if ( $key == 'baseurl' && is_ssl() ) {
+					$retval = str_replace( 'http://', 'https://', $retval );
+				}
+			}
+
+		}
+
+		// Stash in $bp for later use.
+		$bp->avatar->$type = $retval;
+	}
+
+	return $retval;
+}
+
+/**
+ * Get the absolute upload path for the WP installation.
+ *
+ * @since 1.2.0
+ *
+ * @return string Absolute path to WP upload directory.
+ */
+function bp_core_avatar_upload_path() {
+
+	/**
+	 * Filters the absolute upload path for the WP installation.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $value Absolute upload path for the WP installation.
+	 */
+	return apply_filters( 'bp_core_avatar_upload_path', bp_core_get_upload_dir() );
+}
+
+/**
+ * Get the raw base URL for root site upload location.
+ *
+ * @since 1.2.0
+ *
+ * @return string Full URL to current upload location.
+ */
+function bp_core_avatar_url() {
+
+	/**
+	 * Filters the raw base URL for root site upload location.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $value Raw base URL for the root site upload location.
+	 */
+	return apply_filters( 'bp_core_avatar_url', bp_core_get_upload_dir( 'url' ) );
+}
+
+/**
+ * Check if a given user ID has an uploaded avatar.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user whose avatar is being checked.
+ * @return bool True if the user has uploaded a local avatar. Otherwise false.
+ */
+function bp_get_user_has_avatar( $user_id = 0 ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_displayed_user_id();
+
+	$retval = false;
+	if ( bp_core_fetch_avatar( array( 'item_id' => $user_id, 'no_grav' => true, 'html' => false, 'type' => 'full' ) ) != bp_core_avatar_default( 'local' ) )
+		$retval = true;
+
+	/**
+	 * Filters whether or not a user has an uploaded avatar.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $retval  Whether or not a user has an uploaded avatar.
+	 * @param int  $user_id ID of the user being checked.
+	 */
+	return (bool) apply_filters( 'bp_get_user_has_avatar', $retval, $user_id );
+}
+
+/**
+ * Utility function for fetching an avatar dimension setting.
+ *
+ * @since 1.5.0
+ *
+ * @param string $type   Dimension type you're fetching dimensions for. 'thumb'
+ *                       or 'full'. Default: 'thumb'.
+ * @param string $h_or_w Which dimension is being fetched. 'height' or 'width'.
+ *                       Default: 'height'.
+ * @return int|bool $dim The dimension.
+ */
+function bp_core_avatar_dimension( $type = 'thumb', $h_or_w = 'height' ) {
+	$bp  = buddypress();
+	$dim = isset( $bp->avatar->{$type}->{$h_or_w} ) ? (int) $bp->avatar->{$type}->{$h_or_w} : false;
+
+	/**
+	 * Filters the avatar dimension setting.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|bool $dim    Dimension setting for the type.
+	 * @param string   $type   The type of avatar whose dimensions are requested. Default 'thumb'.
+	 * @param string   $h_or_w The dimension parameter being requested. Default 'height'.
+	 */
+	return apply_filters( 'bp_core_avatar_dimension', $dim, $type, $h_or_w );
+}
+
+/**
+ * Get the 'thumb' avatar width setting.
+ *
+ * @since 1.5.0
+ *
+ * @return int The 'thumb' width.
+ */
+function bp_core_avatar_thumb_width() {
+
+	/**
+	 * Filters the 'thumb' avatar width setting.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Value for the 'thumb' avatar width setting.
+	 */
+	return apply_filters( 'bp_core_avatar_thumb_width', bp_core_avatar_dimension( 'thumb', 'width' ) );
+}
+
+/**
+ * Get the 'thumb' avatar height setting.
+ *
+ * @since 1.5.0
+ *
+ * @return int The 'thumb' height.
+ */
+function bp_core_avatar_thumb_height() {
+
+	/**
+	 * Filters the 'thumb' avatar height setting.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Value for the 'thumb' avatar height setting.
+	 */
+	return apply_filters( 'bp_core_avatar_thumb_height', bp_core_avatar_dimension( 'thumb', 'height' ) );
+}
+
+/**
+ * Get the 'full' avatar width setting.
+ *
+ * @since 1.5.0
+ *
+ * @return int The 'full' width.
+ */
+function bp_core_avatar_full_width() {
+
+	/**
+	 * Filters the 'full' avatar width setting.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Value for the 'full' avatar width setting.
+	 */
+	return apply_filters( 'bp_core_avatar_full_width', bp_core_avatar_dimension( 'full', 'width' ) );
+}
+
+/**
+ * Get the 'full' avatar height setting.
+ *
+ * @since 1.5.0
+ *
+ * @return int The 'full' height.
+ */
+function bp_core_avatar_full_height() {
+
+	/**
+	 * Filters the 'full' avatar height setting.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Value for the 'full' avatar height setting.
+	 */
+	return apply_filters( 'bp_core_avatar_full_height', bp_core_avatar_dimension( 'full', 'height' ) );
+}
+
+/**
+ * Get the max width for original avatar uploads.
+ *
+ * @since 1.5.0
+ *
+ * @return int The max width for original avatar uploads.
+ */
+function bp_core_avatar_original_max_width() {
+
+	/**
+	 * Filters the max width for original avatar uploads.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Value for the max width.
+	 */
+	return apply_filters( 'bp_core_avatar_original_max_width', (int) buddypress()->avatar->original_max_width );
+}
+
+/**
+ * Get the max filesize for original avatar uploads.
+ *
+ * @since 1.5.0
+ *
+ * @return int The max filesize for original avatar uploads.
+ */
+function bp_core_avatar_original_max_filesize() {
+
+	/**
+	 * Filters the max filesize for original avatar uploads.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Value for the max filesize.
+	 */
+	return apply_filters( 'bp_core_avatar_original_max_filesize', (int) buddypress()->avatar->original_max_filesize );
+}
+
+/**
+ * Get the URL of the 'full' default avatar.
+ *
+ * @since 1.5.0
+ * @since 2.6.0 Introduced `$params` and `$object_type` parameters.
+ *
+ * @param string $type   'local' if the fallback should be the locally-hosted version
+ *                       of the mystery person, 'gravatar' if the fallback should be
+ *                       Gravatar's version. Default: 'gravatar'.
+ * @param array  $params Parameters passed to bp_core_fetch_avatar().
+ * @return string The URL of the default avatar.
+ */
+function bp_core_avatar_default( $type = 'gravatar', $params = array() ) {
+	// Local override.
+	if ( defined( 'BP_AVATAR_DEFAULT' ) ) {
+		$avatar = BP_AVATAR_DEFAULT;
+
+	// Use the local default image.
+	} elseif ( 'local' === $type ) {
+		$size = '';
+		if (
+			( isset( $params['type'] ) && 'thumb' === $params['type'] && bp_core_avatar_thumb_width() <= 50 ) ||
+			( isset( $params['width'] ) && $params['width'] <= 50 )
+		) {
+
+			$size = '-50';
+		}
+
+		$avatar = buddypress()->plugin_url . "bp-core/images/mystery-man{$size}.jpg";
+
+	// Use Gravatar's mystery person as fallback.
+	} else {
+		$size = '';
+		if ( isset( $params['type'] ) && 'thumb' === $params['type'] ) {
+			$size = bp_core_avatar_thumb_width();
+		} else {
+			$size = bp_core_avatar_full_width();
+		}
+		$avatar = '//www.gravatar.com/avatar/00000000000000000000000000000000?d=mm&amp;s=' . $size;
+	}
+
+	/**
+	 * Filters the URL of the 'full' default avatar.
+	 *
+	 * @since 1.5.0
+	 * @since 2.6.0 Added `$params`.
+	 *
+	 * @param string $avatar URL of the default avatar.
+	 * @param array  $params Params provided to bp_core_fetch_avatar().
+	 */
+	return apply_filters( 'bp_core_avatar_default', $avatar, $params );
+}
+
+/**
+ * Get the URL of the 'thumb' default avatar.
+ *
+ * Uses Gravatar's mystery-person avatar, unless BP_AVATAR_DEFAULT_THUMB has been
+ * defined.
+ *
+ * @since 1.5.0
+ * @since 2.6.0 Introduced `$object_type` parameter.
+ *
+ * @param string $type   'local' if the fallback should be the locally-hosted version
+ *                       of the mystery person, 'gravatar' if the fallback should be
+ *                       Gravatar's version. Default: 'gravatar'.
+ * @param array  $params Parameters passed to bp_core_fetch_avatar().
+ * @return string The URL of the default avatar thumb.
+ */
+function bp_core_avatar_default_thumb( $type = 'gravatar', $params = array() ) {
+	// Local override.
+	if ( defined( 'BP_AVATAR_DEFAULT_THUMB' ) ) {
+		$avatar = BP_AVATAR_DEFAULT_THUMB;
+
+	// Use the local default image.
+	} elseif ( 'local' === $type ) {
+		$avatar = buddypress()->plugin_url . 'bp-core/images/mystery-man-50.jpg';
+
+	// Use Gravatar's mystery person as fallback.
+	} else {
+		$avatar = '//www.gravatar.com/avatar/00000000000000000000000000000000?d=mm&amp;s=' . bp_core_avatar_thumb_width();
+	}
+
+	/**
+	 * Filters the URL of the 'thumb' default avatar.
+	 *
+	 * @since 1.5.0
+	 * @since 2.6.0 Added `$params`.
+	 *
+	 * @param string $avatar URL of the default avatar.
+	 * @param string $params Params provided to bp_core_fetch_avatar().
+	 */
+	return apply_filters( 'bp_core_avatar_thumb', $avatar, $params );
+}
+
+/**
+ * Reset the week parameter of the WordPress main query if needed.
+ *
+ * When cropping an avatar, a $_POST['w'] var is sent, setting the 'week'
+ * parameter of the WordPress main query to this posted var. To avoid
+ * notices, we need to make sure this 'week' query var is reset to 0.
+ *
+ * @since 2.2.0
+ *
+ * @param WP_Query|null $posts_query The main query object.
+ */
+function bp_core_avatar_reset_query( $posts_query = null ) {
+	$reset_w = false;
+
+	// Group's avatar edit screen.
+	if ( bp_is_group_admin_page() ) {
+		$reset_w = bp_is_group_admin_screen( 'group-avatar' );
+
+	// Group's avatar create screen.
+	} elseif ( bp_is_group_create() ) {
+		/**
+		 * We can't use bp_get_groups_current_create_step().
+		 * as it's not set yet
+		 */
+		$reset_w = 'group-avatar' === bp_action_variable( 1 );
+
+	// User's change avatar screen.
+	} else {
+		$reset_w = bp_is_user_change_avatar();
+	}
+
+	// A user or a group is cropping an avatar.
+	if ( true === $reset_w && isset( $_POST['avatar-crop-submit'] ) ) {
+		$posts_query->set( 'w', 0 );
+	}
+}
+add_action( 'bp_parse_query', 'bp_core_avatar_reset_query', 10, 1 );
+
+/**
+ * Checks whether Avatar UI should be loaded.
+ *
+ * @since 2.3.0
+ *
+ * @return bool True if Avatar UI should load, false otherwise.
+ */
+function bp_avatar_is_front_edit() {
+	$retval = false;
+
+	// No need to carry on if the current WordPress version is not supported.
+	if ( ! bp_attachments_is_wp_version_supported() ) {
+		return $retval;
+	}
+
+	if ( bp_is_user_change_avatar() && 'crop-image' !== bp_get_avatar_admin_step() ) {
+		$retval = ! bp_core_get_root_option( 'bp-disable-avatar-uploads' );
+	}
+
+	if ( bp_is_active( 'groups' ) ) {
+		// Group creation.
+		if ( bp_is_group_create() && bp_is_group_creation_step( 'group-avatar' ) && 'crop-image' !== bp_get_avatar_admin_step() ) {
+			$retval = ! bp_disable_group_avatar_uploads();
+
+		// Group Manage.
+		} elseif ( bp_is_group_admin_page() && bp_is_group_admin_screen( 'group-avatar' ) && 'crop-image' !== bp_get_avatar_admin_step() ) {
+			$retval = ! bp_disable_group_avatar_uploads();
+		}
+	}
+
+	/**
+	 * Use this filter if you need to :
+	 * - Load the avatar UI for a component that is !groups or !user (return true regarding your conditions)
+	 * - Completely disable the avatar UI introduced in 2.3 (eg: __return_false())
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param bool $retval Whether or not to load the Avatar UI.
+	 */
+	return apply_filters( 'bp_avatar_is_front_edit', $retval );
+}
+
+/**
+ * Checks whether the Webcam Avatar UI part should be loaded.
+ *
+ * @since 2.3.0
+ *
+ * @global $is_safari
+ * @global $is_IE
+ *
+ * @return bool True to load the Webcam Avatar UI part. False otherwise.
+ */
+function bp_avatar_use_webcam() {
+	global $is_safari, $is_IE, $is_chrome;
+
+	/**
+	 * Do not use the webcam feature for mobile devices
+	 * to avoid possible confusions.
+	 */
+	if ( wp_is_mobile() ) {
+		return false;
+	}
+
+	/**
+	 * Bail when the browser does not support getUserMedia.
+	 *
+	 * @see http://caniuse.com/#feat=stream
+	 */
+	if ( $is_safari || $is_IE || ( $is_chrome && ! is_ssl() ) ) {
+		return false;
+	}
+
+	/**
+	 * Use this filter if you need to disable the webcam capture feature
+	 * by returning false.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param bool $value Whether or not to load Webcam Avatar UI part.
+	 */
+	return apply_filters( 'bp_avatar_use_webcam', true );
+}
+
+/**
+ * Template function to load the Avatar UI javascript templates.
+ *
+ * @since 2.3.0
+ */
+function bp_avatar_get_templates() {
+	if ( ! bp_avatar_is_front_edit() ) {
+		return;
+	}
+
+	bp_attachments_get_template_part( 'avatars/index' );
+}
+
+/**
+ * Trick to check if the theme's BuddyPress templates are up to date.
+ *
+ * If the "avatar templates" are not including the new template tag, this will
+ * help users to get the avatar UI.
+ *
+ * @since 2.3.0
+ */
+function bp_avatar_template_check() {
+	if ( ! bp_avatar_is_front_edit() ) {
+		return;
+	}
+
+	if ( ! did_action( 'bp_attachments_avatar_check_template' ) ) {
+		bp_attachments_get_template_part( 'avatars/index' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-buddybar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-buddybar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-buddybar.php	(revision 12265)
@@ -0,0 +1,917 @@
+<?php
+/**
+ * Core BuddyPress Navigational Functions.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ *
+ * @todo Deprecate BuddyBar functions.
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add an item to the primary navigation of the specified component.
+ *
+ * @since 1.1.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param array|string $args {
+ *     Array describing the new nav item.
+ *     @type string      $name                    Display name for the nav item.
+ *     @type string      $slug                    Unique URL slug for the nav item.
+ *     @type bool|string $item_css_id             Optional. 'id' attribute for the nav item. Default: the value of `$slug`.
+ *     @type bool        $show_for_displayed_user Optional. Whether the nav item should be visible when viewing a
+ *                                                member profile other than your own. Default: true.
+ *     @type bool        $site_admin_only         Optional. Whether the nav item should be visible only to site admins
+ *                                                (those with the 'bp_moderate' cap). Default: false.
+ *     @type int         $position                Optional. Numerical index specifying where the item should appear in
+ *                                                the nav array. Default: 99.
+ *     @type callable    $screen_function         The callback function that will run when the nav item is clicked.
+ *     @type bool|string $default_subnav_slug     Optional. The slug of the default subnav item to select when the nav
+ *                                                item is clicked.
+ * }
+ * @param string       $component The component the navigation is attached to. Defaults to 'members'.
+ * @return null|false Returns false on failure.
+ */
+function bp_core_new_nav_item( $args, $component = 'members' ) {
+	if ( ! bp_is_active( $component ) ) {
+		return;
+	}
+
+	$defaults = array(
+		'name'                    => false, // Display name for the nav item.
+		'slug'                    => false, // URL slug for the nav item.
+		'item_css_id'             => false, // The CSS ID to apply to the HTML of the nav item.
+		'show_for_displayed_user' => true,  // When viewing another user does this nav item show up?
+		'site_admin_only'         => false, // Can only site admins see this nav item?
+		'position'                => 99,    // Index of where this nav item should be positioned.
+		'screen_function'         => false, // The name of the function to run when clicked.
+		'default_subnav_slug'     => false  // The slug of the default subnav item to select when clicked.
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+
+	// Validate nav link data.
+	$nav_item = bp_core_create_nav_link( $r, $component );
+
+	/*
+	 * To mimic legacy behavior, if bp_core_create_nav_link() returns false, we make
+	 * an early exit and don't attempt to register the screen function.
+	 */
+	if ( false === $nav_item ) {
+		return false;
+	}
+
+	// Then, hook the screen function for the added nav item.
+	$hooked = bp_core_register_nav_screen_function( $nav_item );
+	if ( false === $hooked ){
+		return false;
+	}
+
+	/**
+	 * Fires after adding an item to the main BuddyPress navigation array.
+	 * Note that, when possible, the more specific action hooks
+	 * `bp_core_create_nav_link` or `bp_core_register_nav_screen_function`
+	 * should be used.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $r        Parsed arguments for the nav item.
+	 * @param array $args     Originally passed in arguments for the nav item.
+	 * @param array $defaults Default arguments for a nav item.
+	 */
+	do_action( 'bp_core_new_nav_item', $r, $args, $defaults );
+}
+
+/**
+ * Add a link to the main BuddyPress navigation.
+ *
+ * @since 2.4.0
+ * @since 2.6.0 Introduced the `$component` parameter. Began returning a BP_Core_Nav_Item object on success.
+ *
+ * @param array|string $args {
+ *     Array describing the new nav item.
+ *     @type string      $name                    Display name for the nav item.
+ *     @type string      $slug                    Unique URL slug for the nav item.
+ *     @type bool|string $item_css_id             Optional. 'id' attribute for the nav item. Default: the value of `$slug`.
+ *     @type bool        $show_for_displayed_user Optional. Whether the nav item should be visible when viewing a
+ *                                                member profile other than your own. Default: true.
+ *     @type bool        $site_admin_only         Optional. Whether the nav item should be visible only to site admins
+ *                                                (those with the 'bp_moderate' cap). Default: false.
+ *     @type int         $position                Optional. Numerical index specifying where the item should appear in
+ *                                                the nav array. Default: 99.
+ *     @type callable    $screen_function         The callback function that will run when the nav item is clicked.
+ *     @type bool|string $default_subnav_slug     Optional. The slug of the default subnav item to select when the nav
+ *                                                item is clicked.
+ * }
+ * @param string       $component Optional. Component that the nav belongs to.
+ * @return false|array Returns false on failure, new nav item on success.
+ */
+function bp_core_create_nav_link( $args = '', $component = 'members' ) {
+	$bp = buddypress();
+
+	$defaults = array(
+		'name'                    => false, // Display name for the nav item.
+		'slug'                    => false, // URL slug for the nav item.
+		'item_css_id'             => false, // The CSS ID to apply to the HTML of the nav item.
+		'show_for_displayed_user' => true,  // When viewing another user does this nav item show up?
+		'site_admin_only'         => false, // Can only site admins see this nav item?
+		'position'                => 99,    // Index of where this nav item should be positioned.
+		'screen_function'         => false, // The name of the function to run when clicked.
+		'default_subnav_slug'     => false  // The slug of the default subnav item to select when clicked.
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+
+	// If we don't have the required info we need, don't create this nav item.
+	if ( empty( $r['name'] ) || empty( $r['slug'] ) ) {
+		return false;
+	}
+
+	// If this is for site admins only and the user is not one, don't create the nav item.
+	if ( ! empty( $r['site_admin_only'] ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	if ( empty( $r['item_css_id'] ) ) {
+		$r['item_css_id'] = $r['slug'];
+	}
+
+	$nav_item = array(
+		'name'                    => $r['name'],
+		'slug'                    => $r['slug'],
+		'link'                    => trailingslashit( bp_loggedin_user_domain() . $r['slug'] ),
+		'css_id'                  => $r['item_css_id'],
+		'show_for_displayed_user' => $r['show_for_displayed_user'],
+		'position'                => $r['position'],
+		'screen_function'         => &$r['screen_function'],
+		'default_subnav_slug'	  => $r['default_subnav_slug']
+	);
+
+	// Add the item to the nav.
+	buddypress()->{$component}->nav->add_nav( $nav_item );
+
+	/**
+	 * Fires after a link is added to the main BuddyPress nav.
+	 *
+	 * @since 2.4.0
+	 * @since 2.6.0 Added `$component` parameter.
+	 *
+	 * @param array  $r         Parsed arguments for the nav item.
+	 * @param array  $args      Originally passed in arguments for the nav item.
+	 * @param array  $defaults  Default arguments for a nav item.
+	 * @param string $component Component that the nav belongs to.
+	 */
+	do_action( 'bp_core_create_nav_link', $r, $args, $defaults, $component );
+
+	return $nav_item;
+}
+
+/**
+ * Register a screen function for an item in the main nav array.
+ *
+ * @since 2.4.0
+ *
+ * @param array|string $args {
+ *     Array describing the new nav item.
+ *     @type string      $name                    Display name for the nav item.
+ *     @type string      $slug                    Unique URL slug for the nav item.
+ *     @type bool|string $item_css_id             Optional. 'id' attribute for the nav item. Default: the value of `$slug`.
+ *     @type bool        $show_for_displayed_user Optional. Whether the nav item should be visible when viewing a
+ *                                                member profile other than your own. Default: true.
+ *     @type bool        $site_admin_only         Optional. Whether the nav item should be visible only to site admins
+ *                                                (those with the 'bp_moderate' cap). Default: false.
+ *     @type int         $position                Optional. Numerical index specifying where the item should appear in
+ *                                                the nav array. Default: 99.
+ *     @type callable    $screen_function         The callback function that will run when the nav item is clicked.
+ *     @type bool|string $default_subnav_slug     Optional. The slug of the default subnav item to select when the nav
+ *                                                item is clicked.
+ * }
+ * @return false|null Returns false on failure.
+ */
+function bp_core_register_nav_screen_function( $args = '' ) {
+	$bp = buddypress();
+
+	$defaults = array(
+		'name'                    => false, // Display name for the nav item.
+		'slug'                    => false, // URL slug for the nav item.
+		'item_css_id'             => false, // The CSS ID to apply to the HTML of the nav item.
+		'show_for_displayed_user' => true,  // When viewing another user does this nav item show up?
+		'site_admin_only'         => false, // Can only site admins see this nav item?
+		'position'                => 99,    // Index of where this nav item should be positioned.
+		'screen_function'         => false, // The name of the function to run when clicked.
+		'default_subnav_slug'     => false  // The slug of the default subnav item to select when clicked.
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+
+	// If we don't have the required info we need, don't register this screen function.
+	if ( empty( $r['slug'] ) ) {
+		return false;
+	}
+
+	/**
+	 * If this is for site admins only and the user is not one,
+	 * don't register this screen function.
+	 */
+	if ( ! empty( $r['site_admin_only'] ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	/**
+	 * If this nav item is hidden for the displayed user, and
+	 * the logged in user is not the displayed user
+	 * looking at their own profile, don't don't register this screen function.
+	 */
+	if ( empty( $r['show_for_displayed_user'] ) && ! bp_user_has_access() ) {
+		return false;
+	}
+
+	/**
+	 * If the nav item is visible, we are not viewing a user, and this is a root
+	 * component, don't attach the default subnav function so we can display a
+	 * directory or something else.
+	 */
+	if ( ( -1 != $r['position'] ) && bp_is_root_component( $r['slug'] ) && ! bp_displayed_user_id() ) {
+		return;
+	}
+
+	// Look for current component.
+	if ( bp_is_current_component( $r['slug'] ) || bp_is_current_item( $r['slug'] ) ) {
+
+		// The requested URL has explicitly included the default subnav
+		// (eg: http://example.com/members/membername/activity/just-me/)
+		// The canonical version will not contain this subnav slug.
+		if ( ! empty( $r['default_subnav_slug'] ) && bp_is_current_action( $r['default_subnav_slug'] ) && ! bp_action_variable( 0 ) ) {
+			unset( $bp->canonical_stack['action'] );
+		} elseif ( ! bp_current_action() ) {
+
+			// Add our screen hook if screen function is callable.
+			if ( is_callable( $r['screen_function'] ) ) {
+				add_action( 'bp_screens', $r['screen_function'], 3 );
+			}
+
+			if ( ! empty( $r['default_subnav_slug'] ) ) {
+
+				/**
+				 * Filters the default component subnav item.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string $value The slug of the default subnav item
+				 *                      to select when clicked.
+				 * @param array  $r     Parsed arguments for the nav item.
+				 */
+				$bp->current_action = apply_filters( 'bp_default_component_subnav', $r['default_subnav_slug'], $r );
+			}
+		}
+	}
+
+	/**
+	 * Fires after the screen function for an item in the BuddyPress main
+	 * navigation is registered.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $r        Parsed arguments for the nav item.
+	 * @param array $args     Originally passed in arguments for the nav item.
+	 * @param array $defaults Default arguments for a nav item.
+	 */
+	do_action( 'bp_core_register_nav_screen_function', $r, $args, $defaults );
+}
+
+/**
+ * Modify the default subnav item that loads when a top level nav item is clicked.
+ *
+ * @since 1.1.0
+ *
+ * @param array|string $args {
+ *     @type string   $parent_slug     The slug of the nav item whose default is being changed.
+ *     @type callable $screen_function The new default callback function that will run when the nav item is clicked.
+ *     @type string   $subnav_slug     The slug of the new default subnav item.
+ * }
+ */
+function bp_core_new_nav_default( $args = '' ) {
+	$bp = buddypress();
+
+	$defaults = array(
+		'parent_slug'     => false, // Slug of the parent.
+		'screen_function' => false, // The name of the function to run when clicked.
+		'subnav_slug'     => false  // The slug of the subnav item to select when clicked.
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+
+	// This is specific to Members - it's not available in Groups.
+	$parent_nav = $bp->members->nav->get_primary( array( 'slug' => $r['parent_slug'] ), false );
+
+	if ( ! $parent_nav ) {
+		return ;
+	}
+
+	$parent_nav = reset( $parent_nav );
+
+	if ( ! empty( $parent_nav->screen_function ) ) {
+		// Remove our screen hook if screen function is callable.
+		if ( is_callable( $parent_nav->screen_function ) ) {
+			remove_action( 'bp_screens', $parent_nav->screen_function, 3 );
+		}
+	}
+
+	// Edit the screen function for the parent nav.
+	$bp->members->nav->edit_nav( array(
+		'screen_function'     => &$r['screen_function'],
+		'default_subnav_slug' => $r['subnav_slug'],
+	), $parent_nav->slug );
+
+	if ( bp_is_current_component( $parent_nav->slug ) ) {
+
+		// The only way to tell whether to set the subnav is to peek at the unfiltered_uri
+		// Find the component.
+		$component_uri_key = array_search( $parent_nav->slug, $bp->unfiltered_uri );
+
+		if ( false !== $component_uri_key ) {
+			if ( ! empty( $bp->unfiltered_uri[$component_uri_key + 1] ) ) {
+				$unfiltered_action = $bp->unfiltered_uri[$component_uri_key + 1];
+			}
+		}
+
+		// No subnav item has been requested in the URL, so set a new nav default.
+		if ( empty( $unfiltered_action ) ) {
+			if ( ! bp_is_current_action( $r['subnav_slug'] ) ) {
+				if ( is_callable( $r['screen_function'] ) ) {
+					add_action( 'bp_screens', $r['screen_function'], 3 );
+				}
+
+				$bp->current_action = $r['subnav_slug'];
+				unset( $bp->canonical_stack['action'] );
+			}
+
+		// The URL is explicitly requesting the new subnav item, but should be
+		// directed to the canonical URL.
+		} elseif ( $unfiltered_action == $r['subnav_slug'] ) {
+			unset( $bp->canonical_stack['action'] );
+
+		// In all other cases (including the case where the original subnav item
+		// is explicitly called in the URL), the canonical URL will contain the
+		// subnav slug.
+		} else {
+			$bp->canonical_stack['action'] = bp_current_action();
+		}
+	}
+
+	return;
+}
+
+/**
+ * Add an item to secondary navigation of the specified component.
+ *
+ * @since 1.1.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param array|string $args {
+ *     Array describing the new subnav item.
+ *     @type string      $name              Display name for the subnav item.
+ *     @type string      $slug              Unique URL slug for the subnav item.
+ *     @type string      $parent_slug       Slug of the top-level nav item under which the new subnav item should
+ *                                          be added.
+ *     @type string      $parent_url        URL of the parent nav item.
+ *     @type bool|string $item_css_id       Optional. 'id' attribute for the nav item. Default: the value of `$slug`.
+ *     @type bool        $user_has_access   Optional. True if the logged-in user has access to the subnav item,
+ *                                          otherwise false. Can be set dynamically when registering the subnav;
+ *                                          eg, use `bp_is_my_profile()` to restrict access to profile owners only.
+ *                                          Default: true.
+ *     @type bool        $site_admin_only   Optional. Whether the nav item should be visible only to site admins
+ *                                          (those with the 'bp_moderate' cap). Default: false.
+ *     @type int         $position          Optional. Numerical index specifying where the item should appear in the
+ *                                          subnav array. Default: 90.
+ *     @type callable    $screen_function   The callback function that will run when the nav item is clicked.
+ *     @type string      $link              Optional. The URL that the subnav item should point to. Defaults to a value
+ *                                          generated from the `$parent_url` + `$slug`.
+ *     @type bool        $show_in_admin_bar Optional. Whether the nav item should be added into the group's "Edit"
+ *                                          Admin Bar menu for group admins. Default: false.
+ * }
+ * @param string|null    $component The component the navigation is attached to. Defaults to 'members'.
+ * @return null|false Returns false on failure.
+ */
+function bp_core_new_subnav_item( $args, $component = null ) {
+	// Backward compatibility for plugins using `bp_core_new_subnav_item()` without `$component`
+	// to add group subnav items.
+	if ( null === $component && bp_is_active( 'groups' ) && bp_is_group() && isset( $args['parent_slug'] ) ) {
+		/*
+		 * Assume that this item is intended to belong to the current group if:
+		 * a) the 'parent_slug' is the same as the slug of the current group, or
+		 * b) the 'parent_slug' starts with the slug of the current group, and the members nav doesn't have
+		 *    a primary item with that slug.
+		 */
+		$group_slug = bp_get_current_group_slug();
+		if (
+			$group_slug === $args['parent_slug'] ||
+			( 0 === strpos( $args['parent_slug'], $group_slug ) && ! buddypress()->members->nav->get_primary( array( 'slug' => $args['parent_slug'] ), false ) )
+		) {
+			$component = 'groups';
+		}
+	}
+
+	if ( ! $component ) {
+		$component = 'members';
+	}
+
+	if ( ! bp_is_active( $component ) ) {
+		return;
+	}
+
+	// First, register the subnav item in the nav.
+	$subnav_item = bp_core_create_subnav_link( $args, $component );
+
+	/*
+	 * To mimic legacy behavior, if bp_core_create_subnav_link() returns false, we make an
+	 * early exit and don't attempt to register the screen function.
+	 */
+	if ( false === $subnav_item ) {
+		return false;
+	}
+
+	// Then, hook the screen function for the added subnav item.
+	$hooked = bp_core_register_subnav_screen_function( $subnav_item, $component );
+	if ( false === $hooked ) {
+		return false;
+	}
+}
+
+/**
+ * Add a subnav link to the BuddyPress navigation.
+ *
+ * @since 2.4.0
+ * @since 2.6.0 Introduced the `$component` parameter. Began returning a BP_Core_Nav_Item object on success.
+ *
+ * @param array|string $args {
+ *     Array describing the new subnav item.
+ *     @type string      $name              Display name for the subnav item.
+ *     @type string      $slug              Unique URL slug for the subnav item.
+ *     @type string      $parent_slug       Slug of the top-level nav item under which the
+ *                                          new subnav item should be added.
+ *     @type string      $parent_url        URL of the parent nav item.
+ *     @type bool|string $item_css_id       Optional. 'id' attribute for the nav
+ *                                          item. Default: the value of $slug.
+ *     @type bool        $user_has_access   Optional. True if the logged-in user has access to the
+ *                                          subnav item, otherwise false. Can be set dynamically
+ *                                          when registering the subnav; eg, use bp_is_my_profile()
+ *                                          to restrict access to profile owners only. Default: true.
+ *     @type bool        $site_admin_only   Optional. Whether the nav item should be visible only
+ *                                          to site admins (those with the 'bp_moderate' cap).
+ *                                          Default: false.
+ *     @type int         $position          Optional. Numerical index specifying where the item
+ *                                          should appear in the subnav array. Default: 90.
+ *     @type callable    $screen_function   The callback function that will run
+ *                                          when the nav item is clicked.
+ *     @type string      $link              Optional. The URL that the subnav item should point
+ *                                          to. Defaults to a value generated from the $parent_url + $slug.
+ *     @type bool        $show_in_admin_bar Optional. Whether the nav item should be added into
+ *                                          the group's "Edit" Admin Bar menu for group admins.
+ *                                          Default: false.
+ * }
+ * @param string       $component The component the navigation is attached to. Defaults to 'members'.
+ * @return false|array Returns false on failure, new BP_Core_Nav_Item instance on success.
+ */
+function bp_core_create_subnav_link( $args = '', $component = 'members' ) {
+	$bp = buddypress();
+
+	$r = wp_parse_args( $args, array(
+		'name'              => false, // Display name for the nav item.
+		'slug'              => false, // URL slug for the nav item.
+		'parent_slug'       => false, // URL slug of the parent nav item.
+		'parent_url'        => false, // URL of the parent item.
+		'item_css_id'       => false, // The CSS ID to apply to the HTML of the nav item.
+		'user_has_access'   => true,  // Can the logged in user see this nav item?
+		'no_access_url'     => '',
+		'site_admin_only'   => false, // Can only site admins see this nav item?
+		'position'          => 90,    // Index of where this nav item should be positioned.
+		'screen_function'   => false, // The name of the function to run when clicked.
+		'link'              => '',    // The link for the subnav item; optional, not usually required.
+		'show_in_admin_bar' => false, // Show the Manage link in the current group's "Edit" Admin Bar menu.
+	) );
+
+	// If we don't have the required info we need, don't create this subnav item.
+	if ( empty( $r['name'] ) || empty( $r['slug'] ) || empty( $r['parent_slug'] ) || empty( $r['parent_url'] ) || empty( $r['screen_function'] ) )
+		return false;
+
+	// Link was not forced, so create one.
+	if ( empty( $r['link'] ) ) {
+		$r['link'] = trailingslashit( $r['parent_url'] . $r['slug'] );
+
+		$parent_nav = $bp->{$component}->nav->get_primary( array( 'slug' => $r['parent_slug'] ), false );
+
+		// If this sub item is the default for its parent, skip the slug.
+		if ( $parent_nav ) {
+			$parent_nav_item = reset( $parent_nav );
+			if ( ! empty( $parent_nav_item->default_subnav_slug ) && $r['slug'] === $parent_nav_item->default_subnav_slug ) {
+				$r['link'] = trailingslashit( $r['parent_url'] );
+			}
+		}
+	}
+
+	// If this is for site admins only and the user is not one, don't create the subnav item.
+	if ( ! empty( $r['site_admin_only'] ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	if ( empty( $r['item_css_id'] ) ) {
+		$r['item_css_id'] = $r['slug'];
+	}
+
+	$subnav_item = array(
+		'name'              => $r['name'],
+		'link'              => $r['link'],
+		'slug'              => $r['slug'],
+		'parent_slug'       => $r['parent_slug'],
+		'css_id'            => $r['item_css_id'],
+		'position'          => $r['position'],
+		'user_has_access'   => $r['user_has_access'],
+		'no_access_url'     => $r['no_access_url'],
+		'screen_function'   => &$r['screen_function'],
+		'show_in_admin_bar' => (bool) $r['show_in_admin_bar'],
+	);
+
+	buddypress()->{$component}->nav->add_nav( $subnav_item );
+
+	return $subnav_item;
+}
+
+/**
+ * Register a screen function, whether or not a related subnav link exists.
+ *
+ * @since 2.4.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param array|string $args {
+ *     Array describing the new subnav item.
+ *     @type string   $slug              Unique URL slug for the subnav item.
+ *     @type string   $parent_slug       Slug of the top-level nav item under which the
+ *                                       new subnav item should be added.
+ *     @type string   $parent_url        URL of the parent nav item.
+ *     @type bool     $user_has_access   Optional. True if the logged-in user has access to the
+ *                                       subnav item, otherwise false. Can be set dynamically
+ *                                       when registering the subnav; eg, use bp_is_my_profile()
+ *                                       to restrict access to profile owners only. Default: true.
+ *     @type bool     $site_admin_only   Optional. Whether the nav item should be visible
+ *                                       only to site admins (those with the 'bp_moderate' cap).
+ *                                       Default: false.
+ *     @type int      $position          Optional. Numerical index specifying where the item
+ *                                       should appear in the subnav array. Default: 90.
+ *     @type callable $screen_function   The callback function that will run
+ *                                       when the nav item is clicked.
+ *     @type string   $link              Optional. The URL that the subnav item should point to.
+ *                                       Defaults to a value generated from the $parent_url + $slug.
+ *     @type bool     $show_in_admin_bar Optional. Whether the nav item should be added into
+ *                                       the group's "Edit" Admin Bar menu for group admins.
+ *                                       Default: false.
+ * }
+ * @param string       $component The component the navigation is attached to. Defaults to 'members'.
+ * @return null|false Returns false on failure.
+ */
+function bp_core_register_subnav_screen_function( $args = '', $component = 'members' ) {
+	$bp = buddypress();
+
+	$r = wp_parse_args( $args, array(
+		'slug'              => false, // URL slug for the screen.
+		'parent_slug'       => false, // URL slug of the parent screen.
+		'user_has_access'   => true,  // Can the user visit this screen?
+		'no_access_url'     => '',
+		'site_admin_only'   => false, // Can only site admins visit this screen?
+		'screen_function'   => false, // The name of the function to run when clicked.
+	) );
+
+	/*
+	 * Hook the screen function for the added subnav item. But this only needs to
+	 * be done if this subnav item is the current view, and the user has access to the
+	 * subnav item. We figure out whether we're currently viewing this subnav by
+	 * checking the following two conditions:
+	 *   (1) Either:
+	 *       (a) the parent slug matches the current_component, or
+	 *       (b) the parent slug matches the current_item
+	 *   (2) And either:
+	 *       (a) the current_action matches $slug, or
+	 *       (b) there is no current_action (ie, this is the default subnav for the parent nav)
+	 *       and this subnav item is the default for the parent item (which we check by
+	 *       comparing this subnav item's screen function with the screen function of the
+	 *       parent nav item in the component's primary nav). This condition only arises
+	 *       when viewing a user, since groups should always have an action set.
+	 */
+
+	// If we *don't* meet condition (1), return.
+	if ( ! bp_is_current_component( $r['parent_slug'] ) && ! bp_is_current_item( $r['parent_slug'] ) ) {
+		return;
+	}
+
+	$parent_nav = $bp->{$component}->nav->get_primary( array( 'slug' => $r['parent_slug'] ), false );
+	if ( is_array( $parent_nav ) ) {
+		$parent_nav = reset( $parent_nav );
+	}
+
+	// If we *do* meet condition (2), then the added subnav item is currently being requested.
+	if ( ( bp_current_action() && bp_is_current_action( $r['slug'] ) ) || ( bp_is_user() && ! bp_current_action() && ! empty( $parent_nav->screen_function ) && $r['screen_function'] == $parent_nav->screen_function ) ) {
+
+		// If this is for site admins only and the user is not one, don't create the subnav item.
+		if ( ! empty( $r['site_admin_only'] ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+			return false;
+		}
+
+		$hooked = bp_core_maybe_hook_new_subnav_screen_function( $r, $component );
+
+		// If redirect args have been returned, perform the redirect now.
+		if ( ! empty( $hooked['status'] ) && 'failure' === $hooked['status'] && isset( $hooked['redirect_args'] ) ) {
+			bp_core_no_access( $hooked['redirect_args'] );
+		}
+	}
+}
+
+/**
+ * For a given subnav item, either hook the screen function or generate redirect arguments, as necessary.
+ *
+ * @since 2.1.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param array  $subnav_item The subnav array added to the secondary navigation of
+ *                            the component in bp_core_new_subnav_item().
+ * @param string $component   The component the navigation is attached to. Defaults to 'members'.
+ * @return array
+ */
+function bp_core_maybe_hook_new_subnav_screen_function( $subnav_item, $component = 'members' ) {
+	$retval = array(
+		'status' => '',
+	);
+
+	// Is this accessible by site admins only?
+	$site_admin_restricted = false;
+	if ( ! empty( $subnav_item['site_admin_only'] ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+		$site_admin_restricted = true;
+	}
+
+	// User has access, so let's try to hook the display callback.
+	if ( ! empty( $subnav_item['user_has_access'] ) && ! $site_admin_restricted ) {
+
+		// Screen function is invalid.
+		if ( ! is_callable( $subnav_item['screen_function'] ) ) {
+			$retval['status'] = 'failure';
+
+		// Success - hook to bp_screens.
+		} else {
+			add_action( 'bp_screens', $subnav_item['screen_function'], 3 );
+			$retval['status'] = 'success';
+		}
+
+	// User doesn't have access. Determine redirect arguments based on
+	// user status.
+	} else {
+		$retval['status'] = 'failure';
+
+		if ( is_user_logged_in() ) {
+
+			$bp = buddypress();
+
+			// If a redirect URL has been passed to the subnav item, respect it.
+			if ( ! empty( $subnav_item['no_access_url'] ) ) {
+				$message     = __( 'You do not have access to that page.', 'buddypress' );
+				$redirect_to = trailingslashit( $subnav_item['no_access_url'] );
+
+			// In the case of a user page, we try to assume a
+			// redirect URL.
+			} elseif ( bp_is_user() ) {
+
+				$parent_nav_default = $bp->{$component}->nav->get_primary( array( 'slug' => $bp->default_component ), false );
+				if ( $parent_nav_default ) {
+					$parent_nav_default_item = reset( $parent_nav_default );
+				}
+
+				// Redirect to the displayed user's default
+				// component, as long as that component is
+				// publicly accessible.
+				if ( bp_is_my_profile() || ( isset( $parent_nav_default_item ) && $parent_nav_default_item->show_for_displayed_user ) ) {
+					$message     = __( 'You do not have access to that page.', 'buddypress' );
+					$redirect_to = bp_displayed_user_domain();
+
+				// In some cases, the default tab is not accessible to
+				// the logged-in user. So we fall back on a tab that we
+				// know will be accessible.
+				} else {
+					// Try 'activity' first.
+					if ( bp_is_active( 'activity' ) && isset( $bp->pages->activity ) ) {
+						$redirect_to = trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() );
+					// Then try 'profile'.
+					} else {
+						$redirect_to = trailingslashit( bp_displayed_user_domain() . ( 'xprofile' == $bp->profile->id ? 'profile' : $bp->profile->id ) );
+					}
+
+					$message     = '';
+				}
+
+			// Fall back to the home page.
+			} else {
+				$message     = __( 'You do not have access to this page.', 'buddypress' );
+				$redirect_to = bp_get_root_domain();
+			}
+
+			$retval['redirect_args'] = array(
+				'message'  => $message,
+				'root'     => $redirect_to,
+				'redirect' => false,
+				'mode'     => 1
+			);
+
+		} else {
+			// When the user is logged out, pass an empty array
+			// This indicates that the default arguments should be
+			// used in bp_core_no_access().
+			$retval['redirect_args'] = array();
+		}
+	}
+
+	return $retval;
+}
+
+/**
+ * Check whether a given nav item has subnav items.
+ *
+ * @since 1.5.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param string $nav_item  The slug of the top-level nav item whose subnav items you're checking.
+ *                          Default: the current component slug.
+ * @param string $component The component the navigation is attached to. Defaults to 'members'.
+ * @return bool $has_subnav True if the nav item is found and has subnav items; false otherwise.
+ */
+function bp_nav_item_has_subnav( $nav_item = '', $component = 'members' ) {
+	$bp = buddypress();
+
+	if ( ! isset( $bp->{$component}->nav ) ) {
+		return false;
+	}
+
+	if ( ! $nav_item ) {
+		$nav_item = bp_current_component();
+
+		if ( bp_is_group() ) {
+			$nav_item = bp_current_item();
+		}
+	}
+
+	$has_subnav = (bool) $bp->{$component}->nav->get_secondary( array( 'parent_slug' => $nav_item ), false );
+
+	/**
+	 * Filters whether or not a given nav item has subnav items.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $has_subnav Whether or not there is any subnav items.
+	 * @param string $nav_item   The slug of the top-level nav item whose subnav items you're checking.
+	 */
+	return apply_filters( 'bp_nav_item_has_subnav', $has_subnav, $nav_item );
+}
+
+/**
+ * Deletes an item from the primary navigation of the specified component.
+ *
+ * @since 1.0.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param string      $slug      The slug of the primary navigation item.
+ * @param string|null $component The component the navigation is attached to. Defaults to 'members'.
+ * @return bool Returns false on failure, True on success.
+ */
+function bp_core_remove_nav_item( $slug, $component = null ) {
+	$bp = buddypress();
+
+	// Backward compatibility for removing group nav items using the group slug as `$parent_slug`.
+	if ( ! $component && bp_is_active( 'groups' ) && isset( $bp->groups->nav ) ) {
+		if ( $bp->groups->nav->get_primary( array( 'slug' => $slug ) ) ) {
+			$component = 'groups';
+		}
+	}
+
+	if ( ! $component ) {
+		$component = 'members';
+	}
+
+	if ( ! isset( $bp->{$component}->nav ) ) {
+		return false;
+	}
+
+	$screen_functions = $bp->{$component}->nav->delete_nav( $slug );
+
+	// Reset backcompat nav items so that subsequent references will be correct.
+	$bp->bp_nav->reset();
+	$bp->bp_options_nav->reset();
+
+	if ( ! is_array( $screen_functions ) ) {
+		return false;
+	}
+
+	foreach ( $screen_functions as $screen_function ) {
+		// Remove our screen hook if screen function is callable.
+		if ( is_callable( $screen_function ) ) {
+			remove_action( 'bp_screens', $screen_function, 3 );
+		}
+	}
+
+	return true;
+}
+
+/**
+ * Deletes an item from the secondary navigation of the specified component.
+ *
+ * @since 1.0.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param string      $parent_slug The slug of the primary navigation item.
+ * @param string      $slug        The slug of the secondary item to be removed.
+ * @param string|null $component   The component the navigation is attached to. Defaults to 'members'.
+ * @return bool Returns false on failure, True on success.
+ */
+function bp_core_remove_subnav_item( $parent_slug, $slug, $component = null ) {
+	$bp = buddypress();
+
+	// Backward compatibility for removing group nav items using the group slug as `$parent_slug`.
+	if ( ! $component && bp_is_active( 'groups' ) && isset( $bp->groups->nav ) ) {
+		if ( $bp->groups->nav->get_primary( array( 'slug' => $parent_slug ) ) ) {
+			$component = 'groups';
+		}
+	}
+
+	if ( ! $component ) {
+		$component = 'members';
+	}
+
+	if ( ! isset( $bp->{$component}->nav ) ) {
+		return false;
+	}
+
+	$screen_functions = $bp->{$component}->nav->delete_nav( $slug, $parent_slug );
+
+	// Reset backcompat nav items so that subsequent references will be correct.
+	$bp->bp_nav->reset();
+	$bp->bp_options_nav->reset();
+
+	if ( ! is_array( $screen_functions ) ) {
+		return false;
+	}
+
+	$screen_function = reset( $screen_functions );
+
+	// Remove our screen hook if screen function is callable.
+	if ( is_callable( $screen_function ) ) {
+		remove_action( 'bp_screens', $screen_function, 3 );
+	}
+
+	return true;
+}
+
+/**
+ * Clear all subnav items from a specific nav item.
+ *
+ * @since 1.0.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param string $parent_slug The slug of the parent navigation item.
+ * @param string $component   The component the navigation is attached to. Defaults to 'members'.
+ */
+function bp_core_reset_subnav_items( $parent_slug, $component = 'members' ) {
+	$bp = buddypress();
+
+	if ( ! isset( $bp->{$component}->nav ) ) {
+		return;
+	}
+
+	$subnav_items = $bp->{$component}->nav->get_secondary( array( 'parent_slug' => $parent_slug ), false );
+
+	if ( ! $subnav_items ) {
+		return;
+	}
+
+	foreach( $subnav_items as $subnav_item ) {
+		$bp->{$component}->nav->delete_nav( $subnav_item->slug, $parent_slug );
+	}
+}
+
+
+/**
+ * Retrieve the Toolbar display preference of a user based on context.
+ *
+ * This is a direct copy of WP's private _get_admin_bar_pref()
+ *
+ * @since 1.5.0
+ *
+ * @param string $context Context of this preference check. 'admin' or 'front'.
+ * @param int    $user    Optional. ID of the user to check. Default: 0 (which falls back to the logged-in user's ID).
+ * @return bool True if the toolbar should be showing for this user.
+ */
+function bp_get_admin_bar_pref( $context, $user = 0 ) {
+	$pref = get_user_option( "show_admin_bar_{$context}", $user );
+	if ( false === $pref )
+		return true;
+
+	return 'true' === $pref;
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-cache.php	(revision 12265)
@@ -0,0 +1,381 @@
+<?php
+/**
+ * BuddyPress Core Caching Functions.
+ *
+ * Caching functions handle the clearing of cached objects and pages on specific
+ * actions throughout BuddyPress.
+ *
+ * @package BuddyPress
+ * @supackage Cache
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Prune the WP Super Cache.
+ *
+ * When WP Super Cache is installed, this function will clear cached pages
+ * so that success/error messages or time-sensitive content are not cached.
+ *
+ * @since 1.0.0
+ *
+ * @see prune_super_cache()
+ *
+ * @return int
+ */
+function bp_core_clear_cache() {
+	global $cache_path;
+
+	if ( function_exists( 'prune_super_cache' ) ) {
+
+		/**
+		 * Fires before the pruning of WP Super Cache.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'bp_core_clear_cache' );
+		return prune_super_cache( $cache_path, true );
+	}
+}
+
+/**
+ * Clear all cached objects for a user, or those that a user is part of.
+ *
+ * @since 1.0.0
+ *
+ * @param string $user_id User ID to delete cache for.
+ */
+function bp_core_clear_user_object_cache( $user_id ) {
+	wp_cache_delete( 'bp_user_' . $user_id, 'bp' );
+}
+
+/**
+ * Clear member count caches and transients.
+ *
+ * @since 1.6.0
+ */
+function bp_core_clear_member_count_caches() {
+	wp_cache_delete( 'bp_total_member_count', 'bp' );
+	delete_transient( 'bp_active_member_count' );
+}
+add_action( 'bp_core_activated_user',         'bp_core_clear_member_count_caches' );
+add_action( 'bp_core_process_spammer_status', 'bp_core_clear_member_count_caches' );
+add_action( 'bp_core_deleted_account',        'bp_core_clear_member_count_caches' );
+add_action( 'bp_first_activity_for_member',   'bp_core_clear_member_count_caches' );
+add_action( 'deleted_user',                   'bp_core_clear_member_count_caches' );
+
+/**
+ * Clear the directory_pages cache when one of the pages is updated.
+ *
+ * @since 2.0.0
+ *
+ * @param int $post_id ID of the page that was saved.
+ */
+function bp_core_clear_directory_pages_cache_page_edit( $post_id = 0 ) {
+
+	// Bail if BP is not defined here.
+	if ( ! buddypress() ) {
+		return;
+	}
+
+	// Bail if not on the root blog
+	if ( ! bp_is_root_blog() ) {
+		return;
+	}
+
+	$page_ids = bp_core_get_directory_page_ids( 'all' );
+
+	// Bail if post ID is not a directory page
+	if ( ! in_array( $post_id, $page_ids ) ) {
+		return;
+	}
+
+	wp_cache_delete( 'directory_pages', 'bp_pages' );
+}
+add_action( 'save_post', 'bp_core_clear_directory_pages_cache_page_edit' );
+
+/**
+ * Clear the directory_pages cache when the bp-pages option is updated.
+ *
+ * @since 2.0.0
+ *
+ * @param string $option Option name.
+ */
+function bp_core_clear_directory_pages_cache_settings_edit( $option ) {
+	if ( 'bp-pages' === $option ) {
+		wp_cache_delete( 'directory_pages', 'bp_pages' );
+	}
+}
+add_action( 'update_option', 'bp_core_clear_directory_pages_cache_settings_edit' );
+
+/**
+ * Clear the root_blog_options cache when any of its options are updated.
+ *
+ * @since 2.0.0
+ *
+ * @param string $option Option name.
+ */
+function bp_core_clear_root_options_cache( $option ) {
+	foreach ( array( 'add_option', 'add_site_option', 'update_option', 'update_site_option' ) as $action ) {
+		remove_action( $action, 'bp_core_clear_root_options_cache' );
+	}
+
+	// Surrounding code prevents infinite loops on WP < 4.4.
+	$keys = array_keys( bp_get_default_options() );
+
+	foreach ( array( 'add_option', 'add_site_option', 'update_option', 'update_site_option' ) as $action ) {
+		add_action( $action, 'bp_core_clear_root_options_cache' );
+	}
+
+	$keys = array_merge( $keys, array(
+		'registration',
+		'avatar_default',
+		'tags_blog_id',
+		'sitewide_tags_blog',
+		'registration',
+		'fileupload_mask',
+	) );
+
+	if ( in_array( $option, $keys ) ) {
+		wp_cache_delete( 'root_blog_options', 'bp' );
+	}
+}
+add_action( 'update_option', 'bp_core_clear_root_options_cache' );
+add_action( 'update_site_option', 'bp_core_clear_root_options_cache' );
+add_action( 'add_option', 'bp_core_clear_root_options_cache' );
+add_action( 'add_site_option', 'bp_core_clear_root_options_cache' );
+
+/**
+ * Determine which items from a list do not have cached values.
+ *
+ * @since 2.0.0
+ *
+ * @param array  $item_ids    ID list.
+ * @param string $cache_group The cache group to check against.
+ * @return array
+ */
+function bp_get_non_cached_ids( $item_ids, $cache_group ) {
+	$uncached = array();
+
+	foreach ( $item_ids as $item_id ) {
+		$item_id = (int) $item_id;
+		if ( false === wp_cache_get( $item_id, $cache_group ) ) {
+			$uncached[] = $item_id;
+		}
+	}
+
+	return $uncached;
+}
+
+/**
+ * Update the metadata cache for the specified objects.
+ *
+ * Based on WordPress's {@link update_meta_cache()}, this function primes the
+ * cache with metadata related to a set of objects. This is typically done when
+ * querying for a loop of objects; pre-fetching metadata for each queried
+ * object can lead to dramatic performance improvements when using metadata
+ * in the context of template loops.
+ *
+ * @since 1.6.0
+ *
+ * @global object $wpdb WordPress database object for queries..
+ *
+ * @param array $args {
+ *     Array of arguments.
+ *     @type array|string $object_ids       List of object IDs to fetch metadata for.
+ *                                          Accepts an array or a comma-separated list of numeric IDs.
+ *     @type string       $object_type      The type of object, eg 'groups' or 'activity'.
+ *     @type string       $meta_table       The name of the metadata table being queried.
+ *     @type string       $object_column    Optional. The name of the database column where IDs
+ *                                          (those provided by $object_ids) are found. Eg, 'group_id'
+ *                                          for the groups metadata tables. Default: $object_type . '_id'.
+ *     @type string       $cache_key_prefix Optional. The prefix to use when creating
+ *                                          cache key names. Default: the value of $meta_table.
+ * }
+ * @return false|array Metadata cache for the specified objects, or false on failure.
+ */
+function bp_update_meta_cache( $args = array() ) {
+	global $wpdb;
+
+	$defaults = array(
+		'object_ids' 	   => array(), // Comma-separated list or array of item ids.
+		'object_type' 	   => '',      // Canonical component id: groups, members, etc.
+		'cache_group'      => '',      // Cache group.
+		'meta_table' 	   => '',      // Name of the table containing the metadata.
+		'object_column'    => '',      // DB column for the object ids (group_id, etc).
+		'cache_key_prefix' => ''       // Prefix to use when creating cache key names. Eg 'bp_groups_groupmeta'.
+	);
+	$r = wp_parse_args( $args, $defaults );
+	extract( $r );
+
+	if ( empty( $object_ids ) || empty( $object_type ) || empty( $meta_table ) || empty( $cache_group ) ) {
+		return false;
+	}
+
+	if ( empty( $cache_key_prefix ) ) {
+		$cache_key_prefix = $meta_table;
+	}
+
+	if ( empty( $object_column ) ) {
+		$object_column = $object_type . '_id';
+	}
+
+	if ( ! $cache_group ) {
+		return false;
+	}
+
+	$object_ids   = wp_parse_id_list( $object_ids );
+	$uncached_ids = bp_get_non_cached_ids( $object_ids, $cache_group );
+
+	$cache = array();
+
+	// Get meta info.
+	if ( ! empty( $uncached_ids ) ) {
+		$id_list   = join( ',', wp_parse_id_list( $uncached_ids ) );
+		$meta_list = $wpdb->get_results( esc_sql( "SELECT {$object_column}, meta_key, meta_value FROM {$meta_table} WHERE {$object_column} IN ({$id_list})" ), ARRAY_A );
+
+		if ( ! empty( $meta_list ) ) {
+			foreach ( $meta_list as $metarow ) {
+				$mpid = intval( $metarow[$object_column] );
+				$mkey = $metarow['meta_key'];
+				$mval = $metarow['meta_value'];
+
+				// Force subkeys to be array type.
+				if ( !isset( $cache[$mpid] ) || !is_array( $cache[$mpid] ) )
+					$cache[$mpid] = array();
+				if ( !isset( $cache[$mpid][$mkey] ) || !is_array( $cache[$mpid][$mkey] ) )
+					$cache[$mpid][$mkey] = array();
+
+				// Add a value to the current pid/key.
+				$cache[$mpid][$mkey][] = $mval;
+			}
+		}
+
+		foreach ( $uncached_ids as $uncached_id ) {
+			// Cache empty values as well.
+			if ( ! isset( $cache[ $uncached_id ] ) ) {
+				$cache[ $uncached_id ] = array();
+			}
+
+			wp_cache_set( $uncached_id, $cache[ $uncached_id ], $cache_group );
+		}
+	}
+
+	return $cache;
+}
+
+/**
+ * Gets a value that has been cached using an incremented key.
+ *
+ * A utility function for use by query methods like BP_Activity_Activity::get().
+ *
+ * @since 2.7.0
+ * @see bp_core_set_incremented_cache()
+ *
+ * @param string $key   Unique key for the query. Usually a SQL string.
+ * @param string $group Cache group. Eg 'bp_activity'.
+ * @return array|bool False if no cached values are found, otherwise an array of IDs.
+ */
+function bp_core_get_incremented_cache( $key, $group ) {
+	$cache_key = bp_core_get_incremented_cache_key( $key, $group );
+	return wp_cache_get( $cache_key, $group );
+}
+
+/**
+ * Caches a value using an incremented key.
+ *
+ * An "incremented key" is a cache key that is hashed with a unique incrementor,
+ * allowing for bulk invalidation.
+ *
+ * Use this method when caching data that should be invalidated whenever any
+ * object of a given type is created, updated, or deleted. This usually means
+ * data related to object queries, which can only reliably cached until the
+ * underlying set of objects has been modified. See, eg, BP_Activity_Activity::get().
+ *
+ * @since 2.7.0
+ *
+ * @param string $key   Unique key for the query. Usually a SQL string.
+ * @param string $group Cache group. Eg 'bp_activity'.
+ * @param array  $ids   Array of IDs.
+ * @return bool
+ */
+function bp_core_set_incremented_cache( $key, $group, $ids ) {
+	$cache_key = bp_core_get_incremented_cache_key( $key, $group );
+	return wp_cache_set( $cache_key, $ids, $group );
+}
+
+/**
+ * Delete a value that has been cached using an incremented key.
+ *
+ * A utility function for use by query methods like BP_Activity_Activity::get().
+ *
+ * @since 3.0.0
+ * @see bp_core_set_incremented_cache()
+ *
+ * @param string $key   Unique key for the query. Usually a SQL string.
+ * @param string $group Cache group. Eg 'bp_activity'.
+ * @return bool True on successful removal, false on failure.
+ */
+function bp_core_delete_incremented_cache( $key, $group ) {
+	$cache_key = bp_core_get_incremented_cache_key( $key, $group );
+	return wp_cache_delete( $cache_key, $group );
+}
+
+/**
+ * Gets the key to be used when caching a value using an incremented cache key.
+ *
+ * The $key is hashed with a component-specific incrementor, which is used to
+ * invalidate multiple caches at once.
+
+ * @since 2.7.0
+ *
+ * @param string $key   Unique key for the query. Usually a SQL string.
+ * @param string $group Cache group. Eg 'bp_activity'.
+ * @return string
+ */
+function bp_core_get_incremented_cache_key( $key, $group ) {
+	$incrementor = bp_core_get_incrementor( $group );
+	$cache_key = md5( $key . $incrementor );
+	return $cache_key;
+}
+
+/**
+ * Gets a group-specific cache incrementor.
+ *
+ * The incrementor is paired with query identifiers (like SQL strings) to
+ * create cache keys that can be invalidated en masse.
+ *
+ * If an incrementor does not yet exist for the given `$group`, one will
+ * be created.
+ *
+ * @since 2.7.0
+ *
+ * @param string $group Cache group. Eg 'bp_activity'.
+ * @return string
+ */
+function bp_core_get_incrementor( $group ) {
+	$incrementor = wp_cache_get( 'incrementor', $group );
+	if ( ! $incrementor ) {
+		$incrementor = microtime();
+		wp_cache_set( 'incrementor', $incrementor, $group );
+	}
+
+	return $incrementor;
+}
+
+/**
+ * Reset a group-specific cache incrementor.
+ *
+ * Call this function when all incrementor-based caches associated with a given
+ * cache group should be invalidated.
+ *
+ * @since 2.7.0
+ *
+ * @param string $group Cache group. Eg 'bp_activity'.
+ * @return bool True on success, false on failure.
+ */
+function bp_core_reset_incrementor( $group ) {
+	return wp_cache_delete( 'incrementor', $group );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-caps.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-caps.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-caps.php	(revision 12265)
@@ -0,0 +1,447 @@
+<?php
+/**
+ * BuddyPress Capabilities.
+ *
+ * @package BuddyPress
+ * @subpackage Capabilities
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Return an array of roles from the currently loaded blog.
+ *
+ * WordPress roles are dynamically flipped when calls to switch_to_blog() and
+ * restore_current_blog() are made, so we use and trust WordPress core to have
+ * loaded the correct results for us here. As enhancements are made to
+ * WordPress's RBAC, so should our capability functions here.
+ *
+ * @since 2.1.0
+ *
+ * @return object
+ */
+function bp_get_current_blog_roles() {
+	global $wp_roles;
+
+	// Sanity check on roles global variable.
+	$roles = isset( $wp_roles->roles )
+		? $wp_roles->roles
+		: array();
+
+	/**
+	 * Filters the list of editable roles.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $roles List of roles.
+	 */
+	$roles = apply_filters( 'editable_roles', $roles );
+
+	/**
+	 * Filters the array of roles from the currently loaded blog.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array    $roles    Available roles.
+	 * @param WP_Roles $wp_roles Object of WordPress roles.
+	 */
+	return apply_filters( 'bp_get_current_blog_roles', $roles, $wp_roles );
+}
+
+/**
+ * Add capabilities to WordPress user roles.
+ *
+ * This is called on plugin activation.
+ *
+ * @since 1.6.0
+ */
+function bp_add_caps() {
+	global $wp_roles;
+
+	// Load roles if not set.
+	if ( ! isset( $wp_roles ) ) {
+		$wp_roles = new WP_Roles();
+	}
+
+	// Loop through available roles and add them.
+	foreach( $wp_roles->role_objects as $role ) {
+		foreach ( bp_get_caps_for_role( $role->name ) as $cap ) {
+			$role->add_cap( $cap );
+		}
+	}
+
+	/**
+	 * Fires after the addition of capabilities to WordPress user roles.
+	 *
+	 * This is called on plugin activation.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_add_caps' );
+}
+
+/**
+ * Remove capabilities from WordPress user roles.
+ *
+ * This is called on plugin deactivation.
+ *
+ * @since 1.6.0
+ */
+function bp_remove_caps() {
+	global $wp_roles;
+
+	// Load roles if not set.
+	if ( ! isset( $wp_roles ) ) {
+		$wp_roles = new WP_Roles();
+	}
+
+	// Loop through available roles and remove them.
+	foreach( $wp_roles->role_objects as $role ) {
+		foreach ( bp_get_caps_for_role( $role->name ) as $cap ) {
+			$role->remove_cap( $cap );
+		}
+	}
+
+	/**
+	 * Fires after the removal of capabilities from WordPress user roles.
+	 *
+	 * This is called on plugin deactivation.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_remove_caps' );
+}
+
+/**
+ * Map community caps to built in WordPress caps.
+ *
+ * @since 1.6.0
+ *
+ * @see WP_User::has_cap() for description of the arguments passed to the
+ *      'map_meta_cap' filter.
+ *       args.
+ *
+ * @param array  $caps    See {@link WP_User::has_cap()}.
+ * @param string $cap     See {@link WP_User::has_cap()}.
+ * @param int    $user_id See {@link WP_User::has_cap()}.
+ * @param mixed  $args    See {@link WP_User::has_cap()}.
+ * @return array Actual capabilities for meta capability. See {@link WP_User::has_cap()}.
+ */
+function bp_map_meta_caps( $caps, $cap, $user_id, $args ) {
+
+	/**
+	 * Filters the community caps mapping to be built in WordPress caps.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array  $caps    Returns the user's actual capabilities.
+	 * @param string $cap     Capability name.
+	 * @param int    $user_id The user ID.
+	 * @param array  $args    Adds the context to the cap. Typically the object ID.
+	 */
+	return apply_filters( 'bp_map_meta_caps', $caps, $cap, $user_id, $args );
+}
+
+/**
+ * Return community capabilities.
+ *
+ * @since 1.6.0
+ *
+ * @return array Community capabilities.
+ */
+function bp_get_community_caps() {
+
+	// Forum meta caps.
+	$caps = array();
+
+	/**
+	 * Filters community capabilities.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $caps Array of capabilities to add. Empty by default.
+	 */
+	return apply_filters( 'bp_get_community_caps', $caps );
+}
+
+/**
+ * Return an array of capabilities based on the role that is being requested.
+ *
+ * @since 1.6.0
+ *
+ * @param string $role The role for which you're loading caps.
+ * @return array Capabilities for $role.
+ */
+function bp_get_caps_for_role( $role = '' ) {
+
+	// Which role are we looking for?
+	switch ( $role ) {
+
+		// Administrator.
+		case 'administrator' :
+			$caps = array(
+				// Misc.
+				'bp_moderate',
+			);
+
+			break;
+
+		// All other default WordPress blog roles.
+		case 'editor'      :
+		case 'author'      :
+		case 'contributor' :
+		case 'subscriber'  :
+		default            :
+			$caps = array();
+			break;
+	}
+
+	/**
+	 * Filters the array of capabilities based on the role that is being requested.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array  $caps Array of capabilities to return.
+	 * @param string $role The role currently being loaded.
+	 */
+	return apply_filters( 'bp_get_caps_for_role', $caps, $role );
+}
+
+/**
+ * Set a default role for the current user.
+ *
+ * Give a user the default role when creating content on a site they do not
+ * already have a role or capability on.
+ *
+ * @since 1.6.0
+ */
+function bp_set_current_user_default_role() {
+
+	// Bail if not multisite or not root blog.
+	if ( ! is_multisite() || ! bp_is_root_blog() ) {
+		return;
+	}
+
+	// Bail if user is not logged in or already a member.
+	if ( ! is_user_logged_in() || is_user_member_of_blog() ) {
+		return;
+	}
+
+	// Bail if user is not active.
+	if ( bp_is_user_inactive() ) {
+		return;
+	}
+
+	// Set the current users default role.
+	buddypress()->current_user->set_role( bp_get_option( 'default_role', 'subscriber' ) );
+}
+
+/**
+ * Check whether the current user has a given capability.
+ *
+ * @since 1.6.0
+ * @since 2.4.0 Second argument modified to accept an array, rather than `$blog_id`.
+ * @since 2.7.0 Deprecated $args['blog_id'] in favor of $args['site_id'].
+ *
+ * @param string    $capability Capability or role name.
+ * @param array|int $args {
+ *     Array of extra arguments applicable to the capability check.
+ *     @type int   $site_id Optional. Blog ID. Defaults to the BP root blog.
+ *     @type int   $blog_id Deprecated. Use $site_id instead.
+ *     @type mixed $a,...   Optional. Extra arguments applicable to the capability check.
+ * }
+ * @return bool True if the user has the cap for the given parameters.
+ */
+function bp_current_user_can( $capability, $args = array() ) {
+	// Backward compatibility for older $blog_id parameter.
+	if ( is_int( $args ) ) {
+		$site_id = $args;
+		$args = array();
+		$args['site_id'] = $site_id;
+
+	// New format for second parameter.
+	} elseif ( is_array( $args ) && isset( $args['blog_id'] ) ) {
+		// Get the blog ID if set, but don't pass along to `current_user_can_for_blog()`.
+		$args['site_id'] = (int) $args['blog_id'];
+		unset( $args['blog_id'] );
+	}
+
+	// Cast $args as an array.
+	$args = (array) $args;
+
+	// Use root blog if no ID passed.
+	if ( empty( $args['site_id'] ) ) {
+		$args['site_id'] = bp_get_root_blog_id();
+	}
+
+	/** This filter is documented in /bp-core/bp-core-template.php */
+	$current_user_id = apply_filters( 'bp_loggedin_user_id', get_current_user_id() );
+
+	// Call bp_user_can().
+	$retval = bp_user_can( $current_user_id, $capability, $args );
+
+	/**
+	 * Filters whether or not the current user has a given capability.
+	 *
+	 * @since 1.6.0
+	 * @since 2.4.0 Pass `$args` variable.
+	 * @since 2.7.0 Change format of $args variable array.
+	 *
+	 * @param bool   $retval     Whether or not the current user has the capability.
+	 * @param string $capability The capability being checked for.
+	 * @param int    $blog_id    Blog ID. Defaults to the BP root blog.
+	 * @param array  $args       Array of extra arguments as originally passed.
+	 */
+	return (bool) apply_filters( 'bp_current_user_can', $retval, $capability, $args['site_id'], $args );
+}
+
+/**
+ * Check whether the specified user has a given capability on a given site.
+ *
+ * @since 2.7.0
+ *
+ * @param int       $user_id
+ * @param string    $capability Capability or role name.
+ * @param array|int $args {
+ *     Array of extra arguments applicable to the capability check.
+ *
+ *     @type int   $site_id Optional. Site ID. Defaults to the BP root blog.
+ *     @type mixed $a,...   Optional. Extra arguments applicable to the capability check.
+ * }
+ * @return bool True if the user has the cap for the given parameters.
+ */
+function bp_user_can( $user_id, $capability, $args = array() ) {
+	$site_id = bp_get_root_blog_id();
+
+	// Get the site ID if set, but don't pass along to user_can().
+	if ( isset( $args['site_id'] ) ) {
+		$site_id = (int) $args['site_id'];
+		unset( $args['site_id'] );
+	}
+
+	$switched = is_multisite() ? switch_to_blog( $site_id ) : false;
+	$retval   = call_user_func_array( 'user_can', array( $user_id, $capability, $args ) );
+
+	/**
+	 * Filters whether or not the specified user has a given capability on a given site.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param bool   $retval     Whether or not the current user has the capability.
+	 * @param int    $user_id
+	 * @param string $capability The capability being checked for.
+	 * @param int    $site_id    Site ID. Defaults to the BP root blog.
+	 * @param array  $args       Array of extra arguments passed.
+	 */
+	$retval = (bool) apply_filters( 'bp_user_can', $retval, $user_id, $capability, $site_id, $args );
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	return $retval;
+}
+
+/**
+ * Temporary implementation of 'bp_moderate' cap.
+ *
+ * In BuddyPress 1.6, the 'bp_moderate' cap was introduced. In order to
+ * enforce that bp_current_user_can( 'bp_moderate' ) always returns true for
+ * Administrators, we must manually add the 'bp_moderate' cap to the list of
+ * user caps for Admins.
+ *
+ * Note that this level of enforcement is only necessary in the case of
+ * non-Multisite. This is because WordPress automatically assigns every
+ * capability - and thus 'bp_moderate' - to Super Admins on a Multisite
+ * installation. See {@link WP_User::has_cap()}.
+ *
+ * This implementation of 'bp_moderate' is temporary, until BuddyPress properly
+ * matches caps to roles and stores them in the database.
+ *
+ * Plugin authors: Please do not use this function; thank you. :)
+ *
+ * @since 1.6.0
+ *
+ * @access private
+ *
+ * @see WP_User::has_cap()
+ *
+ * @param array  $caps    The caps that WP associates with the given role.
+ * @param string $cap     The caps being tested for in WP_User::has_cap().
+ * @param int    $user_id ID of the user being checked against.
+ * @param array  $args    Miscellaneous arguments passed to the user_has_cap filter.
+ * @return array $allcaps The user's cap list, with 'bp_moderate' appended, if relevant.
+ */
+function _bp_enforce_bp_moderate_cap_for_admins( $caps = array(), $cap = '', $user_id = 0, $args = array() ) {
+
+	// Bail if not checking the 'bp_moderate' cap.
+	if ( 'bp_moderate' !== $cap ) {
+		return $caps;
+	}
+
+	// Bail if BuddyPress is not network activated.
+	if ( bp_is_network_activated() ) {
+		return $caps;
+	}
+
+	// Never trust inactive users.
+	if ( bp_is_user_inactive( $user_id ) ) {
+		return $caps;
+	}
+
+	// Only users that can 'manage_options' on this site can 'bp_moderate'.
+	return array( 'manage_options' );
+}
+add_filter( 'map_meta_cap', '_bp_enforce_bp_moderate_cap_for_admins', 10, 4 );
+
+/** Deprecated ****************************************************************/
+
+/**
+ * Adds BuddyPress-specific user roles.
+ *
+ * This is called on plugin activation.
+ *
+ * @since 1.6.0
+ * @deprecated 1.7.0
+ */
+function bp_add_roles() {
+	_doing_it_wrong( 'bp_add_roles', __( 'Special community roles no longer exist. Use mapped capabilities instead', 'buddypress' ), '1.7' );
+}
+
+/**
+ * Removes BuddyPress-specific user roles.
+ *
+ * This is called on plugin deactivation.
+ *
+ * @since 1.6.0
+ * @deprecated 1.7.0
+ */
+function bp_remove_roles() {
+	_doing_it_wrong( 'bp_remove_roles', __( 'Special community roles no longer exist. Use mapped capabilities instead', 'buddypress' ), '1.7' );
+}
+
+
+/**
+ * The participant role for registered users without roles.
+ *
+ * This is primarily for multisite compatibility when users without roles on
+ * sites that have global communities enabled.
+ *
+ * @since 1.6.0
+ * @deprecated 1.7.0
+ */
+function bp_get_participant_role() {
+	_doing_it_wrong( 'bp_get_participant_role', __( 'Special community roles no longer exist. Use mapped capabilities instead', 'buddypress' ), '1.7' );
+}
+
+/**
+ * The moderator role for BuddyPress users.
+ *
+ * @since 1.6.0
+ * @deprecated 1.7.0
+ */
+function bp_get_moderator_role() {
+	_doing_it_wrong( 'bp_get_moderator_role', __( 'Special community roles no longer exist. Use mapped capabilities instead', 'buddypress' ), '1.7' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-catchuri.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-catchuri.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-catchuri.php	(revision 12265)
@@ -0,0 +1,1085 @@
+<?php
+/**
+ * BuddyPress URI catcher.
+ *
+ * Functions for parsing the URI and determining which BuddyPress template file
+ * to use on-screen.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Analyze the URI and break it down into BuddyPress-usable chunks.
+ *
+ * BuddyPress can use complete custom friendly URIs without the user having to
+ * add new rewrite rules. Custom components are able to use their own custom
+ * URI structures with very little work.
+ *
+ * The URIs are broken down as follows:
+ *   - http:// example.com / members / andy / [current_component] / [current_action] / [action_variables] / [action_variables] / ...
+ *   - OUTSIDE ROOT: http:// example.com / sites / buddypress / members / andy / [current_component] / [current_action] / [action_variables] / [action_variables] / ...
+ *
+ * Example:
+ *    - http://example.com/members/andy/profile/edit/group/5/
+ *    - $bp->current_component: string 'xprofile'
+ *    - $bp->current_action: string 'edit'
+ *    - $bp->action_variables: array ['group', 5]
+ *
+ * @since 1.0.0
+ */
+function bp_core_set_uri_globals() {
+	global $current_blog, $wp_rewrite;
+
+	// Don't catch URIs on non-root blogs unless multiblog mode is on.
+	if ( !bp_is_root_blog() && !bp_is_multiblog_mode() )
+		return false;
+
+	$bp = buddypress();
+
+	// Define local variables.
+	$root_profile = $match   = false;
+	$key_slugs    = $matches = $uri_chunks = array();
+
+	// Fetch all the WP page names for each component.
+	if ( empty( $bp->pages ) )
+		$bp->pages = bp_core_get_directory_pages();
+
+	// Ajax or not?
+	if ( defined( 'DOING_AJAX' ) && DOING_AJAX || strpos( $_SERVER['REQUEST_URI'], 'wp-load.php' ) )
+		$path = bp_get_referer_path();
+	else
+		$path = esc_url( $_SERVER['REQUEST_URI'] );
+
+	/**
+	 * Filters the BuddyPress global URI path.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $path Path to set.
+	 */
+	$path = apply_filters( 'bp_uri', $path );
+
+	// Take GET variables off the URL to avoid problems.
+	$path = strtok( $path, '?' );
+
+	// Fetch current URI and explode each part separated by '/' into an array.
+	$bp_uri = explode( '/', $path );
+
+	// Loop and remove empties.
+	foreach ( (array) $bp_uri as $key => $uri_chunk ) {
+		if ( empty( $bp_uri[$key] ) ) {
+			unset( $bp_uri[$key] );
+		}
+	}
+
+	/*
+	 * If running off blog other than root, any subdirectory names must be
+	 * removed from $bp_uri. This includes two cases:
+	 *
+	 * 1. when WP is installed in a subdirectory,
+	 * 2. when BP is running on secondary blog of a subdirectory
+	 * multisite installation. Phew!
+	 */
+	if ( is_multisite() && !is_subdomain_install() && ( bp_is_multiblog_mode() || 1 != bp_get_root_blog_id() ) ) {
+
+		// Blow chunks.
+		$chunks = explode( '/', $current_blog->path );
+
+		// If chunks exist...
+		if ( !empty( $chunks ) ) {
+
+			// ...loop through them...
+			foreach( $chunks as $key => $chunk ) {
+				$bkey = array_search( $chunk, $bp_uri );
+
+				// ...and unset offending keys
+				if ( false !== $bkey ) {
+					unset( $bp_uri[$bkey] );
+				}
+
+				$bp_uri = array_values( $bp_uri );
+			}
+		}
+	}
+
+	// Get site path items.
+	$paths = explode( '/', bp_core_get_site_path() );
+
+	// Take empties off the end of path.
+	if ( empty( $paths[count( $paths ) - 1] ) )
+		array_pop( $paths );
+
+	// Take empties off the start of path.
+	if ( empty( $paths[0] ) )
+		array_shift( $paths );
+
+	// Reset indexes.
+	$bp_uri = array_values( $bp_uri );
+	$paths  = array_values( $paths );
+
+	// Unset URI indices if they intersect with the paths.
+	foreach ( (array) $bp_uri as $key => $uri_chunk ) {
+		if ( isset( $paths[$key] ) && $uri_chunk == $paths[$key] ) {
+			unset( $bp_uri[$key] );
+		}
+	}
+
+	// Reset the keys by merging with an empty array.
+	$bp_uri = array_merge( array(), $bp_uri );
+
+	/*
+	 * If a component is set to the front page, force its name into $bp_uri
+	 * so that $current_component is populated (unless a specific WP post is being requested
+	 * via a URL parameter, usually signifying Preview mode).
+	 */
+	if ( 'page' == get_option( 'show_on_front' ) && get_option( 'page_on_front' ) && empty( $bp_uri ) && empty( $_GET['p'] ) && empty( $_GET['page_id'] ) ) {
+		$post = get_post( get_option( 'page_on_front' ) );
+		if ( !empty( $post ) ) {
+			$bp_uri[0] = $post->post_name;
+		}
+	}
+
+	// Keep the unfiltered URI safe.
+	$bp->unfiltered_uri = $bp_uri;
+
+	// Don't use $bp_unfiltered_uri, this is only for backpat with old plugins. Use $bp->unfiltered_uri.
+	$GLOBALS['bp_unfiltered_uri'] = &$bp->unfiltered_uri;
+
+	// Get slugs of pages into array.
+	foreach ( (array) $bp->pages as $page_key => $bp_page )
+		$key_slugs[$page_key] = trailingslashit( '/' . $bp_page->slug );
+
+	// Bail if keyslugs are empty, as BP is not setup correct.
+	if ( empty( $key_slugs ) )
+		return;
+
+	// Loop through page slugs and look for exact match to path.
+	foreach ( $key_slugs as $key => $slug ) {
+		if ( $slug == $path ) {
+			$match      = $bp->pages->{$key};
+			$match->key = $key;
+			$matches[]  = 1;
+			break;
+		}
+	}
+
+	// No exact match, so look for partials.
+	if ( empty( $match ) ) {
+
+		// Loop through each page in the $bp->pages global.
+		foreach ( (array) $bp->pages as $page_key => $bp_page ) {
+
+			// Look for a match (check members first).
+			if ( in_array( $bp_page->name, (array) $bp_uri ) ) {
+
+				// Match found, now match the slug to make sure.
+				$uri_chunks = explode( '/', $bp_page->slug );
+
+				// Loop through uri_chunks.
+				foreach ( (array) $uri_chunks as $key => $uri_chunk ) {
+
+					// Make sure chunk is in the correct position.
+					if ( !empty( $bp_uri[$key] ) && ( $bp_uri[$key] == $uri_chunk ) ) {
+						$matches[] = 1;
+
+					// No match.
+					} else {
+						$matches[] = 0;
+					}
+				}
+
+				// Have a match.
+				if ( !in_array( 0, (array) $matches ) ) {
+					$match      = $bp_page;
+					$match->key = $page_key;
+					break;
+				};
+
+				// Unset matches.
+				unset( $matches );
+			}
+
+			// Unset uri chunks.
+			unset( $uri_chunks );
+		}
+	}
+
+	// URLs with BP_ENABLE_ROOT_PROFILES enabled won't be caught above.
+	if ( empty( $matches ) && bp_core_enable_root_profiles() && ! empty( $bp_uri[0] ) ) {
+
+		// Switch field based on compat.
+		$field = bp_is_username_compatibility_mode() ? 'login' : 'slug';
+
+		/**
+		 * Filter the portion of the URI that is the displayed user's slug.
+		 *
+		 * Eg. example.com/ADMIN (when root profiles is enabled)
+		 *     example.com/members/ADMIN (when root profiles isn't enabled)
+		 *
+		 * ADMIN would be the displayed user's slug.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param string $member_slug
+		 */
+		$member_slug = apply_filters( 'bp_core_set_uri_globals_member_slug', $bp_uri[0] );
+
+		// Make sure there's a user corresponding to $bp_uri[0].
+		if ( ! empty( $bp->pages->members ) && $root_profile = get_user_by( $field, $member_slug ) ) {
+
+			// Force BP to recognize that this is a members page.
+			$matches[]  = 1;
+			$match      = $bp->pages->members;
+			$match->key = 'members';
+		}
+	}
+
+	// Search doesn't have an associated page, so we check for it separately.
+	if ( !empty( $bp_uri[0] ) && ( bp_get_search_slug() == $bp_uri[0] ) ) {
+		$matches[]   = 1;
+		$match       = new stdClass;
+		$match->key  = 'search';
+		$match->slug = bp_get_search_slug();
+	}
+
+	// This is not a BuddyPress page, so just return.
+	if ( empty( $matches ) )
+		return false;
+
+	$wp_rewrite->use_verbose_page_rules = false;
+
+	// Find the offset. With $root_profile set, we fudge the offset down so later parsing works.
+	$slug       = !empty ( $match ) ? explode( '/', $match->slug ) : '';
+	$uri_offset = empty( $root_profile ) ? 0 : -1;
+
+	// Rejig the offset.
+	if ( !empty( $slug ) && ( 1 < count( $slug ) ) ) {
+		// Only offset if not on a root profile. Fixes issue when Members page is nested.
+		if ( false === $root_profile ) {
+			array_pop( $slug );
+			$uri_offset = count( $slug );
+		}
+	}
+
+	// Global the unfiltered offset to use in bp_core_load_template().
+	// To avoid PHP warnings in bp_core_load_template(), it must always be >= 0.
+	$bp->unfiltered_uri_offset = $uri_offset >= 0 ? $uri_offset : 0;
+
+	// We have an exact match.
+	if ( isset( $match->key ) ) {
+
+		// Set current component to matched key.
+		$bp->current_component = $match->key;
+
+		// If members component, do more work to find the actual component.
+		if ( 'members' == $match->key ) {
+
+			$after_member_slug = false;
+			if ( ! empty( $bp_uri[ $uri_offset + 1 ] ) ) {
+				$after_member_slug = $bp_uri[ $uri_offset + 1 ];
+			}
+
+			// Are we viewing a specific user?
+			if ( $after_member_slug ) {
+
+				/** This filter is documented in bp-core/bp-core-catchuri.php */
+				$after_member_slug = apply_filters( 'bp_core_set_uri_globals_member_slug', $after_member_slug );
+
+				// If root profile, we've already queried for the user.
+				if ( $root_profile instanceof WP_User ) {
+					$bp->displayed_user->id = $root_profile->ID;
+
+				// Switch the displayed_user based on compatibility mode.
+				} elseif ( bp_is_username_compatibility_mode() ) {
+					$bp->displayed_user->id = (int) bp_core_get_userid( urldecode( $after_member_slug ) );
+
+				} else {
+					$bp->displayed_user->id = (int) bp_core_get_userid_from_nicename( $after_member_slug );
+				}
+			}
+
+			// Is this a member type directory?
+			if ( ! bp_displayed_user_id() && $after_member_slug === bp_get_members_member_type_base() && ! empty( $bp_uri[ $uri_offset + 2 ] ) ) {
+				$matched_types = bp_get_member_types( array(
+					'has_directory'  => true,
+					'directory_slug' => $bp_uri[ $uri_offset + 2 ],
+				) );
+
+				if ( ! empty( $matched_types ) ) {
+					$bp->current_member_type = reset( $matched_types );
+					unset( $bp_uri[ $uri_offset + 1 ] );
+				}
+			}
+
+			// If the slug matches neither a member type nor a specific member, 404.
+			if ( ! bp_displayed_user_id() && ! bp_get_current_member_type() && $after_member_slug ) {
+				// Prevent components from loading their templates.
+				$bp->current_component = '';
+				bp_do_404();
+				return;
+			}
+
+			// If the displayed user is marked as a spammer, 404 (unless logged-in user is a super admin).
+			if ( bp_displayed_user_id() && bp_is_user_spammer( bp_displayed_user_id() ) ) {
+				if ( bp_current_user_can( 'bp_moderate' ) ) {
+					bp_core_add_message( __( 'This user has been marked as a spammer. Only site admins can view this profile.', 'buddypress' ), 'warning' );
+				} else {
+					bp_do_404();
+					return;
+				}
+			}
+
+			// Bump the offset.
+			if ( bp_displayed_user_id() ) {
+				if ( isset( $bp_uri[$uri_offset + 2] ) ) {
+					$bp_uri                = array_merge( array(), array_slice( $bp_uri, $uri_offset + 2 ) );
+					$bp->current_component = $bp_uri[0];
+
+				// No component, so default will be picked later.
+				} else {
+					$bp_uri                = array_merge( array(), array_slice( $bp_uri, $uri_offset + 2 ) );
+					$bp->current_component = '';
+				}
+
+				// Reset the offset.
+				$uri_offset = 0;
+			}
+		}
+	}
+
+	// Determine the current action.
+	$current_action = isset( $bp_uri[ $uri_offset + 1 ] ) ? $bp_uri[ $uri_offset + 1 ] : '';
+
+	/*
+	 * If a BuddyPress directory is set to the WP front page, URLs like example.com/members/?s=foo
+	 * shouldn't interfere with blog searches.
+	 */
+	if ( empty( $current_action) && ! empty( $_GET['s'] ) && 'page' == get_option( 'show_on_front' ) && ! empty( $match->id ) ) {
+		$page_on_front = (int) get_option( 'page_on_front' );
+		if ( (int) $match->id === $page_on_front ) {
+			$bp->current_component = '';
+			return false;
+		}
+	}
+
+	$bp->current_action = $current_action;
+
+	// Slice the rest of the $bp_uri array and reset offset.
+	$bp_uri      = array_slice( $bp_uri, $uri_offset + 2 );
+	$uri_offset  = 0;
+
+	// Set the entire URI as the action variables, we will unset the current_component and action in a second.
+	$bp->action_variables = $bp_uri;
+
+	// Reset the keys by merging with an empty array.
+	$bp->action_variables = array_merge( array(), $bp->action_variables );
+}
+
+/**
+ * Are root profiles enabled and allowed?
+ *
+ * @since 1.6.0
+ *
+ * @return bool True if yes, false if no.
+ */
+function bp_core_enable_root_profiles() {
+
+	$retval = false;
+
+	if ( defined( 'BP_ENABLE_ROOT_PROFILES' ) && ( true == BP_ENABLE_ROOT_PROFILES ) )
+		$retval = true;
+
+	/**
+	 * Filters whether or not root profiles are enabled and allowed.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $retval Whether or not root profiles are available.
+	 */
+	return apply_filters( 'bp_core_enable_root_profiles', $retval );
+}
+
+/**
+ * Load a specific template file with fallback support.
+ *
+ * Example:
+ *   bp_core_load_template( 'members/index' );
+ * Loads:
+ *   wp-content/themes/[activated_theme]/members/index.php
+ *
+ * @since 1.0.0
+ *
+ * @param array $templates Array of templates to attempt to load.
+ */
+function bp_core_load_template( $templates ) {
+	global $wp_query;
+
+	// Reset the post.
+	bp_theme_compat_reset_post( array(
+		'ID'          => 0,
+		'is_404'      => true,
+		'post_status' => 'publish',
+	) );
+
+	// Set theme compat to false since the reset post function automatically sets
+	// theme compat to true.
+	bp_set_theme_compat_active( false );
+
+	// Fetch each template and add the php suffix.
+	$filtered_templates = array();
+	foreach ( (array) $templates as $template ) {
+		$filtered_templates[] = $template . '.php';
+	}
+
+	// Only perform template lookup for bp-default themes.
+	if ( ! bp_use_theme_compat_with_current_theme() ) {
+		$template = locate_template( (array) $filtered_templates, false );
+
+	// Theme compat doesn't require a template lookup.
+	} else {
+		$template = '';
+	}
+
+	/**
+	 * Filters the template locations.
+	 *
+	 * Allows plugins to alter where the template files are located.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string $template           Located template path.
+	 * @param array  $filtered_templates Array of templates to attempt to load.
+	 */
+	$located_template = apply_filters( 'bp_located_template', $template, $filtered_templates );
+
+	/*
+	 * If current page is an embed, wipe out bp-default template.
+	 *
+	 * Wiping out the bp-default template allows WordPress to use their special
+	 * embed template, which is what we want.
+	 */
+	if ( function_exists( 'is_embed' ) && is_embed() ) {
+		$located_template = '';
+	}
+
+	if ( !empty( $located_template ) ) {
+		// Template was located, lets set this as a valid page and not a 404.
+		status_header( 200 );
+		$wp_query->is_page     = true;
+		$wp_query->is_singular = true;
+		$wp_query->is_404      = false;
+
+		/**
+		 * Fires before the loading of a located template file.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $located_template Template found to be loaded.
+		 */
+		do_action( 'bp_core_pre_load_template', $located_template );
+
+		/**
+		 * Filters the selected template right before loading.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $located_template Template found to be loaded.
+		 */
+		load_template( apply_filters( 'bp_load_template', $located_template ) );
+
+		/**
+		 * Fires after the loading of a located template file.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $located_template Template found that was loaded.
+		 */
+		do_action( 'bp_core_post_load_template', $located_template );
+
+		// Kill any other output after this.
+		exit();
+
+	// No template found, so setup theme compatibility.
+	// @todo Some other 404 handling if theme compat doesn't kick in.
+	} else {
+
+		// We know where we are, so reset important $wp_query bits here early.
+		// The rest will be done by bp_theme_compat_reset_post() later.
+		if ( is_buddypress() ) {
+			status_header( 200 );
+			$wp_query->is_page     = true;
+			$wp_query->is_singular = true;
+			$wp_query->is_404      = false;
+		}
+
+		/**
+		 * Fires if there are no found templates to load and theme compat is needed.
+		 *
+		 * @since 1.7.0
+		 */
+		do_action( 'bp_setup_theme_compat' );
+	}
+}
+
+/**
+ * Redirect away from /profile URIs if XProfile is not enabled.
+ *
+ * @since 1.0.0
+ */
+function bp_core_catch_profile_uri() {
+	if ( !bp_is_active( 'xprofile' ) ) {
+
+		/**
+		 * Filters the path to redirect users to if XProfile is not enabled.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Path to redirect users to.
+		 */
+		bp_core_load_template( apply_filters( 'bp_core_template_display_profile', 'members/single/home' ) );
+	}
+}
+
+/**
+ * Members user shortlink redirector.
+ *
+ * Redirects x.com/members/me/* to x.com/members/{LOGGED_IN_USER_SLUG}/*
+ *
+ * @since 2.6.0
+ *
+ * @param string $member_slug The current member slug.
+ * @return string $member_slug The current member slug.
+ */
+function bp_core_members_shortlink_redirector( $member_slug ) {
+
+	/**
+	 * Shortlink slug to redirect to logged-in user.
+	 *
+	 * The x.com/members/me/* url will redirect to x.com/members/{LOGGED_IN_USER_SLUG}/*
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $slug Defaults to 'me'.
+	 */
+	$me_slug = apply_filters( 'bp_core_members_shortlink_slug', 'me' );
+
+	// Check if we're on our special shortlink slug. If not, bail.
+	if ( $me_slug !== $member_slug ) {
+		return $member_slug;
+	}
+
+	// If logged out, redirect user to login.
+	if ( false === is_user_logged_in() ) {
+		// Add our login redirector hook.
+		add_action( 'template_redirect', 'bp_core_no_access', 0 );
+
+		return $member_slug;
+	}
+
+	$user = wp_get_current_user();
+
+	return bp_core_get_username( $user->ID, $user->user_nicename, $user->user_login );
+}
+add_filter( 'bp_core_set_uri_globals_member_slug', 'bp_core_members_shortlink_redirector' );
+
+/**
+ * Catch unauthorized access to certain BuddyPress pages and redirect accordingly.
+ *
+ * @since 1.5.0
+ */
+function bp_core_catch_no_access() {
+	global $wp_query;
+
+	$bp = buddypress();
+
+	// If coming from bp_core_redirect() and $bp_no_status_set is true,
+	// we are redirecting to an accessible page so skip this check.
+	if ( !empty( $bp->no_status_set ) )
+		return false;
+
+	if ( !isset( $wp_query->queried_object ) && !bp_is_blog_page() ) {
+		bp_do_404();
+	}
+}
+add_action( 'bp_template_redirect', 'bp_core_catch_no_access', 1 );
+
+/**
+ * Redirect a user to log in for BP pages that require access control.
+ *
+ * Add an error message (if one is provided).
+ *
+ * If authenticated, redirects user back to requested content by default.
+ *
+ * @since 1.5.0
+ *
+ * @param array|string $args {
+ *     @type int    $mode     Specifies the destination of the redirect. 1 will
+ *                            direct to the root domain (home page), which assumes you have a
+ *                            log-in form there; 2 directs to wp-login.php. Default: 2.
+ *     @type string $redirect The URL the user will be redirected to after successfully
+ *                            logging in. Default: the URL originally requested.
+ *     @type string $root     The root URL of the site, used in case of error or mode 1 redirects.
+ *                            Default: the value of {@link bp_get_root_domain()}.
+ *     @type string $message  An error message to display to the user on the log-in page.
+ *                            Default: "You must log in to access the page you requested."
+ * }
+ */
+function bp_core_no_access( $args = '' ) {
+
+	// Build the redirect URL.
+	$redirect_url  = is_ssl() ? 'https://' : 'http://';
+	$redirect_url .= $_SERVER['HTTP_HOST'];
+	$redirect_url .= $_SERVER['REQUEST_URI'];
+
+	$defaults = array(
+		'mode'     => 2,                    // 1 = $root, 2 = wp-login.php.
+		'redirect' => $redirect_url,        // the URL you get redirected to when a user successfully logs in.
+		'root'     => bp_get_root_domain(), // the landing page you get redirected to when a user doesn't have access.
+		'message'  => __( 'You must log in to access the page you requested.', 'buddypress' )
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+
+	/**
+	 * Filters the arguments used for user redirecting when visiting access controlled areas.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $r Array of parsed arguments for redirect determination.
+	 */
+	$r = apply_filters( 'bp_core_no_access', $r );
+	extract( $r, EXTR_SKIP );
+
+	/*
+	 * @ignore Ignore these filters and use 'bp_core_no_access' above.
+	 */
+	$mode     = apply_filters( 'bp_no_access_mode',     $mode,     $root,     $redirect, $message );
+	$redirect = apply_filters( 'bp_no_access_redirect', $redirect, $root,     $message,  $mode    );
+	$root     = apply_filters( 'bp_no_access_root',     $root,     $redirect, $message,  $mode    );
+	$message  = apply_filters( 'bp_no_access_message',  $message,  $root,     $redirect, $mode    );
+	$root     = trailingslashit( $root );
+
+	switch ( $mode ) {
+
+		// Option to redirect to wp-login.php.
+		// Error message is displayed with bp_core_no_access_wp_login_error().
+		case 2 :
+			if ( !empty( $redirect ) ) {
+				bp_core_redirect( add_query_arg( array(
+					'bp-auth' => 1,
+					'action'  => 'bpnoaccess'
+				), wp_login_url( $redirect ) ) );
+			} else {
+				bp_core_redirect( $root );
+			}
+
+			break;
+
+		// Redirect to root with "redirect_to" parameter.
+		// Error message is displayed with bp_core_add_message().
+		case 1 :
+		default :
+
+			$url = $root;
+			if ( !empty( $redirect ) ) {
+				$url = add_query_arg( 'redirect_to', urlencode( $redirect ), $root );
+			}
+
+			if ( !empty( $message ) ) {
+				bp_core_add_message( $message, 'error' );
+			}
+
+			bp_core_redirect( $url );
+
+			break;
+	}
+}
+
+/**
+ * Login redirector.
+ *
+ * If a link is not publicly available, we can send members from external
+ * locations, like following links in an email, through the login screen.
+ *
+ * If a user clicks on this link and is already logged in, we should attempt
+ * to redirect the user to the authorized content instead of forcing the user
+ * to re-authenticate.
+ *
+ * @since 2.9.0
+ */
+function bp_login_redirector() {
+	// Redirect links must include the `redirect_to` and `bp-auth` parameters.
+	if ( empty( $_GET['redirect_to'] ) || empty( $_GET['bp-auth'] ) ) {
+		return;
+	}
+
+	/*
+	 * If the user is already logged in,
+	 * skip the login form and redirect them to the content.
+	 */
+	if ( bp_loggedin_user_id() ) {
+		wp_safe_redirect( esc_url_raw( $_GET['redirect_to'] ) );
+		exit;
+	}
+}
+add_action( 'login_init', 'bp_login_redirector', 1 );
+
+/**
+ * Add a custom BuddyPress no access error message to wp-login.php.
+ *
+ * @since 1.5.0
+ * @since 2.7.0 Hook moved to 'wp_login_errors' made available since WP 3.6.0.
+ *
+ * @param  WP_Error $errors Current error container.
+ * @return WP_Error
+ */
+function bp_core_no_access_wp_login_error( $errors ) {
+	if ( empty( $_GET['action'] ) || 'bpnoaccess' !== $_GET['action'] ) {
+		return $errors;
+	}
+
+	/**
+	 * Filters the error message for wp-login.php when needing to log in before accessing.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value Error message to display.
+	 * @param string $value URL to redirect user to after successful login.
+	 */
+	$message = apply_filters( 'bp_wp_login_error', __( 'You must log in to access the page you requested.', 'buddypress' ), $_REQUEST['redirect_to'] );
+
+	$errors->add( 'bp_no_access', $message );
+
+	return $errors;
+}
+add_filter( 'wp_login_errors', 'bp_core_no_access_wp_login_error' );
+
+/**
+ * Add our custom error code to WP login's shake error codes.
+ *
+ * @since 2.7.0
+ *
+ * @param  array $codes Array of WP error codes.
+ * @return array
+ */
+function bp_core_login_filter_shake_codes( $codes ) {
+	$codes[] = 'bp_no_access';
+	return $codes;
+}
+add_filter( 'shake_error_codes', 'bp_core_login_filter_shake_codes' );
+
+/**
+ * Canonicalize BuddyPress URLs.
+ *
+ * This function ensures that requests for BuddyPress content are always
+ * redirected to their canonical versions. Canonical versions are always
+ * trailingslashed, and are typically the most general possible versions of the
+ * URL - eg, example.com/groups/mygroup/ instead of
+ * example.com/groups/mygroup/home/.
+ *
+ * @since 1.6.0
+ *
+ * @see BP_Members_Component::setup_globals() where
+ *      $bp->canonical_stack['base_url'] and ['component'] may be set.
+ * @see bp_core_new_nav_item() where $bp->canonical_stack['action'] may be set.
+ */
+function bp_redirect_canonical() {
+
+	/**
+	 * Filters whether or not to do canonical redirects on BuddyPress URLs.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not to do canonical redirects. Default true.
+	 */
+	if ( !bp_is_blog_page() && apply_filters( 'bp_do_redirect_canonical', true ) ) {
+		// If this is a POST request, don't do a canonical redirect.
+		// This is for backward compatibility with plugins that submit form requests to
+		// non-canonical URLs. Plugin authors should do their best to use canonical URLs in
+		// their form actions.
+		if ( !empty( $_POST ) ) {
+			return;
+		}
+
+		// Build the URL in the address bar.
+		$requested_url  = bp_get_requested_url();
+
+		// Stash query args.
+		$url_stack      = explode( '?', $requested_url );
+		$req_url_clean  = $url_stack[0];
+		$query_args     = isset( $url_stack[1] ) ? $url_stack[1] : '';
+
+		$canonical_url  = bp_get_canonical_url();
+
+		// Only redirect if we've assembled a URL different from the request.
+		if ( $canonical_url !== $req_url_clean ) {
+
+			$bp = buddypress();
+
+			// Template messages have been deleted from the cookie by this point, so
+			// they must be readded before redirecting.
+			if ( isset( $bp->template_message ) ) {
+				$message      = stripslashes( $bp->template_message );
+				$message_type = isset( $bp->template_message_type ) ? $bp->template_message_type : 'success';
+
+				bp_core_add_message( $message, $message_type );
+			}
+
+			if ( !empty( $query_args ) ) {
+				$canonical_url .= '?' . $query_args;
+			}
+
+			bp_core_redirect( $canonical_url, 301 );
+		}
+	}
+}
+
+/**
+ * Output rel=canonical header tag for BuddyPress content.
+ *
+ * @since 1.6.0
+ */
+function bp_rel_canonical() {
+	$canonical_url = bp_get_canonical_url();
+
+	// Output rel=canonical tag.
+	echo "<link rel='canonical' href='" . esc_attr( $canonical_url ) . "' />\n";
+}
+
+/**
+ * Get the canonical URL of the current page.
+ *
+ * @since 1.6.0
+ *
+ * @param array $args {
+ *     Optional array of arguments.
+ *     @type bool $include_query_args Whether to include current URL arguments
+ *                                    in the canonical URL returned from the function.
+ * }
+ * @return string Canonical URL for the current page.
+ */
+function bp_get_canonical_url( $args = array() ) {
+
+	// For non-BP content, return the requested url, and let WP do the work.
+	if ( bp_is_blog_page() ) {
+		return bp_get_requested_url();
+	}
+
+	$bp = buddypress();
+
+	$defaults = array(
+		'include_query_args' => false // Include URL arguments, eg ?foo=bar&foo2=bar2.
+	);
+	$r = wp_parse_args( $args, $defaults );
+	extract( $r );
+
+	// Special case: when a BuddyPress directory (eg example.com/members)
+	// is set to be the front page, ensure that the current canonical URL
+	// is the home page URL.
+	if ( 'page' == get_option( 'show_on_front' ) && $page_on_front = (int) get_option( 'page_on_front' ) ) {
+		$front_page_component = array_search( $page_on_front, bp_core_get_directory_page_ids() );
+
+		/*
+		 * If requesting the front page component directory, canonical
+		 * URL is the front page. We detect whether we're detecting a
+		 * component *directory* by checking that bp_current_action()
+		 * is empty - ie, this not a single item, a feed, or an item
+		 * type directory.
+		 */
+		if ( false !== $front_page_component && bp_is_current_component( $front_page_component ) && ! bp_current_action() && ! bp_get_current_member_type() ) {
+			$bp->canonical_stack['canonical_url'] = trailingslashit( bp_get_root_domain() );
+
+		// Except when the front page is set to the registration page
+		// and the current user is logged in. In this case we send to
+		// the members directory to avoid redirect loops.
+		} elseif ( bp_is_register_page() && 'register' == $front_page_component && is_user_logged_in() ) {
+
+			/**
+			 * Filters the logged in register page redirect URL.
+			 *
+			 * @since 1.5.1
+			 *
+			 * @param string $value URL to redirect logged in members to.
+			 */
+			$bp->canonical_stack['canonical_url'] = apply_filters( 'bp_loggedin_register_page_redirect_to', bp_get_members_directory_permalink() );
+		}
+	}
+
+	if ( empty( $bp->canonical_stack['canonical_url'] ) ) {
+		// Build the URL in the address bar.
+		$requested_url  = bp_get_requested_url();
+
+		// Stash query args.
+		$url_stack      = explode( '?', $requested_url );
+
+		// Build the canonical URL out of the redirect stack.
+		if ( isset( $bp->canonical_stack['base_url'] ) )
+			$url_stack[0] = $bp->canonical_stack['base_url'];
+
+		if ( isset( $bp->canonical_stack['component'] ) )
+			$url_stack[0] = trailingslashit( $url_stack[0] . $bp->canonical_stack['component'] );
+
+		if ( isset( $bp->canonical_stack['action'] ) )
+			$url_stack[0] = trailingslashit( $url_stack[0] . $bp->canonical_stack['action'] );
+
+		if ( !empty( $bp->canonical_stack['action_variables'] ) ) {
+			foreach( (array) $bp->canonical_stack['action_variables'] as $av ) {
+				$url_stack[0] = trailingslashit( $url_stack[0] . $av );
+			}
+		}
+
+		// Add trailing slash.
+		$url_stack[0] = trailingslashit( $url_stack[0] );
+
+		// Stash in the $bp global.
+		$bp->canonical_stack['canonical_url'] = implode( '?', $url_stack );
+	}
+
+	$canonical_url = $bp->canonical_stack['canonical_url'];
+
+	if ( !$include_query_args ) {
+		$canonical_url = array_reverse( explode( '?', $canonical_url ) );
+		$canonical_url = array_pop( $canonical_url );
+	}
+
+	/**
+	 * Filters the canonical url of the current page.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $canonical_url Canonical URL of the current page.
+	 * @param array  $args          Array of arguments to help determine canonical URL.
+	 */
+	return apply_filters( 'bp_get_canonical_url', $canonical_url, $args );
+}
+
+/**
+ * Return the URL as requested on the current page load by the user agent.
+ *
+ * @since 1.6.0
+ *
+ * @return string Requested URL string.
+ */
+function bp_get_requested_url() {
+	$bp = buddypress();
+
+	if ( empty( $bp->canonical_stack['requested_url'] ) ) {
+		$bp->canonical_stack['requested_url']  = is_ssl() ? 'https://' : 'http://';
+		$bp->canonical_stack['requested_url'] .= $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+	}
+
+	/**
+	 * Filters the URL as requested on the current page load by the user agent.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $value Requested URL string.
+	 */
+	return apply_filters( 'bp_get_requested_url', $bp->canonical_stack['requested_url'] );
+}
+
+/**
+ * Remove WP's canonical redirect when we are trying to load BP-specific content.
+ *
+ * Avoids issues with WordPress thinking that a BuddyPress URL might actually
+ * be a blog post or page.
+ *
+ * This function should be considered temporary, and may be removed without
+ * notice in future versions of BuddyPress.
+ *
+ * @since 1.6.0
+ */
+function _bp_maybe_remove_redirect_canonical() {
+	if ( ! bp_is_blog_page() )
+		remove_action( 'template_redirect', 'redirect_canonical' );
+}
+add_action( 'bp_init', '_bp_maybe_remove_redirect_canonical' );
+
+/**
+ * Rehook maybe_redirect_404() to run later than the default.
+ *
+ * WordPress's maybe_redirect_404() allows admins on a multisite installation
+ * to define 'NOBLOGREDIRECT', a URL to which 404 requests will be redirected.
+ * maybe_redirect_404() is hooked to template_redirect at priority 10, which
+ * creates a race condition with bp_template_redirect(), our piggyback hook.
+ * Due to a legacy bug in BuddyPress, internal BP content (such as members and
+ * groups) is marked 404 in $wp_query until bp_core_load_template(), when BP
+ * manually overrides the automatic 404. However, the race condition with
+ * maybe_redirect_404() means that this manual un-404-ing doesn't happen in
+ * time, with the results that maybe_redirect_404() thinks that the page is
+ * a legitimate 404, and redirects incorrectly to NOBLOGREDIRECT.
+ *
+ * By switching maybe_redirect_404() to catch at a higher priority, we avoid
+ * the race condition. If bp_core_load_template() runs, it dies before reaching
+ * maybe_redirect_404(). If bp_core_load_template() does not run, it means that
+ * the 404 is legitimate, and maybe_redirect_404() can proceed as expected.
+ *
+ * This function will be removed in a later version of BuddyPress. Plugins
+ * (and plugin authors!) should ignore it.
+ *
+ * @since 1.6.1
+ *
+ * @link https://buddypress.trac.wordpress.org/ticket/4329
+ * @link https://buddypress.trac.wordpress.org/ticket/4415
+ */
+function _bp_rehook_maybe_redirect_404() {
+	if ( defined( 'NOBLOGREDIRECT' ) && is_multisite() ) {
+		remove_action( 'template_redirect', 'maybe_redirect_404' );
+		add_action( 'template_redirect', 'maybe_redirect_404', 100 );
+	}
+}
+add_action( 'template_redirect', '_bp_rehook_maybe_redirect_404', 1 );
+
+/**
+ * Remove WP's rel=canonical HTML tag if we are trying to load BP-specific content.
+ *
+ * This function should be considered temporary, and may be removed without
+ * notice in future versions of BuddyPress.
+ *
+ * @since 1.6.0
+ */
+function _bp_maybe_remove_rel_canonical() {
+	if ( ! bp_is_blog_page() && ! is_404() ) {
+		remove_action( 'wp_head', 'rel_canonical' );
+		add_action( 'bp_head', 'bp_rel_canonical' );
+	}
+}
+add_action( 'wp_head', '_bp_maybe_remove_rel_canonical', 8 );
+
+/**
+ * Stop WordPress performing a DB query for its main loop.
+ *
+ * As of WordPress 4.6, it is possible to bypass the main WP_Query entirely.
+ * This saves us one unnecessary database query! :)
+ *
+ * @since 2.7.0
+ *
+ * @param  null     $retval Current return value for filter.
+ * @param  WP_Query $query  Current WordPress query object.
+ * @return null|array
+ */
+function bp_core_filter_wp_query( $retval, $query ) {
+	if ( ! $query->is_main_query() ) {
+		return $retval;
+	}
+
+	/*
+	 * If not on a BP single page, bail.
+	 * Too early to use bp_is_single_item(), so use BP conditionals.
+	 */
+	if ( false === ( bp_is_group() || bp_is_user() || bp_is_single_activity() ) ) {
+		return $retval;
+	}
+
+	// Set default properties as recommended in the 'posts_pre_query' DocBlock.
+	$query->found_posts   = 0;
+	$query->max_num_pages = 0;
+
+	// Return something other than a null value to bypass WP_Query.
+	return array();
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-component.php	(revision 12265)
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( ! class_exists( 'BP_Component' ) ) {
+	require dirname( __FILE__ ) . '/classes/class-bp-component.php';
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-cssjs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-cssjs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-cssjs.php	(revision 12265)
@@ -0,0 +1,565 @@
+<?php
+/**
+ * Core component CSS & JS.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register scripts commonly used by BuddyPress.
+ *
+ * @since 2.1.0
+ */
+function bp_core_register_common_scripts() {
+	$min = bp_core_get_minified_asset_suffix();
+	$url = buddypress()->plugin_url . 'bp-core/js/';
+
+	/*
+	 * Moment.js locale.
+	 *
+	 * Try to map current WordPress locale to a moment.js locale file for loading.
+	 *
+	 * eg. French (France) locale for WP is fr_FR. Here, we try to find fr-fr.js
+	 *     (this file doesn't exist).
+	 */
+	$locale = sanitize_file_name( strtolower( get_locale() ) );
+	$locale = str_replace( '_', '-', $locale );
+	if ( file_exists( buddypress()->core->path . "bp-core/js/vendor/moment-js/locale/{$locale}{$min}.js" ) ) {
+		$moment_locale_url = $url . "vendor/moment-js/locale/{$locale}{$min}.js";
+
+	/*
+	 * Try to find the short-form locale.
+	 *
+	 * eg. French (France) locale for WP is fr_FR. Here, we try to find fr.js
+	 *     (this exists).
+	 */
+	} else {
+		$locale = substr( $locale, 0, strpos( $locale, '-' ) );
+		if ( file_exists( buddypress()->core->path . "bp-core/js/vendor/moment-js/locale/{$locale}{$min}.js" ) ) {
+			$moment_locale_url = $url . "vendor/moment-js/locale/{$locale}{$min}.js";
+		}
+	}
+
+	// Set up default scripts to register.
+	$scripts = array(
+		// Legacy.
+		'bp-confirm'        => array( 'file' => "{$url}confirm{$min}.js", 'dependencies' => array( 'jquery' ), 'footer' => false ),
+		'bp-widget-members' => array( 'file' => "{$url}widget-members{$min}.js", 'dependencies' => array( 'jquery' ), 'footer' => false ),
+		'bp-jquery-query'   => array( 'file' => "{$url}jquery-query{$min}.js", 'dependencies' => array( 'jquery' ), 'footer' => false ),
+		'bp-jquery-cookie'  => array( 'file' => "{$url}vendor/jquery-cookie{$min}.js", 'dependencies' => array( 'jquery' ), 'footer' => false ),
+		'bp-jquery-scroll-to' => array( 'file' => "{$url}vendor/jquery-scroll-to{$min}.js", 'dependencies' => array( 'jquery' ), 'footer' => false ),
+
+		// Version 2.1.
+		'jquery-caret' => array( 'file' => "{$url}vendor/jquery.caret{$min}.js", 'dependencies' => array( 'jquery' ), 'footer' => true ),
+		'jquery-atwho' => array( 'file' => "{$url}vendor/jquery.atwho{$min}.js", 'dependencies' => array( 'jquery', 'jquery-caret' ), 'footer' => true ),
+
+		// Version 2.3.
+		'bp-plupload' => array( 'file' => "{$url}bp-plupload{$min}.js", 'dependencies' => array( 'plupload', 'jquery', 'json2', 'wp-backbone' ), 'footer' => true ),
+		'bp-avatar'   => array( 'file' => "{$url}avatar{$min}.js", 'dependencies' => array( 'jcrop' ), 'footer' => true ),
+		'bp-webcam'   => array( 'file' => "{$url}webcam{$min}.js", 'dependencies' => array( 'bp-avatar' ), 'footer' => true ),
+
+		// Version 2.4.
+		'bp-cover-image' => array( 'file' => "{$url}cover-image{$min}.js", 'dependencies' => array(), 'footer' => true ),
+
+		// Version 2.7.
+		'bp-moment'    => array( 'file' => "{$url}vendor/moment-js/moment{$min}.js", 'dependencies' => array(), 'footer' => true ),
+		'bp-livestamp' => array( 'file' => "{$url}vendor/livestamp{$min}.js", 'dependencies' => array( 'jquery', 'bp-moment' ), 'footer' => true ),
+	);
+
+	// Version 2.7 - Add Moment.js locale to our $scripts array if we found one.
+	if ( isset( $moment_locale_url ) ) {
+		$scripts['bp-moment-locale'] = array( 'file' => esc_url( $moment_locale_url ), 'dependencies' => array( 'bp-moment' ), 'footer' => true );
+	}
+
+	/**
+	 * Filters the BuddyPress Core javascript files to register.
+	 *
+	 * Default handles include 'bp-confirm', 'bp-widget-members',
+	 * 'bp-jquery-query', 'bp-jquery-cookie', and 'bp-jquery-scroll-to'.
+	 *
+	 * @since 2.1.0 'jquery-caret', 'jquery-atwho' added.
+	 * @since 2.3.0 'bp-plupload', 'bp-avatar', 'bp-webcam' added.
+	 * @since 2.4.0 'bp-cover-image' added.
+	 * @since 2.7.0 'bp-moment', 'bp-livestamp' added.
+	 *              'bp-moment-locale' is added conditionally if a moment.js locale file is found.
+	 *
+	 * @param array $value Array of javascript file information to register.
+	 */
+	$scripts = apply_filters( 'bp_core_register_common_scripts', $scripts );
+
+
+	$version = bp_get_version();
+	foreach ( $scripts as $id => $script ) {
+		wp_register_script( $id, $script['file'], $script['dependencies'], $version, $script['footer'] );
+	}
+}
+add_action( 'bp_enqueue_scripts',       'bp_core_register_common_scripts', 1 );
+add_action( 'bp_admin_enqueue_scripts', 'bp_core_register_common_scripts', 1 );
+
+/**
+ * Register styles commonly used by BuddyPress.
+ *
+ * @since 2.1.0
+ */
+function bp_core_register_common_styles() {
+	$min = bp_core_get_minified_asset_suffix();
+	$url = buddypress()->plugin_url . 'bp-core/css/';
+
+	/**
+	 * Filters the URL for the Admin Bar stylesheet.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string $value URL for the Admin Bar stylesheet.
+	 */
+	$admin_bar_file = apply_filters( 'bp_core_admin_bar_css', "{$url}admin-bar{$min}.css" );
+
+	/**
+	 * Filters the BuddyPress Core stylesheet files to register.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $value Array of stylesheet file information to register.
+	 */
+	$styles = apply_filters( 'bp_core_register_common_styles', array(
+		'bp-admin-bar' => array(
+			'file'         => $admin_bar_file,
+			'dependencies' => array( 'admin-bar' )
+		),
+		'bp-avatar' => array(
+			'file'         => "{$url}avatar{$min}.css",
+			'dependencies' => array( 'jcrop' )
+		),
+	) );
+
+	foreach ( $styles as $id => $style ) {
+		wp_register_style( $id, $style['file'], $style['dependencies'], bp_get_version() );
+
+		wp_style_add_data( $id, 'rtl', true );
+		if ( $min ) {
+			wp_style_add_data( $id, 'suffix', $min );
+		}
+	}
+}
+add_action( 'bp_enqueue_scripts',       'bp_core_register_common_styles', 1 );
+add_action( 'bp_admin_enqueue_scripts', 'bp_core_register_common_styles', 1 );
+
+/**
+ * Load the JS for "Are you sure?" confirm links.
+ *
+ * @since 1.1.0
+ */
+function bp_core_confirmation_js() {
+	if ( is_multisite() && ! bp_is_root_blog() ) {
+		return false;
+	}
+
+	wp_enqueue_script( 'bp-confirm' );
+
+	wp_localize_script( 'bp-confirm', 'BP_Confirm', array(
+		'are_you_sure' => __( 'Are you sure?', 'buddypress' ),
+	) );
+
+}
+add_action( 'bp_enqueue_scripts',       'bp_core_confirmation_js' );
+add_action( 'bp_admin_enqueue_scripts', 'bp_core_confirmation_js' );
+
+/**
+ * Enqueues the css and js required by the Avatar UI.
+ *
+ * @since 2.3.0
+ */
+function bp_core_avatar_scripts() {
+	if ( ! bp_avatar_is_front_edit() ) {
+		return false;
+	}
+
+	// Enqueue the Attachments scripts for the Avatar UI.
+	bp_attachments_enqueue_scripts( 'BP_Attachment_Avatar' );
+
+	// Add Some actions for Theme backcompat.
+	add_action( 'bp_after_profile_avatar_upload_content', 'bp_avatar_template_check' );
+	add_action( 'bp_after_group_admin_content',           'bp_avatar_template_check' );
+	add_action( 'bp_after_group_avatar_creation_step',    'bp_avatar_template_check' );
+}
+add_action( 'bp_enqueue_scripts', 'bp_core_avatar_scripts' );
+
+/**
+ * Enqueues the css and js required by the Cover Image UI.
+ *
+ * @since 2.4.0
+ */
+function bp_core_cover_image_scripts() {
+	if ( ! bp_attachments_cover_image_is_edit() ) {
+		return false;
+	}
+
+	// Enqueue the Attachments scripts for the Cover Image UI.
+	bp_attachments_enqueue_scripts( 'BP_Attachment_Cover_Image' );
+}
+add_action( 'bp_enqueue_scripts', 'bp_core_cover_image_scripts' );
+
+/**
+ * Enqueues jCrop library and hooks BP's custom cropper JS.
+ *
+ * @since 1.1.0
+ */
+function bp_core_add_jquery_cropper() {
+	wp_enqueue_style( 'jcrop' );
+	wp_enqueue_script( 'jcrop', array( 'jquery' ) );
+	add_action( 'wp_head', 'bp_core_add_cropper_inline_js' );
+	add_action( 'wp_head', 'bp_core_add_cropper_inline_css' );
+}
+
+/**
+ * Output the inline JS needed for the cropper to work on a per-page basis.
+ *
+ * @since 1.1.0
+ */
+function bp_core_add_cropper_inline_js() {
+
+	/**
+	 * Filters the return value of getimagesize to determine if an image was uploaded.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array $value Array of data found by getimagesize.
+	 */
+	$image = apply_filters( 'bp_inline_cropper_image', getimagesize( bp_core_avatar_upload_path() . buddypress()->avatar_admin->image->dir ) );
+	if ( empty( $image ) ) {
+		return;
+	}
+
+	// Get avatar full width and height.
+	$full_height = bp_core_avatar_full_height();
+	$full_width  = bp_core_avatar_full_width();
+
+	// Calculate Aspect Ratio.
+	if ( !empty( $full_height ) && ( $full_width != $full_height ) ) {
+		$aspect_ratio = $full_width / $full_height;
+	} else {
+		$aspect_ratio = 1;
+	}
+
+	// Default cropper coordinates.
+	// Smaller than full-width: cropper defaults to entire image.
+	if ( $image[0] < $full_width ) {
+		$crop_left  = 0;
+		$crop_right = $image[0];
+
+	// Less than 2x full-width: cropper defaults to full-width.
+	} elseif ( $image[0] < ( $full_width * 2 ) ) {
+		$padding_w  = round( ( $image[0] - $full_width ) / 2 );
+		$crop_left  = $padding_w;
+		$crop_right = $image[0] - $padding_w;
+
+	// Larger than 2x full-width: cropper defaults to 1/2 image width.
+	} else {
+		$crop_left  = round( $image[0] / 4 );
+		$crop_right = $image[0] - $crop_left;
+	}
+
+	// Smaller than full-height: cropper defaults to entire image.
+	if ( $image[1] < $full_height ) {
+		$crop_top    = 0;
+		$crop_bottom = $image[1];
+
+	// Less than double full-height: cropper defaults to full-height.
+	} elseif ( $image[1] < ( $full_height * 2 ) ) {
+		$padding_h   = round( ( $image[1] - $full_height ) / 2 );
+		$crop_top    = $padding_h;
+		$crop_bottom = $image[1] - $padding_h;
+
+	// Larger than 2x full-height: cropper defaults to 1/2 image height.
+	} else {
+		$crop_top    = round( $image[1] / 4 );
+		$crop_bottom = $image[1] - $crop_top;
+	}
+
+	?>
+
+	<script type="text/javascript">
+		jQuery(window).load( function(){
+			jQuery('#avatar-to-crop').Jcrop({
+				onChange: showPreview,
+				onSelect: updateCoords,
+				aspectRatio: <?php echo (int) $aspect_ratio; ?>,
+				setSelect: [ <?php echo (int) $crop_left; ?>, <?php echo (int) $crop_top; ?>, <?php echo (int) $crop_right; ?>, <?php echo (int) $crop_bottom; ?> ]
+			});
+		});
+
+		function updateCoords(c) {
+			jQuery('#x').val(c.x);
+			jQuery('#y').val(c.y);
+			jQuery('#w').val(c.w);
+			jQuery('#h').val(c.h);
+		}
+
+		function showPreview(coords) {
+			if ( parseInt(coords.w) > 0 ) {
+				var fw = <?php echo (int) $full_width; ?>;
+				var fh = <?php echo (int) $full_height; ?>;
+				var rx = fw / coords.w;
+				var ry = fh / coords.h;
+
+				jQuery( '#avatar-crop-preview' ).css({
+					width: Math.round(rx * <?php echo (int) $image[0]; ?>) + 'px',
+					height: Math.round(ry * <?php echo (int) $image[1]; ?>) + 'px',
+					marginLeft: '-' + Math.round(rx * coords.x) + 'px',
+					marginTop: '-' + Math.round(ry * coords.y) + 'px'
+				});
+			}
+		}
+	</script>
+
+<?php
+}
+
+/**
+ * Output the inline CSS for the BP image cropper.
+ *
+ * @since 1.1.0
+ */
+function bp_core_add_cropper_inline_css() {
+?>
+
+	<style type="text/css">
+		.jcrop-holder { float: left; margin: 0 20px 20px 0; text-align: left; }
+		#avatar-crop-pane { width: <?php echo bp_core_avatar_full_width() ?>px; height: <?php echo bp_core_avatar_full_height() ?>px; overflow: hidden; }
+		#avatar-crop-submit { margin: 20px 0; }
+		.jcrop-holder img,
+		#avatar-crop-pane img,
+		#avatar-upload-form img,
+		#create-group-form img,
+		#group-settings-form img { border: none !important; max-width: none !important; }
+	</style>
+
+<?php
+}
+
+/**
+ * Define the 'ajaxurl' JS variable, used by themes as an AJAX endpoint.
+ *
+ * @since 1.1.0
+ */
+function bp_core_add_ajax_url_js() {
+?>
+
+	<script type="text/javascript">var ajaxurl = '<?php echo bp_core_ajax_url(); ?>';</script>
+
+<?php
+}
+add_action( 'wp_head', 'bp_core_add_ajax_url_js' );
+
+/**
+ * Get the proper value for BP's ajaxurl.
+ *
+ * Designed to be sensitive to FORCE_SSL_ADMIN and non-standard multisite
+ * configurations.
+ *
+ * @since 1.7.0
+ *
+ * @return string AJAX endpoint URL.
+ */
+function bp_core_ajax_url() {
+
+	/**
+	 * Filters the proper value for BuddyPress' ajaxurl.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $value Proper ajaxurl value for BuddyPress.
+	 */
+	return apply_filters( 'bp_core_ajax_url', admin_url( 'admin-ajax.php', is_ssl() ? 'admin' : 'http' ) );
+}
+
+/**
+ * Get the JavaScript dependencies for buddypress.js.
+ *
+ * @since 2.0.0
+ *
+ * @return array The JavaScript dependencies.
+ */
+function bp_core_get_js_dependencies() {
+
+	/**
+	 * Filters the javascript dependencies for buddypress.js.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $value Array of javascript dependencies for buddypress.js.
+	 */
+	return apply_filters( 'bp_core_get_js_dependencies', array(
+		'jquery',
+		'bp-confirm',
+		'bp-widget-members',
+		'bp-jquery-query',
+		'bp-jquery-cookie',
+		'bp-jquery-scroll-to'
+	) );
+}
+
+/**
+ * Add inline css to display the component's single item cover image.
+ *
+ * @since 2.4.0
+ *
+ * @param bool $return True to get the inline css.
+ * @return null|array|false The inline css or an associative array containing
+ *                          the css rules and the style handle.
+ */
+function bp_add_cover_image_inline_css( $return = false ) {
+	$bp = buddypress();
+
+	// Find the component of the current item.
+	if ( bp_is_user() ) {
+
+		// User is not allowed to upload cover images
+		// no need to carry on.
+		if ( bp_disable_cover_image_uploads() ) {
+			return;
+		}
+
+		$cover_image_object = array(
+			'component' => 'xprofile',
+			'object' => $bp->displayed_user
+		);
+	} elseif ( bp_is_group() ) {
+
+		// Users are not allowed to upload cover images for their groups
+		// no need to carry on.
+		if ( bp_disable_group_cover_image_uploads() ) {
+			return;
+		}
+
+		$cover_image_object = array(
+			'component' =>'groups',
+			'object' => $bp->groups->current_group
+		);
+	} else {
+		$cover_image_object = apply_filters( 'bp_current_cover_image_object_inline_css', array() );
+	}
+
+	// Bail if no component were found.
+	if ( empty( $cover_image_object['component'] ) || empty( $cover_image_object['object'] ) || ! bp_is_active( $cover_image_object['component'], 'cover_image' ) ) {
+		return;
+	}
+
+	// Get the settings of the cover image feature for the current component.
+	$params = bp_attachments_get_cover_image_settings( $cover_image_object['component'] );
+
+	// Bail if no params.
+	if ( empty( $params ) ) {
+		return;
+	}
+
+	// Try to call the callback.
+	if ( is_callable( $params['callback'] ) ) {
+
+		$object_dir = $cover_image_object['component'];
+
+		if ( 'xprofile' === $object_dir ) {
+			$object_dir = 'members';
+		}
+
+		$cover_image = bp_attachments_get_attachment( 'url', array(
+			'object_dir' => $object_dir,
+			'item_id'    => $cover_image_object['object']->id,
+		) );
+
+		if ( empty( $cover_image ) ) {
+			if ( ! empty( $params['default_cover'] ) ) {
+				$cover_image = $params['default_cover'];
+			}
+		}
+
+		$inline_css = call_user_func_array( $params['callback'], array( array(
+			'cover_image' => esc_url_raw( $cover_image ),
+			'component'   => sanitize_key( $cover_image_object['component'] ),
+			'object_id'   => (int) $cover_image_object['object']->id,
+			'width'       => (int) $params['width'],
+			'height'      => (int) $params['height'],
+		) ) );
+
+		// Finally add the inline css to the handle.
+		if ( ! empty( $inline_css ) ) {
+
+			// Used to get the css when Ajax setting the cover image.
+			if ( true === $return ) {
+				return array(
+					'css_rules' => '<style type="text/css">' . "\n" . $inline_css . "\n" . '</style>',
+					'handle'    => $params['theme_handle'],
+				);
+			}
+
+			wp_add_inline_style( $params['theme_handle'], $inline_css );
+		} else {
+			return false;
+		}
+	}
+}
+add_action( 'bp_enqueue_scripts', 'bp_add_cover_image_inline_css', 11 );
+
+/**
+ * Enqueues livestamp.js on BuddyPress pages.
+ *
+ * @since 2.7.0
+ */
+function bp_core_add_livestamp() {
+	if ( ! is_buddypress() ) {
+		return;
+	}
+
+	bp_core_enqueue_livestamp();
+}
+add_action( 'bp_enqueue_scripts', 'bp_core_add_livestamp' );
+
+/**
+ * Enqueue and localize livestamp.js script.
+ *
+ * @since 2.7.0
+ */
+function bp_core_enqueue_livestamp() {
+	// If bp-livestamp isn't enqueued, do it now.
+	if ( wp_script_is( 'bp-livestamp' ) ) {
+		return;
+	}
+
+	/*
+	 * Only enqueue Moment.js locale if we registered it in
+	 * bp_core_register_common_scripts().
+	 */
+	if ( wp_script_is( 'bp-moment-locale', 'registered' ) ) {
+		wp_enqueue_script( 'bp-moment-locale' );
+		wp_add_inline_script ( 'bp-livestamp', bp_core_moment_js_config() );
+	}
+
+	wp_enqueue_script( 'bp-livestamp' );
+}
+
+/**
+ * Return moment.js config.
+ *
+ * @since 2.7.0
+ *
+ * @return string
+ */
+function bp_core_moment_js_config() {
+	// Grab the locale from the enqueued JS.
+	$moment_locale = wp_scripts()->query( 'bp-moment-locale' );
+	$moment_locale = substr( $moment_locale->src, strpos( $moment_locale->src, '/moment-js/locale/' ) + 18 );
+	$moment_locale = str_replace( '.js', '', $moment_locale );
+
+	$inline_js = <<<EOD
+jQuery(function() {
+	moment.locale( '{$moment_locale}' );
+});
+EOD;
+
+	return $inline_js;
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-customizer-email.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-customizer-email.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-customizer-email.php	(revision 12265)
@@ -0,0 +1,428 @@
+<?php
+/**
+ * BuddyPress Customizer implementation for email.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Initialize the Customizer for emails.
+ *
+ * @since 2.5.0
+ *
+ * @param WP_Customize_Manager $wp_customize The Customizer object.
+ */
+function bp_email_init_customizer( WP_Customize_Manager $wp_customize ) {
+	if ( ! bp_is_email_customizer() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) {
+		return;
+	}
+
+	$wp_customize->add_panel( 'bp_mailtpl', array(
+		'description' => __( 'Customize the appearance of emails sent by BuddyPress.', 'buddypress' ),
+		'title'       => _x( 'BuddyPress Emails', 'screen heading', 'buddypress' ),
+	) );
+
+	$sections = bp_email_get_customizer_sections();
+	foreach( $sections as $section_id => $args ) {
+		$wp_customize->add_section( $section_id, $args );
+	}
+
+	$settings = bp_email_get_customizer_settings();
+	foreach( $settings as $setting_id => $args ) {
+		$wp_customize->add_setting( $setting_id, $args );
+	}
+
+	/**
+	 * Fires to let plugins register extra Customizer controls for emails.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param WP_Customize_Manager $wp_customize The Customizer object.
+	 */
+	do_action( 'bp_email_customizer_register_sections', $wp_customize );
+
+	$controls = bp_email_get_customizer_controls();
+	foreach ( $controls as $control_id => $args ) {
+		$wp_customize->add_control( new $args['class']( $wp_customize, $control_id, $args ) );
+	}
+
+	/*
+	 * Hook actions/filters for further configuration.
+	 */
+
+	add_filter( 'customize_section_active', 'bp_email_customizer_hide_sections', 12, 2 );
+
+	if ( is_customize_preview() ) {
+		/*
+		 * Enqueue scripts/styles for the Customizer's preview window.
+		 *
+		 * Scripts can't be registered in bp_core_register_common_styles() etc because
+		 * the Customizer loads very, very early.
+		 */
+		$bp  = buddypress();
+		$min = bp_core_get_minified_asset_suffix();
+
+		wp_enqueue_script(
+			'bp-customizer-receiver-emails',
+			"{$bp->plugin_url}bp-core/admin/js/customizer-receiver-emails{$min}.js",
+			array( 'customize-preview' ),
+			bp_get_version(),
+			true
+		);
+
+		// Include the preview loading style.
+		add_action( 'wp_footer', array( $wp_customize, 'customize_preview_loading_style' ) );
+	}
+}
+add_action( 'bp_customize_register', 'bp_email_init_customizer' );
+
+/**
+ * Are we looking at the email customizer?
+ *
+ * @since 2.5.0
+ *
+ * @return bool
+ */
+function bp_is_email_customizer() {
+	return isset( $_GET['bp_customizer'] ) && $_GET['bp_customizer'] === 'email';
+}
+
+/**
+ * Only show email sections in the Customizer.
+ *
+ * @since 2.5.0
+ *
+ * @param bool                 $active  Whether the Customizer section is active.
+ * @param WP_Customize_Section $section {@see WP_Customize_Section} instance.
+ * @return bool
+ */
+function bp_email_customizer_hide_sections( $active, $section ) {
+	if ( ! bp_is_email_customizer() ) {
+		return $active;
+	}
+
+	return in_array( $section->id, array_keys( bp_email_get_customizer_sections() ), true );
+}
+
+/**
+ * Get Customizer sections for emails.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_email_get_customizer_sections() {
+
+	/**
+	 * Filter Customizer sections for emails.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $sections Email Customizer sections to add.
+	 */
+	return apply_filters( 'bp_email_get_customizer_sections', array(
+		'section_bp_mailtpl_header' => array(
+			'capability' => 'bp_moderate',
+			'panel'      => 'bp_mailtpl',
+			'title'      => _x( 'Header', 'email', 'buddypress' ),
+		),
+		'section_bp_mailtpl_body' => array(
+			'capability' => 'bp_moderate',
+			'panel'      => 'bp_mailtpl',
+			'title'      => _x( 'Body', 'email', 'buddypress' ),
+		),
+		'section_bp_mailtpl_footer' => array(
+			'capability' => 'bp_moderate',
+			'panel'      => 'bp_mailtpl',
+			'title'      => _x( 'Footer', 'email', 'buddypress' ),
+		),
+	) );
+}
+
+/**
+ * Get Customizer settings for emails.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_email_get_customizer_settings() {
+	$defaults = bp_email_get_appearance_settings();
+
+	/**
+	 * Filter Customizer settings for emails.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $settings Email Customizer settings to add.
+	 */
+	return apply_filters( 'bp_email_get_customizer_settings', array(
+		'bp_email_options[email_bg]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['email_bg'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[header_bg]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['header_bg'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[header_text_size]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['header_text_size'],
+			'sanitize_callback' => 'absint',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[header_text_color]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['header_text_color'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[highlight_color]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['highlight_color'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[body_bg]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['body_bg'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[body_text_size]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['body_text_size'],
+			'sanitize_callback' => 'absint',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[body_text_color]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['body_text_color'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[footer_text]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['footer_text'],
+			'sanitize_callback' => 'wp_filter_post_kses',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[footer_bg]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['footer_bg'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[footer_text_size]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['footer_text_size'],
+			'sanitize_callback' => 'absint',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+		'bp_email_options[footer_text_color]' => array(
+			'capability'        => 'bp_moderate',
+			'default'           => $defaults['footer_text_color'],
+			'sanitize_callback' => 'sanitize_hex_color',
+			'transport'         => 'postMessage',
+			'type'              => 'option',
+		),
+	) );
+}
+
+/**
+ * Get Customizer controls for emails.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_email_get_customizer_controls() {
+
+	/**
+	 * Filter Customizer controls for emails.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $controls Email Customizer controls to add.
+	 */
+	return apply_filters( 'bp_email_get_customizer_controls', array(
+		'bp_mailtpl_email_bg' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Email background color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_header',
+			'settings' => 'bp_email_options[email_bg]',
+		),
+
+		'bp_mailtpl_header_bg' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Header background color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_header',
+			'settings' => 'bp_email_options[header_bg]',
+		),
+
+		'bp_mailtpl_highlight_color' => array(
+			'class'       => 'WP_Customize_Color_Control',
+			'description' => __( 'Applied to links and other decorative areas.', 'buddypress' ),
+			'label'       => __( 'Highlight color', 'buddypress' ),
+			'section'     => 'section_bp_mailtpl_header',
+			'settings'    => 'bp_email_options[highlight_color]',
+		),
+
+		'bp_mailtpl_header_text_color' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Text color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_header',
+			'settings' => 'bp_email_options[header_text_color]',
+		),
+
+		'bp_mailtpl_header_text_size' => array(
+			'class'    => 'BP_Customizer_Control_Range',
+			'label'    => __( 'Text size', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_header',
+			'settings' => 'bp_email_options[header_text_size]',
+
+			'input_attrs' => array(
+				'max'  => 100,
+				'min'  => 1,
+				'step' => 1,
+			),
+		),
+
+
+		'bp_mailtpl_body_bg' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Background color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_body',
+			'settings' => 'bp_email_options[body_bg]',
+		),
+
+
+		'bp_mailtpl_body_text_color' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Text color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_body',
+			'settings' => 'bp_email_options[body_text_color]',
+		),
+
+		'bp_mailtpl_body_text_size' => array(
+			'class'    => 'BP_Customizer_Control_Range',
+			'label'    => __( 'Text size', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_body',
+			'settings' => 'bp_email_options[body_text_size]',
+
+			'input_attrs' => array(
+				'max'  => 24,
+				'min'  => 8,
+				'step' => 1,
+			),
+		),
+
+
+		'bp_mailtpl_footer_text' => array(
+			'class'       => 'WP_Customize_Control',
+			'description' => __('Change the email footer here', 'buddypress' ),
+			'label'       => __( 'Footer text', 'buddypress' ),
+			'section'     => 'section_bp_mailtpl_footer',
+			'settings'    => 'bp_email_options[footer_text]',
+			'type'        => 'textarea',
+		),
+
+		'bp_mailtpl_footer_bg' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Background color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_footer',
+			'settings' => 'bp_email_options[footer_bg]',
+		),
+
+		'bp_mailtpl_footer_text_color' => array(
+			'class'    => 'WP_Customize_Color_Control',
+			'label'    => __( 'Text color', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_footer',
+			'settings' => 'bp_email_options[footer_text_color]',
+		),
+
+		'bp_mailtpl_footer_text_size' => array(
+			'class'    => 'BP_Customizer_Control_Range',
+			'label'    => __( 'Text size', 'buddypress' ),
+			'section'  => 'section_bp_mailtpl_footer',
+			'settings' => 'bp_email_options[footer_text_size]',
+
+			'input_attrs' => array(
+				'max'  => 24,
+				'min'  => 8,
+				'step' => 1,
+			),
+		),
+	) );
+}
+
+/**
+ * Implements a JS redirect to the Customizer, previewing a randomly selected email.
+ *
+ * @since 2.5.0
+ */
+function bp_email_redirect_to_customizer() {
+	$switched = false;
+
+	// Switch to the root blog, where the email posts live.
+	if ( ! bp_is_root_blog() ) {
+		switch_to_blog( bp_get_root_blog_id() );
+		$switched = true;
+	}
+
+	$email = get_posts( array(
+		'fields'           => 'ids',
+		'orderby'          => 'rand',
+		'post_status'      => 'publish',
+		'post_type'        => bp_get_email_post_type(),
+		'posts_per_page'   => 1,
+		'suppress_filters' => false,
+	) );
+
+	$preview_url = admin_url();
+
+	if ( $email ) {
+		$preview_url = get_post_permalink( $email[0] ) . '&bp_customizer=email';
+	}
+
+	$redirect_url = add_query_arg(
+		array(
+			'autofocus[panel]' => 'bp_mailtpl',
+			'bp_customizer'    => 'email',
+			'return'           => rawurlencode( admin_url() ),
+			'url'              => rawurlencode( $preview_url ),
+		),
+		admin_url( 'customize.php' )
+	);
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	printf(
+		'<script type="text/javascript">window.location = "%s";</script>',
+		esc_url_raw( $redirect_url )
+	);
+
+	exit;
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-dependency.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-dependency.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-dependency.php	(revision 12265)
@@ -0,0 +1,770 @@
+<?php
+/**
+ * Plugin Dependency Action Hooks.
+ *
+ * The purpose of the following hooks is to mimic the behavior of something
+ * called 'plugin dependency' which enables a plugin to have plugins of their
+ * own in a safe and reliable way.
+ *
+ * We do this in BuddyPress by mirroring existing WordPress hooks in many places
+ * allowing dependant plugins to hook into the BuddyPress specific ones, thus
+ * guaranteeing proper code execution only when BuddyPress is active.
+ *
+ * The following functions are wrappers for hooks, allowing them to be
+ * manually called and/or piggy-backed on top of other hooks if needed.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.7.0
+ */
+
+/**
+ * Fire the 'bp_include' action, where plugins should include files.
+ *
+ * @since 1.2.5
+ */
+function bp_include() {
+
+	/**
+	 * Fires inside the 'bp_include' function, where plugins should include files.
+	 *
+	 * @since 1.2.5
+	 */
+	do_action( 'bp_include' );
+}
+
+/**
+ * Fire the 'bp_late_include' action for loading conditional files.
+ *
+ * @since 3.0.0
+ */
+function bp_late_include() {
+
+	/**
+	 * Fires the 'bp_late_include' action.
+	 *
+	 * Allow for conditional includes on certain pages.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_late_include' );
+}
+
+/**
+ * Fire the 'bp_setup_components' action, where plugins should initialize components.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_components() {
+
+	/**
+	 * Fires inside the 'bp_setup_components' function, where plugins should initialize components.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_setup_components' );
+}
+
+/**
+ * Fire the 'bp_setup_canonical_stack' action, where plugins should set up their canonical URL.
+ *
+ * @since 2.1.0
+ */
+function bp_setup_canonical_stack() {
+
+	/**
+	 * Fires inside the 'bp_setup_canonical_stack' function, where plugins should set up their canonical URL.
+	 *
+	 * @since 2.1.0
+	 */
+	do_action( 'bp_setup_canonical_stack' );
+}
+
+/**
+ * Fire the 'bp_register_taxonomies' action, where plugins should register taxonomies.
+ *
+ * @since 2.2.0
+ */
+function bp_register_taxonomies() {
+
+	/**
+	 * Fires inside the 'bp_register_taxonomies' function, where plugins should register taxonomies.
+	 *
+	 * @since 2.2.0
+	 */
+	do_action( 'bp_register_taxonomies' );
+}
+
+/**
+ * Fire the 'bp_register_post_types' action, where plugins should register post types.
+ *
+ * @since 2.5.0
+ */
+function bp_register_post_types() {
+
+	/**
+	 * Fires inside the 'bp_register_post_types' function, where plugins should register post types.
+	 *
+	 * @since 2.5.0
+	 */
+	do_action( 'bp_register_post_types' );
+}
+
+/**
+ * Fire the 'bp_setup_globals' action, where plugins should initialize global settings.
+ *
+ * @since 1.2.0
+ */
+function bp_setup_globals() {
+
+	/**
+	 * Fires inside the 'bp_setup_globals' function, where plugins should initialize global settings.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_setup_globals' );
+}
+
+/**
+ * Fire the 'bp_setup_nav' action, where plugins should register their navigation items.
+ *
+ * @since 1.2.0
+ */
+function bp_setup_nav() {
+
+	/**
+	 * Fires inside the 'bp_setup_nav' function, where plugins should register their navigation items.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_setup_nav' );
+}
+
+/**
+ * Fire the 'bp_setup_admin_bar' action, where plugins should add items to the WP admin bar.
+ *
+ * @since 1.5.0
+ */
+function bp_setup_admin_bar() {
+	if ( bp_use_wp_admin_bar() ) {
+
+		/**
+		 * Fires inside the 'bp_setup_admin_bar' function, where plugins should add items to the WP admin bar.
+		 *
+		 * This hook will only fire if bp_use_wp_admin_bar() returns true.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_setup_admin_bar', array() );
+	}
+}
+
+/**
+ * Fire the 'bp_setup_title' action, where plugins should modify the page title.
+ *
+ * @since 1.5.0
+ */
+function bp_setup_title() {
+
+	/**
+	 * Fires inside the 'bp_setup_title' function, where plugins should modify the page title.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_setup_title' );
+}
+
+/**
+ * Fire the 'bp_register_widgets' action, where plugins should register widgets.
+ *
+ * @since 1.2.0
+ */
+function bp_setup_widgets() {
+
+	/**
+	 * Fires inside the 'bp_register_widgets' function, where plugins should register widgets.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_register_widgets' );
+}
+
+/**
+ * Fire the 'bp_register_member_types' action, where plugins should register member types.
+ *
+ * @since 2.3.0
+ */
+function bp_register_member_types() {
+
+	/**
+	 * Fires inside bp_register_member_types(), so plugins can register member types.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_register_member_types' );
+}
+
+/**
+ * Fire the 'bp_setup_cache_groups' action, where cache groups are registered.
+ *
+ * @since 2.2.0
+ */
+function bp_setup_cache_groups() {
+
+	/**
+	 * Fires inside the 'bp_setup_cache_groups' function, where cache groups are registered.
+	 *
+	 * @since 2.2.0
+	 */
+	do_action( 'bp_setup_cache_groups' );
+}
+
+/**
+ * Set up the currently logged-in user.
+ *
+ * @since 1.7.0
+ *
+ * @link https://buddypress.trac.wordpress.org/ticket/6046
+ * @link https://core.trac.wordpress.org/ticket/24169
+ */
+function bp_setup_current_user() {
+
+	/**
+	 * Fires to set up the current user setup process.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_setup_current_user' );
+}
+
+/**
+ * Fire the 'bp_init' action, BuddyPress's main initialization hook.
+ *
+ * @since 1.2.5
+ */
+function bp_init() {
+
+	/**
+	 * Fires inside the 'bp_init' function, BuddyPress' main initialization hook.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_init' );
+}
+
+/**
+ * Fire the 'bp_rest_api_init' action, where BuddyPress registers REST API endpoints.
+ *
+ * @since 2.6.0
+ */
+function bp_rest_api_init() {
+
+	/**
+	 * Fires the 'bp_rest_api_init' function, where BuddyPress registers REST API endpoints.
+	 *
+	 * @since 2.6.0
+	 */
+	do_action( 'bp_rest_api_init' );
+}
+
+/**
+ * Fire the 'bp_customize_register' action when the Customizer has loaded,
+ * allowing scripts and styles to be initialized.
+ *
+ * @since 2.5.0
+ *
+ * @param WP_Customize_Manager $customizer Customizer instance.
+ */
+function bp_customize_register( WP_Customize_Manager $customizer ) {
+
+	/**
+	 * Fires once the Customizer has loaded, allow scripts and styles to be initialized.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param WP_Customize_Manager $customizer Customizer instance.
+	 */
+	do_action( 'bp_customize_register', $customizer );
+}
+
+/**
+ * Fire the 'bp_loaded' action, which fires after BP's core plugin files have been loaded.
+ *
+ * Attached to 'plugins_loaded'.
+ *
+ * @since 1.2.0
+ */
+function bp_loaded() {
+
+	/**
+	 * Fires inside the 'bp_loaded' function, which fires after BP's core plugin files have been loaded.
+	 *
+	 * @since 1.2.5
+	 */
+	do_action( 'bp_loaded' );
+}
+
+/**
+ * Fire the 'bp_ready' action, which runs after BP is set up and the page is about to render.
+ *
+ * Attached to 'wp'.
+ *
+ * @since 1.6.0
+ */
+function bp_ready() {
+
+	/**
+	 * Fires inside the 'bp_ready' function, which runs after BP is set up and the page is about to render.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_ready' );
+}
+
+/**
+ * Fire the 'bp_actions' action, which runs just before rendering.
+ *
+ * Attach potential template actions, such as catching form requests or routing
+ * custom URLs.
+ *
+ * @since 1.5.0
+ */
+function bp_actions() {
+
+	/**
+	 * Fires inside the 'bp_actions' function, which runs just before rendering.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_actions' );
+}
+
+/**
+ * Fire the 'bp_screens' action, which runs just before rendering.
+ *
+ * Runs just after 'bp_actions'. Use this hook to attach your template
+ * loaders.
+ *
+ * @since 1.5.0
+ */
+function bp_screens() {
+
+	/**
+	 * Fires inside the 'bp_screens' function, which runs just before rendering.
+	 *
+	 * Runs just after 'bp_actions'. Use this hook to attach your template loaders.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_screens' );
+}
+
+/**
+ * Fire 'bp_widgets_init', which runs after widgets have been set up.
+ *
+ * Hooked to 'widgets_init'.
+ *
+ * @since 1.6.0
+ */
+function bp_widgets_init() {
+
+	/**
+	 * Fires inside the 'bp_widgets_init' function, which runs after widgets have been set up.
+	 *
+	 * Hooked to 'widgets_init'.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_widgets_init' );
+}
+
+/**
+ * Fire 'bp_head', which is used to hook scripts and styles in the <head>.
+ *
+ * Hooked to 'wp_head'.
+ *
+ * @since 1.6.0
+ */
+function bp_head() {
+
+	/**
+	 * Fires inside the 'bp_head' function, which runs on 'wp_head'.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_head' );
+}
+
+/** Theme Permissions *********************************************************/
+
+/**
+ * Fire the 'bp_template_redirect' action.
+ *
+ * Run at 'template_redirect', just before WordPress selects and loads a theme
+ * template. The main purpose of this hook in BuddyPress is to redirect users
+ * who do not have the proper permission to access certain content.
+ *
+ * @since 1.6.0
+ */
+function bp_template_redirect() {
+
+	/**
+	 * Fires inside the 'bp_template_redirect' function.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_template_redirect' );
+}
+
+/** Theme Helpers *************************************************************/
+
+/**
+ * Fire the 'bp_register_theme_directory' action.
+ *
+ * The main action used registering theme directories.
+ *
+ * @since 1.5.0
+ */
+function bp_register_theme_directory() {
+
+	/**
+	 * Fires inside the 'bp_register_theme_directory' function.
+	 *
+	 * The main action used registering theme directories.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_register_theme_directory' );
+}
+
+/**
+ * Fire the 'bp_register_theme_packages' action.
+ *
+ * The main action used registering theme packages.
+ *
+ * @since 1.7.0
+ */
+function bp_register_theme_packages() {
+
+	/**
+	 * Fires inside the 'bp_register_theme_packages' function.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_register_theme_packages' );
+}
+
+/**
+ * Fire the 'bp_enqueue_scripts' action, where BP enqueues its CSS and JS.
+ *
+ * @since 1.6.0
+ */
+function bp_enqueue_scripts() {
+
+	/**
+	 * Fires inside the 'bp_enqueue_scripts' function, where BP enqueues its CSS and JS.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_enqueue_scripts' );
+}
+
+/**
+ * Fires the 'bp_enqueue_embed_scripts' action in the <head> for BP oEmbeds.
+ *
+ * @since 2.6.0
+ */
+function bp_enqueue_embed_scripts() {
+	if ( ! is_buddypress() ) {
+		return;
+	}
+
+	/**
+	 * Enqueue CSS and JS files for BuddyPress embeds.
+	 *
+	 * @since 2.6.0
+	 */
+	do_action( 'bp_enqueue_embed_scripts' );
+}
+
+/**
+ * Fire the 'bp_add_rewrite_tag' action, where BP adds its custom rewrite tags.
+ *
+ * @since 1.8.0
+ */
+function bp_add_rewrite_tags() {
+
+	/**
+	 * Fires inside the 'bp_add_rewrite_tags' function, where BP adds its custom rewrite tags.
+	 *
+	 * @since 1.8.0
+	 */
+	do_action( 'bp_add_rewrite_tags' );
+}
+
+/**
+ * Fire the 'bp_add_rewrite_rules' action, where BP adds its custom rewrite rules.
+ *
+ * @since 1.9.0
+ */
+function bp_add_rewrite_rules() {
+
+	/**
+	 * Fires inside the 'bp_add_rewrite_rules' function, where BP adds its custom rewrite rules.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( 'bp_add_rewrite_rules' );
+}
+
+/**
+ * Fire the 'bp_add_permastructs' action, where BP adds its BP-specific permalink structure.
+ *
+ * @since 1.9.0
+ */
+function bp_add_permastructs() {
+
+	/**
+	 * Fires inside the 'bp_add_permastructs' function, where BP adds its BP-specific permalink structure.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( 'bp_add_permastructs' );
+}
+
+/**
+ * Fire the 'bp_setup_theme' action.
+ *
+ * The main purpose of 'bp_setup_theme' is give themes a place to load their
+ * BuddyPress-specific functionality.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_theme() {
+
+	/**
+	 * Fires inside the 'bp_setup_theme' function.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_setup_theme' );
+}
+
+/**
+ * Fire the 'bp_after_setup_theme' action.
+ *
+ * Piggy-back action for BuddyPress-specific theme actions once the theme has
+ * been set up and the theme's functions.php has loaded.
+ *
+ * Hooked to 'after_setup_theme' with a priority of 100. This allows plenty of
+ * time for other themes to load their features, such as BuddyPress support,
+ * before our theme compatibility layer kicks in.
+ *
+ * @since 1.6.0
+ */
+function bp_after_setup_theme() {
+
+	/**
+	 * Fires inside the 'bp_after_setup_theme' function.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_after_setup_theme' );
+}
+
+/** Theme Compatibility Filter ************************************************/
+
+/**
+ * Fire the 'bp_request' filter, a piggy-back of WP's 'request'.
+ *
+ * @since 1.7.0
+ *
+ * @see WP::parse_request() for a description of parameters.
+ *
+ * @param array $query_vars See {@link WP::parse_request()}.
+ * @return array $query_vars See {@link WP::parse_request()}.
+ */
+function bp_request( $query_vars = array() ) {
+
+	/**
+	 * Filters the query_vars for the current request.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $query_vars Array of query variables.
+	 */
+	return apply_filters( 'bp_request', $query_vars );
+}
+
+/**
+ * Fire the 'bp_login_redirect' filter, a piggy-back of WP's 'login_redirect'.
+ *
+ * @since 1.7.0
+ *
+ * @param string $redirect_to     See 'login_redirect'.
+ * @param string $redirect_to_raw See 'login_redirect'.
+ * @param bool   $user            See 'login_redirect'.
+ * @return string
+ */
+function bp_login_redirect( $redirect_to = '', $redirect_to_raw = '', $user = false ) {
+
+	/**
+	 * Filters the URL to redirect to after login.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string           $redirect_to     The redirect destination URL.
+	 * @param string           $redirect_to_raw The requested redirect destination URL passed as a parameter.
+	 * @param WP_User|WP_Error $user            WP_User object if login was successful, WP_Error object otherwise.
+	 */
+	return apply_filters( 'bp_login_redirect', $redirect_to, $redirect_to_raw, $user );
+}
+
+/**
+ * Fire 'bp_template_include', main filter used for theme compatibility and displaying custom BP theme files.
+ *
+ * Hooked to 'template_include'.
+ *
+ * @since 1.6.0
+ *
+ * @param string $template See 'template_include'.
+ * @return string Template file to use.
+ */
+function bp_template_include( $template = '' ) {
+
+	/**
+	 * Filters the template to use with template_include.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $template The path of the template to include.
+	 */
+	return apply_filters( 'bp_template_include', $template );
+}
+
+/**
+ * Fire the 'bp_generate_rewrite_rules' action, where BP generates its rewrite rules.
+ *
+ * @since 1.7.0
+ *
+ * @param WP_Rewrite $wp_rewrite See 'generate_rewrite_rules'.
+ */
+function bp_generate_rewrite_rules( $wp_rewrite ) {
+
+	/**
+	 * Fires inside the 'bp_generate_rewrite_rules' function.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param WP_Rewrite $wp_rewrite WP_Rewrite object. Passed by reference.
+	 */
+	do_action_ref_array( 'bp_generate_rewrite_rules', array( &$wp_rewrite ) );
+}
+
+/**
+ * Fire the 'bp_allowed_themes' filter.
+ *
+ * Filter the allowed themes list for BuddyPress-specific themes.
+ *
+ * @since 1.7.0
+ *
+ * @param array $themes The path of the template to include.
+ * @return array
+ */
+function bp_allowed_themes( $themes ) {
+
+	/**
+	 * Filters the allowed themes list for BuddyPress-specific themes.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $template The path of the template to include.
+	 */
+	return apply_filters( 'bp_allowed_themes', $themes );
+}
+
+/** Requests ******************************************************************/
+
+/**
+ * The main action used for handling theme-side POST requests.
+ *
+ * @since 1.9.0
+ */
+function bp_post_request() {
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no action.
+	if ( empty( $_POST['action'] ) ) {
+		return;
+	}
+
+	// Sanitize the POST action.
+	$action = sanitize_key( $_POST['action'] );
+
+	/**
+	 * Fires at the end of the bp_post_request function.
+	 *
+	 * This dynamic action is probably the one you want to use. It narrows down
+	 * the scope of the 'action' without needing to check it in your function.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( 'bp_post_request_' . $action );
+
+	/**
+	 * Fires at the end of the bp_post_request function.
+	 *
+	 * Use this static action if you don't mind checking the 'action' yourself.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $action The action being run.
+	 */
+	do_action( 'bp_post_request',   $action );
+}
+
+/**
+ * The main action used for handling theme-side GET requests.
+ *
+ * @since 1.9.0
+ */
+function bp_get_request() {
+
+	// Bail if not a POST action.
+	if ( ! bp_is_get_request() ) {
+		return;
+	}
+
+	// Bail if no action.
+	if ( empty( $_GET['action'] ) ) {
+		return;
+	}
+
+	// Sanitize the GET action.
+	$action = sanitize_key( $_GET['action'] );
+
+	/**
+	 * Fires at the end of the bp_get_request function.
+	 *
+	 * This dynamic action is probably the one you want to use. It narrows down
+	 * the scope of the 'action' without needing to check it in your function.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( 'bp_get_request_' . $action );
+
+	/**
+	 * Fires at the end of the bp_get_request function.
+	 *
+	 * Use this static action if you don't mind checking the 'action' yourself.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $action The action being run.
+	 */
+	do_action( 'bp_get_request',   $action );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-filters.php	(revision 12265)
@@ -0,0 +1,1148 @@
+<?php
+/**
+ * BuddyPress Filters.
+ *
+ * This file contains the filters that are used throughout BuddyPress. They are
+ * consolidated here to make searching for them easier, and to help developers
+ * understand at a glance the order in which things occur.
+ *
+ * There are a few common places that additional filters can currently be found.
+ *
+ *  - BuddyPress: In {@link BuddyPress::setup_actions()} in buddypress.php
+ *  - Component: In {@link BP_Component::setup_actions()} in
+ *                bp-core/bp-core-component.php
+ *  - Admin: More in {@link BP_Admin::setup_actions()} in
+ *            bp-core/bp-core-admin.php
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ *
+ * @see bp-core-actions.php
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Attach BuddyPress to WordPress.
+ *
+ * BuddyPress uses its own internal actions to help aid in third-party plugin
+ * development, and to limit the amount of potential future code changes when
+ * updates to WordPress core occur.
+ *
+ * These actions exist to create the concept of 'plugin dependencies'. They
+ * provide a safe way for plugins to execute code *only* when BuddyPress is
+ * installed and activated, without needing to do complicated guesswork.
+ *
+ * For more information on how this works, see the 'Plugin Dependency' section
+ * near the bottom of this file.
+ *
+ *           v--WordPress Actions       v--BuddyPress Sub-actions
+ */
+add_filter( 'request',                 'bp_request',             10    );
+add_filter( 'template_include',        'bp_template_include',    10    );
+add_filter( 'login_redirect',          'bp_login_redirect',      10, 3 );
+add_filter( 'map_meta_cap',            'bp_map_meta_caps',       10, 4 );
+
+// Add some filters to feedback messages.
+add_filter( 'bp_core_render_message_content', 'wptexturize'       );
+add_filter( 'bp_core_render_message_content', 'convert_smilies'   );
+add_filter( 'bp_core_render_message_content', 'convert_chars'     );
+add_filter( 'bp_core_render_message_content', 'wpautop'           );
+add_filter( 'bp_core_render_message_content', 'shortcode_unautop' );
+add_filter( 'bp_core_render_message_content', 'wp_kses_data', 5   );
+
+// Emails.
+add_filter( 'bp_email_set_content_html', 'wp_filter_post_kses', 6 );
+add_filter( 'bp_email_set_content_html', 'stripslashes', 8 );
+add_filter( 'bp_email_set_content_plaintext', 'wp_strip_all_tags', 6 );
+add_filter( 'bp_email_set_subject', 'sanitize_text_field', 6 );
+
+/**
+ * Template Compatibility.
+ *
+ * If you want to completely bypass this and manage your own custom BuddyPress
+ * template hierarchy, start here by removing this filter, then look at how
+ * bp_template_include() works and do something similar. :)
+ */
+add_filter( 'bp_template_include',   'bp_template_include_theme_supports', 2, 1 );
+add_filter( 'bp_template_include',   'bp_template_include_theme_compat',   4, 2 );
+
+// Filter BuddyPress template locations.
+add_filter( 'bp_get_template_stack', 'bp_add_template_stack_locations' );
+
+// Turn comments off for BuddyPress pages.
+add_filter( 'comments_open', 'bp_comments_open', 10, 2 );
+
+// Prevent DB query for WP's main loop.
+add_filter( 'posts_pre_query', 'bp_core_filter_wp_query', 10, 2 );
+
+/**
+ * Prevent specific pages (eg 'Activate') from showing on page listings.
+ *
+ * @since 1.5.0
+ *
+ * @param array $pages List of excluded page IDs, as passed to the
+ *                     'wp_list_pages_excludes' filter.
+ * @return array The exclude list, with BP's pages added.
+ */
+function bp_core_exclude_pages( $pages = array() ) {
+
+	// Bail if not the root blog.
+	if ( ! bp_is_root_blog() )
+		return $pages;
+
+	$bp = buddypress();
+
+	if ( !empty( $bp->pages->activate ) )
+		$pages[] = $bp->pages->activate->id;
+
+	if ( !empty( $bp->pages->register ) )
+		$pages[] = $bp->pages->register->id;
+
+	/**
+	 * Filters specific pages that shouldn't show up on page listings.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $pages Array of pages to exclude.
+	 */
+	return apply_filters( 'bp_core_exclude_pages', $pages );
+}
+add_filter( 'wp_list_pages_excludes', 'bp_core_exclude_pages' );
+
+/**
+ * Prevent specific pages (eg 'Activate') from showing in the Pages meta box of the Menu Administration screen.
+ *
+ * @since 2.0.0
+ *
+ * @param object|null $object The post type object used in the meta box.
+ * @return object|null The $object, with a query argument to remove register and activate pages id.
+ */
+function bp_core_exclude_pages_from_nav_menu_admin( $object = null ) {
+
+	// Bail if not the root blog.
+	if ( ! bp_is_root_blog() ) {
+		return $object;
+	}
+
+	if ( 'page' != $object->name ) {
+		return $object;
+	}
+
+	$bp = buddypress();
+	$pages = array();
+
+	if ( ! empty( $bp->pages->activate ) ) {
+		$pages[] = $bp->pages->activate->id;
+	}
+
+	if ( ! empty( $bp->pages->register ) ) {
+		$pages[] = $bp->pages->register->id;
+	}
+
+	if ( ! empty( $pages ) ) {
+		$object->_default_query['post__not_in'] = $pages;
+	}
+
+	return $object;
+}
+add_filter( 'nav_menu_meta_box_object', 'bp_core_exclude_pages_from_nav_menu_admin', 11, 1 );
+
+/**
+ * Adds current page CSS classes to the parent BP page in a WP Page Menu.
+ *
+ * Because BuddyPress primarily uses virtual pages, we need a way to highlight
+ * the BP parent page during WP menu generation.  This function checks the
+ * current BP component against the current page in the WP menu to see if we
+ * should highlight the WP page.
+ *
+ * @since 2.2.0
+ *
+ * @param array   $retval CSS classes for the current menu page in the menu.
+ * @param WP_Post $page   The page properties for the current menu item.
+ * @return array
+ */
+function bp_core_menu_highlight_parent_page( $retval, $page ) {
+	if ( ! is_buddypress() ) {
+		return $retval;
+	}
+
+	$page_id = false;
+
+	// Loop against all BP component pages.
+	foreach ( (array) buddypress()->pages as $component => $bp_page ) {
+		// Handles the majority of components.
+		if ( bp_is_current_component( $component ) ) {
+			$page_id = (int) $bp_page->id;
+		}
+
+		// Stop if not on a user page.
+		if ( ! bp_is_user() && ! empty( $page_id ) ) {
+			break;
+		}
+
+		// Members component requires an explicit check due to overlapping components.
+		if ( bp_is_user() && 'members' === $component ) {
+			$page_id = (int) $bp_page->id;
+			break;
+		}
+	}
+
+	// Duplicate some logic from Walker_Page::start_el() to highlight menu items.
+	if ( ! empty( $page_id ) ) {
+		$_bp_page = get_post( $page_id );
+		if ( in_array( $page->ID, $_bp_page->ancestors, true ) ) {
+			$retval[] = 'current_page_ancestor';
+		}
+		if ( $page->ID === $page_id ) {
+			$retval[] = 'current_page_item';
+		} elseif ( $_bp_page && $page->ID === $_bp_page->post_parent ) {
+			$retval[] = 'current_page_parent';
+		}
+	}
+
+	$retval = array_unique( $retval );
+
+	return $retval;
+}
+add_filter( 'page_css_class', 'bp_core_menu_highlight_parent_page', 10, 2 );
+
+/**
+ * Adds current page CSS classes to the parent BP page in a WP Nav Menu.
+ *
+ * When {@link wp_nav_menu()} is used, this function helps to highlight the
+ * current BP parent page during nav menu generation.
+ *
+ * @since 2.2.0
+ *
+ * @param array   $retval CSS classes for the current nav menu item in the menu.
+ * @param WP_Post $item   The properties for the current nav menu item.
+ * @return array
+ */
+function bp_core_menu_highlight_nav_menu_item( $retval, $item ) {
+	// If we're not on a BP page or if the current nav item is not a page, stop!
+	if ( ! is_buddypress() || 'page' !== $item->object ) {
+		return $retval;
+	}
+
+	// Get the WP page.
+	$page   = get_post( $item->object_id );
+
+	// See if we should add our highlight CSS classes for the page.
+	$retval = bp_core_menu_highlight_parent_page( $retval, $page );
+
+	return $retval;
+}
+add_filter( 'nav_menu_css_class', 'bp_core_menu_highlight_nav_menu_item', 10, 2 );
+
+/**
+ * Filter the blog post comments array and insert BuddyPress URLs for users.
+ *
+ * @since 1.2.0
+ *
+ * @param array $comments The array of comments supplied to the comments template.
+ * @param int   $post_id  The post ID.
+ * @return array $comments The modified comment array.
+ */
+function bp_core_filter_comments( $comments, $post_id ) {
+	global $wpdb;
+
+	foreach( (array) $comments as $comment ) {
+		if ( $comment->user_id )
+			$user_ids[] = $comment->user_id;
+	}
+
+	if ( empty( $user_ids ) )
+		return $comments;
+
+	$user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
+
+	if ( !$userdata = $wpdb->get_results( "SELECT ID as user_id, user_login, user_nicename FROM {$wpdb->users} WHERE ID IN ({$user_ids})" ) )
+		return $comments;
+
+	foreach( (array) $userdata as $user )
+		$users[$user->user_id] = bp_core_get_user_domain( $user->user_id, $user->user_nicename, $user->user_login );
+
+	foreach( (array) $comments as $i => $comment ) {
+		if ( !empty( $comment->user_id ) ) {
+			if ( !empty( $users[$comment->user_id] ) )
+				$comments[$i]->comment_author_url = $users[$comment->user_id];
+		}
+	}
+
+	return $comments;
+}
+add_filter( 'comments_array', 'bp_core_filter_comments', 10, 2 );
+
+/**
+ * When a user logs in, redirect him in a logical way.
+ *
+ * @since 1.2.0
+ *
+ *       are redirected to on login.
+ *
+ * @param string  $redirect_to     The URL to be redirected to, sanitized in wp-login.php.
+ * @param string  $redirect_to_raw The unsanitized redirect_to URL ($_REQUEST['redirect_to']).
+ * @param WP_User $user            The WP_User object corresponding to a successfully
+ *                                 logged-in user. Otherwise a WP_Error object.
+ * @return string The redirect URL.
+ */
+function bp_core_login_redirect( $redirect_to, $redirect_to_raw, $user ) {
+
+	// Only modify the redirect if we're on the main BP blog.
+	if ( !bp_is_root_blog() ) {
+		return $redirect_to;
+	}
+
+	// Only modify the redirect once the user is logged in.
+	if ( !is_a( $user, 'WP_User' ) ) {
+		return $redirect_to;
+	}
+
+	/**
+	 * Filters whether or not to redirect.
+	 *
+	 * Allows plugins to have finer grained control of redirect upon login.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool    $value           Whether or not to redirect.
+	 * @param string  $redirect_to     Sanitized URL to be redirected to.
+	 * @param string  $redirect_to_raw Unsanitized URL to be redirected to.
+	 * @param WP_User $user            The WP_User object corresponding to a
+	 *                                 successfully logged in user.
+	 */
+	$maybe_redirect = apply_filters( 'bp_core_login_redirect', false, $redirect_to, $redirect_to_raw, $user );
+	if ( false !== $maybe_redirect ) {
+		return $maybe_redirect;
+	}
+
+	// If a 'redirect_to' parameter has been passed that contains 'wp-admin', verify that the
+	// logged-in user has any business to conduct in the Dashboard before allowing the
+	// redirect to go through.
+	if ( !empty( $redirect_to ) && ( false === strpos( $redirect_to, 'wp-admin' ) || user_can( $user, 'edit_posts' ) ) ) {
+		return $redirect_to;
+	}
+
+	if ( false === strpos( wp_get_referer(), 'wp-login.php' ) && false === strpos( wp_get_referer(), 'activate' ) && empty( $_REQUEST['nr'] ) ) {
+		return wp_get_referer();
+	}
+
+	/**
+	 * Filters the URL to redirect users to upon successful login.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $value URL to redirect to.
+	 */
+	return apply_filters( 'bp_core_login_redirect_to', bp_get_root_domain() );
+}
+add_filter( 'bp_login_redirect', 'bp_core_login_redirect', 10, 3 );
+
+/**
+ * Decode HTML entities for plain-text emails.
+ *
+ * @since 2.5.0
+ *
+ * @param string $retval    Current email content.
+ * @param string $prop      Email property to check against.
+ * @param string $transform Either 'raw' or 'replace-tokens'.
+ * @return string|null $retval Modified email content.
+ */
+function bp_email_plaintext_entity_decode( $retval, $prop, $transform ) {
+	switch ( $prop ) {
+		case 'content_plaintext' :
+		case 'subject' :
+			// Only decode if 'replace-tokens' is the current type.
+			if ( 'replace-tokens' === $transform ) {
+				return html_entity_decode( $retval, ENT_QUOTES );
+			} else {
+				return $retval;
+			}
+			break;
+
+		default :
+			return $retval;
+			break;
+	}
+}
+add_filter( 'bp_email_get_property', 'bp_email_plaintext_entity_decode', 10, 3 );
+
+/**
+ * Replace the generated password in the welcome email with '[User Set]'.
+ *
+ * On a standard BP installation, users who register themselves also set their
+ * own passwords. Therefore there is no need for the insecure practice of
+ * emailing the plaintext password to the user in the welcome email.
+ *
+ * This filter will not fire when a user is registered by the site admin.
+ *
+ * @since 1.2.1
+ *
+ * @param string $welcome_email Complete email passed through WordPress.
+ * @return string Filtered $welcome_email with the password replaced
+ *                by '[User Set]'.
+ */
+function bp_core_filter_user_welcome_email( $welcome_email ) {
+
+	// Don't touch the email when a user is registered by the site admin.
+	if ( ( is_admin() || is_network_admin() ) && buddypress()->members->admin->signups_page != get_current_screen()->id ) {
+		return $welcome_email;
+	}
+
+	if ( strpos( bp_get_requested_url(), 'wp-activate.php' ) !== false ) {
+		return $welcome_email;
+	}
+
+	// Don't touch the email if we don't have a custom registration template.
+	if ( ! bp_has_custom_signup_page() ) {
+		return $welcome_email;
+	}
+
+	// [User Set] Replaces 'PASSWORD' in welcome email; Represents value set by user
+	return str_replace( 'PASSWORD', __( '[User Set]', 'buddypress' ), $welcome_email );
+}
+add_filter( 'update_welcome_user_email', 'bp_core_filter_user_welcome_email' );
+
+/**
+ * Replace the generated password in the welcome email with '[User Set]'.
+ *
+ * On a standard BP installation, users who register themselves also set their
+ * own passwords. Therefore there is no need for the insecure practice of
+ * emailing the plaintext password to the user in the welcome email.
+ *
+ * This filter will not fire when a user is registered by the site admin.
+ *
+ * @since 1.2.1
+ *
+ * @param string $welcome_email Complete email passed through WordPress.
+ * @param int    $blog_id       ID of the blog user is joining.
+ * @param int    $user_id       ID of the user joining.
+ * @param string $password      Password of user.
+ * @return string Filtered $welcome_email with $password replaced by '[User Set]'.
+ */
+function bp_core_filter_blog_welcome_email( $welcome_email, $blog_id, $user_id, $password ) {
+
+	// Don't touch the email when a user is registered by the site admin.
+	if ( ( is_admin() || is_network_admin() ) && buddypress()->members->admin->signups_page != get_current_screen()->id ) {
+		return $welcome_email;
+	}
+
+	// Don't touch the email if we don't have a custom registration template.
+	if ( ! bp_has_custom_signup_page() )
+		return $welcome_email;
+
+	// [User Set] Replaces $password in welcome email; Represents value set by user
+	return str_replace( $password, __( '[User Set]', 'buddypress' ), $welcome_email );
+}
+add_filter( 'update_welcome_email', 'bp_core_filter_blog_welcome_email', 10, 4 );
+
+/**
+ * Notify new users of a successful registration (with blog).
+ *
+ * This function filter's WP's 'wpmu_signup_blog_notification', and replaces
+ * WP's default welcome email with a BuddyPress-specific message.
+ *
+ * @since 1.0.0
+ *
+ * @see wpmu_signup_blog_notification() for a description of parameters.
+ *
+ * @param string $domain     The new blog domain.
+ * @param string $path       The new blog path.
+ * @param string $title      The site title.
+ * @param string $user       The user's login name.
+ * @param string $user_email The user's email address.
+ * @param string $key        The activation key created in wpmu_signup_blog().
+ * @return bool              Returns false to stop original WPMU function from continuing.
+ */
+function bp_core_activation_signup_blog_notification( $domain, $path, $title, $user, $user_email, $key ) {
+	$args = array(
+		'tokens' => array(
+			'activate-site.url' => esc_url( bp_get_activation_page() . '?key=' . urlencode( $key ) ),
+			'domain'            => $domain,
+			'key_blog'          => $key,
+			'path'              => $path,
+			'user-site.url'     => esc_url( set_url_scheme( "http://{$domain}{$path}" ) ),
+			'title'             => $title,
+			'user.email'        => $user_email,
+		),
+	);
+	bp_send_email( 'core-user-registration-with-blog', array( array( $user_email => $user ) ), $args );
+
+	// Return false to stop the original WPMU function from continuing.
+	return false;
+}
+add_filter( 'wpmu_signup_blog_notification', 'bp_core_activation_signup_blog_notification', 1, 6 );
+
+/**
+ * Notify new users of a successful registration (without blog).
+ *
+ * @since 1.0.0
+ *
+ * @see wpmu_signup_user_notification() for a full description of params.
+ *
+ * @param string $user       The user's login name.
+ * @param string $user_email The user's email address.
+ * @param string $key        The activation key created in wpmu_signup_user().
+ * @param array  $meta       By default, an empty array.
+ * @return false|string Returns false to stop original WPMU function from continuing.
+ */
+function bp_core_activation_signup_user_notification( $user, $user_email, $key, $meta ) {
+	if ( is_admin() ) {
+
+		// If the user is created from the WordPress Add User screen, don't send BuddyPress signup notifications.
+		if( in_array( get_current_screen()->id, array( 'user', 'user-network' ) ) ) {
+			// If the Super Admin want to skip confirmation email.
+			if ( isset( $_POST[ 'noconfirmation' ] ) && is_super_admin() ) {
+				return false;
+
+			// WordPress will manage the signup process.
+			} else {
+				return $user;
+			}
+
+		/*
+		 * There can be a case where the user was created without the skip confirmation
+		 * And the super admin goes in pending accounts to resend it. In this case, as the
+		 * meta['password'] is not set, the activation url must be WordPress one.
+		 */
+		} elseif ( buddypress()->members->admin->signups_page == get_current_screen()->id ) {
+			$is_hashpass_in_meta = maybe_unserialize( $meta );
+
+			if ( empty( $is_hashpass_in_meta['password'] ) ) {
+				return $user;
+			}
+		}
+	}
+
+	$user_id = 0;
+	$user_object = get_user_by( 'login', $user );
+	if ( $user_object ) {
+		$user_id = $user_object->ID;
+	}
+
+	$args = array(
+		'tokens' => array(
+			'activate.url' => esc_url( trailingslashit( bp_get_activation_page() ) . "{$key}/" ),
+			'key'          => $key,
+			'user.email'   => $user_email,
+			'user.id'      => $user_id,
+		),
+	);
+	bp_send_email( 'core-user-registration', array( array( $user_email => $user ) ), $args );
+
+	// Return false to stop the original WPMU function from continuing.
+	return false;
+}
+add_filter( 'wpmu_signup_user_notification', 'bp_core_activation_signup_user_notification', 1, 4 );
+
+/**
+ * Filter the page title for BuddyPress pages.
+ *
+ * @since 1.5.0
+ *
+ * @see wp_title()
+ * @global object $bp BuddyPress global settings.
+ *
+ * @param string $title       Original page title.
+ * @param string $sep         How to separate the various items within the page title.
+ * @param string $seplocation Direction to display title.
+ * @return string              New page title.
+ */
+function bp_modify_page_title( $title = '', $sep = '&raquo;', $seplocation = 'right' ) {
+	global $paged, $page, $_wp_theme_features;
+
+	// Get the BuddyPress title parts.
+	$bp_title_parts = bp_get_title_parts( $seplocation );
+
+	// If not set, simply return the original title.
+	if ( ! $bp_title_parts ) {
+		return $title;
+	}
+
+	// Get the blog name, so we can check if the original $title included it.
+	$blogname = get_bloginfo( 'name', 'display' );
+
+	/**
+	 * Are we going to fake 'title-tag' theme functionality?
+	 *
+	 * @link https://buddypress.trac.wordpress.org/ticket/6107
+	 * @see wp_title()
+	 */
+	$title_tag_compatibility = (bool) ( ! empty( $_wp_theme_features['title-tag'] ) || ( $blogname && strstr( $title, $blogname ) ) );
+
+	// Append the site title to title parts if theme supports title tag.
+	if ( true === $title_tag_compatibility ) {
+		$bp_title_parts['site'] = $blogname;
+
+		if ( ( $paged >= 2 || $page >= 2 ) && ! is_404() && ! bp_is_single_activity() ) {
+			$bp_title_parts['page'] = sprintf( __( 'Page %s', 'buddypress' ), max( $paged, $page ) );
+		}
+	}
+
+	// Pad the separator with 1 space on each side.
+	$prefix = str_pad( $sep, strlen( $sep ) + 2, ' ', STR_PAD_BOTH );
+
+	// Join the parts together.
+	$new_title = join( $prefix, array_filter( $bp_title_parts ) );
+
+	// Append the prefix for pre `title-tag` compatibility.
+	if ( false === $title_tag_compatibility ) {
+		$new_title = $new_title . $prefix;
+	}
+
+	/**
+	 * Filters the older 'wp_title' page title for BuddyPress pages.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $new_title   The BuddyPress page title.
+	 * @param string $title       The original WordPress page title.
+	 * @param string $sep         The title parts separator.
+	 * @param string $seplocation Location of the separator (left or right).
+	 */
+	return apply_filters( 'bp_modify_page_title', $new_title, $title, $sep, $seplocation );
+}
+add_filter( 'wp_title',             'bp_modify_page_title', 20, 3 );
+add_filter( 'bp_modify_page_title', 'wptexturize'                 );
+add_filter( 'bp_modify_page_title', 'convert_chars'               );
+add_filter( 'bp_modify_page_title', 'esc_html'                    );
+
+/**
+ * Filter the document title for BuddyPress pages.
+ *
+ * @since 2.4.3
+ *
+ * @param array $title The WordPress document title parts.
+ * @return array the unchanged title parts or the BuddyPress ones
+ */
+function bp_modify_document_title_parts( $title = array() ) {
+	// Get the BuddyPress title parts.
+	$bp_title_parts = bp_get_title_parts();
+
+	// If not set, simply return the original title.
+	if ( ! $bp_title_parts ) {
+		return $title;
+	}
+
+	// Get the separator used by wp_get_document_title().
+	$sep = apply_filters( 'document_title_separator', '-' );
+
+	// Build the BuddyPress portion of the title.
+	// We don't need to sanitize this as WordPress will take care of it.
+	$bp_title = array(
+		'title' => join( " $sep ", $bp_title_parts )
+	);
+
+	// Add the pagination number if needed (not sure if this is necessary).
+	if ( isset( $title['page'] ) && ! bp_is_single_activity() ) {
+		$bp_title['page'] = $title['page'];
+	}
+
+	// Add the sitename if needed.
+	if ( isset( $title['site'] ) ) {
+		$bp_title['site'] = $title['site'];
+	}
+
+	/**
+	 * Filters BuddyPress title parts that will be used into the document title.
+	 *
+	 * @since 2.4.3
+	 *
+	 * @param array $bp_title The BuddyPress page title parts.
+	 * @param array $title    The original WordPress title parts.
+	 */
+	return apply_filters( 'bp_modify_document_title_parts', $bp_title, $title );
+}
+add_filter( 'document_title_parts', 'bp_modify_document_title_parts', 20, 1 );
+
+/**
+ * Add BuddyPress-specific items to the wp_nav_menu.
+ *
+ * @since 1.9.0
+ *
+ * @param WP_Post $menu_item The menu item.
+ * @return WP_Post The modified WP_Post object.
+ */
+function bp_setup_nav_menu_item( $menu_item ) {
+	if ( is_admin() ) {
+		return $menu_item;
+	}
+
+	// Prevent a notice error when using the customizer.
+	$menu_classes = $menu_item->classes;
+
+	if ( is_array( $menu_classes ) ) {
+		$menu_classes = implode( ' ', $menu_item->classes);
+	}
+
+	// We use information stored in the CSS class to determine what kind of
+	// menu item this is, and how it should be treated.
+	preg_match( '/\sbp-(.*)-nav/', $menu_classes, $matches );
+
+	// If this isn't a BP menu item, we can stop here.
+	if ( empty( $matches[1] ) ) {
+		return $menu_item;
+	}
+
+	switch ( $matches[1] ) {
+		case 'login' :
+			if ( is_user_logged_in() ) {
+				$menu_item->_invalid = true;
+			} else {
+				$menu_item->url = wp_login_url( bp_get_requested_url() );
+			}
+
+			break;
+
+		case 'logout' :
+			if ( ! is_user_logged_in() ) {
+				$menu_item->_invalid = true;
+			} else {
+				$menu_item->url = wp_logout_url( bp_get_requested_url() );
+			}
+
+			break;
+
+		// Don't show the Register link to logged-in users.
+		case 'register' :
+			if ( is_user_logged_in() ) {
+				$menu_item->_invalid = true;
+			}
+
+			break;
+
+		// All other BP nav items are specific to the logged-in user,
+		// and so are not relevant to logged-out users.
+		default:
+			if ( is_user_logged_in() ) {
+				$menu_item->url = bp_nav_menu_get_item_url( $matches[1] );
+			} else {
+				$menu_item->_invalid = true;
+			}
+
+			break;
+	}
+
+	// If component is deactivated, make sure menu item doesn't render.
+	if ( empty( $menu_item->url ) ) {
+		$menu_item->_invalid = true;
+
+	// Highlight the current page.
+	} else {
+		$current = bp_get_requested_url();
+		if ( strpos( $current, $menu_item->url ) !== false ) {
+			if ( is_array( $menu_item->classes ) ) {
+				$menu_item->classes[] = 'current_page_item';
+				$menu_item->classes[] = 'current-menu-item';
+			} else {
+				$menu_item->classes = array( 'current_page_item', 'current-menu-item' );
+			}
+		}
+	}
+
+	return $menu_item;
+}
+add_filter( 'wp_setup_nav_menu_item', 'bp_setup_nav_menu_item', 10, 1 );
+
+/**
+ * Populate BuddyPress user nav items for the customizer.
+ *
+ * @since 2.3.3
+ *
+ * @param array   $items  The array of menu items.
+ * @param string  $type   The requested type.
+ * @param string  $object The requested object name.
+ * @param integer $page   The page num being requested.
+ * @return array The paginated BuddyPress user nav items.
+ */
+function bp_customizer_nav_menus_get_items( $items = array(), $type = '', $object = '', $page = 0 ) {
+	if ( 'bp_loggedin_nav' === $object ) {
+		$bp_items = bp_nav_menu_get_loggedin_pages();
+	} elseif ( 'bp_loggedout_nav' === $object ) {
+		$bp_items = bp_nav_menu_get_loggedout_pages();
+	} else {
+		return $items;
+	}
+
+	foreach ( $bp_items as $bp_item ) {
+		$items[] = array(
+			'id'         => "bp-{$bp_item->post_excerpt}",
+			'title'      => html_entity_decode( $bp_item->post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
+			'type'       => $type,
+			'url'        => esc_url_raw( $bp_item->guid ),
+			'classes'    => "bp-menu bp-{$bp_item->post_excerpt}-nav",
+			'type_label' => _x( 'Custom Link', 'customizer menu type label', 'buddypress' ),
+			'object'     => $object,
+			'object_id'  => -1,
+		);
+	}
+
+	return array_slice( $items, 10 * $page, 10 );
+}
+add_filter( 'customize_nav_menu_available_items', 'bp_customizer_nav_menus_get_items', 10, 4 );
+
+/**
+ * Set BuddyPress item navs for the customizer.
+ *
+ * @since 2.3.3
+ *
+ * @param array $item_types An associative array structured for the customizer.
+ * @return array $item_types An associative array structured for the customizer.
+ */
+function bp_customizer_nav_menus_set_item_types( $item_types = array() ) {
+	$item_types = array_merge( $item_types, array(
+		'bp_loggedin_nav' => array(
+			'title'  => _x( 'BuddyPress (logged-in)', 'customizer menu section title', 'buddypress' ),
+			'type'   => 'bp_nav',
+			'object' => 'bp_loggedin_nav',
+		),
+		'bp_loggedout_nav' => array(
+			'title'  => _x( 'BuddyPress (logged-out)', 'customizer menu section title', 'buddypress' ),
+			'type'   => 'bp_nav',
+			'object' => 'bp_loggedout_nav',
+		),
+	) );
+
+	return $item_types;
+}
+add_filter( 'customize_nav_menu_available_item_types', 'bp_customizer_nav_menus_set_item_types', 10, 1 );
+
+/**
+ * Filter SQL query strings to swap out the 'meta_id' column.
+ *
+ * WordPress uses the meta_id column for commentmeta and postmeta, and so
+ * hardcodes the column name into its *_metadata() functions. BuddyPress, on
+ * the other hand, uses 'id' for the primary column. To make WP's functions
+ * usable for BuddyPress, we use this just-in-time filter on 'query' to swap
+ * 'meta_id' with 'id.
+ *
+ * @since 2.0.0
+ *
+ * @access private Do not use.
+ *
+ * @param string $q SQL query.
+ * @return string
+ */
+function bp_filter_metaid_column_name( $q ) {
+	/*
+	 * Replace quoted content with __QUOTE__ to avoid false positives.
+	 * This regular expression will match nested quotes.
+	 */
+	$quoted_regex = "/'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'/s";
+	preg_match_all( $quoted_regex, $q, $quoted_matches );
+	$q = preg_replace( $quoted_regex, '__QUOTE__', $q );
+
+	$q = str_replace( 'meta_id', 'id', $q );
+
+	// Put quoted content back into the string.
+	if ( ! empty( $quoted_matches[0] ) ) {
+		for ( $i = 0; $i < count( $quoted_matches[0] ); $i++ ) {
+			$quote_pos = strpos( $q, '__QUOTE__' );
+			$q = substr_replace( $q, $quoted_matches[0][ $i ], $quote_pos, 9 );
+		}
+	}
+
+	return $q;
+}
+
+/**
+ * Filter the edit post link to avoid its display in BuddyPress pages.
+ *
+ * @since 2.1.0
+ *
+ * @param string $edit_link The edit link.
+ * @param int    $post_id   Post ID.
+ * @return false|string Will be a boolean (false) if $post_id is 0. Will be a string (the unchanged edit link)
+ *                      otherwise
+ */
+function bp_core_filter_edit_post_link( $edit_link = '', $post_id = 0 ) {
+	if ( 0 === $post_id ) {
+		$edit_link = false;
+	}
+
+	return $edit_link;
+}
+
+/**
+ * Should BuddyPress load the mentions scripts and related assets, including results to prime the
+ * mentions suggestions?
+ *
+ * @since 2.2.0
+ *
+ * @param bool $load_mentions    True to load mentions assets, false otherwise.
+ * @param bool $mentions_enabled True if mentions are enabled.
+ * @return bool True if mentions scripts should be loaded.
+ */
+function bp_maybe_load_mentions_scripts_for_blog_content( $load_mentions, $mentions_enabled ) {
+	if ( ! $mentions_enabled ) {
+		return $load_mentions;
+	}
+
+	if ( $load_mentions || ( bp_is_blog_page() && is_singular() && comments_open() ) ) {
+		return true;
+	}
+
+	return $load_mentions;
+}
+add_filter( 'bp_activity_maybe_load_mentions_scripts', 'bp_maybe_load_mentions_scripts_for_blog_content', 10, 2 );
+
+/**
+ * Injects specific BuddyPress CSS classes into a widget sidebar.
+ *
+ * Helps to standardize styling of BuddyPress widgets within a theme that
+ * does not use dynamic CSS classes in their widget sidebar's 'before_widget'
+ * call.
+ *
+ * @since 2.4.0
+ * @access private
+ *
+ * @global array $wp_registered_widgets Current registered widgets.
+ *
+ * @param array $params Current sidebar params.
+ * @return array
+ */
+function _bp_core_inject_bp_widget_css_class( $params ) {
+	global $wp_registered_widgets;
+
+	$widget_id = $params[0]['widget_id'];
+
+	// If callback isn't an array, bail.
+	if ( false === is_array( $wp_registered_widgets[ $widget_id ]['callback'] ) ) {
+		return $params;
+	}
+
+	// If the current widget isn't a BuddyPress one, stop!
+	// We determine if a widget is a BuddyPress widget, if the widget class
+	// begins with 'bp_'.
+	if ( 0 !== strpos( $wp_registered_widgets[ $widget_id ]['callback'][0]->id_base, 'bp_' ) ) {
+		return $params;
+	}
+
+	// Dynamically add our widget CSS classes for BP widgets if not already there.
+	$classes = array();
+
+	// Try to find 'widget' CSS class.
+	if ( false === strpos( $params[0]['before_widget'], 'widget ' ) ) {
+		$classes[] = 'widget';
+	}
+
+	// Try to find 'buddypress' CSS class.
+	if ( false === strpos( $params[0]['before_widget'], ' buddypress' ) ) {
+		$classes[] = 'buddypress';
+	}
+
+	// Stop if widget already has our CSS classes.
+	if ( empty( $classes ) ) {
+		return $params;
+	}
+
+	// CSS injection time!
+	$params[0]['before_widget'] = str_replace( 'class="', 'class="' . implode( ' ', $classes ) . ' ', $params[0]['before_widget'] );
+
+	return $params;
+}
+add_filter( 'dynamic_sidebar_params', '_bp_core_inject_bp_widget_css_class' );
+
+/**
+ * Add email link styles to rendered email template.
+ *
+ * This is only used when the email content has been merged into the email template.
+ *
+ * @since 2.5.0
+ *
+ * @param string $value         Property value.
+ * @param string $property_name Email template property name.
+ * @param string $transform     How the return value was transformed.
+ * @return string Updated value.
+ */
+function bp_email_add_link_color_to_template( $value, $property_name, $transform ) {
+	if ( $property_name !== 'template' || $transform !== 'add-content' ) {
+		return $value;
+	}
+
+	$settings    = bp_email_get_appearance_settings();
+	$replacement = 'style="color: ' . esc_attr( $settings['link_text_color'] ) . ';';
+
+	// Find all links.
+	preg_match_all( '#<a[^>]+>#i', $value, $links, PREG_SET_ORDER );
+	foreach ( $links as $link ) {
+		$new_link = $link = array_shift( $link );
+
+		// Add/modify style property.
+		if ( strpos( $link, 'style="' ) !== false ) {
+			$new_link = str_replace( 'style="', $replacement, $link );
+		} else {
+			$new_link = str_replace( '<a ', "<a {$replacement}\" ", $link );
+		}
+
+		if ( $new_link !== $link ) {
+			$value = str_replace( $link, $new_link, $value );
+		}
+	}
+
+	return $value;
+}
+add_filter( 'bp_email_get_property', 'bp_email_add_link_color_to_template', 6, 3 );
+
+/**
+ * Add custom headers to outgoing emails.
+ *
+ * @since 2.5.0
+ *
+ * @param array    $headers   Array of email headers.
+ * @param string   $property  Name of property. Unused.
+ * @param string   $transform Return value transformation. Unused.
+ * @param BP_Email $email     Email object reference.
+ * @return array
+ */
+function bp_email_set_default_headers( $headers, $property, $transform, $email ) {
+	$headers['X-BuddyPress']      = bp_get_version();
+	$headers['X-BuddyPress-Type'] = $email->get( 'type' );
+
+	$tokens = $email->get_tokens();
+
+	// Add 'List-Unsubscribe' header if applicable.
+	if ( ! empty( $tokens['unsubscribe'] ) && $tokens['unsubscribe'] !== wp_login_url() ) {
+		$user = get_user_by( 'email', $tokens['recipient.email'] );
+
+		$link = bp_email_get_unsubscribe_link( array(
+			'user_id'           => $user->ID,
+			'notification_type' => $email->get( 'type' ),
+		) );
+
+		if ( ! empty( $link ) ) {
+			$headers['List-Unsubscribe'] = sprintf( '<%s>', esc_url_raw( $link ) );
+		}
+	}
+
+	return $headers;
+}
+add_filter( 'bp_email_get_headers', 'bp_email_set_default_headers', 6, 4 );
+
+/**
+ * Add default email tokens.
+ *
+ * @since 2.5.0
+ *
+ * @param array    $tokens        Email tokens.
+ * @param string   $property_name Unused.
+ * @param string   $transform     Unused.
+ * @param BP_Email $email         Email being sent.
+ * @return array
+ */
+function bp_email_set_default_tokens( $tokens, $property_name, $transform, $email ) {
+	$tokens['site.admin-email'] = bp_get_option( 'admin_email' );
+	$tokens['site.url']         = home_url();
+	$tokens['email.subject']    = $email->get_subject();
+
+	// These options are escaped with esc_html on the way into the database in sanitize_option().
+	$tokens['site.description'] = wp_specialchars_decode( bp_get_option( 'blogdescription' ), ENT_QUOTES );
+	$tokens['site.name']        = wp_specialchars_decode( bp_get_option( 'blogname' ), ENT_QUOTES );
+
+	// Default values for tokens set conditionally below.
+	$tokens['email.preheader']     = '';
+	$tokens['recipient.email']     = '';
+	$tokens['recipient.name']      = '';
+	$tokens['recipient.username']  = '';
+
+	// Who is the email going to?
+	$recipient = $email->get( 'to' );
+	if ( $recipient ) {
+		$recipient = array_shift( $recipient );
+		$user_obj  = $recipient->get_user( 'search-email' );
+
+		$tokens['recipient.email'] = $recipient->get_address();
+		$tokens['recipient.name']  = $recipient->get_name();
+
+		if ( ! $user_obj && $tokens['recipient.email'] ) {
+			$user_obj = get_user_by( 'email', $tokens['recipient.email'] );
+		}
+
+		if ( $user_obj ) {
+			$tokens['recipient.username'] = $user_obj->user_login;
+
+			if ( bp_is_active( 'settings' ) && empty( $tokens['unsubscribe'] ) ) {
+				$tokens['unsubscribe'] = esc_url( sprintf(
+					'%s%s/notifications/',
+					bp_core_get_user_domain( $user_obj->ID ),
+					bp_get_settings_slug()
+				) );
+			}
+		}
+	}
+
+	// Set default unsubscribe link if not passed.
+	if ( empty( $tokens['unsubscribe'] ) ) {
+		$tokens['unsubscribe'] = wp_login_url();
+	}
+
+	// Email preheader.
+	$post = $email->get_post_object();
+	if ( $post ) {
+		$tokens['email.preheader'] = sanitize_text_field( get_post_meta( $post->ID, 'bp_email_preheader', true ) );
+	}
+
+	return $tokens;
+}
+add_filter( 'bp_email_get_tokens', 'bp_email_set_default_tokens', 6, 4 );
+
+/**
+ * Find and render the template for Email posts (the Customizer and admin previews).
+ *
+ * Misuses the `template_include` filter which expects a string, but as we need to replace
+ * the `{{{content}}}` token with the post's content, we use object buffering to load the
+ * template, replace the token, and render it.
+ *
+ * The function returns an empty string to prevent WordPress rendering another template.
+ *
+ * @since 2.5.0
+ *
+ * @param string $template Path to template (probably single.php).
+ * @return string
+ */
+function bp_core_render_email_template( $template ) {
+	if ( get_post_type() !== bp_get_email_post_type() || ! is_single() ) {
+		return $template;
+	}
+
+	/**
+	 * Filter template used to display Email posts.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $template Path to current template (probably single.php).
+	 */
+	$email_template = apply_filters( 'bp_core_render_email_template',
+		bp_locate_template( bp_email_get_template( get_queried_object() ), false ),
+		$template
+	);
+
+	if ( ! $email_template ) {
+		return $template;
+	}
+
+	ob_start();
+	include( $email_template );
+	$template = ob_get_contents();
+	ob_end_clean();
+
+	// Make sure we add a <title> tag so WP Customizer picks it up.
+	$template = str_replace( '<head>', '<head><title>' . esc_html_x( 'BuddyPress Emails', 'screen heading', 'buddypress' ) . '</title>', $template );
+	echo str_replace( '{{{content}}}', wpautop( get_post()->post_content ), $template );
+
+	/*
+	 * Link colours are applied directly in the email template before sending, so we
+	 * need to add an extra style here to set the colour for the Customizer or preview.
+	 */
+	$settings = bp_email_get_appearance_settings();
+	printf(
+		'<style>a { color: %s; }</style>',
+		esc_attr( $settings['highlight_color'] )
+	);
+
+	return '';
+}
+add_action( 'bp_template_include', 'bp_core_render_email_template', 12 );
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-functions.php	(revision 12265)
@@ -0,0 +1,3866 @@
+<?php
+/**
+ * BuddyPress Common Functions.
+ *
+ * @package BuddyPress
+ * @subpackage Functions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Versions ******************************************************************/
+
+/**
+ * Output the BuddyPress version.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_version() {
+	echo bp_get_version();
+}
+	/**
+	 * Return the BuddyPress version.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return string The BuddyPress version.
+	 */
+	function bp_get_version() {
+		return buddypress()->version;
+	}
+
+/**
+ * Output the BuddyPress database version.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_db_version() {
+	echo bp_get_db_version();
+}
+	/**
+	 * Return the BuddyPress database version.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return string The BuddyPress database version.
+	 */
+	function bp_get_db_version() {
+		return buddypress()->db_version;
+	}
+
+/**
+ * Output the BuddyPress database version.
+ *
+ * @since 1.6.0
+ *
+ */
+function bp_db_version_raw() {
+	echo bp_get_db_version_raw();
+}
+	/**
+	 * Return the BuddyPress database version.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return string The BuddyPress version direct from the database.
+	 */
+	function bp_get_db_version_raw() {
+		$bp = buddypress();
+		return !empty( $bp->db_version_raw ) ? $bp->db_version_raw : 0;
+	}
+
+/** Functions *****************************************************************/
+
+/**
+ * Get the $wpdb base prefix, run through the 'bp_core_get_table_prefix' filter.
+ *
+ * The filter is intended primarily for use in multinetwork installations.
+ *
+ * @since 1.2.6
+ *
+ * @global object $wpdb WordPress database object.
+ *
+ * @return string Filtered database prefix.
+ */
+function bp_core_get_table_prefix() {
+	global $wpdb;
+
+	/**
+	 * Filters the $wpdb base prefix.
+	 *
+	 * Intended primarily for use in multinetwork installations.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param string $base_prefix Base prefix to use.
+	 */
+	return apply_filters( 'bp_core_get_table_prefix', $wpdb->base_prefix );
+}
+
+/**
+ * Sort an array of objects or arrays by a specific key/property.
+ *
+ * The main purpose for this function is so that you can avoid having to create
+ * your own awkward callback function for usort().
+ *
+ * @since 2.2.0
+ * @since 2.7.0 Added $preserve_keys parameter.
+ *
+ * @param array      $items         The items to be sorted. Its constituent items
+ *                                  can be either associative arrays or objects.
+ * @param string|int $key           The array index or property name to sort by.
+ * @param string     $type          Sort type. 'alpha' for alphabetical, 'num'
+ *                                  for numeric. Default: 'alpha'.
+ * @param bool       $preserve_keys Whether to keep the keys or not.
+ *
+ * @return array $items The sorted array.
+ */
+function bp_sort_by_key( $items, $key, $type = 'alpha', $preserve_keys = false ) {
+	$callback = function( $a, $b ) use ( $key, $type ) {
+		$values = array( 0 => false, 1 => false );
+		foreach ( func_get_args() as $indexi => $index ) {
+			if ( isset( $index->{$key} ) ) {
+				$values[ $indexi ] = $index->{$key};
+			} elseif ( isset( $index[ $key ] ) ) {
+				$values[ $indexi ] = $index[ $key ];
+			}
+		}
+
+		if ( isset( $values[0], $values[1] ) ) {
+			if ( 'num' === $type ) {
+				$cmp = $values[0] - $values[1];
+			} else {
+				$cmp = strcmp( $values[0], $values[1] );
+			}
+
+			if ( 0 > $cmp ) {
+				$retval = -1;
+			} elseif ( 0 < $cmp ) {
+				$retval = 1;
+			} else {
+				$retval = 0;
+			}
+			return $retval;
+		} else {
+			return 0;
+		}
+	};
+
+	if ( true === $preserve_keys ) {
+		uasort( $items, $callback );
+	} else {
+		usort( $items, $callback );
+	}
+
+	return $items;
+}
+
+/**
+ * Sort an array of objects or arrays by alphabetically sorting by a specific key/property.
+ *
+ * For instance, if you have an array of WordPress post objects, you can sort
+ * them by post_name as follows:
+ *     $sorted_posts = bp_alpha_sort_by_key( $posts, 'post_name' );
+ *
+ * @since 1.9.0
+ *
+ * @param array      $items The items to be sorted. Its constituent items can be either associative arrays or objects.
+ * @param string|int $key   The array index or property name to sort by.
+ * @return array $items The sorted array.
+ */
+function bp_alpha_sort_by_key( $items, $key ) {
+	return bp_sort_by_key( $items, $key, 'alpha' );
+}
+
+/**
+ * Format numbers the BuddyPress way.
+ *
+ * @since 1.2.0
+ *
+ * @param int  $number   The number to be formatted.
+ * @param bool $decimals Whether to use decimals. See {@link number_format_i18n()}.
+ * @return string The formatted number.
+ */
+function bp_core_number_format( $number = 0, $decimals = false ) {
+
+	// Force number to 0 if needed.
+	if ( ! is_numeric( $number ) ) {
+		$number = 0;
+	}
+
+	/**
+	 * Filters the BuddyPress formatted number.
+	 *
+	 * @since 1.2.4
+	 *
+	 * @param string $value    BuddyPress formatted value.
+	 * @param int    $number   The number to be formatted.
+	 * @param bool   $decimals Whether or not to use decimals.
+	 */
+	return apply_filters( 'bp_core_number_format', number_format_i18n( $number, $decimals ), $number, $decimals );
+}
+
+/**
+ * A utility for parsing individual function arguments into an array.
+ *
+ * The purpose of this function is to help with backward compatibility in cases where
+ *
+ *   function foo( $bar = 1, $baz = false, $barry = array(), $blip = false ) { // ...
+ *
+ * is deprecated in favor of
+ *
+ *   function foo( $args = array() ) {
+ *       $defaults = array(
+ *           'bar'  => 1,
+ *           'arg2' => false,
+ *           'arg3' => array(),
+ *           'arg4' => false,
+ *       );
+ *       $r = wp_parse_args( $args, $defaults ); // ...
+ *
+ * The first argument, $old_args_keys, is an array that matches the parameter positions (keys) to
+ * the new $args keys (values):
+ *
+ *   $old_args_keys = array(
+ *       0 => 'bar', // because $bar was the 0th parameter for foo()
+ *       1 => 'baz', // because $baz was the 1st parameter for foo()
+ *       2 => 'barry', // etc
+ *       3 => 'blip'
+ *   );
+ *
+ * For the second argument, $func_args, you should just pass the value of func_get_args().
+ *
+ * @since 1.6.0
+ *
+ * @param array $old_args_keys Old argument indexs, keyed to their positions.
+ * @param array $func_args     The parameters passed to the originating function.
+ * @return array $new_args The parsed arguments.
+ */
+function bp_core_parse_args_array( $old_args_keys, $func_args ) {
+	$new_args = array();
+
+	foreach( $old_args_keys as $arg_num => $arg_key ) {
+		if ( isset( $func_args[$arg_num] ) ) {
+			$new_args[$arg_key] = $func_args[$arg_num];
+		}
+	}
+
+	return $new_args;
+}
+
+/**
+ * Merge user defined arguments into defaults array.
+ *
+ * This function is used throughout BuddyPress to allow for either a string or
+ * array to be merged into another array. It is identical to wp_parse_args()
+ * except it allows for arguments to be passively or aggressively filtered using
+ * the optional $filter_key parameter. If no $filter_key is passed, no filters
+ * are applied.
+ *
+ * @since 2.0.0
+ *
+ * @param string|array $args       Value to merge with $defaults.
+ * @param array        $defaults   Array that serves as the defaults.
+ * @param string       $filter_key String to key the filters from.
+ * @return array Merged user defined values with defaults.
+ */
+function bp_parse_args( $args, $defaults = array(), $filter_key = '' ) {
+
+	// Setup a temporary array from $args.
+	if ( is_object( $args ) ) {
+		$r = get_object_vars( $args );
+	} elseif ( is_array( $args ) ) {
+		$r =& $args;
+	} else {
+		wp_parse_str( $args, $r );
+	}
+
+	// Passively filter the args before the parse.
+	if ( !empty( $filter_key ) ) {
+
+		/**
+		 * Filters the arguments key before parsing if filter key provided.
+		 *
+		 * This is a dynamic filter dependent on the specified key.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $r Array of arguments to use.
+		 */
+		$r = apply_filters( 'bp_before_' . $filter_key . '_parse_args', $r );
+	}
+
+	// Parse.
+	if ( is_array( $defaults ) && !empty( $defaults ) ) {
+		$r = array_merge( $defaults, $r );
+	}
+
+	// Aggressively filter the args after the parse.
+	if ( !empty( $filter_key ) ) {
+
+		/**
+		 * Filters the arguments key after parsing if filter key provided.
+		 *
+		 * This is a dynamic filter dependent on the specified key.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $r Array of parsed arguments.
+		 */
+		$r = apply_filters( 'bp_after_' . $filter_key . '_parse_args', $r );
+	}
+
+	// Return the parsed results.
+	return $r;
+}
+
+/**
+ * Sanitizes a pagination argument based on both the request override and the
+ * original value submitted via a query argument, likely to a template class
+ * responsible for limiting the resultset of a template loop.
+ *
+ * @since 2.2.0
+ *
+ * @param string $page_arg The $_REQUEST argument to look for.
+ * @param int    $page     The original page value to fall back to.
+ * @return int A sanitized integer value, good for pagination.
+ */
+function bp_sanitize_pagination_arg( $page_arg = '', $page = 1 ) {
+
+	// Check if request overrides exist.
+	if ( isset( $_REQUEST[ $page_arg ] ) ) {
+
+		// Get the absolute integer value of the override.
+		$int = absint( $_REQUEST[ $page_arg ] );
+
+		// If override is 0, do not use it. This prevents unlimited result sets.
+		// @see https://buddypress.trac.wordpress.org/ticket/5796.
+		if ( $int ) {
+			$page = $int;
+		}
+	}
+
+	return intval( $page );
+}
+
+/**
+ * Sanitize an 'order' parameter for use in building SQL queries.
+ *
+ * Strings like 'DESC', 'desc', ' desc' will be interpreted into 'DESC'.
+ * Everything else becomes 'ASC'.
+ *
+ * @since 1.8.0
+ *
+ * @param string $order The 'order' string, as passed to the SQL constructor.
+ * @return string The sanitized value 'DESC' or 'ASC'.
+ */
+function bp_esc_sql_order( $order = '' ) {
+	$order = strtoupper( trim( $order ) );
+	return 'DESC' === $order ? 'DESC' : 'ASC';
+}
+
+/**
+ * Escape special characters in a SQL LIKE clause.
+ *
+ * In WordPress 4.0, like_escape() was deprecated, due to incorrect
+ * documentation and improper sanitization leading to a history of misuse. To
+ * maintain compatibility with versions of WP before 4.0, we duplicate the
+ * logic of the replacement, wpdb::esc_like().
+ *
+ * @since 2.1.0
+ *
+ * @see wpdb::esc_like() for more details on proper use.
+ *
+ * @param string $text The raw text to be escaped.
+ * @return string Text in the form of a LIKE phrase. Not SQL safe. Run through
+ *                wpdb::prepare() before use.
+ */
+function bp_esc_like( $text ) {
+	global $wpdb;
+
+	if ( method_exists( $wpdb, 'esc_like' ) ) {
+		return $wpdb->esc_like( $text );
+	} else {
+		return addcslashes( $text, '_%\\' );
+	}
+}
+
+/**
+ * Are we running username compatibility mode?
+ *
+ * @since 1.5.0
+ *
+ * @todo Move to members component?
+ *
+ * @return bool False when compatibility mode is disabled, true when enabled.
+ *              Default: false.
+ */
+function bp_is_username_compatibility_mode() {
+
+	/**
+	 * Filters whether or not to use username compatibility mode.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $value Whether or not username compatibility mode should be used.
+	 */
+	return apply_filters( 'bp_is_username_compatibility_mode', defined( 'BP_ENABLE_USERNAME_COMPATIBILITY_MODE' ) && BP_ENABLE_USERNAME_COMPATIBILITY_MODE );
+}
+
+/**
+ * Should we use the WP Toolbar?
+ *
+ * The WP Toolbar, introduced in WP 3.1, is fully supported in BuddyPress as
+ * of BP 1.5. For BP 1.6, the WP Toolbar is the default.
+ *
+ * @since 1.5.0
+ *
+ * @return bool Default: true. False when WP Toolbar support is disabled.
+ */
+function bp_use_wp_admin_bar() {
+
+	// Default to true (to avoid loading deprecated BuddyBar code).
+	$use_admin_bar = true;
+
+	// Has the WP Toolbar constant been explicitly opted into?
+	if ( defined( 'BP_USE_WP_ADMIN_BAR' ) ) {
+		$use_admin_bar = (bool) BP_USE_WP_ADMIN_BAR;
+
+	// ...or is the old BuddyBar being forced back into use?
+	} elseif ( bp_force_buddybar( false ) ) {
+		$use_admin_bar = false;
+	}
+
+	/**
+	 * Filters whether or not to use the admin bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $use_admin_bar Whether or not to use the admin bar.
+	 */
+	return (bool) apply_filters( 'bp_use_wp_admin_bar', $use_admin_bar );
+}
+
+
+/**
+ * Return the parent forum ID for the Legacy Forums abstraction layer.
+ *
+ * @since 1.5.0
+ * @since 3.0.0 Supported for compatibility with bbPress 2.
+ *
+ * @return int Forum ID.
+ */
+function bp_forums_parent_forum_id() {
+
+	/**
+	 * Filters the parent forum ID for the bbPress abstraction layer.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int BP_FORUMS_PARENT_FORUM_ID The Parent forum ID constant.
+	 */
+	return apply_filters( 'bp_forums_parent_forum_id', BP_FORUMS_PARENT_FORUM_ID );
+}
+
+/** Directory *****************************************************************/
+
+/**
+ * Returns an array of core component IDs.
+ *
+ * @since 2.1.0
+ *
+ * @return array
+ */
+function bp_core_get_packaged_component_ids() {
+	$components = array(
+		'activity',
+		'members',
+		'groups',
+		'blogs',
+		'xprofile',
+		'friends',
+		'messages',
+		'settings',
+		'notifications',
+	);
+
+	return $components;
+}
+
+/**
+ * Fetch a list of BP directory pages from the appropriate meta table.
+ *
+ * @since 1.5.0
+ *
+ * @param string $status 'active' to return only pages associated with active components, 'all' to return all saved
+ *                       pages. When running save routines, use 'all' to avoid removing data related to inactive
+ *                       components. Default: 'active'.
+ * @return array|string An array of page IDs, keyed by component names, or an
+ *                      empty string if the list is not found.
+ */
+function bp_core_get_directory_page_ids( $status = 'active' ) {
+	$page_ids = bp_get_option( 'bp-pages', array() );
+
+	// Loop through pages
+	foreach ( $page_ids as $component_name => $page_id ) {
+
+		// Ensure that empty indexes are unset. Should only matter in edge cases.
+		if ( empty( $component_name ) || empty( $page_id ) ) {
+			unset( $page_ids[ $component_name ] );
+		}
+
+		// Trashed pages should never appear in results.
+		if ( 'trash' == get_post_status( $page_id ) ) {
+			unset( $page_ids[ $component_name ] );
+		}
+
+		// 'register' and 'activate' do not have components, but should be whitelisted.
+		if ( in_array( $component_name, array( 'register', 'activate' ), true ) ) {
+			continue;
+		}
+
+		// Remove inactive component pages.
+		if ( ( 'active' === $status ) && ! bp_is_active( $component_name ) ) {
+			unset( $page_ids[ $component_name ] );
+		}
+	}
+
+	/**
+	 * Filters the list of BP directory pages from the appropriate meta table.
+	 *
+	 * @since 1.5.0
+	 * @since 2.9.0 Add $status parameter
+	 *
+	 * @param array  $page_ids Array of directory pages.
+	 * @param string $status   Page status to limit results to
+	 */
+	return (array) apply_filters( 'bp_core_get_directory_page_ids', $page_ids, $status );
+}
+
+/**
+ * Get the page ID corresponding to a component directory.
+ *
+ * @since 2.6.0
+ *
+ * @param string|null $component The slug representing the component. Defaults to the current component.
+ * @return int|false The ID of the directory page associated with the component. False if none is found.
+ */
+function bp_core_get_directory_page_id( $component = null ) {
+	if ( ! $component ) {
+		$component = bp_current_component();
+	}
+
+	$bp_pages = bp_core_get_directory_page_ids( 'all' );
+
+	$page_id = false;
+	if ( $component && isset( $bp_pages[ $component ] ) ) {
+		$page_id = (int) $bp_pages[ $component ];
+	}
+
+	return $page_id;
+}
+
+/**
+ * Store the list of BP directory pages in the appropriate meta table.
+ *
+ * The bp-pages data is stored in site_options (falls back to options on non-MS),
+ * in an array keyed by blog_id. This allows you to change your
+ * bp_get_root_blog_id() and go through the setup process again.
+ *
+ * @since 1.5.0
+ *
+ * @param array $blog_page_ids The IDs of the WP pages corresponding to BP
+ *                             component directories.
+ */
+function bp_core_update_directory_page_ids( $blog_page_ids ) {
+	bp_update_option( 'bp-pages', $blog_page_ids );
+}
+
+/**
+ * Get names and slugs for BuddyPress component directory pages.
+ *
+ * @since 1.5.0
+ *
+ * @return object Page names, IDs, and slugs.
+ */
+function bp_core_get_directory_pages() {
+	global $wpdb;
+
+	// Look in cache first.
+	$pages = wp_cache_get( 'directory_pages', 'bp_pages' );
+
+	if ( false === $pages ) {
+
+		// Set pages as standard class.
+		$pages = new stdClass;
+
+		// Get pages and IDs.
+		$page_ids = bp_core_get_directory_page_ids();
+		if ( !empty( $page_ids ) ) {
+
+			// Always get page data from the root blog, except on multiblog mode, when it comes
+			// from the current blog.
+			$posts_table_name = bp_is_multiblog_mode() ? $wpdb->posts : $wpdb->get_blog_prefix( bp_get_root_blog_id() ) . 'posts';
+			$page_ids_sql     = implode( ',', wp_parse_id_list( $page_ids ) );
+			$page_names       = $wpdb->get_results( "SELECT ID, post_name, post_parent, post_title FROM {$posts_table_name} WHERE ID IN ({$page_ids_sql}) AND post_status = 'publish' " );
+
+			foreach ( (array) $page_ids as $component_id => $page_id ) {
+				foreach ( (array) $page_names as $page_name ) {
+					if ( $page_name->ID == $page_id ) {
+						if ( !isset( $pages->{$component_id} ) || !is_object( $pages->{$component_id} ) ) {
+							$pages->{$component_id} = new stdClass;
+						}
+
+						$pages->{$component_id}->name  = $page_name->post_name;
+						$pages->{$component_id}->id    = $page_name->ID;
+						$pages->{$component_id}->title = $page_name->post_title;
+						$slug[]                        = $page_name->post_name;
+
+						// Get the slug.
+						while ( $page_name->post_parent != 0 ) {
+							$parent                 = $wpdb->get_results( $wpdb->prepare( "SELECT post_name, post_parent FROM {$posts_table_name} WHERE ID = %d", $page_name->post_parent ) );
+							$slug[]                 = $parent[0]->post_name;
+							$page_name->post_parent = $parent[0]->post_parent;
+						}
+
+						$pages->{$component_id}->slug = implode( '/', array_reverse( (array) $slug ) );
+					}
+
+					unset( $slug );
+				}
+			}
+		}
+
+		wp_cache_set( 'directory_pages', $pages, 'bp_pages' );
+	}
+
+	/**
+	 * Filters the names and slugs for BuddyPress component directory pages.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param object $pages Object holding page names and slugs.
+	 */
+	return apply_filters( 'bp_core_get_directory_pages', $pages );
+}
+
+/**
+ * Creates necessary directory pages.
+ *
+ * Directory pages are those WordPress pages used by BP components to display
+ * content (eg, the 'groups' page created by BP).
+ *
+ * @since 1.7.0
+ *
+ * @param array  $components Components to create pages for.
+ * @param string $existing   'delete' if you want to delete existing page mappings
+ *                           and replace with new ones. Otherwise existing page mappings
+ *                           are kept, and the gaps filled in with new pages. Default: 'keep'.
+ */
+function bp_core_add_page_mappings( $components, $existing = 'keep' ) {
+
+	// If no value is passed, there's nothing to do.
+	if ( empty( $components ) ) {
+		return;
+	}
+
+	// Make sure that the pages are created on the root blog no matter which
+	// dashboard the setup is being run on.
+	if ( ! bp_is_root_blog() ) {
+		switch_to_blog( bp_get_root_blog_id() );
+	}
+
+	$pages = bp_core_get_directory_page_ids( 'all' );
+
+	// Delete any existing pages.
+	if ( 'delete' === $existing ) {
+		foreach ( $pages as $page_id ) {
+			wp_delete_post( $page_id, true );
+		}
+
+		$pages = array();
+	}
+
+	$page_titles = bp_core_get_directory_page_default_titles();
+
+	$pages_to_create = array();
+	foreach ( array_keys( $components ) as $component_name ) {
+		if ( ! isset( $pages[ $component_name ] ) && isset( $page_titles[ $component_name ] ) ) {
+			$pages_to_create[ $component_name ] = $page_titles[ $component_name ];
+		}
+	}
+
+	// Register and Activate are not components, but need pages when
+	// registration is enabled.
+	if ( bp_get_signup_allowed() ) {
+		foreach ( array( 'register', 'activate' ) as $slug ) {
+			if ( ! isset( $pages[ $slug ] ) ) {
+				$pages_to_create[ $slug ] = $page_titles[ $slug ];
+			}
+		}
+	}
+
+	// No need for a Sites directory unless we're on multisite.
+	if ( ! is_multisite() && isset( $pages_to_create['blogs'] ) ) {
+		unset( $pages_to_create['blogs'] );
+	}
+
+	// Members must always have a page, no matter what.
+	if ( ! isset( $pages['members'] ) && ! isset( $pages_to_create['members'] ) ) {
+		$pages_to_create['members'] = $page_titles['members'];
+	}
+
+	// Create the pages.
+	foreach ( $pages_to_create as $component_name => $page_name ) {
+		$exists = get_page_by_path( $component_name );
+
+		// If page already exists, use it.
+		if ( ! empty( $exists ) ) {
+			$pages[ $component_name ] = $exists->ID;
+		} else {
+			$pages[ $component_name ] = wp_insert_post( array(
+				'comment_status' => 'closed',
+				'ping_status'    => 'closed',
+				'post_status'    => 'publish',
+				'post_title'     => $page_name,
+				'post_type'      => 'page',
+			) );
+		}
+	}
+
+	// Save the page mapping.
+	bp_update_option( 'bp-pages', $pages );
+
+	// If we had to switch_to_blog, go back to the original site.
+	if ( ! bp_is_root_blog() ) {
+		restore_current_blog();
+	}
+}
+
+/**
+ * Get the default page titles for BP directory pages.
+ *
+ * @since 2.7.0
+ *
+ * @return array
+ */
+function bp_core_get_directory_page_default_titles() {
+	$page_default_titles = array(
+		'activity' => _x( 'Activity', 'Page title for the Activity directory.',       'buddypress' ),
+		'groups'   => _x( 'Groups',   'Page title for the Groups directory.',         'buddypress' ),
+		'blogs'    => _x( 'Sites',    'Page title for the Sites directory.',          'buddypress' ),
+		'members'  => _x( 'Members',  'Page title for the Members directory.',        'buddypress' ),
+		'activate' => _x( 'Activate', 'Page title for the user activation screen.',   'buddypress' ),
+		'register' => _x( 'Register', 'Page title for the user registration screen.', 'buddypress' ),
+	);
+
+	/**
+	 * Filters the default page titles array
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $page_default_titles the array of default WP (post_title) titles.
+	 */
+	return apply_filters( 'bp_core_get_directory_page_default_titles', $page_default_titles );
+}
+
+/**
+ * Remove the entry from bp_pages when the corresponding WP page is deleted.
+ *
+ * Bails early on multisite installations when not viewing the root site.
+ *
+ * @link https://buddypress.trac.wordpress.org/ticket/6226
+ *
+ * @since 2.2.0
+ *
+ * @param int $post_id Post ID.
+ */
+function bp_core_on_directory_page_delete( $post_id ) {
+
+	// Stop if we are not on the main BP root blog.
+	if ( ! bp_is_root_blog() ) {
+		return;
+	}
+
+	$page_ids       = bp_core_get_directory_page_ids( 'all' );
+	$component_name = array_search( $post_id, $page_ids );
+
+	if ( ! empty( $component_name ) ) {
+		unset( $page_ids[ $component_name ] );
+	}
+
+	bp_core_update_directory_page_ids( $page_ids );
+}
+add_action( 'delete_post', 'bp_core_on_directory_page_delete' );
+
+/**
+ * Create a default component slug from a WP page root_slug.
+ *
+ * Since 1.5, BP components get their root_slug (the slug used immediately
+ * following the root domain) from the slug of a corresponding WP page.
+ *
+ * E.g. if your BP installation at example.com has its members page at
+ * example.com/community/people, $bp->members->root_slug will be
+ * 'community/people'.
+ *
+ * By default, this function creates a shorter version of the root_slug for
+ * use elsewhere in the URL, by returning the content after the final '/'
+ * in the root_slug ('people' in the example above).
+ *
+ * Filter on 'bp_core_component_slug_from_root_slug' to override this method
+ * in general, or define a specific component slug constant (e.g.
+ * BP_MEMBERS_SLUG) to override specific component slugs.
+ *
+ * @since 1.5.0
+ *
+ * @param string $root_slug The root slug, which comes from $bp->pages->[component]->slug.
+ * @return string The short slug for use in the middle of URLs.
+ */
+function bp_core_component_slug_from_root_slug( $root_slug ) {
+	$slug_chunks = explode( '/', $root_slug );
+	$slug        = array_pop( $slug_chunks );
+
+	/**
+	 * Filters the default component slug from a WP page root_slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $slug      Short slug for use in the middle of URLs.
+	 * @param string $root_slug The root slug which comes from $bp->pages-[component]->slug.
+	 */
+	return apply_filters( 'bp_core_component_slug_from_root_slug', $slug, $root_slug );
+}
+
+/**
+ * Add support for a top-level ("root") component.
+ *
+ * This function originally (pre-1.5) let plugins add support for pages in the
+ * root of the install. These root level pages are now handled by actual
+ * WordPress pages and this function is now a convenience for compatibility
+ * with the new method.
+ *
+ * @since 1.0.0
+ *
+ * @param string $slug The slug of the component being added to the root list.
+ */
+function bp_core_add_root_component( $slug ) {
+	$bp = buddypress();
+
+	if ( empty( $bp->pages ) ) {
+		$bp->pages = bp_core_get_directory_pages();
+	}
+
+	$match = false;
+
+	// Check if the slug is registered in the $bp->pages global.
+	foreach ( (array) $bp->pages as $key => $page ) {
+		if ( $key == $slug || $page->slug == $slug ) {
+			$match = true;
+		}
+	}
+
+	// Maybe create the add_root array.
+	if ( empty( $bp->add_root ) ) {
+		$bp->add_root = array();
+	}
+
+	// If there was no match, add a page for this root component.
+	if ( empty( $match ) ) {
+		$add_root_items   = $bp->add_root;
+		$add_root_items[] = $slug;
+		$bp->add_root     = $add_root_items;
+	}
+
+	// Make sure that this component is registered as requiring a top-level directory.
+	if ( isset( $bp->{$slug} ) ) {
+		$bp->loaded_components[$bp->{$slug}->slug] = $bp->{$slug}->id;
+		$bp->{$slug}->has_directory = true;
+	}
+}
+
+/**
+ * Create WordPress pages to be used as BP component directories.
+ *
+ * @since 1.5.0
+ */
+function bp_core_create_root_component_page() {
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	$new_page_ids = array();
+
+	foreach ( (array) $bp->add_root as $slug ) {
+		$new_page_ids[ $slug ] = wp_insert_post( array(
+			'comment_status' => 'closed',
+			'ping_status'    => 'closed',
+			'post_title'     => ucwords( $slug ),
+			'post_status'    => 'publish',
+			'post_type'      => 'page'
+		) );
+	}
+
+	$page_ids = array_merge( $new_page_ids, bp_core_get_directory_page_ids( 'all' ) );
+	bp_core_update_directory_page_ids( $page_ids );
+}
+
+/**
+ * Add illegal blog names to WP so that root components will not conflict with blog names on a subdirectory installation.
+ *
+ * For example, it would stop someone creating a blog with the slug "groups".
+ *
+ * @since 1.0.0
+ *
+ * @todo Deprecate?
+ */
+function bp_core_add_illegal_names() {
+	update_site_option( 'illegal_names', get_site_option( 'illegal_names' ), array() );
+}
+
+/**
+ * Get the 'search' query argument for a given component.
+ *
+ * @since 2.4.0
+ * @since 2.7.0 The `$component` parameter was made optional, with the current component
+ *              as the fallback value.
+ *
+ * @param string|null $component Optional. Component name. Defaults to current component.
+ * @return string|bool Query argument on success. False on failure.
+ */
+function bp_core_get_component_search_query_arg( $component = null ) {
+	if ( ! $component ) {
+		$component = bp_current_component();
+	}
+
+	$query_arg = false;
+	if ( isset( buddypress()->{$component}->search_query_arg ) ) {
+		$query_arg = sanitize_title( buddypress()->{$component}->search_query_arg );
+	}
+
+	/**
+	 * Filters the query arg for a component search string.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $query_arg Query argument.
+	 * @param string $component Component name.
+	 */
+	return apply_filters( 'bp_core_get_component_search_query_arg', $query_arg, $component );
+}
+
+/**
+ * Determine whether BuddyPress should register the bp-themes directory.
+ *
+ * @since 1.9.0
+ *
+ * @return bool True if bp-themes should be registered, false otherwise.
+ */
+function bp_do_register_theme_directory() {
+	// If bp-default exists in another theme directory, bail.
+	// This ensures that the version of bp-default in the regular themes
+	// directory will always take precedence, as part of a migration away
+	// from the version packaged with BuddyPress.
+	foreach ( array_values( (array) $GLOBALS['wp_theme_directories'] ) as $directory ) {
+		if ( is_dir( $directory . '/bp-default' ) ) {
+			return false;
+		}
+	}
+
+	// If the current theme is bp-default (or a bp-default child), BP
+	// should register its directory.
+	$register = 'bp-default' === get_stylesheet() || 'bp-default' === get_template();
+
+	// Legacy sites continue to have the theme registered.
+	if ( empty( $register ) && ( 1 == get_site_option( '_bp_retain_bp_default' ) ) ) {
+		$register = true;
+	}
+
+	/**
+	 * Filters whether BuddyPress should register the bp-themes directory.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param bool $register If bp-themes should be registered.
+	 */
+	return apply_filters( 'bp_do_register_theme_directory', $register );
+}
+
+/** URI ***********************************************************************/
+
+/**
+ * Return the domain for the root blog.
+ *
+ * Eg: http://example.com OR https://example.com
+ *
+ * @since 1.0.0
+ *
+ * @return string The domain URL for the blog.
+ */
+function bp_core_get_root_domain() {
+
+	$domain = get_home_url( bp_get_root_blog_id() );
+
+	/**
+	 * Filters the domain for the root blog.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string $domain The domain URL for the blog.
+	 */
+	return apply_filters( 'bp_core_get_root_domain', $domain );
+}
+
+/**
+ * Perform a status-safe wp_redirect() that is compatible with BP's URI parser.
+ *
+ * @since 1.0.0
+ *
+ * @param string $location The redirect URL.
+ * @param int    $status   Optional. The numeric code to give in the redirect
+ *                         headers. Default: 302.
+ */
+function bp_core_redirect( $location = '', $status = 302 ) {
+
+	// On some setups, passing the value of wp_get_referer() may result in an
+	// empty value for $location, which results in an error. Ensure that we
+	// have a valid URL.
+	if ( empty( $location ) ) {
+		$location = bp_get_root_domain();
+	}
+
+	// Make sure we don't call status_header() in bp_core_do_catch_uri() as this
+	// conflicts with wp_redirect() and wp_safe_redirect().
+	buddypress()->no_status_set = true;
+
+	wp_safe_redirect( $location, $status );
+
+	// If PHPUnit is running, do not kill execution.
+	if ( ! defined( 'BP_TESTS_DIR' ) ) {
+		die;
+	}
+}
+
+/**
+ * Return the URL path of the referring page.
+ *
+ * This is a wrapper for `wp_get_referer()` that sanitizes the referer URL to
+ * a webroot-relative path. For example, 'http://example.com/foo/' will be
+ * reduced to '/foo/'.
+ *
+ * @since 2.3.0
+ *
+ * @return bool|string Returns false on error, a URL path on success.
+ */
+function bp_get_referer_path() {
+	$referer = wp_get_referer();
+
+	if ( false === $referer ) {
+		return false;
+	}
+
+	// Turn into an absolute path.
+	$referer = preg_replace( '|https?\://[^/]+/|', '/', $referer );
+
+	return $referer;
+}
+
+/**
+ * Get the path of the current site.
+ *
+ * @since 1.0.0
+ *
+ * @global object $current_site
+ *
+ * @return string URL to the current site.
+ */
+function bp_core_get_site_path() {
+	global $current_site;
+
+	if ( is_multisite() ) {
+		$site_path = $current_site->path;
+	} else {
+		$site_path = (array) explode( '/', home_url() );
+
+		if ( count( $site_path ) < 2 ) {
+			$site_path = '/';
+		} else {
+			// Unset the first three segments (http(s)://example.com part).
+			unset( $site_path[0] );
+			unset( $site_path[1] );
+			unset( $site_path[2] );
+
+			if ( !count( $site_path ) ) {
+				$site_path = '/';
+			} else {
+				$site_path = '/' . implode( '/', $site_path ) . '/';
+			}
+		}
+	}
+
+	/**
+	 * Filters the path of the current site.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $site_path URL to the current site.
+	 */
+	return apply_filters( 'bp_core_get_site_path', $site_path );
+}
+
+/** Time **********************************************************************/
+
+/**
+ * Get the current GMT time to save into the DB.
+ *
+ * @since 1.2.6
+ *
+ * @param bool   $gmt  True to use GMT (rather than local) time. Default: true.
+ * @param string $type See the 'type' parameter in {@link current_time()}.
+ *                     Default: 'mysql'.
+ * @return string Current time in 'Y-m-d h:i:s' format.
+ */
+function bp_core_current_time( $gmt = true, $type = 'mysql' ) {
+
+	/**
+	 * Filters the current GMT time to save into the DB.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param string $value Current GMT time.
+	 */
+	return apply_filters( 'bp_core_current_time', current_time( $type, $gmt ) );
+}
+
+/**
+ * Get an English-language representation of the time elapsed since a given date.
+ *
+ * Based on function created by Dunstan Orchard - http://1976design.com
+ *
+ * This function will return an English representation of the time elapsed
+ * since a given date.
+ * eg: 2 hours and 50 minutes
+ * eg: 4 days
+ * eg: 4 weeks and 6 days
+ *
+ * Note that fractions of minutes are not represented in the return string. So
+ * an interval of 3 minutes will be represented by "3 minutes ago", as will an
+ * interval of 3 minutes 59 seconds.
+ *
+ * @since 1.0.0
+ *
+ * @param int|string $older_date The earlier time from which you're calculating
+ *                               the time elapsed. Enter either as an integer Unix timestamp,
+ *                               or as a date string of the format 'Y-m-d h:i:s'.
+ * @param int|bool   $newer_date Optional. Unix timestamp of date to compare older
+ *                               date to. Default: false (current time).
+ * @return string String representing the time since the older date, eg
+ *         "2 hours and 50 minutes".
+ */
+function bp_core_time_since( $older_date, $newer_date = false ) {
+
+	/**
+	 * Filters whether or not to bypass BuddyPress' time_since calculations.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool   $value      Whether or not to bypass.
+	 * @param string $older_date Earlier time from which we're calculating time elapsed.
+	 * @param string $newer_date Unix timestamp of date to compare older time to.
+	 */
+	$pre_value = apply_filters( 'bp_core_time_since_pre', false, $older_date, $newer_date );
+	if ( false !== $pre_value ) {
+		return $pre_value;
+	}
+
+	/**
+	 * Filters the value to use if the time since is unknown.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value String representing the time since the older date.
+	 */
+	$unknown_text   = apply_filters( 'bp_core_time_since_unknown_text',   __( 'sometime',  'buddypress' ) );
+
+	/**
+	 * Filters the value to use if the time since is right now.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value String representing the time since the older date.
+	 */
+	$right_now_text = apply_filters( 'bp_core_time_since_right_now_text', __( 'right now', 'buddypress' ) );
+
+	/**
+	 * Filters the value to use if the time since is some time ago.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value String representing the time since the older date.
+	 */
+	$ago_text       = apply_filters( 'bp_core_time_since_ago_text',       __( '%s ago',    'buddypress' ) );
+
+	// Array of time period chunks.
+	$chunks = array(
+		YEAR_IN_SECONDS,
+		30 * DAY_IN_SECONDS,
+		WEEK_IN_SECONDS,
+		DAY_IN_SECONDS,
+		HOUR_IN_SECONDS,
+		MINUTE_IN_SECONDS,
+		1
+	);
+
+	if ( !empty( $older_date ) && !is_numeric( $older_date ) ) {
+		$time_chunks = explode( ':', str_replace( ' ', ':', $older_date ) );
+		$date_chunks = explode( '-', str_replace( ' ', '-', $older_date ) );
+		$older_date  = gmmktime( (int) $time_chunks[1], (int) $time_chunks[2], (int) $time_chunks[3], (int) $date_chunks[1], (int) $date_chunks[2], (int) $date_chunks[0] );
+	}
+
+	/**
+	 * $newer_date will equal false if we want to know the time elapsed between
+	 * a date and the current time. $newer_date will have a value if we want to
+	 * work out time elapsed between two known dates.
+	 */
+	$newer_date = ( !$newer_date ) ? bp_core_current_time( true, 'timestamp' ) : $newer_date;
+
+	// Difference in seconds.
+	$since = $newer_date - $older_date;
+
+	// Something went wrong with date calculation and we ended up with a negative date.
+	if ( 0 > $since ) {
+		$output = $unknown_text;
+
+	/**
+	 * We only want to output two chunks of time here, eg:
+	 * x years, xx months
+	 * x days, xx hours
+	 * so there's only two bits of calculation below:
+	 */
+	} else {
+
+		// Step one: the first chunk.
+		for ( $i = 0, $j = count( $chunks ); $i < $j; ++$i ) {
+			$seconds = $chunks[$i];
+
+			// Finding the biggest chunk (if the chunk fits, break).
+			$count = floor( $since / $seconds );
+			if ( 0 != $count ) {
+				break;
+			}
+		}
+
+		// If $i iterates all the way to $j, then the event happened 0 seconds ago.
+		if ( !isset( $chunks[$i] ) ) {
+			$output = $right_now_text;
+
+		} else {
+
+			// Set output var.
+			switch ( $seconds ) {
+				case YEAR_IN_SECONDS :
+					$output = sprintf( _n( '%s year',   '%s years',   $count, 'buddypress' ), $count );
+					break;
+				case 30 * DAY_IN_SECONDS :
+					$output = sprintf( _n( '%s month',  '%s months',  $count, 'buddypress' ), $count );
+					break;
+				case WEEK_IN_SECONDS :
+					$output = sprintf( _n( '%s week',   '%s weeks',   $count, 'buddypress' ), $count );
+					break;
+				case DAY_IN_SECONDS :
+					$output = sprintf( _n( '%s day',    '%s days',    $count, 'buddypress' ), $count );
+					break;
+				case HOUR_IN_SECONDS :
+					$output = sprintf( _n( '%s hour',   '%s hours',   $count, 'buddypress' ), $count );
+					break;
+				case MINUTE_IN_SECONDS :
+					$output = sprintf( _n( '%s minute', '%s minutes', $count, 'buddypress' ), $count );
+					break;
+				default:
+					$output = sprintf( _n( '%s second', '%s seconds', $count, 'buddypress' ), $count );
+			}
+
+			// Step two: the second chunk
+			// A quirk in the implementation means that this
+			// condition fails in the case of minutes and seconds.
+			// We've left the quirk in place, since fractions of a
+			// minute are not a useful piece of information for our
+			// purposes.
+			if ( $i + 2 < $j ) {
+				$seconds2 = $chunks[$i + 1];
+				$count2   = floor( ( $since - ( $seconds * $count ) ) / $seconds2 );
+
+				// Add to output var.
+				if ( 0 != $count2 ) {
+					$output .= _x( ',', 'Separator in time since', 'buddypress' ) . ' ';
+
+					switch ( $seconds2 ) {
+						case 30 * DAY_IN_SECONDS :
+							$output .= sprintf( _n( '%s month',  '%s months',  $count2, 'buddypress' ), $count2 );
+							break;
+						case WEEK_IN_SECONDS :
+							$output .= sprintf( _n( '%s week',   '%s weeks',   $count2, 'buddypress' ), $count2 );
+							break;
+						case DAY_IN_SECONDS :
+							$output .= sprintf( _n( '%s day',    '%s days',    $count2, 'buddypress' ), $count2 );
+							break;
+						case HOUR_IN_SECONDS :
+							$output .= sprintf( _n( '%s hour',   '%s hours',   $count2, 'buddypress' ), $count2 );
+							break;
+						case MINUTE_IN_SECONDS :
+							$output .= sprintf( _n( '%s minute', '%s minutes', $count2, 'buddypress' ), $count2 );
+							break;
+						default:
+							$output .= sprintf( _n( '%s second', '%s seconds', $count2, 'buddypress' ), $count2 );
+					}
+				}
+			}
+
+			// No output, so happened right now.
+			if ( ! (int) trim( $output ) ) {
+				$output = $right_now_text;
+			}
+		}
+	}
+
+	// Append 'ago' to the end of time-since if not 'right now'.
+	if ( $output != $right_now_text ) {
+		$output = sprintf( $ago_text, $output );
+	}
+
+	/**
+	 * Filters the English-language representation of the time elapsed since a given date.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $output     Final 'time since' string.
+	 * @param string $older_date Earlier time from which we're calculating time elapsed.
+	 * @param string $newer_date Unix timestamp of date to compare older time to.
+	 */
+	return apply_filters( 'bp_core_time_since', $output, $older_date, $newer_date );
+}
+
+/**
+ * Output an ISO-8601 date from a date string.
+ *
+ * @since 2.7.0
+ *
+ * @param string String of date to convert. Timezone should be UTC before using this.
+ * @return string|null
+ */
+ function bp_core_iso8601_date( $timestamp = '' ) {
+	echo bp_core_get_iso8601_date( $timestamp );
+}
+	/**
+	 * Return an ISO-8601 date from a date string.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string String of date to convert. Timezone should be UTC before using this.
+	 * @return string
+	 */
+	 function bp_core_get_iso8601_date( $timestamp = '' ) {
+		if ( ! $timestamp ) {
+			return '';
+		}
+
+		try {
+			$date = new DateTime( $timestamp, new DateTimeZone( 'UTC' ) );
+
+		// Not a valid date, so return blank string.
+		} catch( Exception $e ) {
+			return '';
+		}
+
+		return $date->format( DateTime::ISO8601 );
+	}
+
+/** Messages ******************************************************************/
+
+/**
+ * Add a feedback (error/success) message to the WP cookie so it can be displayed after the page reloads.
+ *
+ * @since 1.0.0
+ *
+ * @param string $message Feedback message to be displayed.
+ * @param string $type    Message type. 'updated', 'success', 'error', 'warning'.
+ *                        Default: 'success'.
+ */
+function bp_core_add_message( $message, $type = '' ) {
+
+	// Success is the default.
+	if ( empty( $type ) ) {
+		$type = 'success';
+	}
+
+	// Send the values to the cookie for page reload display.
+	@setcookie( 'bp-message',      $message, time() + 60 * 60 * 24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	@setcookie( 'bp-message-type', $type,    time() + 60 * 60 * 24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	/**
+	 * Send the values to the $bp global so we can still output messages
+	 * without a page reload
+	 */
+	$bp->template_message      = $message;
+	$bp->template_message_type = $type;
+}
+
+/**
+ * Set up the display of the 'template_notices' feedback message.
+ *
+ * Checks whether there is a feedback message in the WP cookie and, if so, adds
+ * a "template_notices" action so that the message can be parsed into the
+ * template and displayed to the user.
+ *
+ * After the message is displayed, it removes the message vars from the cookie
+ * so that the message is not shown to the user multiple times.
+ *
+ * @since 1.1.0
+ *
+ */
+function bp_core_setup_message() {
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	if ( empty( $bp->template_message ) && isset( $_COOKIE['bp-message'] ) ) {
+		$bp->template_message = stripslashes( $_COOKIE['bp-message'] );
+	}
+
+	if ( empty( $bp->template_message_type ) && isset( $_COOKIE['bp-message-type'] ) ) {
+		$bp->template_message_type = stripslashes( $_COOKIE['bp-message-type'] );
+	}
+
+	add_action( 'template_notices', 'bp_core_render_message' );
+
+	if ( isset( $_COOKIE['bp-message'] ) ) {
+		@setcookie( 'bp-message', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	}
+
+	if ( isset( $_COOKIE['bp-message-type'] ) ) {
+		@setcookie( 'bp-message-type', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	}
+}
+add_action( 'bp_actions', 'bp_core_setup_message', 5 );
+
+/**
+ * Render the 'template_notices' feedback message.
+ *
+ * The hook action 'template_notices' is used to call this function, it is not
+ * called directly.
+ *
+ * @since 1.1.0
+ */
+function bp_core_render_message() {
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	if ( !empty( $bp->template_message ) ) :
+		$type    = ( 'success' === $bp->template_message_type ) ? 'updated' : 'error';
+
+		/**
+		 * Filters the 'template_notices' feedback message content.
+		 *
+		 * @since 1.5.5
+		 *
+		 * @param string $template_message Feedback message content.
+		 * @param string $type             The type of message being displayed.
+		 *                                 Either 'updated' or 'error'.
+		 */
+		$content = apply_filters( 'bp_core_render_message_content', $bp->template_message, $type ); ?>
+
+		<div id="message" class="bp-template-notice <?php echo esc_attr( $type ); ?>">
+
+			<?php echo $content; ?>
+
+		</div>
+
+	<?php
+
+		/**
+		 * Fires after the display of any template_notices feedback messages.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_core_render_message' );
+
+	endif;
+}
+
+/** Last active ***************************************************************/
+
+/**
+ * Listener function for the logged-in user's 'last_activity' metadata.
+ *
+ * Many functions use a "last active" feature to show the length of time since
+ * the user was last active. This function will update that time as a usermeta
+ * setting for the user every 5 minutes while the user is actively browsing the
+ * site.
+ *
+ * @since 1.0.0
+ *
+ *       usermeta table.
+ *
+ * @return false|null Returns false if there is nothing to do.
+ */
+function bp_core_record_activity() {
+
+	// Bail if user is not logged in.
+	if ( ! is_user_logged_in() ) {
+		return false;
+	}
+
+	// Get the user ID.
+	$user_id = bp_loggedin_user_id();
+
+	// Bail if user is not active.
+	if ( bp_is_user_inactive( $user_id ) ) {
+		return false;
+	}
+
+	// Get the user's last activity.
+	$activity = bp_get_user_last_activity( $user_id );
+
+	// Make sure it's numeric.
+	if ( ! is_numeric( $activity ) ) {
+		$activity = strtotime( $activity );
+	}
+
+	// Get current time.
+	$current_time = bp_core_current_time( true, 'timestamp' );
+
+	// Use this action to detect the very first activity for a given member.
+	if ( empty( $activity ) ) {
+
+		/**
+		 * Fires inside the recording of an activity item.
+		 *
+		 * Use this action to detect the very first activity for a given member.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param int $user_id ID of the user whose activity is recorded.
+		 */
+		do_action( 'bp_first_activity_for_member', $user_id );
+	}
+
+	// If it's been more than 5 minutes, record a newer last-activity time.
+	if ( empty( $activity ) || ( $current_time >= strtotime( '+5 minutes', $activity ) ) ) {
+		bp_update_user_last_activity( $user_id, date( 'Y-m-d H:i:s', $current_time ) );
+	}
+}
+add_action( 'wp_head', 'bp_core_record_activity' );
+
+/**
+ * Format last activity string based on time since date given.
+ *
+ * @since 1.0.0
+ *
+ *       representation of the time elapsed.
+ *
+ * @param int|string $last_activity_date The date of last activity.
+ * @param string     $string             A sprintf()-able statement of the form 'active %s'.
+ * @return string $last_active A string of the form '3 years ago'.
+ */
+function bp_core_get_last_activity( $last_activity_date = '', $string = '' ) {
+
+	// Setup a default string if none was passed.
+	$string = empty( $string )
+		? '%s'     // Gettext placeholder.
+		: $string;
+
+	// Use the string if a last activity date was passed.
+	$last_active = empty( $last_activity_date )
+		? __( 'Not recently active', 'buddypress' )
+		: sprintf( $string, bp_core_time_since( $last_activity_date ) );
+
+	/**
+	 * Filters last activity string based on time since date given.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $last_active        Last activity string based on time since date given.
+	 * @param string $last_activity_date The date of last activity.
+	 * @param string $string             A sprintf()-able statement of the form 'active %s'.
+	 */
+	return apply_filters( 'bp_core_get_last_activity', $last_active, $last_activity_date, $string );
+}
+
+/** Meta **********************************************************************/
+
+/**
+ * Get the meta_key for a given piece of user metadata
+ *
+ * BuddyPress stores a number of pieces of userdata in the WordPress central
+ * usermeta table. In order to allow plugins to enable multiple instances of
+ * BuddyPress on a single WP installation, BP's usermeta keys are filtered
+ * through this function, so that they can be altered on the fly.
+ *
+ * Plugin authors should use BP's _user_meta() functions, which bakes in
+ * bp_get_user_meta_key():
+ *    $friend_count = bp_get_user_meta( $user_id, 'total_friend_count', true );
+ * If you must use WP's _user_meta() functions directly for some reason, you
+ * should use this function to determine the $key parameter, eg
+ *    $friend_count = get_user_meta( $user_id, bp_get_user_meta_key( 'total_friend_count' ), true );
+ * If using the WP functions, do not not hardcode your meta keys.
+ *
+ * @since 1.5.0
+ *
+ * @param string|bool $key The usermeta meta_key.
+ * @return string $key The usermeta meta_key.
+ */
+function bp_get_user_meta_key( $key = false ) {
+
+	/**
+	 * Filters the meta_key for a given piece of user metadata.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $key The usermeta meta key.
+	 */
+	return apply_filters( 'bp_get_user_meta_key', $key );
+}
+
+/**
+ * Get a piece of usermeta.
+ *
+ * This is a wrapper for get_user_meta() that allows for easy use of
+ * bp_get_user_meta_key(), thereby increasing compatibility with non-standard
+ * BP setups.
+ *
+ * @since 1.5.0
+ *
+ * @see get_user_meta() For complete details about parameters and return values.
+ *
+ * @param int    $user_id The ID of the user whose meta you're fetching.
+ * @param string $key     The meta key to retrieve.
+ * @param bool   $single  Whether to return a single value.
+ * @return mixed Will be an array if $single is false. Will be value of meta data field if $single
+ *               is true.
+ */
+function bp_get_user_meta( $user_id, $key, $single = false ) {
+	return get_user_meta( $user_id, bp_get_user_meta_key( $key ), $single );
+}
+
+/**
+ * Update a piece of usermeta.
+ *
+ * This is a wrapper for update_user_meta() that allows for easy use of
+ * bp_get_user_meta_key(), thereby increasing compatibility with non-standard
+ * BP setups.
+ *
+ * @since 1.5.0
+ *
+ * @see update_user_meta() For complete details about parameters and return values.
+ *
+ * @param int    $user_id    The ID of the user whose meta you're setting.
+ * @param string $key        The meta key to set.
+ * @param mixed  $value      Metadata value.
+ * @param mixed  $prev_value Optional. Previous value to check before removing.
+ * @return bool False on failure, true on success.
+ */
+function bp_update_user_meta( $user_id, $key, $value, $prev_value = '' ) {
+	return update_user_meta( $user_id, bp_get_user_meta_key( $key ), $value, $prev_value );
+}
+
+/**
+ * Delete a piece of usermeta.
+ *
+ * This is a wrapper for delete_user_meta() that allows for easy use of
+ * bp_get_user_meta_key(), thereby increasing compatibility with non-standard
+ * BP setups.
+ *
+ * @since 1.5.0
+ *
+ * @see delete_user_meta() For complete details about parameters and return values.
+ *
+ * @param int    $user_id The ID of the user whose meta you're deleting.
+ * @param string $key     The meta key to delete.
+ * @param mixed  $value   Optional. Metadata value.
+ * @return bool False for failure. True for success.
+ */
+function bp_delete_user_meta( $user_id, $key, $value = '' ) {
+	return delete_user_meta( $user_id, bp_get_user_meta_key( $key ), $value );
+}
+
+/** Embeds ********************************************************************/
+
+/**
+ * Initializes {@link BP_Embed} after everything is loaded.
+ *
+ * @since 1.5.0
+ */
+function bp_embed_init() {
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	if ( empty( $bp->embed ) ) {
+		$bp->embed = new BP_Embed();
+	}
+}
+add_action( 'bp_init', 'bp_embed_init', 9 );
+
+/**
+ * Are oembeds allowed in activity items?
+ *
+ * @since 1.5.0
+ *
+ * @return bool False when activity embed support is disabled; true when
+ *              enabled. Default: true.
+ */
+function bp_use_embed_in_activity() {
+
+	/**
+	 * Filters whether or not oEmbeds are allowed in activity items.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $value Whether or not oEmbeds are allowed.
+	 */
+	return apply_filters( 'bp_use_oembed_in_activity', !defined( 'BP_EMBED_DISABLE_ACTIVITY' ) || !BP_EMBED_DISABLE_ACTIVITY );
+}
+
+/**
+ * Are oembeds allowed in activity replies?
+ *
+ * @since 1.5.0
+ *
+ * @return bool False when activity replies embed support is disabled; true
+ *              when enabled. Default: true.
+ */
+function bp_use_embed_in_activity_replies() {
+
+	/**
+	 * Filters whether or not oEmbeds are allowed in activity replies.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $value Whether or not oEmbeds are allowed.
+	 */
+	return apply_filters( 'bp_use_embed_in_activity_replies', !defined( 'BP_EMBED_DISABLE_ACTIVITY_REPLIES' ) || !BP_EMBED_DISABLE_ACTIVITY_REPLIES );
+}
+
+/**
+ * Are oembeds allowed in private messages?
+ *
+ * @since 1.5.0
+ *
+ * @return bool False when private message embed support is disabled; true when
+ *              enabled. Default: true.
+ */
+function bp_use_embed_in_private_messages() {
+
+	/**
+	 * Filters whether or not oEmbeds are allowed in private messages.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $value Whether or not oEmbeds are allowed.
+	 */
+	return apply_filters( 'bp_use_embed_in_private_messages', !defined( 'BP_EMBED_DISABLE_PRIVATE_MESSAGES' ) || !BP_EMBED_DISABLE_PRIVATE_MESSAGES );
+}
+
+/**
+ * Extracts media metadata from a given content.
+ *
+ * @since 2.6.0
+ *
+ * @param string     $content The content to check.
+ * @param string|int $type    The type to check. Can also use a bitmask. See the class constants in the
+ *                             BP_Media_Extractor class for more info.
+ * @return false|array If media exists, will return array of media metadata. Else, boolean false.
+ */
+function bp_core_extract_media_from_content( $content = '', $type = 'all' ) {
+	if ( is_string( $type ) ) {
+		$class = new ReflectionClass( 'BP_Media_Extractor' );
+		$bitmask = $class->getConstant( strtoupper( $type ) );
+	} else {
+		$bitmask = (int) $type;
+	}
+
+	// Type isn't valid, so bail.
+	if ( empty( $bitmask ) ) {
+		return false;
+	}
+
+	$x = new BP_Media_Extractor;
+	$media = $x->extract( $content, $bitmask );
+
+	unset( $media['has'] );
+	$retval = array_filter( $media );
+
+	return ! empty( $retval ) ? $retval : false;
+}
+
+/** Admin *********************************************************************/
+
+/**
+ * Output the correct admin URL based on BuddyPress and WordPress configuration.
+ *
+ * @since 1.5.0
+ *
+ * @see bp_get_admin_url() For description of parameters.
+ *
+ * @param string $path   See {@link bp_get_admin_url()}.
+ * @param string $scheme See {@link bp_get_admin_url()}.
+ */
+function bp_admin_url( $path = '', $scheme = 'admin' ) {
+	echo esc_url( bp_get_admin_url( $path, $scheme ) );
+}
+	/**
+	 * Return the correct admin URL based on BuddyPress and WordPress configuration.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @param string $path   Optional. The sub-path under /wp-admin to be
+	 *                       appended to the admin URL.
+	 * @param string $scheme The scheme to use. Default is 'admin', which
+	 *                       obeys {@link force_ssl_admin()} and {@link is_ssl()}. 'http'
+	 *                       or 'https' can be passed to force those schemes.
+	 * @return string Admin url link with optional path appended.
+	 */
+	function bp_get_admin_url( $path = '', $scheme = 'admin' ) {
+
+		// Links belong in network admin.
+		if ( bp_core_do_network_admin() ) {
+			$url = network_admin_url( $path, $scheme );
+
+		// Links belong in site admin.
+		} else {
+			$url = admin_url( $path, $scheme );
+		}
+
+		return $url;
+	}
+
+/**
+ * Should BuddyPress appear in network admin (vs a single site Dashboard)?
+ *
+ * Because BuddyPress can be installed in multiple ways and with multiple
+ * configurations, we need to check a few things to be confident about where
+ * to hook into certain areas of WordPress's admin.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the BP admin screen should appear in the Network Admin,
+ *              otherwise false.
+ */
+function bp_core_do_network_admin() {
+
+	// Default.
+	$retval = bp_is_network_activated();
+
+	if ( bp_is_multiblog_mode() ) {
+		$retval = false;
+	}
+
+	/**
+	 * Filters whether or not BuddyPress should appear in network admin.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $retval Whether or not BuddyPress should be in the network admin.
+	 */
+	return (bool) apply_filters( 'bp_core_do_network_admin', $retval );
+}
+
+/**
+ * Return the action name that BuddyPress nav setup callbacks should be hooked to.
+ *
+ * Functions used to set up BP Dashboard pages (wrapping such admin-panel
+ * functions as add_submenu_page()) should use bp_core_admin_hook() for the
+ * first parameter in add_action(). BuddyPress will then determine
+ * automatically whether to load the panels in the Network Admin. Ie:
+ *
+ *     add_action( bp_core_admin_hook(), 'myplugin_dashboard_panel_setup' );
+ *
+ * @since 1.5.0
+ *
+ * @return string $hook The proper hook ('network_admin_menu' or 'admin_menu').
+ */
+function bp_core_admin_hook() {
+	$hook = bp_core_do_network_admin() ? 'network_admin_menu' : 'admin_menu';
+
+	/**
+	 * Filters the action name that BuddyPress nav setup callbacks should be hooked to.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $hook Action name to be attached to.
+	 */
+	return apply_filters( 'bp_core_admin_hook', $hook );
+}
+
+/** Multisite *****************************************************************/
+
+/**
+ * Is this the root blog?
+ *
+ * @since 1.5.0
+ *
+ * @param int $blog_id Optional. Default: the ID of the current blog.
+ * @return bool $is_root_blog Returns true if this is bp_get_root_blog_id().
+ */
+function bp_is_root_blog( $blog_id = 0 ) {
+
+	// Assume false.
+	$is_root_blog = false;
+
+	// Use current blog if no ID is passed.
+	if ( empty( $blog_id ) || ! is_int( $blog_id ) ) {
+		$blog_id = get_current_blog_id();
+	}
+
+	// Compare to root blog ID.
+	if ( bp_get_root_blog_id() === $blog_id ) {
+		$is_root_blog = true;
+	}
+
+	/**
+	 * Filters whether or not we're on the root blog.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $is_root_blog Whether or not we're on the root blog.
+	 */
+	return (bool) apply_filters( 'bp_is_root_blog', (bool) $is_root_blog );
+}
+
+/**
+ * Get the ID of the root blog.
+ *
+ * The "root blog" is the blog on a WordPress network where BuddyPress content
+ * appears (where member profile URLs resolve, where a given theme is loaded,
+ * etc.).
+ *
+ * @since 1.5.0
+ *
+ * @return int The root site ID.
+ */
+function bp_get_root_blog_id() {
+
+	/**
+	 * Filters the ID for the root blog.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $root_blog_id ID for the root blog.
+	 */
+	return (int) apply_filters( 'bp_get_root_blog_id', (int) buddypress()->root_blog_id );
+}
+
+/**
+ * Are we running multiblog mode?
+ *
+ * Note that BP_ENABLE_MULTIBLOG is different from (but dependent on) WordPress
+ * Multisite. "Multiblog" is BuddyPress setup that allows BuddyPress components
+ * to be viewed on every blog on the network, each with their own settings.
+ *
+ * Thus, instead of having all 'boonebgorges' links go to
+ *   http://example.com/members/boonebgorges
+ * on the root blog, each blog will have its own version of the same content, eg
+ *   http://site2.example.com/members/boonebgorges (for subdomains)
+ *   http://example.com/site2/members/boonebgorges (for subdirectories)
+ *
+ * Multiblog mode is disabled by default, meaning that all BuddyPress content
+ * must be viewed on the root blog. It's also recommended not to use the
+ * BP_ENABLE_MULTIBLOG constant beyond 1.7, as BuddyPress can now be activated
+ * on individual sites.
+ *
+ * Why would you want to use this? Originally it was intended to allow
+ * BuddyPress to live in mu-plugins and be visible on mapped domains. This is
+ * a very small use-case with large architectural shortcomings, so do not go
+ * down this road unless you specifically need to.
+ *
+ * @since 1.5.0
+ *
+ * @return bool False when multiblog mode is disabled; true when enabled.
+ *              Default: false.
+ */
+function bp_is_multiblog_mode() {
+
+	// Setup some default values.
+	$retval         = false;
+	$is_multisite   = is_multisite();
+	$network_active = bp_is_network_activated();
+	$is_multiblog   = defined( 'BP_ENABLE_MULTIBLOG' ) && BP_ENABLE_MULTIBLOG;
+
+	// Multisite, Network Activated, and Specifically Multiblog.
+	if ( $is_multisite && $network_active && $is_multiblog ) {
+		$retval = true;
+
+	// Multisite, but not network activated.
+	} elseif ( $is_multisite && ! $network_active ) {
+		$retval = true;
+	}
+
+	/**
+	 * Filters whether or not we're running in multiblog mode.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $retval Whether or not we're running multiblog mode.
+	 */
+	return apply_filters( 'bp_is_multiblog_mode', $retval );
+}
+
+/**
+ * Is BuddyPress active at the network level for this network?
+ *
+ * Used to determine admin menu placement, and where settings and options are
+ * stored. If you're being *really* clever and manually pulling BuddyPress in
+ * with an mu-plugin or some other method, you'll want to filter
+ * 'bp_is_network_activated' and override the auto-determined value.
+ *
+ * @since 1.7.0
+ *
+ * @return bool True if BuddyPress is network activated.
+ */
+function bp_is_network_activated() {
+
+	// Default to is_multisite().
+	$retval  = is_multisite();
+
+	// Check the sitewide plugins array.
+	$base    = buddypress()->basename;
+	$plugins = get_site_option( 'active_sitewide_plugins' );
+
+	// Override is_multisite() if not network activated.
+	if ( ! is_array( $plugins ) || ! isset( $plugins[ $base ] ) ) {
+		$retval = false;
+	}
+
+	/**
+	 * Filters whether or not we're active at the network level.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool $retval Whether or not we're network activated.
+	 */
+	return (bool) apply_filters( 'bp_is_network_activated', $retval );
+}
+
+/** Global Manipulators *******************************************************/
+
+/**
+ * Set the "is_directory" global.
+ *
+ * @since 1.5.0
+ *
+ * @param bool   $is_directory Optional. Default: false.
+ * @param string $component    Optional. Component name. Default: the current
+ *                             component.
+ */
+function bp_update_is_directory( $is_directory = false, $component = '' ) {
+
+	if ( empty( $component ) ) {
+		$component = bp_current_component();
+	}
+
+	/**
+	 * Filters the "is_directory" global value.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $is_directory Whether or not we're "is_directory".
+	 * @param string $component    Component name. Default: the current component.
+	 */
+	buddypress()->is_directory = apply_filters( 'bp_update_is_directory', $is_directory, $component );
+}
+
+/**
+ * Set the "is_item_admin" global.
+ *
+ * @since 1.5.0
+ *
+ * @param bool   $is_item_admin Optional. Default: false.
+ * @param string $component     Optional. Component name. Default: the current
+ *                              component.
+ */
+function bp_update_is_item_admin( $is_item_admin = false, $component = '' ) {
+
+	if ( empty( $component ) ) {
+		$component = bp_current_component();
+	}
+
+	/**
+	 * Filters the "is_item_admin" global value.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $is_item_admin Whether or not we're "is_item_admin".
+	 * @param string $component     Component name. Default: the current component.
+	 */
+	buddypress()->is_item_admin = apply_filters( 'bp_update_is_item_admin', $is_item_admin, $component );
+}
+
+/**
+ * Set the "is_item_mod" global.
+ *
+ * @since 1.5.0
+ *
+ * @param bool   $is_item_mod Optional. Default: false.
+ * @param string $component   Optional. Component name. Default: the current
+ *                            component.
+ */
+function bp_update_is_item_mod( $is_item_mod = false, $component = '' ) {
+
+	if ( empty( $component ) ) {
+		$component = bp_current_component();
+	}
+
+	/**
+	 * Filters the "is_item_mod" global value.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $is_item_mod Whether or not we're "is_item_mod".
+	 * @param string $component   Component name. Default: the current component.
+	 */
+	buddypress()->is_item_mod = apply_filters( 'bp_update_is_item_mod', $is_item_mod, $component );
+}
+
+/**
+ * Trigger a 404.
+ *
+ * @since 1.5.0
+ *
+ * @global WP_Query $wp_query WordPress query object.
+ *
+ * @param string $redirect If 'remove_canonical_direct', remove WordPress' "helpful"
+ *                         redirect_canonical action. Default: 'remove_canonical_redirect'.
+ */
+function bp_do_404( $redirect = 'remove_canonical_direct' ) {
+	global $wp_query;
+
+	/**
+	 * Fires inside the triggering of a 404.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $redirect Redirect type used to determine if redirect_canonical
+	 *                         function should be be removed.
+	 */
+	do_action( 'bp_do_404', $redirect );
+
+	$wp_query->set_404();
+	status_header( 404 );
+	nocache_headers();
+
+	if ( 'remove_canonical_direct' === $redirect ) {
+		remove_action( 'template_redirect', 'redirect_canonical' );
+	}
+}
+
+/** Nonces ********************************************************************/
+
+/**
+ * Makes sure the user requested an action from another page on this site.
+ *
+ * To avoid security exploits within the theme.
+ *
+ * @since 1.6.0
+ *
+ * @param string $action    Action nonce.
+ * @param string $query_arg Where to look for nonce in $_REQUEST.
+ * @return bool True if the nonce is verified, otherwise false.
+ */
+function bp_verify_nonce_request( $action = '', $query_arg = '_wpnonce' ) {
+
+	/* Home URL **************************************************************/
+
+	// Parse home_url() into pieces to remove query-strings, strange characters,
+	// and other funny things that plugins might to do to it.
+	$parsed_home = parse_url( home_url( '/', ( is_ssl() ? 'https' : 'http' ) ) );
+
+	// Maybe include the port, if it's included in home_url().
+	if ( isset( $parsed_home['port'] ) ) {
+		$parsed_host = $parsed_home['host'] . ':' . $parsed_home['port'];
+	} else {
+		$parsed_host = $parsed_home['host'];
+	}
+
+	// Set the home URL for use in comparisons.
+	$home_url = trim( strtolower( $parsed_home['scheme'] . '://' . $parsed_host . $parsed_home['path'] ), '/' );
+
+	/* Requested URL *********************************************************/
+
+	// Maybe include the port, if it's included in home_url().
+	if ( isset( $parsed_home['port'] ) && false === strpos( $_SERVER['HTTP_HOST'], ':' ) ) {
+		$request_host = $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT'];
+	} else {
+		$request_host = $_SERVER['HTTP_HOST'];
+	}
+
+	// Build the currently requested URL.
+	$scheme        = is_ssl() ? 'https://' : 'http://';
+	$requested_url = strtolower( $scheme . $request_host . $_SERVER['REQUEST_URI'] );
+
+	/* Look for match ********************************************************/
+
+	/**
+	 * Filters the requested URL being nonce-verified.
+	 *
+	 * Useful for configurations like reverse proxying.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $requested_url The requested URL.
+	 */
+	$matched_url = apply_filters( 'bp_verify_nonce_request_url', $requested_url );
+
+	// Check the nonce.
+	$result = isset( $_REQUEST[$query_arg] ) ? wp_verify_nonce( $_REQUEST[$query_arg], $action ) : false;
+
+	// Nonce check failed.
+	if ( empty( $result ) || empty( $action ) || ( strpos( $matched_url, $home_url ) !== 0 ) ) {
+		$result = false;
+	}
+
+	/**
+	 * Fires at the end of the nonce verification check.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $action Action nonce.
+	 * @param bool   $result Boolean result of nonce verification.
+	 */
+	do_action( 'bp_verify_nonce_request', $action, $result );
+
+	return $result;
+}
+
+/** Requests ******************************************************************/
+
+/**
+ * Return true|false if this is a POST request.
+ *
+ * @since 1.9.0
+ *
+ * @return bool
+ */
+function bp_is_post_request() {
+	return (bool) ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) );
+}
+
+/**
+ * Return true|false if this is a GET request.
+ *
+ * @since 1.9.0
+ *
+ * @return bool
+ */
+function bp_is_get_request() {
+	return (bool) ( 'GET' === strtoupper( $_SERVER['REQUEST_METHOD'] ) );
+}
+
+
+/** Miscellaneous hooks *******************************************************/
+
+/**
+ * Load the buddypress translation file for current language.
+ *
+ * @since 1.0.2
+ *
+ * @see load_textdomain() for a description of return values.
+ *
+ * @return bool True on success, false on failure.
+ */
+function bp_core_load_buddypress_textdomain() {
+	$domain = 'buddypress';
+
+	/**
+	 * Filters the locale to be loaded for the language files.
+	 *
+	 * @since 1.0.2
+	 *
+	 * @param string $value Current locale for the install.
+	 */
+	$mofile_custom = sprintf( '%s-%s.mo', $domain, apply_filters( 'buddypress_locale', get_locale() ) );
+
+	/**
+	 * Filters the locations to load language files from.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $value Array of directories to check for language files in.
+	 */
+	$locations = apply_filters( 'buddypress_locale_locations', array(
+		trailingslashit( WP_LANG_DIR . '/' . $domain  ),
+		trailingslashit( WP_LANG_DIR ),
+	) );
+
+	// Try custom locations in WP_LANG_DIR.
+	foreach ( $locations as $location ) {
+		if ( load_textdomain( 'buddypress', $location . $mofile_custom ) ) {
+			return true;
+		}
+	}
+
+	// Default to WP and glotpress.
+	return load_plugin_textdomain( $domain );
+}
+add_action( 'bp_core_loaded', 'bp_core_load_buddypress_textdomain' );
+
+/**
+ * A JavaScript-free implementation of the search functions in BuddyPress.
+ *
+ * @since 1.0.1
+ *
+ * @param string $slug The slug to redirect to for searching.
+ */
+function bp_core_action_search_site( $slug = '' ) {
+
+	if ( ! bp_is_current_component( bp_get_search_slug() ) ) {
+		return;
+	}
+
+	if ( empty( $_POST['search-terms'] ) ) {
+		bp_core_redirect( bp_get_root_domain() );
+		return;
+	}
+
+	$search_terms = stripslashes( $_POST['search-terms'] );
+	$search_which = !empty( $_POST['search-which'] ) ? $_POST['search-which'] : '';
+	$query_string = '/?s=';
+
+	if ( empty( $slug ) ) {
+		switch ( $search_which ) {
+			case 'posts':
+				$slug = '';
+				$var  = '/?s=';
+
+				// If posts aren't displayed on the front page, find the post page's slug.
+				if ( 'page' == get_option( 'show_on_front' ) ) {
+					$page = get_post( get_option( 'page_for_posts' ) );
+
+					if ( !is_wp_error( $page ) && !empty( $page->post_name ) ) {
+						$slug = $page->post_name;
+						$var  = '?s=';
+					}
+				}
+				break;
+
+			case 'blogs':
+				$slug = bp_is_active( 'blogs' )  ? bp_get_blogs_root_slug()  : '';
+				break;
+
+			case 'groups':
+				$slug = bp_is_active( 'groups' ) ? bp_get_groups_root_slug() : '';
+				break;
+
+			case 'members':
+			default:
+				$slug = bp_get_members_root_slug();
+				break;
+		}
+
+		if ( empty( $slug ) && 'posts' != $search_which ) {
+			bp_core_redirect( bp_get_root_domain() );
+			return;
+		}
+	}
+
+	/**
+	 * Filters the constructed url for use with site searching.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value        URL for use with site searching.
+	 * @param array  $search_terms Array of search terms.
+	 */
+	bp_core_redirect( apply_filters( 'bp_core_search_site', home_url( $slug . $query_string . urlencode( $search_terms ) ), $search_terms ) );
+}
+add_action( 'bp_init', 'bp_core_action_search_site', 7 );
+
+/**
+ * Remove "prev" and "next" relational links from <head> on BuddyPress pages.
+ *
+ * WordPress automatically generates these relational links to the current
+ * page.  However, BuddyPress doesn't adhere to these links.  In this
+ * function, we remove these links when on a BuddyPress page.  This also
+ * prevents additional, unnecessary queries from running.
+ *
+ * @since 2.1.0
+ */
+function bp_remove_adjacent_posts_rel_link() {
+	if ( ! is_buddypress() ) {
+		return;
+	}
+
+	remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10 );
+}
+add_action( 'bp_init', 'bp_remove_adjacent_posts_rel_link' );
+
+/**
+ * Strip the span count of a menu item or of a title part.
+ *
+ * @since 2.2.2
+ *
+ * @param string $title_part Title part to clean up.
+ * @return string
+ */
+function _bp_strip_spans_from_title( $title_part = '' ) {
+	$title = $title_part;
+	$span = strpos( $title, '<span' );
+	if ( false !== $span ) {
+		$title = substr( $title, 0, $span - 1 );
+	}
+	return trim( $title );
+}
+
+/**
+ * Get the correct filename suffix for minified assets.
+ *
+ * @since 2.5.0
+ *
+ * @return string
+ */
+function bp_core_get_minified_asset_suffix() {
+	$ext = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
+
+	// Ensure the assets can be located when running from /src/.
+	if ( defined( 'BP_SOURCE_SUBDIRECTORY' ) && BP_SOURCE_SUBDIRECTORY === 'src' ) {
+		$ext = str_replace( '.min', '', $ext );
+	}
+
+	return $ext;
+}
+
+/**
+ * Return a list of component information.
+ *
+ * @since 2.6.0
+ *
+ * @param string $type Optional; component type to fetch. Default value is 'all', or 'optional', 'retired', 'required'.
+ * @return array Requested components' data.
+ */
+function bp_core_get_components( $type = 'all' ) {
+	$required_components = array(
+		'core' => array(
+			'title'       => __( 'BuddyPress Core', 'buddypress' ),
+			'description' => __( 'It&#8216;s what makes <del>time travel</del> BuddyPress possible!', 'buddypress' )
+		),
+		'members' => array(
+			'title'       => __( 'Community Members', 'buddypress' ),
+			'description' => __( 'Everything in a BuddyPress community revolves around its members.', 'buddypress' )
+		),
+	);
+
+	$retired_components = array(
+	);
+
+	$optional_components = array(
+		'xprofile' => array(
+			'title'       => __( 'Extended Profiles', 'buddypress' ),
+			'description' => __( 'Customize your community with fully editable profile fields that allow your users to describe themselves.', 'buddypress' )
+		),
+		'settings' => array(
+			'title'       => __( 'Account Settings', 'buddypress' ),
+			'description' => __( 'Allow your users to modify their account and notification settings directly from within their profiles.', 'buddypress' )
+		),
+		'friends'  => array(
+			'title'       => __( 'Friend Connections', 'buddypress' ),
+			'description' => __( 'Let your users make connections so they can track the activity of others and focus on the people they care about the most.', 'buddypress' )
+		),
+		'messages' => array(
+			'title'       => __( 'Private Messaging', 'buddypress' ),
+			'description' => __( 'Allow your users to talk to each other directly and in private. Not just limited to one-on-one discussions, messages can be sent between any number of members.', 'buddypress' )
+		),
+		'activity' => array(
+			'title'       => __( 'Activity Streams', 'buddypress' ),
+			'description' => __( 'Global, personal, and group activity streams with threaded commenting, direct posting, favoriting, and @mentions, all with full RSS feed and email notification support.', 'buddypress' )
+		),
+		'notifications' => array(
+			'title'       => __( 'Notifications', 'buddypress' ),
+			'description' => __( 'Notify members of relevant activity with a toolbar bubble and/or via email, and allow them to customize their notification settings.', 'buddypress' )
+		),
+		'groups'   => array(
+			'title'       => __( 'User Groups', 'buddypress' ),
+			'description' => __( 'Groups allow your users to organize themselves into specific public, private or hidden sections with separate activity streams and member listings.', 'buddypress' )
+		),
+		'blogs'    => array(
+			'title'       => __( 'Site Tracking', 'buddypress' ),
+			'description' => __( 'Record activity for new posts and comments from your site.', 'buddypress' )
+		)
+	);
+
+	// Add blogs tracking if multisite.
+	if ( is_multisite() ) {
+		$optional_components['blogs']['description'] = __( 'Record activity for new sites, posts, and comments across your network.', 'buddypress' );
+	}
+
+	switch ( $type ) {
+		case 'required' :
+			$components = $required_components;
+			break;
+		case 'optional' :
+			$components = $optional_components;
+			break;
+		case 'retired' :
+			$components = $retired_components;
+			break;
+		case 'all' :
+		default :
+			$components = array_merge( $required_components, $optional_components, $retired_components );
+			break;
+	}
+
+	/**
+	 * Filters the list of component information.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array  $components Array of component information.
+	 * @param string $type       Type of component list requested.
+	 *                           Possible values are 'all', 'optional', 'retired', 'required'.
+	 */
+	return apply_filters( 'bp_core_get_components', $components, $type );
+}
+
+/** Nav Menu ******************************************************************/
+
+/**
+ * Create fake "post" objects for BP's logged-in nav menu for use in the WordPress "Menus" settings page.
+ *
+ * WordPress nav menus work by representing post or tax term data as a custom
+ * post type, which is then used to populate the checkboxes that appear on
+ * Dashboard > Appearance > Menu as well as the menu as rendered on the front
+ * end. Most of the items in the BuddyPress set of nav items are neither posts
+ * nor tax terms, so we fake a post-like object so as to be compatible with the
+ * menu.
+ *
+ * This technique also allows us to generate links dynamically, so that, for
+ * example, "My Profile" will always point to the URL of the profile of the
+ * logged-in user.
+ *
+ * @since 1.9.0
+ *
+ * @return mixed A URL or an array of dummy pages.
+ */
+function bp_nav_menu_get_loggedin_pages() {
+
+	// Try to catch the cached version first.
+	if ( ! empty( buddypress()->wp_nav_menu_items->loggedin ) ) {
+		return buddypress()->wp_nav_menu_items->loggedin;
+	}
+
+	// Pull up a list of items registered in BP's primary nav for the member.
+	$bp_menu_items = buddypress()->members->nav->get_primary();
+
+	// Some BP nav menu items will not be represented in bp_nav, because
+	// they are not real BP components. We add them manually here.
+	$bp_menu_items[] = array(
+		'name' => __( 'Log Out', 'buddypress' ),
+		'slug' => 'logout',
+		'link' => wp_logout_url(),
+	);
+
+	// If there's nothing to show, we're done.
+	if ( count( $bp_menu_items ) < 1 ) {
+		return false;
+	}
+
+	$page_args = array();
+
+	foreach ( $bp_menu_items as $bp_item ) {
+
+		// Remove <span>number</span>.
+		$item_name = _bp_strip_spans_from_title( $bp_item['name'] );
+
+		$page_args[ $bp_item['slug'] ] = (object) array(
+			'ID'             => -1,
+			'post_title'     => $item_name,
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_excerpt'   => $bp_item['slug'],
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'comment_status' => 'closed',
+			'guid'           => $bp_item['link']
+		);
+	}
+
+	if ( empty( buddypress()->wp_nav_menu_items ) ) {
+		buddypress()->wp_nav_menu_items = new stdClass;
+	}
+
+	buddypress()->wp_nav_menu_items->loggedin = $page_args;
+
+	return $page_args;
+}
+
+/**
+ * Create fake "post" objects for BP's logged-out nav menu for use in the WordPress "Menus" settings page.
+ *
+ * WordPress nav menus work by representing post or tax term data as a custom
+ * post type, which is then used to populate the checkboxes that appear on
+ * Dashboard > Appearance > Menu as well as the menu as rendered on the front
+ * end. Most of the items in the BuddyPress set of nav items are neither posts
+ * nor tax terms, so we fake a post-like object so as to be compatible with the
+ * menu.
+ *
+ * @since 1.9.0
+ *
+ * @return mixed A URL or an array of dummy pages.
+ */
+function bp_nav_menu_get_loggedout_pages() {
+
+	// Try to catch the cached version first.
+	if ( ! empty( buddypress()->wp_nav_menu_items->loggedout ) ) {
+		return buddypress()->wp_nav_menu_items->loggedout;
+	}
+
+	$bp_menu_items = array();
+
+	// Some BP nav menu items will not be represented in bp_nav, because
+	// they are not real BP components. We add them manually here.
+	$bp_menu_items[] = array(
+		'name' => __( 'Log In', 'buddypress' ),
+		'slug' => 'login',
+		'link' => wp_login_url(),
+	);
+
+	// The Register page will not always be available (ie, when
+	// registration is disabled).
+	$bp_directory_page_ids = bp_core_get_directory_page_ids();
+
+	if( ! empty( $bp_directory_page_ids['register'] ) ) {
+		$register_page = get_post( $bp_directory_page_ids['register'] );
+		$bp_menu_items[] = array(
+			'name' => $register_page->post_title,
+			'slug' => 'register',
+			'link' => get_permalink( $register_page->ID ),
+		);
+	}
+
+	// If there's nothing to show, we're done.
+	if ( count( $bp_menu_items ) < 1 ) {
+		return false;
+	}
+
+	$page_args = array();
+
+	foreach ( $bp_menu_items as $bp_item ) {
+		$page_args[ $bp_item['slug'] ] = (object) array(
+			'ID'             => -1,
+			'post_title'     => $bp_item['name'],
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_excerpt'   => $bp_item['slug'],
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'comment_status' => 'closed',
+			'guid'           => $bp_item['link']
+		);
+	}
+
+	if ( empty( buddypress()->wp_nav_menu_items ) ) {
+		buddypress()->wp_nav_menu_items = new stdClass;
+	}
+
+	buddypress()->wp_nav_menu_items->loggedout = $page_args;
+
+	return $page_args;
+}
+
+/**
+ * Get the URL for a BuddyPress WP nav menu item, based on slug.
+ *
+ * BuddyPress-specific WP nav menu items have dynamically generated URLs,
+ * based on the identity of the current user. This function lets you fetch the
+ * proper URL for a given nav item slug (such as 'login' or 'messages').
+ *
+ * @since 1.9.0
+ *
+ * @param string $slug The slug of the nav item: login, register, or one of the
+ *                     slugs from the members navigation.
+ * @return string $nav_item_url The URL generated for the current user.
+ */
+function bp_nav_menu_get_item_url( $slug ) {
+	$nav_item_url   = '';
+	$nav_menu_items = bp_nav_menu_get_loggedin_pages();
+
+	if ( isset( $nav_menu_items[ $slug ] ) ) {
+		$nav_item_url = $nav_menu_items[ $slug ]->guid;
+	}
+
+	return $nav_item_url;
+}
+
+/** Suggestions***************************************************************/
+
+/**
+ * BuddyPress Suggestions API for types of at-mentions.
+ *
+ * This is used to power BuddyPress' at-mentions suggestions, but it is flexible enough to be used
+ * for similar kinds of future requirements, or those implemented by third-party developers.
+ *
+ * @since 2.1.0
+ *
+ * @param array $args Array of args for the suggestions.
+ * @return array|WP_Error Array of results. If there were any problems, returns a WP_Error object.
+ */
+function bp_core_get_suggestions( $args ) {
+	$args = bp_parse_args( $args, array(), 'get_suggestions' );
+
+	if ( ! $args['type'] ) {
+		return new WP_Error( 'missing_parameter' );
+	}
+
+	// Members @name suggestions.
+	if ( $args['type'] === 'members' ) {
+		$class = 'BP_Members_Suggestions';
+
+		// Members @name suggestions for users in a specific Group.
+		if ( isset( $args['group_id'] ) ) {
+			$class = 'BP_Groups_Member_Suggestions';
+		}
+
+	} else {
+
+		/**
+		 * Filters the default suggestions service to use.
+		 *
+		 * Use this hook to tell BP the name of your class
+		 * if you've built a custom suggestions service.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $value Custom class to use. Default: none.
+		 * @param array  $args  Array of arguments for sugggestions.
+		 */
+		$class = apply_filters( 'bp_suggestions_services', '', $args );
+	}
+
+	if ( ! $class || ! class_exists( $class ) ) {
+		return new WP_Error( 'missing_parameter' );
+	}
+
+
+	$suggestions = new $class( $args );
+	$validation  = $suggestions->validate();
+
+	if ( is_wp_error( $validation ) ) {
+		$retval = $validation;
+	} else {
+		$retval = $suggestions->get_suggestions();
+	}
+
+	/**
+	 * Filters the available type of at-mentions.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array|WP_Error $retval Array of results or WP_Error object.
+	 * @param array          $args   Array of arguments for suggestions.
+	 */
+	return apply_filters( 'bp_core_get_suggestions', $retval, $args );
+}
+
+/**
+ * Set data from the BP root blog's upload directory.
+ *
+ * Handy for multisite instances because all uploads are made on the BP root
+ * blog and we need to query the BP root blog for the upload directory data.
+ *
+ * This function ensures that we only need to use {@link switch_to_blog()}
+ * once to get what we need.
+ *
+ * @since 2.3.0
+ *
+ * @return bool|array
+ */
+function bp_upload_dir() {
+	$bp = buddypress();
+
+	if ( empty( $bp->upload_dir ) ) {
+		$need_switch = (bool) ( is_multisite() && ! bp_is_root_blog() );
+
+		// Maybe juggle to root blog.
+		if ( true === $need_switch ) {
+			switch_to_blog( bp_get_root_blog_id() );
+		}
+
+		// Get the upload directory (maybe for root blog).
+		$wp_upload_dir = wp_upload_dir();
+
+		// Maybe juggle back to current blog.
+		if ( true === $need_switch ) {
+			restore_current_blog();
+		}
+
+		// Bail if an error occurred.
+		if ( ! empty( $wp_upload_dir['error'] ) ) {
+			return false;
+		}
+
+		$bp->upload_dir = $wp_upload_dir;
+	}
+
+	return $bp->upload_dir;
+}
+
+
+/** Post Types *****************************************************************/
+
+/**
+ * Output the name of the email post type.
+ *
+ * @since 2.5.0
+ */
+function bp_email_post_type() {
+	echo bp_get_email_post_type();
+}
+	/**
+	 * Returns the name of the email post type.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string The name of the email post type.
+	 */
+	function bp_get_email_post_type() {
+
+		/**
+		 * Filters the name of the email post type.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $value Email post type name.
+		 */
+		return apply_filters( 'bp_get_email_post_type', buddypress()->email_post_type );
+	}
+
+/**
+ * Return labels used by the email post type.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_get_email_post_type_labels() {
+
+	/**
+	 * Filters email post type labels.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $value Associative array (name => label).
+	 */
+	return apply_filters( 'bp_get_email_post_type_labels', array(
+		'add_new'               => _x( 'Add New', 'email post type label', 'buddypress' ),
+		'add_new_item'          => _x( 'Add a New Email', 'email post type label', 'buddypress' ),
+		'all_items'             => _x( 'All Emails', 'email post type label', 'buddypress' ),
+		'edit_item'             => _x( 'Edit Email', 'email post type label', 'buddypress' ),
+		'filter_items_list'     => _x( 'Filter email list', 'email post type label', 'buddypress' ),
+		'items_list'            => _x( 'Email list', 'email post type label', 'buddypress' ),
+		'items_list_navigation' => _x( 'Email list navigation', 'email post type label', 'buddypress' ),
+		'menu_name'             => _x( 'Emails', 'email post type name', 'buddypress' ),
+		'name'                  => _x( 'BuddyPress Emails', 'email post type label', 'buddypress' ),
+		'new_item'              => _x( 'New Email', 'email post type label', 'buddypress' ),
+		'not_found'             => _x( 'No emails found', 'email post type label', 'buddypress' ),
+		'not_found_in_trash'    => _x( 'No emails found in Trash', 'email post type label', 'buddypress' ),
+		'search_items'          => _x( 'Search Emails', 'email post type label', 'buddypress' ),
+		'singular_name'         => _x( 'Email', 'email post type singular name', 'buddypress' ),
+		'uploaded_to_this_item' => _x( 'Uploaded to this email', 'email post type label', 'buddypress' ),
+		'view_item'             => _x( 'View Email', 'email post type label', 'buddypress' ),
+	) );
+}
+
+/**
+ * Return array of features that the email post type supports.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_get_email_post_type_supports() {
+
+	/**
+	 * Filters the features that the email post type supports.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $value Supported features.
+	 */
+	return apply_filters( 'bp_get_email_post_type_supports', array(
+		'custom-fields',
+		'editor',
+		'excerpt',
+		'revisions',
+		'title',
+	) );
+}
+
+
+/** Taxonomies *****************************************************************/
+
+/**
+ * Output the name of the email type taxonomy.
+ *
+ * @since 2.5.0
+ */
+function bp_email_tax_type() {
+	echo bp_get_email_tax_type();
+}
+	/**
+	 * Return the name of the email type taxonomy.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string The unique email taxonomy type ID.
+	 */
+	function bp_get_email_tax_type() {
+
+		/**
+		 * Filters the name of the email type taxonomy.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $value Email type taxonomy name.
+		 */
+		return apply_filters( 'bp_get_email_tax_type', buddypress()->email_taxonomy_type );
+	}
+
+/**
+ * Return labels used by the email type taxonomy.
+ *
+ * @since 2.5.0
+ *
+ * @return array
+ */
+function bp_get_email_tax_type_labels() {
+
+	/**
+	 * Filters email type taxonomy labels.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $value Associative array (name => label).
+	 */
+	return apply_filters( 'bp_get_email_tax_type_labels', array(
+		'add_new_item'          => _x( 'New Email Situation', 'email type taxonomy label', 'buddypress' ),
+		'all_items'             => _x( 'All Email Situations', 'email type taxonomy label', 'buddypress' ),
+		'edit_item'             => _x( 'Edit Email Situations', 'email type taxonomy label', 'buddypress' ),
+		'items_list'            => _x( 'Email list', 'email type taxonomy label', 'buddypress' ),
+		'items_list_navigation' => _x( 'Email list navigation', 'email type taxonomy label', 'buddypress' ),
+		'menu_name'             => _x( 'Situations', 'email type taxonomy label', 'buddypress' ),
+		'name'                  => _x( 'Situation', 'email type taxonomy name', 'buddypress' ),
+		'new_item_name'         => _x( 'New email situation name', 'email type taxonomy label', 'buddypress' ),
+		'not_found'             => _x( 'No email situations found.', 'email type taxonomy label', 'buddypress' ),
+		'no_terms'              => _x( 'No email situations', 'email type taxonomy label', 'buddypress' ),
+		'popular_items'         => _x( 'Popular Email Situation', 'email type taxonomy label', 'buddypress' ),
+		'search_items'          => _x( 'Search Emails', 'email type taxonomy label', 'buddypress' ),
+		'singular_name'         => _x( 'Email', 'email type taxonomy singular name', 'buddypress' ),
+		'update_item'           => _x( 'Update Email Situation', 'email type taxonomy label', 'buddypress' ),
+		'view_item'             => _x( 'View Email Situation', 'email type taxonomy label', 'buddypress' ),
+	) );
+}
+
+
+/** Email *****************************************************************/
+
+/**
+ * Get an BP_Email object for the specified email type.
+ *
+ * This function pre-populates the object with the subject, content, and template from the appropriate
+ * email post type item. It does not replace placeholder tokens in the content with real values.
+ *
+ * @since 2.5.0
+ *
+ * @param string $email_type Unique identifier for a particular type of email.
+ * @return BP_Email|WP_Error BP_Email object, or WP_Error if there was a problem.
+ */
+function bp_get_email( $email_type ) {
+	$switched = false;
+
+	// Switch to the root blog, where the email posts live.
+	if ( ! bp_is_root_blog() ) {
+		switch_to_blog( bp_get_root_blog_id() );
+		$switched = true;
+	}
+
+	$args = array(
+		'no_found_rows'    => true,
+		'numberposts'      => 1,
+		'post_status'      => 'publish',
+		'post_type'        => bp_get_email_post_type(),
+		'suppress_filters' => false,
+
+		'tax_query'        => array(
+			array(
+				'field'    => 'slug',
+				'taxonomy' => bp_get_email_tax_type(),
+				'terms'    => $email_type,
+			)
+		),
+	);
+
+	/**
+	 * Filters arguments used to find an email post type object.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array  $args       Arguments for get_posts() used to fetch a post object.
+	 * @param string $email_type Unique identifier for a particular type of email.
+	 */
+	$args = apply_filters( 'bp_get_email_args', $args, $email_type );
+	$post = get_posts( $args );
+	if ( ! $post ) {
+		if ( $switched ) {
+			restore_current_blog();
+		}
+
+		return new WP_Error( 'missing_email', __FUNCTION__, array( $email_type, $args ) );
+	}
+
+	/**
+	 * Filters arguments used to create the BP_Email object.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param WP_Post $post       Post object containing the contents of the email.
+	 * @param string  $email_type Unique identifier for a particular type of email.
+	 * @param array   $args       Arguments used with get_posts() to fetch a post object.
+	 * @param WP_Post $post       All posts retrieved by get_posts( $args ). May only contain $post.
+	 */
+	$post  = apply_filters( 'bp_get_email_post', $post[0], $email_type, $args, $post );
+	$email = new BP_Email( $email_type );
+
+
+	/*
+	 * Set some email properties for convenience.
+	 */
+
+	// Post object (sets subject, content, template).
+	$email->set_post_object( $post );
+
+	/**
+	 * Filters the BP_Email object returned by bp_get_email().
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param BP_Email $email      An object representing a single email, ready for mailing.
+	 * @param string   $email_type Unique identifier for a particular type of email.
+	 * @param array    $args       Arguments used with get_posts() to fetch a post object.
+	 * @param WP_Post  $post       All posts retrieved by get_posts( $args ). May only contain $post.
+	 */
+	$retval = apply_filters( 'bp_get_email', $email, $email_type, $args, $post );
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	return $retval;
+}
+
+/**
+ * Send email, similar to WordPress' wp_mail().
+ *
+ * A true return value does not automatically mean that the user received the
+ * email successfully. It just only means that the method used was able to
+ * process the request without any errors.
+ *
+ * @since 2.5.0
+ *
+ * @param string                   $email_type Type of email being sent.
+ * @param string|array|int|WP_User $to         Either a email address, user ID, WP_User object,
+ *                                             or an array containg the address and name.
+ * @param array                    $args {
+ *     Optional. Array of extra parameters.
+ *
+ *     @type array $tokens Optional. Assocative arrays of string replacements for the email.
+ * }
+ * @return bool|WP_Error True if the email was sent successfully. Otherwise, a WP_Error object
+ *                       describing why the email failed to send. The contents will vary based
+ *                       on the email delivery class you are using.
+ */
+function bp_send_email( $email_type, $to, $args = array() ) {
+	static $is_default_wpmail = null;
+	static $wp_html_emails    = null;
+
+	// Has wp_mail() been filtered to send HTML emails?
+	if ( is_null( $wp_html_emails ) ) {
+		/** This filter is documented in wp-includes/pluggable.php */
+		$wp_html_emails = apply_filters( 'wp_mail_content_type', 'text/plain' ) === 'text/html';
+	}
+
+	// Since wp_mail() is a pluggable function, has it been re-defined by another plugin?
+	if ( is_null( $is_default_wpmail ) ) {
+		try {
+			$mirror            = new ReflectionFunction( 'wp_mail' );
+			$is_default_wpmail = substr( $mirror->getFileName(), -strlen( 'pluggable.php' ) ) === 'pluggable.php';
+		} catch ( Exception $e ) {
+			$is_default_wpmail = true;
+		}
+	}
+
+	$args = bp_parse_args( $args, array(
+		'tokens' => array(),
+	), 'send_email' );
+
+
+	/*
+	 * Build the email.
+	 */
+
+	$email = bp_get_email( $email_type );
+	if ( is_wp_error( $email ) ) {
+		return $email;
+	}
+
+	// From, subject, content are set automatically.
+	$email->set_to( $to );
+	$email->set_tokens( $args['tokens'] );
+
+	/**
+	 * Gives access to an email before it is sent.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param BP_Email                 $email      The email (object) about to be sent.
+	 * @param string                   $email_type Type of email being sent.
+	 * @param string|array|int|WP_User $to         Either a email address, user ID, WP_User object,
+	 *                                             or an array containg the address and name.
+     * @param array                    $args {
+	 *     Optional. Array of extra parameters.
+	 *
+	 *     @type array $tokens Optional. Assocative arrays of string replacements for the email.
+	 * }
+	 */
+	do_action_ref_array( 'bp_send_email', array( &$email, $email_type, $to, $args ) );
+
+	$status = $email->validate();
+	if ( is_wp_error( $status ) ) {
+		return $status;
+	}
+
+	/**
+	 * Filter this to skip BP's email handling and instead send everything to wp_mail().
+	 *
+	 * This is done if wp_mail_content_type() has been configured for HTML,
+	 * or if wp_mail() has been redeclared (it's a pluggable function).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param bool $use_wp_mail Whether to fallback to the regular wp_mail() function or not.
+	 */
+	$must_use_wpmail = apply_filters( 'bp_email_use_wp_mail', $wp_html_emails || ! $is_default_wpmail );
+
+	if ( $must_use_wpmail ) {
+		$to = $email->get( 'to' );
+
+		return wp_mail(
+			array_shift( $to )->get_address(),
+			$email->get( 'subject', 'replace-tokens' ),
+			$email->get( 'content_plaintext', 'replace-tokens' )
+		);
+	}
+
+
+	/*
+	 * Send the email.
+	 */
+
+	/**
+	 * Filter the email delivery class.
+	 *
+	 * Defaults to BP_PHPMailer, which as you can guess, implements PHPMailer.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string       $deliver_class The email delivery class name.
+	 * @param string       $email_type    Type of email being sent.
+	 * @param array|string $to            Array or comma-separated list of email addresses to the email to.
+	 * @param array        $args {
+	 *     Optional. Array of extra parameters.
+	 *
+	 *     @type array $tokens Optional. Assocative arrays of string replacements for the email.
+	 * }
+	 */
+	$delivery_class = apply_filters( 'bp_send_email_delivery_class', 'BP_PHPMailer', $email_type, $to, $args );
+	if ( ! class_exists( $delivery_class ) ) {
+		return new WP_Error( 'missing_class', __CLASS__, $this );
+	}
+
+	$delivery = new $delivery_class();
+	$status   = $delivery->bp_email( $email );
+
+	if ( is_wp_error( $status ) ) {
+
+		/**
+		 * Fires after BuddyPress has tried - and failed - to send an email.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param WP_Error $status A WP_Error object describing why the email failed to send. The contents
+		 *                         will vary based on the email delivery class you are using.
+		 * @param BP_Email $email  The email we tried to send.
+		 */
+		do_action( 'bp_send_email_failure', $status, $email );
+
+	} else {
+
+		/**
+		 * Fires after BuddyPress has succesfully sent an email.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param bool     $status True if the email was sent successfully.
+		 * @param BP_Email $email  The email sent.
+		 */
+		do_action( 'bp_send_email_success', $status, $email );
+	}
+
+	return $status;
+}
+
+/**
+ * Return email appearance settings.
+ *
+ * @since 2.5.0
+ * @since 3.0.0 Added "direction" parameter for LTR/RTL email support, and
+ *              "link_text_color" to override that in the email body.
+ *
+ * @return array
+ */
+function bp_email_get_appearance_settings() {
+	/* translators: This is the copyright text for email footers. 1. Copyright year, 2. Site name */
+	$footer_text = array(
+		sprintf(
+			_x( '&copy; %1$s %2$s', 'email', 'buddypress' ),
+			date_i18n( 'Y' ),
+			bp_get_option( 'blogname' )
+		)
+	);
+
+	if ( version_compare( $GLOBALS['wp_version'], '4.9.6', '>=' ) ) {
+		$privacy_policy_url = get_privacy_policy_url();
+		if ( $privacy_policy_url ) {
+			$footer_text[] = sprintf(
+				'<a href="%s">%s</a>',
+				esc_url( $privacy_policy_url ),
+				esc_html__( 'Privacy Policy', 'buddypress' )
+			);
+		}
+	}
+
+	$default_args = array(
+		'body_bg'           => '#FFFFFF',
+		'body_text_color'   => '#555555',
+		'body_text_size'    => 15,
+		'email_bg'          => '#F7F3F0',
+		'footer_bg'         => '#F7F3F0',
+		'footer_text_color' => '#525252',
+		'footer_text_size'  => 12,
+		'header_bg'         => '#F7F3F0',
+		'highlight_color'   => '#D84800',
+		'header_text_color' => '#000000',
+		'header_text_size'  => 30,
+		'direction'         => is_rtl() ? 'right' : 'left',
+
+		'footer_text' => implode( ' &middot; ', $footer_text ),
+	);
+
+	$options = bp_parse_args(
+		bp_get_option( 'bp_email_options', array() ),
+		$default_args,
+		'email_appearance_settings'
+	);
+
+	// Link text colour defaults to the highlight colour.
+	if ( ! isset( $options['link_text_color'] ) ) {
+		$options['link_text_color'] = $options['highlight_color'];
+	}
+
+	return $options;
+}
+
+/**
+ * Get the paths to possible templates for the specified email object.
+ *
+ * @since 2.5.0
+ *
+ * @param WP_Post $object Post to get email template for.
+ * @return array
+ */
+function bp_email_get_template( WP_Post $object ) {
+	$single = "single-{$object->post_type}";
+
+	/**
+	 * Filter the possible template paths for the specified email object.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array   $value  Array of possible template paths.
+	 * @param WP_Post $object WP_Post object.
+	 */
+	return apply_filters( 'bp_email_get_template', array(
+		"assets/emails/{$single}-{$object->post_name}.php",
+		"{$single}-{$object->post_name}.php",
+		"{$single}.php",
+		"assets/emails/{$single}.php",
+	), $object );
+}
+
+/**
+ * Replace all tokens in the input text with appropriate values.
+ *
+ * Intended for use with the email system introduced in BuddyPress 2.5.0.
+ *
+ * @since 2.5.0
+ *
+ * @param string $text   Text to replace tokens in.
+ * @param array  $tokens Token names and replacement values for the $text.
+ * @return string
+ */
+function bp_core_replace_tokens_in_text( $text, $tokens ) {
+	$unescaped = array();
+	$escaped   = array();
+
+	foreach ( $tokens as $token => $value ) {
+		if ( ! is_string( $value ) && is_callable( $value ) ) {
+			$value = call_user_func( $value );
+		}
+
+		// Tokens could be objects or arrays.
+		if ( ! is_scalar( $value ) ) {
+			continue;
+		}
+
+		$unescaped[ '{{{' . $token . '}}}' ] = $value;
+		$escaped[ '{{' . $token . '}}' ]     = esc_html( $value );
+	}
+
+	$text = strtr( $text, $unescaped );  // Do first.
+	$text = strtr( $text, $escaped );
+
+	/**
+	 * Filters text that has had tokens replaced.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $text
+	 * @param array $tokens Token names and replacement values for the $text.
+	 */
+	return apply_filters( 'bp_core_replace_tokens_in_text', $text, $tokens );
+}
+
+/**
+ * Get a list of emails for populating the email post type.
+ *
+ * @since 2.5.1
+ *
+ * @return array
+ */
+function bp_email_get_schema() {
+	return array(
+		'activity-comment' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] {{poster.name}} replied to one of your updates', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "{{poster.name}} replied to one of your updates:\n\n<blockquote>&quot;{{usermessage}}&quot;</blockquote>\n\n<a href=\"{{{thread.url}}}\">Go to the discussion</a> to reply or catch up on the conversation.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{poster.name}} replied to one of your updates:\n\n\"{{usermessage}}\"\n\nGo to the discussion to reply or catch up on the conversation: {{{thread.url}}}", 'buddypress' ),
+		),
+		'activity-comment-author' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] {{poster.name}} replied to one of your comments', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "{{poster.name}} replied to one of your comments:\n\n<blockquote>&quot;{{usermessage}}&quot;</blockquote>\n\n<a href=\"{{{thread.url}}}\">Go to the discussion</a> to reply or catch up on the conversation.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{poster.name}} replied to one of your comments:\n\n\"{{usermessage}}\"\n\nGo to the discussion to reply or catch up on the conversation: {{{thread.url}}}", 'buddypress' ),
+		),
+		'activity-at-message' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] {{poster.name}} mentioned you in a status update', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "{{poster.name}} mentioned you in a status update:\n\n<blockquote>&quot;{{usermessage}}&quot;</blockquote>\n\n<a href=\"{{{mentioned.url}}}\">Go to the discussion</a> to reply or catch up on the conversation.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{poster.name}} mentioned you in a status update:\n\n\"{{usermessage}}\"\n\nGo to the discussion to reply or catch up on the conversation: {{{mentioned.url}}}", 'buddypress' ),
+		),
+		'groups-at-message' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] {{poster.name}} mentioned you in an update', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "{{poster.name}} mentioned you in the group \"{{group.name}}\":\n\n<blockquote>&quot;{{usermessage}}&quot;</blockquote>\n\n<a href=\"{{{mentioned.url}}}\">Go to the discussion</a> to reply or catch up on the conversation.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{poster.name}} mentioned you in the group \"{{group.name}}\":\n\n\"{{usermessage}}\"\n\nGo to the discussion to reply or catch up on the conversation: {{{mentioned.url}}}", 'buddypress' ),
+		),
+		'core-user-registration' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Activate your account', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "Thanks for registering!\n\nTo complete the activation of your account, go to the following link and click on the <strong>Activate</strong> button:\n<a href=\"{{{activate.url}}}\">{{{activate.url}}}</a>\n\nIf the 'Activation Key' field is empty, copy and paste the following into the field - {{key}}", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "Thanks for registering!\n\nTo complete the activation of your account, go to the following link and click on the 'Activate' button: {{{activate.url}}}\n\nIf the 'Activation Key' field is empty, copy and paste the following into the field - {{key}}", 'buddypress' )
+		),
+		'core-user-registration-with-blog' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Activate {{{user-site.url}}}', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "Thanks for registering!\n\nTo complete the activation of your account and site, go to the following link: <a href=\"{{{activate-site.url}}}\">{{{activate-site.url}}}</a>.\n\nAfter you activate, you can visit your site at <a href=\"{{{user-site.url}}}\">{{{user-site.url}}}</a>.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "Thanks for registering!\n\nTo complete the activation of your account and site, go to the following link: {{{activate-site.url}}}\n\nAfter you activate, you can visit your site at {{{user-site.url}}}.", 'buddypress' ),
+			'args'         => array(
+				'multisite' => true,
+			),
+		),
+		'friends-request' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] New friendship request from {{initiator.name}}', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "<a href=\"{{{initiator.url}}}\">{{initiator.name}}</a> wants to add you as a friend.\n\nTo accept this request and manage all of your pending requests, visit: <a href=\"{{{friend-requests.url}}}\">{{{friend-requests.url}}}</a>", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{initiator.name}} wants to add you as a friend.\n\nTo accept this request and manage all of your pending requests, visit: {{{friend-requests.url}}}\n\nTo view {{initiator.name}}'s profile, visit: {{{initiator.url}}}", 'buddypress' ),
+		),
+		'friends-request-accepted' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] {{friend.name}} accepted your friendship request', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "<a href=\"{{{friendship.url}}}\">{{friend.name}}</a> accepted your friend request.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{friend.name}} accepted your friend request.\n\nTo learn more about them, visit their profile: {{{friendship.url}}}", 'buddypress' ),
+		),
+		'groups-details-updated' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Group details updated', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "Group details for the group &quot;<a href=\"{{{group.url}}}\">{{group.name}}</a>&quot; were updated:\n<blockquote>{{changed_text}}</blockquote>", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "Group details for the group \"{{group.name}}\" were updated:\n\n{{changed_text}}\n\nTo view the group, visit: {{{group.url}}}", 'buddypress' ),
+		),
+		'groups-invitation' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] You have an invitation to the group: "{{group.name}}"', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "<a href=\"{{{inviter.url}}}\">{{inviter.name}}</a> has invited you to join the group: &quot;{{group.name}}&quot;.\n<a href=\"{{{invites.url}}}\">Go here to accept your invitation</a> or <a href=\"{{{group.url}}}\">visit the group</a> to learn more.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{inviter.name}} has invited you to join the group: \"{{group.name}}\".\n\nTo accept your invitation, visit: {{{invites.url}}}\n\nTo learn more about the group, visit: {{{group.url}}}.\nTo view {{inviter.name}}'s profile, visit: {{{inviter.url}}}", 'buddypress' ),
+		),
+		'groups-member-promoted' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] You have been promoted in the group: "{{group.name}}"', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "You have been promoted to <b>{{promoted_to}}</b> in the group &quot;<a href=\"{{{group.url}}}\">{{group.name}}</a>&quot;.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "You have been promoted to {{promoted_to}} in the group: \"{{group.name}}\".\n\nTo visit the group, go to: {{{group.url}}}", 'buddypress' ),
+		),
+		'groups-membership-request' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Membership request for group: {{group.name}}', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "<a href=\"{{{profile.url}}}\">{{requesting-user.name}}</a> wants to join the group &quot;{{group.name}}&quot;. As you are an administrator of this group, you must either accept or reject the membership request.\n\n<a href=\"{{{group-requests.url}}}\">Go here to manage this</a> and all other pending requests.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{requesting-user.name}} wants to join the group \"{{group.name}}\". As you are the administrator of this group, you must either accept or reject the membership request.\n\nTo manage this and all other pending requests, visit: {{{group-requests.url}}}\n\nTo view {{requesting-user.name}}'s profile, visit: {{{profile.url}}}", 'buddypress' ),
+		),
+		'messages-unread' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] New message from {{sender.name}}', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "{{sender.name}} sent you a new message: &quot;{{usersubject}}&quot;\n\n<blockquote>&quot;{{usermessage}}&quot;</blockquote>\n\n<a href=\"{{{message.url}}}\">Go to the discussion</a> to reply or catch up on the conversation.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "{{sender.name}} sent you a new message: \"{{usersubject}}\"\n\n\"{{usermessage}}\"\n\nGo to the discussion to reply or catch up on the conversation: {{{message.url}}}", 'buddypress' ),
+		),
+		'settings-verify-email-change' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Verify your new email address', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "You recently changed the email address associated with your account on {{site.name}} to {{user.email}}. If this is correct, <a href=\"{{{verify.url}}}\">go here to confirm the change</a>.\n\nOtherwise, you can safely ignore and delete this email if you have changed your mind, or if you think you have received this email in error.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "You recently changed the email address associated with your account on {{site.name}} to {{user.email}}. If this is correct, go to the following link to confirm the change: {{{verify.url}}}\n\nOtherwise, you can safely ignore and delete this email if you have changed your mind, or if you think you have received this email in error.", 'buddypress' ),
+		),
+		'groups-membership-request-accepted' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Membership request for group "{{group.name}}" accepted', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "Your membership request for the group &quot;<a href=\"{{{group.url}}}\">{{group.name}}</a>&quot; has been accepted.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "Your membership request for the group \"{{group.name}}\" has been accepted.\n\nTo view the group, visit: {{{group.url}}}", 'buddypress' ),
+		),
+		'groups-membership-request-rejected' => array(
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_title'   => __( '[{{{site.name}}}] Membership request for group "{{group.name}}" rejected', 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_content' => __( "Your membership request for the group &quot;<a href=\"{{{group.url}}}\">{{group.name}}</a>&quot; has been rejected.", 'buddypress' ),
+			/* translators: do not remove {} brackets or translate its contents. */
+			'post_excerpt' => __( "Your membership request for the group \"{{group.name}}\" has been rejected.\n\nTo request membership again, visit: {{{group.url}}}", 'buddypress' ),
+		),
+	);
+}
+
+/**
+ * Get a list of emails for populating email type taxonomy terms.
+ *
+ * @since 2.5.1
+ * @since 2.7.0 $field argument added.
+ *
+ * @param string $field Optional; defaults to "description" for backwards compatibility. Other values: "all".
+ * @return array {
+ *     The array of email types and their schema.
+ *
+ *     @type string $description The description of the action which causes this to trigger.
+ *     @type array  $unsubscribe {
+ *         Replacing this with false indicates that a user cannot unsubscribe from this type.
+ *
+ *         @type string $meta_key The meta_key used to toggle the email setting for this notification.
+ *         @type string $message  The message shown when the user has successfully unsubscribed.
+ *     }
+ */
+function bp_email_get_type_schema( $field = 'description' ) {
+	$activity_comment = array(
+		'description'	=> __( 'A member has replied to an activity update that the recipient posted.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_activity_new_reply',
+			'message'	=> __( 'You will no longer receive emails when someone replies to an update or comment you posted.', 'buddypress' ),
+			),
+	);
+
+	$activity_comment_author = array(
+		'description'	=> __( 'A member has replied to a comment on an activity update that the recipient posted.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_activity_new_reply',
+			'message'	=> __( 'You will no longer receive emails when someone replies to an update or comment you posted.', 'buddypress' ),
+			),
+	);
+
+	$activity_at_message = array(
+		'description'	=> __( 'Recipient was mentioned in an activity update.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_activity_new_mention',
+			'message'	=> __( 'You will no longer receive emails when someone mentions you in an update.', 'buddypress' ),
+		),
+	);
+
+	$groups_at_message = array(
+		'description'	=> __( 'Recipient was mentioned in a group activity update.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_activity_new_mention',
+			'message'	=> __( 'You will no longer receive emails when someone mentions you in an update.', 'buddypress' ),
+		),
+	);
+
+	$core_user_registration = array(
+		'description'	=> __( 'Recipient has registered for an account.', 'buddypress' ),
+		'unsubscribe'	=> false,
+	);
+
+	$core_user_registration_with_blog = array(
+		'description'	=> __( 'Recipient has registered for an account and site.', 'buddypress' ),
+		'unsubscribe'	=> false,
+	);
+
+	$friends_request = array(
+		'description'	=> __( 'A member has sent a friend request to the recipient.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_friends_friendship_request',
+			'message'	=> __( 'You will no longer receive emails when someone sends you a friend request.', 'buddypress' ),
+		),
+	);
+
+	$friends_request_accepted = array(
+		'description'	=> __( 'Recipient has had a friend request accepted by a member.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_friends_friendship_accepted',
+			'message'	=> __( 'You will no longer receive emails when someone accepts your friendship request.', 'buddypress' ),
+		),
+	);
+
+	$groups_details_updated = array(
+		'description'	=> __( "A group's details were updated.", 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_groups_group_updated',
+			'message'	=> __( 'You will no longer receive emails when one of your groups is updated.', 'buddypress' ),
+		),
+	);
+
+	$groups_invitation = array(
+		'description'	=> __( 'A member has sent a group invitation to the recipient.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_groups_invite',
+			'message'	=> __( 'You will no longer receive emails when you are invited to join a group.', 'buddypress' ),
+		),
+	);
+
+	$groups_member_promoted = array(
+		'description'	=> __( "Recipient's status within a group has changed.", 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_groups_admin_promotion',
+			'message'	=> __( 'You will no longer receive emails when you have been promoted in a group.', 'buddypress' ),
+		),
+	);
+
+	$groups_membership_request = array(
+		'description'	=> __( 'A member has requested permission to join a group.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_groups_membership_request',
+			'message'	=> __( 'You will no longer receive emails when someone requests to be a member of your group.', 'buddypress' ),
+		),
+	);
+
+	$messages_unread = array(
+		'description'	=> __( 'Recipient has received a private message.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_messages_new_message',
+			'message'	=> __( 'You will no longer receive emails when someone sends you a message.', 'buddypress' ),
+		),
+	);
+
+	$settings_verify_email_change = array(
+		'description'	=> __( 'Recipient has changed their email address.', 'buddypress' ),
+		'unsubscribe'	=> false,
+	);
+
+	$groups_membership_request_accepted = array(
+		'description'	=> __( 'Recipient had requested to join a group, which was accepted.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_membership_request_completed',
+			'message'	=> __( 'You will no longer receive emails when your request to join a group has been accepted or denied.', 'buddypress' ),
+		),
+	);
+
+	$groups_membership_request_rejected = array(
+		'description'	=> __( 'Recipient had requested to join a group, which was rejected.', 'buddypress' ),
+		'unsubscribe'	=> array(
+			'meta_key'	=> 'notification_membership_request_completed',
+			'message'	=> __( 'You will no longer receive emails when your request to join a group has been accepted or denied.', 'buddypress' ),
+		),
+	);
+
+	$types = array(
+		'activity-comment'                   => $activity_comment,
+		'activity-comment-author'            => $activity_comment_author,
+		'activity-at-message'                => $activity_at_message,
+		'groups-at-message'                  => $groups_at_message,
+		'core-user-registration'             => $core_user_registration,
+		'core-user-registration-with-blog'   => $core_user_registration_with_blog,
+		'friends-request'                    => $friends_request,
+		'friends-request-accepted'           => $friends_request_accepted,
+		'groups-details-updated'             => $groups_details_updated,
+		'groups-invitation'                  => $groups_invitation,
+		'groups-member-promoted'             => $groups_member_promoted,
+		'groups-membership-request'          => $groups_membership_request,
+		'messages-unread'                    => $messages_unread,
+		'settings-verify-email-change'       => $settings_verify_email_change,
+		'groups-membership-request-accepted' => $groups_membership_request_accepted,
+		'groups-membership-request-rejected' => $groups_membership_request_rejected,
+	);
+
+	if ( $field !== 'all' ) {
+		return wp_list_pluck( $types, $field );
+	} else {
+		return $types;
+	}
+}
+
+/**
+ * Handles unsubscribing user from notification emails.
+ *
+ * @since 2.7.0
+ */
+function bp_email_unsubscribe_handler() {
+	$emails         = bp_email_get_unsubscribe_type_schema();
+	$raw_email_type = ! empty( $_GET['nt'] ) ? $_GET['nt'] : '';
+	$raw_hash       = ! empty( $_GET['nh'] ) ? $_GET['nh'] : '';
+	$raw_user_id    = ! empty( $_GET['uid'] ) ? absint( $_GET['uid'] ) : 0;
+	$new_hash       = hash_hmac( 'sha1', "{$raw_email_type}:{$raw_user_id}", bp_email_get_salt() );
+
+	// Check required values.
+	if ( ! $raw_user_id || ! $raw_email_type || ! $raw_hash || ! array_key_exists( $raw_email_type, $emails ) ) {
+		$redirect_to = wp_login_url();
+		$result_msg  = __( 'Something has gone wrong.', 'buddypress' );
+		$unsub_msg   = __( 'Please log in and go to your settings to unsubscribe from notification emails.', 'buddypress' );
+
+	// Check valid hash.
+	} elseif ( ! hash_equals( $new_hash, $raw_hash ) ) {
+		$redirect_to = wp_login_url();
+		$result_msg  = __( 'Something has gone wrong.', 'buddypress' );
+		$unsub_msg   = __( 'Please log in and go to your settings to unsubscribe from notification emails.', 'buddypress' );
+
+	// Don't let authenticated users unsubscribe other users' email notifications.
+	} elseif ( is_user_logged_in() && get_current_user_id() !== $raw_user_id ) {
+		$result_msg  = __( 'Something has gone wrong.', 'buddypress' );
+		$unsub_msg   = __( 'Please go to your notifications settings to unsubscribe from emails.', 'buddypress' );
+
+		if ( bp_is_active( 'settings' ) ) {
+			$redirect_to = sprintf(
+				'%s%s/notifications/',
+				bp_core_get_user_domain( get_current_user_id() ),
+				bp_get_settings_slug()
+			);
+		} else {
+			$redirect_to = bp_core_get_user_domain( get_current_user_id() );
+		}
+
+	} else {
+		if ( bp_is_active( 'settings' ) ) {
+			$redirect_to = sprintf(
+				'%s%s/notifications/',
+				bp_core_get_user_domain( $raw_user_id ),
+				bp_get_settings_slug()
+			);
+		} else {
+			$redirect_to = bp_core_get_user_domain( $raw_user_id );
+		}
+
+		// Unsubscribe.
+		$meta_key = $emails[ $raw_email_type ]['unsubscribe']['meta_key'];
+		bp_update_user_meta( $raw_user_id, $meta_key, 'no' );
+
+		$result_msg = $emails[ $raw_email_type ]['unsubscribe']['message'];
+		$unsub_msg  = __( 'You can change this or any other email notification preferences in your email settings.', 'buddypress' );
+	}
+
+	$message = sprintf(
+		'%1$s <a href="%2$s">%3$s</a>',
+		$result_msg,
+		esc_url( $redirect_to ),
+		esc_html( $unsub_msg )
+	);
+
+	bp_core_add_message( $message );
+	bp_core_redirect( bp_core_get_user_domain( $raw_user_id ) );
+
+	exit;
+}
+
+/**
+ * Creates unsubscribe link for notification emails.
+ *
+ * @since 2.7.0
+ *
+ * @param string $redirect_to The URL to which the unsubscribe query string is appended.
+ * @param array $args {
+ *    Used to build unsubscribe query string.
+ *
+ *    @type string $notification_type Which notification type is being sent.
+ *    @type string $user_id           The ID of the user to whom the notification is sent.
+ *    @type string $redirect_to       Optional. The url to which the user will be redirected. Default is the activity directory.
+ * }
+ * @return string The unsubscribe link.
+ */
+function bp_email_get_unsubscribe_link( $args ) {
+	$emails = bp_email_get_unsubscribe_type_schema();
+
+	if ( empty( $args['notification_type'] ) || ! array_key_exists( $args['notification_type'], $emails ) ) {
+		return wp_login_url();
+	}
+
+	$email_type  = $args['notification_type'];
+	$redirect_to = ! empty( $args['redirect_to'] ) ? $args['redirect_to'] : site_url();
+	$user_id     = (int) $args['user_id'];
+
+	// Bail out if the activity type is not un-unsubscribable.
+	if ( empty( $emails[ $email_type ]['unsubscribe'] ) ) {
+		return '';
+	}
+
+	$link = add_query_arg(
+		array(
+			'action' => 'unsubscribe',
+			'nh'     => hash_hmac( 'sha1', "{$email_type}:{$user_id}", bp_email_get_salt() ),
+			'nt'     => $args['notification_type'],
+			'uid'    => $user_id,
+		),
+		$redirect_to
+	);
+
+	/**
+	 * Filters the unsubscribe link.
+	 *
+	 * @since 2.7.0
+	 */
+	return apply_filters( 'bp_email_get_link', $link, $redirect_to, $args );
+}
+
+/**
+ * Get a persistent salt for email unsubscribe links.
+ *
+ * @since 2.7.0
+ *
+ * @return string|null Returns null if value isn't set, otherwise string.
+ */
+function bp_email_get_salt() {
+	return bp_get_option( 'bp-emails-unsubscribe-salt', null );
+}
+
+/**
+ * Get a list of emails for use in our unsubscribe functions.
+ *
+ * @since 2.8.0
+ *
+ * @see https://buddypress.trac.wordpress.org/ticket/7431
+ *
+ * @return array The array of email types and their schema.
+ */
+function bp_email_get_unsubscribe_type_schema() {
+	$emails = bp_email_get_type_schema( 'all' );
+
+	/**
+	 * Filters the return of `bp_email_get_type_schema( 'all' )` for use with
+	 * our unsubscribe functionality.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param array $emails The array of email types and their schema.
+	 */
+	return (array) apply_filters( 'bp_email_get_unsubscribe_type_schema', $emails );
+}
+
+/**
+ * Get BuddyPress content allowed tags.
+ *
+ * @since  3.0.0
+ *
+ * @global array $allowedtags KSES allowed HTML elements.
+ * @return array              BuddyPress content allowed tags.
+ */
+function bp_get_allowedtags() {
+	global $allowedtags;
+
+	return array_merge_recursive( $allowedtags, array(
+		'a' => array(
+			'aria-label'      => array(),
+			'class'           => array(),
+			'data-bp-tooltip' => array(),
+			'id'              => array(),
+			'rel'             => array(),
+		),
+		'img' => array(
+			'src'    => array(),
+			'alt'    => array(),
+			'width'  => array(),
+			'height' => array(),
+			'class'  => array(),
+			'id'     => array(),
+		),
+		'span'=> array(
+			'class'          => array(),
+			'data-livestamp' => array(),
+		),
+		'ul' => array(),
+		'ol' => array(),
+		'li' => array(),
+	) );
+}
+
+/**
+ * Remove script and style tags from a string.
+ *
+ * @since 3.0.1
+ *
+ * @param  string $string The string to strip tags from.
+ * @return string         The stripped tags string.
+ */
+function bp_strip_script_and_style_tags( $string ) {
+	return preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $string );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-loader.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BuddyPress Core Loader.
+ *
+ * Core contains the commonly used functions, classes, and APIs.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-core component.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_core() {
+	buddypress()->core = new BP_Core();
+}
+add_action( 'bp_loaded', 'bp_setup_core', 0 );
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-moderation.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-moderation.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-moderation.php	(revision 12265)
@@ -0,0 +1,343 @@
+<?php
+/**
+ * BuddyPress Moderation Functions.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Moderation ****************************************************************/
+
+/**
+ * Check for flooding.
+ *
+ * Check to make sure that a user is not making too many posts in a short amount
+ * of time.
+ *
+ * @since 1.6.0
+ *
+ * @param int $user_id User id to check for flood.
+ * @return bool True if there is no flooding, false if there is.
+ */
+function bp_core_check_for_flood( $user_id = 0 ) {
+
+	// Option disabled. No flood checks.
+	if ( !$throttle_time = bp_get_option( '_bp_throttle_time' ) ) {
+		return true;
+	}
+
+	// Bail if no user ID passed.
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	$last_posted = get_user_meta( $user_id, '_bp_last_posted', true );
+	if ( isset( $last_posted ) && ( time() < ( $last_posted + $throttle_time ) ) && !current_user_can( 'throttle' ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Check for moderation keys and too many links.
+ *
+ * @since 1.6.0
+ * @since 2.6.0 Added $error_type parameter.
+ *
+ * @param int    $user_id    User ID.
+ * @param string $title      The title of the content.
+ * @param string $content    The content being posted.
+ * @param string $error_type The error type to return. Either 'bool' or 'wp_error'.
+ * @return bool|WP_Error True if test is passed, false if fail.
+ */
+function bp_core_check_for_moderation( $user_id = 0, $title = '', $content = '', $error_type = 'bool' ) {
+
+	/**
+	 * Filters whether or not to bypass checking for moderation keys and too many links.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param bool   $value   Whether or not to bypass checking. Default false.
+	 * @param int    $user_id Topic of reply author ID.
+	 * @param string $title   The title of the content.
+	 * @param string $content $the content being posted.
+	 */
+	if ( apply_filters( 'bp_bypass_check_for_moderation', false, $user_id, $title, $content ) ) {
+		return true;
+	}
+
+	// Bail if super admin is author.
+	if ( is_super_admin( $user_id ) ) {
+		return true;
+	}
+
+	// Define local variable(s).
+	$_post     = array();
+	$match_out = '';
+
+	/** User Data ************************************************************
+	 */
+
+	if ( ! empty( $user_id ) ) {
+
+		// Get author data.
+		$user = get_userdata( $user_id );
+
+		// If data exists, map it.
+		if ( ! empty( $user ) ) {
+			$_post['author'] = $user->display_name;
+			$_post['email']  = $user->user_email;
+			$_post['url']    = $user->user_url;
+		}
+	}
+
+	// Current user IP and user agent.
+	$_post['user_ip'] = bp_core_current_user_ip();
+	$_post['user_ua'] = bp_core_current_user_ua();
+
+	// Post title and content.
+	$_post['title']   = $title;
+	$_post['content'] = $content;
+
+	/** Max Links ************************************************************
+	 */
+
+	$max_links = get_option( 'comment_max_links' );
+	if ( ! empty( $max_links ) ) {
+
+		// How many links?
+		$num_links = preg_match_all( '/(http|ftp|https):\/\//i', $content, $match_out );
+
+		// Allow for bumping the max to include the user's URL.
+		if ( ! empty( $_post['url'] ) ) {
+
+			/**
+			 * Filters the maximum amount of links allowed to include the user's URL.
+			 *
+			 * @since 1.6.0
+			 *
+			 * @param string $num_links How many links found.
+			 * @param string $value     User's url.
+			 */
+			$num_links = apply_filters( 'comment_max_links_url', $num_links, $_post['url'] );
+		}
+
+		// Das ist zu viele links!
+		if ( $num_links >= $max_links ) {
+			if ( 'bool' === $error_type ) {
+				return false;
+			} else {
+				return new WP_Error( 'bp_moderation_too_many_links', __( 'You have posted too many links', 'buddypress' ) );
+			}
+		}
+	}
+
+	/** Blacklist ************************************************************
+	 */
+
+	// Get the moderation keys.
+	$blacklist = trim( get_option( 'moderation_keys' ) );
+
+	// Bail if blacklist is empty.
+	if ( ! empty( $blacklist ) ) {
+
+		// Get words separated by new lines.
+		$words = explode( "\n", $blacklist );
+
+		// Loop through words.
+		foreach ( (array) $words as $word ) {
+
+			// Trim the whitespace from the word.
+			$word = trim( $word );
+
+			// Skip empty lines.
+			if ( empty( $word ) ) {
+				continue;
+			}
+
+			// Do some escaping magic so that '#' chars in the
+			// spam words don't break things.
+			$word    = preg_quote( $word, '#' );
+			$pattern = "#$word#i";
+
+			// Loop through post data.
+			foreach ( $_post as $post_data ) {
+
+				// Check each user data for current word.
+				if ( preg_match( $pattern, $post_data ) ) {
+					if ( 'bool' === $error_type ) {
+						return false;
+					} else {
+						return new WP_Error( 'bp_moderation_word_match', _x( 'You have posted an inappropriate word.', 'Comment moderation', 'buddypress' ) );
+					}
+				}
+			}
+		}
+	}
+
+	// Check passed successfully.
+	return true;
+}
+
+/**
+ * Check for blocked keys.
+ *
+ * @since 1.6.0
+ * @since 2.6.0 Added $error_type parameter.
+ *
+ * @todo Why don't we use wp_blacklist_check() for this?
+ *
+ * @param int    $user_id    User ID.
+ * @param string $title      The title of the content.
+ * @param string $content    The content being posted.
+ * @param string $error_type The error type to return. Either 'bool' or 'wp_error'.
+ * @return bool|WP_Error True if test is passed, false if fail.
+ */
+function bp_core_check_for_blacklist( $user_id = 0, $title = '', $content = '', $error_type = 'bool' ) {
+
+	/**
+	 * Filters whether or not to bypass checking for blocked keys.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param bool   $value   Whether or not to bypass checking. Default false.
+	 * @param int    $user_id Topic of reply author ID.
+	 * @param string $title   The title of the content.
+	 * @param string $content $the content being posted.
+	 */
+	if ( apply_filters( 'bp_bypass_check_for_blacklist', false, $user_id, $title, $content ) ) {
+		return true;
+	}
+
+	// Bail if super admin is author.
+	if ( is_super_admin( $user_id ) ) {
+		return true;
+	}
+
+	// Define local variable.
+	$_post = array();
+
+	/** Blacklist ************************************************************
+	 */
+
+	// Get the moderation keys.
+	$blacklist = trim( get_option( 'blacklist_keys' ) );
+
+	// Bail if blacklist is empty.
+	if ( empty( $blacklist ) ) {
+		return true;
+	}
+
+	/** User Data ************************************************************
+	 */
+
+	// Map current user data.
+	if ( ! empty( $user_id ) ) {
+
+		// Get author data.
+		$user = get_userdata( $user_id );
+
+		// If data exists, map it.
+		if ( ! empty( $user ) ) {
+			$_post['author'] = $user->display_name;
+			$_post['email']  = $user->user_email;
+			$_post['url']    = $user->user_url;
+		}
+	}
+
+	// Current user IP and user agent.
+	$_post['user_ip'] = bp_core_current_user_ip();
+	$_post['user_ua'] = bp_core_current_user_ua();
+
+	// Post title and content.
+	$_post['title']   = $title;
+	$_post['content'] = $content;
+
+	/** Words ****************************************************************
+	 */
+
+	// Get words separated by new lines.
+	$words = explode( "\n", $blacklist );
+
+	// Loop through words.
+	foreach ( (array) $words as $word ) {
+
+		// Trim the whitespace from the word.
+		$word = trim( $word );
+
+		// Skip empty lines.
+		if ( empty( $word ) ) { continue; }
+
+		// Do some escaping magic so that '#' chars in the
+		// spam words don't break things.
+		$word    = preg_quote( $word, '#' );
+		$pattern = "#$word#i";
+
+		// Loop through post data.
+		foreach( $_post as $post_data ) {
+
+			// Check each user data for current word.
+			if ( preg_match( $pattern, $post_data ) ) {
+				if ( 'bool' === $error_type ) {
+					return false;
+				} else {
+					return new WP_Error( 'bp_moderation_blacklist_match', _x( 'You have posted an inappropriate word.', 'Comment blacklist', 'buddypress' ) );
+				}
+			}
+		}
+	}
+
+	// Check passed successfully.
+	return true;
+}
+
+/**
+ * Get the current user's IP address.
+ *
+ * @since 1.6.0
+ *
+ * @return string IP address.
+ */
+function bp_core_current_user_ip() {
+	$retval = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
+
+	/**
+	 * Filters the current user's IP address.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $retval Current user's IP Address.
+	 */
+	return apply_filters( 'bp_core_current_user_ip', $retval );
+}
+
+/**
+ * Get the current user's user-agent.
+ *
+ * @since 1.6.0
+ *
+ * @return string User agent string.
+ */
+function bp_core_current_user_ua() {
+
+	// Sanity check the user agent.
+	if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
+		$retval = substr( $_SERVER['HTTP_USER_AGENT'], 0, 254 );
+	} else {
+		$retval = '';
+	}
+
+	/**
+	 * Filters the current user's user-agent.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $retval Current user's user-agent.
+	 */
+	return apply_filters( 'bp_core_current_user_ua', $retval );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-options.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-options.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-options.php	(revision 12265)
@@ -0,0 +1,752 @@
+<?php
+/**
+ * BuddyPress Options.
+ *
+ * @package BuddyPress
+ * @subpackage Options
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Get the default site options and their values.
+ *
+ * Default values should not be set by calls to `get_option()` or `get_site_option()` due to
+ * these causing load order problems with `bp_core_clear_root_options_cache()`; see #BP7227.
+ *
+ * @since 1.6.0
+ *
+ * @return array Filtered option names and values.
+ */
+function bp_get_default_options() {
+
+	// Default options.
+	$options = array (
+
+		/* Components ********************************************************/
+
+		'bp-deactivated-components'            => array(),
+
+		/* XProfile **********************************************************/
+
+		// Base profile groups name.
+		'bp-xprofile-base-group-name'          => 'Base',
+
+		// Base fullname field name.
+		'bp-xprofile-fullname-field-name'      => 'Name',
+
+		/* Blogs *************************************************************/
+
+		// Used to decide if blogs need indexing.
+		'bp-blogs-first-install'               => false,
+
+		/* Settings **********************************************************/
+
+		// Disable the WP to BP profile sync.
+		'bp-disable-profile-sync'              => false,
+
+		// Hide the Toolbar for logged out users.
+		'hide-loggedout-adminbar'              => false,
+
+		// Avatar uploads.
+		'bp-disable-avatar-uploads'            => false,
+
+		// Cover image uploads.
+		'bp-disable-cover-image-uploads'       => false,
+
+		// Group Profile Photos.
+		'bp-disable-group-avatar-uploads'      => false,
+
+		// Group Cover image uploads.
+		'bp-disable-group-cover-image-uploads' => false,
+
+		// Allow users to delete their own accounts.
+		'bp-disable-account-deletion'          => false,
+
+		// Allow comments on post and comment activity items.
+		'bp-disable-blogforum-comments'        => true,
+
+		// The ID for the current theme package.
+		'_bp_theme_package_id'                 => 'nouveau',
+
+		// Email unsubscribe salt.
+		'bp-emails-unsubscribe-salt'           => '',
+
+		/* Groups ************************************************************/
+
+		// @todo Move this into the groups component
+		// Restrict group creation to super admins.
+		'bp_restrict_group_creation'           => false,
+
+		/* Akismet ***********************************************************/
+
+		// Users from all sites can post.
+		'_bp_enable_akismet'                   => true,
+
+		/* Activity HeartBeat ************************************************/
+
+		// HeartBeat is on to refresh activities.
+		'_bp_enable_heartbeat_refresh'         => true,
+
+		/* BuddyBar **********************************************************/
+
+		// Force the BuddyBar.
+		'_bp_force_buddybar'                   => false,
+
+		/* Legacy *********************************************/
+
+		// Do not register the bp-default themes directory.
+		'_bp_retain_bp_default'                => false,
+
+		// Ignore deprecated code.
+		'_bp_ignore_deprecated_code'           => true,
+
+		/* Widgets **************************************************/
+		'widget_bp_core_login_widget'                => false,
+		'widget_bp_core_members_widget'              => false,
+		'widget_bp_core_whos_online_widget'          => false,
+		'widget_bp_core_recently_active_widget'      => false,
+		'widget_bp_groups_widget'                    => false,
+		'widget_bp_messages_sitewide_notices_widget' => false,
+	);
+
+	/**
+	 * Filters the default options to be set upon activation.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $options Array of default options to set.
+	 */
+	return apply_filters( 'bp_get_default_options', $options );
+}
+
+/**
+ * Add default options when BuddyPress is first activated.
+ *
+ * Only called once when BuddyPress is activated.
+ * Non-destructive, so existing settings will not be overridden.
+ *
+ * @since 1.6.0
+ */
+function bp_add_options() {
+
+	// Get the default options and values.
+	$options = bp_get_default_options();
+
+	// Add default options.
+	foreach ( $options as $key => $value ) {
+		bp_add_option( $key, $value );
+	}
+
+	/**
+	 * Fires after the addition of default options when BuddyPress is first activated.
+	 *
+	 * Allows previously activated plugins to append their own options.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_add_options' );
+}
+
+/**
+ * Delete default options.
+ *
+ * Hooked to bp_uninstall, it is only called once when BuddyPress is uninstalled.
+ * This is destructive, so existing settings will be destroyed.
+ *
+ * Currently unused.
+ *
+ * @since 1.6.0
+ */
+function bp_delete_options() {
+
+	// Get the default options and values.
+	$options = bp_get_default_options();
+
+	// Add default options.
+	foreach ( array_keys( $options ) as $key ) {
+		delete_option( $key );
+	}
+
+	/**
+	 * Fires after the deletion of default options when BuddyPress is first deactivated.
+	 *
+	 * Allows previously activated plugins to append their own options.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_delete_options' );
+}
+
+/**
+ * Add filters to each BP option, allowing them to be overloaded from inside the $bp->options array.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_option_filters() {
+
+	// Get the default options and values.
+	$options = bp_get_default_options();
+
+	// Add filters to each BuddyPress option.
+	foreach ( array_keys( $options ) as $key ) {
+		add_filter( 'pre_option_' . $key, 'bp_pre_get_option' );
+	}
+
+	/**
+	 * Fires after the addition of filters to each BuddyPress option.
+	 *
+	 * Allows previously activated plugins to append their own options.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_setup_option_filters' );
+}
+
+/**
+ * Filter default options and allow them to be overloaded from inside the $bp->options array.
+ *
+ * @since 1.6.0
+ *
+ * @param bool $value Optional. Default value false.
+ * @return mixed False if not overloaded, mixed if set.
+ */
+function bp_pre_get_option( $value = false ) {
+	$bp = buddypress();
+
+	// Remove the filter prefix.
+	$option = str_replace( 'pre_option_', '', current_filter() );
+
+	// Check the options global for preset value.
+	if ( ! empty( $bp->options[ $option ] ) ) {
+		$value = $bp->options[ $option ];
+	}
+
+	// Always return a value, even if false.
+	return $value;
+}
+
+/**
+ * Retrieve an option.
+ *
+ * This is a wrapper for {@link get_blog_option()}, which in turn stores settings data
+ * (such as bp-pages) on the appropriate blog, given your current setup.
+ *
+ * The 'bp_get_option' filter is primarily for backward-compatibility.
+ *
+ * @since 1.5.0
+ *
+ * @param string $option_name The option to be retrieved.
+ * @param string $default     Optional. Default value to be returned if the option
+ *                            isn't set. See {@link get_blog_option()}.
+ * @return mixed The value for the option.
+ */
+function bp_get_option( $option_name, $default = '' ) {
+	$value = get_blog_option( bp_get_root_blog_id(), $option_name, $default );
+
+	/**
+	 * Filters the option value for the requested option.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param mixed $value The value for the option.
+	 */
+	return apply_filters( 'bp_get_option', $value );
+}
+
+/**
+ * Add an option.
+ *
+ * This is a wrapper for {@link add_blog_option()}, which in turn stores
+ * settings data on the appropriate blog, given your current setup.
+ *
+ * @since 2.0.0
+ *
+ * @param string $option_name The option key to be set.
+ * @param mixed  $value       The value to be set.
+ * @return bool True on success, false on failure.
+ */
+function bp_add_option( $option_name, $value ) {
+	return add_blog_option( bp_get_root_blog_id(), $option_name, $value );
+}
+
+/**
+ * Save an option.
+ *
+ * This is a wrapper for {@link update_blog_option()}, which in turn stores
+ * settings data (such as bp-pages) on the appropriate blog, given your current
+ * setup.
+ *
+ * @since 1.5.0
+ *
+ * @param string $option_name The option key to be set.
+ * @param mixed  $value       The value to be set.
+ * @return bool True on success, false on failure.
+ */
+function bp_update_option( $option_name, $value ) {
+	return update_blog_option( bp_get_root_blog_id(), $option_name, $value );
+}
+
+/**
+ * Delete an option.
+ *
+ * This is a wrapper for {@link delete_blog_option()}, which in turn deletes
+ * settings data (such as bp-pages) on the appropriate blog, given your current
+ * setup.
+ *
+ * @since 1.5.0
+ *
+ * @param string $option_name The option key to be deleted.
+ * @return bool True on success, false on failure.
+ */
+function bp_delete_option( $option_name ) {
+	return delete_blog_option( bp_get_root_blog_id(), $option_name );
+}
+
+/**
+ * Copy BP options from a single site to multisite config.
+ *
+ * Run when switching from single to multisite and we need to copy blog options
+ * to site options.
+ *
+ * This function is no longer used.
+ *
+ * @since 1.2.4
+ * @deprecated 1.6.0
+ *
+ * @param array $keys Array of site options.
+ * @return bool
+ */
+function bp_core_activate_site_options( $keys = array() ) {
+
+	if ( !empty( $keys ) && is_array( $keys ) ) {
+		$bp = buddypress();
+
+		$errors = false;
+
+		foreach ( $keys as $key => $default ) {
+			if ( empty( $bp->site_options[ $key ] ) ) {
+				$bp->site_options[ $key ] = bp_get_option( $key, $default );
+
+				if ( !bp_update_option( $key, $bp->site_options[ $key ] ) ) {
+					$errors = true;
+				}
+			}
+		}
+
+		if ( empty( $errors ) ) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/**
+ * Fetch global BP options.
+ *
+ * BuddyPress uses common options to store configuration settings. Many of these
+ * settings are needed at run time. Instead of fetching them all and adding many
+ * initial queries to each page load, let's fetch them all in one go.
+ *
+ * @since 1.5.0
+ *
+ * @todo Use settings API and audit these methods.
+ *
+ * @return array $root_blog_options_meta List of options.
+ */
+function bp_core_get_root_options() {
+	global $wpdb;
+
+	// Get all the BuddyPress settings, and a few useful WP ones too.
+	$root_blog_options                   = bp_get_default_options();
+	$root_blog_options['registration']   = '0';
+	$root_blog_options['avatar_default'] = 'mysteryman';
+	$root_blog_option_keys               = array_keys( $root_blog_options );
+
+	// Do some magic to get all the root blog options in 1 swoop
+	// Check cache first - We cache here instead of using the standard WP
+	// settings cache because the current blog may not be the root blog,
+	// and it's not practical to access the cache across blogs.
+	$root_blog_options_meta = wp_cache_get( 'root_blog_options', 'bp' );
+
+	if ( false === $root_blog_options_meta ) {
+		$blog_options_keys      = "'" . join( "', '", (array) $root_blog_option_keys ) . "'";
+		$blog_options_table	    = bp_is_multiblog_mode() ? $wpdb->options : $wpdb->get_blog_prefix( bp_get_root_blog_id() ) . 'options';
+		$blog_options_query     = "SELECT option_name AS name, option_value AS value FROM {$blog_options_table} WHERE option_name IN ( {$blog_options_keys} )";
+		$root_blog_options_meta = $wpdb->get_results( $blog_options_query );
+
+		// On Multisite installations, some options must always be fetched from sitemeta.
+		if ( is_multisite() ) {
+
+			/**
+			 * Filters multisite options retrieved from sitemeta.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param array $value Array of multisite options from sitemeta table.
+			 */
+			$network_options = apply_filters( 'bp_core_network_options', array(
+				'tags_blog_id'       => '0',
+				'sitewide_tags_blog' => '',
+				'registration'       => '0',
+				'fileupload_maxk'    => '1500'
+			) );
+
+			$current_site           = get_current_site();
+			$network_option_keys    = array_keys( $network_options );
+			$sitemeta_options_keys  = "'" . join( "', '", (array) $network_option_keys ) . "'";
+			$sitemeta_options_query = $wpdb->prepare( "SELECT meta_key AS name, meta_value AS value FROM {$wpdb->sitemeta} WHERE meta_key IN ( {$sitemeta_options_keys} ) AND site_id = %d", $current_site->id );
+			$network_options_meta   = $wpdb->get_results( $sitemeta_options_query );
+
+			// Sitemeta comes second in the merge, so that network 'registration' value wins.
+			$root_blog_options_meta = array_merge( $root_blog_options_meta, $network_options_meta );
+		}
+
+		// Loop through our results and make them usable.
+		foreach ( $root_blog_options_meta as $root_blog_option ) {
+			$root_blog_options[$root_blog_option->name] = $root_blog_option->value;
+		}
+
+		// Copy the options no the return val.
+		$root_blog_options_meta = $root_blog_options;
+
+		// Clean up our temporary copy.
+		unset( $root_blog_options );
+
+		wp_cache_set( 'root_blog_options', $root_blog_options_meta, 'bp' );
+	}
+
+	/**
+	 * Filters the global BP options.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $root_blog_options_meta Array of global BP options.
+	 */
+	return apply_filters( 'bp_core_get_root_options', $root_blog_options_meta );
+}
+
+/**
+ * Get a root option.
+ *
+ * "Root options" are those that apply across an entire installation, and are fetched only a single
+ * time during a pageload and stored in `buddypress()->site_options` to prevent future lookups.
+ * See {@see bp_core_get_root_options()}.
+ *
+ * @since 2.3.0
+ *
+ * @param string $option Name of the option key.
+ * @return mixed Value, if found.
+ */
+function bp_core_get_root_option( $option ) {
+	$bp = buddypress();
+
+	if ( ! isset( $bp->site_options ) ) {
+		$bp->site_options = bp_core_get_root_options();
+	}
+
+	$value = '';
+	if ( isset( $bp->site_options[ $option ] ) ) {
+		$value = $bp->site_options[ $option ];
+	}
+
+	return $value;
+}
+
+/** Active? *******************************************************************/
+
+/**
+ * Is profile syncing disabled?
+ *
+ * @since 1.6.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if profile sync is enabled, otherwise false.
+ */
+function bp_disable_profile_sync( $default = false ) {
+
+	/**
+	 * Filters whether or not profile syncing is disabled.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not syncing is disabled.
+	 */
+	return (bool) apply_filters( 'bp_disable_profile_sync', (bool) bp_get_option( 'bp-disable-profile-sync', $default ) );
+}
+
+/**
+ * Is the Toolbar hidden for logged out users?
+ *
+ * @since 1.6.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if the admin bar should be hidden for logged-out users,
+ *              otherwise false.
+ */
+function bp_hide_loggedout_adminbar( $default = true ) {
+
+	/**
+	 * Filters whether or not the toolbar is hidden for logged out users.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not the toolbar is hidden.
+	 */
+	return (bool) apply_filters( 'bp_hide_loggedout_adminbar', (bool) bp_get_option( 'hide-loggedout-adminbar', $default ) );
+}
+
+/**
+ * Are members able to upload their own avatars?
+ *
+ * @since 1.6.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if avatar uploads are disabled, otherwise false.
+ */
+function bp_disable_avatar_uploads( $default = true ) {
+
+	/**
+	 * Filters whether or not members are able to upload their own avatars.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not members are able to upload their own avatars.
+	 */
+	return (bool) apply_filters( 'bp_disable_avatar_uploads', (bool) bp_get_option( 'bp-disable-avatar-uploads', $default ) );
+}
+
+/**
+ * Are members able to upload their own cover images?
+ *
+ * @since 2.4.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: false.
+ * @return bool True if cover image uploads are disabled, otherwise false.
+ */
+function bp_disable_cover_image_uploads( $default = false ) {
+
+	/**
+	 * Filters whether or not members are able to upload their own cover images.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param bool $value Whether or not members are able to upload their own cover images.
+	 */
+	return (bool) apply_filters( 'bp_disable_cover_image_uploads', (bool) bp_get_option( 'bp-disable-cover-image-uploads', $default ) );
+}
+
+/**
+ * Are group avatars disabled?
+ *
+ * For backward compatibility, this option falls back on the value of 'bp-disable-avatar-uploads' when no value is
+ * found in the database.
+ *
+ * @since 2.3.0
+ *
+ * @param bool|null $default Optional. Fallback value if not found in the database.
+ *                           Defaults to the value of `bp_disable_avatar_uploads()`.
+ * @return bool True if group avatar uploads are disabled, otherwise false.
+ */
+function bp_disable_group_avatar_uploads( $default = null ) {
+	$disabled = bp_get_option( 'bp-disable-group-avatar-uploads', '' );
+
+	if ( '' === $disabled ) {
+		if ( is_null( $default ) ) {
+			$disabled = bp_disable_avatar_uploads();
+		} else {
+			$disabled = $default;
+		}
+	}
+
+	/**
+	 * Filters whether or not members are able to upload group avatars.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param bool $disabled Whether or not members are able to upload their groups avatars.
+	 * @param bool $default  Default value passed to the function.
+	 */
+	return (bool) apply_filters( 'bp_disable_group_avatar_uploads', $disabled, $default );
+}
+
+/**
+ * Are group cover images disabled?
+ *
+ * @since 2.4.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: false.
+ * @return bool True if group cover image uploads are disabled, otherwise false.
+ */
+function bp_disable_group_cover_image_uploads( $default = false ) {
+
+	/**
+	 * Filters whether or not members are able to upload group cover images.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param bool $value Whether or not members are able to upload thier groups cover images.
+	 */
+	return (bool) apply_filters( 'bp_disable_group_cover_image_uploads', (bool) bp_get_option( 'bp-disable-group-cover-image-uploads', $default ) );
+}
+
+/**
+ * Are members able to delete their own accounts?
+ *
+ * @since 1.6.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if users are able to delete their own accounts, otherwise
+ *              false.
+ */
+function bp_disable_account_deletion( $default = false ) {
+
+	/**
+	 * Filters whether or not members are able to delete their own accounts.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not members are able to delete their own accounts.
+	 */
+	return apply_filters( 'bp_disable_account_deletion', (bool) bp_get_option( 'bp-disable-account-deletion', $default ) );
+}
+
+/**
+ * Are post/comment activity stream comments disabled?
+ *
+ * @since 1.6.0
+ *
+ * @todo split and move into blog and forum components.
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: false.
+ * @return bool True if activity comments are disabled for blog and forum
+ *              items, otherwise false.
+ */
+function bp_disable_blogforum_comments( $default = false ) {
+
+	/**
+	 * Filters whether or not blog and forum activity stream comments are disabled.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not blog and forum activity stream comments are disabled.
+	 */
+	return (bool) apply_filters( 'bp_disable_blogforum_comments', (bool) bp_get_option( 'bp-disable-blogforum-comments', $default ) );
+}
+
+/**
+ * Is group creation turned off?
+ *
+ * @since 1.6.0
+ *
+ * @todo Move into groups component.
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if group creation is restricted, otherwise false.
+ */
+function bp_restrict_group_creation( $default = true ) {
+
+	/**
+	 * Filters whether or not group creation is turned off.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not group creation is turned off.
+	 */
+	return (bool) apply_filters( 'bp_restrict_group_creation', (bool) bp_get_option( 'bp_restrict_group_creation', $default ) );
+}
+
+/**
+ * Should the old BuddyBar be forced in place of the WP admin bar?
+ *
+ * @since 1.6.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if the BuddyBar should be forced on, otherwise false.
+ */
+function bp_force_buddybar( $default = true ) {
+
+	/**
+	 * Filters whether or not BuddyBar should be forced in place of WP Admin Bar.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not BuddyBar should be forced in place of WP Admin Bar.
+	 */
+	return (bool) apply_filters( 'bp_force_buddybar', (bool) bp_get_option( '_bp_force_buddybar', $default ) );
+}
+
+/**
+ * Check whether Akismet is enabled.
+ *
+ * @since 1.6.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if Akismet is enabled, otherwise false.
+ */
+function bp_is_akismet_active( $default = true ) {
+
+	/**
+	 * Filters whether or not Akismet is enabled.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $value Whether or not Akismet is enabled.
+	 */
+	return (bool) apply_filters( 'bp_is_akismet_active', (bool) bp_get_option( '_bp_enable_akismet', $default ) );
+}
+
+/**
+ * Check whether Activity Heartbeat refresh is enabled.
+ *
+ * @since 2.0.0
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: true.
+ * @return bool True if Heartbeat refresh is enabled, otherwise false.
+ */
+function bp_is_activity_heartbeat_active( $default = true ) {
+
+	/**
+	 * Filters whether or not Activity Heartbeat refresh is enabled.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param bool $value Whether or not Activity Heartbeat refresh is enabled.
+	 */
+	return (bool) apply_filters( 'bp_is_activity_heartbeat_active', (bool) bp_get_option( '_bp_enable_heartbeat_refresh', $default ) );
+}
+
+/**
+ * Get the current theme package ID.
+ *
+ * @since 1.7.0
+ *
+ * @param string $default Optional. Fallback value if not found in the database.
+ *                        Default: 'legacy'.
+ * @return string ID of the theme package.
+ */
+function bp_get_theme_package_id( $default = 'legacy' ) {
+
+	/**
+	 * Filters the current theme package ID.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $value The current theme package ID.
+	 */
+	return apply_filters( 'bp_get_theme_package_id', bp_get_option( '_bp_theme_package_id', $default ) );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-taxonomy.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-taxonomy.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-taxonomy.php	(revision 12265)
@@ -0,0 +1,279 @@
+<?php
+/**
+ * BuddyPress taxonomy functions.
+ *
+ * Most BuddyPress taxonomy functions are wrappers for their WordPress counterparts.
+ * Because BuddyPress can be activated in various ways in a network environment, we
+ * must switch to the root blog before using the WP functions.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register our default taxonomies.
+ *
+ * @since 2.2.0
+ */
+function bp_register_default_taxonomies() {
+	// Member Type.
+	register_taxonomy( bp_get_member_type_tax_name(), 'user', array(
+		'public' => false,
+	) );
+
+	// Email type.
+	register_taxonomy(
+		bp_get_email_tax_type(),
+		bp_get_email_post_type(),
+		apply_filters( 'bp_register_email_tax_type', array(
+			'description'   => _x( 'BuddyPress email types', 'email type taxonomy description', 'buddypress' ),
+			'labels'        => bp_get_email_tax_type_labels(),
+			'meta_box_cb'   => 'bp_email_tax_type_metabox',
+			'public'        => false,
+			'query_var'     => false,
+			'rewrite'       => false,
+			'show_in_menu'  => false,
+			'show_tagcloud' => false,
+			'show_ui'       => bp_is_root_blog() && bp_current_user_can( 'bp_moderate' ),
+		) )
+	);
+}
+add_action( 'bp_register_taxonomies', 'bp_register_default_taxonomies' );
+
+/**
+ * Gets the ID of the site that BP should use for taxonomy term storage.
+ *
+ * Defaults to the root blog ID.
+ *
+ * @since 2.6.0
+ *
+ * @param string $taxonomy Taxonomy slug to check for.
+ * @return int
+ */
+function bp_get_taxonomy_term_site_id( $taxonomy = '' ) {
+	$site_id = bp_get_root_blog_id();
+
+	/**
+	 * Filters the ID of the site where BP should store taxonomy terms.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int    $site_id  Site ID to cehck for.
+	 * @param string $taxonomy Taxonomy slug to check for.
+	 */
+	return (int) apply_filters( 'bp_get_taxonomy_term_site_id', $site_id, $taxonomy );
+}
+
+/**
+ * Set taxonomy terms on a BuddyPress object.
+ *
+ * @since 2.2.0
+ *
+ * @see wp_set_object_terms() for a full description of function and parameters.
+ *
+ * @param int          $object_id Object ID.
+ * @param string|array $terms     Term or terms to set.
+ * @param string       $taxonomy  Taxonomy name.
+ * @param bool         $append    Optional. True to append terms to existing terms. Default: false.
+ * @return array Array of term taxonomy IDs.
+ */
+function bp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) {
+	$site_id = bp_get_taxonomy_term_site_id( $taxonomy );
+
+	$switched = false;
+	if ( $site_id !== get_current_blog_id() ) {
+		switch_to_blog( $site_id );
+		bp_register_taxonomies();
+		$switched = true;
+	}
+
+	$tt_ids = wp_set_object_terms( $object_id, $terms, $taxonomy, $append );
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	/**
+	 * Fires when taxonomy terms have been set on BuddyPress objects.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int    $object_id Object ID.
+	 * @param array  $terms     Term or terms to remove.
+	 * @param array  $tt_ids    Array of term taxonomy IDs.
+	 * @param string $taxonomy  Taxonomy name.
+	 */
+	do_action( 'bp_set_object_terms', $object_id, $terms, $tt_ids, $taxonomy );
+
+	return $tt_ids;
+}
+
+/**
+ * Get taxonomy terms for a BuddyPress object.
+ *
+ * @since 2.2.0
+ *
+ * @see wp_get_object_terms() for a full description of function and parameters.
+ *
+ * @param int|array    $object_ids ID or IDs of objects.
+ * @param string|array $taxonomies Name or names of taxonomies to match.
+ * @param array        $args       See {@see wp_get_object_terms()}.
+ * @return array
+ */
+function bp_get_object_terms( $object_ids, $taxonomies, $args = array() ) {
+	// Different taxonomies must be stored on different sites.
+	$taxonomy_site_map = array();
+	foreach ( (array) $taxonomies as $taxonomy ) {
+		$taxonomy_site_id = bp_get_taxonomy_term_site_id( $taxonomy );
+		$taxonomy_site_map[ $taxonomy_site_id ][] = $taxonomy;
+	}
+
+	$retval = array();
+	foreach ( $taxonomy_site_map as $taxonomy_site_id => $site_taxonomies ) {
+		$switched = false;
+		if ( $taxonomy_site_id !== get_current_blog_id() ) {
+			switch_to_blog( $taxonomy_site_id );
+			bp_register_taxonomies();
+			$switched = true;
+		}
+
+		$site_terms = wp_get_object_terms( $object_ids, $site_taxonomies, $args );
+		$retval     = array_merge( $retval, $site_terms );
+
+		if ( $switched ) {
+			restore_current_blog();
+		}
+	}
+
+	return $retval;
+}
+
+/**
+ * Remove taxonomy terms on a BuddyPress object.
+ *
+ * @since 2.3.0
+ *
+ * @see wp_remove_object_terms() for a full description of function and parameters.
+ *
+ * @param int          $object_id Object ID.
+ * @param string|array $terms     Term or terms to remove.
+ * @param string       $taxonomy  Taxonomy name.
+ * @return bool|WP_Error True on success, false or WP_Error on failure.
+ */
+function bp_remove_object_terms( $object_id, $terms, $taxonomy ) {
+	$site_id = bp_get_taxonomy_term_site_id( $taxonomy );
+
+	$switched = false;
+	if ( $site_id !== get_current_blog_id() ) {
+		switch_to_blog( $site_id );
+		bp_register_taxonomies();
+		$switched = true;
+	}
+
+	$retval = wp_remove_object_terms( $object_id, $terms, $taxonomy );
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	/**
+	 * Fires when taxonomy terms have been removed from BuddyPress objects.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int    $object_id Object ID.
+	 * @param array  $terms     Term or terms to remove.
+	 * @param string $taxonomy  Taxonomy name.
+	 */
+	do_action( 'bp_remove_object_terms', $object_id, $terms, $taxonomy );
+
+	return $retval;
+}
+
+/**
+ * Retrieve IDs of objects in valid taxonomies and terms for BuddyPress-related taxonomies.
+ *
+ * Note that object IDs are from the `bp_get_taxonomy_term_site_id()`, which on some
+ * multisite configurations may not be the same as the current site.
+ *
+ * @since 2.7.0
+ *
+ * @see get_objects_in_term() for a full description of function and parameters.
+ *
+ * @param int|array    $term_ids   Term id or array of term ids of terms that will be used.
+ * @param string|array $taxonomies String of taxonomy name or Array of string values of taxonomy names.
+ * @param array|string $args       Change the order of the object_ids, either ASC or DESC.
+ *
+ * @return WP_Error|array If the taxonomy does not exist, then WP_Error will be returned. On success,
+ *                        the array can be empty, meaning that there are no $object_ids found. When
+ *                        object IDs are found, an array of those IDs will be returned.
+ */
+function bp_get_objects_in_term( $term_ids, $taxonomies, $args = array() ) {
+	// Different taxonomies may be stored on different sites.
+	$taxonomy_site_map = array();
+	foreach ( (array) $taxonomies as $taxonomy ) {
+		$taxonomy_site_id = bp_get_taxonomy_term_site_id( $taxonomy );
+		$taxonomy_site_map[ $taxonomy_site_id ][] = $taxonomy;
+	}
+
+	$retval = array();
+	foreach ( $taxonomy_site_map as $taxonomy_site_id => $site_taxonomies ) {
+		$switched = false;
+		if ( $taxonomy_site_id !== get_current_blog_id() ) {
+			switch_to_blog( $taxonomy_site_id );
+			bp_register_taxonomies();
+			$switched = true;
+		}
+
+		$site_objects = get_objects_in_term( $term_ids, $site_taxonomies, $args );
+		$retval       = array_merge( $retval, $site_objects );
+
+		if ( $switched ) {
+			restore_current_blog();
+		}
+	}
+
+	return $retval;
+}
+
+/**
+ * Get term data for terms in BuddyPress taxonomies.
+ *
+ * Note that term data is from the `bp_get_taxonomy_term_site_id()`, which on some
+ * multisite configurations may not be the same as the current site.
+ *
+ * @since 2.7.0
+ *
+ * @see get_term_by() for a full description of function and parameters.
+ *
+ * @param string     $field    Either 'slug', 'name', 'id' (term_id), or 'term_taxonomy_id'
+ * @param string|int $value    Search for this term value
+ * @param string     $taxonomy Taxonomy name. Optional, if `$field` is 'term_taxonomy_id'.
+ * @param string     $output   Constant OBJECT, ARRAY_A, or ARRAY_N
+ * @param string     $filter   Optional, default is raw or no WordPress defined filter will applied.
+ *
+ * @return WP_Term|bool WP_Term instance on success. Will return false if `$taxonomy` does not exist
+ *                      or `$term` was not found.
+ */
+function bp_get_term_by( $field, $value, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) {
+	$site_id = bp_get_taxonomy_term_site_id( $taxonomy );
+
+	$switched = false;
+	if ( $site_id !== get_current_blog_id() ) {
+		switch_to_blog( $site_id );
+		bp_register_taxonomies();
+		$switched = true;
+	}
+
+	$term = get_term_by( $field, $value, $taxonomy, $output, $filter );
+
+	if ( $switched ) {
+		restore_current_blog();
+	}
+
+	return $term;
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-template-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-template-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-template-loader.php	(revision 12265)
@@ -0,0 +1,621 @@
+<?php
+/**
+ * BuddyPress Template Functions.
+ *
+ * This file contains functions necessary to mirror the WordPress core template
+ * loading process. Many of those functions are not filterable, and even then
+ * would not be robust enough to predict where BuddyPress templates might exist.
+ *
+ * @package BuddyPress
+ * @subpackage TemplateFunctions
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Get a BuddyPress template part for display in a theme.
+ *
+ * @since 1.7.0
+ *
+ * @param string      $slug Template part slug. Used to generate filenames,
+ *                          eg 'friends' for 'friends.php'.
+ * @param string|null $name Optional. Template part name. Used to generate
+ *                          secondary filenames, eg 'personal' for 'activity-personal.php'.
+ * @return false|string Path to located template. See {@link bp_locate_template()}.
+ */
+function bp_get_template_part( $slug, $name = null ) {
+
+	/**
+	 * Fires at the start of bp_get_template_part().
+	 *
+	 * This is a variable hook that is dependent on the slug passed in.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $slug Template part slug requested.
+	 * @param string $name Template part name requested.
+	 */
+	do_action( 'get_template_part_' . $slug, $slug, $name );
+
+	// Setup possible parts.
+	$templates = array();
+	if ( isset( $name ) ) {
+		$templates[] = $slug . '-' . $name . '.php';
+	}
+	$templates[] = $slug . '.php';
+
+	/**
+	 * Filters the template parts to be loaded.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array  $templates Array of templates located.
+	 * @param string $slug      Template part slug requested.
+	 * @param string $name      Template part name requested.
+	 */
+	$templates = apply_filters( 'bp_get_template_part', $templates, $slug, $name );
+
+	// Return the part that is found.
+	return bp_locate_template( $templates, true, false );
+}
+
+/**
+ * Get an asset template part.
+ *
+ * Basically the same as {@link bp_get_template_part()}, but with 'assets/'
+ * prepended to the slug.
+ *
+ * @since 2.6.0
+ *
+ * @see bp_get_template_part() for full documentation.
+ *
+ * @param string      $slug Template slug.
+ * @param string|null $name Template name.
+ * @return false|string
+ */
+function bp_get_asset_template_part( $slug, $name = null ) {
+	return bp_get_template_part( "assets/{$slug}", $name );
+}
+
+/**
+ * Retrieve the name of the highest priority template file that exists.
+ *
+ * Searches in the STYLESHEETPATH before TEMPLATEPATH so that themes which
+ * inherit from a parent theme can just overload one file. If the template is
+ * not found in either of those, it looks in the theme-compat folder last.
+ *
+ * @since 1.7.0
+ *
+ * @param string|array $template_names Template file(s) to search for, in order.
+ * @param bool         $load           Optional. If true, the template file will be loaded when
+ *                                     found. If false, the path will be returned. Default: false.
+ * @param bool         $require_once   Optional. Whether to require_once or require. Has
+ *                                     no effect if $load is false. Default: true.
+ * @return string The template filename if one is located.
+ */
+function bp_locate_template( $template_names, $load = false, $require_once = true ) {
+
+	// Bail when there are no templates to locate
+	if ( empty( $template_names ) ) {
+		return false;
+	}
+
+	// No file found yet.
+	$located            = false;
+	$template_locations = bp_get_template_stack();
+
+	// Try to find a template file.
+	foreach ( (array) $template_names as $template_name ) {
+
+		// Continue if template is empty.
+		if ( empty( $template_name ) ) {
+			continue;
+		}
+
+		// Trim off any slashes from the template name.
+		$template_name  = ltrim( $template_name, '/' );
+
+		// Loop through template stack.
+		foreach ( (array) $template_locations as $template_location ) {
+
+			// Continue if $template_location is empty.
+			if ( empty( $template_location ) ) {
+				continue;
+			}
+
+			// Check child theme first.
+			if ( file_exists( trailingslashit( $template_location ) . $template_name ) ) {
+				$located = trailingslashit( $template_location ) . $template_name;
+				break 2;
+			}
+		}
+	}
+
+	/**
+	 * This action exists only to follow the standard BuddyPress coding convention,
+	 * and should not be used to short-circuit any part of the template locator.
+	 *
+	 * If you want to override a specific template part, please either filter
+	 * 'bp_get_template_part' or add a new location to the template stack.
+	 */
+	do_action( 'bp_locate_template', $located, $template_name, $template_names, $template_locations, $load, $require_once );
+
+	/**
+	 * Filter here to allow/disallow template loading.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param bool $value True to load the template, false otherwise.
+	 */
+	$load_template = (bool) apply_filters( 'bp_locate_template_and_load', true );
+
+	if ( $load_template && $load && ! empty( $located ) ) {
+		load_template( $located, $require_once );
+	}
+
+	return $located;
+}
+
+/**
+ * Get file data of the highest priority asset that exists.
+ *
+ * Similar to {@link bp_locate_template()}, but for files like CSS and JS.
+ *
+ * @since 2.6.0
+ *
+ * @param string $filename Relative filename to search for.
+ * @return false|array Array of asset data if one is located (includes absolute filepath and URI).
+ *                     Boolean false on failure.
+ */
+function bp_locate_template_asset( $filename ) {
+	// Ensure assets can be located when running from /src/.
+	if ( defined( 'BP_SOURCE_SUBDIRECTORY' ) && 'src' === BP_SOURCE_SUBDIRECTORY ) {
+		$filename = str_replace( '.min', '', $filename );
+	}
+
+	// Use bp_locate_template() to find our asset.
+	$located = bp_locate_template( $filename, false );
+	if ( false === $located ) {
+		return false;
+	}
+
+	// Set up data array.
+	$data = array();
+	$data['file'] = $data['uri'] = $located;
+
+	$find = array(
+		get_theme_root(),
+		bp_get_theme_compat_dir()
+	);
+
+	$replace = array(
+		get_theme_root_uri(),
+		bp_get_theme_compat_url()
+	);
+
+	// Make sure URI path is relative to site URL.
+	$data['uri'] = str_replace( $find, $replace, $data['uri'] );
+
+	return $data;
+}
+
+/**
+ * Register a new template stack location.
+ *
+ * This allows for templates to live in places beyond just the parent/child
+ * relationship, to allow for custom template locations. Used in conjunction
+ * with bp_locate_template(), this allows for easy template overrides.
+ *
+ * @since 1.7.0
+ *
+ * @param string $location_callback Callback function that returns the stack location.
+ * @param int    $priority          Optional. The priority parameter as passed to
+ *                                  add_filter(). Default: 10.
+ * @return bool See {@link add_filter()}.
+ */
+function bp_register_template_stack( $location_callback = '', $priority = 10 ) {
+
+	// Bail if no location, or function/method is not callable.
+	if ( empty( $location_callback ) || ! is_callable( $location_callback ) ) {
+		return false;
+	}
+
+	// Add location callback to template stack.
+	return add_filter( 'bp_template_stack', $location_callback, (int) $priority );
+}
+
+/**
+ * Deregister a previously registered template stack location.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_register_template_stack()
+ *
+ * @param string $location_callback Callback function that returns the stack location.
+ * @param int    $priority          Optional. The priority parameter passed to
+ *                                  {@link bp_register_template_stack()}. Default: 10.
+ * @return bool See {@link remove_filter()}.
+ */
+function bp_deregister_template_stack( $location_callback = '', $priority = 10 ) {
+
+	// Bail if no location, or function/method is not callable.
+	if ( empty( $location_callback ) || ! is_callable( $location_callback ) ) {
+		return false;
+	}
+
+	// Add location callback to template stack.
+	return remove_filter( 'bp_template_stack', $location_callback, (int) $priority );
+}
+
+/**
+ * Get the "template stack", a list of registered directories where templates can be found.
+ *
+ * Calls the functions added to the 'bp_template_stack' filter hook, and return
+ * an array of the template locations.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_register_template_stack()
+ *
+ * @global array $wp_filter         Stores all of the filters.
+ * @global array $merged_filters    Merges the filter hooks using this function.
+ * @global array $wp_current_filter Stores the list of current filters with
+ *                                  the current one last.
+ * @return array The filtered value after all hooked functions are applied to it.
+ */
+function bp_get_template_stack() {
+	global $wp_filter, $merged_filters, $wp_current_filter;
+
+	// Setup some default variables.
+	$tag  = 'bp_template_stack';
+	$args = $stack = array();
+
+	// Add 'bp_template_stack' to the current filter array.
+	$wp_current_filter[] = $tag;
+
+	// Sort.
+	if ( class_exists( 'WP_Hook' ) ) {
+		$filter = $wp_filter[ $tag ]->callbacks;
+	} else {
+		$filter = &$wp_filter[ $tag ];
+
+		if ( ! isset( $merged_filters[ $tag ] ) ) {
+			ksort( $filter );
+			$merged_filters[ $tag ] = true;
+		}
+	}
+
+	// Ensure we're always at the beginning of the filter array.
+	reset( $filter );
+
+	// Loop through 'bp_template_stack' filters, and call callback functions.
+	do {
+		foreach( (array) current( $filter ) as $the_ ) {
+			if ( ! is_null( $the_['function'] ) ) {
+				$args[1] = $stack;
+				$stack[] = call_user_func_array( $the_['function'], array_slice( $args, 1, (int) $the_['accepted_args'] ) );
+			}
+		}
+	} while ( next( $filter ) !== false );
+
+	// Remove 'bp_template_stack' from the current filter array.
+	array_pop( $wp_current_filter );
+
+	// Remove empties and duplicates.
+	$stack = array_unique( array_filter( $stack ) );
+
+	/**
+	 * Filters the "template stack" list of registered directories where templates can be found.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $stack Array of registered directories for template locations.
+	 */
+	return (array) apply_filters( 'bp_get_template_stack', $stack ) ;
+}
+
+/**
+ * Put a template part into an output buffer, and return it.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_get_template_part() for a description of $slug and $name params.
+ *
+ * @param string      $slug See {@link bp_get_template_part()}.
+ * @param string|null $name See {@link bp_get_template_part()}.
+ * @param bool        $echo If true, template content will be echoed. If false,
+ *                          returned. Default: true.
+ * @return string|null If $echo, returns the template content.
+ */
+function bp_buffer_template_part( $slug, $name = null, $echo = true ) {
+	ob_start();
+
+	// Remove 'bp_replace_the_content' filter to prevent infinite loops.
+	remove_filter( 'the_content', 'bp_replace_the_content' );
+
+	bp_get_template_part( $slug, $name );
+
+	// Remove 'bp_replace_the_content' filter to prevent infinite loops.
+	add_filter( 'the_content', 'bp_replace_the_content' );
+
+	// Get the output buffer contents.
+	$output = ob_get_clean();
+
+	// Echo or return the output buffer contents.
+	if ( true === $echo ) {
+		echo $output;
+	} else {
+		return $output;
+	}
+}
+
+/**
+ * Retrieve the path to a template.
+ *
+ * Used to quickly retrieve the path of a template without including the file
+ * extension. It will also check the parent theme and theme-compat theme with
+ * the use of {@link bp_locate_template()}. Allows for more generic template
+ * locations without the use of the other get_*_template() functions.
+ *
+ * @since 1.7.0
+ *
+ * @param string $type      Filename without extension.
+ * @param array  $templates An optional list of template candidates.
+ * @return string Full path to file.
+ */
+function bp_get_query_template( $type, $templates = array() ) {
+	$type = preg_replace( '|[^a-z0-9-]+|', '', $type );
+
+	if ( empty( $templates ) ) {
+		$templates = array( "{$type}.php" );
+	}
+
+	/**
+	 * Filters possible file paths to check for for a template.
+	 *
+	 * This is a variable filter based on the type passed into
+	 * bp_get_query_template.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $templates Array of template files already prepared.
+	 */
+	$templates = apply_filters( "bp_get_{$type}_template", $templates );
+
+	// Filter possible templates, try to match one, and set any BuddyPress theme
+	// compat properties so they can be cross-checked later.
+	$templates = bp_set_theme_compat_templates( $templates );
+	$template  = bp_locate_template( $templates );
+	$template  = bp_set_theme_compat_template( $template );
+
+	/**
+	 * Filters the path to a template file.
+	 *
+	 * This is a variable filter based on the type passed into
+	 * bp_get_query_template.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $template Path to the most appropriate found template file.
+	 */
+	return apply_filters( "bp_{$type}_template", $template );
+}
+
+/**
+ * Get the possible subdirectories to check for templates in.
+ *
+ * @since 1.7.0
+ *
+ * @param array $templates Templates we are looking for.
+ * @return array Possible subfolders to look in.
+ */
+function bp_get_template_locations( $templates = array() ) {
+	$locations = array(
+		'buddypress',
+		'community',
+		''
+	);
+
+	/**
+	 * Filters the possible subdirectories to check for templates in.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $locations Array of subfolders to look in.
+	 * @param array $templates Array of templates we are looking for.
+	 */
+	return apply_filters( 'bp_get_template_locations', $locations, $templates );
+}
+
+/**
+ * Add template locations to template files being searched for.
+ *
+ * @since 1.7.0
+ *
+ * @param array $stacks Array of template locations.
+ * @return array Array of all template locations registered so far.
+ */
+function bp_add_template_stack_locations( $stacks = array() ) {
+	$retval = array();
+
+	// Get alternate locations.
+	$locations = bp_get_template_locations();
+
+	// Loop through locations and stacks and combine.
+	foreach ( (array) $stacks as $stack ) {
+		foreach ( (array) $locations as $custom_location ) {
+			$retval[] = untrailingslashit( trailingslashit( $stack ) . $custom_location );
+		}
+	}
+
+	/**
+	 * Filters the template locations to template files being searched for.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $value  Array of all template locations registered so far.
+	 * @param array $stacks Array of template locations.
+	 */
+	return apply_filters( 'bp_add_template_stack_locations', array_unique( $retval ), $stacks );
+}
+
+/**
+ * Add checks for BuddyPress conditions to 'parse_query' action.
+ *
+ * @since 1.7.0
+ *
+ * @param WP_Query $posts_query WP_Query object.
+ */
+function bp_parse_query( $posts_query ) {
+
+	// Bail if $posts_query is not the main loop.
+	if ( ! $posts_query->is_main_query() ) {
+		return;
+	}
+
+	// Bail if filters are suppressed on this query.
+	if ( true == $posts_query->get( 'suppress_filters' ) ) {
+		return;
+	}
+
+	// Bail if in admin.
+	if ( is_admin() ) {
+		return;
+	}
+
+	/**
+	 * Fires at the end of the bp_parse_query function.
+	 *
+	 * Allow BuddyPress components to parse the main query.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param WP_Query $posts_query WP_Query instance. Passed by reference.
+	 */
+	do_action_ref_array( 'bp_parse_query', array( &$posts_query ) );
+}
+
+/**
+ * Possibly intercept the template being loaded.
+ *
+ * Listens to the 'template_include' filter and waits for any BuddyPress specific
+ * template condition to be met. If one is met and the template file exists,
+ * it will be used; otherwise.
+ *
+ * Note that the _edit() checks are ahead of their counterparts, to prevent them
+ * from being stomped on accident.
+ *
+ * @since 1.7.0
+ *
+ * @param string $template The path to the template file that is being used.
+ * @return string The path to the template file that is being used.
+ */
+function bp_template_include_theme_supports( $template = '' ) {
+
+	/**
+	 * Filters whether or not to override the template being loaded in parent/child themes.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool   $value    Whether or not there is a file override. Default false.
+	 * @param string $template The path to the template file that is being used.
+	 */
+	$new_template = apply_filters( 'bp_get_root_template', false, $template );
+
+	// A BuddyPress template file was located, so override the WordPress
+	// template and use it to switch off BuddyPress's theme compatibility.
+	if ( ! empty( $new_template ) ) {
+		$template = bp_set_template_included( $new_template );
+	}
+
+	/**
+	 * Filters the final template being loaded in parent/child themes.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $template The path to the template file that is being used.
+	 */
+	return apply_filters( 'bp_template_include_theme_supports', $template );
+}
+
+/**
+ * Set the included template.
+ *
+ * @since 1.8.0
+ *
+ * @param mixed $template Default: false.
+ * @return mixed False if empty. Template name if template included.
+ */
+function bp_set_template_included( $template = false ) {
+	buddypress()->theme_compat->found_template = $template;
+
+	return buddypress()->theme_compat->found_template;
+}
+
+/**
+ * Is a BuddyPress template being included?
+ *
+ * @since 1.8.0
+ *
+ * @return bool True if yes, false if no.
+ */
+function bp_is_template_included() {
+	return ! empty( buddypress()->theme_compat->found_template );
+}
+
+/**
+ * Attempt to load a custom BP functions file, similar to each themes functions.php file.
+ *
+ * @since 1.7.0
+ *
+ * @global string $pagenow
+ */
+function bp_load_theme_functions() {
+	global $pagenow, $wp_query;
+
+	// Do not load our custom BP functions file if theme compat is disabled.
+	if ( ! bp_use_theme_compat_with_current_theme() ) {
+		return;
+	}
+
+	// Do not include on BuddyPress deactivation.
+	if ( bp_is_deactivation() ) {
+		return;
+	}
+
+	// If the $wp_query global is empty (the main query has not been run,
+	// or has been reset), load_template() will fail at setting certain
+	// global values. This does not happen on a normal page load, but can
+	// cause problems when running automated tests.
+	if ( ! is_a( $wp_query, 'WP_Query' ) ) {
+		return;
+	}
+
+	// Only include if not installing or if activating via wp-activate.php.
+	if ( ! defined( 'WP_INSTALLING' ) || 'wp-activate.php' === $pagenow ) {
+		bp_locate_template( 'buddypress-functions.php', true );
+	}
+}
+
+/**
+ * Get the templates to use as the endpoint for BuddyPress template parts.
+ *
+ * @since 1.7.0
+ * @since 2.4.0 Added singular.php to stack
+ *
+ * @return string Possible root level wrapper template files.
+ */
+function bp_get_theme_compat_templates() {
+	return bp_get_query_template( 'buddypress', array(
+		'plugin-buddypress.php',
+		'buddypress.php',
+		'community.php',
+		'generic.php',
+		'page.php',
+		'single.php',
+		'singular.php',
+		'index.php'
+	) );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-template.php	(revision 12265)
@@ -0,0 +1,3744 @@
+<?php
+/**
+ * Core component template tag functions.
+ *
+ * @package BuddyPress
+ * @subpackage TemplateFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the "options nav", the secondary-level single item navigation menu.
+ *
+ * Uses the component's nav global to render out the sub navigation for the
+ * current component. Each component adds to its sub navigation array within
+ * its own setup_nav() function.
+ *
+ * This sub navigation array is the secondary level navigation, so for profile
+ * it contains:
+ *      [Public, Edit Profile, Change Avatar]
+ *
+ * The function will also analyze the current action for the current component
+ * to determine whether or not to highlight a particular sub nav item.
+ *
+ * @since 1.0.0
+ *
+ *       viewed user.
+ *
+ * @param string $parent_slug Options nav slug.
+ * @return string
+ */
+function bp_get_options_nav( $parent_slug = '' ) {
+	$bp = buddypress();
+
+	// If we are looking at a member profile, then the we can use the current
+	// component as an index. Otherwise we need to use the component's root_slug.
+	$component_index = !empty( $bp->displayed_user ) ? bp_current_component() : bp_get_root_slug( bp_current_component() );
+	$selected_item   = bp_current_action();
+
+	// Default to the Members nav.
+	if ( ! bp_is_single_item() ) {
+		// Set the parent slug, if not provided.
+		if ( empty( $parent_slug ) ) {
+			$parent_slug = $component_index;
+		}
+
+		$secondary_nav_items = $bp->members->nav->get_secondary( array( 'parent_slug' => $parent_slug ) );
+
+		if ( ! $secondary_nav_items ) {
+			return false;
+		}
+
+	// For a single item, try to use the component's nav.
+	} else {
+		$current_item = bp_current_item();
+		$single_item_component = bp_current_component();
+
+		// Adjust the selected nav item for the current single item if needed.
+		if ( ! empty( $parent_slug ) ) {
+			$current_item  = $parent_slug;
+			$selected_item = bp_action_variable( 0 );
+		}
+
+		// If the nav is not defined by the parent component, look in the Members nav.
+		if ( ! isset( $bp->{$single_item_component}->nav ) ) {
+			$secondary_nav_items = $bp->members->nav->get_secondary( array( 'parent_slug' => $current_item ) );
+		} else {
+			$secondary_nav_items = $bp->{$single_item_component}->nav->get_secondary( array( 'parent_slug' => $current_item ) );
+		}
+
+		if ( ! $secondary_nav_items ) {
+			return false;
+		}
+	}
+
+	// Loop through each navigation item.
+	foreach ( $secondary_nav_items as $subnav_item ) {
+		if ( empty( $subnav_item->user_has_access ) ) {
+			continue;
+		}
+
+		// If the current action or an action variable matches the nav item id, then add a highlight CSS class.
+		if ( $subnav_item->slug === $selected_item ) {
+			$selected = ' class="current selected"';
+		} else {
+			$selected = '';
+		}
+
+		// List type depends on our current component.
+		$list_type = bp_is_group() ? 'groups' : 'personal';
+
+		/**
+		 * Filters the "options nav", the secondary-level single item navigation menu.
+		 *
+		 * This is a dynamic filter that is dependent on the provided css_id value.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value         HTML list item for the submenu item.
+		 * @param array  $subnav_item   Submenu array item being displayed.
+		 * @param string $selected_item Current action.
+		 */
+		echo apply_filters( 'bp_get_options_nav_' . $subnav_item->css_id, '<li id="' . esc_attr( $subnav_item->css_id . '-' . $list_type . '-li' ) . '" ' . $selected . '><a id="' . esc_attr( $subnav_item->css_id ) . '" href="' . esc_url( $subnav_item->link ) . '">' . $subnav_item->name . '</a></li>', $subnav_item, $selected_item );
+	}
+}
+
+/**
+ * Get the 'bp_options_title' property from the BP global.
+ *
+ * Not currently used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_get_options_title() {
+	$bp = buddypress();
+
+	if ( empty( $bp->bp_options_title ) ) {
+		$bp->bp_options_title = __( 'Options', 'buddypress' );
+	}
+
+	echo apply_filters( 'bp_get_options_title', esc_attr( $bp->bp_options_title ) );
+}
+
+/**
+ * Get the directory title for a component.
+ *
+ * Used for the <title> element and the page header on the component directory
+ * page.
+ *
+ * @since 2.0.0
+ *
+ * @param string $component Component to get directory title for.
+ * @return string
+ */
+function bp_get_directory_title( $component = '' ) {
+	$title = '';
+
+	// Use the string provided by the component.
+	if ( ! empty( buddypress()->{$component}->directory_title ) ) {
+		$title = buddypress()->{$component}->directory_title;
+
+	// If none is found, concatenate.
+	} elseif ( isset( buddypress()->{$component}->name ) ) {
+		$title = sprintf( __( '%s Directory', 'buddypress' ), buddypress()->{$component}->name );
+	}
+
+	/**
+	 * Filters the directory title for a component.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $title     Text to be used in <title> tag.
+	 * @param string $component Current componet being displayed.
+	 */
+	return apply_filters( 'bp_get_directory_title', $title, $component );
+}
+
+/** Avatars *******************************************************************/
+
+/**
+ * Check to see if there is an options avatar.
+ *
+ * An options avatar is an avatar for something like a group, or a friend.
+ * Basically an avatar that appears in the sub nav options bar.
+ *
+ * Not currently used in BuddyPress.
+ *
+ * @return bool $value Returns true if an options avatar has been set, otherwise false.
+ */
+function bp_has_options_avatar() {
+	return (bool) buddypress()->bp_options_avatar;
+}
+
+/**
+ * Output the options avatar.
+ *
+ * Not currently used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_get_options_avatar() {
+	echo apply_filters( 'bp_get_options_avatar', buddypress()->bp_options_avatar );
+}
+
+/**
+ * Output a comment author's avatar.
+ *
+ * Not currently used in BuddyPress.
+ */
+function bp_comment_author_avatar() {
+	global $comment;
+
+	echo apply_filters( 'bp_comment_author_avatar', bp_core_fetch_avatar( array(
+		'item_id' => $comment->user_id,
+		'type'    => 'thumb',
+		'alt'     => sprintf( __( 'Profile photo of %s', 'buddypress' ), bp_core_get_user_displayname( $comment->user_id ) )
+	) ) );
+}
+
+/**
+ * Output a post author's avatar.
+ *
+ * Not currently used in BuddyPress.
+ */
+function bp_post_author_avatar() {
+	global $post;
+
+	echo apply_filters( 'bp_post_author_avatar', bp_core_fetch_avatar( array(
+		'item_id' => $post->post_author,
+		'type'    => 'thumb',
+		'alt'     => sprintf( __( 'Profile photo of %s', 'buddypress' ), bp_core_get_user_displayname( $post->post_author ) )
+	) ) );
+}
+
+/**
+ * Output the current avatar upload step.
+ *
+ * @since 1.1.0
+ */
+function bp_avatar_admin_step() {
+	echo bp_get_avatar_admin_step();
+}
+	/**
+	 * Return the current avatar upload step.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string The current avatar upload step. Returns 'upload-image'
+	 *         if none is found.
+	 */
+	function bp_get_avatar_admin_step() {
+		$bp   = buddypress();
+		$step = isset( $bp->avatar_admin->step )
+			? $step = $bp->avatar_admin->step
+			: 'upload-image';
+
+		/**
+		 * Filters the current avatar upload step.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $step The current avatar upload step.
+		 */
+		return apply_filters( 'bp_get_avatar_admin_step', $step );
+	}
+
+/**
+ * Output the URL of the avatar to crop.
+ *
+ * @since 1.1.0
+ */
+function bp_avatar_to_crop() {
+	echo bp_get_avatar_to_crop();
+}
+	/**
+	 * Return the URL of the avatar to crop.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string URL of the avatar awaiting cropping.
+	 */
+	function bp_get_avatar_to_crop() {
+		$bp  = buddypress();
+		$url = isset( $bp->avatar_admin->image->url )
+			? $bp->avatar_admin->image->url
+			: '';
+
+		/**
+		 * Filters the URL of the avatar to crop.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $url URL for the avatar.
+		 */
+		return apply_filters( 'bp_get_avatar_to_crop', $url );
+	}
+
+/**
+ * Output the relative file path to the avatar to crop.
+ *
+ * @since 1.1.0
+ */
+function bp_avatar_to_crop_src() {
+	echo bp_get_avatar_to_crop_src();
+}
+	/**
+	 * Return the relative file path to the avatar to crop.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string Relative file path to the avatar.
+	 */
+	function bp_get_avatar_to_crop_src() {
+		$bp  = buddypress();
+		$src = isset( $bp->avatar_admin->image->dir )
+			? str_replace( WP_CONTENT_DIR, '', $bp->avatar_admin->image->dir )
+			: '';
+
+		/**
+		 * Filters the relative file path to the avatar to crop.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $src Relative file path for the avatar.
+		 */
+		return apply_filters( 'bp_get_avatar_to_crop_src', $src );
+	}
+
+/**
+ * Output the avatar cropper <img> markup.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_avatar_cropper() {
+?>
+	<img id="avatar-to-crop" class="avatar" src="<?php echo esc_url( buddypress()->avatar_admin->image ); ?>" />
+<?php
+}
+
+/**
+ * Output the name of the BP site. Used in RSS headers.
+ *
+ * @since 1.0.0
+ */
+function bp_site_name() {
+	echo bp_get_site_name();
+}
+	/**
+	 * Returns the name of the BP site. Used in RSS headers.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return string
+	 */
+	function bp_get_site_name() {
+
+		/**
+		 * Filters the name of the BP site. Used in RSS headers.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Current BP site name.
+		 */
+		return apply_filters( 'bp_site_name', get_bloginfo( 'name', 'display' ) );
+	}
+
+/**
+ * Format a date based on a UNIX timestamp.
+ *
+ * This function can be used to turn a UNIX timestamp into a properly formatted
+ * (and possibly localized) string, userful for ouputting the date & time an
+ * action took place.
+ *
+ * Not to be confused with `bp_core_time_since()`, this function is best used
+ * for displaying a more exact date and time vs. a human-readable time.
+ *
+ * Note: This function may be improved or removed at a later date, as it is
+ * hardly used and adds an additional layer of complexity to calculating dates
+ * and times together with timezone offsets and i18n.
+ *
+ * @since 1.1.0
+ *
+ * @param int|string $time         The UNIX timestamp to be formatted.
+ * @param bool       $exclude_time Optional. True to return only the month + day, false
+ *                                 to return month, day, and time. Default: false.
+ * @param bool       $gmt          Optional. True to display in local time, false to
+ *                                  leave in GMT. Default: true.
+ * @return mixed A string representation of $time, in the format
+ *               "March 18, 2014 at 2:00 pm" (or whatever your
+ *               'date_format' and 'time_format' settings are
+ *               on your root blog). False on failure.
+ */
+function bp_format_time( $time = '', $exclude_time = false, $gmt = true ) {
+
+	// Bail if time is empty or not numeric
+	// @todo We should output something smarter here.
+	if ( empty( $time ) || ! is_numeric( $time ) ) {
+		return false;
+	}
+
+	// Get GMT offset from root blog.
+	if ( true === $gmt ) {
+
+		// Use Timezone string if set.
+		$timezone_string = bp_get_option( 'timezone_string' );
+		if ( ! empty( $timezone_string ) ) {
+			$timezone_object = timezone_open( $timezone_string );
+			$datetime_object = date_create( "@{$time}" );
+			$timezone_offset = timezone_offset_get( $timezone_object, $datetime_object ) / HOUR_IN_SECONDS;
+
+		// Fall back on less reliable gmt_offset.
+		} else {
+			$timezone_offset = bp_get_option( 'gmt_offset' );
+		}
+
+		// Calculate time based on the offset.
+		$calculated_time = $time + ( $timezone_offset * HOUR_IN_SECONDS );
+
+	// No localizing, so just use the time that was submitted.
+	} else {
+		$calculated_time = $time;
+	}
+
+	// Formatted date: "March 18, 2014".
+	$formatted_date = date_i18n( bp_get_option( 'date_format' ), $calculated_time, $gmt );
+
+	// Should we show the time also?
+	if ( true !== $exclude_time ) {
+
+		// Formatted time: "2:00 pm".
+		$formatted_time = date_i18n( bp_get_option( 'time_format' ), $calculated_time, $gmt );
+
+		// Return string formatted with date and time.
+		$formatted_date = sprintf( esc_html__( '%1$s at %2$s', 'buddypress' ), $formatted_date, $formatted_time );
+	}
+
+	/**
+	 * Filters the date based on a UNIX timestamp.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $formatted_date Formatted date from the timestamp.
+	 */
+	return apply_filters( 'bp_format_time', $formatted_date );
+}
+
+/**
+ * Select between two dynamic strings, according to context.
+ *
+ * This function can be used in cases where a phrase used in a template will
+ * differ for a user looking at his own profile and a user looking at another
+ * user's profile (eg, "My Friends" and "Joe's Friends"). Pass both versions
+ * of the phrase, and bp_word_or_name() will detect which is appropriate, and
+ * do the necessary argument swapping for dynamic phrases.
+ *
+ * @since 1.0.0
+ *
+ * @param string $youtext    The "you" version of the phrase (eg "Your Friends").
+ * @param string $nametext   The other-user version of the phrase. Should be in
+ *                           a format appropriate for sprintf() - use %s in place of the displayed
+ *                           user's name (eg "%'s Friends").
+ * @param bool   $capitalize Optional. Force into title case. Default: true.
+ * @param bool   $echo       Optional. True to echo the results, false to return them.
+ *                           Default: true.
+ * @return string|null $nametext If ! $echo, returns the appropriate string.
+ */
+function bp_word_or_name( $youtext, $nametext, $capitalize = true, $echo = true ) {
+
+	if ( ! empty( $capitalize ) ) {
+		$youtext = bp_core_ucfirst( $youtext );
+	}
+
+	if ( bp_displayed_user_id() == bp_loggedin_user_id() ) {
+		if ( true == $echo ) {
+
+			/**
+			 * Filters the text used based on context of own profile or someone else's profile.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param string $youtext Context-determined string to display.
+			 */
+			echo apply_filters( 'bp_word_or_name', $youtext );
+		} else {
+
+			/** This filter is documented in bp-core/bp-core-template.php */
+			return apply_filters( 'bp_word_or_name', $youtext );
+		}
+	} else {
+		$fullname = bp_get_displayed_user_fullname();
+		$fullname = (array) explode( ' ', $fullname );
+		$nametext = sprintf( $nametext, $fullname[0] );
+		if ( true == $echo ) {
+
+			/** This filter is documented in bp-core/bp-core-template.php */
+			echo apply_filters( 'bp_word_or_name', $nametext );
+		} else {
+
+			/** This filter is documented in bp-core/bp-core-template.php */
+			return apply_filters( 'bp_word_or_name', $nametext );
+		}
+	}
+}
+
+/**
+ * Do the 'bp_styles' action, and call wp_print_styles().
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_styles() {
+	do_action( 'bp_styles' );
+	wp_print_styles();
+}
+
+/** Search Form ***************************************************************/
+
+/**
+ * Return the "action" attribute for search forms.
+ *
+ * @since 1.0.0
+ *
+ * @return string URL action attribute for search forms, eg example.com/search/.
+ */
+function bp_search_form_action() {
+
+	/**
+	 * Filters the "action" attribute for search forms.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Search form action url.
+	 */
+	return apply_filters( 'bp_search_form_action', trailingslashit( bp_get_root_domain() . '/' . bp_get_search_slug() ) );
+}
+
+/**
+ * Generate the basic search form as used in BP-Default's header.
+ *
+ * @since 1.0.0
+ *
+ * @return string HTML <select> element.
+ */
+function bp_search_form_type_select() {
+
+	$options = array();
+
+	if ( bp_is_active( 'xprofile' ) ) {
+		$options['members'] = _x( 'Members', 'search form', 'buddypress' );
+	}
+
+	if ( bp_is_active( 'groups' ) ) {
+		$options['groups']  = _x( 'Groups', 'search form', 'buddypress' );
+	}
+
+	if ( bp_is_active( 'blogs' ) && is_multisite() ) {
+		$options['blogs']   = _x( 'Blogs', 'search form', 'buddypress' );
+	}
+
+	$options['posts'] = _x( 'Posts', 'search form', 'buddypress' );
+
+	// Eventually this won't be needed and a page will be built to integrate all search results.
+	$selection_box  = '<label for="search-which" class="accessibly-hidden">' . _x( 'Search these:', 'search form', 'buddypress' ) . '</label>';
+	$selection_box .= '<select name="search-which" id="search-which" style="width: auto">';
+
+	/**
+	 * Filters all of the component options available for search scope.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $options Array of options to add to select field.
+	 */
+	$options = apply_filters( 'bp_search_form_type_select_options', $options );
+	foreach( (array) $options as $option_value => $option_title ) {
+		$selection_box .= sprintf( '<option value="%s">%s</option>', $option_value, $option_title );
+	}
+
+	$selection_box .= '</select>';
+
+	/**
+	 * Filters the complete <select> input used for search scope.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $selection_box <select> input for selecting search scope.
+	 */
+	return apply_filters( 'bp_search_form_type_select', $selection_box );
+}
+
+/**
+ * Output the 'name' attribute for search form input element.
+ *
+ * @since 2.7.0
+ *
+ * @param string $component See bp_get_search_input_name().
+ */
+function bp_search_input_name( $component = '' ) {
+	echo esc_attr( bp_get_search_input_name( $component ) );
+}
+
+/**
+ * Get the 'name' attribute for the search form input element.
+ *
+ * @since 2.7.0
+ *
+ * @param string $component Component name. Defaults to current component.
+ * @return string Text for the 'name' attribute.
+ */
+function bp_get_search_input_name( $component = '' ) {
+	if ( ! $component ) {
+		$component = bp_current_component();
+	}
+
+	$bp = buddypress();
+
+	$name = '';
+	if ( isset( $bp->{$component}->id ) ) {
+		$name = $bp->{$component}->id . '_search';
+	}
+
+	return $name;
+}
+
+/**
+ * Output the placeholder text for the search box for a given component.
+ *
+ * @since 2.7.0
+ *
+ * @param string $component See bp_get_search_placeholder().
+ */
+function bp_search_placeholder( $component = '' ) {
+	echo esc_attr( bp_get_search_placeholder( $component ) );
+}
+
+/**
+ * Get the placeholder text for the search box for a given component.
+ *
+ * @since 2.7.0
+ *
+ * @param string $component Component name. Defaults to current component.
+ * @return string Placeholder text for the search field.
+ */
+function bp_get_search_placeholder( $component = '' ) {
+	$query_arg = bp_core_get_component_search_query_arg( $component );
+
+	if ( $query_arg && ! empty( $_REQUEST[ $query_arg ] ) ) {
+		$placeholder = wp_unslash( $_REQUEST[ $query_arg ] );
+	} else {
+		$placeholder = bp_get_search_default_text( $component );
+	}
+
+	return $placeholder;
+}
+
+/**
+ * Output the default text for the search box for a given component.
+ *
+ * @since 1.5.0
+ *
+ * @see bp_get_search_default_text()
+ *
+ * @param string $component See {@link bp_get_search_default_text()}.
+ */
+function bp_search_default_text( $component = '' ) {
+	echo bp_get_search_default_text( $component );
+}
+	/**
+	 * Return the default text for the search box for a given component.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $component Component name. Default: current component.
+	 * @return string Placeholder text for search field.
+	 */
+	function bp_get_search_default_text( $component = '' ) {
+
+		$bp = buddypress();
+
+		if ( empty( $component ) ) {
+			$component = bp_current_component();
+		}
+
+		$default_text = __( 'Search anything...', 'buddypress' );
+
+		// Most of the time, $component will be the actual component ID.
+		if ( !empty( $component ) ) {
+			if ( !empty( $bp->{$component}->search_string ) ) {
+				$default_text = $bp->{$component}->search_string;
+			} else {
+				// When the request comes through AJAX, we need to get the component
+				// name out of $bp->pages.
+				if ( !empty( $bp->pages->{$component}->slug ) ) {
+					$key = $bp->pages->{$component}->slug;
+					if ( !empty( $bp->{$key}->search_string ) ) {
+						$default_text = $bp->{$key}->search_string;
+					}
+				}
+			}
+		}
+
+		/**
+		 * Filters the default text for the search box for a given component.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $default_text Default text for search box.
+		 * @param string $component    Current component displayed.
+		 */
+		return apply_filters( 'bp_get_search_default_text', $default_text, $component );
+	}
+
+/**
+ * Fire the 'bp_custom_profile_boxes' action.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_custom_profile_boxes() {
+	do_action( 'bp_custom_profile_boxes' );
+}
+
+/**
+ * Fire the 'bp_custom_profile_sidebar_boxes' action.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_custom_profile_sidebar_boxes() {
+	do_action( 'bp_custom_profile_sidebar_boxes' );
+}
+
+/**
+ * Output the attributes for a form field.
+ *
+ * @since 2.2.0
+ *
+ * @param string $name       The field name to output attributes for.
+ * @param array  $attributes Array of existing attributes to add.
+ */
+function bp_form_field_attributes( $name = '', $attributes = array() ) {
+	echo bp_get_form_field_attributes( $name, $attributes );
+}
+	/**
+	 * Get the attributes for a form field.
+	 *
+	 * Primarily to add better support for touchscreen devices, but plugin devs
+	 * can use the 'bp_get_form_field_extra_attributes' filter for further
+	 * manipulation.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $name       The field name to get attributes for.
+	 * @param array  $attributes Array of existing attributes to add.
+	 * @return string
+	 */
+	function bp_get_form_field_attributes( $name = '', $attributes = array() ) {
+		$retval = '';
+
+		if ( empty( $attributes ) ) {
+			$attributes = array();
+		}
+
+		$name = strtolower( $name );
+
+		switch ( $name ) {
+			case 'username' :
+			case 'blogname' :
+				$attributes['autocomplete']   = 'off';
+				$attributes['autocapitalize'] = 'none';
+				break;
+
+			case 'email' :
+				if ( wp_is_mobile() ) {
+					$attributes['autocapitalize'] = 'none';
+				}
+				break;
+
+			case 'password' :
+				$attributes['spellcheck']   = 'false';
+				$attributes['autocomplete'] = 'off';
+
+				if ( wp_is_mobile() ) {
+					$attributes['autocorrect']    = 'false';
+					$attributes['autocapitalize'] = 'none';
+				}
+				break;
+		}
+
+		/**
+		 * Filter the attributes for a field before rendering output.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param array  $attributes The field attributes.
+		 * @param string $name       The field name.
+		 */
+		$attributes = (array) apply_filters( 'bp_get_form_field_attributes', $attributes, $name );
+
+		foreach( $attributes as $attr => $value ) {
+			// Numeric keyed array.
+			if (is_numeric( $attr ) ) {
+				$retval .= sprintf( ' %s', esc_attr( $value ) );
+
+			// Associative keyed array.
+			} else {
+				$retval .= sprintf( ' %s="%s"', sanitize_key( $attr ), esc_attr( $value ) );
+			}
+		}
+
+		return $retval;
+	}
+
+/**
+ * Create and output a button.
+ *
+ * @since 1.2.6
+ *
+ * @see bp_get_button()
+ *
+ * @param array|string $args See {@link BP_Button}.
+ */
+function bp_button( $args = '' ) {
+	echo bp_get_button( $args );
+}
+	/**
+	 * Create and return a button.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @see BP_Button for a description of arguments and return value.
+	 *
+	 * @param array|string $args See {@link BP_Button}.
+	 * @return string HTML markup for the button.
+	 */
+	function bp_get_button( $args = '' ) {
+		$button = new BP_Button( $args );
+
+		/**
+		 * Filters the requested button output.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string    $contents  Button context to be used.
+		 * @param array     $args      Array of args for the button.
+		 * @param BP_Button $button    BP_Button object.
+		 */
+		return apply_filters( 'bp_get_button', $button->contents, $args, $button );
+	}
+
+/**
+ * Truncate text.
+ *
+ * Cuts a string to the length of $length and replaces the last characters
+ * with the ending if the text is longer than length.
+ *
+ * This function is borrowed from CakePHP v2.0, under the MIT license. See
+ * http://book.cakephp.org/view/1469/Text#truncate-1625
+ *
+ * @since 1.0.0
+ * @since 2.6.0 Added 'strip_tags' and 'remove_links' as $options args.
+ *
+ * @param string $text   String to truncate.
+ * @param int    $length Optional. Length of returned string, including ellipsis.
+ *                       Default: 225.
+ * @param array  $options {
+ *     An array of HTML attributes and options. Each item is optional.
+ *     @type string $ending            The string used after truncation.
+ *                                     Default: ' [&hellip;]'.
+ *     @type bool   $exact             If true, $text will be trimmed to exactly $length.
+ *                                     If false, $text will not be cut mid-word. Default: false.
+ *     @type bool   $html              If true, don't include HTML tags when calculating
+ *                                     excerpt length. Default: true.
+ *     @type bool   $filter_shortcodes If true, shortcodes will be stripped.
+ *                                     Default: true.
+ *     @type bool   $strip_tags        If true, HTML tags will be stripped. Default: false.
+ *                                     Only applicable if $html is set to false.
+ *     @type bool   $remove_links      If true, URLs will be stripped. Default: false.
+ *                                     Only applicable if $html is set to false.
+ * }
+ * @return string Trimmed string.
+ */
+function bp_create_excerpt( $text, $length = 225, $options = array() ) {
+
+	// Backward compatibility. The third argument used to be a boolean $filter_shortcodes.
+	$filter_shortcodes_default = is_bool( $options ) ? $options : true;
+
+	$r = bp_parse_args( $options, array(
+		'ending'            => __( ' [&hellip;]', 'buddypress' ),
+		'exact'             => false,
+		'html'              => true,
+		'filter_shortcodes' => $filter_shortcodes_default,
+		'strip_tags'        => false,
+		'remove_links'      => false,
+	), 'create_excerpt' );
+
+	// Save the original text, to be passed along to the filter.
+	$original_text = $text;
+
+	/**
+	 * Filters the excerpt length to trim text to.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $length Length of returned string, including ellipsis.
+	 */
+	$length = apply_filters( 'bp_excerpt_length',      $length      );
+
+	/**
+	 * Filters the excerpt appended text value.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value Text to append to the end of the excerpt.
+	 */
+	$ending = apply_filters( 'bp_excerpt_append_text', $r['ending'] );
+
+	// Remove shortcodes if necessary.
+	if ( ! empty( $r['filter_shortcodes'] ) ) {
+		$text = strip_shortcodes( $text );
+	}
+
+	// When $html is true, the excerpt should be created without including HTML tags in the
+	// excerpt length.
+	if ( ! empty( $r['html'] ) ) {
+
+		// The text is short enough. No need to truncate.
+		if ( mb_strlen( preg_replace( '/<.*?>/', '', $text ) ) <= $length ) {
+			return $text;
+		}
+
+		$totalLength = mb_strlen( strip_tags( $ending ) );
+		$openTags    = array();
+		$truncate    = '';
+
+		// Find all the tags and HTML comments and put them in a stack for later use.
+		preg_match_all( '/(<\/?([\w+!]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER );
+
+		foreach ( $tags as $tag ) {
+			// Process tags that need to be closed.
+			if ( !preg_match( '/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s',  $tag[2] ) ) {
+				if ( preg_match( '/<[\w]+[^>]*>/s', $tag[0] ) ) {
+					array_unshift( $openTags, $tag[2] );
+				} elseif ( preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag ) ) {
+					$pos = array_search( $closeTag[1], $openTags );
+					if ( $pos !== false ) {
+						array_splice( $openTags, $pos, 1 );
+					}
+				}
+			}
+
+			$truncate     .= $tag[1];
+			$contentLength = mb_strlen( preg_replace( '/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3] ) );
+
+			if ( $contentLength + $totalLength > $length ) {
+				$left = $length - $totalLength;
+				$entitiesLength = 0;
+				if ( preg_match_all( '/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE ) ) {
+					foreach ( $entities[0] as $entity ) {
+						if ( $entity[1] + 1 - $entitiesLength <= $left ) {
+							$left--;
+							$entitiesLength += mb_strlen( $entity[0] );
+						} else {
+							break;
+						}
+					}
+				}
+
+				$truncate .= mb_substr( $tag[3], 0 , $left + $entitiesLength );
+				break;
+			} else {
+				$truncate .= $tag[3];
+				$totalLength += $contentLength;
+			}
+			if ( $totalLength >= $length ) {
+				break;
+			}
+		}
+	} else {
+		// Strip HTML tags if necessary.
+		if ( ! empty( $r['strip_tags'] ) ) {
+			$text = strip_tags( $text );
+		}
+
+		// Remove links if necessary.
+		if ( ! empty( $r['remove_links'] ) ) {
+			$text = preg_replace( '#^\s*(https?://[^\s"]+)\s*$#im', '', $text );
+		}
+
+		if ( mb_strlen( $text ) <= $length ) {
+			/**
+			 * Filters the final generated excerpt.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $truncate      Generated excerpt.
+			 * @param string $original_text Original text provided.
+			 * @param int    $length        Length of returned string, including ellipsis.
+			 * @param array  $options       Array of HTML attributes and options.
+			 */
+			return apply_filters( 'bp_create_excerpt', $text, $original_text, $length, $options );
+		} else {
+			$truncate = mb_substr( $text, 0, $length - mb_strlen( $ending ) );
+		}
+	}
+
+	// If $exact is false, we can't break on words.
+	if ( empty( $r['exact'] ) ) {
+		// Find the position of the last space character not part of a tag.
+		preg_match_all( '/<[a-z\!\/][^>]*>/', $truncate, $_truncate_tags, PREG_OFFSET_CAPTURE );
+
+		// Rekey tags by the string index of their last character.
+		$truncate_tags = array();
+		if ( ! empty( $_truncate_tags[0] ) ) {
+			foreach ( $_truncate_tags[0] as $_tt ) {
+				$_tt['start'] = $_tt[1];
+				$_tt['end']   = $_tt[1] + strlen( $_tt[0] );
+				$truncate_tags[ $_tt['end'] ] = $_tt;
+			}
+		}
+
+		$truncate_length = mb_strlen( $truncate );
+		$spacepos = $truncate_length + 1;
+		for ( $pos = $truncate_length - 1; $pos >= 0; $pos-- ) {
+			// Word boundaries are spaces and the close of HTML tags, when the tag is preceded by a space.
+			$is_word_boundary = ' ' === $truncate[ $pos ];
+			if ( ! $is_word_boundary && isset( $truncate_tags[ $pos - 1 ] ) ) {
+				$preceding_tag    = $truncate_tags[ $pos - 1 ];
+				if ( ' ' === $truncate[ $preceding_tag['start'] - 1 ] ) {
+					$is_word_boundary = true;
+					break;
+				}
+			}
+
+			if ( ! $is_word_boundary ) {
+				continue;
+			}
+
+			// If there are no tags in the string, the first space found is the right one.
+			if ( empty( $truncate_tags ) ) {
+				$spacepos = $pos;
+				break;
+			}
+
+			// Look at each tag to see if the space is inside of it.
+			$intag = false;
+			foreach ( $truncate_tags as $tt ) {
+				if ( $pos > $tt['start'] && $pos < $tt['end'] ) {
+					$intag = true;
+					break;
+				}
+			}
+
+			if ( ! $intag ) {
+				$spacepos = $pos;
+				break;
+			}
+		}
+
+		if ( $r['html'] ) {
+			$bits = mb_substr( $truncate, $spacepos );
+			preg_match_all( '/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER );
+			if ( !empty( $droppedTags ) ) {
+				foreach ( $droppedTags as $closingTag ) {
+					if ( !in_array( $closingTag[1], $openTags ) ) {
+						array_unshift( $openTags, $closingTag[1] );
+					}
+				}
+			}
+		}
+
+		$truncate = rtrim( mb_substr( $truncate, 0, $spacepos ) );
+	}
+	$truncate .= $ending;
+
+	if ( !empty( $r['html'] ) ) {
+		foreach ( $openTags as $tag ) {
+			$truncate .= '</' . $tag . '>';
+		}
+	}
+
+	/** This filter is documented in /bp-core/bp-core-template.php */
+	return apply_filters( 'bp_create_excerpt', $truncate, $original_text, $length, $options );
+}
+add_filter( 'bp_create_excerpt', 'stripslashes_deep'  );
+add_filter( 'bp_create_excerpt', 'force_balance_tags' );
+
+/**
+ * Output the total member count for the site.
+ *
+ * @since 1.2.0
+ */
+function bp_total_member_count() {
+	echo bp_get_total_member_count();
+}
+	/**
+	 * Return the total member count in your BP instance.
+	 *
+	 * Since BuddyPress 1.6, this function has used bp_core_get_active_member_count(),
+	 * which counts non-spam, non-deleted users who have last_activity.
+	 * This value will correctly match the total member count number used
+	 * for pagination on member directories.
+	 *
+	 * Before BuddyPress 1.6, this function used bp_core_get_total_member_count(),
+	 * which did not take into account last_activity, and thus often
+	 * resulted in higher counts than shown by member directory pagination.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return int Member count.
+	 */
+	function bp_get_total_member_count() {
+
+		/**
+		 * Filters the total member count in your BP instance.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $value Member count.
+		 */
+		return apply_filters( 'bp_get_total_member_count', bp_core_get_active_member_count() );
+	}
+	add_filter( 'bp_get_total_member_count', 'bp_core_number_format' );
+
+/**
+ * Output whether blog signup is allowed.
+ *
+ * @todo Deprecate. It doesn't make any sense to echo a boolean.
+ */
+function bp_blog_signup_allowed() {
+	echo bp_get_blog_signup_allowed();
+}
+	/**
+	 * Is blog signup allowed?
+	 *
+	 * Returns true if is_multisite() and blog creation is enabled at
+	 * Network Admin > Settings.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return bool True if blog signup is allowed, otherwise false.
+	 */
+	function bp_get_blog_signup_allowed() {
+
+		if ( ! is_multisite() ) {
+			return false;
+		}
+
+		$status = bp_core_get_root_option( 'registration' );
+		if ( ( 'none' !== $status ) && ( 'user' !== $status ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+/**
+ * Check whether an activation has just been completed.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the activation_complete global flag has been set,
+ *              otherwise false.
+ */
+function bp_account_was_activated() {
+	$activation_complete = ! empty( buddypress()->activation_complete ) || ( bp_is_current_component( 'activate' ) && ! empty( $_GET['activated'] ) );
+
+	return $activation_complete;
+}
+
+/**
+ * Check whether registrations require activation on this installation.
+ *
+ * On a normal BuddyPress installation, all registrations require email
+ * activation. This filter exists so that customizations that omit activation
+ * can remove certain notification text from the registration screen.
+ *
+ * @since 1.2.0
+ *
+ * @return bool True by default.
+ */
+function bp_registration_needs_activation() {
+
+	/**
+	 * Filters whether registrations require activation on this installation.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param bool $value Whether registrations require activation. Default true.
+	 */
+	return apply_filters( 'bp_registration_needs_activation', true );
+}
+
+/**
+ * Retrieve a client friendly version of the root blog name.
+ *
+ * The blogname option is escaped with esc_html on the way into the database in
+ * sanitize_option, we want to reverse this for the plain text arena of emails.
+ *
+ * @since 1.7.0
+ * @since 2.5.0 No longer used by BuddyPress, but not deprecated in case any existing plugins use it.
+ *
+ * @see https://buddypress.trac.wordpress.org/ticket/4401
+ *
+ * @param array $args {
+ *     Array of optional parameters.
+ *     @type string $before  String to appear before the site name in the
+ *                           email subject. Default: '['.
+ *     @type string $after   String to appear after the site name in the
+ *                           email subject. Default: ']'.
+ *     @type string $default The default site name, to be used when none is
+ *                           found in the database. Default: 'Community'.
+ *     @type string $text    Text to append to the site name (ie, the main text of
+ *                           the email subject).
+ * }
+ * @return string Sanitized email subject.
+ */
+function bp_get_email_subject( $args = array() ) {
+
+	$r = bp_parse_args( $args, array(
+		'before'  => '[',
+		'after'   => ']',
+		'default' => __( 'Community', 'buddypress' ),
+		'text'    => ''
+	), 'get_email_subject' );
+
+	$subject = $r['before'] . wp_specialchars_decode( bp_get_option( 'blogname', $r['default'] ), ENT_QUOTES ) . $r['after'] . ' ' . $r['text'];
+
+	/**
+	 * Filters a client friendly version of the root blog name.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $subject Client friendy version of the root blog name.
+	 * @param array  $r       Array of arguments for the email subject.
+	 */
+	return apply_filters( 'bp_get_email_subject', $subject, $r );
+}
+
+/**
+ * Allow templates to pass parameters directly into the template loops via AJAX.
+ *
+ * For the most part this will be filtered in a theme's functions.php for
+ * example in the default theme it is filtered via bp_dtheme_ajax_querystring().
+ *
+ * By using this template tag in the templates it will stop them from showing
+ * errors if someone copies the templates from the default theme into another
+ * WordPress theme without coping the functions from functions.php.
+ *
+ * @since 1.2.0
+ *
+ * @param string|bool $object Current template component.
+ * @return string The AJAX querystring.
+ */
+function bp_ajax_querystring( $object = false ) {
+	$bp = buddypress();
+
+	if ( ! isset( $bp->ajax_querystring ) ) {
+		$bp->ajax_querystring = '';
+	}
+
+	/**
+	 * Filters the template paramenters to be used in the query string.
+	 *
+	 * Allows templates to pass parameters into the template loops via AJAX.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $ajax_querystring Current query string.
+	 * @param string $object           Current template component.
+	 */
+	return apply_filters( 'bp_ajax_querystring', $bp->ajax_querystring, $object );
+}
+
+/** Template Classes and _is functions ****************************************/
+
+/**
+ * Return the name of the current component.
+ *
+ * @since 1.0.0
+ *
+ * @return string Component name.
+ */
+function bp_current_component() {
+	$bp                = buddypress();
+	$current_component = !empty( $bp->current_component )
+		? $bp->current_component
+		: false;
+
+	/**
+	 * Filters the name of the current component.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string|bool $current_component Current component if available or false.
+	 */
+	return apply_filters( 'bp_current_component', $current_component );
+}
+
+/**
+ * Return the name of the current action.
+ *
+ * @since 1.0.0
+ *
+ * @return string Action name.
+ */
+function bp_current_action() {
+	$bp             = buddypress();
+	$current_action = !empty( $bp->current_action )
+		? $bp->current_action
+		: '';
+
+	/**
+	 * Filters the name of the current action.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $current_action Current action.
+	 */
+	return apply_filters( 'bp_current_action', $current_action );
+}
+
+/**
+ * Return the name of the current item.
+ *
+ * @since 1.1.0
+ *
+ * @return string|bool
+ */
+function bp_current_item() {
+	$bp           = buddypress();
+	$current_item = !empty( $bp->current_item )
+		? $bp->current_item
+		: false;
+
+	/**
+	 * Filters the name of the current item.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string|bool $current_item Current item if available or false.
+	 */
+	return apply_filters( 'bp_current_item', $current_item );
+}
+
+/**
+ * Return the value of $bp->action_variables.
+ *
+ * @since 1.0.0
+ *
+ * @return array|bool $action_variables The action variables array, or false
+ *                                      if the array is empty.
+ */
+function bp_action_variables() {
+	$bp               = buddypress();
+	$action_variables = !empty( $bp->action_variables )
+		? $bp->action_variables
+		: false;
+
+	/**
+	 * Filters the value of $bp->action_variables.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array|bool $action_variables Available action variables.
+	 */
+	return apply_filters( 'bp_action_variables', $action_variables );
+}
+
+/**
+ * Return the value of a given action variable.
+ *
+ * @since 1.5.0
+ *
+ * @param int $position The key of the action_variables array that you want.
+ * @return string|bool $action_variable The value of that position in the
+ *                                      array, or false if not found.
+ */
+function bp_action_variable( $position = 0 ) {
+	$action_variables = bp_action_variables();
+	$action_variable  = isset( $action_variables[ $position ] )
+		? $action_variables[ $position ]
+		: false;
+
+	/**
+	 * Filters the value of a given action variable.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string|bool $action_variable Requested action variable based on position.
+	 * @param int         $position        The key of the action variable requested.
+	 */
+	return apply_filters( 'bp_action_variable', $action_variable, $position );
+}
+
+/**
+ * Output the "root domain", the URL of the BP root blog.
+ *
+ * @since 1.1.0
+ */
+function bp_root_domain() {
+	echo bp_get_root_domain();
+}
+	/**
+	 * Return the "root domain", the URL of the BP root blog.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string URL of the BP root blog.
+	 */
+	function bp_get_root_domain() {
+		$bp = buddypress();
+
+		if ( ! empty( $bp->root_domain ) ) {
+			$domain = $bp->root_domain;
+		} else {
+			$domain          = bp_core_get_root_domain();
+			$bp->root_domain = $domain;
+		}
+
+		/**
+		 * Filters the "root domain", the URL of the BP root blog.
+		 *
+		 * @since 1.2.4
+		 *
+		 * @param string $domain URL of the BP root blog.
+		 */
+		return apply_filters( 'bp_get_root_domain', $domain );
+	}
+
+/**
+ * Output the root slug for a given component.
+ *
+ * @since 1.5.0
+ *
+ * @param string $component The component name.
+ */
+function bp_root_slug( $component = '' ) {
+	echo bp_get_root_slug( $component );
+}
+	/**
+	 * Get the root slug for given component.
+	 *
+	 * The "root slug" is the string used when concatenating component
+	 * directory URLs. For example, on an installation where the Groups
+	 * component's directory is located at http://example.com/groups/, the
+	 * root slug for the Groups component is 'groups'. This string
+	 * generally corresponds to page_name of the component's directory
+	 * page.
+	 *
+	 * In order to maintain backward compatibility, the following procedure
+	 * is used:
+	 * 1) Use the short slug to get the canonical component name from the
+	 *    active component array.
+	 * 2) Use the component name to get the root slug out of the
+	 *    appropriate part of the $bp global.
+	 * 3) If nothing turns up, it probably means that $component is itself
+	 *    a root slug.
+	 *
+	 * Example: If your groups directory is at /community/companies, this
+	 * function first uses the short slug 'companies' (ie the current
+	 * component) to look up the canonical name 'groups' in
+	 * $bp->active_components. Then it uses 'groups' to get the root slug,
+	 * from $bp->groups->root_slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $component Optional. Defaults to the current component.
+	 * @return string $root_slug The root slug.
+	 */
+	function bp_get_root_slug( $component = '' ) {
+		$bp        = buddypress();
+		$root_slug = '';
+
+		// Use current global component if none passed.
+		if ( empty( $component ) ) {
+			$component = bp_current_component();
+		}
+
+		// Component is active.
+		if ( ! empty( $bp->active_components[ $component ] ) ) {
+
+			// Backward compatibility: in legacy plugins, the canonical component id
+			// was stored as an array value in $bp->active_components.
+			$component_name = ( '1' == $bp->active_components[ $component ] )
+				? $component
+				: $bp->active_components[$component];
+
+			// Component has specific root slug.
+			if ( ! empty( $bp->{$component_name}->root_slug ) ) {
+				$root_slug = $bp->{$component_name}->root_slug;
+			}
+		}
+
+		// No specific root slug, so fall back to component slug.
+		if ( empty( $root_slug ) ) {
+			$root_slug = $component;
+		}
+
+		/**
+		 * Filters the root slug for given component.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $root_slug Root slug for given component.
+		 * @param string $component Current component.
+		 */
+		return apply_filters( 'bp_get_root_slug', $root_slug, $component );
+	}
+
+/**
+ * Return the component name based on a root slug.
+ *
+ * @since 1.5.0
+ *
+ * @param string $root_slug Needle to our active component haystack.
+ * @return mixed False if none found, component name if found.
+ */
+function bp_get_name_from_root_slug( $root_slug = '' ) {
+	$bp = buddypress();
+
+	// If no slug is passed, look at current_component.
+	if ( empty( $root_slug ) ) {
+		$root_slug = bp_current_component();
+	}
+
+	// No current component or root slug, so flee.
+	if ( empty( $root_slug ) ) {
+		return false;
+	}
+
+	// Loop through active components and look for a match.
+	foreach ( array_keys( $bp->active_components ) as $component ) {
+		if ( ( ! empty( $bp->{$component}->slug ) && ( $bp->{$component}->slug == $root_slug ) ) || ( ! empty( $bp->{$component}->root_slug ) && ( $bp->{$component}->root_slug === $root_slug ) ) ) {
+			return $bp->{$component}->name;
+		}
+	}
+
+	return false;
+}
+
+/**
+ * Returns whether or not a user has access.
+ *
+ * @since 1.2.4
+ *
+ * @return bool
+ */
+function bp_user_has_access() {
+	$has_access = bp_current_user_can( 'bp_moderate' ) || bp_is_my_profile();
+
+	/**
+	 * Filters whether or not a user has access.
+	 *
+	 * @since 1.2.4
+	 *
+	 * @param bool $has_access Whether or not user has access.
+	 */
+	return (bool) apply_filters( 'bp_user_has_access', $has_access );
+}
+
+/**
+ * Output the search slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_search_slug() {
+	echo bp_get_search_slug();
+}
+	/**
+	 * Return the search slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string The search slug. Default: 'search'.
+	 */
+	function bp_get_search_slug() {
+
+		/**
+		 * Filters the search slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @const string BP_SEARCH_SLUG The search slug. Default "search".
+		 */
+		return apply_filters( 'bp_get_search_slug', BP_SEARCH_SLUG );
+	}
+
+/**
+ * Get the ID of the currently displayed user.
+ *
+ * @since 1.0.0
+ *
+ * @return int $id ID of the currently displayed user.
+ */
+function bp_displayed_user_id() {
+	$bp = buddypress();
+	$id = !empty( $bp->displayed_user->id )
+		? $bp->displayed_user->id
+		: 0;
+
+	/**
+	 * Filters the ID of the currently displayed user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the currently displayed user.
+	 */
+	return (int) apply_filters( 'bp_displayed_user_id', $id );
+}
+
+/**
+ * Get the ID of the currently logged-in user.
+ *
+ * @since 1.0.0
+ *
+ * @return int ID of the logged-in user.
+ */
+function bp_loggedin_user_id() {
+	$bp = buddypress();
+	$id = !empty( $bp->loggedin_user->id )
+		? $bp->loggedin_user->id
+		: 0;
+
+	/**
+	 * Filters the ID of the currently logged-in user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the currently logged-in user.
+	 */
+	return (int) apply_filters( 'bp_loggedin_user_id', $id );
+}
+
+/** The is_() functions to determine the current page *****************************/
+
+/**
+ * Check to see whether the current page belongs to the specified component.
+ *
+ * This function is designed to be generous, accepting several different kinds
+ * of value for the $component parameter. It checks $component_name against:
+ * - the component's root_slug, which matches the page slug in $bp->pages.
+ * - the component's regular slug.
+ * - the component's id, or 'canonical' name.
+ *
+ * @since 1.5.0
+ *
+ * @param string $component Name of the component being checked.
+ * @return bool Returns true if the component matches, or else false.
+ */
+function bp_is_current_component( $component = '' ) {
+
+	// Default is no match. We'll check a few places for matches.
+	$is_current_component = false;
+
+	// Always return false if a null value is passed to the function.
+	if ( empty( $component ) ) {
+		return false;
+	}
+
+	// Backward compatibility: 'xprofile' should be read as 'profile'.
+	if ( 'xprofile' === $component ) {
+		$component = 'profile';
+	}
+
+	$bp = buddypress();
+
+	// Only check if BuddyPress found a current_component.
+	if ( ! empty( $bp->current_component ) ) {
+
+		// First, check to see whether $component_name and the current
+		// component are a simple match.
+		if ( $bp->current_component == $component ) {
+			$is_current_component = true;
+
+		// Since the current component is based on the visible URL slug let's
+		// check the component being passed and see if its root_slug matches.
+		} elseif ( isset( $bp->{$component}->root_slug ) && $bp->{$component}->root_slug == $bp->current_component ) {
+			$is_current_component = true;
+
+		// Because slugs can differ from root_slugs, we should check them too.
+		} elseif ( isset( $bp->{$component}->slug ) && $bp->{$component}->slug == $bp->current_component ) {
+			$is_current_component = true;
+
+		// Next, check to see whether $component is a canonical,
+		// non-translatable component name. If so, we can return its
+		// corresponding slug from $bp->active_components.
+		} elseif ( $key = array_search( $component, $bp->active_components ) ) {
+			if ( strstr( $bp->current_component, $key ) ) {
+				$is_current_component = true;
+			}
+
+		// If we haven't found a match yet, check against the root_slugs
+		// created by $bp->pages, as well as the regular slugs.
+		} else {
+			foreach ( $bp->active_components as $id ) {
+				// If the $component parameter does not match the current_component,
+				// then move along, these are not the droids you are looking for.
+				if ( empty( $bp->{$id}->root_slug ) || $bp->{$id}->root_slug != $bp->current_component ) {
+					continue;
+				}
+
+				if ( $id == $component ) {
+					$is_current_component = true;
+					break;
+				}
+			}
+		}
+	}
+
+	/**
+	 * Filters whether the current page belongs to the specified component.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $is_current_component Whether or not the current page belongs to specified component.
+	 * @param string $component            Name of the component being checked.
+	 */
+	return apply_filters( 'bp_is_current_component', $is_current_component, $component );
+}
+
+/**
+ * Check to see whether the current page matches a given action.
+ *
+ * Along with bp_is_current_component() and bp_is_action_variable(), this
+ * function is mostly used to help determine when to use a given screen
+ * function.
+ *
+ * In BP parlance, the current_action is the URL chunk that comes directly
+ * after the current item slug. E.g., in
+ *   http://example.com/groups/my-group/members
+ * the current_action is 'members'.
+ *
+ * @since 1.5.0
+ *
+ * @param string $action The action being tested against.
+ * @return bool True if the current action matches $action.
+ */
+function bp_is_current_action( $action = '' ) {
+	return (bool) ( $action === bp_current_action() );
+}
+
+/**
+ * Check to see whether the current page matches a given action_variable.
+ *
+ * Along with bp_is_current_component() and bp_is_current_action(), this
+ * function is mostly used to help determine when to use a given screen
+ * function.
+ *
+ * In BP parlance, action_variables are an array made up of the URL chunks
+ * appearing after the current_action in a URL. For example,
+ *   http://example.com/groups/my-group/admin/group-settings
+ * $action_variables[0] is 'group-settings'.
+ *
+ * @since 1.5.0
+ *
+ * @param string   $action_variable The action_variable being tested against.
+ * @param int|bool $position        Optional. The array key you're testing against. If you
+ *                                  don't provide a $position, the function will return true if the
+ *                                  $action_variable is found *anywhere* in the action variables array.
+ * @return bool True if $action_variable matches at the $position provided.
+ */
+function bp_is_action_variable( $action_variable = '', $position = false ) {
+	$is_action_variable = false;
+
+	if ( false !== $position ) {
+		// When a $position is specified, check that slot in the action_variables array.
+		if ( $action_variable ) {
+			$is_action_variable = $action_variable == bp_action_variable( $position );
+		} else {
+			// If no $action_variable is provided, we are essentially checking to see
+			// whether the slot is empty.
+			$is_action_variable = !bp_action_variable( $position );
+		}
+	} else {
+		// When no $position is specified, check the entire array.
+		$is_action_variable = in_array( $action_variable, (array)bp_action_variables() );
+	}
+
+	/**
+	 * Filters whether the current page matches a given action_variable.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $is_action_variable Whether the current page matches a given action_variable.
+	 * @param string $action_variable    The action_variable being tested against.
+	 * @param int    $position           The array key tested against.
+	 */
+	return apply_filters( 'bp_is_action_variable', $is_action_variable, $action_variable, $position );
+}
+
+/**
+ * Check against the current_item.
+ *
+ * @since 1.5.0
+ *
+ * @param string $item The item being checked.
+ * @return bool True if $item is the current item.
+ */
+function bp_is_current_item( $item = '' ) {
+	$retval = ( $item === bp_current_item() );
+
+	/**
+	 * Filters whether or not an item is the current item.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool   $retval Whether or not an item is the current item.
+	 * @param string $item   The item being checked.
+	 */
+	return (bool) apply_filters( 'bp_is_current_item', $retval, $item );
+}
+
+/**
+ * Are we looking at a single item? (group, user, etc).
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if looking at a single item, otherwise false.
+ */
+function bp_is_single_item() {
+	$bp     = buddypress();
+	$retval = false;
+
+	if ( isset( $bp->is_single_item ) ) {
+		$retval = $bp->is_single_item;
+	}
+
+	/**
+	 * Filters whether or not an item is the a single item. (group, user, etc)
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not an item is a single item.
+	 */
+	return (bool) apply_filters( 'bp_is_single_item', $retval );
+}
+
+/**
+ * Is the logged-in user an admin for the current item?
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current user is an admin for the current item,
+ *              otherwise false.
+ */
+function bp_is_item_admin() {
+	$bp     = buddypress();
+	$retval = false;
+
+	if ( isset( $bp->is_item_admin ) ) {
+		$retval = $bp->is_item_admin;
+	}
+
+	/**
+	 * Filters whether or not the logged-in user is an admin for the current item.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not the logged-in user is an admin.
+	 */
+	return (bool) apply_filters( 'bp_is_item_admin', $retval );
+}
+
+/**
+ * Is the logged-in user a mod for the current item?
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current user is a mod for the current item,
+ *              otherwise false.
+ */
+function bp_is_item_mod() {
+	$bp     = buddypress();
+	$retval = false;
+
+	if ( isset( $bp->is_item_mod ) ) {
+		$retval = $bp->is_item_mod;
+	}
+
+	/**
+	 * Filters whether or not the logged-in user is a mod for the current item.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not the logged-in user is a mod.
+	 */
+	return (bool) apply_filters( 'bp_is_item_mod', $retval );
+}
+
+/**
+ * Is this a component directory page?
+ *
+ * @since 1.0.0
+ *
+ * @return bool True if the current page is a component directory, otherwise false.
+ */
+function bp_is_directory() {
+	$bp     = buddypress();
+	$retval = false;
+
+	if ( isset( $bp->is_directory ) ) {
+		$retval = $bp->is_directory;
+	}
+
+	/**
+	 * Filters whether or not user is on a component directory page.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not user is on a component directory page.
+	 */
+	return (bool) apply_filters( 'bp_is_directory', $retval );
+}
+
+/**
+ * Check to see if a component's URL should be in the root, not under a member page.
+ *
+ * - Yes ('groups' is root)    : http://example.com/groups/the-group
+ * - No  ('groups' is not-root): http://example.com/members/andy/groups/the-group
+ *
+ * This function is on the chopping block. It's currently only used by a few
+ * already deprecated functions.
+ *
+ * @since 1.5.0
+ *
+ * @param string $component_name Component name to check.
+ *
+ * @return bool True if root component, else false.
+ */
+function bp_is_root_component( $component_name = '' ) {
+	$bp     = buddypress();
+	$retval = false;
+
+	// Default to the current component if none is passed.
+	if ( empty( $component_name ) ) {
+		$component_name = bp_current_component();
+	}
+
+	// Loop through active components and check for key/slug matches.
+	if ( ! empty( $bp->active_components ) ) {
+		foreach ( (array) $bp->active_components as $key => $slug ) {
+			if ( ( $key === $component_name ) || ( $slug === $component_name ) ) {
+				$retval = true;
+				break;
+			}
+		}
+	}
+
+	/**
+	 * Filters whether or not a component's URL should be in the root, not under a member page.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not URL should be in the root.
+	 */
+	return (bool) apply_filters( 'bp_is_root_component', $retval );
+}
+
+/**
+ * Check if the specified BuddyPress component directory is set to be the front page.
+ *
+ * Corresponds to the setting in wp-admin's Settings > Reading screen.
+ *
+ * @since 1.5.0
+ *
+ * @global int $current_blog WordPress global for the current blog.
+ *
+ * @param string $component Optional. Name of the component to check for.
+ *                          Default: current component.
+ * @return bool True if the specified component is set to be the site's front
+ *              page, otherwise false.
+ */
+function bp_is_component_front_page( $component = '' ) {
+	global $current_blog;
+
+	$bp = buddypress();
+
+	// Default to the current component if none is passed.
+	if ( empty( $component ) ) {
+		$component = bp_current_component();
+	}
+
+	// Get the path for the current blog/site.
+	$path = is_main_site()
+		? bp_core_get_site_path()
+		: $current_blog->path;
+
+	// Get the front page variables.
+	$show_on_front = get_option( 'show_on_front' );
+	$page_on_front = get_option( 'page_on_front' );
+
+	if ( ( 'page' !== $show_on_front ) || empty( $component ) || empty( $bp->pages->{$component} ) || ( $_SERVER['REQUEST_URI'] !== $path ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters whether or not the specified BuddyPress component directory is set to be the front page.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool   $value     Whether or not the specified component directory is set as front page.
+	 * @param string $component Current component being checked.
+	 */
+	return (bool) apply_filters( 'bp_is_component_front_page', ( $bp->pages->{$component}->id == $page_on_front ), $component );
+}
+
+/**
+ * Is this a blog page, ie a non-BP page?
+ *
+ * You can tell if a page is displaying BP content by whether the
+ * current_component has been defined.
+ *
+ * @since 1.0.0
+ *
+ * @return bool True if it's a non-BP page, false otherwise.
+ */
+function bp_is_blog_page() {
+
+	$is_blog_page = false;
+
+	// Generally, we can just check to see that there's no current component.
+	// The one exception is single user home tabs, where $bp->current_component
+	// is unset. Thus the addition of the bp_is_user() check.
+	if ( ! bp_current_component() && ! bp_is_user() ) {
+		$is_blog_page = true;
+	}
+
+	/**
+	 * Filters whether or not current page is a blog page or not.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $is_blog_page Whether or not current page is a blog page.
+	 */
+	return (bool) apply_filters( 'bp_is_blog_page', $is_blog_page );
+}
+
+/**
+ * Is this a BuddyPress component?
+ *
+ * You can tell if a page is displaying BP content by whether the
+ * current_component has been defined.
+ *
+ * Generally, we can just check to see that there's no current component.
+ * The one exception is single user home tabs, where $bp->current_component
+ * is unset. Thus the addition of the bp_is_user() check.
+ *
+ * @since 1.7.0
+ *
+ * @return bool True if it's a BuddyPress page, false otherwise.
+ */
+function is_buddypress() {
+	$retval = (bool) ( bp_current_component() || bp_is_user() );
+
+	/**
+	 * Filters whether or not this is a BuddyPress component.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool $retval Whether or not this is a BuddyPress component.
+	 */
+	return apply_filters( 'is_buddypress', $retval );
+}
+
+/** Components ****************************************************************/
+
+/**
+ * Check whether a given component (or feature of a component) is active.
+ *
+ * @since 1.2.0 See r2539.
+ * @since 2.3.0 Added $feature as a parameter.
+ *
+ * @param string $component The component name.
+ * @param string $feature   The feature name.
+ * @return bool
+ */
+function bp_is_active( $component = '', $feature = '' ) {
+	$retval = false;
+
+	// Default to the current component if none is passed.
+	if ( empty( $component ) ) {
+		$component = bp_current_component();
+	}
+
+	// Is component in either the active or required components arrays.
+	if ( isset( buddypress()->active_components[ $component ] ) || isset( buddypress()->required_components[ $component ] ) ) {
+		$retval = true;
+
+		// Is feature active?
+		if ( ! empty( $feature ) ) {
+			// The xProfile component is specific.
+			if ( 'xprofile' === $component ) {
+				$component = 'profile';
+			}
+
+			if ( empty( buddypress()->$component->features ) || false === in_array( $feature, buddypress()->$component->features, true ) ) {
+				$retval = false;
+			}
+
+			/**
+			 * Filters whether or not a given feature for a component is active.
+			 *
+			 * This is a variable filter that is based on the component and feature
+			 * that you are checking of active status of.
+			 *
+			 * @since 2.3.0
+			 *
+			 * @param bool $retval
+			 */
+			$retval = apply_filters( "bp_is_{$component}_{$feature}_active", $retval );
+		}
+	}
+
+	/**
+	 * Filters whether or not a given component has been activated by the admin.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool   $retval    Whether or not a given component has been activated by the admin.
+	 * @param string $component Current component being checked.
+	 */
+	return apply_filters( 'bp_is_active', $retval, $component );
+}
+
+/**
+ * Check whether the current page is part of the Members component.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is part of the Members component.
+ */
+function bp_is_members_component() {
+	return (bool) bp_is_current_component( 'members' );
+}
+
+/**
+ * Check whether the current page is part of the Profile component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Profile component.
+ */
+function bp_is_profile_component() {
+	return (bool) bp_is_current_component( 'xprofile' );
+}
+
+/**
+ * Check whether the current page is part of the Activity component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Activity component.
+ */
+function bp_is_activity_component() {
+	return (bool) bp_is_current_component( 'activity' );
+}
+
+/**
+ * Check whether the current page is part of the Blogs component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Blogs component.
+ */
+function bp_is_blogs_component() {
+	return (bool) ( is_multisite() && bp_is_current_component( 'blogs' ) );
+}
+
+/**
+ * Check whether the current page is part of the Messages component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Messages component.
+ */
+function bp_is_messages_component() {
+	return (bool) bp_is_current_component( 'messages' );
+}
+
+/**
+ * Check whether the current page is part of the Friends component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Friends component.
+ */
+function bp_is_friends_component() {
+	return (bool) bp_is_current_component( 'friends' );
+}
+
+/**
+ * Check whether the current page is part of the Groups component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Groups component.
+ */
+function bp_is_groups_component() {
+	return (bool) bp_is_current_component( 'groups' );
+}
+
+/**
+ * Check whether the current page is part of the Forums component.
+ *
+ * @since 1.5.0
+ * @since 3.0.0 Required for bbPress 2 integration.
+ *
+ * @return bool True if the current page is part of the Forums component.
+ */
+function bp_is_forums_component() {
+	return (bool) bp_is_current_component( 'forums' );
+}
+
+/**
+ * Check whether the current page is part of the Notifications component.
+ *
+ * @since 1.9.0
+ *
+ * @return bool True if the current page is part of the Notifications component.
+ */
+function bp_is_notifications_component() {
+	return (bool) bp_is_current_component( 'notifications' );
+}
+
+/**
+ * Check whether the current page is part of the Settings component.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the Settings component.
+ */
+function bp_is_settings_component() {
+	return (bool) bp_is_current_component( 'settings' );
+}
+
+/**
+ * Is the current component an active core component?
+ *
+ * Use this function when you need to check if the current component is an
+ * active core component of BuddyPress. If the current component is inactive, it
+ * will return false. If the current component is not part of BuddyPress core,
+ * it will return false. If the current component is active, and is part of
+ * BuddyPress core, it will return true.
+ *
+ * @since 1.7.0
+ *
+ * @return bool True if the current component is active and is one of BP's
+ *              packaged components.
+ */
+function bp_is_current_component_core() {
+	$retval = false;
+
+	foreach ( bp_core_get_packaged_component_ids() as $active_component ) {
+		if ( bp_is_current_component( $active_component ) ) {
+			$retval = true;
+			break;
+		}
+	}
+
+	return $retval;
+}
+
+/** Activity ******************************************************************/
+
+/**
+ * Is the current page the activity directory?
+ *
+ * @since 2.0.0
+ *
+ * @return bool True if the current page is the activity directory.
+ */
+function bp_is_activity_directory() {
+	if ( ! bp_displayed_user_id() && bp_is_activity_component() && ! bp_current_action() ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Is the current page a single activity item permalink?
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a single activity item permalink.
+ */
+function bp_is_single_activity() {
+	return (bool) ( bp_is_activity_component() && is_numeric( bp_current_action() ) );
+}
+
+/** User **********************************************************************/
+
+/**
+ * Is the current page the members directory?
+ *
+ * @since 2.0.0
+ *
+ * @return bool True if the current page is the members directory.
+ */
+function bp_is_members_directory() {
+	if ( ! bp_is_user() && bp_is_members_component() ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Is the current page part of the profile of the logged-in user?
+ *
+ * Will return true for any subpage of the logged-in user's profile, eg
+ * http://example.com/members/joe/friends/.
+ *
+ * @since 1.2.0
+ *
+ * @return bool True if the current page is part of the profile of the logged-in user.
+ */
+function bp_is_my_profile() {
+	if ( is_user_logged_in() && bp_loggedin_user_id() == bp_displayed_user_id() ) {
+		$my_profile = true;
+	} else {
+		$my_profile = false;
+	}
+
+	/**
+	 * Filters whether or not current page is part of the profile for the logged-in user.
+	 *
+	 * @since 1.2.4
+	 *
+	 * @param bool $my_profile Whether or not current page is part of the profile for the logged-in user.
+	 */
+	return apply_filters( 'bp_is_my_profile', $my_profile );
+}
+
+/**
+ * Is the current page a user page?
+ *
+ * Will return true anytime there is a displayed user.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user page.
+ */
+function bp_is_user() {
+	return (bool) bp_displayed_user_id();
+}
+
+/**
+ * Is the current page a user custom front page?
+ *
+ * Will return true anytime there is a custom front page for the displayed user.
+ *
+ * @since 2.6.0
+ *
+ * @return bool True if the current page is a user custom front page.
+ */
+function bp_is_user_front() {
+	return (bool) ( bp_is_user() && bp_is_current_component( 'front' ) );
+}
+
+/**
+ * Is the current page a user's activity stream page?
+ *
+ * Eg http://example.com/members/joe/activity/ (or any subpages thereof).
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's activity stream page.
+ */
+function bp_is_user_activity() {
+	return (bool) ( bp_is_user() && bp_is_activity_component() );
+}
+
+/**
+ * Is the current page a user's Friends activity stream?
+ *
+ * Eg http://example.com/members/joe/friends/
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Friends activity stream.
+ */
+function bp_is_user_friends_activity() {
+
+	if ( ! bp_is_active( 'friends' ) ) {
+		return false;
+	}
+
+	$slug = bp_get_friends_slug();
+
+	if ( empty( $slug ) ) {
+		$slug = 'friends';
+	}
+
+	if ( bp_is_user_activity() && bp_is_current_action( $slug ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Is the current page a user's Groups activity stream?
+ *
+ * Eg http://example.com/members/joe/groups/
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's Groups activity stream.
+ */
+function bp_is_user_groups_activity() {
+
+	if ( ! bp_is_active( 'groups' ) ) {
+		return false;
+	}
+
+	$slug = ( bp_get_groups_slug() )
+		? bp_get_groups_slug()
+		: 'groups';
+
+	if ( bp_is_user_activity() && bp_is_current_action( $slug ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Is the current page part of a user's extended profile?
+ *
+ * Eg http://example.com/members/joe/profile/ (or a subpage thereof).
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of a user's extended profile.
+ */
+function bp_is_user_profile() {
+	return (bool) ( bp_is_profile_component() || bp_is_current_component( 'profile' ) );
+}
+
+/**
+ * Is the current page part of a user's profile editing section?
+ *
+ * Eg http://example.com/members/joe/profile/edit/ (or a subpage thereof).
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's profile edit page.
+ */
+function bp_is_user_profile_edit() {
+	return (bool) ( bp_is_profile_component() && bp_is_current_action( 'edit' ) );
+}
+
+/**
+ * Is the current page part of a user's profile avatar editing section?
+ *
+ * Eg http://example.com/members/joe/profile/change-avatar/ (or a subpage thereof).
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is the user's avatar edit page.
+ */
+function bp_is_user_change_avatar() {
+	return (bool) ( bp_is_profile_component() && bp_is_current_action( 'change-avatar' ) );
+}
+
+/**
+ * Is the current page the a user's change cover image profile page?
+ *
+ * Eg http://example.com/members/joe/profile/change-cover-image/ (or a subpage thereof).
+ *
+ * @since 2.4.0
+ *
+ * @return bool True if the current page is a user's profile edit cover image page.
+ */
+function bp_is_user_change_cover_image() {
+	return (bool) ( bp_is_profile_component() && bp_is_current_action( 'change-cover-image' ) );
+}
+
+/**
+ * Is the current page part of a user's Groups page?
+ *
+ * Eg http://example.com/members/joe/groups/ (or a subpage thereof).
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Groups page.
+ */
+function bp_is_user_groups() {
+	return (bool) ( bp_is_user() && bp_is_groups_component() );
+}
+
+/**
+ * Is the current page part of a user's Blogs page?
+ *
+ * Eg http://example.com/members/joe/blogs/ (or a subpage thereof).
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Blogs page.
+ */
+function bp_is_user_blogs() {
+	return (bool) ( bp_is_user() && bp_is_blogs_component() );
+}
+
+/**
+ * Is the current page a user's Recent Blog Posts page?
+ *
+ * Eg http://example.com/members/joe/blogs/recent-posts/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Recent Blog Posts page.
+ */
+function bp_is_user_recent_posts() {
+	return (bool) ( bp_is_user_blogs() && bp_is_current_action( 'recent-posts' ) );
+}
+
+/**
+ * Is the current page a user's Recent Blog Comments page?
+ *
+ * Eg http://example.com/members/joe/blogs/recent-comments/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Recent Blog Comments page.
+ */
+function bp_is_user_recent_commments() {
+	return (bool) ( bp_is_user_blogs() && bp_is_current_action( 'recent-comments' ) );
+}
+
+/**
+ * Is the current page a user's Friends page?
+ *
+ * Eg http://example.com/members/joe/blogs/friends/ (or a subpage thereof).
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Friends page.
+ */
+function bp_is_user_friends() {
+	return (bool) ( bp_is_user() && bp_is_friends_component() );
+}
+
+/**
+ * Is the current page a user's Friend Requests page?
+ *
+ * Eg http://example.com/members/joe/friends/requests/.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's Friends Requests page.
+ */
+function bp_is_user_friend_requests() {
+	return (bool) ( bp_is_user_friends() && bp_is_current_action( 'requests' ) );
+}
+
+/**
+ * Is this a user's notifications page?
+ *
+ * Eg http://example.com/members/joe/notifications/ (or a subpage thereof).
+ *
+ * @since 1.9.0
+ *
+ * @return bool True if the current page is a user's Notifications page.
+ */
+function bp_is_user_notifications() {
+	return (bool) ( bp_is_user() && bp_is_notifications_component() );
+}
+
+/**
+ * Is this a user's settings page?
+ *
+ * Eg http://example.com/members/joe/settings/ (or a subpage thereof).
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's Settings page.
+ */
+function bp_is_user_settings() {
+	return (bool) ( bp_is_user() && bp_is_settings_component() );
+}
+
+/**
+ * Is this a user's General Settings page?
+ *
+ * Eg http://example.com/members/joe/settings/general/.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's General Settings page.
+ */
+function bp_is_user_settings_general() {
+	return (bool) ( bp_is_user_settings() && bp_is_current_action( 'general' ) );
+}
+
+/**
+ * Is this a user's Notification Settings page?
+ *
+ * Eg http://example.com/members/joe/settings/notifications/.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's Notification Settings page.
+ */
+function bp_is_user_settings_notifications() {
+	return (bool) ( bp_is_user_settings() && bp_is_current_action( 'notifications' ) );
+}
+
+/**
+ * Is this a user's Account Deletion page?
+ *
+ * Eg http://example.com/members/joe/settings/delete-account/.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if the current page is a user's Delete Account page.
+ */
+function bp_is_user_settings_account_delete() {
+	return (bool) ( bp_is_user_settings() && bp_is_current_action( 'delete-account' ) );
+}
+
+/**
+ * Is this a user's profile settings?
+ *
+ * Eg http://example.com/members/joe/settings/profile/.
+ *
+ * @since 2.0.0
+ *
+ * @return bool True if the current page is a user's Profile Settings page.
+ */
+function bp_is_user_settings_profile() {
+	return (bool) ( bp_is_user_settings() && bp_is_current_action( 'profile' ) );
+}
+
+/** Groups ********************************************************************/
+
+/**
+ * Is the current page the groups directory?
+ *
+ * @since 2.0.0
+ *
+ * @return bool True if the current page is the groups directory.
+ */
+function bp_is_groups_directory() {
+	if ( bp_is_groups_component() && ! bp_is_group() && ( ! bp_current_action() || ( bp_action_variable() && bp_is_current_action( bp_get_groups_group_type_base() ) ) ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Does the current page belong to a single group?
+ *
+ * Will return true for any subpage of a single group.
+ *
+ * @since 1.2.0
+ *
+ * @return bool True if the current page is part of a single group.
+ */
+function bp_is_group() {
+	$retval = bp_is_active( 'groups' );
+
+	if ( ! empty( $retval ) ) {
+		$retval = bp_is_groups_component() && groups_get_current_group();
+	}
+
+	return (bool) $retval;
+}
+
+/**
+ * Is the current page a single group's home page?
+ *
+ * URL will vary depending on which group tab is set to be the "home". By
+ * default, it's the group's recent activity.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a single group's home page.
+ */
+function bp_is_group_home() {
+	if ( bp_is_single_item() && bp_is_groups_component() && ( ! bp_current_action() || bp_is_current_action( 'home' ) ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Is the current page part of the group creation process?
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of the group creation process.
+ */
+function bp_is_group_create() {
+	return (bool) ( bp_is_groups_component() && bp_is_current_action( 'create' ) );
+}
+
+/**
+ * Is the current page part of a single group's admin screens?
+ *
+ * Eg http://example.com/groups/mygroup/admin/settings/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of a single group's admin.
+ */
+function bp_is_group_admin_page() {
+	return (bool) ( bp_is_single_item() && bp_is_groups_component() && bp_is_current_action( 'admin' ) );
+}
+
+/**
+ * Is the current page a group's activity page?
+ *
+ * @since 1.2.1
+ *
+ * @return bool True if the current page is a group's activity page.
+ */
+function bp_is_group_activity() {
+	$retval = false;
+
+	if ( bp_is_single_item() && bp_is_groups_component() && bp_is_current_action( 'activity' ) ) {
+		$retval = true;
+	}
+
+	if ( bp_is_group_home() && bp_is_active( 'activity' ) && ! bp_is_group_custom_front() ) {
+		$retval = true;
+	}
+
+	return $retval;
+}
+
+/**
+ * Is the current page a group forum topic?
+ *
+ * @since 1.1.0
+ * @since 3.0.0 Required for bbPress 2 integration.
+ *
+ * @return bool True if the current page is part of a group forum topic.
+ */
+function bp_is_group_forum_topic() {
+	return (bool) ( bp_is_single_item() && bp_is_groups_component() && bp_is_current_action( 'forum' ) && bp_is_action_variable( 'topic', 0 ) );
+}
+
+/**
+ * Is the current page a group forum topic edit page?
+ *
+ * @since 1.2.0
+ * @since 3.0.0 Required for bbPress 2 integration.
+ *
+ * @return bool True if the current page is part of a group forum topic edit page.
+ */
+function bp_is_group_forum_topic_edit() {
+	return (bool) ( bp_is_single_item() && bp_is_groups_component() && bp_is_current_action( 'forum' ) && bp_is_action_variable( 'topic', 0 ) && bp_is_action_variable( 'edit', 2 ) );
+}
+
+/**
+ * Is the current page a group's Members page?
+ *
+ * Eg http://example.com/groups/mygroup/members/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is part of a group's Members page.
+ */
+function bp_is_group_members() {
+	$retval = false;
+
+	if ( bp_is_single_item() && bp_is_groups_component() && bp_is_current_action( 'members' ) ) {
+		$retval = true;
+	}
+
+	if ( bp_is_group_home() && ! bp_is_active( 'activity' ) && ! bp_is_group_custom_front() ) {
+		$retval = true;
+	}
+
+	return $retval;
+}
+
+/**
+ * Is the current page a group's Invites page?
+ *
+ * Eg http://example.com/groups/mygroup/send-invites/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a group's Send Invites page.
+ */
+function bp_is_group_invites() {
+	return (bool) ( bp_is_groups_component() && bp_is_current_action( 'send-invites' ) );
+}
+
+/**
+ * Is the current page a group's Request Membership page?
+ *
+ * Eg http://example.com/groups/mygroup/request-membership/.
+ *
+ * @since 1.2.0
+ *
+ * @return bool True if the current page is a group's Request Membership page.
+ */
+function bp_is_group_membership_request() {
+	return (bool) ( bp_is_groups_component() && bp_is_current_action( 'request-membership' ) );
+}
+
+/**
+ * Is the current page a leave group attempt?
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a Leave Group attempt.
+ */
+function bp_is_group_leave() {
+	return (bool) ( bp_is_groups_component() && bp_is_single_item() && bp_is_current_action( 'leave-group' ) );
+}
+
+/**
+ * Is the current page part of a single group?
+ *
+ * Not currently used by BuddyPress.
+ *
+ * @todo How is this functionally different from bp_is_group()?
+ *
+ * @return bool True if the current page is part of a single group.
+ */
+function bp_is_group_single() {
+	return (bool) ( bp_is_groups_component() && bp_is_single_item() );
+}
+
+/**
+ * Is the current group page a custom front?
+ *
+ * @since 2.4.0
+ *
+ * @return bool True if the current group page is a custom front.
+ */
+function bp_is_group_custom_front() {
+	$bp = buddypress();
+	return (bool) bp_is_group_home() && ! empty( $bp->groups->current_group->front_template );
+}
+
+/**
+ * Is the current page the Create a Blog page?
+ *
+ * Eg http://example.com/sites/create/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is the Create a Blog page.
+ */
+function bp_is_create_blog() {
+	return (bool) ( bp_is_blogs_component() && bp_is_current_action( 'create' ) );
+}
+
+/**
+ * Is the current page the blogs directory ?
+ *
+ * @since 2.0.0
+ *
+ * @return bool True if the current page is the blogs directory.
+ */
+function bp_is_blogs_directory() {
+	if ( is_multisite() && bp_is_blogs_component() && ! bp_current_action() ) {
+		return true;
+	}
+
+	return false;
+}
+
+/** Messages ******************************************************************/
+
+/**
+ * Is the current page part of a user's Messages pages?
+ *
+ * Eg http://example.com/members/joe/messages/ (or a subpage thereof).
+ *
+ * @since 1.2.0
+ *
+ * @return bool True if the current page is part of a user's Messages pages.
+ */
+function bp_is_user_messages() {
+	return (bool) ( bp_is_user() && bp_is_messages_component() );
+}
+
+/**
+ * Is the current page a user's Messages Inbox?
+ *
+ * Eg http://example.com/members/joe/messages/inbox/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Messages Inbox.
+ */
+function bp_is_messages_inbox() {
+	if ( bp_is_user_messages() && ( ! bp_current_action() || bp_is_current_action( 'inbox' ) ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Is the current page a user's Messages Sentbox?
+ *
+ * Eg http://example.com/members/joe/messages/sentbox/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Messages Sentbox.
+ */
+function bp_is_messages_sentbox() {
+	return (bool) ( bp_is_user_messages() && bp_is_current_action( 'sentbox' ) );
+}
+
+/**
+ * Is the current page a user's Messages Compose screen??
+ *
+ * Eg http://example.com/members/joe/messages/compose/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is a user's Messages Compose screen.
+ */
+function bp_is_messages_compose_screen() {
+	return (bool) ( bp_is_user_messages() && bp_is_current_action( 'compose' ) );
+}
+
+/**
+ * Is the current page the Notices screen?
+ *
+ * Eg http://example.com/members/joe/messages/notices/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is the Notices screen.
+ */
+function bp_is_notices() {
+	return (bool) ( bp_is_user_messages() && bp_is_current_action( 'notices' ) );
+}
+
+/**
+ * Is the current page a single Messages conversation thread?
+ *
+ * @since 1.6.0
+ *
+ * @return bool True if the current page a single Messages conversation thread?
+ */
+function bp_is_messages_conversation() {
+	return (bool) ( bp_is_user_messages() && ( bp_is_current_action( 'view' ) ) );
+}
+
+/**
+ * Not currently used by BuddyPress.
+ *
+ * @param string $component Current component to check for.
+ * @param string $callback  Callback to invoke.
+ * @return bool
+ */
+function bp_is_single( $component, $callback ) {
+	return (bool) ( bp_is_current_component( $component ) && ( true === call_user_func( $callback ) ) );
+}
+
+/** Registration **************************************************************/
+
+/**
+ * Is the current page the Activate page?
+ *
+ * Eg http://example.com/activate/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is the Activate page.
+ */
+function bp_is_activation_page() {
+	return (bool) bp_is_current_component( 'activate' );
+}
+
+/**
+ * Is the current page the Register page?
+ *
+ * Eg http://example.com/register/.
+ *
+ * @since 1.1.0
+ *
+ * @return bool True if the current page is the Register page.
+ */
+function bp_is_register_page() {
+	return (bool) bp_is_current_component( 'register' );
+}
+
+/**
+ * Get the title parts of the BuddyPress displayed page
+ *
+ * @since 2.4.3
+ *
+ * @param string $seplocation Location for the separator.
+ * @return array the title parts
+ */
+function bp_get_title_parts( $seplocation = 'right' ) {
+	$bp = buddypress();
+
+	// Defaults to an empty array.
+	$bp_title_parts = array();
+
+	// If this is not a BP page, return the empty array.
+	if ( bp_is_blog_page() ) {
+		return $bp_title_parts;
+	}
+
+	// If this is a 404, return the empty array.
+	if ( is_404() ) {
+		return $bp_title_parts;
+	}
+
+	// If this is the front page of the site, return the empty array.
+	if ( is_front_page() || is_home() ) {
+		return $bp_title_parts;
+	}
+
+	// Return the empty array if not a BuddyPress page.
+	if ( ! is_buddypress() ) {
+		return $bp_title_parts;
+	}
+
+	// Now we can build the BP Title Parts
+	// Is there a displayed user, and do they have a name?
+	$displayed_user_name = bp_get_displayed_user_fullname();
+
+	// Displayed user.
+	if ( ! empty( $displayed_user_name ) && ! is_404() ) {
+
+		// Get the component's ID to try and get its name.
+		$component_id = $component_name = bp_current_component();
+
+		// Set empty subnav name.
+		$component_subnav_name = '';
+
+		if ( ! empty( $bp->members->nav ) ) {
+			$primary_nav_item = $bp->members->nav->get_primary( array( 'slug' => $component_id ), false );
+			$primary_nav_item = reset( $primary_nav_item );
+		}
+
+		// Use the component nav name.
+		if ( ! empty( $primary_nav_item->name ) ) {
+			$component_name = _bp_strip_spans_from_title( $primary_nav_item->name );
+
+		// Fall back on the component ID.
+		} elseif ( ! empty( $bp->{$component_id}->id ) ) {
+			$component_name = ucwords( $bp->{$component_id}->id );
+		}
+
+		if ( ! empty( $bp->members->nav ) ) {
+			$secondary_nav_item = $bp->members->nav->get_secondary( array(
+				'parent_slug' => $component_id,
+				'slug'        => bp_current_action()
+			), false );
+
+			if ( $secondary_nav_item ) {
+				$secondary_nav_item = reset( $secondary_nav_item );
+			}
+		}
+
+		// Append action name if we're on a member component sub-page.
+		if ( ! empty( $secondary_nav_item->name ) && ! empty( $bp->canonical_stack['action'] ) ) {
+			$component_subnav_name = $secondary_nav_item->name;
+		}
+
+		// If on the user profile's landing page, just use the fullname.
+		if ( bp_is_current_component( $bp->default_component ) && ( bp_get_requested_url() === bp_displayed_user_domain() ) ) {
+			$bp_title_parts[] = $displayed_user_name;
+
+		// Use component name on member pages.
+		} else {
+			$bp_title_parts = array_merge( $bp_title_parts, array_map( 'strip_tags', array(
+				$displayed_user_name,
+				$component_name,
+			) ) );
+
+			// If we have a subnav name, add it separately for localization.
+			if ( ! empty( $component_subnav_name ) ) {
+				$bp_title_parts[] = strip_tags( $component_subnav_name );
+			}
+		}
+
+	// A single item from a component other than Members.
+	} elseif ( bp_is_single_item() ) {
+		$component_id = bp_current_component();
+
+		if ( ! empty( $bp->{$component_id}->nav ) ) {
+			$secondary_nav_item = $bp->{$component_id}->nav->get_secondary( array(
+				'parent_slug' => bp_current_item(),
+				'slug'        => bp_current_action()
+			), false );
+
+			if ( $secondary_nav_item ) {
+				$secondary_nav_item = reset( $secondary_nav_item );
+			}
+		}
+
+		$single_item_subnav = '';
+
+		if ( ! empty( $secondary_nav_item->name ) ) {
+			$single_item_subnav = $secondary_nav_item->name;
+		}
+
+		$bp_title_parts = array( $bp->bp_options_title, $single_item_subnav );
+
+	// An index or directory.
+	} elseif ( bp_is_directory() ) {
+		$current_component = bp_current_component();
+
+		// No current component (when does this happen?).
+		$bp_title_parts = array( _x( 'Directory', 'component directory title', 'buddypress' ) );
+
+		if ( ! empty( $current_component ) ) {
+			$bp_title_parts = array( bp_get_directory_title( $current_component ) );
+		}
+
+	// Sign up page.
+	} elseif ( bp_is_register_page() ) {
+		$bp_title_parts = array( __( 'Create an Account', 'buddypress' ) );
+
+	// Activation page.
+	} elseif ( bp_is_activation_page() ) {
+		$bp_title_parts = array( __( 'Activate Your Account', 'buddypress' ) );
+
+	// Group creation page.
+	} elseif ( bp_is_group_create() ) {
+		$bp_title_parts = array( __( 'Create a Group', 'buddypress' ) );
+
+	// Blog creation page.
+	} elseif ( bp_is_create_blog() ) {
+		$bp_title_parts = array( __( 'Create a Site', 'buddypress' ) );
+	}
+
+	// Strip spans.
+	$bp_title_parts = array_map( '_bp_strip_spans_from_title', $bp_title_parts );
+
+	// Sep on right, so reverse the order.
+	if ( 'right' === $seplocation ) {
+		$bp_title_parts = array_reverse( $bp_title_parts );
+	}
+
+	/**
+	 * Filter BuddyPress title parts before joining.
+	 *
+	 * @since 2.4.3
+	 *
+	 * @param array $bp_title_parts Current BuddyPress title parts.
+	 * @return array
+	 */
+	return (array) apply_filters( 'bp_get_title_parts', $bp_title_parts );
+}
+
+/**
+ * Customize the body class, according to the currently displayed BP content.
+ *
+ * @since 1.1.0
+ */
+function bp_the_body_class() {
+	echo bp_get_the_body_class();
+}
+	/**
+	 * Customize the body class, according to the currently displayed BP content.
+	 *
+	 * Uses the above is_() functions to output a body class for each scenario.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array      $wp_classes     The body classes coming from WP.
+	 * @param array|bool $custom_classes Classes that were passed to get_body_class().
+	 * @return array $classes The BP-adjusted body classes.
+	 */
+	function bp_get_the_body_class( $wp_classes = array(), $custom_classes = false ) {
+
+		$bp_classes = array();
+
+		/* Pages *************************************************************/
+
+		if ( is_front_page() ) {
+			$bp_classes[] = 'home-page';
+		}
+
+		if ( bp_is_directory() ) {
+			$bp_classes[] = 'directory';
+		}
+
+		if ( bp_is_single_item() ) {
+			$bp_classes[] = 'single-item';
+		}
+
+		/* Components ********************************************************/
+
+		if ( ! bp_is_blog_page() ) {
+			if ( bp_is_user_profile() )  {
+				$bp_classes[] = 'xprofile';
+			}
+
+			if ( bp_is_activity_component() ) {
+				$bp_classes[] = 'activity';
+			}
+
+			if ( bp_is_blogs_component() ) {
+				$bp_classes[] = 'blogs';
+			}
+
+			if ( bp_is_messages_component() ) {
+				$bp_classes[] = 'messages';
+			}
+
+			if ( bp_is_friends_component() ) {
+				$bp_classes[] = 'friends';
+			}
+
+			if ( bp_is_groups_component() ) {
+				$bp_classes[] = 'groups';
+			}
+
+			if ( bp_is_settings_component()  ) {
+				$bp_classes[] = 'settings';
+			}
+		}
+
+		/* User **************************************************************/
+
+		if ( bp_is_user() ) {
+			$bp_classes[] = 'bp-user';
+
+			// Add current user member types.
+			if ( $member_types = bp_get_member_type( bp_displayed_user_id(), false ) ) {
+				foreach( $member_types as $member_type ) {
+					$bp_classes[] = sprintf( 'member-type-%s', esc_attr( $member_type ) );
+				}
+			}
+		}
+
+		if ( ! bp_is_directory() ) {
+			if ( bp_is_user_blogs() ) {
+				$bp_classes[] = 'my-blogs';
+			}
+
+			if ( bp_is_user_groups() ) {
+				$bp_classes[] = 'my-groups';
+			}
+
+			if ( bp_is_user_activity() ) {
+				$bp_classes[] = 'my-activity';
+			}
+		} else {
+			if ( bp_get_current_member_type() || ( bp_is_groups_directory() && bp_get_current_group_directory_type() ) ) {
+				$bp_classes[] = 'type';
+			}
+		}
+
+		if ( bp_is_my_profile() ) {
+			$bp_classes[] = 'my-account';
+		}
+
+		if ( bp_is_user_profile() ) {
+			$bp_classes[] = 'my-profile';
+		}
+
+		if ( bp_is_user_friends() ) {
+			$bp_classes[] = 'my-friends';
+		}
+
+		if ( bp_is_user_messages() ) {
+			$bp_classes[] = 'my-messages';
+		}
+
+		if ( bp_is_user_recent_commments() ) {
+			$bp_classes[] = 'recent-comments';
+		}
+
+		if ( bp_is_user_recent_posts() ) {
+			$bp_classes[] = 'recent-posts';
+		}
+
+		if ( bp_is_user_change_avatar() ) {
+			$bp_classes[] = 'change-avatar';
+		}
+
+		if ( bp_is_user_profile_edit() ) {
+			$bp_classes[] = 'profile-edit';
+		}
+
+		if ( bp_is_user_friends_activity() ) {
+			$bp_classes[] = 'friends-activity';
+		}
+
+		if ( bp_is_user_groups_activity() ) {
+			$bp_classes[] = 'groups-activity';
+		}
+
+		/* Messages **********************************************************/
+
+		if ( bp_is_messages_inbox() ) {
+			$bp_classes[] = 'inbox';
+		}
+
+		if ( bp_is_messages_sentbox() ) {
+			$bp_classes[] = 'sentbox';
+		}
+
+		if ( bp_is_messages_compose_screen() ) {
+			$bp_classes[] = 'compose';
+		}
+
+		if ( bp_is_notices() ) {
+			$bp_classes[] = 'notices';
+		}
+
+		if ( bp_is_user_friend_requests() ) {
+			$bp_classes[] = 'friend-requests';
+		}
+
+		if ( bp_is_create_blog() ) {
+			$bp_classes[] = 'create-blog';
+		}
+
+		/* Groups ************************************************************/
+
+		if ( bp_is_group() ) {
+			$bp_classes[] = 'group-' . groups_get_current_group()->slug;
+
+			// Add current group types.
+			if ( $group_types = bp_groups_get_group_type( bp_get_current_group_id(), false ) ) {
+				foreach ( $group_types as $group_type ) {
+					$bp_classes[] = sprintf( 'group-type-%s', esc_attr( $group_type ) );
+				}
+			}
+		}
+
+		if ( bp_is_group_leave() ) {
+			$bp_classes[] = 'leave-group';
+		}
+
+		if ( bp_is_group_invites() ) {
+			$bp_classes[] = 'group-invites';
+		}
+
+		if ( bp_is_group_members() ) {
+			$bp_classes[] = 'group-members';
+		}
+
+		if ( bp_is_group_admin_page() ) {
+			$bp_classes[] = 'group-admin';
+			$bp_classes[] = bp_get_group_current_admin_tab();
+		}
+
+		if ( bp_is_group_create() ) {
+			$bp_classes[] = 'group-create';
+			$bp_classes[] = bp_get_groups_current_create_step();
+		}
+
+		if ( bp_is_group_home() ) {
+			$bp_classes[] = 'group-home';
+		}
+
+		if ( bp_is_single_activity() ) {
+			$bp_classes[] = 'activity-permalink';
+		}
+
+		/* Registration ******************************************************/
+
+		if ( bp_is_register_page() ) {
+			$bp_classes[] = 'registration';
+		}
+
+		if ( bp_is_activation_page() ) {
+			$bp_classes[] = 'activation';
+		}
+
+		/* Current Component & Action ****************************************/
+
+		if ( ! bp_is_blog_page() ) {
+			$bp_classes[] = bp_current_component();
+			$bp_classes[] = bp_current_action();
+		}
+
+		/* Clean up ***********************************************************/
+
+		// Add BuddyPress class if we are within a BuddyPress page.
+		if ( ! bp_is_blog_page() ) {
+			$bp_classes[] = 'buddypress';
+		}
+
+		// Add the theme name/id to the body classes
+		$bp_classes[] = 'bp-' . bp_get_theme_compat_id();
+
+		// Merge WP classes with BuddyPress classes and remove any duplicates.
+		$classes = array_unique( array_merge( (array) $bp_classes, (array) $wp_classes ) );
+
+		/**
+		 * Filters the BuddyPress classes to be added to body_class()
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param array $classes        Array of body classes to add.
+		 * @param array $bp_classes     Array of BuddyPress-based classes.
+		 * @param array $wp_classes     Array of WordPress-based classes.
+		 * @param array $custom_classes Array of classes that were passed to get_body_class().
+		 */
+		return apply_filters( 'bp_get_the_body_class', $classes, $bp_classes, $wp_classes, $custom_classes );
+	}
+	add_filter( 'body_class', 'bp_get_the_body_class', 10, 2 );
+
+/**
+ * Customizes the post CSS class according to BuddyPress content.
+ *
+ * Hooked to the 'post_class' filter.
+ *
+ * @since 2.1.0
+ *
+ * @param array $wp_classes The post classes coming from WordPress.
+ * @return array
+ */
+function bp_get_the_post_class( $wp_classes = array() ) {
+	// Don't do anything if we're not on a BP page.
+	if ( ! is_buddypress() ) {
+		return $wp_classes;
+	}
+
+	$bp_classes = array();
+
+	if ( bp_is_user() || bp_is_single_activity() ) {
+		$bp_classes[] = 'bp_members';
+
+	} elseif ( bp_is_group() ) {
+		$bp_classes[] = 'bp_group';
+
+	} elseif ( bp_is_activity_component() ) {
+		$bp_classes[] = 'bp_activity';
+
+	} elseif ( bp_is_blogs_component() ) {
+		$bp_classes[] = 'bp_blogs';
+
+	} elseif ( bp_is_register_page() ) {
+		$bp_classes[] = 'bp_register';
+
+	} elseif ( bp_is_activation_page() ) {
+		$bp_classes[] = 'bp_activate';
+	}
+
+	if ( empty( $bp_classes ) ) {
+		return $wp_classes;
+	}
+
+	// Emulate post type css class.
+	foreach ( $bp_classes as $bp_class ) {
+		$bp_classes[] = "type-{$bp_class}";
+	}
+
+	// Okay let's merge!
+	return array_unique( array_merge( $bp_classes, $wp_classes ) );
+}
+add_filter( 'post_class', 'bp_get_the_post_class' );
+
+/**
+ * Sort BuddyPress nav menu items by their position property.
+ *
+ * This is an internal convenience function and it will probably be removed in
+ * a later release. Do not use.
+ *
+ * @access private
+ * @since 1.7.0
+ *
+ * @param array $a First item.
+ * @param array $b Second item.
+ * @return int Returns an integer less than, equal to, or greater than zero if
+ *             the first argument is considered to be respectively less than,
+ *             equal to, or greater than the second.
+ */
+function _bp_nav_menu_sort( $a, $b ) {
+	if ( $a['position'] == $b['position'] ) {
+		return 0;
+	} elseif ( $a['position'] < $b['position'] ) {
+		return -1;
+	} else {
+		return 1;
+	}
+}
+
+/**
+ * Get the items registered in the primary and secondary BuddyPress navigation menus.
+ *
+ * @since 1.7.0
+ * @since 2.6.0 Introduced the `$component` parameter.
+ *
+ * @param string $component Optional. Component whose nav items are being fetched.
+ * @return array A multidimensional array of all navigation items.
+ */
+function bp_get_nav_menu_items( $component = 'members' ) {
+	$bp    = buddypress();
+	$menus = array();
+
+	if ( ! isset( $bp->{$component}->nav ) ) {
+		return $menus;
+	}
+
+	// Get the item nav and build the menus.
+	foreach ( $bp->{$component}->nav->get_item_nav() as $nav_menu ) {
+		// Get the correct menu link. See https://buddypress.trac.wordpress.org/ticket/4624.
+		$link = bp_loggedin_user_domain() ? str_replace( bp_loggedin_user_domain(), bp_displayed_user_domain(), $nav_menu->link ) : trailingslashit( bp_displayed_user_domain() . $nav_menu->link );
+
+		// Add this menu.
+		$menu         = new stdClass;
+		$menu->class  = array( 'menu-parent' );
+		$menu->css_id = $nav_menu->css_id;
+		$menu->link   = $link;
+		$menu->name   = $nav_menu->name;
+		$menu->parent = 0;
+
+		if ( ! empty( $nav_menu->children ) ) {
+			$submenus = array();
+
+			foreach( $nav_menu->children as $sub_menu ) {
+				$submenu = new stdClass;
+				$submenu->class  = array( 'menu-child' );
+				$submenu->css_id = $sub_menu->css_id;
+				$submenu->link   = $sub_menu->link;
+				$submenu->name   = $sub_menu->name;
+				$submenu->parent = $nav_menu->slug;
+
+				// If we're viewing this item's screen, record that we need to mark its parent menu to be selected.
+				if ( bp_is_current_action( $sub_menu->slug ) && bp_is_current_component( $nav_menu->slug ) ) {
+					$menu->class[]    = 'current-menu-parent';
+					$submenu->class[] = 'current-menu-item';
+				}
+
+				$submenus[] = $submenu;
+			}
+		}
+
+		$menus[] = $menu;
+
+		if ( ! empty( $submenus ) ) {
+			$menus = array_merge( $menus, $submenus );
+		}
+	}
+
+	/**
+	 * Filters the items registered in the primary and secondary BuddyPress navigation menus.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $menus Array of items registered in the primary and secondary BuddyPress navigation.
+	 */
+	return apply_filters( 'bp_get_nav_menu_items', $menus );
+}
+
+/**
+ * Display a navigation menu.
+ *
+ * @since 1.7.0
+ *
+ * @param string|array $args {
+ *     An array of optional arguments.
+ *
+ *     @type string $after           Text after the link text. Default: ''.
+ *     @type string $before          Text before the link text. Default: ''.
+ *     @type string $container       The name of the element to wrap the navigation
+ *                                   with. 'div' or 'nav'. Default: 'div'.
+ *     @type string $container_class The class that is applied to the container.
+ *                                   Default: 'menu-bp-container'.
+ *     @type string $container_id    The ID that is applied to the container.
+ *                                   Default: ''.
+ *     @type int    $depth           How many levels of the hierarchy are to be included.
+ *                                   0 means all. Default: 0.
+ *     @type bool   $echo            True to echo the menu, false to return it.
+ *                                   Default: true.
+ *     @type bool   $fallback_cb     If the menu doesn't exist, should a callback
+ *                                   function be fired? Default: false (no fallback).
+ *     @type string $items_wrap      How the list items should be wrapped. Should be
+ *                                   in the form of a printf()-friendly string, using numbered
+ *                                   placeholders. Default: '<ul id="%1$s" class="%2$s">%3$s</ul>'.
+ *     @type string $link_after      Text after the link. Default: ''.
+ *     @type string $link_before     Text before the link. Default: ''.
+ *     @type string $menu_class      CSS class to use for the <ul> element which
+ *                                   forms the menu. Default: 'menu'.
+ *     @type string $menu_id         The ID that is applied to the <ul> element which
+ *                                   forms the menu. Default: 'menu-bp', incremented.
+ *     @type string $walker          Allows a custom walker class to be specified.
+ *                                   Default: 'BP_Walker_Nav_Menu'.
+ * }
+ * @return string|null If $echo is false, returns a string containing the nav
+ *                     menu markup.
+ */
+function bp_nav_menu( $args = array() ) {
+	static $menu_id_slugs = array();
+
+	$defaults = array(
+		'after'           => '',
+		'before'          => '',
+		'container'       => 'div',
+		'container_class' => '',
+		'container_id'    => '',
+		'depth'           => 0,
+		'echo'            => true,
+		'fallback_cb'     => false,
+		'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
+		'link_after'      => '',
+		'link_before'     => '',
+		'menu_class'      => 'menu',
+		'menu_id'         => '',
+		'walker'          => '',
+	);
+	$args = wp_parse_args( $args, $defaults );
+
+	/**
+	 * Filters the parsed bp_nav_menu arguments.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $args Array of parsed arguments.
+	 */
+	$args = apply_filters( 'bp_nav_menu_args', $args );
+	$args = (object) $args;
+
+	$items = $nav_menu = '';
+	$show_container = false;
+
+	// Create custom walker if one wasn't set.
+	if ( empty( $args->walker ) ) {
+		$args->walker = new BP_Walker_Nav_Menu;
+	}
+
+	// Sanitise values for class and ID.
+	$args->container_class = sanitize_html_class( $args->container_class );
+	$args->container_id    = sanitize_html_class( $args->container_id );
+
+	// Whether to wrap the ul, and what to wrap it with.
+	if ( $args->container ) {
+
+		/**
+		 * Filters the allowed tags for the wp_nav_menu_container.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array $value Array of allowed tags. Default 'div' and 'nav'.
+		 */
+		$allowed_tags = apply_filters( 'wp_nav_menu_container_allowedtags', array( 'div', 'nav', ) );
+
+		if ( in_array( $args->container, $allowed_tags ) ) {
+			$show_container = true;
+
+			$class     = $args->container_class ? ' class="' . esc_attr( $args->container_class ) . '"' : ' class="menu-bp-container"';
+			$id        = $args->container_id    ? ' id="' . esc_attr( $args->container_id ) . '"'       : '';
+			$nav_menu .= '<' . $args->container . $id . $class . '>';
+		}
+	}
+
+	/**
+	 * Filters the BuddyPress menu objects.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $value Array of nav menu objects.
+	 * @param array $args  Array of arguments for the menu.
+	 */
+	$menu_items = apply_filters( 'bp_nav_menu_objects', bp_get_nav_menu_items(), $args );
+	$items      = walk_nav_menu_tree( $menu_items, $args->depth, $args );
+	unset( $menu_items );
+
+	// Set the ID that is applied to the ul element which forms the menu.
+	if ( ! empty( $args->menu_id ) ) {
+		$wrap_id = $args->menu_id;
+
+	} else {
+		$wrap_id = 'menu-bp';
+
+		// If a specific ID wasn't requested, and there are multiple menus on the same screen, make sure the autogenerated ID is unique.
+		while ( in_array( $wrap_id, $menu_id_slugs ) ) {
+			if ( preg_match( '#-(\d+)$#', $wrap_id, $matches ) ) {
+				$wrap_id = preg_replace('#-(\d+)$#', '-' . ++$matches[1], $wrap_id );
+			} else {
+				$wrap_id = $wrap_id . '-1';
+			}
+		}
+	}
+	$menu_id_slugs[] = $wrap_id;
+
+	/**
+	 * Filters the BuddyPress menu items.
+	 *
+	 * Allow plugins to hook into the menu to add their own <li>'s
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $items Array of nav menu items.
+	 * @param array $args  Array of arguments for the menu.
+	 */
+	$items = apply_filters( 'bp_nav_menu_items', $items, $args );
+
+	// Build the output.
+	$wrap_class  = $args->menu_class ? $args->menu_class : '';
+	$nav_menu   .= sprintf( $args->items_wrap, esc_attr( $wrap_id ), esc_attr( $wrap_class ), $items );
+	unset( $items );
+
+	// If we've wrapped the ul, close it.
+	if ( ! empty( $show_container ) ) {
+		$nav_menu .= '</' . $args->container . '>';
+	}
+
+	/**
+	 * Filters the final BuddyPress menu output.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $nav_menu Final nav menu output.
+	 * @param array  $args     Array of arguments for the menu.
+	 */
+	$nav_menu = apply_filters( 'bp_nav_menu', $nav_menu, $args );
+
+	if ( ! empty( $args->echo ) ) {
+		echo $nav_menu;
+	} else {
+		return $nav_menu;
+	}
+}
+
+/**
+ * Prints the Recipient Salutation.
+ *
+ * @since 2.5.0
+ *
+ * @param array $settings Email Settings.
+ */
+function bp_email_the_salutation( $settings = array() ) {
+	echo bp_email_get_salutation( $settings );
+}
+
+	/**
+	 * Gets the Recipient Salutation.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param array $settings Email Settings.
+	 * @return string The Recipient Salutation.
+	 */
+	function bp_email_get_salutation( $settings = array() ) {
+		$token = '{{recipient.name}}';
+
+		/**
+		 * Filters The Recipient Salutation inside the Email Template.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $value    The Recipient Salutation.
+		 * @param array  $settings Email Settings.
+		 * @param string $token    The Recipient token.
+		 */
+		return apply_filters( 'bp_email_get_salutation', sprintf( _x( 'Hi %s,', 'recipient salutation', 'buddypress' ), $token ), $settings, $token );
+	}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-theme-compatibility.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-theme-compatibility.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-theme-compatibility.php	(revision 12265)
@@ -0,0 +1,1012 @@
+<?php
+/**
+ * BuddyPress Core Theme Compatibility.
+ *
+ * @package BuddyPress
+ * @subpackage ThemeCompatibility
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Theme Compat **************************************************************/
+
+/**
+ * What follows is an attempt at intercepting the natural page load process
+ * to replace the_content() with the appropriate BuddyPress content.
+ *
+ * To do this, BuddyPress does several direct manipulations of global variables
+ * and forces them to do what they are not supposed to be doing.
+ *
+ * Don't try anything you're about to witness here, at home. Ever.
+ */
+
+/** Functions *****************************************************************/
+
+/**
+ * Set up the default theme compat theme.
+ *
+ * @since 1.7.0
+ *
+ * @param string $theme Optional. The unique ID identifier of a theme package.
+ */
+function bp_setup_theme_compat( $theme = '' ) {
+	$bp = buddypress();
+
+	// Make sure theme package is available, set to default if not.
+	if ( ! isset( $bp->theme_compat->packages[$theme] ) || ! is_a( $bp->theme_compat->packages[$theme], 'BP_Theme_Compat' ) ) {
+		$theme = 'legacy';
+	}
+
+	// Set the active theme compat theme.
+	$bp->theme_compat->theme = $bp->theme_compat->packages[$theme];
+}
+
+/**
+ * Get the ID of the theme package being used.
+ *
+ * This can be filtered or set manually. Tricky theme authors can override the
+ * default and include their own BuddyPress compatibility layers for their themes.
+ *
+ * @since 1.7.0
+ *
+ * @return string ID of the theme package in use.
+ */
+function bp_get_theme_compat_id() {
+
+	/**
+	 * Filters the ID of the theme package being used.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $id ID of the theme package in use.
+	 */
+	return apply_filters( 'bp_get_theme_compat_id', buddypress()->theme_compat->theme->id );
+}
+
+/**
+ * Get the name of the theme package being used.
+ *
+ * This can be filtered or set manually. Tricky theme authors can override the
+ * default and include their own BuddyPress compatibility layers for their themes.
+ *
+ * @since 1.7.0
+ *
+ * @return string Name of the theme package currently in use.
+ */
+function bp_get_theme_compat_name() {
+
+	/**
+	 * Filters the name of the theme package being used.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $name Name of the theme package in use.
+	 */
+	return apply_filters( 'bp_get_theme_compat_name', buddypress()->theme_compat->theme->name );
+}
+
+/**
+ * Get the version of the theme package being used.
+ *
+ * This can be filtered or set manually. Tricky theme authors can override the
+ * default and include their own BuddyPress compatibility layers for their themes.
+ *
+ * @since 1.7.0
+ *
+ * @return string The version string of the theme package currently in use.
+ */
+function bp_get_theme_compat_version() {
+
+	/**
+	 * Filters the version of the theme package being used.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $version The version string of the theme package in use.
+	 */
+	return apply_filters( 'bp_get_theme_compat_version', buddypress()->theme_compat->theme->version );
+}
+
+/**
+ * Get the absolute path of the theme package being used.
+ *
+ * Or set manually. Tricky theme authors can override the default and include
+ * their own BuddyPress compatibility layers for their themes.
+ *
+ * @since 1.7.0
+ *
+ * @return string The absolute path of the theme package currently in use.
+ */
+function bp_get_theme_compat_dir() {
+
+	/**
+	 * Filters the absolute path of the theme package being used.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $dir The absolute path of the theme package in use.
+	 */
+	return apply_filters( 'bp_get_theme_compat_dir', buddypress()->theme_compat->theme->dir );
+}
+
+/**
+ * Get the URL of the theme package being used.
+ *
+ * This can be filtered, or set manually. Tricky theme authors can override
+ * the default and include their own BuddyPress compatibility layers for their
+ * themes.
+ *
+ * @since 1.7.0
+ *
+ * @return string URL of the theme package currently in use.
+ */
+function bp_get_theme_compat_url() {
+
+	/**
+	 * Filters the URL of the theme package being used.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $url URL of the theme package in use.
+	 */
+	return apply_filters( 'bp_get_theme_compat_url', buddypress()->theme_compat->theme->url );
+}
+
+/**
+ * Should we use theme compat for this theme?
+ *
+ * If the current theme's need for theme compat hasn't yet been detected, we
+ * do so using bp_detect_theme_compat_with_current_theme().
+ *
+ * @since 1.9.0
+ *
+ * @return bool True if the current theme needs theme compatibility.
+ */
+function bp_use_theme_compat_with_current_theme() {
+	if ( ! isset( buddypress()->theme_compat->use_with_current_theme ) ) {
+		bp_detect_theme_compat_with_current_theme();
+	}
+
+	/**
+	 * Filters whether or not to use theme compat for the active theme.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param bool $use_with_current_theme True if the current theme needs theme compatibility.
+	 */
+	return apply_filters( 'bp_use_theme_compat_with_current_theme', buddypress()->theme_compat->use_with_current_theme );
+}
+
+/**
+ * Set our flag to determine whether theme compat should be enabled.
+ *
+ * Theme compat is disabled when a theme meets one of the following criteria:
+ * 1) It declares BP support with add_theme_support( 'buddypress' )
+ * 2) It is bp-default, or a child theme of bp-default
+ * 3) A legacy template is found at members/members-loop.php. This is a
+ *    fallback check for themes that were derived from bp-default, and have
+ *    not been updated for BP 1.7+; we make the assumption that any theme in
+ *    this category will have the members-loop.php template, and so use its
+ *    presence as an indicator that theme compatibility is not required
+ *
+ * @since 1.9.0
+ *
+ * @return bool True if the current theme needs theme compatibility.
+ */
+function bp_detect_theme_compat_with_current_theme() {
+	if ( isset( buddypress()->theme_compat->use_with_current_theme ) ) {
+		return buddypress()->theme_compat->use_with_current_theme;
+	}
+
+	// Theme compat enabled by default.
+	$theme_compat = true;
+
+	// If the theme supports 'buddypress', bail.
+	if ( current_theme_supports( 'buddypress' ) ) {
+		$theme_compat = false;
+
+	// If the theme doesn't support BP, do some additional checks.
+	} else {
+		// Bail if theme is a derivative of bp-default.
+		if ( in_array( 'bp-default', array( get_template(), get_stylesheet() ) ) ) {
+			$theme_compat = false;
+
+		// Brute-force check for a BP template.
+		// Examples are clones of bp-default.
+		} elseif ( locate_template( 'members/members-loop.php', false, false ) ) {
+			$theme_compat = false;
+		}
+	}
+
+	// Set a flag in the buddypress() singleton so we don't have to run this again.
+	buddypress()->theme_compat->use_with_current_theme = $theme_compat;
+
+	return $theme_compat;
+}
+
+/**
+ * Is the current page using theme compatibility?
+ *
+ * @since 1.7.0
+ *
+ * @return bool True if the current page uses theme compatibility.
+ */
+function bp_is_theme_compat_active() {
+	$bp = buddypress();
+
+	if ( empty( $bp->theme_compat->active ) ) {
+		return false;
+	}
+
+	return $bp->theme_compat->active;
+}
+
+/**
+ * Set the flag that tells whether the current page is using theme compatibility.
+ *
+ * @since 1.7.0
+ *
+ * @param bool $set True to set the flag to true, false to set it to false.
+ * @return bool Returns the value of $set.
+ */
+function bp_set_theme_compat_active( $set = true ) {
+	buddypress()->theme_compat->active = $set;
+
+	return (bool) buddypress()->theme_compat->active;
+}
+
+/**
+ * Set the theme compat templates global.
+ *
+ * Stash possible template files for the current query. Useful if plugins want
+ * to override them, or see what files are being scanned for inclusion.
+ *
+ * @since 1.7.0
+ *
+ * @param array $templates The template stack.
+ * @return array The template stack (value of $templates).
+ */
+function bp_set_theme_compat_templates( $templates = array() ) {
+	buddypress()->theme_compat->templates = $templates;
+
+	return buddypress()->theme_compat->templates;
+}
+
+/**
+ * Set the theme compat template global.
+ *
+ * Stash the template file for the current query. Useful if plugins want
+ * to override it, or see what file is being included.
+ *
+ * @since 1.7.0
+ *
+ * @param string $template The template currently in use.
+ * @return string The template currently in use (value of $template).
+ */
+function bp_set_theme_compat_template( $template = '' ) {
+	buddypress()->theme_compat->template = $template;
+
+	return buddypress()->theme_compat->template;
+}
+
+/**
+ * Set the theme compat original_template global.
+ *
+ * Stash the original template file for the current query. Useful for checking
+ * if BuddyPress was able to find a more appropriate template.
+ *
+ * @since 1.7.0
+ *
+ * @param string $template The template originally selected by WP.
+ * @return string The template originally selected by WP (value of $template).
+ */
+function bp_set_theme_compat_original_template( $template = '' ) {
+	buddypress()->theme_compat->original_template = $template;
+
+	return buddypress()->theme_compat->original_template;
+}
+
+/**
+ * Set a theme compat feature
+ *
+ * @since 2.4.0
+ *
+ * @param string $theme_id The theme id (eg: legacy).
+ * @param array  $feature  An associative array (eg: array( name => 'feature_name', 'settings' => array() )).
+ */
+function bp_set_theme_compat_feature( $theme_id, $feature = array() ) {
+	if ( empty( $theme_id ) || empty( $feature['name'] ) ) {
+		return;
+	}
+
+	// Get BuddyPress instance.
+	$bp = buddypress();
+
+	// Get current theme compat theme.
+	$theme_compat_theme = $bp->theme_compat->theme;
+
+	// Bail if the Theme Compat theme is not in use.
+	if ( $theme_id !== bp_get_theme_compat_id() ) {
+		return;
+	}
+
+	$features = $theme_compat_theme->__get( 'features' );
+	if ( empty( $features ) ) {
+		$features = array();
+	}
+
+	// Bail if the feature is already registered or no settings were provided.
+	if ( isset( $features[ $feature['name'] ] ) || empty( $feature['settings'] ) ) {
+		return;
+	}
+
+	// Add the feature.
+	$features[ $feature['name'] ] = (object) $feature['settings'];
+
+	// The feature is attached to components.
+	if ( isset( $features[ $feature['name'] ]->components ) ) {
+		// Set the feature for each concerned component.
+		foreach ( (array) $features[ $feature['name'] ]->components as $component ) {
+			// The xProfile component is specific.
+			if ( 'xprofile' === $component ) {
+				$component = 'profile';
+			}
+
+			if ( isset( $bp->{$component} ) ) {
+				if ( isset( $bp->{$component}->features ) ) {
+					$bp->{$component}->features[] = $feature['name'];
+				} else {
+					$bp->{$component}->features = array( $feature['name'] );
+				}
+			}
+		}
+	}
+
+	// Finally update the theme compat features.
+	$theme_compat_theme->__set( 'features', $features );
+}
+
+/**
+ * Get a theme compat feature
+ *
+ * @since 2.4.0
+ *
+ * @param string $feature The feature (eg: cover_image).
+ * @return object The feature settings.
+ */
+function bp_get_theme_compat_feature( $feature = '' ) {
+	// Get current theme compat theme.
+	$theme_compat_theme = buddypress()->theme_compat->theme;
+
+	// Get features.
+	$features = $theme_compat_theme->__get( 'features' );
+
+	if ( ! isset( $features[ $feature ] ) ) {
+		return false;
+	}
+
+	return $features[ $feature ];
+}
+
+/**
+ * Setup the theme's features.
+ *
+ * Note: BP Legacy's buddypress-functions.php is not loaded in WP Administration
+ * as it's loaded using bp_locate_template(). That's why this function is here.
+ *
+ * @since 2.4.0
+ *
+ * @global string $content_width the content width of the theme
+ */
+function bp_register_theme_compat_default_features() {
+	global $content_width;
+
+	// Do not set up default features on deactivation.
+	if ( bp_is_deactivation() ) {
+		return;
+	}
+
+	// If the current theme doesn't need theme compat, bail at this point.
+	if ( ! bp_use_theme_compat_with_current_theme() ) {
+		return;
+	}
+
+	// Make sure BP Legacy is the Theme Compat in use.
+	if ( 'legacy' !== bp_get_theme_compat_id() ) {
+		return;
+	}
+
+	// Get the theme.
+	$current_theme = wp_get_theme();
+	$theme_handle  = $current_theme->get_stylesheet();
+	$parent        = $current_theme->parent();
+
+	if ( $parent ) {
+		$theme_handle = $parent->get_stylesheet();
+	}
+
+	/**
+	 * Since Companion stylesheets, the $content_width is smaller
+	 * than the width used by BuddyPress, so we need to manually set the
+	 * content width for the concerned themes.
+	 *
+	 * Example: array( stylesheet => content width used by BuddyPress )
+	 */
+	$bp_content_widths = array(
+		'twentyfifteen'  => 1300,
+		'twentyfourteen' => 955,
+		'twentythirteen' => 890,
+	);
+
+	// Default values.
+	$bp_content_width = (int) $content_width;
+	$bp_handle        = 'bp-legacy-css';
+
+	// Specific to themes having companion stylesheets.
+	if ( isset( $bp_content_widths[ $theme_handle ] ) ) {
+		$bp_content_width = $bp_content_widths[ $theme_handle ];
+		$bp_handle        = 'bp-' . $theme_handle;
+	}
+
+	if ( is_rtl() ) {
+		$bp_handle .= '-rtl';
+	}
+
+	$top_offset    = 150;
+	$avatar_height = apply_filters( 'bp_core_avatar_full_height', $top_offset );
+
+	if ( $avatar_height > $top_offset ) {
+		$top_offset = $avatar_height;
+	}
+
+	bp_set_theme_compat_feature( 'legacy', array(
+		'name'     => 'cover_image',
+		'settings' => array(
+			'components'   => array( 'xprofile', 'groups' ),
+			'width'        => $bp_content_width,
+			'height'       => $top_offset + round( $avatar_height / 2 ),
+			'callback'     => 'bp_legacy_theme_cover_image',
+			'theme_handle' => $bp_handle,
+		),
+	) );
+}
+
+/**
+ * Check whether a given template is the one that WP originally selected to display current page.
+ *
+ * @since 1.7.0
+ *
+ * @param string $template The template name to check.
+ * @return bool True if the value of $template is the same as the
+ *              "original_template" originally selected by WP. Otherwise false.
+ */
+function bp_is_theme_compat_original_template( $template = '' ) {
+	$bp = buddypress();
+
+	if ( empty( $bp->theme_compat->original_template ) ) {
+		return false;
+	}
+
+	return (bool) ( $bp->theme_compat->original_template == $template );
+}
+
+/**
+ * Register a new BuddyPress theme package in the active theme packages array.
+ *
+ * For an example of how this function is used, see:
+ * {@link BuddyPress::register_theme_packages()}.
+ *
+ * @since 1.7.0
+ *
+ * @see BP_Theme_Compat for a description of the $theme parameter arguments.
+ *
+ * @param array $theme    See {@link BP_Theme_Compat}.
+ * @param bool  $override If true, overrides whatever package is currently set.
+ *                        Default: true.
+ */
+function bp_register_theme_package( $theme = array(), $override = true ) {
+
+	// Create new BP_Theme_Compat object from the $theme array.
+	if ( is_array( $theme ) ) {
+		$theme = new BP_Theme_Compat( $theme );
+	}
+
+	// Bail if $theme isn't a proper object.
+	if ( ! is_a( $theme, 'BP_Theme_Compat' ) ) {
+		return;
+	}
+
+	// Load up BuddyPress.
+	$bp = buddypress();
+
+	// Only set if the theme package was not previously registered or if the
+	// override flag is set.
+	if ( empty( $bp->theme_compat->packages[$theme->id] ) || ( true === $override ) ) {
+		$bp->theme_compat->packages[$theme->id] = $theme;
+	}
+}
+
+/**
+ * Populate various WordPress globals with dummy data to prevent errors.
+ *
+ * This dummy data is necessary because theme compatibility essentially fakes
+ * WordPress into thinking that there is content where, in fact, there is none
+ * (at least, no WordPress post content). By providing dummy data, we ensure
+ * that template functions - things like is_page() - don't throw errors.
+ *
+ * @since 1.7.0
+ *
+ * @global WP_Query $wp_query WordPress database access object.
+ * @global object $post Current post object.
+ *
+ * @param array $args Array of optional arguments. Arguments parallel the properties
+ *                    of {@link WP_Post}; see that class for more details.
+ */
+function bp_theme_compat_reset_post( $args = array() ) {
+	global $wp_query, $post;
+
+	// Switch defaults if post is set.
+	if ( isset( $wp_query->post ) ) {
+		$dummy = wp_parse_args( $args, array(
+			'ID'                    => $wp_query->post->ID,
+			'post_status'           => $wp_query->post->post_status,
+			'post_author'           => $wp_query->post->post_author,
+			'post_parent'           => $wp_query->post->post_parent,
+			'post_type'             => $wp_query->post->post_type,
+			'post_date'             => $wp_query->post->post_date,
+			'post_date_gmt'         => $wp_query->post->post_date_gmt,
+			'post_modified'         => $wp_query->post->post_modified,
+			'post_modified_gmt'     => $wp_query->post->post_modified_gmt,
+			'post_content'          => $wp_query->post->post_content,
+			'post_title'            => $wp_query->post->post_title,
+			'post_excerpt'          => $wp_query->post->post_excerpt,
+			'post_content_filtered' => $wp_query->post->post_content_filtered,
+			'post_mime_type'        => $wp_query->post->post_mime_type,
+			'post_password'         => $wp_query->post->post_password,
+			'post_name'             => $wp_query->post->post_name,
+			'guid'                  => $wp_query->post->guid,
+			'menu_order'            => $wp_query->post->menu_order,
+			'pinged'                => $wp_query->post->pinged,
+			'to_ping'               => $wp_query->post->to_ping,
+			'ping_status'           => $wp_query->post->ping_status,
+			'comment_status'        => $wp_query->post->comment_status,
+			'comment_count'         => $wp_query->post->comment_count,
+			'filter'                => $wp_query->post->filter,
+
+			'is_404'                => false,
+			'is_page'               => false,
+			'is_single'             => false,
+			'is_archive'            => false,
+			'is_tax'                => false,
+		) );
+	} else {
+		$dummy = wp_parse_args( $args, array(
+			'ID'                    => -9999,
+			'post_status'           => 'public',
+			'post_author'           => 0,
+			'post_parent'           => 0,
+			'post_type'             => 'page',
+			'post_date'             => 0,
+			'post_date_gmt'         => 0,
+			'post_modified'         => 0,
+			'post_modified_gmt'     => 0,
+			'post_content'          => '',
+			'post_title'            => '',
+			'post_excerpt'          => '',
+			'post_content_filtered' => '',
+			'post_mime_type'        => '',
+			'post_password'         => '',
+			'post_name'             => '',
+			'guid'                  => '',
+			'menu_order'            => 0,
+			'pinged'                => '',
+			'to_ping'               => '',
+			'ping_status'           => '',
+			'comment_status'        => 'closed',
+			'comment_count'         => 0,
+			'filter'                => 'raw',
+
+			'is_404'                => false,
+			'is_page'               => false,
+			'is_single'             => false,
+			'is_archive'            => false,
+			'is_tax'                => false,
+		) );
+	}
+
+	// Bail if dummy post is empty.
+	if ( empty( $dummy ) ) {
+		return;
+	}
+
+	// Set the $post global.
+	$post = new WP_Post( (object) $dummy );
+
+	// Copy the new post global into the main $wp_query.
+	$wp_query->post       = $post;
+	$wp_query->posts      = array( $post );
+
+	// Prevent comments form from appearing.
+	$wp_query->post_count = 1;
+	$wp_query->is_404     = $dummy['is_404'];
+	$wp_query->is_page    = $dummy['is_page'];
+	$wp_query->is_single  = $dummy['is_single'];
+	$wp_query->is_archive = $dummy['is_archive'];
+	$wp_query->is_tax     = $dummy['is_tax'];
+
+	// Clean up the dummy post.
+	unset( $dummy );
+
+	/**
+	 * Force the header back to 200 status if not a deliberate 404.
+	 *
+	 * @see https://bbpress.trac.wordpress.org/ticket/1973
+	 */
+	if ( ! $wp_query->is_404() ) {
+		status_header( 200 );
+	}
+
+	// If we are resetting a post, we are in theme compat.
+	bp_set_theme_compat_active( true );
+
+	// If we are in theme compat, we don't need the 'Edit' post link.
+	add_filter( 'get_edit_post_link', 'bp_core_filter_edit_post_link', 10, 2 );
+}
+
+/**
+ * Reset main query vars and filter 'the_content' to output a BuddyPress template part as needed.
+ *
+ * @since 1.7.0
+ *
+ * @param string $template Template name.
+ * @return string $template Template name.
+ */
+function bp_template_include_theme_compat( $template = '' ) {
+	// If embed template, bail.
+	if ( true === function_exists( 'is_embed' ) && is_embed() ) {
+		return $template;
+	}
+
+	// If the current theme doesn't need theme compat, bail at this point.
+	if ( ! bp_use_theme_compat_with_current_theme() ) {
+		return $template;
+	}
+
+	/**
+	 * Fires when resetting main query vars and filtering 'the_content' to output BuddyPress template parts.
+	 *
+	 * Use this action to execute code that will communicate to BuddyPress's
+	 * theme compatibility layer whether or not we're replacing the_content()
+	 * with some other template part.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_template_include_reset_dummy_post_data' );
+
+	// Bail if the template already matches a BuddyPress template.
+	if ( ! empty( buddypress()->theme_compat->found_template ) ) {
+		return $template;
+	}
+
+	/**
+	 * If we are relying on BuddyPress's built in theme compatibility to load
+	 * the proper content, we need to intercept the_content, replace the
+	 * output, and display ours instead.
+	 *
+	 * To do this, we first remove all filters from 'the_content' and hook
+	 * our own function into it, which runs a series of checks to determine
+	 * the context, and then uses the built in shortcodes to output the
+	 * correct results from inside an output buffer.
+	 *
+	 * Uses bp_get_theme_compat_templates() to provide fall-backs that
+	 * should be coded without superfluous mark-up and logic (prev/next
+	 * navigation, comments, date/time, etc...)
+	 *
+	 * Hook into 'bp_get_buddypress_template' to override the array of
+	 * possible templates, or 'bp_buddypress_template' to override the result.
+	 */
+	if ( bp_is_theme_compat_active() ) {
+		$template = bp_get_theme_compat_templates();
+
+		add_filter( 'the_content', 'bp_replace_the_content' );
+
+		// Add BuddyPress's head action to wp_head.
+		if ( ! has_action( 'wp_head', 'bp_head' ) ) {
+			add_action( 'wp_head', 'bp_head' );
+		}
+	}
+
+	/**
+	 * Filters the template name to include.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $template Template name.
+	 */
+	return apply_filters( 'bp_template_include_theme_compat', $template );
+}
+
+/**
+ * Conditionally replace 'the_content'.
+ *
+ * Replaces the_content() if the post_type being displayed is one that would
+ * normally be handled by BuddyPress, but proper single page templates do not
+ * exist in the currently active theme.
+ *
+ * @since 1.7.0
+ *
+ * @param string $content Original post content.
+ * @return string $content Post content, potentially modified.
+ */
+function bp_replace_the_content( $content = '' ) {
+
+	// Bail if not the main loop where theme compat is happening.
+	if ( ! bp_do_theme_compat() ) {
+		return $content;
+	}
+
+	// Set theme compat to false early, to avoid recursion from nested calls to
+	// the_content() that execute before theme compat has unhooked itself.
+	bp_set_theme_compat_active( false );
+
+	/**
+	 * Filters the content to replace in the post.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $content Original post content.
+	 */
+	$new_content = apply_filters( 'bp_replace_the_content', $content );
+
+	// Juggle the content around and try to prevent unsightly comments.
+	if ( ! empty( $new_content ) && ( $new_content !== $content ) ) {
+
+		// Set the content to be the new content.
+		$content = $new_content;
+
+		// Clean up after ourselves.
+		unset( $new_content );
+
+		// Reset the $post global.
+		wp_reset_postdata();
+	}
+
+	// Return possibly hi-jacked content.
+	return $content;
+}
+
+/**
+ * Are we currently replacing the_content?
+ *
+ * @since 1.8.0
+ *
+ * @return bool True if the_content is currently in the process of being
+ *              filtered and replaced.
+ */
+function bp_do_theme_compat() {
+	return (bool) ( ! bp_is_template_included() && in_the_loop() && bp_is_theme_compat_active() );
+}
+
+/** Filters *******************************************************************/
+
+/**
+ * Remove all filters from a WordPress filter hook.
+ *
+ * Removed filters are stashed in the $bp global, in case they need to be
+ * restored later.
+ *
+ * @since 1.7.0
+ *
+ * @global WP_filter $wp_filter
+ * @global array $merged_filters
+ *
+ * @param string   $tag      The filter tag to remove filters from.
+ * @param int|bool $priority Optional. If present, only those callbacks attached
+ *                           at a given priority will be removed. Otherwise, all callbacks
+ *                           attached to the tag will be removed, regardless of priority.
+ * @return bool True on success.
+ */
+function bp_remove_all_filters( $tag, $priority = false ) {
+	global $wp_filter, $merged_filters;
+
+	$bp = buddypress();
+
+	// Filters exist.
+	if ( isset( $wp_filter[$tag] ) ) {
+
+		// Filters exist in this priority.
+		if ( ! empty( $priority ) && isset( $wp_filter[$tag][$priority] ) ) {
+
+			// Store filters in a backup.
+			$bp->filters->wp_filter[$tag][$priority] = $wp_filter[$tag][$priority];
+
+			// Unset the filters.
+			unset( $wp_filter[$tag][$priority] );
+
+		// Priority is empty.
+		} else {
+
+			// Store filters in a backup.
+			$bp->filters->wp_filter[$tag] = $wp_filter[$tag];
+
+			// Unset the filters.
+			unset( $wp_filter[$tag] );
+		}
+	}
+
+	// Check merged filters.
+	if ( isset( $merged_filters[$tag] ) ) {
+
+		// Store filters in a backup.
+		$bp->filters->merged_filters[$tag] = $merged_filters[$tag];
+
+		// Unset the filters.
+		unset( $merged_filters[$tag] );
+	}
+
+	return true;
+}
+
+/**
+ * Restore filters that were removed using bp_remove_all_filters().
+ *
+ * @since 1.7.0
+ *
+ * @global WP_filter $wp_filter
+ * @global array $merged_filters
+ *
+ * @param string   $tag      The tag to which filters should be restored.
+ * @param int|bool $priority Optional. If present, only those filters that were originally
+ *                           attached to the tag with $priority will be restored. Otherwise,
+ *                           all available filters will be restored, regardless of priority.
+ * @return bool True on success.
+ */
+function bp_restore_all_filters( $tag, $priority = false ) {
+	global $wp_filter, $merged_filters;
+
+	$bp = buddypress();
+
+	// Filters exist.
+	if ( isset( $bp->filters->wp_filter[$tag] ) ) {
+
+		// Filters exist in this priority.
+		if ( ! empty( $priority ) && isset( $bp->filters->wp_filter[$tag][$priority] ) ) {
+
+			// Store filters in a backup.
+			$wp_filter[$tag][$priority] = $bp->filters->wp_filter[$tag][$priority];
+
+			// Unset the filters.
+			unset( $bp->filters->wp_filter[$tag][$priority] );
+
+		// Priority is empty.
+		} else {
+
+			// Store filters in a backup.
+			$wp_filter[$tag] = $bp->filters->wp_filter[$tag];
+
+			// Unset the filters.
+			unset( $bp->filters->wp_filter[$tag] );
+		}
+	}
+
+	// Check merged filters.
+	if ( isset( $bp->filters->merged_filters[$tag] ) ) {
+
+		// Store filters in a backup.
+		$merged_filters[$tag] = $bp->filters->merged_filters[$tag];
+
+		// Unset the filters.
+		unset( $bp->filters->merged_filters[$tag] );
+	}
+
+	return true;
+}
+
+/**
+ * Force comments_status to 'closed' for BuddyPress post types.
+ *
+ * @since 1.7.0
+ *
+ * @param bool $open    True if open, false if closed.
+ * @param int  $post_id ID of the post to check.
+ * @return bool True if open, false if closed.
+ */
+function bp_comments_open( $open, $post_id = 0 ) {
+
+	$retval = is_buddypress() ? false : $open;
+
+	/**
+	 * Filters whether or not to force comments_status to closed for BuddyPress post types.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param bool $retval  Whether or not we are on a BuddyPress post type.
+	 * @param bool $open    True if comments open, false if closed.
+	 * @param int  $post_id Post ID for the checked post.
+	 */
+	return apply_filters( 'bp_force_comment_status', $retval, $open, $post_id );
+}
+
+/**
+ * Do not allow {@link comments_template()} to render during theme compatibility.
+ *
+ * When theme compatibility sets the 'is_page' flag to true via
+ * {@link bp_theme_compat_reset_post()}, themes that use comments_template()
+ * in their page template will run.
+ *
+ * To prevent comments_template() from rendering, we set the 'is_page' and
+ * 'is_single' flags to false since that function looks at these conditionals
+ * before querying the database for comments and loading the comments template.
+ *
+ * This is done during the output buffer as late as possible to prevent any
+ * wonkiness.
+ *
+ * @since 1.9.2
+ *
+ * @param string $retval The current post content.
+ * @return string $retval
+ */
+function bp_theme_compat_toggle_is_page( $retval = '' ) {
+	global $wp_query;
+
+	if ( $wp_query->is_page ) {
+		$wp_query->is_page = false;
+
+		// Set a switch so we know that we've toggled these WP_Query properties.
+		buddypress()->theme_compat->is_page_toggled = true;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_replace_the_content', 'bp_theme_compat_toggle_is_page', 9999 );
+
+/**
+ * Restores the 'is_single' and 'is_page' flags if toggled by BuddyPress.
+ *
+ * @since 1.9.2
+ *
+ * @see bp_theme_compat_toggle_is_page()
+ * @param object $query The WP_Query object.
+ */
+function bp_theme_compat_loop_end( $query ) {
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	// Bail if page is not toggled.
+	if ( ! isset( $bp->theme_compat->is_page_toggled ) ) {
+		return;
+	}
+
+	// Revert our toggled WP_Query properties.
+	$query->is_page = true;
+
+	// Unset our switch.
+	unset( $bp->theme_compat->is_page_toggled );
+}
+add_action( 'loop_end', 'bp_theme_compat_loop_end' );
+
+/**
+ * Maybe override the preferred template pack if the theme declares a dependency.
+ *
+ * @since 3.0.0
+ */
+function bp_check_theme_template_pack_dependency() {
+	if ( bp_is_deactivation() ) {
+		return;
+	}
+
+	$all_packages = array_keys( buddypress()->theme_compat->packages );
+
+	foreach ( $all_packages as $package ) {
+		// e.g. "buddypress-use-nouveau", "buddypress-use-legacy".
+		if ( ! current_theme_supports( "buddypress-use-{$package}" ) ) {
+			continue;
+		}
+
+		bp_setup_theme_compat( $package );
+		return;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-update.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-update.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-update.php	(revision 12265)
@@ -0,0 +1,785 @@
+<?php
+/**
+ * BuddyPress Updater.
+ *
+ * @package BuddyPress
+ * @subpackage Updater
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Is this a fresh installation of BuddyPress?
+ *
+ * If there is no raw DB version, we infer that this is the first installation.
+ *
+ * @since 1.7.0
+ *
+ * @return bool True if this is a fresh BP install, otherwise false.
+ */
+function bp_is_install() {
+	return ! bp_get_db_version_raw();
+}
+
+/**
+ * Is this a BuddyPress update?
+ *
+ * Determined by comparing the registered BuddyPress version to the version
+ * number stored in the database. If the registered version is greater, it's
+ * an update.
+ *
+ * @since 1.6.0
+ *
+ * @return bool True if update, otherwise false.
+ */
+function bp_is_update() {
+
+	// Current DB version of this site (per site in a multisite network).
+	$current_db   = bp_get_option( '_bp_db_version' );
+	$current_live = bp_get_db_version();
+
+	// Compare versions (cast as int and bool to be safe).
+	$is_update = (bool) ( (int) $current_db < (int) $current_live );
+
+	// Return the product of version comparison.
+	return $is_update;
+}
+
+/**
+ * Determine whether BuddyPress is in the process of being activated.
+ *
+ * @since 1.6.0
+ *
+ * @param string $basename BuddyPress basename.
+ * @return bool True if activating BuddyPress, false if not.
+ */
+function bp_is_activation( $basename = '' ) {
+	$bp     = buddypress();
+	$action = false;
+
+	if ( ! empty( $_REQUEST['action'] ) && ( '-1' != $_REQUEST['action'] ) ) {
+		$action = $_REQUEST['action'];
+	} elseif ( ! empty( $_REQUEST['action2'] ) && ( '-1' != $_REQUEST['action2'] ) ) {
+		$action = $_REQUEST['action2'];
+	}
+
+	// Bail if not activating.
+	if ( empty( $action ) || !in_array( $action, array( 'activate', 'activate-selected' ) ) ) {
+		return false;
+	}
+
+	// The plugin(s) being activated.
+	if ( $action == 'activate' ) {
+		$plugins = isset( $_GET['plugin'] ) ? array( $_GET['plugin'] ) : array();
+	} else {
+		$plugins = isset( $_POST['checked'] ) ? (array) $_POST['checked'] : array();
+	}
+
+	// Set basename if empty.
+	if ( empty( $basename ) && !empty( $bp->basename ) ) {
+		$basename = $bp->basename;
+	}
+
+	// Bail if no basename.
+	if ( empty( $basename ) ) {
+		return false;
+	}
+
+	// Is BuddyPress being activated?
+	return in_array( $basename, $plugins );
+}
+
+/**
+ * Determine whether BuddyPress is in the process of being deactivated.
+ *
+ * @since 1.6.0
+ *
+ * @param string $basename BuddyPress basename.
+ * @return bool True if deactivating BuddyPress, false if not.
+ */
+function bp_is_deactivation( $basename = '' ) {
+	$bp     = buddypress();
+	$action = false;
+
+	if ( ! empty( $_REQUEST['action'] ) && ( '-1' != $_REQUEST['action'] ) ) {
+		$action = $_REQUEST['action'];
+	} elseif ( ! empty( $_REQUEST['action2'] ) && ( '-1' != $_REQUEST['action2'] ) ) {
+		$action = $_REQUEST['action2'];
+	}
+
+	// Bail if not deactivating.
+	if ( empty( $action ) || !in_array( $action, array( 'deactivate', 'deactivate-selected' ) ) ) {
+		return false;
+	}
+
+	// The plugin(s) being deactivated.
+	if ( 'deactivate' == $action ) {
+		$plugins = isset( $_GET['plugin'] ) ? array( $_GET['plugin'] ) : array();
+	} else {
+		$plugins = isset( $_POST['checked'] ) ? (array) $_POST['checked'] : array();
+	}
+
+	// Set basename if empty.
+	if ( empty( $basename ) && !empty( $bp->basename ) ) {
+		$basename = $bp->basename;
+	}
+
+	// Bail if no basename.
+	if ( empty( $basename ) ) {
+		return false;
+	}
+
+	// Is bbPress being deactivated?
+	return in_array( $basename, $plugins );
+}
+
+/**
+ * Update the BP version stored in the database to the current version.
+ *
+ * @since 1.6.0
+ */
+function bp_version_bump() {
+	bp_update_option( '_bp_db_version', bp_get_db_version() );
+}
+
+/**
+ * Set up the BuddyPress updater.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_updater() {
+
+	// Are we running an outdated version of BuddyPress?
+	if ( ! bp_is_update() ) {
+		return;
+	}
+
+	bp_version_updater();
+}
+
+/**
+ * Initialize an update or installation of BuddyPress.
+ *
+ * BuddyPress's version updater looks at what the current database version is,
+ * and runs whatever other code is needed - either the "update" or "install"
+ * code.
+ *
+ * This is most often used when the data schema changes, but should also be used
+ * to correct issues with BuddyPress metadata silently on software update.
+ *
+ * @since 1.7.0
+ */
+function bp_version_updater() {
+
+	// Get the raw database version.
+	$raw_db_version = (int) bp_get_db_version_raw();
+
+	/**
+	 * Filters the default components to activate for a new install.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $value Array of default components to activate.
+	 */
+	$default_components = apply_filters( 'bp_new_install_default_components', array(
+		'activity'      => 1,
+		'members'       => 1,
+		'settings'      => 1,
+		'xprofile'      => 1,
+		'notifications' => 1,
+	) );
+
+	require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
+	require_once( buddypress()->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
+	$switched_to_root_blog = false;
+
+	// Make sure the current blog is set to the root blog.
+	if ( ! bp_is_root_blog() ) {
+		switch_to_blog( bp_get_root_blog_id() );
+		bp_register_taxonomies();
+
+		$switched_to_root_blog = true;
+	}
+
+	// Install BP schema and activate only Activity and XProfile.
+	if ( bp_is_install() ) {
+
+		// Apply schema and set Activity and XProfile components as active.
+		bp_core_install( $default_components );
+		bp_update_option( 'bp-active-components', $default_components );
+		bp_core_add_page_mappings( $default_components, 'delete' );
+		bp_core_install_emails();
+
+	// Upgrades.
+	} else {
+
+		// Run the schema install to update tables.
+		bp_core_install();
+
+		// Version 1.5.0.
+		if ( $raw_db_version < 1801 ) {
+			bp_update_to_1_5();
+			bp_core_add_page_mappings( $default_components, 'delete' );
+		}
+
+		// Version 1.6.0.
+		if ( $raw_db_version < 6067 ) {
+			bp_update_to_1_6();
+		}
+
+		// Version 1.9.0.
+		if ( $raw_db_version < 7553 ) {
+			bp_update_to_1_9();
+		}
+
+		// Version 1.9.2.
+		if ( $raw_db_version < 7731 ) {
+			bp_update_to_1_9_2();
+		}
+
+		// Version 2.0.0.
+		if ( $raw_db_version < 7892 ) {
+			bp_update_to_2_0();
+		}
+
+		// Version 2.0.1.
+		if ( $raw_db_version < 8311 ) {
+			bp_update_to_2_0_1();
+		}
+
+		// Version 2.2.0.
+		if ( $raw_db_version < 9181 ) {
+			bp_update_to_2_2();
+		}
+
+		// Version 2.3.0.
+		if ( $raw_db_version < 9615 ) {
+			bp_update_to_2_3();
+		}
+
+		// Version 2.5.0.
+		if ( $raw_db_version < 10440 ) {
+			bp_update_to_2_5();
+		}
+
+		// Version 2.7.0.
+		if ( $raw_db_version < 11105 ) {
+			bp_update_to_2_7();
+		}
+	}
+
+	/* All done! *************************************************************/
+
+	// Bump the version.
+	bp_version_bump();
+
+	if ( $switched_to_root_blog ) {
+		restore_current_blog();
+	}
+}
+
+/**
+ * Perform database operations that must take place before the general schema upgrades.
+ *
+ * `dbDelta()` cannot handle certain operations - like changing indexes - so we do it here instead.
+ *
+ * @since 2.3.0
+ */
+function bp_pre_schema_upgrade() {
+	global $wpdb;
+
+	$raw_db_version = (int) bp_get_db_version_raw();
+	$bp_prefix      = bp_core_get_table_prefix();
+
+	// 2.3.0: Change index lengths to account for utf8mb4.
+	if ( $raw_db_version < 9695 ) {
+		// Map table_name => columns.
+		$tables = array(
+			$bp_prefix . 'bp_activity_meta'       => array( 'meta_key' ),
+			$bp_prefix . 'bp_groups_groupmeta'    => array( 'meta_key' ),
+			$bp_prefix . 'bp_messages_meta'       => array( 'meta_key' ),
+			$bp_prefix . 'bp_notifications_meta'  => array( 'meta_key' ),
+			$bp_prefix . 'bp_user_blogs_blogmeta' => array( 'meta_key' ),
+			$bp_prefix . 'bp_xprofile_meta'       => array( 'meta_key' ),
+		);
+
+		foreach ( $tables as $table_name => $indexes ) {
+			foreach ( $indexes as $index ) {
+				if ( $wpdb->query( $wpdb->prepare( "SHOW TABLES LIKE %s", bp_esc_like( $table_name ) ) ) ) {
+					$wpdb->query( "ALTER TABLE {$table_name} DROP INDEX {$index}" );
+				}
+			}
+		}
+	}
+}
+
+/** Upgrade Routines **********************************************************/
+
+/**
+ * Remove unused metadata from database when upgrading from < 1.5.
+ *
+ * Database update methods based on version numbers.
+ *
+ * @since 1.7.0
+ */
+function bp_update_to_1_5() {
+
+	// Delete old database version options.
+	delete_site_option( 'bp-activity-db-version' );
+	delete_site_option( 'bp-blogs-db-version'    );
+	delete_site_option( 'bp-friends-db-version'  );
+	delete_site_option( 'bp-groups-db-version'   );
+	delete_site_option( 'bp-messages-db-version' );
+	delete_site_option( 'bp-xprofile-db-version' );
+}
+
+/**
+ * Remove unused metadata from database when upgrading from < 1.6.0.
+ *
+ * Database update methods based on version numbers.
+ *
+ * @since 1.7.0
+ */
+function bp_update_to_1_6() {
+
+	// Delete possible site options.
+	delete_site_option( 'bp-db-version'       );
+	delete_site_option( '_bp_db_version'      );
+	delete_site_option( 'bp-core-db-version'  );
+	delete_site_option( '_bp-core-db-version' );
+
+	// Delete possible blog options.
+	delete_blog_option( bp_get_root_blog_id(), 'bp-db-version'       );
+	delete_blog_option( bp_get_root_blog_id(), 'bp-core-db-version'  );
+	delete_site_option( bp_get_root_blog_id(), '_bp-core-db-version' );
+	delete_site_option( bp_get_root_blog_id(), '_bp_db_version'      );
+}
+
+/**
+ * Add the notifications component to active components.
+ *
+ * Notifications was added in 1.9.0, and previous installations will already
+ * have the core notifications API active. We need to add the new Notifications
+ * component to the active components option to retain existing functionality.
+ *
+ * @since 1.9.0
+ */
+function bp_update_to_1_9() {
+
+	// Setup hardcoded keys.
+	$active_components_key      = 'bp-active-components';
+	$notifications_component_id = 'notifications';
+
+	// Get the active components.
+	$active_components          = bp_get_option( $active_components_key );
+
+	// Add notifications.
+	if ( ! in_array( $notifications_component_id, $active_components ) ) {
+		$active_components[ $notifications_component_id ] = 1;
+	}
+
+	// Update the active components option.
+	bp_update_option( $active_components_key, $active_components );
+}
+
+/**
+ * Perform database updates for BP 1.9.2.
+ *
+ * In 1.9, BuddyPress stopped registering its theme directory when it detected
+ * that bp-default (or a child theme) was not currently being used, in effect
+ * deprecating bp-default. However, this ended up causing problems when site
+ * admins using bp-default would switch away from the theme temporarily:
+ * bp-default would no longer be available, with no obvious way (outside of
+ * a manual filter) to restore it. In 1.9.2, we add an option that flags
+ * whether bp-default or a child theme is active at the time of upgrade; if so,
+ *
+ * the theme directory will continue to be registered even if the theme is
+ * deactivated temporarily. Thus, new installations will not see bp-default,
+ * but legacy installations using the theme will continue to see it.
+ *
+ * @since 1.9.2
+ */
+function bp_update_to_1_9_2() {
+	if ( 'bp-default' === get_stylesheet() || 'bp-default' === get_template() ) {
+		update_site_option( '_bp_retain_bp_default', 1 );
+	}
+}
+
+/**
+ * 2.0 update routine.
+ *
+ * - Ensure that the activity tables are installed, for last_activity storage.
+ * - Migrate last_activity data from usermeta to activity table.
+ * - Add values for all BuddyPress options to the options table.
+ *
+ * @since 2.0.0
+ */
+function bp_update_to_2_0() {
+
+	/* Install activity tables for 'last_activity' ***************************/
+
+	bp_core_install_activity_streams();
+
+	/* Migrate 'last_activity' data ******************************************/
+
+	bp_last_activity_migrate();
+
+	/* Migrate signups data **************************************************/
+
+	if ( ! is_multisite() ) {
+
+		// Maybe install the signups table.
+		bp_core_maybe_install_signups();
+
+		// Run the migration script.
+		bp_members_migrate_signups();
+	}
+
+	/* Add BP options to the options table ***********************************/
+
+	bp_add_options();
+}
+
+/**
+ * 2.0.1 database upgrade routine.
+ *
+ * @since 2.0.1
+ */
+function bp_update_to_2_0_1() {
+
+	// We purposely call this during both the 2.0 upgrade and the 2.0.1 upgrade.
+	// Don't worry; it won't break anything, and safely handles all cases.
+	bp_core_maybe_install_signups();
+}
+
+/**
+ * 2.2.0 update routine.
+ *
+ * - Add messages meta table.
+ * - Update the component field of the 'new members' activity type.
+ * - Clean up hidden friendship activities.
+ *
+ * @since 2.2.0
+ */
+function bp_update_to_2_2() {
+
+	// Also handled by `bp_core_install()`.
+	if ( bp_is_active( 'messages' ) ) {
+		bp_core_install_private_messaging();
+	}
+
+	if ( bp_is_active( 'activity' ) ) {
+		bp_migrate_new_member_activity_component();
+
+		if ( bp_is_active( 'friends' ) ) {
+			bp_cleanup_friendship_activities();
+		}
+	}
+}
+
+/**
+ * 2.3.0 update routine.
+ *
+ * - Add notifications meta table.
+ *
+ * @since 2.3.0
+ */
+function bp_update_to_2_3() {
+
+	// Also handled by `bp_core_install()`.
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_core_install_notifications();
+	}
+}
+
+/**
+ * 2.5.0 update routine.
+ *
+ * - Add emails.
+ *
+ * @since 2.5.0
+ */
+function bp_update_to_2_5() {
+	bp_core_install_emails();
+}
+
+/**
+ * 2.7.0 update routine.
+ *
+ * - Add email unsubscribe salt.
+ * - Save legacy directory titles to the corresponding WP pages.
+ * - Add ignore deprecated code option (false for updates).
+ *
+ * @since 2.7.0
+ */
+function bp_update_to_2_7() {
+	bp_add_option( 'bp-emails-unsubscribe-salt', base64_encode( wp_generate_password( 64, true, true ) ) );
+
+	// Update post_titles
+	bp_migrate_directory_page_titles();
+
+	/*
+	 * Add `parent_id` column to groups table.
+	 * Also handled by `bp_core_install()`.
+	 */
+	if ( bp_is_active( 'groups' ) ) {
+		bp_core_install_groups();
+
+		// Invalidate all cached group objects.
+		global $wpdb;
+		$bp = buddypress();
+
+		$group_ids = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name}" );
+
+		foreach ( $group_ids as $group_id ) {
+			wp_cache_delete( $group_id, 'bp_groups' );
+		}
+	}
+
+	// Do not ignore deprecated code for existing installs.
+	bp_add_option( '_bp_ignore_deprecated_code', false );
+}
+
+/**
+ * Updates the component field for new_members type.
+ *
+ * @since 2.2.0
+ *
+ * @global $wpdb
+ */
+function bp_migrate_new_member_activity_component() {
+	global $wpdb;
+	$bp = buddypress();
+
+	// Update the component for the new_member type.
+	$wpdb->update(
+		// Activity table.
+		$bp->members->table_name_last_activity,
+		array(
+			'component' => $bp->members->id,
+		),
+		array(
+			'component' => 'xprofile',
+			'type'      => 'new_member',
+		),
+		// Data sanitization format.
+		array(
+			'%s',
+		),
+		// WHERE sanitization format.
+		array(
+			'%s',
+			'%s'
+		)
+	);
+}
+
+/**
+ * Remove all hidden friendship activities.
+ *
+ * @since 2.2.0
+ */
+function bp_cleanup_friendship_activities() {
+	bp_activity_delete( array(
+		'component'     => buddypress()->friends->id,
+		'type'          => 'friendship_created',
+		'hide_sitewide' => true,
+	) );
+}
+
+/**
+ * Update WP pages so that their post_title matches the legacy component directory title.
+ *
+ * As of 2.7.0, component directory titles come from the `post_title` attribute of the corresponding WP post object,
+ * instead of being hardcoded. To ensure that directory titles don't change for existing installations, we update these
+ * WP posts with the formerly hardcoded titles.
+ *
+ * @since 2.7.0
+ */
+function bp_migrate_directory_page_titles() {
+	$bp_pages = bp_core_get_directory_page_ids( 'all' );
+
+	$default_titles = bp_core_get_directory_page_default_titles();
+
+	$legacy_titles = array(
+		'activity' => _x( 'Site-Wide Activity', 'component directory title', 'buddypress' ),
+		'blogs'    => _x( 'Sites', 'component directory title', 'buddypress' ),
+		'groups'   => _x( 'Groups', 'component directory title', 'buddypress' ),
+		'members'  => _x( 'Members', 'component directory title', 'buddypress' ),
+	);
+
+	foreach ( $bp_pages as $component => $page_id ) {
+		if ( ! isset( $legacy_titles[ $component ] ) ) {
+			continue;
+		}
+
+		$page = get_post( $page_id );
+		if ( ! $page ) {
+			continue;
+		}
+
+		// If the admin has changed the default title, don't touch it.
+		if ( isset( $default_titles[ $component ] ) && $default_titles[ $component ] !== $page->post_title ) {
+			continue;
+		}
+
+		// If the saved page title is the same as the legacy title, there's nothing to do.
+		if ( $legacy_titles[ $component ] == $page->post_title ) {
+			continue;
+		}
+
+		// Update the page with the legacy title.
+		wp_update_post( array(
+			'ID' => $page_id,
+			'post_title' => $legacy_titles[ $component ],
+		) );
+	}
+}
+
+/**
+ * Redirect user to BP's What's New page on first page load after activation.
+ *
+ * @since 1.7.0
+ *
+ * @internal Used internally to redirect BuddyPress to the about page on activation.
+ */
+function bp_add_activation_redirect() {
+
+	// Bail if activating from network, or bulk.
+	if ( isset( $_GET['activate-multi'] ) ) {
+		return;
+	}
+
+	// Record that this is a new installation, so we show the right
+	// welcome message.
+	if ( bp_is_install() ) {
+		set_transient( '_bp_is_new_install', true, 30 );
+	}
+
+	// Add the transient to redirect.
+	set_transient( '_bp_activation_redirect', true, 30 );
+}
+
+/** Signups *******************************************************************/
+
+/**
+ * Check if the signups table needs to be created or upgraded.
+ *
+ * @since 2.0.0
+ *
+ * @global WPDB $wpdb
+ */
+function bp_core_maybe_install_signups() {
+	global $wpdb;
+
+	// The table to run queries against.
+	$signups_table = $wpdb->base_prefix . 'signups';
+
+	// Suppress errors because users shouldn't see what happens next.
+	$old_suppress  = $wpdb->suppress_errors();
+
+	// Never use bp_core_get_table_prefix() for any global users tables.
+	$table_exists  = (bool) $wpdb->get_results( "DESCRIBE {$signups_table};" );
+
+	// Table already exists, so maybe upgrade instead?
+	if ( true === $table_exists ) {
+
+		// Look for the 'signup_id' column.
+		$column_exists = $wpdb->query( "SHOW COLUMNS FROM {$signups_table} LIKE 'signup_id'" );
+
+		// 'signup_id' column doesn't exist, so run the upgrade
+		if ( empty( $column_exists ) ) {
+			bp_core_upgrade_signups();
+		}
+
+	// Table does not exist, and we are a single site, so install the multisite
+	// signups table using WordPress core's database schema.
+	} elseif ( ! is_multisite() ) {
+		bp_core_install_signups();
+	}
+
+	// Restore previous error suppression setting.
+	$wpdb->suppress_errors( $old_suppress );
+}
+
+/** Activation Actions ********************************************************/
+
+/**
+ * Fire activation hooks and events.
+ *
+ * Runs on BuddyPress activation.
+ *
+ * @since 1.6.0
+ */
+function bp_activation() {
+
+	// Force refresh theme roots.
+	delete_site_transient( 'theme_roots' );
+
+	// Add options.
+	bp_add_options();
+
+	/**
+	 * Fires during the activation of BuddyPress.
+	 *
+	 * Use as of 1.6.0.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_activation' );
+
+	// @deprecated as of 1.6.0
+	do_action( 'bp_loader_activate' );
+}
+
+/**
+ * Fire deactivation hooks and events.
+ *
+ * Runs on BuddyPress deactivation.
+ *
+ * @since 1.6.0
+ */
+function bp_deactivation() {
+
+	// Force refresh theme roots.
+	delete_site_transient( 'theme_roots' );
+
+	// Switch to WordPress's default theme if current parent or child theme
+	// depend on bp-default. This is to prevent white screens of doom.
+	if ( in_array( 'bp-default', array( get_template(), get_stylesheet() ) ) ) {
+		switch_theme( WP_DEFAULT_THEME, WP_DEFAULT_THEME );
+		update_option( 'template_root',   get_raw_theme_root( WP_DEFAULT_THEME, true ) );
+		update_option( 'stylesheet_root', get_raw_theme_root( WP_DEFAULT_THEME, true ) );
+	}
+
+	/**
+	 * Fires during the deactivation of BuddyPress.
+	 *
+	 * Use as of 1.6.0.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_deactivation' );
+
+	// @deprecated as of 1.6.0
+	do_action( 'bp_loader_deactivate' );
+}
+
+/**
+ * Fire uninstall hook.
+ *
+ * Runs when uninstalling BuddyPress.
+ *
+ * @since 1.6.0
+ */
+function bp_uninstall() {
+
+	/**
+	 * Fires during the uninstallation of BuddyPress.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_uninstall' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-widgets.php	(revision 12265)
@@ -0,0 +1,21 @@
+<?php
+/**
+ * BuddyPress Core Component Widgets.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register bp-core widgets.
+ *
+ * @since 1.0.0
+ */
+function bp_core_register_widgets() {
+	add_action( 'widgets_init', function() { register_widget( 'BP_Core_Login_Widget' ); } );
+}
+add_action( 'bp_register_widgets', 'bp_core_register_widgets' );
Index: /tags/4.0.0-beta1/src/bp-core/bp-core-wpabstraction.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/bp-core-wpabstraction.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/bp-core-wpabstraction.php	(revision 12265)
@@ -0,0 +1,309 @@
+<?php
+/**
+ * WordPress Abstraction.
+ *
+ * The functions within this file will detect the version of WordPress you are
+ * running and will alter the environment so BuddyPress can run regardless.
+ *
+ * The code below mostly contains function mappings. This file is subject to
+ * change at any time.
+ *
+ * @package BuddyPress
+ * @subpackage WPAbstraction
+ * @since 1.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Parse the WordPress core version number into the major release.
+ *
+ * @since 1.5.2
+ *
+ * @global string $wp_version
+ *
+ * @return double $wp_version
+ */
+function bp_get_major_wp_version() {
+	global $wp_version;
+
+	return (float) $wp_version;
+}
+
+/*
+ * Only add MS-specific abstraction functions if WordPress is not in multisite mode.
+ */
+if ( !is_multisite() ) {
+	global $wpdb;
+
+	$wpdb->base_prefix = $wpdb->prefix;
+	$wpdb->blogid      = BP_ROOT_BLOG;
+
+	if ( !function_exists( 'get_blog_option' ) ) {
+
+		/**
+		 * Retrieve blog option.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @see get_blog_option()
+		 *
+		 * @param int    $blog_id     Blog ID to fetch for. Not used.
+		 * @param string $option_name Option name to fetch.
+		 * @param bool   $default     Whether or not default.
+		 * @return mixed
+		 */
+		function get_blog_option( $blog_id, $option_name, $default = false ) {
+			return get_option( $option_name, $default );
+		}
+	}
+
+	if ( ! function_exists( 'add_blog_option' ) ) {
+
+		/**
+		 * Add blog option.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @see add_blog_option()
+		 *
+		 * @param int    $blog_id      Blog ID to add for. Not used.
+		 * @param string $option_name  Option name to add.
+		 * @param mixed  $option_value Option value to add.
+		 * @return mixed
+		 */
+		function add_blog_option( $blog_id, $option_name, $option_value ) {
+			return add_option( $option_name, $option_value );
+		}
+	}
+
+	if ( !function_exists( 'update_blog_option' ) ) {
+
+		/**
+		 * Update blog option.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @see update_blog_option()
+		 *
+		 * @param int    $blog_id     Blog ID to update for. Not used.
+		 * @param string $option_name Option name to update.
+		 * @param mixed  $value       Option value to update.
+		 * @return mixed
+		 */
+		function update_blog_option( $blog_id, $option_name, $value ) {
+			return update_option( $option_name, $value );
+		}
+	}
+
+	if ( !function_exists( 'delete_blog_option' ) ) {
+
+		/**
+		 * Delete blog option.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @see delete_blog_option()
+		 *
+		 * @param int    $blog_id     Blog ID to delete for. Not used.
+		 * @param string $option_name Option name to delete.
+		 * @return mixed
+		 */
+		function delete_blog_option( $blog_id, $option_name ) {
+			return delete_option( $option_name );
+		}
+	}
+
+	if ( !function_exists( 'switch_to_blog' ) ) {
+
+		/**
+		 * Switch to specified blog.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @see switch_to_blog()
+		 *
+		 * @param mixed $new_blog   New blog to switch to. Not used.
+		 * @param null  $deprecated Whether or not deprecated. Not used.
+		 * @return int
+		 */
+		function switch_to_blog( $new_blog, $deprecated = null ) {
+			return bp_get_root_blog_id();
+		}
+	}
+
+	if ( !function_exists( 'restore_current_blog' ) ) {
+
+		/**
+		 * Restore current blog.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @see restore_current_blog()
+		 *
+		 * @return int
+		 */
+		function restore_current_blog() {
+			return bp_get_root_blog_id();
+		}
+	}
+
+	if ( !function_exists( 'get_blogs_of_user' ) ) {
+
+		/**
+		 * Retrive blogs associated with user.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @see get_blogs_of_user()
+		 *
+		 * @param int  $user_id ID of the user. Not used.
+		 * @param bool $all     Whether or not to return all. Not used.
+		 * @return bool
+		 */
+		function get_blogs_of_user( $user_id, $all = false ) {
+			return false;
+		}
+	}
+
+	if ( !function_exists( 'update_blog_status' ) ) {
+
+		/**
+		 * Whether or not to update blog status.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @see update_blog_status()
+		 *
+		 * @param int    $blog_id    Blog to update status for. Not used.
+		 * @param mixed  $pref       Preference. Not used.
+		 * @param string $value      Value. Not used.
+		 * @param null   $deprecated Whether or not deprecated. Not used.
+		 * @return bool
+		 */
+		function update_blog_status( $blog_id, $pref, $value, $deprecated = null ) {
+			return true;
+		}
+	}
+
+	if ( !function_exists( 'is_subdomain_install' ) ) {
+
+		/**
+		 * Whether or not if subdomain install.
+		 *
+		 * @since 1.2.5.1
+		 *
+		 * @see is_subdomain_install()
+		 *
+		 * @return bool
+		 */
+		function is_subdomain_install() {
+			if ( ( defined( 'VHOST' ) && 'yes' == VHOST ) || ( defined( 'SUBDOMAIN_INSTALL' ) && SUBDOMAIN_INSTALL ) )
+				return true;
+
+			return false;
+		}
+	}
+}
+
+/**
+ * Get SQL chunk for filtering spam users from member queries.
+ *
+ * @internal
+ * @todo Why is this function defined in this file?
+ *
+ * @param string|bool $prefix Global table prefix.
+ * @return string SQL chunk.
+ */
+function bp_core_get_status_sql( $prefix = false ) {
+	if ( !is_multisite() )
+		return "{$prefix}user_status = 0";
+	else
+		return "{$prefix}spam = 0 AND {$prefix}deleted = 0 AND {$prefix}user_status = 0";
+}
+
+/**
+ * Multibyte encoding fallback functions.
+ *
+ * The PHP multibyte encoding extension is not enabled by default. In cases where it is not enabled,
+ * these functions provide a fallback.
+ *
+ * Borrowed from MediaWiki, under the GPLv2. Thanks!
+ */
+if ( !function_exists( 'mb_strlen' ) ) {
+
+	/**
+	 * Fallback implementation of mb_strlen(), hardcoded to UTF-8.
+	 *
+	 * @param string $str String to be measured.
+	 * @param string $enc Optional. Encoding type. Ignored.
+	 * @return int String length.
+	 */
+	function mb_strlen( $str, $enc = '' ) {
+		$counts = count_chars( $str );
+		$total = 0;
+
+		// Count ASCII bytes.
+		for( $i = 0; $i < 0x80; $i++ ) {
+			$total += $counts[$i];
+		}
+
+		// Count multibyte sequence heads.
+		for( $i = 0xc0; $i < 0xff; $i++ ) {
+			$total += $counts[$i];
+		}
+		return $total;
+	}
+}
+
+if ( !function_exists( 'mb_strpos' ) ) {
+
+	/**
+	 * Fallback implementation of mb_strpos(), hardcoded to UTF-8.
+	 *
+	 * @param string $haystack String to search in.
+	 * @param string $needle String to search for.
+	 * @param int    $offset Optional. Start position for the search. Default: 0.
+	 * @param string $encoding Optional. Encoding type. Ignored.
+	 * @return int|false Position of needle in haystack if found, else false.
+	 */
+	function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) {
+		$needle = preg_quote( $needle, '/' );
+
+		$ar = array();
+		preg_match( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset );
+
+		if( isset( $ar[0][1] ) ) {
+			return $ar[0][1];
+		} else {
+			return false;
+		}
+	}
+}
+
+if ( !function_exists( 'mb_strrpos' ) ) {
+
+	/**
+	 * Fallback implementation of mb_strrpos(), hardcoded to UTF-8.
+	 *
+	 * @param string $haystack String to search in.
+	 * @param string $needle String to search for.
+	 * @param int    $offset Optional. Start position for the search. Default: 0.
+	 * @param string $encoding Optional. Encoding type. Ignored.
+	 * @return string|false Position of last needle in haystack if found, else false.
+	 */
+	function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) {
+		$needle = preg_quote( $needle, '/' );
+
+		$ar = array();
+		preg_match_all( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset );
+
+		if( isset( $ar[0] ) && count( $ar[0] ) > 0 &&
+			isset( $ar[0][count( $ar[0] ) - 1][1] ) ) {
+			return $ar[0][count( $ar[0] ) - 1][1];
+		} else {
+			return false;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-admin.php	(revision 12265)
@@ -0,0 +1,1214 @@
+<?php
+/**
+ * Main BuddyPress Admin Class.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdministration
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( !class_exists( 'BP_Admin' ) ) :
+
+/**
+ * Load BuddyPress plugin admin area.
+ *
+ * @todo Break this apart into each applicable Component.
+ *
+ * @since 1.6.0
+ */
+class BP_Admin {
+
+	/** Directory *************************************************************/
+
+	/**
+	 * Path to the BuddyPress admin directory.
+	 *
+	 * @since 1.6.0
+	 * @var string $admin_dir
+	 */
+	public $admin_dir = '';
+
+	/** URLs ******************************************************************/
+
+	/**
+	 * URL to the BuddyPress admin directory.
+	 *
+	 * @since 1.6.0
+	 * @var string $admin_url
+	 */
+	public $admin_url = '';
+
+	/**
+	 * URL to the BuddyPress images directory.
+	 *
+	 * @since 1.6.0
+	 * @var string $images_url
+	 */
+	public $images_url = '';
+
+	/**
+	 * URL to the BuddyPress admin CSS directory.
+	 *
+	 * @since 1.6.0
+	 * @var string $css_url
+	 */
+	public $css_url = '';
+
+	/**
+	 * URL to the BuddyPress admin JS directory.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $js_url = '';
+
+	/** Other *****************************************************************/
+
+	/**
+	 * Notices used for user feedback, like saving settings.
+	 *
+	 * @since 1.9.0
+	 * @var array()
+	 */
+	public $notices = array();
+
+	/** Methods ***************************************************************/
+
+	/**
+	 * The main BuddyPress admin loader.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+	}
+
+	/**
+	 * Set admin-related globals.
+	 *
+	 * @since 1.6.0
+	 */
+	private function setup_globals() {
+		$bp = buddypress();
+
+		// Paths and URLs
+		$this->admin_dir  = trailingslashit( $bp->plugin_dir  . 'bp-core/admin' ); // Admin path.
+		$this->admin_url  = trailingslashit( $bp->plugin_url  . 'bp-core/admin' ); // Admin url.
+		$this->images_url = trailingslashit( $this->admin_url . 'images'        ); // Admin images URL.
+		$this->css_url    = trailingslashit( $this->admin_url . 'css'           ); // Admin css URL.
+		$this->js_url     = trailingslashit( $this->admin_url . 'js'            ); // Admin css URL.
+
+		// Main settings page.
+		$this->settings_page = bp_core_do_network_admin() ? 'settings.php' : 'options-general.php';
+
+		// Main capability.
+		$this->capability = bp_core_do_network_admin() ? 'manage_network_options' : 'manage_options';
+	}
+
+	/**
+	 * Include required files.
+	 *
+	 * @since 1.6.0
+	 */
+	private function includes() {
+		require( $this->admin_dir . 'bp-core-admin-actions.php'    );
+		require( $this->admin_dir . 'bp-core-admin-settings.php'   );
+		require( $this->admin_dir . 'bp-core-admin-functions.php'  );
+		require( $this->admin_dir . 'bp-core-admin-components.php' );
+		require( $this->admin_dir . 'bp-core-admin-slugs.php'      );
+		require( $this->admin_dir . 'bp-core-admin-tools.php'      );
+	}
+
+	/**
+	 * Set up the admin hooks, actions, and filters.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	private function setup_actions() {
+
+		/* General Actions ***************************************************/
+
+		// Add some page specific output to the <head>.
+		add_action( 'bp_admin_head',            array( $this, 'admin_head'  ), 999 );
+
+		// Add menu item to settings menu.
+		add_action( 'admin_menu',               array( $this, 'site_admin_menus' ), 5 );
+		add_action( bp_core_admin_hook(),       array( $this, 'admin_menus' ), 5 );
+
+		// Enqueue all admin JS and CSS.
+		add_action( 'bp_admin_enqueue_scripts', array( $this, 'admin_register_styles' ), 1 );
+		add_action( 'bp_admin_enqueue_scripts', array( $this, 'admin_register_scripts' ), 1 );
+		add_action( 'bp_admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+
+		/* BuddyPress Actions ************************************************/
+
+		// Load the BuddyPress metabox in the WP Nav Menu Admin UI.
+		add_action( 'load-nav-menus.php', 'bp_admin_wp_nav_menu_meta_box' );
+
+		// Add settings.
+		add_action( 'bp_register_admin_settings', array( $this, 'register_admin_settings' ) );
+
+		// Add a link to BuddyPress Hello in the admin bar.
+		add_action( 'admin_bar_menu', array( $this, 'admin_bar_about_link' ), 100 );
+
+		// Add a description of new BuddyPress tools in the available tools page.
+		add_action( 'tool_box',            'bp_core_admin_available_tools_intro' );
+		add_action( 'bp_network_tool_box', 'bp_core_admin_available_tools_intro' );
+
+		// On non-multisite, catch.
+		add_action( 'load-users.php', 'bp_core_admin_user_manage_spammers' );
+
+		// Emails.
+		add_filter( 'manage_' . bp_get_email_post_type() . '_posts_columns',       array( $this, 'emails_register_situation_column' ) );
+		add_action( 'manage_' . bp_get_email_post_type() . '_posts_custom_column', array( $this, 'emails_display_situation_column_data' ), 10, 2 );
+
+		// Privacy Policy.
+		add_action( 'bp_admin_init', array( $this, 'add_privacy_policy_content' ) );
+
+		// BuddyPress Hello.
+		add_action( 'admin_footer', array( $this, 'about_screen' ) );
+
+		/* Filters ***********************************************************/
+
+		// Add link to settings page.
+		add_filter( 'plugin_action_links',               array( $this, 'modify_plugin_action_links' ), 10, 2 );
+		add_filter( 'network_admin_plugin_action_links', array( $this, 'modify_plugin_action_links' ), 10, 2 );
+
+		// Add "Mark as Spam" row actions on users.php.
+		add_filter( 'ms_user_row_actions', 'bp_core_admin_user_row_actions', 10, 2 );
+		add_filter( 'user_row_actions',    'bp_core_admin_user_row_actions', 10, 2 );
+
+		// Emails
+		add_filter( 'bp_admin_menu_order', array( $this, 'emails_admin_menu_order' ), 20 );
+	}
+
+	/**
+	 * Register site- or network-admin nav menu elements.
+	 *
+	 * Contextually hooked to site or network-admin depending on current configuration.
+	 *
+	 * @since 1.6.0
+	 */
+	public function admin_menus() {
+
+		// Bail if user cannot moderate.
+		if ( ! bp_current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
+		$hooks = array();
+
+		// Changed in BP 1.6 . See bp_core_admin_backpat_menu().
+		$hooks[] = add_menu_page(
+			__( 'BuddyPress', 'buddypress' ),
+			__( 'BuddyPress', 'buddypress' ),
+			$this->capability,
+			'bp-general-settings',
+			'bp_core_admin_backpat_menu',
+			'div'
+		);
+
+		$hooks[] = add_submenu_page(
+			'bp-general-settings',
+			__( 'BuddyPress Help', 'buddypress' ),
+			__( 'Help', 'buddypress' ),
+			$this->capability,
+			'bp-general-settings',
+			'bp_core_admin_backpat_page'
+		);
+
+		// Add the option pages.
+		$hooks[] = add_submenu_page(
+			$this->settings_page,
+			__( 'BuddyPress Components', 'buddypress' ),
+			__( 'BuddyPress', 'buddypress' ),
+			$this->capability,
+			'bp-components',
+			'bp_core_admin_components_settings'
+		);
+
+		$hooks[] = add_submenu_page(
+			$this->settings_page,
+			__( 'BuddyPress Pages', 'buddypress' ),
+			__( 'BuddyPress Pages', 'buddypress' ),
+			$this->capability,
+			'bp-page-settings',
+			'bp_core_admin_slugs_settings'
+		);
+
+		$hooks[] = add_submenu_page(
+			$this->settings_page,
+			__( 'BuddyPress Options', 'buddypress' ),
+			__( 'BuddyPress Options', 'buddypress' ),
+			$this->capability,
+			'bp-settings',
+			'bp_core_admin_settings'
+		);
+
+		// Credits.
+		$hooks[] = add_submenu_page(
+			$this->settings_page,
+			__( 'BuddyPress Credits', 'buddypress' ),
+			__( 'BuddyPress Credits', 'buddypress' ),
+			$this->capability,
+			'bp-credits',
+			array( $this, 'credits_screen' )
+		);
+
+		// For consistency with non-Multisite, we add a Tools menu in
+		// the Network Admin as a home for our Tools panel.
+		if ( is_multisite() && bp_core_do_network_admin() ) {
+			$tools_parent = 'network-tools';
+
+			$hooks[] = add_menu_page(
+				__( 'Tools', 'buddypress' ),
+				__( 'Tools', 'buddypress' ),
+				$this->capability,
+				$tools_parent,
+				'bp_core_tools_top_level_item',
+				'',
+				24 // Just above Settings.
+			);
+
+			$hooks[] = add_submenu_page(
+				$tools_parent,
+				__( 'Available Tools', 'buddypress' ),
+				__( 'Available Tools', 'buddypress' ),
+				$this->capability,
+				'available-tools',
+				'bp_core_admin_available_tools_page'
+			);
+		} else {
+			$tools_parent = 'tools.php';
+		}
+
+		$hooks[] = add_submenu_page(
+			$tools_parent,
+			__( 'BuddyPress Tools', 'buddypress' ),
+			__( 'BuddyPress', 'buddypress' ),
+			$this->capability,
+			'bp-tools',
+			'bp_core_admin_tools'
+		);
+
+		// For network-wide configs, add a link to (the root site's) Emails screen.
+		if ( is_network_admin() && bp_is_network_activated() ) {
+			$email_labels = bp_get_email_post_type_labels();
+			$email_url    = get_admin_url( bp_get_root_blog_id(), 'edit.php?post_type=' . bp_get_email_post_type() );
+
+			$hooks[] = add_menu_page(
+				$email_labels['name'],
+				$email_labels['menu_name'],
+				$this->capability,
+				'',
+				'',
+				'dashicons-email',
+				26
+			);
+
+			// Hack: change the link to point to the root site's admin, not the network admin.
+			$GLOBALS['menu'][26][2] = esc_url_raw( $email_url );
+		}
+
+		foreach( $hooks as $hook ) {
+			add_action( "admin_head-$hook", 'bp_core_modify_admin_menu_highlight' );
+		}
+	}
+
+	/**
+	 * Register site-admin nav menu elements.
+	 *
+	 * @since 2.5.0
+	 */
+	public function site_admin_menus() {
+		if ( ! bp_current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
+		$hooks = array();
+
+		// Appearance > Emails.
+		$hooks[] = add_theme_page(
+			_x( 'Emails', 'screen heading', 'buddypress' ),
+			_x( 'Emails', 'screen heading', 'buddypress' ),
+			$this->capability,
+			'bp-emails-customizer-redirect',
+			'bp_email_redirect_to_customizer'
+		);
+
+		// Emails > Customize.
+		$hooks[] = add_submenu_page(
+			'edit.php?post_type=' . bp_get_email_post_type(),
+			_x( 'Customize', 'email menu label', 'buddypress' ),
+			_x( 'Customize', 'email menu label', 'buddypress' ),
+			$this->capability,
+			'bp-emails-customizer-redirect',
+			'bp_email_redirect_to_customizer'
+		);
+
+		foreach( $hooks as $hook ) {
+			add_action( "admin_head-$hook", 'bp_core_modify_admin_menu_highlight' );
+		}
+	}
+
+	/**
+	 * Register the settings.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	public function register_admin_settings() {
+
+		/* Main Section ******************************************************/
+
+		// Add the main section.
+		add_settings_section( 'bp_main', __( 'Main Settings', 'buddypress' ), 'bp_admin_setting_callback_main_section', 'buddypress' );
+
+		// Hide toolbar for logged out users setting.
+		add_settings_field( 'hide-loggedout-adminbar', __( 'Toolbar', 'buddypress' ), 'bp_admin_setting_callback_admin_bar', 'buddypress', 'bp_main' );
+		register_setting( 'buddypress', 'hide-loggedout-adminbar', 'intval' );
+
+		// Only show 'switch to Toolbar' option if the user chose to retain the BuddyBar during the 1.6 upgrade.
+		if ( (bool) bp_get_option( '_bp_force_buddybar', false ) ) {
+			// Load deprecated code if not available.
+			if ( ! function_exists( 'bp_admin_setting_callback_force_buddybar' ) ) {
+				require buddypress()->plugin_dir . 'bp-core/deprecated/2.1.php';
+			}
+
+			add_settings_field( '_bp_force_buddybar', __( 'Toolbar', 'buddypress' ), 'bp_admin_setting_callback_force_buddybar', 'buddypress', 'bp_main' );
+			register_setting( 'buddypress', '_bp_force_buddybar', 'bp_admin_sanitize_callback_force_buddybar' );
+		}
+
+		// Allow account deletion.
+		add_settings_field( 'bp-disable-account-deletion', __( 'Account Deletion', 'buddypress' ), 'bp_admin_setting_callback_account_deletion', 'buddypress', 'bp_main' );
+		register_setting( 'buddypress', 'bp-disable-account-deletion', 'intval' );
+
+		// Template pack picker.
+		add_settings_field( '_bp_theme_package_id', __( 'Template Pack', 'buddypress' ), 'bp_admin_setting_callback_theme_package_id', 'buddypress', 'bp_main', array( 'label_for' => '_bp_theme_package_id' ) );
+		register_setting( 'buddypress', '_bp_theme_package_id', 'sanitize_text_field' );
+
+		/* XProfile Section **************************************************/
+
+		if ( bp_is_active( 'xprofile' ) ) {
+
+			// Add the main section.
+			add_settings_section( 'bp_xprofile', _x( 'Profile Settings', 'BuddyPress setting tab', 'buddypress' ), 'bp_admin_setting_callback_xprofile_section', 'buddypress' );
+
+			// Avatars.
+			add_settings_field( 'bp-disable-avatar-uploads', __( 'Profile Photo Uploads', 'buddypress' ), 'bp_admin_setting_callback_avatar_uploads', 'buddypress', 'bp_xprofile' );
+			register_setting( 'buddypress', 'bp-disable-avatar-uploads', 'intval' );
+
+			// Cover images.
+			if ( bp_is_active( 'xprofile', 'cover_image' ) ) {
+				add_settings_field( 'bp-disable-cover-image-uploads', __( 'Cover Image Uploads', 'buddypress' ), 'bp_admin_setting_callback_cover_image_uploads', 'buddypress', 'bp_xprofile' );
+				register_setting( 'buddypress', 'bp-disable-cover-image-uploads', 'intval' );
+			}
+
+			// Profile sync setting.
+			add_settings_field( 'bp-disable-profile-sync',   __( 'Profile Syncing',  'buddypress' ), 'bp_admin_setting_callback_profile_sync', 'buddypress', 'bp_xprofile' );
+			register_setting  ( 'buddypress', 'bp-disable-profile-sync', 'intval' );
+		}
+
+		/* Groups Section ****************************************************/
+
+		if ( bp_is_active( 'groups' ) ) {
+
+			// Add the main section.
+			add_settings_section( 'bp_groups', __( 'Groups Settings',  'buddypress' ), 'bp_admin_setting_callback_groups_section', 'buddypress' );
+
+			// Allow subscriptions setting.
+			add_settings_field( 'bp_restrict_group_creation', __( 'Group Creation', 'buddypress' ), 'bp_admin_setting_callback_group_creation',   'buddypress', 'bp_groups' );
+			register_setting( 'buddypress', 'bp_restrict_group_creation', 'intval' );
+
+			// Allow group avatars.
+			add_settings_field( 'bp-disable-group-avatar-uploads', __( 'Group Photo Uploads', 'buddypress' ), 'bp_admin_setting_callback_group_avatar_uploads', 'buddypress', 'bp_groups' );
+			register_setting( 'buddypress', 'bp-disable-group-avatar-uploads', 'intval' );
+
+			// Allow group cover images.
+			if ( bp_is_active( 'groups', 'cover_image' ) ) {
+				add_settings_field( 'bp-disable-group-cover-image-uploads', __( 'Group Cover Image Uploads', 'buddypress' ), 'bp_admin_setting_callback_group_cover_image_uploads', 'buddypress', 'bp_groups' );
+				register_setting( 'buddypress', 'bp-disable-group-cover-image-uploads', 'intval' );
+			}
+		}
+
+		/* Activity Section **************************************************/
+
+		if ( bp_is_active( 'activity' ) ) {
+
+			// Add the main section.
+			add_settings_section( 'bp_activity', __( 'Activity Settings', 'buddypress' ), 'bp_admin_setting_callback_activity_section', 'buddypress' );
+
+			// Activity commenting on post and comments.
+			add_settings_field( 'bp-disable-blogforum-comments', __( 'Post Comments', 'buddypress' ), 'bp_admin_setting_callback_blogforum_comments', 'buddypress', 'bp_activity' );
+			register_setting( 'buddypress', 'bp-disable-blogforum-comments', 'bp_admin_sanitize_callback_blogforum_comments' );
+
+			// Activity Heartbeat refresh.
+			add_settings_field( '_bp_enable_heartbeat_refresh', __( 'Activity auto-refresh', 'buddypress' ), 'bp_admin_setting_callback_heartbeat', 'buddypress', 'bp_activity' );
+			register_setting( 'buddypress', '_bp_enable_heartbeat_refresh', 'intval' );
+
+			// Allow activity akismet.
+			if ( is_plugin_active( 'akismet/akismet.php' ) && defined( 'AKISMET_VERSION' ) ) {
+				add_settings_field( '_bp_enable_akismet', __( 'Akismet', 'buddypress' ), 'bp_admin_setting_callback_activity_akismet', 'buddypress', 'bp_activity' );
+				register_setting( 'buddypress', '_bp_enable_akismet', 'intval' );
+			}
+		}
+	}
+
+	/**
+	 * Add a link to BuddyPress Hello to the admin bar.
+	 *
+	 * @since 1.9.0
+	 * @since 3.0.0 Hooked at priority 100 (was 15).
+	 *
+	 * @param WP_Admin_Bar $wp_admin_bar
+	 */
+	public function admin_bar_about_link( $wp_admin_bar ) {
+		if ( ! is_user_logged_in() ) {
+			return;
+		}
+
+		$wp_admin_bar->add_menu( array(
+			'parent' => 'wp-logo',
+			'id'     => 'bp-about',
+			'title'  => esc_html_x( 'Hello, BuddyPress!', 'Colloquial alternative to "learn about BuddyPress"', 'buddypress' ),
+			'href'   => bp_get_admin_url( '?hello=buddypress' ),
+			'meta'   => array(
+				'class' => 'say-hello-buddypress',
+			),
+		) );
+	}
+
+	/**
+	 * Add Settings link to plugins area.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array  $links Links array in which we would prepend our link.
+	 * @param string $file  Current plugin basename.
+	 * @return array Processed links.
+	 */
+	public function modify_plugin_action_links( $links, $file ) {
+
+		// Return normal links if not BuddyPress.
+		if ( plugin_basename( buddypress()->basename ) != $file ) {
+			return $links;
+		}
+
+		// Add a few links to the existing links array.
+		return array_merge( $links, array(
+			'settings' => '<a href="' . esc_url( add_query_arg( array( 'page' => 'bp-components' ), bp_get_admin_url( $this->settings_page ) ) ) . '">' . esc_html__( 'Settings', 'buddypress' ) . '</a>',
+			'about'    => '<a href="' . esc_url( bp_get_admin_url( '?hello=buddypress' ) ) . '">' . esc_html_x( 'Hello, BuddyPress!', 'Colloquial alternative to "learn about BuddyPress"', 'buddypress' ) . '</a>'
+		) );
+	}
+
+	/**
+	 * Add some general styling to the admin area.
+	 *
+	 * @since 1.6.0
+	 */
+	public function admin_head() {
+
+		// Settings pages.
+		remove_submenu_page( $this->settings_page, 'bp-page-settings' );
+		remove_submenu_page( $this->settings_page, 'bp-settings'      );
+		remove_submenu_page( $this->settings_page, 'bp-credits'       );
+
+		// Network Admin Tools.
+		remove_submenu_page( 'network-tools', 'network-tools' );
+
+		// About and Credits pages.
+		remove_submenu_page( 'index.php', 'bp-about'   );
+		remove_submenu_page( 'index.php', 'bp-credits' );
+	}
+
+	/**
+	 * Add some general styling to the admin area.
+	 *
+	 * @since 1.6.0
+	 */
+	public function enqueue_scripts() {
+		wp_enqueue_style( 'bp-admin-common-css' );
+
+		// BuddyPress Hello
+		if ( 0 === strpos( get_current_screen()->id, 'dashboard' ) && ! empty( $_GET['hello'] ) && $_GET['hello'] === 'buddypress' ) {
+			wp_enqueue_style( 'bp-hello-css' );
+			wp_enqueue_script( 'bp-hello-js' );
+		}
+	}
+
+	/**
+	 * Registers BuddyPress's suggested privacy policy language.
+	 *
+	 * @since 3.x.y
+	 */
+	public function add_privacy_policy_content() {
+		// Nothing to do if we're running < WP 4.9.6.
+		if ( version_compare( $GLOBALS['wp_version'], '4.9.6', '<' ) ) {
+			return;
+		}
+
+		$suggested_text = '<strong class="privacy-policy-tutorial">' . esc_html__( 'Suggested text:', 'buddypress' ) . ' </strong>';
+		$content = '';
+
+		$content .= '<div class="wp-suggested-text">';
+
+		$content .= '<h2>' . esc_html__( 'What personal data we collect and why we collect it', 'buddypress' ) . '</h2>';
+		$content .= '<p class="privacy-policy-tutorial">' . esc_html__( 'Sites powered by BuddyPress rely heavily on user-provided data. In this section, you should note what data you collect, from both registered users and anonymous visitors.', 'buddypress' ) . '</p>';
+
+		if ( bp_is_active( 'xprofile' ) ) {
+			$content .= '<h3>' . esc_html__( 'Profile Data', 'buddypress' ) . '</h3>';
+			$content .= '<p class="privacy-policy-tutorial">' . esc_html__( 'In this section you should note what information is collected on user profiles. The suggested text gives an overview of the kinds of profile data collected by BuddyPress.', 'buddypress' ) . '</p>';
+
+			$content .= '<p>' . $suggested_text . esc_html__( 'When you register for the site, you may be asked to provide certain personal data for display on your profile. The "Name" field is required as well as public, and user profiles are visible to any site visitor. Other profile information may be required or optional, as configured by the site administrator.', 'buddypress' ) . '</p>';
+			$content .= '<p>' . esc_html__( 'User information provided during account registration can be modified or removed on the Profile > Edit panel. In most cases, users also have control over who is able to view a particular piece of profile content, limiting visibility on a field-by-field basis to friends, logged-in users, or administrators only. Site administrators can read and edit all profile data for all users.', 'buddypress' ) . '</p>';
+		}
+
+		if ( bp_is_active( 'activity' ) ) {
+			$content .= '<h3>' . esc_html__( 'Activity', 'buddypress' ) . '</h3>';
+			$content .= '<p class="privacy-policy-tutorial">' . esc_html__( 'In this section you should describe the kinds of information collected in the activity stream, how and whether it can be edited or deleted, and to whom the activity is visible.', 'buddypress' ) . '</p>';
+
+			$content .= '<p>' . $suggested_text . esc_html__( 'This site records certain user actions, in the form of "activity" data. Activity includes updates and comments posted directly to activity streams, as well as descriptions of other actions performed while using the site, such as new friendships, newly joined groups, and profile updates.', 'buddypress' ) . '</p>';
+			$content .= '<p>' . esc_html__( 'The content of activity items obey the same privacy rules as the contexts in which the activity items are created. For example, activity updates created in a user\'s profile is publicly visible, while activity items generated in a private group are visible only to members of that group. Site administrators can view all activity items, regardless of context.', 'buddypress' ) . '</p>';
+			$content .= '<p>' . esc_html__( 'Activity items may be deleted at any time by users who created them. Site administrators can edit all activity items.', 'buddypress' ) . '</p>';
+		}
+
+		if ( bp_is_active( 'messages' ) ) {
+			$content .= '<h3>' . esc_html__( 'Messages', 'buddypress' ) . '</h3>';
+			$content .= '<p class="privacy-policy-tutorial">' . esc_html__( 'In this section you should describe any personal data related to private messages.', 'buddypress' ) . '</p>';
+
+			$content .= '<p>' . $suggested_text . esc_html__( 'The content of private messages is visible only to the sender and the recipients of the message. With the exception of site administrators, who can read all private messages, private message content is never visible to other users or site visitors. Site administrators may delete the content of any message.', 'buddypress' ) . '</p>';
+		}
+
+		$content .= '<h3>' . esc_html__( 'Cookies', 'buddypress' ) . '</h3>';
+		$content .= '<p class="privacy-policy-tutorial">' . esc_html__( 'In this section you should describe the BuddyPress-specific cookies that your site collects. The suggested text describes the default cookies.', 'buddypress' ) . '</p>';
+
+		$content .= '<p>' . $suggested_text . esc_html__( 'We use a cookie to show success and failure messages to logged-in users, in response to certain actions, like joining a group. These cookies contain no personal data, and are deleted immediately after the next page load.', 'buddypress' ) . '</p>';
+
+		$content .= '<p>' . esc_html__( 'We use cookies on group, member, and activity directories to keep track of a user\'s browsing preferences. These preferences include the last-selected values of the sort and filter dropdowns, as well as pagination information. These cookies contain no personal data, and are deleted after 24 hours.', 'buddypress' ) . '</p>';
+
+		if ( bp_is_active( 'groups' ) ) {
+			$content .= '<p>' . esc_html__( 'When a logged-in user creates a new group, we use a number of cookies to keep track of the group creation process. These cookies contain no personal data, and are deleted either upon the successful creation of the group or after 24 hours.', 'buddypress' ) . '</p>';
+		}
+
+		$content .= '</div><!-- .wp-suggested-text -->';
+
+		wp_add_privacy_policy_content(
+			'BuddyPress',
+			wp_kses_post( wpautop( $content, false ) )
+		);
+	}
+
+	/** About *****************************************************************/
+
+	/**
+	 * Output the BuddyPress Hello template.
+	 *
+	 * @since 1.7.0 Screen content.
+	 * @since 3.0.0 Now outputs BuddyPress Hello template.
+	 */
+	public function about_screen() {
+		if ( 0 !== strpos( get_current_screen()->id, 'dashboard' ) || empty( $_GET['hello'] ) || $_GET['hello'] !== 'buddypress' ) {
+			return;
+		}
+	?>
+
+		<div id="bp-hello-backdrop" style="display: none;">
+		</div>
+
+		<div id="bp-hello-container" role="dialog" aria-labelledby="bp-hello-title" style="display: none;">
+			<div class="bp-hello-header" role="document">
+				<div class="bp-hello-close">
+					<button type="button" class="close-modal button bp-tooltip" data-bp-tooltip="<?php echo esc_attr( 'Close pop-up', 'buddypress' ); ?>">
+						<span class="screen-reader-text"><?php esc_html_e( 'Close pop-up', 'buddypress' ); ?></span>
+					</button>
+				</div>
+
+				<div class="bp-hello-title">
+					<h1 id="bp-hello-title" tabindex="-1"><?php esc_html_e( _x( 'New in BuddyPress', 'section heading', 'buddypress' ) ); ?></h1>
+				</div>
+			</div>
+
+			<div class="bp-hello-content">
+				<h2><?php esc_html_e( __( 'Say hello to &ldquo;Nouveau&rdquo;!', 'buddypress' ) ); ?></h2>
+				<p>
+					<?php
+					printf(
+						__( 'A bold reimagining of our legacy templates, Nouveau is our celebration of <a href="%s">10 years of BuddyPress</a>! Nouveau delivers modern markup with fresh JavaScript-powered templates, and full integration with WordPress\' Customizer, allowing more out-of-the-box control of your BuddyPress content than ever before.', 'buddypress' ),
+						esc_url( 'https://buddypress.org/2018/03/10-years/' )
+					);
+					?>
+				</p>
+				<p><?php esc_html_e( 'Nouveau provides vertical and horizontal layout options for BuddyPress navigation, and for the component directories, you can choose between a grid layout, and a classic flat list.', 'buddypress' ); ?></p>
+				<p>
+					<?php
+					printf(
+						__( 'Nouveau is fully compatible with WordPress. Existing BuddyPress themes have been written for our legacy template pack, and until they are updated, resolve any compatibility issues by choosing the legacy template pack option in <a href="%s">Settings &gt; BuddyPress</a>.', 'buddypress' ),
+						esc_url( bp_get_admin_url( 'admin.php?page=bp-settings' ) )
+					);
+					?>
+				</p>
+
+				<?php echo $GLOBALS['wp_embed']->autoembed( 'https://player.vimeo.com/video/270507360' ); ?>
+
+				<h2><?php esc_html_e( __( 'Support for WP-CLI', 'buddypress' ) ); ?></h2>
+				<p>
+					<?php
+					printf(
+						__( '<a href="%s">WP-CLI</a> is the command-line interface for WordPress. You can update plugins, configure multisite installs, and much more, without using a web browser. With this version of BuddyPress, you can now manage your BuddyPress content from WP-CLI.', 'buddypress' ),
+						esc_url( 'https://wp-cli.org' )
+					);
+					?>
+				</p>
+
+				<h2><?php esc_html_e( _x( 'Control site-wide notices from your dashboard', 'section heading', 'buddypress' ) ); ?></h2>
+				<p><?php esc_html_e( 'Site Notices are a feature within the Private Messaging component that allows community managers to share important messages with all members of their community. With Nouveau, the management interface for Site Notices has been removed from the front-end theme templates.', 'buddypress' ); ?></p>
+
+				<?php if ( bp_is_active( 'messages' ) ) : ?>
+				<p>
+					<?php
+					printf(
+						__( 'Explore the new management interface at <a href="%s">Users &gt; Site Notices</a>.', 'buddypress' ),
+						esc_url( bp_get_admin_url( 'users.php?page=bp-notices' ) )
+					);
+					?>
+				</p>
+				<?php endif; ?>
+
+				<h2><?php esc_html_e( __( 'New profile field type: telephone numbers', 'buddypress' ) ); ?></h2>
+				<p><?php esc_html_e( 'A new telephone number field type has been added to the Extended Profiles component, with support for all international number formats. With a modern web browser, your members can use this field type to touch-to-dial a number directly.', 'buddypress' ); ?></p>
+
+				<h2><?php esc_html_e( __( "BuddyPress: leaner, faster, stronger", 'buddypress' ) ); ?></h2>
+				<p><?php esc_html_e( 'With every BuddyPress version, we strive to make performance improvements alongside new features and fixes; this version is no exception. Memory use has been optimised &mdash; within active components, we now only load each individual code file when it\'s needed, not before.', 'buddypress' ); ?></p>
+				<p>
+					<?php
+					printf(
+						__( 'Most notably, the <a href="%s">Legacy Forums component has been removed</a> after 9 years of service. If your site was using Legacy Forums, you need to <a href="%s">migrate to the bbPress plugin</a>.', 'buddypress' ),
+						esc_url( 'https://bpdevel.wordpress.com/2017/12/07/legacy-forums-support-will-be/' ),
+						esc_url( 'https://codex.buddypress.org/getting-started/guides/migrating-from-old-forums-to-bbpress-2/' )
+					);
+					?>
+				</p>
+
+				<p><em>
+					<?php
+					printf(
+						__( 'To read the full list of features, fixes, and changes in this version of BuddyPress, <a href="%s">visit Trac</a>.', 'buddypress' ),
+						esc_url( 'https://buddypress.trac.wordpress.org/query?group=status&milestone=3.0' )
+					);
+					?>
+				</em></p>
+
+				<h2><?php esc_html_e( _x( 'Your feedback', 'screen heading', 'buddypress' ) ); ?></h2>
+				<p>
+					<?php
+					printf(
+						__( ' How are you using BuddyPress? Receiving your feedback and suggestions for future versions of BuddyPress genuinely motivates and encourages our contributors. Please <a href="%s">share your feedback</a> about this version of BuddyPress on our website. ', 'buddypress' ),
+						esc_url( 'https://buddypress.org/support/' )
+					);
+					?>
+				</p>
+				<p><?php esc_html_e( 'Thank you for using BuddyPress! 😊', 'buddypress' ); ?></p>
+
+				<br /><br />
+			</div>
+
+			<div class="bp-hello-footer">
+				<div class="bp-hello-social-cta">
+					<p>
+						<?php
+						printf(
+							_n( 'Built by <a href="%s">%s volunteer</a>.', 'Built by <a href="%s">%s volunteers</a>.', 57, 'buddypress' ),
+							esc_url( bp_get_admin_url( 'admin.php?page=bp-credits' ) ),
+							number_format_i18n( 57 )
+						);
+						?>
+					</p>
+				</div>
+
+				<div class="bp-hello-social-links">
+					<ul class="bp-hello-social">
+						<li>
+							<?php
+							printf(
+								'<a class="twitter bp-tooltip" data-bp-tooltip="%1$s" href="%2$s"><span class="screen-reader-text">%3$s</span></a>',
+								esc_attr( 'Follow BuddyPress on Twitter', 'buddypress' ),
+								esc_url( 'https://twitter.com/buddypress' ),
+								esc_html( 'Follow BuddyPress on Twitter', 'buddypress' )
+							);
+							?>
+						</li>
+
+						<li>
+							<?php
+							printf(
+								'<a class="support bp-tooltip" data-bp-tooltip="%1$s" href="%2$s"><span class="screen-reader-text">%3$s</span></a>',
+								esc_attr( 'Visit the Support Forums', 'buddypress' ),
+								esc_url( 'https://buddypress.org/support/' ),
+								esc_html( 'Visit the Support Forums', 'buddypress' )
+							);
+							?>
+						</li>
+					</ul>
+				</div>
+			</div>
+		</div>
+
+		<?php
+	}
+
+	/**
+	 * Output the credits screen.
+	 *
+	 * Hardcoding this in here is pretty janky. It's fine for now, but we'll
+	 * want to leverage api.wordpress.org eventually.
+	 *
+	 * @since 1.7.0
+	 */
+	public function credits_screen() {
+	?>
+
+		<div class="wrap bp-about-wrap">
+
+		<h1><?php _e( 'BuddyPress Settings', 'buddypress' ); ?> </h1>
+
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Credits', 'buddypress' ) ); ?></h2>
+
+			<p class="about-description"><?php _e( 'Meet the contributors behind BuddyPress:', 'buddypress' ); ?></p>
+
+			<h3 class="wp-people-group"><?php _e( 'Project Leaders', 'buddypress' ); ?></h3>
+			<ul class="wp-people-group " id="wp-people-group-project-leaders">
+				<li class="wp-person" id="wp-person-johnjamesjacoby">
+					<a class="web" href="https://profiles.wordpress.org/johnjamesjacoby"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/7a2644fb53ae2f7bfd7143b504af396c?s=120">
+					John James Jacoby</a>
+					<span class="title"><?php _e( 'Project Lead', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-boonebgorges">
+					<a class="web" href="https://profiles.wordpress.org/boonebgorges"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/9cf7c4541a582729a5fc7ae484786c0c?s=120">
+					Boone B. Gorges</a>
+					<span class="title"><?php _e( 'Lead Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-djpaul">
+					<a class="web" href="https://profiles.wordpress.org/djpaul"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/3bc9ab796299d67ce83dceb9554f75df?s=120">
+					Paul Gibbs</a>
+					<span class="title"><?php _e( 'Release Lead', 'buddypress' ); ?></span>
+				</li>
+			</ul>
+
+			<h3 class="wp-people-group"><?php _e( 'BuddyPress Team', 'buddypress' ); ?></h3>
+			<ul class="wp-people-group " id="wp-people-group-core-team">
+				<li class="wp-person" id="wp-person-r-a-y">
+					<a class="web" href="https://profiles.wordpress.org/r-a-y"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/3bfa556a62b5bfac1012b6ba5f42ebfa?s=120">
+					Ray</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-hnla">
+					<a class="web" href="https://profiles.wordpress.org/hnla"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/3860c955aa3f79f13b92826ae47d07fe?s=120">
+					Hugo Ashmore</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-imath">
+					<a class="web" href="https://profiles.wordpress.org/imath"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/8b208ca408dad63888253ee1800d6a03?s=120">
+					Mathieu Viet</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-mercime">
+					<a class="web" href="https://profiles.wordpress.org/mercime"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/fae451be6708241627983570a1a1817a?s=120">
+					Mercime</a>
+					<span class="title"><?php _e( 'Navigator', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-dcavins">
+					<a class="web" href="https://profiles.wordpress.org/dcavins"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/a5fa7e83d59cb45ebb616235a176595a?s=120">
+					David Cavins</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-tw2113">
+					<a class="web" href="https://profiles.wordpress.org/tw2113"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/a5d7c934621fa1c025b83ee79bc62366?s=120">
+					Michael Beckwith</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-henry-wright">
+					<a class="web" href="https://profiles.wordpress.org/henry.wright"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/0da2f1a9340d6af196b870f6c107a248?s=120">
+					Henry Wright</a>
+					<span class="title"><?php _e( 'Community Support', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-danbp">
+					<a class="web" href="https://profiles.wordpress.org/danbp"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/0deae2e7003027fbf153500cd3fa5501?s=120">
+					danbp</a>
+					<span class="title"><?php _e( 'Community Support', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-shanebp">
+					<a class="web" href="https://profiles.wordpress.org/shanebp"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/ffd294ab5833ba14aaf175f9acc71cc4?s=120">
+					shanebp</a>
+					<span class="title"><?php _e( 'Community Support', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-slaffik">
+					<a class="web" href="https://profiles.wordpress.org/r-a-y"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/61fb07ede3247b63f19015f200b3eb2c?s=120">
+					Slava Abakumov</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-offereins">
+					<a class="web" href="https://profiles.wordpress.org/Offereins"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/2404ed0a35bb41aedefd42b0a7be61c1?s=120">
+					Laurens Offereins</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-netweb">
+					<a class="web" href="https://profiles.wordpress.org/netweb"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/97e1620b501da675315ba7cfb740e80f?s=120">
+					Stephen Edgar</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-espellcaste">
+					<a class="web" href="https://profiles.wordpress.org/espellcaste"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/b691e67be0ba5cad6373770656686bc3?s=120">
+					Renato Alves</a>
+					<span class="title"><?php _e( 'Core Developer', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-venutius">
+					<a class="web" href="https://profiles.wordpress.org/venutius"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/6a7c42a77fd94b82b217a7a97afdddbc?s=120">
+					Venutius</a>
+					<span class="title"><?php _e( 'Community Support', 'buddypress' ); ?></span>
+				</li>
+			</ul>
+
+			<h3 class="wp-people-group"><?php _e( 'Recent Rockstars', 'buddypress' ); ?></h3>
+			<ul class="wp-people-group " id="wp-people-group-rockstars">
+				<li class="wp-person" id="wp-person-dimensionmedia">
+					<a class="web" href="https://profiles.wordpress.org/dimensionmedia"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/7735aada1ec39d0c1118bd92ed4551f1?s=120">
+					David Bisset</a>
+				</li>
+				<li class="wp-person" id="wp-person-garrett-eclipse">
+					<a class="web" href="https://profiles.wordpress.org/garrett-eclipse"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/7f68f24441c61514d5d0e1451bb5bc9d?s=120">
+					Garrett Hyder</a>
+				</li>
+				<li class="wp-person" id="wp-person-thebrandonallen">
+					<a class="web" href="https://profiles.wordpress.org/thebrandonallen"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/6d3f77bf3c9ca94c406dea401b566950?s=120">
+					Brandon Allen</a>
+				</li>
+				<li class="wp-person" id="wp-person-ramiy">
+					<a class="web" href="https://profiles.wordpress.org/ramiy"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/ce2a269e424156d79cb0c4e1d4d82db1?s=120">
+					Rami Yushuvaev</a>
+				</li>
+				<li class="wp-person" id="wp-person-vapvarun">
+					<a class="web" href="https://profiles.wordpress.org/vapvarun"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/78a3bf7eb3a1132fc667f96f2631e448?s=120">
+					Vapvarun</a>
+				</li>
+			</ul>
+
+			<h3 class="wp-people-group"><?php printf( esc_html__( 'Contributors to BuddyPress %s', 'buddypress' ), self::display_version() ); ?></h3>
+			<p class="wp-credits-list">
+				<a href="https://profiles.wordpress.org/1naveengiri">1naveengiri</a>,
+				<a href="https://profiles.wordpress.org/abhishekfdd/">Abhishek Kumar (abhishekfdd)</a>,
+				<a href="https://profiles.wordpress.org/andrewteg/">Andrew Tegenkamp (andrewteg)</a>,
+				<a href="https://profiles.wordpress.org/ankit-k-gupta/">Ankit K Gupta (ankit-k-gupta)</a>,
+				<a href="https://profiles.wordpress.org/antonioeatgoat/">Antonio Mangiacapra (antonioeatgoat)</a>,
+				<a href="https://profiles.wordpress.org/boonebgorges/">Boone B Gorges (boonebgorges)</a>,
+				<a href="https://profiles.wordpress.org/thebrandonallen/">Brandon Allen (thebrandonallen)</a>,
+				<a href="https://profiles.wordpress.org/brandonliles/">brandonliles</a>,
+				<a href="https://profiles.wordpress.org/sbrajesh/">Brajesh Singh (sbrajesh)</a>,
+				<a href="https://profiles.wordpress.org/ketuchetan/">chetansatasiya (ketuchetan)</a>,
+				<a href="https://profiles.wordpress.org/chherbst/">chherbst</a>,
+				<a href="https://profiles.wordpress.org/needle/">Christian Wach (needle)</a>,
+				<a href="https://profiles.wordpress.org/coach-afrane/">Coach Afrane</a>,
+				<a href="https://profiles.wordpress.org/cshinkin/">cshinkin</a>,
+				<a href="https://profiles.wordpress.org/danbp/">danbp</a>,
+				<a href="https://profiles.wordpress.org/dcavins/">David Cavins (dcavins)</a>,
+				<a href="https://profiles.wordpress.org/devitate/">devitate</a>,
+				<a href="https://profiles.wordpress.org/garrett-eclipse/">Garrett Hyder (garrett-eclipse)</a>,
+				<a href="https://profiles.wordpress.org/geminorum/">geminorum</a>,
+				<a href="https://profiles.wordpress.org/Mamaduka/">George Mamadashvili (Mamaduka)</a>,
+				<a href="https://profiles.wordpress.org/januzi_pl/">januzi_pl</a>,
+				<a href="https://profiles.wordpress.org/jcrr/">jcrr</a>,
+				<a href="https://profiles.wordpress.org/jdgrimes/">J.D. Grimes (jdgrimes)</a>,
+				<a href="https://profiles.wordpress.org/JohnPBloch/">John P. Bloch (JohnPBloch)</a>,
+				<a href="https://profiles.wordpress.org/joost-abrahams/">Joost Abrahams (joost-abrahams)</a>,
+				<a href="https://profiles.wordpress.org/henry.wright">Henry Wright (henry.wright)</a>,
+				<a href="https://profiles.wordpress.org/hnla/">Hugo (hnla)</a>,
+				<a href="https://profiles.wordpress.org/idofri/">Ido Friedlander (idofri)</a>,
+				<a href="https://profiles.wordpress.org/dunhakdis/">Joseph G. (dunhakdis)</a>,
+				<a href="https://profiles.wordpress.org/johnjamesjacoby/">John James Jacoby (johnjamesjacoby)</a>,
+				<a href="https://profiles.wordpress.org/Offereins">Laurens Offereins (Offereins)</a>,
+				<a href="https://profiles.wordpress.org/mechter/">Markus Echterhoff (mechter)</a>,
+				<a href="https://profiles.wordpress.org/imath/">Mathieu Viet (imath)</a>,
+				<a href="https://profiles.wordpress.org/meitar/">meitar</a>,
+				<a href="https://profiles.wordpress.org/mercime/">mercime</a>,
+				<a href="https://profiles.wordpress.org/tw2113/">Michael Beckwith (tw2113)</a>,
+				<a href="https://profiles.wordpress.org/mauteri/">Mike Auteri (mauteri)</a>,
+				<a href="https://profiles.wordpress.org/modemlooper/">modemlooper</a>,
+				<a href="https://profiles.wordpress.org/m_uysl/">Mustafa Uysal (m_uysl)</a>,
+				<a href="https://profiles.wordpress.org/pareshradadiya/">paresh.radadiya (pareshradadiya)</a>,
+				<a href="https://profiles.wordpress.org/DJPaul/">Paul Gibbs (DJPaul)</a>,
+				<a href="https://profiles.wordpress.org/pavloopanasenko/">pavlo.opanasenko (pavloopanasenko)</a>,
+				<a href="https://profiles.wordpress.org/pscolv/">pscolv</a>,
+				<a href="https://profiles.wordpress.org/r-a-y/">r-a-y</a>,
+				<a href="https://profiles.wordpress.org/rachelbaker/">Rachel Baker (rachelbaker)</a>,
+				<a href="https://profiles.wordpress.org/rekmla/">rekmla</a>,
+				<a href="https://profiles.wordpress.org/espellcaste/">Renato Alves (espellcaste)</a>,
+				<a href="https://profiles.wordpress.org/rianrietveld/">Rian Rietveld (rianrietvelde)</a>,
+				<a href="https://profiles.wordpress.org/ripstechcom/">ripstechcom</a>,
+				<a href="https://profiles.wordpress.org/cyclic/">Ryan Williams (cyclic)</a>,
+				<a href="https://profiles.wordpress.org/slaffik/">Slava Abakumov (slaffik)</a>,
+				<a href="https://profiles.wordpress.org/netweb/">Stephen Edgar (netweb)</a>,
+				<a href="https://profiles.wordpress.org/tobiashonold/">Tobias Honold (tobiashonold)</a>,
+				<a href="https://profiles.wordpress.org/uzosky/">uzosky</a>,
+				<a href="https://profiles.wordpress.org/vapvarun/">vapvarun</a>,
+				<a href="https://profiles.wordpress.org/Venutius/">Venutius</a>,
+				<a href="https://profiles.wordpress.org/yahil/">Yahil Madakiya (yahil)</a>
+			</p>
+
+			<h3 class="wp-people-group"><?php _e( 'With our thanks to these Open Source projects', 'buddypress' ); ?></h3>
+			<p class="wp-credits-list">
+				<a href="https://github.com/ichord/At.js">At.js</a>,
+				<a href="https://bbpress.org">bbPress</a>,
+				<a href="https://github.com/ichord/Caret.js">Caret.js</a>,
+				<a href="https://tedgoas.github.io/Cerberus/">Cerberus</a>,
+				<a href="https://ionicons.com/">Ionicons</a>,
+				<a href="https://github.com/carhartl/jquery-cookie">jquery.cookie</a>,
+				<a href="https://mattbradley.github.io/livestampjs/">Livestamp.js</a>,
+				<a href="https://www.mediawiki.org/wiki/MediaWiki">MediaWiki</a>,
+				<a href="https://momentjs.com/">Moment.js</a>,
+				<a href="https://wordpress.org">WordPress</a>.
+			</p>
+
+			<h3 class="wp-people-group"><?php _e( 'Contributor Emeriti', 'buddypress' ); ?></h3>
+			<ul class="wp-people-group " id="wp-people-group-emeriti">
+				<li class="wp-person" id="wp-person-apeatling">
+					<a class="web" href="https://profiles.wordpress.org/johnjamesjacoby"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/bb29d699b5cba218c313b61aa82249da?s=120">
+					Andy Peatling</a>
+					<span class="title"><?php _e( 'Project Founder', 'buddypress' ); ?></span>
+				</li>
+				<li class="wp-person" id="wp-person-burtadsit">
+					<a class="web" href="https://profiles.wordpress.org/burtadsit"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/185e1d3e2d653af9d49a4e8e4fc379df?s=120">
+					Burt Adsit</a>
+				</li>
+				<li class="wp-person" id="wp-person-jeffsayre">
+					<a class="web" href="https://profiles.wordpress.org/jeffsayre"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/8e009a84ff5d245c22a69c7df6ab45f7?s=120">
+					Jeff Sayre</a>
+				</li>
+				<li class="wp-person" id="wp-person-karmatosed">
+					<a class="web" href="https://profiles.wordpress.org/karmatosed"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/ca7d4273a689cdbf524d8332771bb1ca?s=120">
+					Tammie Lister</a>
+				</li>
+				<li class="wp-person" id="wp-person-modemlooper">
+					<a class="web" href="https://profiles.wordpress.org/modemlooper"><img alt="" class="gravatar" src="//www.gravatar.com/avatar/1c07be1016e845de514931477c939307?s=120">
+					modemlooper</a>
+				</li>
+			</ul>
+		</div>
+
+		<?php
+	}
+
+	/** Emails ****************************************************************/
+
+	/**
+	 * Registers 'Situations' column on Emails dashboard page.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $columns Current column data.
+	 * @return array
+	 */
+	public function emails_register_situation_column( $columns = array() ) {
+		$situation = array(
+			'situation' => _x( 'Situations', 'Email post type', 'buddypress' )
+		);
+
+		// Inject our 'Situations' column just before the last 'Date' column.
+		return array_slice( $columns, 0, -1, true ) + $situation + array_slice( $columns, -1, null, true );
+	}
+
+	/**
+	 * Output column data for our custom 'Situations' column.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $column  Current column name.
+	 * @param int    $post_id Current post ID.
+	 */
+	public function emails_display_situation_column_data( $column = '', $post_id = 0 ) {
+		if ( 'situation' !== $column ) {
+			return;
+		}
+
+		// Grab email situations for the current post.
+		$situations = wp_list_pluck( get_the_terms( $post_id, bp_get_email_tax_type() ), 'description' );
+
+		// Output each situation as a list item.
+		echo '<ul><li>';
+		echo implode( '</li><li>', $situations );
+		echo '</li></ul>';
+	}
+
+	/** Helpers ***************************************************************/
+
+	/**
+	 * Return true/false based on whether a query argument is set.
+	 *
+	 * @see bp_do_activation_redirect()
+	 *
+	 * @since 2.2.0
+	 *
+	 * @return bool
+	 */
+	public static function is_new_install() {
+		return (bool) isset( $_GET['is_new_install'] );
+	}
+
+	/**
+	 * Return a user-friendly version-number string, for use in translations.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @return string
+	 */
+	public static function display_version() {
+
+		// Use static variable to prevent recalculations.
+		static $display = '';
+
+		// Only calculate on first run.
+		if ( '' === $display ) {
+
+			// Get current version.
+			$version = bp_get_version();
+
+			// Check for prerelease hyphen.
+			$pre     = strpos( $version, '-' );
+
+			// Strip prerelease suffix.
+			$display = ( false !== $pre )
+				? substr( $version, 0, $pre )
+				: $version;
+		}
+
+		// Done!
+		return $display;
+	}
+
+	/**
+	 * Add Emails menu item to custom menus array.
+	 *
+	 * Several BuddyPress components have top-level menu items in the Dashboard,
+	 * which all appear together in the middle of the Dashboard menu. This function
+	 * adds the Emails screen to the array of these menu items.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $custom_menus The list of top-level BP menu items.
+	 * @return array $custom_menus List of top-level BP menu items, with Emails added.
+	 */
+	public function emails_admin_menu_order( $custom_menus = array() ) {
+		array_push( $custom_menus, 'edit.php?post_type=' . bp_get_email_post_type() );
+
+		if ( is_network_admin() && bp_is_network_activated() ) {
+			array_push(
+				$custom_menus,
+				get_admin_url( bp_get_root_blog_id(), 'edit.php?post_type=' . bp_get_email_post_type() )
+			);
+		}
+
+		return $custom_menus;
+	}
+
+	/**
+	 * Register styles commonly used by BuddyPress wp-admin screens.
+	 *
+	 * @since 2.5.0
+	 */
+	public function admin_register_styles() {
+		$min = bp_core_get_minified_asset_suffix();
+		$url = $this->css_url;
+
+		/**
+		 * Filters the BuddyPress Core Admin CSS file path.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $file File path for the admin CSS.
+		 */
+		$common_css = apply_filters( 'bp_core_admin_common_css', "{$url}common{$min}.css" );
+
+		/**
+		 * Filters the BuddyPress admin stylesheet files to register.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param array $value Array of admin stylesheet file information to register.
+		 */
+		$styles = apply_filters( 'bp_core_admin_register_styles', array(
+			// Legacy.
+			'bp-admin-common-css' => array(
+				'file'         => $common_css,
+				'dependencies' => array(),
+			),
+
+			// 2.5
+			'bp-customizer-controls' => array(
+				'file'         => "{$url}customizer-controls{$min}.css",
+				'dependencies' => array(),
+			),
+
+			// 3.0
+			'bp-hello-css' => array(
+				'file'         => "{$url}hello{$min}.css",
+				'dependencies' => array( 'bp-admin-common-css' ),
+			),
+		) );
+
+		$version = bp_get_version();
+
+		foreach ( $styles as $id => $style ) {
+			wp_register_style( $id, $style['file'], $style['dependencies'], $version );
+			wp_style_add_data( $id, 'rtl', true );
+
+			if ( $min ) {
+				wp_style_add_data( $id, 'suffix', $min );
+			}
+		}
+	}
+
+	/**
+	 * Register JS commonly used by BuddyPress wp-admin screens.
+	 *
+	 * @since 2.5.0
+	 */
+	public function admin_register_scripts() {
+		$min = bp_core_get_minified_asset_suffix();
+		$url = $this->js_url;
+
+		/**
+		 * Filters the BuddyPress admin JS files to register.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param array $value Array of admin JS file information to register.
+		 */
+		$scripts = apply_filters( 'bp_core_admin_register_scripts', array(
+			// 2.5
+			'bp-customizer-controls' => array(
+				'file'         => "{$url}customizer-controls{$min}.js",
+				'dependencies' => array( 'jquery' ),
+				'footer'       => true,
+			),
+
+			// 3.0
+			'bp-hello-js' => array(
+				'file'         => "{$url}hello{$min}.js",
+				'dependencies' => array(),
+				'footer'       => true,
+			),
+		) );
+
+		$version = bp_get_version();
+
+		foreach ( $scripts as $id => $script ) {
+			wp_register_script( $id, $script['file'], $script['dependencies'], $version, $script['footer'] );
+		}
+	}
+}
+endif; // End class_exists check.
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment-avatar.php	(revision 12265)
@@ -0,0 +1,435 @@
+<?php
+/**
+ * Core Avatars attachment class.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BP Attachment Avatar class.
+ *
+ * Extends BP Attachment to manage the avatar uploads.
+ *
+ * @since 2.3.0
+ */
+class BP_Attachment_Avatar extends BP_Attachment {
+
+	/**
+	 * Construct Upload parameters.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @see  BP_Attachment::__construct() for list of parameters
+	 */
+	public function __construct() {
+		// Allowed avatar types.
+		$allowed_types = bp_core_get_allowed_avatar_types();
+
+		parent::__construct( array(
+			'action'                => 'bp_avatar_upload',
+			'file_input'            => 'file',
+			'original_max_filesize' => bp_core_avatar_original_max_filesize(),
+
+			// Specific errors for avatars.
+			'upload_error_strings'  => array(
+				9  => sprintf( __( 'That photo is too big. Please upload one smaller than %s', 'buddypress' ), size_format( bp_core_avatar_original_max_filesize() ) ),
+				10 => sprintf( _n( 'Please upload only this file type: %s.', 'Please upload only these file types: %s.', count( $allowed_types ), 'buddypress' ), self::get_avatar_types( $allowed_types ) ),
+			),
+		) );
+	}
+
+	/**
+	 * Gets the available avatar types.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $allowed_types Array of allowed avatar types.
+	 * @return string comma separated list of allowed avatar types.
+	 */
+	public static function get_avatar_types( $allowed_types = array() ) {
+		$types = array_map( 'strtoupper', $allowed_types );
+		$comma = _x( ',', 'avatar types separator', 'buddypress' );
+		return join( $comma . ' ', $types );
+	}
+
+	/**
+	 * Set Upload Dir data for avatars.
+	 *
+	 * @since 2.3.0
+	 */
+	public function set_upload_dir() {
+		if ( bp_core_avatar_upload_path() && bp_core_avatar_url() ) {
+			$this->upload_path = bp_core_avatar_upload_path();
+			$this->url         = bp_core_avatar_url();
+			$this->upload_dir  = bp_upload_dir();
+		} else {
+			parent::set_upload_dir();
+		}
+	}
+
+	/**
+	 * Avatar specific rules.
+	 *
+	 * Adds an error if the avatar size or type don't match BuddyPress needs.
+	 * The error code is the index of $upload_error_strings.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $file the temporary file attributes (before it has been moved).
+	 * @return array the file with extra errors if needed.
+	 */
+	public function validate_upload( $file = array() ) {
+		// Bail if already an error.
+		if ( ! empty( $file['error'] ) ) {
+			return $file;
+		}
+
+		// File size is too big.
+		if ( ! bp_core_check_avatar_size( array( 'file' => $file ) ) ) {
+			$file['error'] = 9;
+
+		// File is of invalid type.
+		} elseif ( ! bp_core_check_avatar_type( array( 'file' => $file ) ) ) {
+			$file['error'] = 10;
+		}
+
+		// Return with error code attached.
+		return $file;
+	}
+
+	/**
+	 * Maybe shrink the attachment to fit maximum allowed width.
+	 *
+	 * @since 2.3.0
+	 * @since 2.4.0 Add the $ui_available_width parameter, to inform about the Avatar UI width.
+	 *
+	 * @param string $file               The absolute path to the file.
+	 * @param int    $ui_available_width Available width for the UI.
+	 * @return false|string|WP_Image_Editor|WP_Error
+	 */
+	public static function shrink( $file = '', $ui_available_width = 0 ) {
+		// Get image size.
+		$avatar_data = parent::get_image_data( $file );
+
+		// Init the edit args.
+		$edit_args = array();
+
+		// Defaults to the Avatar original max width constant.
+		$original_max_width = bp_core_avatar_original_max_width();
+
+		// The ui_available_width is defined and it's smaller than the Avatar original max width.
+		if ( ! empty( $ui_available_width ) && $ui_available_width < $original_max_width ) {
+			/**
+			 * In this case, to make sure the content of the image will be fully displayed
+			 * during the cropping step, let's use the Avatar UI Available width.
+			 */
+			$original_max_width = $ui_available_width;
+
+			// $original_max_width has to be larger than the avatar's full width
+			if ( $original_max_width < bp_core_avatar_full_width() ) {
+				$original_max_width = bp_core_avatar_full_width();
+			}
+		}
+
+		// Do we need to resize the image?
+		if ( isset( $avatar_data['width'] ) && $avatar_data['width'] > $original_max_width ) {
+			$edit_args = array(
+				'max_w' => $original_max_width,
+				'max_h' => $original_max_width,
+			);
+		}
+
+		// Do we need to rotate the image?
+		$angles = array(
+			3 => 180,
+			6 => -90,
+			8 =>  90,
+		);
+
+		if ( isset( $avatar_data['meta']['orientation'] ) && isset( $angles[ $avatar_data['meta']['orientation'] ] ) ) {
+			$edit_args['rotate'] = $angles[ $avatar_data['meta']['orientation'] ];
+		}
+
+		// No need to edit the avatar, original file will be used.
+		if ( empty( $edit_args ) ) {
+			return false;
+
+		// Add the file to the edit arguments.
+		} else {
+			$edit_args['file'] = $file;
+		}
+
+		return parent::edit_image( 'avatar', $edit_args );
+	}
+
+	/**
+	 * Check if the image dimensions are smaller than full avatar dimensions.
+	 *
+	 * @since 2.3.0
+	 *
+	 *
+	 * @param string $file the absolute path to the file.
+	 * @return bool
+	 */
+	public static function is_too_small( $file = '' ) {
+		$uploaded_image = @getimagesize( $file );
+		$full_width     = bp_core_avatar_full_width();
+		$full_height    = bp_core_avatar_full_height();
+
+		if ( isset( $uploaded_image[0] ) && $uploaded_image[0] < $full_width || $uploaded_image[1] < $full_height ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Crop the avatar.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @see  BP_Attachment::crop for the list of parameters
+	 *
+	 * @param array $args Array of arguments for the cropping.
+	 * @return array The cropped avatars (full and thumb).
+	 */
+	public function crop( $args = array() ) {
+		// Bail if the original file is missing.
+		if ( empty( $args['original_file'] ) ) {
+			return false;
+		}
+
+		if ( ! bp_attachments_current_user_can( 'edit_avatar', $args ) ) {
+			return false;
+		}
+
+		if ( 'user' === $args['object'] ) {
+			$avatar_dir = 'avatars';
+		} else {
+			$avatar_dir = sanitize_key( $args['object'] ) . '-avatars';
+		}
+
+		$args['item_id'] = (int) $args['item_id'];
+
+		/**
+		 * Original file is a relative path to the image
+		 * eg: /avatars/1/avatar.jpg
+		 */
+		$relative_path = sprintf( '/%s/%s/%s', $avatar_dir, $args['item_id'], basename( $args['original_file'] ) );
+		$absolute_path = $this->upload_path . $relative_path;
+
+		// Bail if the avatar is not available.
+		if ( ! file_exists( $absolute_path ) )  {
+			return false;
+		}
+
+		if ( empty( $args['item_id'] ) ) {
+
+			/** This filter is documented in bp-core/bp-core-avatars.php */
+			$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', dirname( $absolute_path ), $args['item_id'], $args['object'], $args['avatar_dir'] );
+		} else {
+
+			/** This filter is documented in bp-core/bp-core-avatars.php */
+			$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', $this->upload_path . '/' . $args['avatar_dir'] . '/' . $args['item_id'], $args['item_id'], $args['object'], $args['avatar_dir'] );
+		}
+
+		// Bail if the avatar folder is missing for this item_id.
+		if ( ! file_exists( $avatar_folder_dir ) ) {
+			return false;
+		}
+
+		// Delete the existing avatar files for the object.
+		$existing_avatar = bp_core_fetch_avatar( array(
+			'object'  => $args['object'],
+			'item_id' => $args['item_id'],
+			'html' => false,
+		) );
+
+		/**
+		 * Check that the new avatar doesn't have the same name as the
+		 * old one before deleting
+		 */
+		if ( ! empty( $existing_avatar ) && $existing_avatar !== $this->url . $relative_path ) {
+			bp_core_delete_existing_avatar( array( 'object' => $args['object'], 'item_id' => $args['item_id'], 'avatar_path' => $avatar_folder_dir ) );
+		}
+
+		// Make sure we at least have minimal data for cropping.
+		if ( empty( $args['crop_w'] ) ) {
+			$args['crop_w'] = bp_core_avatar_full_width();
+		}
+
+		if ( empty( $args['crop_h'] ) ) {
+			$args['crop_h'] = bp_core_avatar_full_height();
+		}
+
+		// Get the file extension.
+		$data = @getimagesize( $absolute_path );
+		$ext  = $data['mime'] == 'image/png' ? 'png' : 'jpg';
+
+		$args['original_file'] = $absolute_path;
+		$args['src_abs']       = false;
+		$avatar_types = array( 'full' => '', 'thumb' => '' );
+
+		foreach ( $avatar_types as $key_type => $type ) {
+			if ( 'thumb' === $key_type ) {
+				$args['dst_w'] = bp_core_avatar_thumb_width();
+				$args['dst_h'] = bp_core_avatar_thumb_height();
+			} else {
+				$args['dst_w'] = bp_core_avatar_full_width();
+				$args['dst_h'] = bp_core_avatar_full_height();
+			}
+
+			$filename         = wp_unique_filename( $avatar_folder_dir, uniqid() . "-bp{$key_type}.{$ext}" );
+			$args['dst_file'] = $avatar_folder_dir . '/' . $filename;
+
+			$avatar_types[ $key_type ] = parent::crop( $args );
+		}
+
+		// Remove the original.
+		@unlink( $absolute_path );
+
+		// Return the full and thumb cropped avatars.
+		return $avatar_types;
+	}
+
+	/**
+	 * Get the user id to set its avatar.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return integer The user ID.
+	 */
+	private function get_user_id() {
+		$bp = buddypress();
+		$user_id = 0;
+
+		if ( bp_is_user() ) {
+			$user_id = bp_displayed_user_id();
+		}
+
+		if ( ! empty( $bp->members->admin->user_id ) ) {
+			$user_id = $bp->members->admin->user_id;
+		}
+
+		return $user_id;
+	}
+
+	/**
+	 * Get the group id to set its avatar.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return integer The group ID.
+	 */
+	private function get_group_id() {
+		$group_id = 0;
+
+		if ( bp_is_group() ) {
+			$group_id = bp_get_current_group_id();
+		}
+
+		return $group_id;
+	}
+
+	/**
+	 * Build script datas for the Uploader UI.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return array The javascript localization data.
+	 */
+	public function script_data() {
+		// Get default script data.
+		$script_data = parent::script_data();
+
+		// Defaults to Avatar Backbone script.
+		$js_scripts = array( 'bp-avatar' );
+
+		// Default object.
+		$object = '';
+
+		// Get the possible item ids.
+		$user_id  = $this->get_user_id();
+		$group_id = $this->get_group_id();
+
+		if ( ! empty( $user_id ) ) {
+			// Should we load the the Webcam Avatar javascript file.
+			if ( bp_avatar_use_webcam() ) {
+				$js_scripts = array( 'bp-webcam' );
+			}
+
+			$script_data['bp_params'] = array(
+				'object'     => 'user',
+				'item_id'    => $user_id,
+				'has_avatar' => bp_get_user_has_avatar( $user_id ),
+				'nonces'  => array(
+					'set'    => wp_create_nonce( 'bp_avatar_cropstore' ),
+					'remove' => wp_create_nonce( 'bp_delete_avatar_link' ),
+				),
+			);
+
+			// Set feedback messages.
+			$script_data['feedback_messages'] = array(
+				1 => __( 'There was a problem cropping your profile photo.', 'buddypress' ),
+				2 => __( 'Your new profile photo was uploaded successfully.', 'buddypress' ),
+				3 => __( 'There was a problem deleting your profile photo. Please try again.', 'buddypress' ),
+				4 => __( 'Your profile photo was deleted successfully!', 'buddypress' ),
+			);
+		} elseif ( ! empty( $group_id ) ) {
+			$script_data['bp_params'] = array(
+				'object'     => 'group',
+				'item_id'    => $group_id,
+				'has_avatar' => bp_get_group_has_avatar( $group_id ),
+				'nonces'     => array(
+					'set'    => wp_create_nonce( 'bp_avatar_cropstore' ),
+					'remove' => wp_create_nonce( 'bp_group_avatar_delete' ),
+				),
+			);
+
+			// Set feedback messages.
+			$script_data['feedback_messages'] = array(
+				1 => __( 'There was a problem cropping the group profile photo.', 'buddypress' ),
+				2 => __( 'The group profile photo was uploaded successfully.', 'buddypress' ),
+				3 => __( 'There was a problem deleting the group profile photo. Please try again.', 'buddypress' ),
+				4 => __( 'The group profile photo was deleted successfully!', 'buddypress' ),
+			);
+		} else {
+
+			/**
+			 * Use this filter to include specific BuddyPress params for your object.
+			 * e.g. Blavatar.
+			 *
+			 * @since 2.3.0
+			 *
+			 * @param array $value The avatar specific BuddyPress parameters.
+			 */
+			$script_data['bp_params'] = apply_filters( 'bp_attachment_avatar_params', array() );
+		}
+
+		// Include the specific css.
+		$script_data['extra_css'] = array( 'bp-avatar' );
+
+		// Include the specific css.
+		$script_data['extra_js']  = $js_scripts;
+
+		// Set the object to contextualize the filter.
+		if ( isset( $script_data['bp_params']['object'] ) ) {
+			$object = $script_data['bp_params']['object'];
+		}
+
+		/**
+		 * Use this filter to override/extend the avatar script data.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $script_data The avatar script data.
+		 * @param string $object      The object the avatar belongs to (eg: user or group).
+		 */
+		return apply_filters( 'bp_attachment_avatar_script_data', $script_data, $object );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment-cover-image.php	(revision 12265)
@@ -0,0 +1,258 @@
+<?php
+/**
+ * Core Cover Image attachment class.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.4.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BP Attachment Cover Image class.
+ *
+ * Extends BP Attachment to manage the cover images uploads.
+ *
+ * @since 2.4.0
+ */
+class BP_Attachment_Cover_Image extends BP_Attachment {
+	/**
+	 * The constuctor.
+	 *
+	 * @since 2.4.0
+	 */
+	public function __construct() {
+		// Allowed cover image types & upload size.
+		$allowed_types        = bp_attachments_get_allowed_types( 'cover_image' );
+		$max_upload_file_size = bp_attachments_get_max_upload_file_size( 'cover_image' );
+
+		parent::__construct( array(
+			'action'                => 'bp_cover_image_upload',
+			'file_input'            => 'file',
+			'original_max_filesize' => $max_upload_file_size,
+			'base_dir'              => bp_attachments_uploads_dir_get( 'dir' ),
+			'required_wp_files'     => array( 'file', 'image' ),
+
+			// Specific errors for cover images.
+			'upload_error_strings'  => array(
+				11  => sprintf( __( 'That image is too big. Please upload one smaller than %s', 'buddypress' ), size_format( $max_upload_file_size ) ),
+				12  => sprintf( _n( 'Please upload only this file type: %s.', 'Please upload only these file types: %s.', count( $allowed_types ), 'buddypress' ), self::get_cover_image_types( $allowed_types ) ),
+			),
+		) );
+	}
+
+	/**
+	 * Gets the available cover image types.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $allowed_types Array of allowed cover image types.
+	 * @return string $value Comma-separated list of allowed cover image types.
+	 */
+	public static function get_cover_image_types( $allowed_types = array() ) {
+		$types = array_map( 'strtoupper', $allowed_types );
+		$comma = _x( ',', 'cover image types separator', 'buddypress' );
+		return join( $comma . ' ', $types );
+	}
+
+	/**
+	 * Cover image specific rules.
+	 *
+	 * Adds an error if the cover image size or type don't match BuddyPress needs.
+	 * The error code is the index of $upload_error_strings.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $file The temporary file attributes (before it has been moved).
+	 * @return array $file The file with extra errors if needed.
+	 */
+	public function validate_upload( $file = array() ) {
+		// Bail if already an error.
+		if ( ! empty( $file['error'] ) ) {
+			return $file;
+		}
+
+		// File size is too big.
+		if ( $file['size'] > $this->original_max_filesize ) {
+			$file['error'] = 11;
+
+		// File is of invalid type.
+		} elseif ( ! bp_attachments_check_filetype( $file['tmp_name'], $file['name'], bp_attachments_get_allowed_mimes( 'cover_image' ) ) ) {
+			$file['error'] = 12;
+		}
+
+		// Return with error code attached.
+		return $file;
+	}
+
+	/**
+	 * Set the directory when uploading a file.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $upload_dir The original Uploads dir.
+	 * @return array $value Upload data (path, url, basedir...).
+	 */
+	public function upload_dir_filter( $upload_dir = array() ) {
+		return bp_attachments_cover_image_upload_dir();
+	}
+
+	/**
+	 * Adjust the cover image to fit with advised width & height.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $file       The absolute path to the file.
+	 * @param array  $dimensions Array of dimensions for the cover image.
+	 * @return mixed
+	 */
+	public function fit( $file = '', $dimensions = array() ) {
+		if ( empty( $dimensions['width'] ) || empty( $dimensions['height'] ) ) {
+			return false;
+		}
+
+		// Get image size.
+		$cover_data = parent::get_image_data( $file );
+
+		// Init the edit args.
+		$edit_args = array();
+
+		// Do we need to resize the image?
+		if ( ( isset( $cover_data['width'] ) && $cover_data['width'] > $dimensions['width'] ) ||
+		( isset( $cover_data['height'] ) && $cover_data['height'] > $dimensions['height'] ) ) {
+			$edit_args = array(
+				'max_w' => $dimensions['width'],
+				'max_h' => $dimensions['height'],
+				'crop'  => true,
+			);
+		}
+
+		// Do we need to rotate the image?
+		$angles = array(
+			3 => 180,
+			6 => -90,
+			8 =>  90,
+		);
+
+		if ( isset( $cover_data['meta']['orientation'] ) && isset( $angles[ $cover_data['meta']['orientation'] ] ) ) {
+			$edit_args['rotate'] = $angles[ $cover_data['meta']['orientation'] ];
+		}
+
+		// No need to edit the avatar, original file will be used.
+		if ( empty( $edit_args ) ) {
+			return false;
+
+		// Add the file to the edit arguments.
+		} else {
+			$edit_args = array_merge( $edit_args, array( 'file' => $file, 'save' => false ) );
+		}
+
+		// Get the editor so that we can use a specific save method.
+		$editor = parent::edit_image( 'cover_image', $edit_args );
+
+		if ( is_wp_error( $editor ) )  {
+			return $editor;
+		} elseif ( ! is_a( $editor, 'WP_Image_Editor' ) ) {
+			return false;
+		}
+
+		// Save the new image file.
+		return $editor->save( $this->generate_filename( $file ) );
+	}
+
+	/**
+	 * Generate a filename for the cover image.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $file The absolute path to the file.
+	 * @return false|string $value The absolute path to the new file name.
+	 */
+	public function generate_filename( $file = '' ) {
+		if ( empty( $file ) || ! file_exists( $file ) ) {
+			return false;
+		}
+
+		$info = pathinfo( $file );
+		$ext  = strtolower( $info['extension'] );
+		$name = wp_unique_filename( $info['dirname'], uniqid() . "-bp-cover-image.$ext" );
+
+		return trailingslashit( $info['dirname'] ) . $name;
+	}
+
+	/**
+	 * Build script datas for the Uploader UI.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @return array The javascript localization data
+	 */
+	public function script_data() {
+		// Get default script data.
+		$script_data = parent::script_data();
+
+		if ( bp_is_user() ) {
+			$item_id = bp_displayed_user_id();
+
+			$script_data['bp_params'] = array(
+				'object'          => 'user',
+				'item_id'         => $item_id,
+				'has_cover_image' => bp_attachments_get_user_has_cover_image( $item_id ),
+				'nonces'  => array(
+					'remove' => wp_create_nonce( 'bp_delete_cover_image' ),
+				),
+			);
+
+			// Set feedback messages.
+			$script_data['feedback_messages'] = array(
+				1 => __( 'Your new cover image was uploaded successfully.', 'buddypress' ),
+				2 => __( 'There was a problem deleting your cover image. Please try again.', 'buddypress' ),
+				3 => __( 'Your cover image was deleted successfully!', 'buddypress' ),
+			);
+		} elseif ( bp_is_group() ) {
+			$item_id = bp_get_current_group_id();
+
+			$script_data['bp_params'] = array(
+				'object'          => 'group',
+				'item_id'         => bp_get_current_group_id(),
+				'has_cover_image' => bp_attachments_get_group_has_cover_image( $item_id ),
+				'nonces'  => array(
+					'remove' => wp_create_nonce( 'bp_delete_cover_image' ),
+				),
+			);
+
+			// Set feedback messages.
+			$script_data['feedback_messages'] = array(
+				1 => __( 'The group cover image was uploaded successfully.', 'buddypress' ),
+				2 => __( 'There was a problem deleting the group cover image. Please try again.', 'buddypress' ),
+				3 => __( 'The group cover image was deleted successfully!', 'buddypress' ),
+			);
+		} else {
+
+			/**
+			 * Filters the cover image params to include specific BuddyPress params for your object.
+			 * e.g. Cover image for blogs single item.
+			 *
+			 * @since 2.4.0
+			 *
+			 * @param array $value The cover image specific BuddyPress parameters.
+			 */
+			$script_data['bp_params'] = apply_filters( 'bp_attachment_cover_image_params', array() );
+		}
+
+		// Include our specific js & css.
+		$script_data['extra_js']  = array( 'bp-cover-image' );
+		$script_data['extra_css'] = array( 'bp-avatar' );
+
+		/**
+		 * Filters the cover image script data.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param array $script_data Array of data for the cover image.
+		 */
+		return apply_filters( 'bp_attachments_cover_image_script_data', $script_data );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-attachment.php	(revision 12265)
@@ -0,0 +1,666 @@
+<?php
+/**
+ * Core attachment class.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BP Attachment class.
+ *
+ * Extend it to manage your component's uploads.
+ *
+ * @since 2.3.0
+ */
+abstract class BP_Attachment {
+
+	/** Upload properties *****************************************************/
+
+	/**
+	 * The file being uploaded.
+	 *
+	 * @var array
+	 */
+	public $attachment = array();
+
+	/**
+	 * The default args to be merged with the
+	 * ones passed by the child class.
+	 *
+	 * @var array
+	 */
+	protected $default_args = array(
+		'original_max_filesize'  => 0,
+		'allowed_mime_types'     => array(),
+		'base_dir'               => '',
+		'action'                 => '',
+		'file_input'             => '',
+		'upload_error_strings'   => array(),
+		'required_wp_files'      => array( 'file' ),
+		'upload_dir_filter_args' => 0,
+	);
+
+	/**
+	 * Construct Upload parameters.
+	 *
+	 * @since 2.3.0
+	 * @since 2.4.0 Add the $upload_dir_filter_args argument to the $arguments array
+	 *
+	 * @param array|string $args {
+	 *     @type int    $original_max_filesize  Maximum file size in kilobytes. Defaults to php.ini settings.
+	 *     @type array  $allowed_mime_types     List of allowed file extensions (eg: array( 'jpg', 'gif', 'png' ) ).
+	 *                                          Defaults to WordPress allowed mime types.
+	 *     @type string $base_dir               Component's upload base directory. Defaults to WordPress 'uploads'.
+	 *     @type string $action                 The upload action used when uploading a file, $_POST['action'] must be set
+	 *                                          and its value must equal $action {@link wp_handle_upload()} (required).
+	 *     @type string $file_input             The name attribute used in the file input. (required).
+	 *     @type array  $upload_error_strings   A list of specific error messages (optional).
+	 *     @type array  $required_wp_files      The list of required WordPress core files. Default: array( 'file' ).
+	 *     @type int    $upload_dir_filter_args 1 to receive the original Upload dir array in the Upload dir filter, 0 otherwise.
+	 *                                          Defaults to 0 (optional).
+	 * }
+	 */
+	public function __construct( $args = '' ) {
+		// Upload action and the file input name are required parameters.
+		if ( empty( $args['action'] ) || empty( $args['file_input'] ) ) {
+			return false;
+		}
+
+		// Sanitize the action ID and the file input name.
+		$this->action     = sanitize_key( $args['action'] );
+		$this->file_input = sanitize_key( $args['file_input'] );
+
+		/**
+		 * Max file size defaults to php ini settings or, in the case of
+		 * a multisite config, the root site fileupload_maxk option
+		 */
+		$this->default_args['original_max_filesize'] = (int) wp_max_upload_size();
+
+		$params = bp_parse_args( $args, $this->default_args, $this->action . '_upload_params' );
+
+		foreach ( $params as $key => $param ) {
+			if ( 'upload_error_strings' === $key ) {
+				$this->{$key} = $this->set_upload_error_strings( $param );
+
+			// Sanitize the base dir.
+			} elseif ( 'base_dir' === $key ) {
+				$this->{$key} = sanitize_title( $param );
+
+			// Sanitize the upload dir filter arg to pass.
+			} elseif ( 'upload_dir_filter_args' === $key ) {
+				$this->{$key} = (int) $param;
+
+			// Action & File input are already set and sanitized.
+			} elseif ( 'action' !== $key && 'file_input' !== $key ) {
+				$this->{$key} = $param;
+			}
+		}
+
+		// Set the path/url and base dir for uploads.
+		$this->set_upload_dir();
+	}
+
+	/**
+	 * Set upload path and url for the component.
+	 *
+	 * @since 2.3.0
+	 *
+	 */
+	public function set_upload_dir() {
+		// Set the directory, path, & url variables.
+		$this->upload_dir  = bp_upload_dir();
+
+		if ( empty( $this->upload_dir ) ) {
+			return false;
+		}
+
+		$this->upload_path = $this->upload_dir['basedir'];
+		$this->url         = $this->upload_dir['baseurl'];
+
+		// Ensure URL is https if SSL is set/forced.
+		if ( is_ssl() ) {
+			$this->url = str_replace( 'http://', 'https://', $this->url );
+		}
+
+		/**
+		 * Custom base dir.
+		 *
+		 * If the component set this property, set the specific path, url and create the dir
+		 */
+		if ( ! empty( $this->base_dir ) ) {
+			$this->upload_path = trailingslashit( $this->upload_path ) . $this->base_dir;
+			$this->url         = trailingslashit( $this->url  ) . $this->base_dir;
+
+			// Finally create the base dir.
+			$this->create_dir();
+		}
+	}
+
+	/**
+	 * Set Upload error messages.
+	 *
+	 * Used into the $overrides argument of BP_Attachment->upload()
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $param A list of error messages to add to BuddyPress core ones.
+	 * @return array $upload_errors The list of upload errors.
+	 */
+	public function set_upload_error_strings( $param = array() ) {
+		/**
+		 * Index of the array is the error code
+		 * Custom errors will start at 9 code
+		 */
+		$upload_errors = array(
+			0 => __( 'The file was uploaded successfully', 'buddypress' ),
+			1 => __( 'The uploaded file exceeds the maximum allowed file size for this site', 'buddypress' ),
+			2 => sprintf( __( 'The uploaded file exceeds the maximum allowed file size of: %s', 'buddypress' ), size_format( $this->original_max_filesize ) ),
+			3 => __( 'The uploaded file was only partially uploaded.', 'buddypress' ),
+			4 => __( 'No file was uploaded.', 'buddypress' ),
+			5 => '',
+			6 => __( 'Missing a temporary folder.', 'buddypress' ),
+			7 => __( 'Failed to write file to disk.', 'buddypress' ),
+			8 => __( 'File upload stopped by extension.', 'buddypress' ),
+		);
+
+		if ( ! array_intersect_key( $upload_errors, (array) $param ) ) {
+			foreach ( $param as $key_error => $error_message ) {
+				$upload_errors[ $key_error ] = $error_message;
+			}
+		}
+
+		return $upload_errors;
+	}
+
+	/**
+	 * Include the WordPress core needed files.
+	 *
+	 * @since 2.3.0
+	 */
+	public function includes() {
+		foreach ( array_unique( $this->required_wp_files ) as $wp_file ) {
+			if ( ! file_exists( ABSPATH . "/wp-admin/includes/{$wp_file}.php" ) ) {
+				continue;
+			}
+
+			require_once( ABSPATH . "/wp-admin/includes/{$wp_file}.php" );
+		}
+	}
+
+	/**
+	 * Upload the attachment.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array       $file              The appropriate entry the from $_FILES superglobal.
+	 * @param string      $upload_dir_filter A specific filter to be applied to 'upload_dir' (optional).
+	 * @param string|null $time              Optional. Time formatted in 'yyyy/mm'. Default null.
+	 * @return array On success, returns an associative array of file attributes.
+	 *               On failure, returns an array containing the error message
+	 *               (eg: array( 'error' => $message ) )
+	 */
+	public function upload( $file, $upload_dir_filter = '', $time = null ) {
+		/**
+		 * Upload action and the file input name are required parameters.
+		 *
+		 * @see BP_Attachment:__construct()
+		 */
+		if ( empty( $this->action ) || empty( $this->file_input ) ) {
+			return false;
+		}
+
+		/**
+		 * Add custom rules before enabling the file upload
+		 */
+		add_filter( "{$this->action}_prefilter", array( $this, 'validate_upload' ), 10, 1 );
+
+		// Set Default overrides.
+		$overrides = array(
+			'action'               => $this->action,
+			'upload_error_strings' => $this->upload_error_strings,
+		);
+
+		/**
+		 * Add a mime override if needed
+		 * Used to restrict uploads by extensions
+		 */
+		if ( ! empty( $this->allowed_mime_types ) ) {
+			$mime_types = $this->validate_mime_types();
+
+			if ( ! empty( $mime_types ) ) {
+				$overrides['mimes'] = $mime_types;
+			}
+		}
+
+		/**
+		 * If you need to add some overrides we haven't thought of.
+		 *
+		 * @param array $overrides The wp_handle_upload overrides
+		 */
+		$overrides = apply_filters( 'bp_attachment_upload_overrides', $overrides );
+
+		$this->includes();
+
+		/**
+		 * If the $base_dir was set when constructing the class,
+		 * and no specific filter has been requested, use a default
+		 * filter to create the specific $base dir
+		 * @see  BP_Attachment->upload_dir_filter()
+		 */
+		if ( empty( $upload_dir_filter ) && ! empty( $this->base_dir ) ) {
+			$upload_dir_filter = array( $this, 'upload_dir_filter' );
+		}
+
+		// Make sure the file will be uploaded in the attachment directory.
+		if ( ! empty( $upload_dir_filter ) ) {
+			add_filter( 'upload_dir', $upload_dir_filter, 10, $this->upload_dir_filter_args );
+		}
+
+		// Helper for utf-8 filenames.
+		add_filter( 'sanitize_file_name', array( $this, 'sanitize_utf8_filename' ) );
+
+		// Upload the attachment.
+		$this->attachment = wp_handle_upload( $file[ $this->file_input ], $overrides, $time );
+
+		remove_filter( 'sanitize_file_name', array( $this, 'sanitize_utf8_filename' ) );
+
+		// Restore WordPress Uploads data.
+		if ( ! empty( $upload_dir_filter ) ) {
+			remove_filter( 'upload_dir', $upload_dir_filter, 10 );
+		}
+
+		// Finally return the uploaded file or the error.
+		return $this->attachment;
+	}
+
+	/**
+	 * Helper to convert utf-8 characters in filenames to their ASCII equivalent.
+	 *
+	 * @since 2.9.0
+	 *
+	 * @param  string $retval Filename.
+	 * @return string
+	 */
+	public function sanitize_utf8_filename( $retval ) {
+		// PHP 5.4+ or with PECL intl 2.0+
+		if ( function_exists( 'transliterator_transliterate' ) && seems_utf8( $retval ) ) {
+			$retval = transliterator_transliterate( 'Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove', $retval );
+
+		// Older.
+		} else {
+			// Use WP's built-in function to convert accents to their ASCII equivalent.
+			$retval = remove_accents( $retval );
+
+			// Still here? use iconv().
+			if ( function_exists( 'iconv' ) && seems_utf8( $retval ) ) {
+				$retval = iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $retval );
+			}
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Validate the allowed mime types using WordPress allowed mime types.
+	 *
+	 * In case of a multisite, the mime types are already restricted by
+	 * the 'upload_filetypes' setting. BuddyPress will respect this setting.
+	 *
+	 * @see check_upload_mimes()
+	 *
+	 * @since 2.3.0
+	 *
+	 */
+	protected function validate_mime_types() {
+		$wp_mimes = get_allowed_mime_types();
+		$valid_mimes = array();
+
+		// Set the allowed mimes for the upload.
+		foreach ( (array) $this->allowed_mime_types as $ext ) {
+			foreach ( $wp_mimes as $ext_pattern => $mime ) {
+				if ( $ext !== '' && strpos( $ext_pattern, $ext ) !== false ) {
+					$valid_mimes[$ext_pattern] = $mime;
+				}
+			}
+		}
+		return $valid_mimes;
+	}
+
+	/**
+	 * Specific upload rules.
+	 *
+	 * Override this function from your child class to build your specific rules
+	 * By default, if an original_max_filesize is provided, a check will be done
+	 * on the file size.
+	 *
+	 * @see BP_Attachment_Avatar->validate_upload() for an example of use
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $file The temporary file attributes (before it has been moved).
+	 * @return array The file.
+	 */
+	public function validate_upload( $file = array() ) {
+		// Bail if already an error.
+		if ( ! empty( $file['error'] ) ) {
+			return $file;
+		}
+
+		if ( ! empty( $this->original_max_filesize ) && $file['size'] > $this->original_max_filesize ) {
+			$file['error'] = 2;
+		}
+
+		// Return the file.
+		return $file;
+	}
+
+	/**
+	 * Default filter to save the attachments.
+	 *
+	 * @since 2.3.0
+	 * @since 2.4.0 Add the $upload_dir parameter to the method
+	 *
+	 *       regarding to context
+	 *
+	 * @param array $upload_dir The original Uploads dir.
+	 * @return array The upload directory data.
+	 */
+	public function upload_dir_filter( $upload_dir = array() ) {
+
+		/**
+		 * Filters the component's upload directory.
+		 *
+		 * @since 2.3.0
+		 * @since 2.4.0 Include the original Upload directory as the second parameter of the filter.
+		 *
+		 * @param array $value          Array containing the path, URL, and other helpful settings.
+		 * @param array $upload_dir     The original Uploads dir.
+		 */
+		return apply_filters( 'bp_attachment_upload_dir', array(
+			'path'    => $this->upload_path,
+			'url'     => $this->url,
+			'subdir'  => false,
+			'basedir' => $this->upload_path,
+			'baseurl' => $this->url,
+			'error'   => false
+		), $upload_dir );
+	}
+
+	/**
+	 * Create the custom base directory for the component uploads.
+	 *
+	 * Override this function in your child class to run specific actions.
+	 * (eg: add an .htaccess file)
+	 *
+	 * @since 2.3.0
+	 *
+	 */
+	public function create_dir() {
+		// Bail if no specific base dir is set.
+		if ( empty( $this->base_dir ) ) {
+			return false;
+		}
+
+		// Check if upload path already exists.
+		if ( ! is_dir( $this->upload_path ) ) {
+
+			// If path does not exist, attempt to create it.
+			if ( ! wp_mkdir_p( $this->upload_path ) ) {
+				return false;
+			}
+		}
+
+		// Directory exists.
+		return true;
+	}
+
+	/**
+	 * Crop an image file.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $args {
+	 *     @type string $original_file The source file (absolute path) for the Attachment.
+	 *     @type int    $crop_x        The start x position to crop from.
+	 *     @type int    $crop_y        The start y position to crop from.
+	 *     @type int    $crop_w        The width to crop.
+	 *     @type int    $crop_h        The height to crop.
+	 *     @type int    $dst_w         The destination width.
+	 *     @type int    $dst_h         The destination height.
+	 *     @type int    $src_abs       Optional. If the source crop points are absolute.
+	 *     @type string $dst_file      Optional. The destination file to write to.
+	 * }
+	 *
+	 * @return string|WP_Error New filepath on success, WP_Error on failure.
+	 */
+	public function crop( $args = array() ) {
+		$wp_error = new WP_Error();
+
+		$r = bp_parse_args( $args, array(
+			'original_file' => '',
+			'crop_x'        => 0,
+			'crop_y'        => 0,
+			'crop_w'        => 0,
+			'crop_h'        => 0,
+			'dst_w'         => 0,
+			'dst_h'         => 0,
+			'src_abs'       => false,
+			'dst_file'      => false,
+		), 'bp_attachment_crop_args' );
+
+		if ( empty( $r['original_file'] ) || ! file_exists( $r['original_file'] ) ) {
+			$wp_error->add( 'crop_error', __( 'Cropping the file failed: missing source file.', 'buddypress' ) );
+			return $wp_error;
+		}
+
+		// Check image file pathes.
+		$path_error = __( 'Cropping the file failed: the file path is not allowed.', 'buddypress' );
+
+		// Make sure it's coming from an uploaded file.
+		if ( false === strpos( $r['original_file'], $this->upload_path ) ) {
+			$wp_error->add( 'crop_error', $path_error );
+			return $wp_error;
+		}
+
+		/**
+		 * If no destination file is provided, WordPress will use a default name
+		 * and will write the file in the source file's folder.
+		 * If a destination file is provided, we need to make sure it's going into uploads.
+		 */
+		if ( ! empty( $r['dst_file'] ) && false === strpos( $r['dst_file'], $this->upload_path ) ) {
+			$wp_error->add( 'crop_error', $path_error );
+			return $wp_error;
+		}
+
+		// Check image file types.
+		$check_types = array( 'src_file' => array( 'file' => $r['original_file'], 'error' => _x( 'source file', 'Attachment source file', 'buddypress' ) ) );
+		if ( ! empty( $r['dst_file'] ) ) {
+			$check_types['dst_file'] = array( 'file' => $r['dst_file'], 'error' => _x( 'destination file', 'Attachment destination file', 'buddypress' ) );
+		}
+
+		/**
+		 * WordPress image supported types.
+		 * @see wp_attachment_is()
+		 */
+		$supported_image_types = array(
+			'jpg'  => 1,
+			'jpeg' => 1,
+			'jpe'  => 1,
+			'gif'  => 1,
+			'png'  => 1,
+		);
+
+		foreach ( $check_types as $file ) {
+			$is_image      = wp_check_filetype( $file['file'] );
+			$ext           = $is_image['ext'];
+
+			if ( empty( $ext ) || empty( $supported_image_types[ $ext ] ) ) {
+				$wp_error->add( 'crop_error', sprintf( __( 'Cropping the file failed: %s is not a supported image file.', 'buddypress' ), $file['error'] ) );
+				return $wp_error;
+			}
+		}
+
+		// Add the image.php to the required WordPress files, if it's not already the case.
+		$required_files = array_flip( $this->required_wp_files );
+		if ( ! isset( $required_files['image'] ) ) {
+			$this->required_wp_files[] = 'image';
+		}
+
+		// Load the files.
+		$this->includes();
+
+		// Finally crop the image.
+		return wp_crop_image( $r['original_file'], (int) $r['crop_x'], (int) $r['crop_y'], (int) $r['crop_w'], (int) $r['crop_h'], (int) $r['dst_w'], (int) $r['dst_h'], $r['src_abs'], $r['dst_file'] );
+	}
+
+	/**
+	 * Build script datas for the Uploader UI.
+	 *
+	 * Override this method from your child class to build the script datas.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return array The javascript localization data.
+	 */
+	public function script_data() {
+		$script_data = array(
+			'action'            => $this->action,
+			'file_data_name'    => $this->file_input,
+			'max_file_size'     => $this->original_max_filesize,
+			'feedback_messages' => array(
+				1 => __( 'Sorry, uploading the file failed.', 'buddypress' ),
+				2 => __( 'File successfully uploaded.', 'buddypress' ),
+			),
+		);
+
+		return $script_data;
+	}
+
+	/**
+	 * Get full data for an image
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $file Absolute path to the uploaded image.
+	 * @return bool|array   An associate array containing the width, height and metadatas.
+	 *                      False in case an important image attribute is missing.
+	 */
+	public static function get_image_data( $file ) {
+		// Try to get image basic data.
+		list( $width, $height, $sourceImageType ) = @getimagesize( $file );
+
+		// No need to carry on if we couldn't get image's basic data.
+		if ( is_null( $width ) || is_null( $height ) || is_null( $sourceImageType ) ) {
+			return false;
+		}
+
+		// Initialize the image data.
+		$image_data = array(
+			'width'  => $width,
+			'height' => $height,
+		);
+
+		/**
+		 * Make sure the wp_read_image_metadata function is reachable for the old Avatar UI
+		 * or if WordPress < 3.9 (New Avatar UI is not available in this case)
+		 */
+		if ( ! function_exists( 'wp_read_image_metadata' ) ) {
+			require_once( ABSPATH . 'wp-admin/includes/image.php' );
+		}
+
+		// Now try to get image's meta data.
+		$meta = wp_read_image_metadata( $file );
+		if ( ! empty( $meta ) ) {
+			$image_data['meta'] = $meta;
+		}
+
+		/**
+		 * Filter here to add/remove/edit data to the image full data
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param array $image_data An associate array containing the width, height and metadatas.
+		 */
+		return apply_filters( 'bp_attachments_get_image_data', $image_data );
+	}
+
+	/**
+	 * Edit an image file to resize it or rotate it
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $attachment_type The attachment type (eg: avatar or cover_image). Required.
+	 * @param array  $args {
+	 *     @type string $file     Absolute path to the image file (required).
+	 *     @type int    $max_w    Max width attribute for the editor's resize method (optional).
+	 *     @type int    $max_h    Max height attribute for the editor's resize method (optional).
+	 *     @type bool   $crop     Crop attribute for the editor's resize method (optional).
+	 *     @type float  $rotate   Angle for the editor's rotate method (optional).
+	 *     @type int    $quality  Compression quality on a 1-100% scale (optional).
+	 *     @type bool   $save     Whether to use the editor's save method or not (optional).
+	 * }
+	 * @return string|WP_Image_Editor|WP_Error The edited image path or the WP_Image_Editor object in case of success,
+	 *                                         an WP_Error object otherwise.
+	 */
+	public static function edit_image( $attachment_type, $args = array() ) {
+		if ( empty( $attachment_type ) ) {
+			return new WP_Error( 'missing_parameter' );
+		}
+
+		$r = bp_parse_args( $args, array(
+			'file'   => '',
+			'max_w'   => 0,
+			'max_h'   => 0,
+			'crop'    => false,
+			'rotate'  => 0,
+			'quality' => 90,
+			'save'    => true,
+		), 'attachment_' . $attachment_type . '_edit_image' );
+
+		// Make sure we have to edit the image.
+		if ( empty( $r['max_w'] ) && empty( $r['max_h'] ) && empty( $r['rotate'] ) && empty( $r['file'] ) ) {
+			return new WP_Error( 'missing_parameter' );
+		}
+
+		// Get the image editor.
+		$editor = wp_get_image_editor( $r['file'] );
+
+		if ( is_wp_error( $editor ) ) {
+			return $editor;
+		}
+
+		$editor->set_quality( $r['quality'] );
+
+		if ( ! empty( $r['rotate'] ) ) {
+			$rotated = $editor->rotate( $r['rotate'] );
+
+			// Stop in case of error.
+			if ( is_wp_error( $rotated ) ) {
+				return $rotated;
+			}
+		}
+
+		if ( ! empty( $r['max_w'] ) || ! empty( $r['max_h'] ) ) {
+			$resized = $editor->resize( $r['max_w'], $r['max_h'], $r['crop'] );
+
+			// Stop in case of error.
+			if ( is_wp_error( $resized ) ) {
+				return $resized;
+			}
+		}
+
+		// Use the editor save method to get a path to the edited image.
+		if ( true === $r['save'] ) {
+			return $editor->save( $editor->generate_filename() );
+
+		// Need to do some other edit actions or use a specific method to save file.
+		} else {
+			return $editor;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-button.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-button.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-button.php	(revision 12265)
@@ -0,0 +1,435 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.2.6
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * API to create BuddyPress buttons.
+ *
+ * @since 1.2.6
+ * @since 2.7.0 Introduced $parent_element, $parent_attr, $button_element, $button_attr as
+ *              $args parameters.
+ *              Deprecated $wrapper, $wrapper_id, $wrapper_class, $link_href, $link_class,
+ *              $link_id, $link_rel, $link_title as $args params.
+ *
+ * @param array $args {
+ *     Array of arguments.
+ *
+ *     @type string      $id                String describing the button type.
+ *     @type string      $component         The name of the component the button belongs to. Default: 'core'.
+ *     @type bool        $must_be_logged_in Optional. Does the user need to be logged in to see this button? Default:
+ *                                          true.
+ *     @type bool        $block_self        Optional. True if the button should be hidden when a user is viewing his
+ *                                          own profile. Default: true.
+ *     @type string      $parent_element    Optional. Parent element to wrap button around. Default: 'div'.
+ *     @type array       $parent_attr       Optional. Element attributes for parent element. Set whatever attributes
+ *                                          like 'id', 'class' as array keys.
+ *     @type string      $button_element    Optional. Button element. Default: 'a'.
+ *     @type array       $button_attr       Optional. Button attributes. Set whatever attributes like 'id', 'class' as
+ *                                          array keys.
+ *     @type string      $link_text         Optional. Text to appear on the button. Default: ''.
+ *     @type string|bool $wrapper           Deprecated. Use $parent_element instead.
+ *     @type string      $wrapper_id        Deprecated. Use $parent_attr and set 'id' as array key.
+ *     @type string      $wrapper_class     Deprecated. Use $parent_attr and set 'class' as array key.
+ *     @type string      $link_href         Deprecated. Use $button_attr and set 'href' as array key.
+ *     @type string      $link_class        Deprecated. Use $button_attr and set 'class' as array key.
+ *     @type string      $link_id           Deprecated. Use $button_attr and set 'id' as array key.
+ *     @type string      $link_rel          Deprecated. Use $button_attr and set 'rel' as array key.
+ *     @type string      $link_title        Deprecated. Use $button_attr and set 'title' as array key.
+ * }
+ */
+class BP_Button {
+
+	/** Button properties *****************************************************/
+
+	/**
+	 * The button ID.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @var string
+	 */
+	public $id = '';
+
+	/**
+	 * The name of the component that the button belongs to.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @var string
+	 */
+	public $component = 'core';
+
+	/**
+	 * Does the user need to be logged in to see this button?
+	 *
+	 * @since 1.2.6
+	 *
+	 * @var bool
+	 */
+	public $must_be_logged_in = true;
+
+	/**
+	 * Whether the button should be hidden when viewing your own profile.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @var bool
+	 */
+	public $block_self = true;
+
+	/** Wrapper ***************************************************************/
+
+	/**
+	 * Parent element to wrap button around.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var string Default: 'div'.
+	 */
+	public $parent_element = '';
+
+	/**
+	 * Element attributes for parent element.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var array Set whatever attributes like 'id', 'class' as array key.
+	 */
+	public $parent_attr = array();
+
+	/** Button ****************************************************************/
+
+	/**
+	 * Button element.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var string Default: 'a'.
+	 */
+	public $button_element = 'a';
+
+	/**
+	 * Button attributes.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var array Set whatever attributes like 'id', 'href' as array key.
+	 */
+	public $button_attr = array();
+
+	/**
+	 * The contents of the button link.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @var string
+	 */
+	public $link_text = '';
+
+	/**
+	 * HTML result.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @var string
+	 */
+	public $contents = '';
+
+	/** Deprecated ***********************************************************/
+
+	/**
+	 * The type of DOM element to use for a wrapper.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Use $parent_element instead.
+	 *
+	 * @var string|bool
+	 */
+	public $wrapper = 'div';
+
+	/**
+	 * The DOM class of the button wrapper.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'class' key in $parent_attr instead.
+	 *
+	 * @var string
+	 */
+	public $wrapper_class = '';
+
+	/**
+	 * The DOM ID of the button wrapper.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'id' key in $parent_attr instead.
+	 *
+	 * @var string
+	 */
+	public $wrapper_id = '';
+
+	/**
+	 * The destination link of the button.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'href' key in $button_attr instead.
+	 *
+	 * @var string
+	 */
+	public $link_href = '';
+
+	/**
+	 * The DOM class of the button link.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'class' key in $button_attr instead.
+	 *
+	 * @var string
+	 */
+	public $link_class = '';
+
+	/**
+	 * The DOM ID of the button link.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'id' key in $button_attr instead.
+	 *
+	 * @var string
+	 */
+	public $link_id = '';
+
+	/**
+	 * The DOM rel value of the button link.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'rel' key in $button_attr instead.
+	 *
+	 * @var string
+	 */
+	public $link_rel = '';
+
+	/**
+	 * Title of the button link.
+	 *
+	 * @since      1.2.6
+	 * @deprecated 2.7.0 Set 'title' key in $button_attr instead.
+	 *
+	 * @var string
+	 */
+	public $link_title = '';
+
+	/** Methods ***************************************************************/
+
+	/**
+	 * Builds the button based on class parameters.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param array|string $args See {@BP_Button}.
+	 */
+	public function __construct( $args = '' ) {
+
+		$r = wp_parse_args( $args, get_class_vars( __CLASS__ ) );
+
+		// Backward compatibility with deprecated parameters.
+		$r = $this->backward_compatibility_args( $r );
+
+		// Deprecated. Subject to removal in a future release.
+		$this->wrapper = $r['wrapper'];
+		if ( !empty( $r['link_id']    ) ) $this->link_id    = ' id="' .    $r['link_id']    . '"';
+		if ( !empty( $r['link_href']  ) ) $this->link_href  = ' href="' .  $r['link_href']  . '"';
+		if ( !empty( $r['link_title'] ) ) $this->link_title = ' title="' . $r['link_title'] . '"';
+		if ( !empty( $r['link_rel']   ) ) $this->link_rel   = ' rel="' .   $r['link_rel']   . '"';
+		if ( !empty( $r['link_class'] ) ) $this->link_class = ' class="' . $r['link_class'] . '"';
+		if ( !empty( $r['link_text']  ) ) $this->link_text  =              $r['link_text'];
+
+		// Required button properties.
+		$this->id                = $r['id'];
+		$this->component         = $r['component'];
+		$this->must_be_logged_in = (bool) $r['must_be_logged_in'];
+		$this->block_self        = (bool) $r['block_self'];
+
+		// $id and $component are required and component must be active.
+		if ( empty( $r['id'] ) || empty( $r['component'] ) || ! bp_is_active( $this->component ) ) {
+			return false;
+		}
+
+		// No button for guests if must be logged in.
+		if ( true == $this->must_be_logged_in && ! is_user_logged_in() ) {
+			return false;
+		}
+
+		// The block_self property.
+		if ( true == $this->block_self ) {
+			/*
+			 * No button if you are the current user in a members loop.
+			 *
+			 * This condition takes precedence, because members loops can be found on user
+			 * profiles.
+			 */
+			if ( bp_get_member_user_id() ) {
+				if ( is_user_logged_in() && bp_loggedin_user_id() == bp_get_member_user_id() ) {
+					return false;
+				}
+
+			// No button if viewing your own profile (and not in a members loop).
+			} elseif ( bp_is_my_profile() ) {
+				return false;
+			}
+		}
+
+		// Should we use a parent element?
+		if ( ! empty( $r['parent_element'] ) ) {
+			if ( ! isset( $r['parent_attr']['class'] ) ) {
+				$r['parent_attr']['class'] = '';
+			}
+
+			// Always add 'generic-button' class.
+			if ( false === strpos( $r['parent_attr']['class'], 'generic-button' ) ) {
+				if ( ! is_array( $r['parent_attr'] ) ) {
+					$r['parent_attr'] = array();
+				}
+				if ( ! empty( $r['parent_attr']['class'] ) ) {
+					$r['parent_attr']['class'] .= ' ';
+				}
+				$r['parent_attr']['class'] .= 'generic-button';
+			}
+
+			// Set parent element props.
+			$this->parent_element = $r['parent_element'];
+			$this->parent_attr    = $r['parent_attr'];
+
+			// Render parent element attributes.
+			$parent_elem = new BP_Core_HTML_Element( array(
+				'element' => $r['parent_element'],
+				'attr'    => $r['parent_attr']
+			) );
+
+			// Set before and after.
+			$before = $parent_elem->get( 'open_tag' );
+			$after  = $parent_elem->get( 'close_tag' );
+
+		// No parent element.
+		} else {
+			$before = $after = '';
+		}
+
+		// Button properties.
+		$button = '';
+		if ( ! empty( $r['button_element'] ) ) {
+			$button = new BP_Core_HTML_Element( array(
+				'element'    => $r['button_element'],
+				'attr'       => $r['button_attr'],
+				'inner_html' => ! empty( $r['link_text'] ) ? $r['link_text'] : ''
+			) );
+			$button = $button->contents();
+		}
+
+		// Build the button.
+		$this->contents = $before . $button . $after;
+
+		/**
+		 * Filters the button based on class parameters.
+		 *
+		 * This filter is a dynamic filter based on component and component ID and
+		 * allows button to be manipulated externally.
+		 *
+		 * @since 1.2.6
+		 * @since 2.7.0 Added $r as a parameter.
+		 *
+		 * @param string    $contents HTML being used for the button.
+		 * @param BP_Button $this     Current BP_Button instance.
+		 * @param string    $before   HTML appended before the actual button.
+		 * @param string    $after    HTML appended after the actual button.
+		 * @param array     $r        Parsed button arguments.
+		 */
+		$this->contents = apply_filters( 'bp_button_' . $this->component . '_' . $this->id, $this->contents, $this, $before, $after, $r );
+	}
+
+
+	/**
+	 * Provide backward compatibility for deprecated button arguments.
+	 *
+	 * @since 2.7.0.
+	 *
+	 * @param  array $r See {@link BP_Button} class for full documentation.
+	 * @return array
+	 */
+	protected function backward_compatibility_args( $r = array() ) {
+		// Array of deprecated arguments.
+		$backpat_args = array(
+			'wrapper', 'wrapper_class', 'wrapper_id',
+			'link_href', 'link_class', 'link_id', 'link_rel', 'link_title'
+		);
+
+		foreach ( $backpat_args as $prop ) {
+			if ( empty( $r[ $prop ] ) ) {
+				continue;
+			}
+
+			$parent = $child = false;
+			$sep    = strpos( $prop, '_' );
+
+			// Check if this is an attribute.
+			if ( false !== $sep ) {
+				$child  = true;
+				$parent = substr( $prop, 0, $sep );
+			} else {
+				$parent = $prop;
+			}
+
+			if ( 'wrapper' === $parent ) {
+				$parent = 'parent';
+			} else {
+				$parent = 'button';
+			}
+
+			// Set element.
+			if ( false === $child && empty( $r[ "{$parent}_element" ] ) ) {
+				$r[ "{$parent}_element" ] = $r[ $prop ];
+
+			// Set attributes.
+			} elseif ( true === $child ) {
+				$new_prop = substr( $prop, strpos( $prop, '_' ) +1 );
+				if ( empty( $r[ "{$parent}_attr" ] ) ) {
+					$r[ "{$parent}_attr" ] = array();
+				}
+
+				if ( empty( $r[ "{$parent}_attr" ][ $new_prop ] ) ) {
+					$r[ "{$parent}_attr" ][ $new_prop ] = $r[ $prop ];
+				}
+			}
+		}
+
+		return $r;
+	}
+
+
+	/**
+	 * Return the markup for the generated button.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @return string Button markup.
+	 */
+	public function contents() {
+		return $this->contents;
+	}
+
+	/**
+	 * Output the markup of button.
+	 *
+	 * @since 1.2.6
+	 */
+	public function display() {
+		if ( !empty( $this->contents ) )
+			echo $this->contents;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-component.php	(revision 12265)
@@ -0,0 +1,858 @@
+<?php
+/**
+ * Component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( !class_exists( 'BP_Component' ) ) :
+
+/**
+ * BuddyPress Component Class.
+ *
+ * The BuddyPress component class is responsible for simplifying the creation
+ * of components that share similar behaviors and routines. It is used
+ * internally by BuddyPress to create the bundled components, but can be
+ * extended to create other really neat things.
+ *
+ * @since 1.5.0
+ */
+class BP_Component {
+
+	/** Variables *************************************************************/
+
+	/**
+	 * Translatable name for the component.
+	 *
+	 * @internal
+	 * @var string $name
+	 */
+	public $name = '';
+
+	/**
+	 * Unique ID for the component.
+	 *
+	 * @since 1.5.0
+	 * @var string $id
+	 */
+	public $id = '';
+
+	/**
+	 * Unique slug for the component, for use in query strings and URLs.
+	 *
+	 * @since 1.5.0
+	 * @var string $slug
+	 */
+	public $slug = '';
+
+	/**
+	 * Does the component need a top-level directory?
+	 *
+	 * @since 1.5.0
+	 * @var bool $has_directory
+	 */
+	public $has_directory = false;
+
+	/**
+	 * The path to the component's files.
+	 *
+	 * @since 1.5.0
+	 * @var string $path
+	 */
+	public $path = '';
+
+	/**
+	 * The WP_Query loop for this component.
+	 *
+	 * @since 1.5.0
+	 * @var WP_Query $query
+	 */
+	public $query = false;
+
+	/**
+	 * The current ID of the queried object.
+	 *
+	 * @since 1.5.0
+	 * @var string $current_id
+	 */
+	public $current_id = '';
+
+	/**
+	 * Callback for formatting notifications.
+	 *
+	 * @since 1.5.0
+	 * @var callable $notification_callback
+	 */
+	public $notification_callback = '';
+
+	/**
+	 * WordPress Toolbar links.
+	 *
+	 * @since 1.5.0
+	 * @var array $admin_menu
+	 */
+	public $admin_menu = '';
+
+	/**
+	 * Placeholder text for component directory search box.
+	 *
+	 * @since 1.6.0
+	 * @var string $search_string
+	 */
+	public $search_string = '';
+
+	/**
+	 * Root slug for the component.
+	 *
+	 * @since 1.6.0
+	 * @var string $root_slug
+	 */
+	public $root_slug = '';
+
+	/**
+	 * Metadata tables for the component (if applicable).
+	 *
+	 * @since 2.0.0
+	 *
+	 * @var array
+	 */
+	public $meta_tables = array();
+
+	/**
+	 * Global tables for the component (if applicable).
+	 *
+	 * @since 2.0.0
+	 *
+	 * @var array
+	 */
+	public $global_tables = array();
+
+	/**
+	 * Query argument for component search URLs.
+	 *
+	 * @since 2.4.0
+	 * @var string
+	 */
+	public $search_query_arg = 's';
+
+	/** Methods ***************************************************************/
+
+	/**
+	 * Component loader.
+	 *
+	 * @since 1.5.0
+	 * @since 1.9.0 Added $params as a parameter.
+	 * @since 2.3.0 Added $params['features'] as a configurable value.
+	 * @since 2.4.0 Added $params['search_query_arg'] as a configurable value.
+	 *
+	 * @param string $id   Unique ID. Letters, numbers, and underscores only.
+	 * @param string $name Unique name. This should be a translatable name, eg.
+	 *                     __( 'Groups', 'buddypress' ).
+	 * @param string $path The file path for the component's files. Used by {@link BP_Component::includes()}.
+	 * @param array  $params {
+	 *     Additional parameters used by the component.
+	 *     @type int    $adminbar_myaccount_order Set the position for our menu under the WP Toolbar's "My Account menu".
+	 *     @type array  $features                 An array of feature names. This is used to load additional files from your
+	 *                                            component directory and for feature active checks. eg. array( 'awesome' )
+	 *                                            would look for a file called "bp-{$this->id}-awesome.php" and you could use
+	 *                                            bp_is_active( $this->id, 'awesome' ) to determine if the feature is active.
+	 *     @type string $search_query_arg         String to be used as the query argument in component search URLs.
+	 * }
+	 */
+	public function start( $id = '', $name = '', $path = '', $params = array() ) {
+
+		// Internal identifier of component.
+		$this->id   = $id;
+
+		// Internal component name.
+		$this->name = $name;
+
+		// Path for includes.
+		$this->path = $path;
+
+		// Miscellaneous component parameters that need to be set early on.
+		if ( ! empty( $params ) ) {
+			// Sets the position for our menu under the WP Toolbar's "My Account" menu.
+			if ( ! empty( $params['adminbar_myaccount_order'] ) ) {
+				$this->adminbar_myaccount_order = (int) $params['adminbar_myaccount_order'];
+			}
+
+			// Register features.
+			if ( ! empty( $params['features'] ) ) {
+				$this->features = array_map( 'sanitize_title', (array) $params['features'] );
+			}
+
+			if ( ! empty( $params['search_query_arg'] ) ) {
+				$this->search_query_arg = sanitize_title( $params['search_query_arg'] );
+			}
+
+		// Set defaults if not passed.
+		} else {
+			// New component menus are added before the settings menu if not set.
+			$this->adminbar_myaccount_order = 90;
+		}
+
+		// Move on to the next step.
+		$this->setup_actions();
+	}
+
+	/**
+	 * Set up component global variables.
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @param array $args {
+	 *     All values are optional.
+	 *     @type string   $slug                  The component slug. Used to construct certain URLs, such as 'friends' in
+	 *                                           http://example.com/members/joe/friends/. Default: the value of $this->id.
+	 *     @type string   $root_slug             The component root slug. Note that this value is generally unused if the
+	 *                                           component has a root directory (the slug will be overridden by the
+	 *                                           post_name of the directory page). Default: the slug of the directory page
+	 *                                           if one is found, otherwise an empty string.
+	 *     @type bool     $has_directory         Set to true if the component requires an associated WordPress page.
+	 *     @type callable $notification_callback Optional. The callable function that formats the component's notifications.
+	 *     @type string   $search_term           Optional. The placeholder text in the component directory search box. Eg,
+	 *                                           'Search Groups...'.
+	 *     @type array    $global_tables         Optional. An array of database table names.
+	 *     @type array    $meta_tables           Optional. An array of metadata table names.
+	 * }
+	 */
+	public function setup_globals( $args = array() ) {
+
+		/** Slugs ************************************************************
+		 */
+
+		// If a WP directory page exists for the component, it should
+		// be the default value of 'root_slug'.
+		$default_root_slug = isset( buddypress()->pages->{$this->id}->slug ) ? buddypress()->pages->{$this->id}->slug : '';
+
+		$r = wp_parse_args( $args, array(
+			'slug'                  => $this->id,
+			'root_slug'             => $default_root_slug,
+			'has_directory'         => false,
+			'directory_title'       => '',
+			'notification_callback' => '',
+			'search_string'         => '',
+			'global_tables'         => '',
+			'meta_tables'           => '',
+		) );
+
+		/**
+		 * Filters the slug to be used for the permalink URI chunk after root.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Slug to use in permalink URI chunk.
+		 */
+		$this->slug                  = apply_filters( 'bp_' . $this->id . '_slug',                  $r['slug']                  );
+
+		/**
+		 * Filters the slug used for root directory.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Root directory slug.
+		 */
+		$this->root_slug             = apply_filters( 'bp_' . $this->id . '_root_slug',             $r['root_slug']             );
+
+		/**
+		 * Filters the component's top-level directory if available.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param bool $value Whether or not there is a top-level directory.
+		 */
+		$this->has_directory         = apply_filters( 'bp_' . $this->id . '_has_directory',         $r['has_directory']         );
+
+		/**
+		 * Filters the component's directory title.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $value Title to use for the directory.
+		 */
+		$this->directory_title       = apply_filters( 'bp_' . $this->id . '_directory_title',       $r['directory_title']         );
+
+		/**
+		 * Filters the placeholder text for search inputs for component.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Name to use in search input placeholders.
+		 */
+		$this->search_string         = apply_filters( 'bp_' . $this->id . '_search_string',         $r['search_string']         );
+
+		/**
+		 * Filters the callable function that formats the component's notifications.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Function callback.
+		 */
+		$this->notification_callback = apply_filters( 'bp_' . $this->id . '_notification_callback', $r['notification_callback'] );
+
+		// Set the global table names, if applicable.
+		if ( ! empty( $r['global_tables'] ) ) {
+			$this->register_global_tables( $r['global_tables'] );
+		}
+
+		// Set the metadata table, if applicable.
+		if ( ! empty( $r['meta_tables'] ) ) {
+			$this->register_meta_tables( $r['meta_tables'] );
+		}
+
+		/** BuddyPress *******************************************************
+		 */
+
+		// Register this component in the loaded components array.
+		buddypress()->loaded_components[$this->slug] = $this->id;
+
+		/**
+		 * Fires at the end of the setup_globals method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_setup_globals' );
+	}
+
+	/**
+	 * Include required files.
+	 *
+	 * Please note that, by default, this method is fired on the bp_include
+	 * hook, with priority 8. This is necessary so that core components are
+	 * loaded in time to be available to third-party plugins. However, this
+	 * load order means that third-party plugins whose main files are
+	 * loaded at bp_include with priority 10 (as recommended), will not be
+	 * loaded in time for their includes() method to fire automatically.
+	 *
+	 * For this reason, it is recommended that your plugin has its own
+	 * method or function for requiring necessary files. If you must use
+	 * this method, you will have to call it manually in your constructor
+	 * class, ie
+	 *   $this->includes();
+	 *
+	 * Note that when you pass an array value like 'actions' to includes,
+	 * it looks for the following three files (assuming your component is
+	 * called 'my_component'):
+	 *   - ./actions
+	 *   - ./bp-my_component/actions
+	 *   - ./bp-my_component/bp-my_component-actions.php
+	 *
+	 * @since 1.5.0
+	 *
+	 *
+	 * @param array $includes An array of file names, or file name chunks,
+	 *                        to be parsed and then included.
+	 */
+	public function includes( $includes = array() ) {
+
+		// Bail if no files to include.
+		if ( ! empty( $includes ) ) {
+			$slashed_path = trailingslashit( $this->path );
+
+			// Loop through files to be included.
+			foreach ( (array) $includes as $file ) {
+
+				$paths = array(
+
+					// Passed with no extension.
+					'bp-' . $this->id . '/bp-' . $this->id . '-' . $file  . '.php',
+					'bp-' . $this->id . '-' . $file . '.php',
+					'bp-' . $this->id . '/' . $file . '.php',
+
+					// Passed with extension.
+					$file,
+					'bp-' . $this->id . '-' . $file,
+					'bp-' . $this->id . '/' . $file,
+				);
+
+				foreach ( $paths as $path ) {
+					if ( @is_file( $slashed_path . $path ) ) {
+						require( $slashed_path . $path );
+						break;
+					}
+				}
+			}
+		}
+
+		/**
+		 * Fires at the end of the includes method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		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 3.0.0
+	 */
+	public function late_includes() {}
+
+	/**
+	 * Set up the actions.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function setup_actions() {
+
+		// Setup globals.
+		add_action( 'bp_setup_globals',          array( $this, 'setup_globals'          ), 10 );
+
+		// Set up canonical stack.
+		add_action( 'bp_setup_canonical_stack',  array( $this, 'setup_canonical_stack'  ), 10 );
+
+		// Include required files. Called early to ensure that BP core
+		// components are loaded before plugins that hook their loader functions
+		// to bp_include with the default priority of 10. This is for backwards
+		// compatibility; henceforth, plugins should register themselves by
+		// extending this base class.
+		add_action( 'bp_include',                array( $this, 'includes'               ), 8 );
+
+		// Load files conditionally, based on certain pages.
+		add_action( 'bp_late_include',           array( $this, 'late_includes'          ) );
+
+		// Setup navigation.
+		add_action( 'bp_setup_nav',              array( $this, 'setup_nav'              ), 10 );
+
+		// Setup WP Toolbar menus.
+		add_action( 'bp_setup_admin_bar',        array( $this, 'setup_admin_bar'        ), $this->adminbar_myaccount_order );
+
+		// Setup component title.
+		add_action( 'bp_setup_title',            array( $this, 'setup_title'            ), 10 );
+
+		// Setup cache groups.
+		add_action( 'bp_setup_cache_groups',     array( $this, 'setup_cache_groups'     ), 10 );
+
+		// Register post types.
+		add_action( 'bp_register_post_types',    array( $this, 'register_post_types'    ), 10 );
+
+		// Register taxonomies.
+		add_action( 'bp_register_taxonomies',    array( $this, 'register_taxonomies'    ), 10 );
+
+		// Add the rewrite tags.
+		add_action( 'bp_add_rewrite_tags',       array( $this, 'add_rewrite_tags'       ), 10 );
+
+		// Add the rewrite rules.
+		add_action( 'bp_add_rewrite_rules',      array( $this, 'add_rewrite_rules'      ), 10 );
+
+		// Add the permalink structure.
+		add_action( 'bp_add_permastructs',       array( $this, 'add_permastructs'       ), 10 );
+
+		// Allow components to parse the main query.
+		add_action( 'bp_parse_query',            array( $this, 'parse_query'            ), 10 );
+
+		// Generate rewrite rules.
+		add_action( 'bp_generate_rewrite_rules', array( $this, 'generate_rewrite_rules' ), 10 );
+
+		/**
+		 * Fires at the end of the setup_actions method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_setup_actions' );
+	}
+
+	/**
+	 * Set up the canonical URL stack for this component.
+	 *
+	 * @since 2.1.0
+	 */
+	public function setup_canonical_stack() {}
+
+	/**
+	 * Set up component navigation.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see bp_core_new_nav_item() For a description of the $main_nav
+	 *      parameter formatting.
+	 * @see bp_core_new_subnav_item() For a description of how each item
+	 *      in the $sub_nav parameter array should be formatted.
+	 *
+	 * @param array $main_nav Optional. Passed directly to bp_core_new_nav_item().
+	 *                        See that function for a description.
+	 * @param array $sub_nav  Optional. Multidimensional array, each item in
+	 *                        which is passed to bp_core_new_subnav_item(). See that
+	 *                        function for a description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// No sub nav items without a main nav item.
+		if ( !empty( $main_nav ) ) {
+			bp_core_new_nav_item( $main_nav, 'members' );
+
+			// Sub nav items are not required.
+			if ( !empty( $sub_nav ) ) {
+				foreach( (array) $sub_nav as $nav ) {
+					bp_core_new_subnav_item( $nav, 'members' );
+				}
+			}
+		}
+
+		/**
+		 * Fires at the end of the setup_nav method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_setup_nav' );
+	}
+
+	/**
+	 * Set up the component entries in the WordPress Admin Bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see WP_Admin_Bar::add_menu() for a description of the syntax
+	 *      required by each item in the $wp_admin_nav parameter array.
+	 * @global object $wp_admin_bar
+	 *
+	 * @param array $wp_admin_nav An array of nav item arguments. Each item in this parameter
+	 *                            array is passed to {@link WP_Admin_Bar::add_menu()}.
+	 *                            See that method for a description of the required syntax for
+	 *                            each item.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Bail if this is an ajax request.
+		if ( defined( 'DOING_AJAX' ) ) {
+			return;
+		}
+
+		// Do not proceed if BP_USE_WP_ADMIN_BAR constant is not set or is false.
+		if ( ! bp_use_wp_admin_bar() ) {
+			return;
+		}
+
+		/**
+		 * Filters the admin navigation passed into setup_admin_bar.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param array $wp_admin_nav Array of navigation items to add.
+		 */
+		$wp_admin_nav = apply_filters( 'bp_' . $this->id . '_admin_nav', $wp_admin_nav );
+
+		// Do we have Toolbar menus to add?
+		if ( !empty( $wp_admin_nav ) ) {
+			// Fill in position if one wasn't passed for backpat.
+			$pos = 0;
+			$not_set_pos = 1;
+			foreach( $wp_admin_nav as $key => $nav ) {
+				if ( ! isset( $nav['position'] ) ) {
+					$wp_admin_nav[$key]['position'] = $pos + $not_set_pos;
+
+					if ( 9 !== $not_set_pos ) {
+						++$not_set_pos;
+					}
+				} else {
+					$pos = $nav['position'];
+
+					// Reset not set pos to 1
+					if ( $pos % 10 === 0 ) {
+						$not_set_pos = 1;
+					}
+				}
+			}
+
+			// Sort admin nav by position.
+			$wp_admin_nav = bp_sort_by_key( $wp_admin_nav, 'position', 'num' );
+
+			// Set this objects menus.
+			$this->admin_menu = $wp_admin_nav;
+
+			// Define the WordPress global.
+			global $wp_admin_bar;
+
+			// Add each admin menu.
+			foreach( $this->admin_menu as $admin_menu ) {
+				$wp_admin_bar->add_menu( $admin_menu );
+			}
+		}
+
+		/**
+		 * Fires at the end of the setup_admin_bar method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_setup_admin_bar' );
+	}
+
+	/**
+	 * Set up the component title.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function setup_title() {
+
+		/**
+		 * Fires in the setup_title method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action(  'bp_' . $this->id . '_setup_title' );
+	}
+
+	/**
+	 * Setup component-specific cache groups.
+	 *
+	 * @since 2.2.0
+	 *
+	 */
+	public function setup_cache_groups() {
+
+		/**
+		 * Fires in the setup_cache_groups method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 2.2.0
+		 */
+		do_action( 'bp_' . $this->id . '_setup_cache_groups' );
+	}
+
+	/**
+	 * Register global tables for the component, so that it may use WordPress's database API.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $tables Table names to register.
+	 */
+	public function register_global_tables( $tables = array() ) {
+
+		/**
+		 * Filters the global tables for the component, so that it may use WordPress' database API.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 * It allows for component-specific filtering of table names. To filter
+		 * *all* tables, use the 'bp_core_get_table_prefix' filter instead.
+		 *
+		 * @since 1.6.0
+		 */
+		$tables = apply_filters( 'bp_' . $this->id . '_global_tables', $tables );
+
+		// Add to the BuddyPress global object.
+		if ( !empty( $tables ) && is_array( $tables ) ) {
+			foreach ( $tables as $global_name => $table_name ) {
+				$this->{$global_name} = $table_name;
+			}
+
+			// Keep a record of the metadata tables in the component.
+			$this->global_tables = $tables;
+		}
+
+		/**
+		 * Fires at the end of the register_global_tables method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 2.0.0
+		 */
+		do_action( 'bp_' . $this->id . '_register_global_tables' );
+	}
+
+	/**
+	 * Register component metadata tables.
+	 *
+	 * Metadata tables are registered in the $wpdb global, for
+	 * compatibility with the WordPress metadata API.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $tables Table names to register.
+	 */
+	public function register_meta_tables( $tables = array() ) {
+		global $wpdb;
+
+		/**
+		 * Filters the global meta_tables for the component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 * It allows for component-specific filtering of table names. To filter
+		 * *all* tables, use the 'bp_core_get_table_prefix' filter instead.
+		 *
+		 * @since 2.0.0
+		 */
+		$tables = apply_filters( 'bp_' . $this->id . '_meta_tables', $tables );
+
+		/**
+		 * Add the name of each metadata table to WPDB to allow BuddyPress
+		 * components to play nicely with the WordPress metadata API.
+		 */
+		if ( !empty( $tables ) && is_array( $tables ) ) {
+			foreach( $tables as $meta_prefix => $table_name ) {
+				$wpdb->{$meta_prefix . 'meta'} = $table_name;
+			}
+
+			// Keep a record of the metadata tables in the component.
+			$this->meta_tables = $tables;
+		}
+
+		/**
+		 * Fires at the end of the register_meta_tables method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 2.0.0
+		 */
+		do_action( 'bp_' . $this->id . '_register_meta_tables' );
+	}
+
+	/**
+	 * Set up the component post types.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function register_post_types() {
+
+		/**
+		 * Fires in the register_post_types method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_register_post_types' );
+	}
+
+	/**
+	 * Register component-specific taxonomies.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function register_taxonomies() {
+
+		/**
+		 * Fires in the register_taxonomies method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_register_taxonomies' );
+	}
+
+	/**
+	 * Add any additional rewrite tags.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function add_rewrite_tags() {
+
+		/**
+		 * Fires in the add_rewrite_tags method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_add_rewrite_tags' );
+	}
+
+	/**
+	 * Add any additional rewrite rules.
+	 *
+	 * @since 1.9.0
+	 *
+	 */
+	public function add_rewrite_rules() {
+
+		/**
+		 * Fires in the add_rewrite_rules method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.9.0
+		 */
+		do_action( 'bp_' . $this->id . '_add_rewrite_rules' );
+	}
+
+	/**
+	 * Add any permalink structures.
+	 *
+	 * @since 1.9.0
+	 *
+	 */
+	public function add_permastructs() {
+
+		/**
+		 * Fires in the add_permastructs method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.9.0
+		 */
+		do_action( 'bp_' . $this->id . '_add_permastructs' );
+	}
+
+	/**
+	 * Allow components to parse the main query.
+	 *
+	 * @since 1.9.0
+	 *
+	 *
+	 * @param object $query The main WP_Query.
+	 */
+	public function parse_query( $query ) {
+
+		/**
+		 * Fires in the parse_query method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param object $query Main WP_Query object. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_' . $this->id . '_parse_query', array( &$query ) );
+	}
+
+	/**
+	 * Generate any additional rewrite rules.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function generate_rewrite_rules() {
+
+		/**
+		 * Fires in the generate_rewrite_rules method inside BP_Component.
+		 *
+		 * This is a dynamic hook that is based on the component string ID.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_' . $this->id . '_generate_rewrite_rules' );
+	}
+}
+endif; // BP_Component.
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-bp-nav-backcompat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-bp-nav-backcompat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-bp-nav-backcompat.php	(revision 12265)
@@ -0,0 +1,270 @@
+<?php
+/**
+ * Backward compatibility for the $bp->bp_nav global.
+ *
+ * @since 2.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * bp_nav backward compatibility class.
+ *
+ * This class is used to provide backward compatibility for extensions that access and modify
+ * the $bp->bp_nav global.
+ *
+ * @since 2.6.0
+ */
+class BP_Core_BP_Nav_BackCompat implements ArrayAccess {
+	/**
+	 * Nav items.
+	 *
+	 * @since 2.6.0
+	 * @access public
+	 * @var array
+	 */
+	public $backcompat_nav = array();
+
+	/**
+	 * Component to which nav items belong.
+	 *
+	 * @since 2.6.0
+	 * @access public
+	 * @var array
+	 */
+	public $component;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $backcompat_nav Optional. Array of nav items.
+	 */
+	public function __construct( $backcompat_nav = array() ) {
+		foreach ( $backcompat_nav as $key => $value ) {
+			if ( is_array( $value ) ) {
+				$this->backcompat_nav[ $key ] = new self( $value );
+			} else {
+				$this->backcompat_nav[ $key ] = $value;
+			}
+		}
+	}
+
+	/**
+	 * Assign a value to the nav array at the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @param array $value  Nav item.
+	 */
+	public function offsetSet( $offset, $value ) {
+		_doing_it_wrong(
+			'bp_nav',
+			__( 'The bp_nav and bp_options_nav globals should not be used directly and are deprecated. Please use the BuddyPress nav functions instead.', 'buddypress' ),
+			'2.6.0'
+		);
+
+		$bp = buddypress();
+
+		if ( is_array( $value ) ) {
+			$value = new self( $value );
+		}
+
+		if ( $offset !== null ) {
+			// Temporarily set the backcompat_nav.
+			$this->backcompat_nav[ $offset ] = $value;
+
+			$args = $this->to_array();
+			if ( isset( $args['parent_slug'] ) ) {
+				$this->get_component_nav( $args['parent_slug'] )->edit_nav( $args, $args['slug'], $args['parent_slug'] );
+			} elseif ( isset( $args['slug'] ) ) {
+				$bp->members->nav->edit_nav( $args, $args['slug'] );
+			}
+		}
+	}
+
+	/**
+	 * Get a value of the nav array at the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @return BP_Core_BP_Nav_BackCompat
+	 */
+	public function offsetGet( $offset ) {
+		_doing_it_wrong(
+			'bp_nav',
+			__( 'The bp_nav and bp_options_nav globals should not be used directly and are deprecated. Please use the BuddyPress nav functions instead.', 'buddypress' ),
+			'2.6.0'
+		);
+
+//		if ( ! isset( $this->backcompat_nav[ $offset ] ) ) {
+			$nav = $this->get_nav( $offset );
+			if ( $nav && isset( $nav[ $offset ] ) ) {
+				$this->backcompat_nav[ $offset ] = new self( $nav[ $offset ] );
+			}
+//		}
+
+		return $this->backcompat_nav[ $offset ];
+	}
+
+	/**
+	 * Check whether nav array has a value at the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @return bool
+	 */
+	public function offsetExists( $offset ) {
+		_doing_it_wrong(
+			'bp_nav',
+			__( 'The bp_nav and bp_options_nav globals should not be used directly and are deprecated. Please use the BuddyPress nav functions instead.', 'buddypress' ),
+			'2.6.0'
+		);
+
+		if ( isset( $this->backcompat_nav[ $offset ] ) ) {
+			return true;
+		}
+
+		$nav = $this->get_nav( $offset );
+		if ( $nav && isset( $nav[ $offset ] ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Unset a nav array value at the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 */
+	public function offsetUnset( $offset ) {
+		_doing_it_wrong(
+			'bp_nav',
+			__( 'The bp_nav and bp_options_nav globals should not be used directly and are deprecated. Please use the BuddyPress nav functions instead.', 'buddypress' ),
+			'2.6.0'
+		);
+
+		// For top-level nav items, the backcompat nav hasn't yet been initialized.
+		if ( ! isset( $this->backcompat_nav[ $offset ] ) ) {
+			buddypress()->members->nav->delete_nav( $offset );
+			unset( $this->backcompat_nav[ $offset ] );
+		}
+	}
+
+	/**
+	 * Set the component to which the nav belongs.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $component
+	 */
+	public function set_component( $component ) {
+		$this->component = $component;
+	}
+
+	/**
+	 * Get the component to which the a nav item belongs.
+	 *
+	 * We use the following heuristic to guess, based on an offset, which component the item belongs to:
+	 *   - If this is a group, and the offset is the same as the current group's slug, it's a group nav item.
+	 *   - Otherwise, it's a member nav item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @return string|array
+	 */
+	public function get_component( $offset = '' ) {
+		if ( ! isset( $this->component ) ) {
+			if ( bp_is_active( 'groups' ) && $offset === bp_get_current_group_slug() ) {
+				$this->component = 'groups';
+			} else {
+				$this->component = 'members';
+			}
+		}
+
+		return $this->component;
+	}
+
+	/**
+	 * Reset the cached nav items.
+	 *
+	 * Called when the nav API removes items from the nav array.
+	 *
+	 * @since 2.6.0
+	 */
+	public function reset() {
+		$this->backcompat_nav = array();
+	}
+
+	/**
+	 * Get the nav object corresponding to the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @return bool|array
+	 */
+	protected function get_nav( $offset ) {
+		$bp = buddypress();
+
+		$component_nav = $this->get_component_nav( $offset );
+		$primary_nav   = $component_nav->get_primary( array( 'slug' => $offset ), false );
+
+		$nav = array();
+
+		if ( empty( $primary_nav ) ) {
+			return $nav;
+		}
+
+		foreach ( $primary_nav as $item ) {
+			$nav[ $item->slug ] = (array) $item;
+		}
+
+		return $nav;
+	}
+
+	/**
+	 * Get the BP_Core_Nav object corresponding to the component, based on a nav item name.
+	 *
+	 * The way bp_nav was previously organized makes it impossible to know for sure which component's nav is
+	 * being referenced by a given nav item name. We guess in the following manner:
+	 *   - If we're looking at a group, and the nav item name (`$offset`) is the same as the slug of the current
+	 *     group, we assume that the proper component nav is 'groups'.
+	 *   - Otherwise, fall back on 'members'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $offset Nav item name.
+	 * @return BP_Core_Nav
+	 */
+	protected function get_component_nav( $offset = '' ) {
+		$component = $this->get_component( $offset );
+
+		$bp = buddypress();
+		if ( ! isset( $bp->{$component}->nav ) ) {
+			return false;
+		}
+
+		return $bp->{$component}->nav;
+	}
+
+	/**
+	 * Get the nav data, formatted as a flat array.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return array
+	 */
+	protected function to_array() {
+		return $this->backcompat_nav;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-bp-options-nav-backcompat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-bp-options-nav-backcompat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-bp-options-nav-backcompat.php	(revision 12265)
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Backward compatibility for the $bp->bp_options_nav global.
+ *
+ * @since 2.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * bp_options_nav backward compatibility class.
+ *
+ * This class is used to provide backward compatibility for extensions that access and modify
+ * the $bp->bp_options_nav global.
+ *
+ * @since 2.6.0
+ */
+class BP_Core_BP_Options_Nav_BackCompat extends BP_Core_BP_Nav_BackCompat {
+	/**
+	 * Parent slug of the current nav item.
+	 *
+	 * @since 2.6.0
+	 * @access protected
+	 * @var string
+	 */
+	protected $parent_slug = '';
+
+	/**
+	 * Get a value of the nav array at the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @return BP_Core_BP_Nav_BackCompat
+	 */
+	public function offsetGet( $offset ) {
+		_doing_it_wrong(
+			'bp_nav',
+			__( 'These globals should not be used directly and are deprecated. Please use the BuddyPress nav functions instead.', 'buddypress' ),
+			'2.6.0'
+		);
+
+		if ( empty( $this->backcompat_nav[ $offset ] ) ) {
+			$nav = $this->get_nav( $offset );
+			if ( $nav ) {
+				$subnavs = $this->get_component_nav( $offset )->get_secondary( array( 'parent_slug' => $offset ) );
+				$subnav_keyed = array();
+				if ( $subnavs ) {
+					foreach ( $subnavs as $subnav ) {
+						$subnav_keyed[ $subnav->slug ] = (array) $subnav;
+					}
+				}
+
+				$subnav_object = new self( $subnav_keyed );
+				$subnav_object->set_component( $this->get_component() );
+				$subnav_object->set_parent_slug( $offset );
+
+				$this->backcompat_nav[ $offset ] = $subnav_object;
+			}
+		}
+
+		if ( isset( $this->backcompat_nav[ $offset ] ) ) {
+			return $this->backcompat_nav[ $offset ];
+		}
+
+		return false;
+	}
+
+	/**
+	 * Unset a nav array value at the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 */
+	public function offsetUnset( $offset ) {
+		_doing_it_wrong(
+			'bp_nav',
+			__( 'These globals should not be used directly and are deprecated. Please use the BuddyPress nav functions instead.', 'buddypress' ),
+			'2.6.0'
+		);
+
+		$this->get_component_nav( $offset )->delete_nav( $offset, $this->get_parent_slug() );
+
+		// Clear the cached nav.
+		unset( $this->backcompat_nav[ $offset ] );
+	}
+
+	/**
+	 * Get the parent slug of the current nav item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return string
+	 */
+	public function get_parent_slug() {
+		return $this->parent_slug;
+	}
+
+	/**
+	 * Set the parent slug of the current nav item.
+	 *
+	 * @since 2.6.0
+	 */
+	public function set_parent_slug( $slug ) {
+		$this->parent_slug = $slug;
+	}
+
+	/**
+	 * Get the nav object corresponding to the specified offset.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param mixed $offset Array offset.
+	 * @return bool|array
+	 */
+	public function get_nav( $offset ) {
+		$nav = parent::get_nav( $offset );
+
+		if ( ! $nav ) {
+			$component_nav = $this->get_component_nav( $offset );
+			$secondary_nav = $component_nav->get_secondary( array( 'slug' => $offset ), false );
+
+			$nav = array();
+
+			if ( empty( $primary_nav ) ) {
+				return $nav;
+			}
+
+			foreach ( $primary_nav as $item ) {
+				$nav[ $item->slug ] = (array) $item;
+			}
+		}
+
+		return $nav;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-html-element.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-html-element.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-html-element.php	(revision 12265)
@@ -0,0 +1,127 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.7.0
+ */
+
+/**
+ * Generate markup for an HTML element.
+ *
+ * @since 2.7.0
+ */
+class BP_Core_HTML_Element {
+	/**
+	 * Open tag for an element.
+	 *
+	 * This would include attributes if applicable. eg. '<a href="" class="">'
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var string
+	 */
+	public $open_tag   = '';
+
+	/**
+	 * Inner HTML for an element.
+	 *
+	 * For example, this could be anchor text within an <a> element.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var string
+	 */
+	public $inner_html = '';
+
+	/**
+	 * Closing tag for an element.
+	 *
+	 * For example, "</a>".
+	 *
+	 * @since 2.7.0
+	 *
+	 * @var string
+	 */
+	public $close_tag  = '';
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $r {
+	 *     An array of arguments.
+	 *     @type string $element    The element to render. eg. 'a' for the anchor element.
+	 *     @type array  $attr       Optional. The element's attributes set as key/value pairs. eg.
+	 *                              array( 'href' => 'http://example.com', 'class' => 'my-class' )
+	 *     @type string $inner_html Optional. The inner HTML for the element if applicable. Please note that
+	 *                              this isn't sanitized, so you should use your own sanitization routine
+	 *                              before using this parameter.
+	 * }
+	 */
+	public function __construct( $r = array() ) {
+		$elem = sanitize_html_class( $r['element'] );
+		if ( empty( $elem ) ) {
+			return;
+		}
+
+		// Render attributes.
+		$attributes = '';
+		foreach( (array) $r['attr'] as $attr => $val ) {
+			// If attribute is empty, skip.
+			if ( empty( $val ) ) {
+				continue;
+			}
+
+			if ( 'href' === $attr || 'formaction' === $attr || 'src' === $attr ) {
+				$val = esc_url( $val );
+			} elseif ( 'id' === $attr ) {
+				$val = sanitize_html_class( $val );
+			} else {
+				$val = esc_attr( $val );
+			}
+
+			$attributes .= sprintf( '%s="%s" ', sanitize_html_class( $attr ), $val );
+		}
+
+		// <input> / <img> is self-closing.
+		if ( 'input' === $elem || 'img' === $elem ) {
+			$this->open_tag = sprintf( '<%1$s %2$s />', $elem, $attributes );
+
+		// All other elements.
+		} else {
+			$this->open_tag   = sprintf( '<%1$s %2$s>', $elem, $attributes );
+			$this->inner_html = ! empty( $r['inner_html'] ) ? $r['inner_html'] : '';
+			$this->close_tag  = sprintf( '</%1$s>',	$elem );
+		}
+	}
+
+	/**
+	 * Returns a property from this class.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param  string $prop Property name. Either 'open_tag', 'inner_html', 'close_tag'.
+	 * @return string
+	 */
+	public function get( $prop = '' ) {
+		if ( ! isset( $this->{$prop} ) ) {
+			return '';
+		}
+
+		return $this->{$prop};
+	}
+
+	/**
+	 * Returns full contents of HTML element.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return string
+	 */
+	public function contents() {
+		return $this->open_tag . $this->inner_html . $this->close_tag;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-login-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-login-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-login-widget.php	(revision 12265)
@@ -0,0 +1,186 @@
+<?php
+/**
+ * BuddyPress Core Login Widget.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Login Widget.
+ *
+ * @since 1.9.0
+ */
+class BP_Core_Login_Widget extends WP_Widget {
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.9.0
+	 */
+	public function __construct() {
+		parent::__construct(
+			false,
+			_x( '(BuddyPress) Log In', 'Title of the login widget', 'buddypress' ),
+			array(
+				'description'                 => __( 'Show a Log In form to logged-out visitors, and a Log Out link to those who are logged in.', 'buddypress' ),
+				'classname'                   => 'widget_bp_core_login_widget buddypress widget',
+				'customize_selective_refresh' => true,
+			)
+		);
+	}
+
+	/**
+	 * Display the login widget.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see WP_Widget::widget() for description of parameters.
+	 *
+	 * @param array $args     Widget arguments.
+	 * @param array $instance Widget settings, as saved by the user.
+	 */
+	public function widget( $args, $instance ) {
+		$title = isset( $instance['title'] ) ? $instance['title'] : '';
+
+		/**
+		 * Filters the title of the Login widget.
+		 *
+		 * @since 1.9.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
+
+		echo $args['before_widget'];
+
+		echo $args['before_title'] . esc_html( $title ) . $args['after_title']; ?>
+
+		<?php if ( is_user_logged_in() ) : ?>
+
+			<?php
+			/**
+			 * Fires before the display of widget content if logged in.
+			 *
+			 * @since 1.9.0
+			 */
+			do_action( 'bp_before_login_widget_loggedin' ); ?>
+
+			<div class="bp-login-widget-user-avatar">
+				<a href="<?php echo bp_loggedin_user_domain(); ?>">
+					<?php bp_loggedin_user_avatar( 'type=thumb&width=50&height=50' ); ?>
+				</a>
+			</div>
+
+			<div class="bp-login-widget-user-links">
+				<div class="bp-login-widget-user-link"><?php echo bp_core_get_userlink( bp_loggedin_user_id() ); ?></div>
+				<div class="bp-login-widget-user-logout"><a class="logout" href="<?php echo wp_logout_url( bp_get_requested_url() ); ?>"><?php _e( 'Log Out', 'buddypress' ); ?></a></div>
+			</div>
+
+			<?php
+
+			/**
+			 * Fires after the display of widget content if logged in.
+			 *
+			 * @since 1.9.0
+			 */
+			do_action( 'bp_after_login_widget_loggedin' ); ?>
+
+		<?php else : ?>
+
+			<?php
+
+			/**
+			 * Fires before the display of widget content if logged out.
+			 *
+			 * @since 1.9.0
+			 */
+			do_action( 'bp_before_login_widget_loggedout' ); ?>
+
+			<form name="bp-login-form" id="bp-login-widget-form" class="standard-form" action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post">
+				<label for="bp-login-widget-user-login"><?php _e( 'Username', 'buddypress' ); ?></label>
+				<input type="text" name="log" id="bp-login-widget-user-login" class="input" value="" />
+
+				<label for="bp-login-widget-user-pass"><?php _e( 'Password', 'buddypress' ); ?></label>
+				<input type="password" name="pwd" id="bp-login-widget-user-pass" class="input" value="" <?php bp_form_field_attributes( 'password' ) ?> />
+
+				<div class="forgetmenot"><label for="bp-login-widget-rememberme"><input name="rememberme" type="checkbox" id="bp-login-widget-rememberme" value="forever" /> <?php _e( 'Remember Me', 'buddypress' ); ?></label></div>
+
+				<input type="submit" name="wp-submit" id="bp-login-widget-submit" value="<?php esc_attr_e( 'Log In', 'buddypress' ); ?>" />
+
+				<?php if ( bp_get_signup_allowed() ) : ?>
+
+					<span class="bp-login-widget-register-link"><a href="<?php echo esc_url( bp_get_signup_page() ); ?>"><?php _e( 'Register', 'buddypress' ); ?></a></span>
+
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires inside the display of the login widget form.
+				 *
+				 * @since 2.4.0
+				 */
+				do_action( 'bp_login_widget_form' ); ?>
+
+			</form>
+
+			<?php
+
+			/**
+			 * Fires after the display of widget content if logged out.
+			 *
+			 * @since 1.9.0
+			 */
+			do_action( 'bp_after_login_widget_loggedout' ); ?>
+
+		<?php endif;
+
+		echo $args['after_widget'];
+	}
+
+	/**
+	 * Update the login widget options.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $new_instance The new instance options.
+	 * @param array $old_instance The old instance options.
+	 * @return array $instance The parsed options to be saved.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance             = $old_instance;
+		$instance['title']    = isset( $new_instance['title'] ) ? strip_tags( $new_instance['title'] ) : '';
+
+		return $instance;
+	}
+
+	/**
+	 * Output the login widget options form.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $instance Settings for this widget.
+	 * @return void
+	 */
+	public function form( $instance = array() ) {
+
+		$settings = wp_parse_args( $instance, array(
+			'title' => '',
+		) ); ?>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'buddypress' ); ?>
+			<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $settings['title'] ); ?>" /></label>
+		</p>
+
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-nav-item.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-nav-item.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-nav-item.php	(revision 12265)
@@ -0,0 +1,15 @@
+<?php
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Navigation item.
+ *
+ * @since 2.6.0
+ */
+class BP_Core_Nav_Item extends ArrayObject {
+	public function __construct( $data ) {
+		parent::__construct( $data, ArrayObject::ARRAY_AS_PROPS );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-nav.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-nav.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-nav.php	(revision 12265)
@@ -0,0 +1,402 @@
+<?php
+/**
+ * Core component class.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Nav.
+ *
+ * This class is used to build each component's navigation.
+ *
+ * @since 2.6.0
+ */
+class BP_Core_Nav {
+	/**
+	 * An associative array containing the nav items for the object ID.
+	 *
+	 * @since 2.6.0
+	 * @var array
+	 */
+	protected $nav;
+
+	/**
+	 * The current object ID.
+	 *
+	 * @since 2.6.0
+	 * @var int
+	 */
+	private $object_id;
+
+	/**
+	 * Initializes the Nav belonging to the specified object.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int $object_id The item ID to build the nav for. Default is the displayed user ID.
+	 */
+	public function __construct( $object_id = 0 ) {
+		if ( empty( $object_id ) ) {
+			$this->object_id = (int) bp_displayed_user_id();
+		} else {
+			$this->object_id = (int) $object_id;
+		}
+
+		$this->nav[ $this->object_id ] = array();
+	}
+
+	/**
+	 * Checks whether a nav item is set.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $key The requested nav slug.
+	 * @return bool True if the nav item is set, false otherwise.
+	 */
+	public function __isset( $key ) {
+		return isset( $this->nav[ $this->object_id ][ $key ] );
+	}
+
+	/**
+	 * Gets a nav item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $key The requested nav slug.
+	 * @return mixed The value corresponding to the requested nav item.
+	 */
+	public function __get( $key ) {
+		if ( ! isset( $this->nav[ $this->object_id ][ $key ] ) ) {
+			$this->nav[ $this->object_id ][ $key ] = null;
+		}
+
+		return $this->nav[ $this->object_id ][ $key ];
+	}
+
+	/**
+	 * Sets a nav item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $key   The requested nav slug.
+	 * @param mixed  $value The value of the nav item.
+	 */
+	public function __set( $key, $value ) {
+		if ( is_array( $value ) ) {
+			$value['primary'] = true;
+		}
+
+		$this->nav[ $this->object_id ][ $key ] = new BP_Core_Nav_Item( $value );
+	}
+
+	/**
+	 * Gets a specific nav item or array of nav items.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $key The nav item slug to get. Optional.
+	 * @return mixed       An array of nav item, a single nav item, or null if none found.
+	 */
+	public function get( $key = '' ) {
+		$return = null;
+
+		// Return the requested nav item.
+		if ( ! empty( $key ) ) {
+			if ( ! isset( $this->nav[ $this->object_id ][ $key ] ) ) {
+				$return = null;
+			} else {
+				$return = $this->nav[ $this->object_id ][ $key ];
+			}
+
+		// Return all nav item items.
+		} else {
+			$return = $this->nav[ $this->object_id ];
+		}
+
+		return $return;
+	}
+
+	/**
+	 * Adds a new nav item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $args The nav item's arguments.
+	 * @return BP_Core_Nav_Item
+	 */
+	public function add_nav( $args ) {
+		if ( empty( $args['slug'] ) ) {
+			return false;
+		}
+
+		// We have a child and the parent exists.
+		if ( ! empty( $args['parent_slug'] ) ) {
+			$slug              = $args['parent_slug'] . '/' . $args['slug'];
+			$args['secondary'] = true;
+
+		// This is a parent.
+		} else {
+			$slug            = $args['slug'];
+			$args['primary'] = true;
+		}
+
+		// Add to the nav.
+		$this->nav[ $this->object_id ][ $slug ] = new BP_Core_Nav_Item( $args );
+
+		return $this->nav[ $this->object_id ][ $slug ];
+	}
+
+	/**
+	 * Edits a nav item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array  $args        The nav item's arguments.
+	 * @param string $slug        The slug of the nav item.
+	 * @param string $parent_slug The slug of the parent nav item (required to edit a child).
+	 * @return BP_Core_Nav_Item
+	 */
+	public function edit_nav( $args = array(), $slug = '', $parent_slug = '' ) {
+		if ( empty( $slug ) ) {
+			return false;
+		}
+
+		// We're editing a parent!
+		if ( empty( $parent_slug ) ) {
+			$nav_items = $this->get_primary( array( 'slug' => $slug ), false );
+
+			if ( ! $nav_items ) {
+				return false;
+			}
+
+			$nav_item = reset( $nav_items );
+			$this->nav[ $this->object_id ][ $slug ] = new BP_Core_Nav_Item( wp_parse_args( $args, (array) $nav_item ) );
+
+			// Return the edited object.
+			return $this->nav[ $this->object_id ][ $slug ];
+
+		// We're editing a child.
+		} else {
+			$sub_items = $this->get_secondary( array( 'parent_slug' => $parent_slug, 'slug' => $slug ), false );
+
+			if ( ! $sub_items ) {
+				return false;
+			}
+
+			$sub_item = reset( $sub_items );
+
+			$params = wp_parse_args( $args, (array) $sub_item );
+
+			// When we have parents, it's for life, we can't change them!
+			if ( empty( $params['parent_slug'] ) || $parent_slug !== $params['parent_slug'] ) {
+				return false;
+			}
+
+			$this->nav[ $this->object_id ][ $parent_slug . '/' . $slug ] = new BP_Core_Nav_Item( $params );
+
+			// Return the edited object.
+			return $this->nav[ $this->object_id ][ $parent_slug . '/' . $slug ];
+		}
+	}
+
+	/**
+	 * Unset an item or a subitem of the nav.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $slug        The slug of the main item.
+	 * @param string $parent_slug The slug of the sub item.
+	 * @return false|callable|array False on failure, the screen function(s) on success.
+	 */
+	public function delete_nav( $slug = '', $parent_slug = '' ) {
+
+		// Bail if slug is empty
+		if ( empty( $slug ) ) {
+			return false;
+		}
+
+		// We're deleting a child
+		if ( ! empty( $parent_slug ) ) {
+
+			// Validate the subnav
+			$sub_items = $this->get_secondary( array( 'parent_slug' => $parent_slug, 'slug' => $slug ), false );
+
+			if ( ! $sub_items ) {
+				return false;
+			}
+
+			$sub_item = reset( $sub_items );
+
+			if ( empty( $sub_item->slug ) ) {
+				return false;
+			}
+
+			// Delete the child
+			unset( $this->nav[ $this->object_id ][ $parent_slug . '/' . $slug ] );
+
+			// Return the deleted item's screen function
+			return array( $sub_item->screen_function );
+
+		// We're deleting a parent
+		} else {
+			// Validate the nav
+			$nav_items = $this->get_primary( array( 'slug' => $slug ), false );
+
+			if ( ! $nav_items ) {
+				return false;
+			}
+
+			$nav_item = reset( $nav_items );
+
+			if ( empty( $nav_item->slug ) ) {
+				return false;
+			}
+
+			$screen_functions = array( $nav_item->screen_function );
+
+			// Life's unfair, children won't survive the parent :(
+			$sub_items = $this->get_secondary( array( 'parent_slug' => $nav_item->slug ), false );
+
+			if ( ! empty( $sub_items ) ) {
+				foreach ( $sub_items as $sub_item ) {
+					$screen_functions[] = $sub_item->screen_function;
+
+					// Delete the child
+					unset( $this->nav[ $this->object_id ][ $nav_item->slug . '/' . $sub_item->slug ] );
+				}
+			}
+
+			// Delete the parent.
+			unset( $this->nav[ $this->object_id ][ $nav_item->slug ] );
+
+			// Return the deleted item's screen functions.
+			return $screen_functions;
+		}
+	}
+
+	/**
+	 * Sorts a list of nav items.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $items The nav items to sort.
+	 * @return array
+	 */
+	public function sort_nav( $items ) {
+		$sorted = array();
+
+		foreach ( $items as $item ) {
+			// Default position
+			$position = 99;
+
+			if ( isset( $item->position ) ) {
+				$position = (int) $item->position;
+			}
+
+			// If position is already taken, move to the first next available
+			if ( isset( $sorted[ $position ] ) ) {
+				$sorted_keys = array_keys( $sorted );
+
+				do {
+					$position += 1;
+				} while ( in_array( $position, $sorted_keys ) );
+			}
+
+			$sorted[ $position ] = $item;
+		}
+
+		ksort( $sorted );
+		return $sorted;
+	}
+
+	/**
+	 * Gets the primary nav items.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $args Filters to select the specific primary items. See wp_list_filter().
+	 * @param bool  $sort True to sort the nav items. False otherwise.
+	 * @return array The list of primary objects nav
+	 */
+	public function get_primary( $args = array(), $sort = true ) {
+		$params = wp_parse_args( $args, array( 'primary' => true ) );
+
+		// This parameter is not overridable.
+		if ( empty( $params['primary'] ) ) {
+			return false;
+		}
+
+		$primary_nav = wp_list_filter( $this->nav[ $this->object_id ], $params );
+
+		if ( ! $primary_nav ) {
+			return false;
+		}
+
+		if ( true !== $sort ) {
+			return $primary_nav;
+		}
+
+		return $this->sort_nav( $primary_nav );
+	}
+
+	/**
+	 * Gets the secondary nav items.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $args Filters to select the specific secondary items. See wp_list_filter().
+	 * @param bool  $sort True to sort the nav items. False otherwise.
+	 * @return bool|array The list of secondary objects nav, or false if none set.
+	 */
+	public function get_secondary( $args = array(), $sort = true ) {
+		$params = wp_parse_args( $args, array( 'parent_slug' => '' ) );
+
+		// No need to search children if the parent is not set.
+		if ( empty( $params['parent_slug'] ) && empty( $params['secondary'] ) ) {
+			return false;
+		}
+
+		$secondary_nav = wp_list_filter( $this->nav[ $this->object_id ], $params );
+
+		if ( ! $secondary_nav ) {
+			return false;
+		}
+
+		if ( true !== $sort ) {
+			return $secondary_nav;
+		}
+
+		return $this->sort_nav( $secondary_nav );
+	}
+
+	/**
+	 * Gets a nested list of visible nav items.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return array The list of visible nav items.
+	 */
+	public function get_item_nav() {
+		$primary_nav_items = $this->get_primary( array( 'show_for_displayed_user' => true ) );
+
+		if ( $primary_nav_items ) {
+			foreach( $primary_nav_items as $key_nav => $primary_nav ) {
+				// Try to get the children.
+				$children = $this->get_secondary( array( 'parent_slug' => $primary_nav->slug, 'user_has_access' => true ) );
+
+				if ( $children ) {
+					$primary_nav_items[ $key_nav ] = clone $primary_nav;
+					$primary_nav_items[ $key_nav ]->children = $children;
+				}
+			}
+		}
+
+		return $primary_nav_items;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-notification.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-notification.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-notification.php	(revision 12265)
@@ -0,0 +1,282 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BP_Core_Notification is deprecated.
+ *
+ * Use BP_Notifications_Notification instead.
+ *
+ * @deprecated since 1.9.0
+ */
+class BP_Core_Notification {
+
+	/**
+	 * The notification id.
+	 *
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * The ID to which the notification relates to within the component.
+	 *
+	 * @var int
+	 */
+	public $item_id;
+
+	/**
+	 * The secondary ID to which the notification relates to within the component.
+	 *
+	 * @var int
+	 */
+	public $secondary_item_id = null;
+
+	/**
+	 * The user ID for who the notification is for.
+	 *
+	 * @var int
+	 */
+	public $user_id;
+
+	/**
+	 * The name of the component that the notification is for.
+	 *
+	 * @var string
+	 */
+	public $component_name;
+
+	/**
+	 * The action within the component which the notification is related to.
+	 *
+	 * @var string
+	 */
+	public $component_action;
+
+	/**
+	 * The date the notification was created.
+	 *
+	 * @var string
+	 */
+	public $date_notified;
+
+	/**
+	 * Is the notification new or has it already been read.
+	 *
+	 * @var boolean
+	 */
+	public $is_new;
+
+	/** Public Methods ********************************************************/
+
+	/**
+	 * Constructor
+	 *
+	 * @param int $id ID for the notification.
+	 */
+	public function __construct( $id = 0 ) {
+		if ( !empty( $id ) ) {
+			$this->id = $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Update or insert notification details into the database.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @return bool Success or failure.
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// Update.
+		if ( !empty( $this->id ) ) {
+			$sql = $wpdb->prepare( "UPDATE {$bp->core->table_name_notifications} SET item_id = %d, secondary_item_id = %d, user_id = %d, component_name = %s, component_action = %d, date_notified = %s, is_new = %d ) WHERE id = %d", $this->item_id, $this->secondary_item_id, $this->user_id, $this->component_name, $this->component_action, $this->date_notified, $this->is_new, $this->id );
+
+		// Save.
+		} else {
+			$sql = $wpdb->prepare( "INSERT INTO {$bp->core->table_name_notifications} ( item_id, secondary_item_id, user_id, component_name, component_action, date_notified, is_new ) VALUES ( %d, %d, %d, %s, %s, %s, %d )", $this->item_id, $this->secondary_item_id, $this->user_id, $this->component_name, $this->component_action, $this->date_notified, $this->is_new );
+		}
+
+		if ( !$result = $wpdb->query( $sql ) )
+			return false;
+
+		$this->id = $wpdb->insert_id;
+
+		return true;
+	}
+
+	/** Private Methods *******************************************************/
+
+	/**
+	 * Fetches the notification data from the database.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( $notification = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->core->table_name_notifications} WHERE id = %d", $this->id ) ) ) {
+			$this->item_id = $notification->item_id;
+			$this->secondary_item_id = $notification->secondary_item_id;
+			$this->user_id           = $notification->user_id;
+			$this->component_name    = $notification->component_name;
+			$this->component_action  = $notification->component_action;
+			$this->date_notified     = $notification->date_notified;
+			$this->is_new            = $notification->is_new;
+		}
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Check the access for a user.
+	 *
+	 * @param int $user_id         ID to check access for.
+	 * @param int $notification_id Notification ID to check for.
+	 * @return string
+	 */
+	public static function check_access( $user_id, $notification_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->core->table_name_notifications} WHERE id = %d AND user_id = %d", $notification_id, $user_id ) );
+	}
+
+	/**
+	 * Fetches all the notifications in the database for a specific user.
+	 *
+	 * @global wpdb $wpdb WordPress database object
+	 *
+	 * @static
+	 *
+	 * @param int    $user_id User ID.
+	 * @param string $status 'is_new' or 'all'.
+	 * @return array Associative array
+	 */
+	public static function get_all_for_user( $user_id, $status = 'is_new' ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$is_new = ( 'is_new' === $status )
+			? ' AND is_new = 1 '
+			: '';
+
+		return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->core->table_name_notifications} WHERE user_id = %d {$is_new}", $user_id ) );
+	}
+
+	/**
+	 * Delete all the notifications for a user based on the component name and action.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @static
+	 *
+	 * @param int    $user_id          ID of the user to delet notification for.
+	 * @param string $component_name   Component name.
+	 * @param string $component_action Component action.
+	 * @return mixed
+	 */
+	public static function delete_for_user_by_type( $user_id, $component_name, $component_action ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE user_id = %d AND component_name = %s AND component_action = %s", $user_id, $component_name, $component_action ) );
+	}
+
+	/**
+	 * Delete all the notifications that have a specific item id, component name and action.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @static
+	 *
+	 * @param int      $user_id           The ID of the user who the notifications are for.
+	 * @param int      $item_id           The item ID of the notifications we wish to delete.
+	 * @param string   $component_name    The name of the component that the notifications we wish to delete.
+	 * @param string   $component_action  The action of the component that the notifications we wish to delete.
+	 * @param int|bool $secondary_item_id (optional) The secondary item id of the notifications that we wish to
+	 *                                    use to delete.
+	 * @return mixed
+	 */
+	public static function delete_for_user_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id = false ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$secondary_item_sql = !empty( $secondary_item_id )
+			? $wpdb->prepare( " AND secondary_item_id = %d", $secondary_item_id )
+			: '';
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE user_id = %d AND item_id = %d AND component_name = %s AND component_action = %s{$secondary_item_sql}", $user_id, $item_id, $component_name, $component_action ) );
+	}
+
+	/**
+	 * Deletes all the notifications sent by a specific user, by component and action.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @static
+	 *
+	 * @param int    $user_id          The ID of the user whose sent notifications we wish to delete.
+	 * @param string $component_name   The name of the component the notification was sent from.
+	 * @param string $component_action The action of the component the notification was sent from.
+	 * @return mixed
+	 */
+	public static function delete_from_user_by_type( $user_id, $component_name, $component_action ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE item_id = %d AND component_name = %s AND component_action = %s", $user_id, $component_name, $component_action ) );
+	}
+
+	/**
+	 * Deletes all the notifications for all users by item id, and optional secondary item id,
+	 * and component name and action.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @static
+	 *
+	 * @param string $item_id           The item id that they notifications are to be for.
+	 * @param string $component_name    The component that the notifications are to be from.
+	 * @param string $component_action  The action that the notifications are to be from.
+	 * @param string $secondary_item_id Optional secondary item id that the notifications are to have.
+	 * @return mixed
+	 */
+	public static function delete_all_by_type( $item_id, $component_name, $component_action, $secondary_item_id ) {
+		global $wpdb;
+
+		if ( $component_action )
+			$component_action_sql = $wpdb->prepare( "AND component_action = %s", $component_action );
+		else
+			$component_action_sql = '';
+
+		if ( $secondary_item_id )
+			$secondary_item_sql = $wpdb->prepare( "AND secondary_item_id = %d", $secondary_item_id );
+		else
+			$secondary_item_sql = '';
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE item_id = %d AND component_name = %s {$component_action_sql} {$secondary_item_sql}", $item_id, $component_name ) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-oembed-extension.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-oembed-extension.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-oembed-extension.php	(revision 12265)
@@ -0,0 +1,624 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * API for responding and returning a custom oEmbed request.
+ *
+ * @since 2.6.0
+ */
+abstract class BP_Core_oEmbed_Extension {
+
+	/** START PROPERTIES ****************************************************/
+
+	/**
+	 * (required) The slug endpoint.
+	 *
+	 * Should be your component id.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var string
+	 */
+	public $slug_endpoint = '';
+
+	/** END PROPERTIES ******************************************************/
+
+	/**
+	 * Constructor.
+	 */
+	final public function __construct() {
+		$this->setup_properties();
+
+		// Some rudimentary logic checking.
+		if ( empty( $this->slug_endpoint ) ) {
+			return;
+		}
+
+		$this->setup_hooks();
+		$this->custom_hooks();
+	}
+
+	/** REQUIRED METHODS ****************************************************/
+
+	/**
+	 * Add content for your oEmbed response here.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return null
+	 */
+	abstract protected function content();
+
+	/**
+	 * Add a check for when you are on the page you want to oEmbed.
+	 *
+	 * You'll want to return a boolean here. eg. bp_is_single_activity().
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return bool
+	 */
+	abstract protected function is_page();
+
+	/**
+	 * Validate the URL to see if it matches your item ID.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $url URL to validate.
+	 * @return int Your item ID
+	 */
+	abstract protected function validate_url_to_item_id( $url );
+
+	/**
+	 * Set the oEmbed response data.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int $item_id Your item ID to do checks against.
+	 * @return array Should contain 'content', 'title', 'author_url', 'author_name' as array
+	 *               keys. 'author_url' and 'author_name' is optional; the rest are required.
+	 */
+	abstract protected function set_oembed_response_data( $item_id );
+
+	/**
+	 * Sets the fallback HTML for the oEmbed response.
+	 *
+	 * In a WordPress oEmbed item, the fallback HTML is a <blockquote>.  This is
+	 * usually hidden after the <iframe> is loaded.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int $item_id Your item ID to do checks against.
+	 * @return string Fallback HTML you want to output.
+	 */
+	abstract protected function set_fallback_html( $item_id );
+
+	/** OPTIONAL METHODS ****************************************************/
+
+	/**
+	 * If your oEmbed endpoint requires additional arguments, set them here.
+	 *
+	 * @see register_rest_route() View the $args parameter for more info.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return array
+	 */
+	protected function set_route_args() {
+		return array();
+	}
+
+	/**
+	 * Set the iframe title.
+	 *
+	 * If not set, this will fallback to WP's 'Embedded WordPress Post'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int $item_id The item ID to do checks for.
+	 */
+	protected function set_iframe_title( $item_id ) {}
+
+	/**
+	 * Do what you need to do here to initialize any custom hooks.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function custom_hooks() {}
+
+	/**
+	 * Set permalink for oEmbed link discovery.
+	 *
+	 * This method will be called on the page we want to oEmbed.  In most cases,
+	 * you will not need to override this method.  However, if you need to, do
+	 * override in your extended class.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function set_permalink() {
+		$url = bp_get_requested_url();
+
+		// Remove querystring from bp_get_requested_url().
+		if ( false !== strpos( bp_get_requested_url(), '?' ) ) {
+			$url = substr( bp_get_requested_url(), 0, strpos( bp_get_requested_url(), '?' ) );
+		}
+
+		return $url;
+	}
+
+	/** HELPERS *************************************************************/
+
+	/**
+	 * Get the item ID when filtering the oEmbed HTML.
+	 *
+	 * Should only be used during the 'embed_html' hook.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function get_item_id() {
+		return $this->is_page() ? $this->validate_url_to_item_id( $this->set_permalink() ) : buddypress()->{$this->slug_endpoint}->embedid_in_progress;
+	}
+
+	/** SET UP **************************************************************/
+
+	/**
+	 * Set up properties.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function setup_properties() {
+		$this->slug_endpoint = sanitize_title( $this->slug_endpoint );
+	}
+
+	/**
+	 * Hooks! We do the dirty work here, so you don't have to! :)
+	 *
+	 * More hooks are available in the setup_template_parts() method.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function setup_hooks() {
+		add_action( 'rest_api_init',    array( $this, 'register_route' ) );
+		add_action( 'bp_embed_content', array( $this, 'inject_content' ) );
+
+		add_filter( 'embed_template', array( $this, 'setup_template_parts' ) );
+		add_filter( 'post_embed_url', array( $this, 'filter_embed_url' ) );
+		add_filter( 'embed_html',     array( $this, 'filter_embed_html' ) );
+		add_filter( 'oembed_discovery_links', array( $this, 'add_oembed_discovery_links' ) );
+		add_filter( 'rest_pre_serve_request', array( $this, 'oembed_xml_request' ), 20, 4 );
+	}
+
+	/** HOOKS ***************************************************************/
+
+	/**
+	 * Register the oEmbed REST API route.
+	 *
+	 * @since 2.6.0
+	 */
+	public function register_route() {
+		/** This filter is documented in wp-includes/class-wp-oembed-controller.php */
+		$maxwidth = apply_filters( 'oembed_default_width', 600 );
+
+		// Required arguments.
+		$args = array(
+			'url'      => array(
+				'required'          => true,
+				'sanitize_callback' => 'esc_url_raw',
+			),
+			'format'   => array(
+				'default'           => 'json',
+				'sanitize_callback' => 'wp_oembed_ensure_format',
+			),
+			'maxwidth' => array(
+				'default'           => $maxwidth,
+				'sanitize_callback' => 'absint',
+			)
+		);
+
+		// Merge custom arguments here.
+		$args = $args + (array) $this->set_route_args();
+
+		register_rest_route( 'oembed/1.0', "/embed/{$this->slug_endpoint}", array(
+			array(
+				'methods'  => WP_REST_Server::READABLE,
+				'callback' => array( $this, 'get_item' ),
+				'args'     => $args
+			),
+		) );
+	}
+
+	/**
+	 * Set up custom embed template parts for BuddyPress use.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $template File path to current embed template.
+	 * @return string
+	 */
+	public function setup_template_parts( $template ) {
+		// Determine if we're on our BP page.
+		if ( ! $this->is_page() || is_404() ) {
+			return $template;
+		}
+
+		// Set up some BP-specific embed template overrides.
+		add_action( 'get_template_part_embed', array( $this, 'content_buffer_start' ), -999, 2 );
+		add_action( 'get_footer',              array( $this, 'content_buffer_end' ), -999 );
+
+		// Return the original WP embed template.
+		return $template;
+	}
+
+	/**
+	 * Start object buffer.
+	 *
+	 * We're going to override WP's get_template_part( 'embed, 'content' ) call
+	 * and inject our own template for BuddyPress use.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $slug Template slug.
+	 * @param string $name Template name.
+	 */
+	public function content_buffer_start( $slug, $name ) {
+		if ( 'embed' !== $slug || 'content' !== $name ) {
+			return;
+		}
+
+		// Start the buffer to wipe out get_template_part( 'embed, 'content' ).
+		ob_start();
+	}
+
+	/**
+	 * End object buffer.
+	 *
+	 * We're going to override WP's get_template_part( 'embed, 'content' ) call
+	 * and inject our own template for BuddyPress use.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $name Template name.
+	 */
+	public function content_buffer_end( $name ) {
+		if ( 'embed' !== $name || is_404() ) {
+			return;
+		}
+
+		// Wipe out get_template_part( 'embed, 'content' ).
+		ob_end_clean();
+
+		// Start our custom BuddyPress embed template!
+		echo '<div ';
+		post_class( 'wp-embed' );
+		echo '>';
+
+		// Template part for our embed header.
+		bp_get_asset_template_part( 'embeds/header', bp_current_component() );
+
+		/**
+		 * Inject BuddyPress embed content on this hook.
+		 *
+		 * You shouldn't really need to use this if you extend the
+		 * {@link BP_oEmbed_Component} class.
+		 *
+		 * @since 2.6.0
+		 */
+		do_action( 'bp_embed_content' );
+
+		// Template part for our embed footer.
+		bp_get_asset_template_part( 'embeds/footer', bp_current_component() );
+
+		echo '</div>';
+	}
+
+	/**
+	 * Adds oEmbed discovery links on single activity pages.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $retval Current discovery links.
+	 * @return string
+	 */
+	public function add_oembed_discovery_links( $retval ) {
+		if ( ! $this->is_page() ) {
+			return $retval;
+		}
+
+		$permalink = $this->set_permalink();
+		if ( empty( $permalink ) ) {
+			return $retval;
+		}
+
+		add_filter( 'rest_url' , array( $this, 'filter_rest_url' ) );
+
+		$retval = '<link rel="alternate" type="application/json+oembed" href="' . esc_url( get_oembed_endpoint_url( $permalink ) ) . '" />' . "\n";
+
+		if ( class_exists( 'SimpleXMLElement' ) ) {
+			$retval .= '<link rel="alternate" type="text/xml+oembed" href="' . esc_url( get_oembed_endpoint_url( $permalink, 'xml' ) ) . '" />' . "\n";
+		}
+
+		remove_filter( 'rest_url' , array( $this, 'filter_rest_url' ) );
+
+		return $retval;
+	}
+
+	/**
+	 * Fetch our oEmbed response data to return.
+	 *
+	 * A simplified version of {@link get_oembed_response_data()}.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @link http://oembed.com/ View the 'Response parameters' section for more details.
+	 *
+	 * @param array $item  Custom oEmbed response data.
+	 * @param int   $width The requested width.
+	 * @return array
+	 */
+	protected function get_oembed_response_data( $item, $width ) {
+		$data = wp_parse_args( $item, array(
+			'version'       => '1.0',
+			'provider_name' => get_bloginfo( 'name' ),
+			'provider_url'  => get_home_url(),
+			'author_name'   => get_bloginfo( 'name' ),
+			'author_url'    => get_home_url(),
+			'title'         => ucfirst( $this->slug_endpoint ),
+			'type'          => 'rich',
+		) );
+
+		/** This filter is documented in /wp-includes/embed.php */
+		$min_max_width = apply_filters( 'oembed_min_max_width', array(
+			'min' => 200,
+			'max' => 600
+		) );
+
+		$width  = min( max( $min_max_width['min'], $width ), $min_max_width['max'] );
+		$height = max( ceil( $width / 16 * 9 ), 200 );
+
+		$data['width']  = absint( $width );
+		$data['height'] = absint( $height );
+
+		// Set 'html' parameter.
+		if ( 'video' === $data['type'] || 'rich' === $data['type'] ) {
+			// Fake a WP post so we can use get_post_embed_html().
+			$post = new stdClass;
+			$post->post_content = $data['content'];
+			$post->post_title   = $data['title'];
+
+			$data['html'] = get_post_embed_html( $data['width'], $data['height'], $post );
+		}
+
+		// Remove temporary parameters.
+		unset( $data['content'] );
+
+		return $data;
+	}
+
+	/**
+	 * Callback for the API endpoint.
+	 *
+	 * Returns the JSON object for the item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 * @return WP_Error|array oEmbed response data or WP_Error on failure.
+	 */
+	public function get_item( $request ) {
+		$url = $request['url'];
+
+		$data = false;
+
+		$item_id = (int) $this->validate_url_to_item_id( $url );
+
+		if ( ! empty( $item_id ) ) {
+			// Add markers to tell that we're embedding a single activity.
+			// This is needed for various oEmbed response data filtering.
+			if ( empty( buddypress()->{$this->slug_endpoint} ) ) {
+				buddypress()->{$this->slug_endpoint} = new stdClass;
+			}
+			buddypress()->{$this->slug_endpoint}->embedurl_in_progress = $url;
+			buddypress()->{$this->slug_endpoint}->embedid_in_progress  = $item_id;
+
+			// Save custom route args as well.
+			$custom_args = array_keys( (array) $this->set_route_args() );
+			if ( ! empty( $custom_args ) ) {
+				buddypress()->{$this->slug_endpoint}->embedargs_in_progress = array();
+
+				foreach( $custom_args as $arg ) {
+					if ( isset( $request[ $arg ] ) ) {
+						buddypress()->{$this->slug_endpoint}->embedargs_in_progress[ $arg ] = $request[ $arg ];
+					}
+				}
+			}
+
+			// Grab custom oEmbed response data.
+			$item = $this->set_oembed_response_data( $item_id );
+
+			// Set oEmbed response data.
+			$data = $this->get_oembed_response_data( $item, $request['maxwidth'] );
+		}
+
+		if ( ! $data ) {
+			return new WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) );
+		}
+
+		return $data;
+	}
+
+	/**
+	 * If oEmbed request wants XML, return XML instead of JSON.
+	 *
+	 * Basically a copy of {@link _oembed_rest_pre_serve_request()}. Unfortunate
+	 * that we have to duplicate this just for a URL check.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param bool                      $served  Whether the request has already been served.
+	 * @param WP_HTTP_ResponseInterface $result  Result to send to the client. Usually a WP_REST_Response.
+	 * @param WP_REST_Request           $request Request used to generate the response.
+	 * @param WP_REST_Server            $server  Server instance.
+	 * @return bool
+	 */
+	public function oembed_xml_request( $served, $result, $request, $server ) {
+		$params = $request->get_params();
+
+		if ( ! isset( $params['format'] ) || 'xml' !== $params['format'] ) {
+			return $served;
+		}
+
+		// Validate URL against our oEmbed endpoint. If not valid, bail.
+		// This is our mod to _oembed_rest_pre_serve_request().
+		$query_params = $request->get_query_params();
+		if ( false === $this->validate_url_to_item_id( $query_params['url'] ) ) {
+			return $served;
+		}
+
+		// Embed links inside the request.
+		$data = $server->response_to_data( $result, false );
+
+		if ( ! class_exists( 'SimpleXMLElement' ) ) {
+			status_header( 501 );
+			die( get_status_header_desc( 501 ) );
+		}
+
+		$result = _oembed_create_xml( $data );
+
+		// Bail if there's no XML.
+		if ( ! $result ) {
+			status_header( 501 );
+			return get_status_header_desc( 501 );
+		}
+
+		if ( ! headers_sent() ) {
+			$server->send_header( 'Content-Type', 'text/xml; charset=' . get_option( 'blog_charset' ) );
+		}
+
+		echo $result;
+
+		return true;
+	}
+
+	/**
+	 * Pass our BuddyPress activity permalink for embedding.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @see bp_activity_embed_rest_route_callback()
+	 *
+	 * @param string $retval Current embed URL.
+	 * @return string
+	 */
+	public function filter_embed_url( $retval ) {
+		if ( false === isset( buddypress()->{$this->slug_endpoint}->embedurl_in_progress ) && ! $this->is_page() ) {
+			return $retval;
+		}
+
+		$url = $this->is_page() ? $this->set_permalink() : buddypress()->{$this->slug_endpoint}->embedurl_in_progress;
+		$url = trailingslashit( $url );
+
+		// This is for the 'WordPress Embed' block
+		// @see bp_activity_embed_comments_button().
+		if ( 'the_permalink' !== current_filter() ) {
+			$url = add_query_arg( 'embed', 'true', trailingslashit( $url ) );
+
+			// Add custom route args to iframe.
+			if ( ! empty( buddypress()->{$this->slug_endpoint}->embedargs_in_progress ) ) {
+				foreach( buddypress()->{$this->slug_endpoint}->embedargs_in_progress as $key => $value ) {
+					$url = add_query_arg( $key, $value, $url );
+				}
+			}
+		}
+
+		return $url;
+	}
+
+	/**
+	 * Filters the embed HTML for our BP oEmbed endpoint.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $retval Current embed HTML.
+	 * @return string
+	 */
+	public function filter_embed_html( $retval ) {
+		if ( false === isset( buddypress()->{$this->slug_endpoint}->embedurl_in_progress ) && ! $this->is_page() ) {
+			return $retval;
+		}
+
+		$url = $this->set_permalink();
+
+		$item_id = $this->is_page() ? $this->validate_url_to_item_id( $url ) : buddypress()->{$this->slug_endpoint}->embedid_in_progress;
+
+		// Change 'Embedded WordPress Post' to custom title.
+		$custom_title = $this->set_iframe_title( $item_id );
+		if ( ! empty( $custom_title ) ) {
+			$title_pos = strpos( $retval, 'title=' ) + 7;
+			$title_end_pos = strpos( $retval, '"', $title_pos );
+
+			$retval = substr_replace( $retval, esc_attr( $custom_title ), $title_pos, $title_end_pos - $title_pos );
+		}
+
+		// Add 'max-width' CSS attribute to IFRAME.
+		// This will make our oEmbeds responsive.
+		if ( false === strpos( $retval, 'style="max-width' ) ) {
+			$retval = str_replace( '<iframe', '<iframe style="max-width:100%"', $retval );
+		}
+
+		// Remove default <blockquote>.
+		$retval = substr( $retval, strpos( $retval, '</blockquote>' ) + 13 );
+
+		// Set up new fallback HTML
+		// @todo Maybe use KSES?
+		$fallback_html = $this->set_fallback_html( $item_id );
+
+		/**
+		 * Dynamic filter to return BP oEmbed HTML.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @var string $retval
+		 */
+		return apply_filters( "bp_{$this->slug_endpoint}_embed_html", $fallback_html . $retval );
+	}
+
+	/**
+	 * Append our custom slug endpoint to oEmbed endpoint URL.
+	 *
+	 * Meant to be used as a filter on 'rest_url' before any call to
+	 * {@link get_oembed_endpoint_url()} is used.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @see add_oembed_discovery_links()
+	 *
+	 * @param string $retval Current oEmbed endpoint URL.
+	 * @return string
+	 */
+	public function filter_rest_url( $retval = '' ) {
+		return $retval . "/{$this->slug_endpoint}";
+	}
+
+	/**
+	 * Inject content into the embed template.
+	 *
+	 * @since 2.6.0
+	 */
+	public function inject_content() {
+		if ( ! $this->is_page() ) {
+			return;
+		}
+
+		$this->content();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-user.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-user.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core-user.php	(revision 12265)
@@ -0,0 +1,944 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Fetch data about a BuddyPress user.
+ *
+ * BP_Core_User class can be used by any component. It will fetch useful
+ * details for any user when provided with a user_id.
+ *
+ * Example:
+ *    $user = new BP_Core_User( $user_id );
+ *    $user_avatar = $user->avatar;
+ *    $user_email = $user->email;
+ *    $user_status = $user->status;
+ *    etc.
+ */
+class BP_Core_User {
+
+	/**
+	 * ID of the user which the object relates to.
+	 *
+	 * @var integer
+	 */
+	public $id;
+
+	/**
+	 * The URL to the full size of the avatar for the user.
+	 *
+	 * @var string
+	 */
+	public $avatar;
+
+	/**
+	 * The URL to the thumb size of the avatar for the user.
+	 *
+	 * @var string
+	 */
+	public $avatar_thumb;
+
+	/**
+	 * The URL to the mini size of the avatar for the user.
+	 *
+	 * @var string
+	 */
+	public $avatar_mini;
+
+	/**
+	 * The full name of the user.
+	 *
+	 * @var string
+	 */
+	public $fullname;
+
+	/**
+	 * The email for the user.
+	 *
+	 * @var string
+	 */
+	public $email;
+
+	/**
+	 * The absolute url for the user's profile.
+	 *
+	 * @var string
+	 */
+	public $user_url;
+
+	/**
+	 * The HTML for the user link, with the link text being the user's full name.
+	 *
+	 * @var string
+	 */
+	public $user_link;
+
+	/**
+	 * Contains a formatted string when the last time the user was active.
+	 *
+	 * Example: "active 2 hours and 50 minutes ago"
+	 *
+	 * @var string
+	 */
+	public $last_active;
+
+	/* Extras */
+
+	/**
+	 * The total number of "Friends" the user has on site.
+	 *
+	 * @var integer
+	 */
+	public $total_friends;
+
+	/**
+	 * The total number of blog posts posted by the user.
+	 *
+	 * @var integer
+	 * @deprecated No longer used
+	 */
+	public $total_blogs;
+
+	/**
+	 * The total number of groups the user is a part of.
+	 *
+	 * Example: "1 group", "2 groups"
+	 *
+	 * @var string
+	 */
+	public $total_groups;
+
+	/**
+	 * Profile information for the specific user.
+	 *
+	 * @since 1.2.0
+	 * @var array
+	 */
+	public $profile_data;
+
+	/** Public Methods *******************************************************/
+
+	/**
+	 * Class constructor.
+	 *
+	 * @param integer $user_id         The ID for the user being queried.
+	 * @param bool    $populate_extras Whether to fetch extra information such as
+	 *                                 group/friendship counts or not. Default: false.
+	 */
+	public function __construct( $user_id, $populate_extras = false ) {
+		if ( !empty( $user_id ) ) {
+			$this->id = $user_id;
+			$this->populate();
+
+			if ( !empty( $populate_extras ) ) {
+				$this->populate_extras();
+			}
+		}
+	}
+
+	/**
+	 * Populate the instantiated class with data based on the User ID provided.
+	 */
+	public function populate() {
+
+		if ( bp_is_active( 'xprofile' ) )
+			$this->profile_data = $this->get_profile_data();
+
+		if ( !empty( $this->profile_data ) ) {
+			$full_name_field_name = bp_xprofile_fullname_field_name();
+
+			$this->user_url  = bp_core_get_user_domain( $this->id, $this->profile_data['user_nicename'], $this->profile_data['user_login'] );
+			$this->fullname  = esc_attr( $this->profile_data[$full_name_field_name]['field_data'] );
+			$this->user_link = "<a href='{$this->user_url}'>{$this->fullname}</a>";
+			$this->email     = esc_attr( $this->profile_data['user_email'] );
+		} else {
+			$this->user_url  = bp_core_get_user_domain( $this->id );
+			$this->user_link = bp_core_get_userlink( $this->id );
+			$this->fullname  = esc_attr( bp_core_get_user_displayname( $this->id ) );
+			$this->email     = esc_attr( bp_core_get_user_email( $this->id ) );
+		}
+
+		$this->avatar       = bp_core_fetch_avatar( array( 'item_id' => $this->id, 'type' => 'full', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->fullname ) ) );
+		$this->avatar_thumb = bp_core_fetch_avatar( array( 'item_id' => $this->id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->fullname ) ) );
+		$this->avatar_mini  = bp_core_fetch_avatar( array( 'item_id' => $this->id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->fullname ), 'width' => 30, 'height' => 30 ) );
+		$this->last_active  = bp_core_get_last_activity( bp_get_user_last_activity( $this->id ), __( 'active %s', 'buddypress' ) );
+	}
+
+	/**
+	 * Populates extra fields such as group and friendship counts.
+	 */
+	public function populate_extras() {
+
+		if ( bp_is_active( 'friends' ) ) {
+			$this->total_friends = BP_Friends_Friendship::total_friend_count( $this->id );
+		}
+
+		if ( bp_is_active( 'groups' ) ) {
+			$this->total_groups = BP_Groups_Member::total_group_count( $this->id );
+			$this->total_groups = sprintf( _n( '%d group', '%d groups', $this->total_groups, 'buddypress' ), $this->total_groups );
+		}
+	}
+
+	/**
+	 * Fetch xprofile data for the current user.
+	 *
+	 * @see BP_XProfile_ProfileData::get_all_for_user() for description of
+	 *      return value.
+	 *
+	 * @return array See {@link BP_XProfile_Profile_Data::get_all_for_user()}.
+	 */
+	public function get_profile_data() {
+		return BP_XProfile_ProfileData::get_all_for_user( $this->id );
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Get a list of users that match the query parameters.
+	 *
+	 * Since BuddyPress 1.7, use {@link BP_User_Query} instead.
+	 *
+	 * @deprecated 1.7.0 Use {@link BP_User_Query}.
+	 *
+	 * @see BP_User_Query for a description of parameters, most of which
+	 *      are used there in the same way.
+	 *
+	 * @param string      $type            See {@link BP_User_Query}.
+	 * @param int         $limit           See {@link BP_User_Query}. Default: 0.
+	 * @param int         $page            See {@link BP_User_Query}. Default: 1.
+	 * @param int         $user_id         See {@link BP_User_Query}. Default: 0.
+	 * @param mixed       $include         See {@link BP_User_Query}. Default: false.
+	 * @param string|bool $search_terms    See {@link BP_User_Query}.
+	 *                                     Default: false.
+	 * @param bool        $populate_extras See {@link BP_User_Query}.
+	 *                                     Default: true.
+	 * @param mixed       $exclude         See {@link BP_User_Query}. Default: false.
+	 * @param string|bool $meta_key        See {@link BP_User_Query}.
+	 *                                     Default: false.
+	 * @param string|bool $meta_value      See {@link BP_User_Query}.
+	 *                                     Default: false.
+	 * @return false|array {
+	 *     @type int   $total_users Total number of users matched by query
+	 *                              params.
+	 *     @type array $paged_users The current page of users matched by
+	 *                              query params.
+	 * }
+	 */
+	public static function get_users( $type, $limit = 0, $page = 1, $user_id = 0, $include = false, $search_terms = false, $populate_extras = true, $exclude = false, $meta_key = false, $meta_value = false ) {
+		global $wpdb;
+
+		_deprecated_function( __METHOD__, '1.7', 'BP_User_Query' );
+
+		$bp = buddypress();
+
+		$sql = array();
+
+		$sql['select_main'] = "SELECT DISTINCT u.ID as id, u.user_registered, u.user_nicename, u.user_login, u.display_name, u.user_email";
+
+		if ( 'active' == $type || 'online' == $type || 'newest' == $type  ) {
+			$sql['select_active'] = ", um.meta_value as last_activity";
+		}
+
+		if ( 'popular' == $type ) {
+			$sql['select_popular'] = ", um.meta_value as total_friend_count";
+		}
+
+		if ( 'alphabetical' == $type ) {
+			$sql['select_alpha'] = ", pd.value as fullname";
+		}
+
+		if ( $meta_key ) {
+			$sql['select_meta'] = ", umm.meta_key";
+
+			if ( $meta_value ) {
+				$sql['select_meta'] .= ", umm.meta_value";
+			}
+		}
+
+		$sql['from'] = "FROM {$wpdb->users} u LEFT JOIN {$wpdb->usermeta} um ON um.user_id = u.ID";
+
+		// We search against xprofile fields, so we must join the table.
+		if ( $search_terms && bp_is_active( 'xprofile' ) ) {
+			$sql['join_profiledata_search'] = "LEFT JOIN {$bp->profile->table_name_data} spd ON u.ID = spd.user_id";
+		}
+
+		// Alphabetical sorting is done by the xprofile Full Name field.
+		if ( 'alphabetical' == $type ) {
+			$sql['join_profiledata_alpha'] = "LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id";
+		}
+
+		if ( $meta_key ) {
+			$sql['join_meta'] = "LEFT JOIN {$wpdb->usermeta} umm ON umm.user_id = u.ID";
+		}
+
+		$sql['where'] = 'WHERE ' . bp_core_get_status_sql( 'u.' );
+
+		if ( 'active' == $type || 'online' == $type || 'newest' == $type ) {
+			$sql['where_active'] = $wpdb->prepare( "AND um.meta_key = %s", bp_get_user_meta_key( 'last_activity' ) );
+		}
+
+		if ( 'popular' == $type ) {
+			$sql['where_popular'] = $wpdb->prepare( "AND um.meta_key = %s", bp_get_user_meta_key( 'total_friend_count' ) );
+		}
+
+		if ( 'online' == $type ) {
+			$sql['where_online'] = "AND DATE_ADD( um.meta_value, INTERVAL 5 MINUTE ) >= UTC_TIMESTAMP()";
+		}
+
+		if ( 'alphabetical' == $type ) {
+			$sql['where_alpha'] = "AND pd.field_id = 1";
+		}
+
+		if ( !empty( $exclude ) ) {
+			$exclude              = implode( ',', wp_parse_id_list( $exclude ) );
+			$sql['where_exclude'] = "AND u.ID NOT IN ({$exclude})";
+		}
+
+		// Passing an $include value of 0 or '0' will necessarily result in an empty set
+		// returned. The default value of false will hit the 'else' clause.
+		if ( 0 === $include || '0' === $include ) {
+			$sql['where_users'] = "AND 0 = 1";
+		} else {
+			if ( !empty( $include ) ) {
+				$include = implode( ',',  wp_parse_id_list( $include ) );
+				$sql['where_users'] = "AND u.ID IN ({$include})";
+			} elseif ( !empty( $user_id ) && bp_is_active( 'friends' ) ) {
+				$friend_ids = friends_get_friend_user_ids( $user_id );
+
+				if ( !empty( $friend_ids ) ) {
+					$friend_ids = implode( ',', wp_parse_id_list( $friend_ids ) );
+					$sql['where_friends'] = "AND u.ID IN ({$friend_ids})";
+
+				// User has no friends, return false since there will be no users to fetch.
+				} else {
+					return false;
+				}
+			}
+		}
+
+		if ( !empty( $search_terms ) && bp_is_active( 'xprofile' ) ) {
+			$search_terms_like        = '%' . bp_esc_like( $search_terms ) . '%';
+			$sql['where_searchterms'] = $wpdb->prepare( "AND spd.value LIKE %s", $search_terms_like );
+		}
+
+		if ( !empty( $meta_key ) ) {
+			$sql['where_meta'] = $wpdb->prepare( " AND umm.meta_key = %s", $meta_key );
+
+			// If a meta value is provided, match it.
+			if ( $meta_value ) {
+				$sql['where_meta'] .= $wpdb->prepare( " AND umm.meta_value = %s", $meta_value );
+			}
+		}
+
+		switch ( $type ) {
+			case 'active': case 'online': default:
+				$sql[] = "ORDER BY um.meta_value DESC";
+				break;
+			case 'newest':
+				$sql[] = "ORDER BY u.ID DESC";
+				break;
+			case 'alphabetical':
+				$sql[] = "ORDER BY pd.value ASC";
+				break;
+			case 'random':
+				$sql[] = "ORDER BY rand()";
+				break;
+			case 'popular':
+				$sql[] = "ORDER BY CONVERT(um.meta_value, SIGNED) DESC";
+				break;
+		}
+
+		if ( !empty( $limit ) && !empty( $page ) ) {
+			$sql['pagination'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+		}
+
+		/**
+		 * Filters the SQL used to query for paged users.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string $value Concatenated SQL statement for the query.
+		 * @param array  $sql   Array of SQL statement parts for the query.
+		 */
+		$paged_users_sql = apply_filters( 'bp_core_get_paged_users_sql', join( ' ', (array) $sql ), $sql );
+		$paged_users     = $wpdb->get_results( $paged_users_sql );
+
+		// Re-jig the SQL so we can get the total user count.
+		unset( $sql['select_main'] );
+
+		if ( !empty( $sql['select_active'] ) ) {
+			unset( $sql['select_active'] );
+		}
+
+		if ( !empty( $sql['select_popular'] ) ) {
+			unset( $sql['select_popular'] );
+		}
+
+		if ( !empty( $sql['select_alpha'] ) ) {
+			unset( $sql['select_alpha'] );
+		}
+
+		if ( !empty( $sql['pagination'] ) ) {
+			unset( $sql['pagination'] );
+		}
+
+		array_unshift( $sql, "SELECT COUNT(u.ID)" );
+
+		/**
+		 * Filters the SQL used to query for total users.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string $value Concatenated SQL statement for the query.
+		 * @param array  $sql   Array of SQL statement parts for the query.
+		 */
+		$total_users_sql = apply_filters( 'bp_core_get_total_users_sql', join( ' ', (array) $sql ), $sql );
+		$total_users     = $wpdb->get_var( $total_users_sql );
+
+		/**
+		 * Lets fetch some other useful data in a separate queries, this will be faster than querying the data for every user in a list.
+		 * We can't add these to the main query above since only users who have this information will be returned (since the much of the data is in usermeta and won't support any type of directional join).
+		 */
+		if ( !empty( $populate_extras ) ) {
+			$user_ids = array();
+
+			foreach ( (array) $paged_users as $user ) {
+				$user_ids[] = $user->id;
+			}
+
+			// Add additional data to the returned results.
+			$paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids, $type );
+		}
+
+		return array( 'users' => $paged_users, 'total' => $total_users );
+	}
+
+
+	/**
+	 * Fetch the details for all users whose usernames start with the given letter.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param string   $letter          The letter the users names are to start with.
+	 * @param int|null $limit           The number of users we wish to retrive.
+	 * @param int      $page            The page number we are currently on, used in conjunction
+	 *                                  with $limit to get the start position for the limit.
+	 * @param bool     $populate_extras If we should populate extra user fields.
+	 * @param string   $exclude         Comma-separated IDs of users whose results
+	 *                                  aren't to be fetched.
+	 * @return false|array False on error, otherwise associative array of results.
+	 */
+	public static function get_users_by_letter( $letter, $limit = null, $page = 1, $populate_extras = true, $exclude = '' ) {
+		global $wpdb;
+
+		$pag_sql = '';
+		if ( $limit && $page ) {
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+		}
+
+		// Multibyte compliance.
+		if ( function_exists( 'mb_strlen' ) ) {
+			if ( mb_strlen( $letter, 'UTF-8' ) > 1 || is_numeric( $letter ) || !$letter ) {
+				return false;
+			}
+		} else {
+			if ( strlen( $letter ) > 1 || is_numeric( $letter ) || !$letter ) {
+				return false;
+			}
+		}
+
+		$bp = buddypress();
+
+		$letter_like = bp_esc_like( $letter ) . '%';
+		$status_sql  = bp_core_get_status_sql( 'u.' );
+
+		if ( !empty( $exclude ) ) {
+			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
+			$exclude_sql = " AND u.id NOT IN ({$exclude})";
+		} else {
+			$exclude_sql = '';
+		}
+
+		/**
+		 * Filters the SQL used to query for total user count by first letter.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value SQL prepared statement for the user count query.
+		 */
+		$total_users_sql = apply_filters( 'bp_core_users_by_letter_count_sql', $wpdb->prepare( "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id LEFT JOIN {$bp->profile->table_name_fields} pf ON pd.field_id = pf.id WHERE {$status_sql} AND pf.name = %s {$exclude_sql} AND pd.value LIKE %s ORDER BY pd.value ASC", bp_xprofile_fullname_field_name(), $letter_like ) );
+
+		/**
+		 * Filters the SQL used to query for users by first letter.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value SQL prepared statement for the user query.
+		 */
+		$paged_users_sql = apply_filters( 'bp_core_users_by_letter_sql',       $wpdb->prepare( "SELECT DISTINCT u.ID as id, u.user_registered, u.user_nicename, u.user_login, u.user_email FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id LEFT JOIN {$bp->profile->table_name_fields} pf ON pd.field_id = pf.id WHERE {$status_sql} AND pf.name = %s {$exclude_sql} AND pd.value LIKE %s ORDER BY pd.value ASC{$pag_sql}", bp_xprofile_fullname_field_name(), $letter_like ) );
+
+		$total_users = $wpdb->get_var( $total_users_sql );
+		$paged_users = $wpdb->get_results( $paged_users_sql );
+
+		/**
+		 * Lets fetch some other useful data in a separate queries, this will be
+		 * faster than querying the data for every user in a list. We can't add
+		 * these to the main query above since only users who have this
+		 * information will be returned (since the much of the data is in
+		 * usermeta and won't support any type of directional join)
+		 */
+		$user_ids = array();
+		foreach ( (array) $paged_users as $user )
+			$user_ids[] = (int) $user->id;
+
+		// Add additional data to the returned results.
+		if ( $populate_extras ) {
+			$paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids );
+		}
+
+		return array( 'users' => $paged_users, 'total' => $total_users );
+	}
+
+	/**
+	 * Get details of specific users from the database.
+	 *
+	 * Use {@link BP_User_Query} with the 'user_ids' param instead.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param array    $user_ids        The user IDs of the users who we wish to
+	 *                                  fetch information on.
+	 * @param int|null $limit           The limit of results we want.
+	 * @param int      $page            The page we are on for pagination.
+	 * @param bool     $populate_extras If we should populate extra user fields.
+	 * @return array Associative array.
+	 */
+	public static function get_specific_users( $user_ids, $limit = null, $page = 1, $populate_extras = true ) {
+		global $wpdb;
+
+		$pag_sql = '';
+		if ( $limit && $page )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		$user_ids   = implode( ',', wp_parse_id_list( $user_ids ) );
+		$status_sql = bp_core_get_status_sql();
+
+		/**
+		 * Filter the SQL string used for querying specific user count.
+		 *
+		 * This same filter name is used for the paged user SQL, and so should be avoided.
+		 * Use 'bp_core_user_get_specific_users_count_sql' instead.
+		 *
+		 * @deprecated 2.3.0
+		 *
+		 * @param string $sql SQL string.
+		 */
+		$total_users_sql = apply_filters( 'bp_core_get_specific_users_count_sql', "SELECT COUNT(ID) FROM {$wpdb->users} WHERE {$status_sql} AND ID IN ({$user_ids})" );
+
+		/**
+		 * Filter the SQL string used for querying specific user count results.
+		 *
+		 * Use this instead of the deprecated 'bp_core_get_specific_users_count_sql'.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string   $sql             SQL string.
+		 * @param array    $user_ids        Array of IDs of specific users to fetch.
+		 * @param int|null $limit           Max number of records to return. Null for no limit.
+		 * @param int      $page            The page we're on for pagination.
+		 * @param bool     $populate_extras Whether to populate extra user fields.
+		 */
+		$total_users_sql = apply_filters( 'bp_core_user_get_specific_users_count_sql', $total_users_sql, $user_ids, $limit, $page, $populate_extras );
+
+		/**
+		 * Filter the SQL string used for querying specific user paged results.
+		 *
+		 * This same filter name is used for the user count SQL, and so should be avoided.
+		 * Use 'bp_core_user_get_specific_users_paged_sql' instead.
+		 *
+		 * @deprecated 2.3.0
+		 *
+		 * @param string $sql SQL string.
+		 */
+		$paged_users_sql = apply_filters( 'bp_core_get_specific_users_count_sql', "SELECT ID as id, user_registered, user_nicename, user_login, user_email FROM {$wpdb->users} WHERE {$status_sql} AND ID IN ({$user_ids}) {$pag_sql}" );
+
+		/**
+		 * Filter the SQL string used for querying specific user paged results.
+		 *
+		 * Use this instead of the deprecated 'bp_core_get_specific_users_count_sql'.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string   $sql             SQL string.
+		 * @param array    $user_ids        Array of IDs of specific users to fetch.
+		 * @param int|null $limit           Max number of records to return. Null for no limit.
+		 * @param int      $page            The page we're on for pagination.
+		 * @param bool     $populate_extras Whether to populate extra user fields.
+		 */
+		$paged_users_sql = apply_filters( 'bp_core_user_get_specific_users_paged_sql', $paged_users_sql, $user_ids, $limit, $page, $populate_extras );
+
+		$total_users = $wpdb->get_var( $total_users_sql );
+		$paged_users = $wpdb->get_results( $paged_users_sql );
+
+		/**
+		 * Lets fetch some other useful data in a separate queries, this will be
+		 * faster than querying the data for every user in a list. We can't add
+		 * these to the main query above since only users who have this
+		 * information will be returned (since the much of the data is in
+		 * usermeta and won't support any type of directional join)
+		 */
+
+		// Add additional data to the returned results.
+		if ( !empty( $populate_extras ) ) {
+			$paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids );
+		}
+
+		return array( 'users' => $paged_users, 'total' => $total_users );
+	}
+
+	/**
+	 * Find users who match on the value of an xprofile data.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param string   $search_terms    The terms to search the profile table
+	 *                                  value column for.
+	 * @param int|null $limit           The limit of results we want.
+	 * @param int      $page            The page we are on for pagination.
+	 * @param boolean  $populate_extras If we should populate extra user fields.
+	 * @return array Associative array.
+	 */
+	public static function search_users( $search_terms, $limit = null, $page = 1, $populate_extras = true ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$user_ids = array();
+		$pag_sql  = $limit && $page ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * intval( $limit ) ), intval( $limit ) ) : '';
+
+		$search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
+		$status_sql        = bp_core_get_status_sql( 'u.' );
+
+		/**
+		 * Filters the SQL used to query for searched users count.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value SQL statement for the searched users count query.
+		 */
+		$total_users_sql = apply_filters( 'bp_core_search_users_count_sql', $wpdb->prepare( "SELECT COUNT(DISTINCT u.ID) as id FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id WHERE {$status_sql} AND pd.value LIKE %s ORDER BY pd.value ASC", $search_terms_like ), $search_terms );
+
+		/**
+		 * Filters the SQL used to query for searched users.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value SQL statement for the searched users query.
+		 */
+		$paged_users_sql = apply_filters( 'bp_core_search_users_sql',       $wpdb->prepare( "SELECT DISTINCT u.ID as id, u.user_registered, u.user_nicename, u.user_login, u.user_email FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id WHERE {$status_sql} AND pd.value LIKE %s ORDER BY pd.value ASC{$pag_sql}", $search_terms_like ), $search_terms, $pag_sql );
+
+		$total_users = $wpdb->get_var( $total_users_sql );
+		$paged_users = $wpdb->get_results( $paged_users_sql );
+
+		/**
+		 * Lets fetch some other useful data in a separate queries, this will be faster than querying the data for every user in a list.
+		 * We can't add these to the main query above since only users who have this information will be returned (since the much of the data is in usermeta and won't support any type of directional join)
+		 */
+		foreach ( (array) $paged_users as $user )
+			$user_ids[] = $user->id;
+
+		// Add additional data to the returned results.
+		if ( $populate_extras )
+			$paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids );
+
+		return array( 'users' => $paged_users, 'total' => $total_users );
+	}
+
+	/**
+	 * Fetch extra user information, such as friend count and last profile update message.
+	 *
+	 * Accepts multiple user IDs to fetch data for.
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @param array       $paged_users An array of stdClass containing the users.
+	 * @param string      $user_ids    The user ids to select information about.
+	 * @param string|bool $type        The type of fields we wish to get.
+	 * @return mixed False on error, otherwise associative array of results.
+	 */
+	public static function get_user_extras( &$paged_users, &$user_ids, $type = false ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( empty( $user_ids ) )
+			return $paged_users;
+
+		// Sanitize user IDs.
+		$user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
+
+		// Fetch the user's full name.
+		if ( bp_is_active( 'xprofile' ) && 'alphabetical' != $type ) {
+			$names = $wpdb->get_results( $wpdb->prepare( "SELECT pd.user_id as id, pd.value as fullname FROM {$bp->profile->table_name_fields} pf, {$bp->profile->table_name_data} pd WHERE pf.id = pd.field_id AND pf.name = %s AND pd.user_id IN ( {$user_ids} )", bp_xprofile_fullname_field_name() ) );
+			for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
+				foreach ( (array) $names as $name ) {
+					if ( $name->id == $paged_users[$i]->id )
+						$paged_users[$i]->fullname = $name->fullname;
+				}
+			}
+		}
+
+		// Fetch the user's total friend count.
+		if ( 'popular' != $type ) {
+			$friend_count = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as total_friend_count FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'total_friend_count' ) ) );
+			for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
+				foreach ( (array) $friend_count as $fcount ) {
+					if ( $fcount->id == $paged_users[$i]->id )
+						$paged_users[$i]->total_friend_count = (int) $fcount->total_friend_count;
+				}
+			}
+		}
+
+		// Fetch whether or not the user is a friend.
+		if ( bp_is_active( 'friends' ) ) {
+			$friend_status = $wpdb->get_results( $wpdb->prepare( "SELECT initiator_user_id, friend_user_id, is_confirmed FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d AND friend_user_id IN ( {$user_ids} ) ) OR (initiator_user_id IN ( {$user_ids} ) AND friend_user_id = %d )", bp_loggedin_user_id(), bp_loggedin_user_id() ) );
+			for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
+				foreach ( (array) $friend_status as $status ) {
+					if ( $status->initiator_user_id == $paged_users[$i]->id || $status->friend_user_id == $paged_users[$i]->id )
+						$paged_users[$i]->is_friend = $status->is_confirmed;
+				}
+			}
+		}
+
+		// Fetch the user's last_activity.
+		if ( 'active' != $type ) {
+			$user_activity = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as last_activity FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'last_activity' ) ) );
+			for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
+				foreach ( (array) $user_activity as $activity ) {
+					if ( $activity->id == $paged_users[$i]->id )
+						$paged_users[$i]->last_activity = $activity->last_activity;
+				}
+			}
+		}
+
+		// Fetch the user's latest update.
+		$user_update = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as latest_update FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'bp_latest_update' ) ) );
+		for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
+			foreach ( (array) $user_update as $update ) {
+				if ( $update->id == $paged_users[$i]->id )
+					$paged_users[$i]->latest_update = $update->latest_update;
+			}
+		}
+
+		return $paged_users;
+	}
+
+	/**
+	 * Get WordPress user details for a specified user.
+	 *
+	 * @since 3.0.0 Results might be from cache
+	 *
+	 * @param int $user_id User ID.
+	 * @return false|object WP_User if successful, false on failure.
+	 */
+	public static function get_core_userdata( $user_id ) {
+		return WP_User::get_data_by( 'id', $user_id );
+	}
+
+	/**
+	 * Get last activity data for a user or set of users.
+	 *
+	 * @param int|array $user_id User IDs or multiple user IDs.
+	 * @return false|array
+	 */
+	public static function get_last_activity( $user_id ) {
+		global $wpdb;
+
+		// Sanitize and remove empty values.
+		$user_ids = array_filter( wp_parse_id_list( $user_id ) );
+
+		if ( empty( $user_ids ) ) {
+			return false;
+		}
+
+		$uncached_user_ids = bp_get_non_cached_ids( $user_ids, 'bp_last_activity' );
+		if ( ! empty( $uncached_user_ids ) ) {
+			$bp = buddypress();
+
+			$user_ids_sql = implode( ',', $uncached_user_ids );
+			$user_count   = count( $uncached_user_ids );
+
+			$last_activities = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, date_recorded FROM {$bp->members->table_name_last_activity} WHERE component = %s AND type = 'last_activity' AND user_id IN ({$user_ids_sql}) LIMIT {$user_count}", $bp->members->id ) );
+
+			foreach ( $last_activities as $last_activity ) {
+				wp_cache_set( $last_activity->user_id, array(
+					'user_id'       => $last_activity->user_id,
+					'date_recorded' => $last_activity->date_recorded,
+					'activity_id'   => $last_activity->id,
+				), 'bp_last_activity' );
+			}
+		}
+
+		// Fetch all user data from the cache.
+		$retval = array();
+		foreach ( $user_ids as $user_id ) {
+			$retval[ $user_id ] = wp_cache_get( $user_id, 'bp_last_activity' );
+
+			if ( isset( $retval['user_id'] ) ) {
+				$retval[ $user_id ]['user_id']     = (int) $retval[ $user_id ]['user_id'];
+			}
+			if ( isset( $retval['activity_id'] ) ) {
+				$retval[ $user_id ]['activity_id'] = (int) $retval[ $user_id ]['activity_id'];
+			}
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Set a user's last_activity value.
+	 *
+	 * Will create a new entry if it does not exist. Otherwise updates the
+	 * existing entry.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int    $user_id ID of the user whose last_activity you are updating.
+	 * @param string $time    MySQL-formatted time string.
+	 * @return bool True on success, false on failure.
+	 */
+	public static function update_last_activity( $user_id, $time ) {
+		global $wpdb;
+
+		$table_name = buddypress()->members->table_name_last_activity;
+
+		$activity = self::get_last_activity( $user_id );
+
+		if ( ! empty( $activity[ $user_id ] ) ) {
+			$updated = $wpdb->update(
+				$table_name,
+
+				// Data to update.
+				array(
+					'date_recorded' => $time,
+				),
+
+				// WHERE.
+				array(
+					'id' => $activity[ $user_id ]['activity_id'],
+				),
+
+				// Data sanitization format.
+				array(
+					'%s',
+				),
+
+				// WHERE sanitization format.
+				array(
+					'%d',
+				)
+			);
+
+			// Add new date to existing activity entry for caching.
+			$activity[ $user_id ]['date_recorded'] = $time;
+
+		} else {
+			$updated = $wpdb->insert(
+				$table_name,
+
+				// Data.
+				array(
+					'user_id'       => $user_id,
+					'component'     => buddypress()->members->id,
+					'type'          => 'last_activity',
+					'action'        => '',
+					'content'       => '',
+					'primary_link'  => '',
+					'item_id'       => 0,
+					'date_recorded' => $time,
+				),
+
+				// Data sanitization format.
+				array(
+					'%d',
+					'%s',
+					'%s',
+					'%s',
+					'%s',
+					'%s',
+					'%d',
+					'%s',
+				)
+			);
+
+			// Set up activity array for caching.
+			// View the foreach loop in the get_last_activity() method for format.
+			$activity = array();
+			$activity[ $user_id ] = array(
+				'user_id'       => $user_id,
+				'date_recorded' => $time,
+				'activity_id'   => $wpdb->insert_id,
+			);
+		}
+
+		// Set cache.
+		wp_cache_set( $user_id, $activity[ $user_id ], 'bp_last_activity' );
+
+		/**
+		 * Fires when a user's last_activity value has been updated.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param int    $user_id ID of the user.
+		 * @param string $time    Last activity timestamp, in 'Y-m-d H:i:s' format.
+		 */
+		do_action( 'bp_core_user_updated_last_activity', $user_id, $time );
+
+		return $updated;
+	}
+
+	/**
+	 * Delete a user's last_activity value.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int $user_id ID of the user whose activity should be deleted.
+	 * @return bool True on success, false on failure or if no last_activity
+	 *              is found for the user.
+	 */
+	public static function delete_last_activity( $user_id ) {
+		global $wpdb;
+
+		$existing = self::get_last_activity( $user_id );
+
+		if ( empty( $existing ) || empty( $existing[ $user_id ]['activity_id'] ) ) {
+			return false;
+		}
+
+		$deleted = $wpdb->delete(
+			buddypress()->members->table_name_last_activity,
+
+			// WHERE.
+			array(
+				'id' => $existing[ $user_id ]['activity_id'],
+			),
+
+			// WHERE sanitization format.
+			array(
+				'%s',
+			)
+		);
+
+		wp_cache_delete( $user_id, 'bp_last_activity' );
+
+		return $deleted;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-core.php	(revision 12265)
@@ -0,0 +1,324 @@
+<?php
+/**
+ * BuddyPress Core Loader.
+ *
+ * Core contains the commonly used functions, classes, and APIs.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Creates the Core component.
+ *
+ * @since 1.5.0
+ */
+class BP_Core extends BP_Component {
+
+	/**
+	 * Start the members component creation process.
+	 *
+	 * @since 1.5.0
+	 *
+	 */
+	public function __construct() {
+		parent::start(
+			'core',
+			__( 'BuddyPress Core', 'buddypress' ),
+			buddypress()->plugin_dir
+		);
+
+		$this->bootstrap();
+	}
+
+	/**
+	 * Populate the global data needed before BuddyPress can continue.
+	 *
+	 * This involves figuring out the currently required, activated, deactivated,
+	 * and optional components.
+	 *
+	 * @since 1.5.0
+	 */
+	private function bootstrap() {
+		$bp = buddypress();
+
+		/**
+		 * Fires before the loading of individual components and after BuddyPress Core.
+		 *
+		 * Allows plugins to run code ahead of the other components.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_core_loaded' );
+
+		/** Components *******************************************************
+		 */
+
+		/**
+		 * Filters the included and optional components.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param array $value Array of included and optional components.
+		 */
+		$bp->optional_components = apply_filters( 'bp_optional_components', array( 'activity', 'blogs', 'friends', 'groups', 'messages', 'notifications', 'settings', 'xprofile' ) );
+
+		/**
+		 * Filters the required components.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param array $value Array of required components.
+		 */
+		$bp->required_components = apply_filters( 'bp_required_components', array( 'members' ) );
+
+		// Get a list of activated components.
+		if ( $active_components = bp_get_option( 'bp-active-components' ) ) {
+
+			/** This filter is documented in bp-core/admin/bp-core-admin-components.php */
+			$bp->active_components      = apply_filters( 'bp_active_components', $active_components );
+
+			/**
+			 * Filters the deactivated components.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param array $value Array of deactivated components.
+			 */
+			$bp->deactivated_components = apply_filters( 'bp_deactivated_components', array_values( array_diff( array_values( array_merge( $bp->optional_components, $bp->required_components ) ), array_keys( $bp->active_components ) ) ) );
+
+		// Pre 1.5 Backwards compatibility.
+		} elseif ( $deactivated_components = bp_get_option( 'bp-deactivated-components' ) ) {
+
+			// Trim off namespace and filename.
+			foreach ( array_keys( (array) $deactivated_components ) as $component ) {
+				$trimmed[] = str_replace( '.php', '', str_replace( 'bp-', '', $component ) );
+			}
+
+			/** This filter is documented in bp-core/bp-core-loader.php */
+			$bp->deactivated_components = apply_filters( 'bp_deactivated_components', $trimmed );
+
+			// Setup the active components.
+			$active_components     = array_fill_keys( array_diff( array_values( array_merge( $bp->optional_components, $bp->required_components ) ), array_values( $bp->deactivated_components ) ), '1' );
+
+			/** This filter is documented in bp-core/admin/bp-core-admin-components.php */
+			$bp->active_components = apply_filters( 'bp_active_components', $bp->active_components );
+
+		// Default to all components active.
+		} else {
+
+			// Set globals.
+			$bp->deactivated_components = array();
+
+			// Setup the active components.
+			$active_components     = array_fill_keys( array_values( array_merge( $bp->optional_components, $bp->required_components ) ), '1' );
+
+			/** This filter is documented in bp-core/admin/bp-core-admin-components.php */
+			$bp->active_components = apply_filters( 'bp_active_components', $bp->active_components );
+		}
+
+		// Loop through optional components.
+		foreach( $bp->optional_components as $component ) {
+			if ( bp_is_active( $component ) && file_exists( $bp->plugin_dir . '/bp-' . $component . '/bp-' . $component . '-loader.php' ) ) {
+				include( $bp->plugin_dir . '/bp-' . $component . '/bp-' . $component . '-loader.php' );
+			}
+		}
+
+		// Loop through required components.
+		foreach( $bp->required_components as $component ) {
+			if ( file_exists( $bp->plugin_dir . '/bp-' . $component . '/bp-' . $component . '-loader.php' ) ) {
+				include( $bp->plugin_dir . '/bp-' . $component . '/bp-' . $component . '-loader.php' );
+			}
+		}
+
+		// Add Core to required components.
+		$bp->required_components[] = 'core';
+
+		/**
+		 * Fires after the loading of individual components.
+		 *
+		 * @since 2.0.0
+		 */
+		do_action( 'bp_core_components_included' );
+	}
+
+	/**
+	 * Include bp-core files.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @see BP_Component::includes() for description of parameters.
+	 *
+	 * @param array $includes See {@link BP_Component::includes()}.
+	 */
+	public function includes( $includes = array() ) {
+
+		if ( ! is_admin() ) {
+			return;
+		}
+
+		$includes = array(
+			'admin'
+		);
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Set up bp-core global settings.
+	 *
+	 * Sets up a majority of the BuddyPress globals that require a minimal
+	 * amount of processing, meaning they cannot be set in the BuddyPress class.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_globals() for description of parameters.
+	 *
+	 * @param array $args See {@link BP_Component::setup_globals()}.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		/** Database *********************************************************
+		 */
+
+		// Get the base database prefix.
+		if ( empty( $bp->table_prefix ) ) {
+			$bp->table_prefix = bp_core_get_table_prefix();
+		}
+
+		// The domain for the root of the site where the main blog resides.
+		if ( empty( $bp->root_domain ) ) {
+			$bp->root_domain = bp_core_get_root_domain();
+		}
+
+		// Fetches all of the core BuddyPress settings in one fell swoop.
+		if ( empty( $bp->site_options ) ) {
+			$bp->site_options = bp_core_get_root_options();
+		}
+
+		// The names of the core WordPress pages used to display BuddyPress content.
+		if ( empty( $bp->pages ) ) {
+			$bp->pages = bp_core_get_directory_pages();
+		}
+
+		/** Basic current user data ******************************************
+		 */
+
+		// Logged in user is the 'current_user'.
+		$current_user            = wp_get_current_user();
+
+		// The user ID of the user who is currently logged in.
+		$bp->loggedin_user       = new stdClass;
+		$bp->loggedin_user->id   = isset( $current_user->ID ) ? $current_user->ID : 0;
+
+		/** Avatars **********************************************************
+		 */
+
+		// Fetches the default Gravatar image to use if the user/group/blog has no avatar or gravatar.
+		$bp->grav_default        = new stdClass;
+
+		/**
+		 * Filters the default user Gravatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Default user Gravatar.
+		 */
+		$bp->grav_default->user  = apply_filters( 'bp_user_gravatar_default',  $bp->site_options['avatar_default'] );
+
+		/**
+		 * Filters the default group Gravatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Default group Gravatar.
+		 */
+		$bp->grav_default->group = apply_filters( 'bp_group_gravatar_default', $bp->grav_default->user );
+
+		/**
+		 * Filters the default blog Gravatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Default blog Gravatar.
+		 */
+		$bp->grav_default->blog  = apply_filters( 'bp_blog_gravatar_default',  $bp->grav_default->user );
+
+		// Notifications table. Included here for legacy purposes. Use
+		// bp-notifications instead.
+		$bp->core->table_name_notifications = $bp->table_prefix . 'bp_notifications';
+
+		// Backward compatibility for plugins modifying the legacy bp_nav and bp_options_nav global properties.
+		$bp->bp_nav         = new BP_Core_BP_Nav_BackCompat();
+		$bp->bp_options_nav = new BP_Core_BP_Options_Nav_BackCompat();
+
+		/**
+		 * Used to determine if user has admin rights on current content. If the
+		 * logged in user is viewing their own profile and wants to delete
+		 * something, is_item_admin is used. This is a generic variable so it
+		 * can be used by other components. It can also be modified, so when
+		 * viewing a group 'is_item_admin' would be 'true' if they are a group
+		 * admin, and 'false' if they are not.
+		 */
+		bp_update_is_item_admin( bp_user_has_access(), 'core' );
+
+		// Is the logged in user is a mod for the current item?
+		bp_update_is_item_mod( false,                  'core' );
+
+		/**
+		 * Fires at the end of the setup of bp-core globals setting.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_core_setup_globals' );
+	}
+
+	/**
+	 * Setup cache groups
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp'
+		) );
+
+		parent::setup_cache_groups();
+	}
+
+	/**
+	 * Set up post types.
+	 *
+	 * @since BuddyPress (2.4.0)
+	 */
+	public function register_post_types() {
+
+		// Emails
+		if ( bp_is_root_blog() && ! is_network_admin() ) {
+			register_post_type(
+				bp_get_email_post_type(),
+				apply_filters( 'bp_register_email_post_type', array(
+					'description'       => _x( 'BuddyPress emails', 'email post type description', 'buddypress' ),
+					'labels'            => bp_get_email_post_type_labels(),
+					'menu_icon'         => 'dashicons-email',
+					'public'            => false,
+					'publicly_queryable' => bp_current_user_can( 'bp_moderate' ),
+					'query_var'         => false,
+					'rewrite'           => false,
+					'show_in_admin_bar' => false,
+					'show_ui'           => bp_current_user_can( 'bp_moderate' ),
+					'supports'          => bp_get_email_post_type_supports(),
+				) )
+			);
+		}
+
+		parent::register_post_types();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-customizer-control-range.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-customizer-control-range.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-customizer-control-range.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Core component class.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Customizer font size control.
+ *
+ * @since 2.5.0
+ */
+class BP_Customizer_Control_Range extends WP_Customize_Control {
+	/**
+	 * @var string
+	 */
+	public $type = 'range';
+
+	/**
+	 * Enqueue scripts/styles for the color picker.
+	 *
+	 * @since 2.5.0
+	 */
+	public function enqueue() {
+		wp_enqueue_script( 'bp-customizer-controls' );
+		wp_enqueue_style( 'bp-customizer-controls' );
+	}
+
+	/**
+	 * Render the control.
+	 *
+	 * @since 2.5.0
+	 */
+	public function render_content() {
+		$id    = 'customize-control-' . str_replace( '[', '-', str_replace( ']', '', $this->id ) );
+		$class = 'customize-control customize-control-' . $this->type;
+
+		?><li id="<?php echo esc_attr( $id ); ?>" class="<?php echo esc_attr( $class ); ?>">
+			<?php if ( $this->label ) : ?>
+				<label for="<?php echo esc_attr( "{$id}-range" ); ?>">
+					<span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
+				</label>
+			<?php endif; ?>
+
+			<input type="range" id="<?php echo esc_attr( "{$id}-range" ); ?>" <?php $this->link(); $this->input_attrs(); ?> value="<?php echo esc_attr( $this->value() ); ?>" />
+			<output for="<?php echo esc_attr( "{$id}-range" ); ?>"><?php echo esc_html( $this->value() ); ?></output>
+
+			<?php if ( $this->description ) : ?>
+				<p><span class="description customize-control-description"><?php echo esc_html( $this->description ); ?></span></p>
+			<?php endif; ?>
+		</li><?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-date-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-date-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-date-query.php	(revision 12265)
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( class_exists( 'WP_Date_Query' ) ) :
+
+/**
+ * BuddyPress date query class.
+ *
+ * Extends the {@link WP_Date_Query} class for use with BuddyPress.
+ *
+ * @since 2.1.0
+ *
+ * @param array $date_query {
+ *     Date query arguments.  See first parameter of {@link WP_Date_Query::__construct()}.
+ * }
+ * @param string $column The DB column to query against.
+ */
+class BP_Date_Query extends WP_Date_Query {
+	/**
+	 * The column to query against. Can be changed via the query arguments.
+	 *
+	 * @var string
+	 */
+	public $column;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param array  $date_query Date query arguments.
+	 * @param string $column     THe DB column to query against.
+	 *
+	 * @see WP_Date_Query::__construct()
+	 */
+	public function __construct( $date_query, $column = '' ) {
+		if ( ! empty( $column ) ) {
+			$this->column = $column;
+			add_filter( 'date_query_valid_columns', array( $this, 'register_date_column' ) );
+		}
+
+		parent::__construct( $date_query, $column );
+	}
+
+	/**
+	 * Destructor.
+	 */
+	public function __destruct() {
+		remove_filter( 'date_query_valid_columns', array( $this, 'register_date_column' ) );
+	}
+
+	/**
+	 * Registers our date column with WP Date Query to pass validation.
+	 *
+	 * @param array $retval Current DB columns.
+	 * @return array
+	 */
+	public function register_date_column( $retval = array() ) {
+		$retval[] = $this->column;
+		return $retval;
+	}
+}
+endif;
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email-delivery.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email-delivery.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email-delivery.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Email delivery implementation base class.
+ *
+ * When implementing support for an email delivery service into BuddyPress,
+ * you are required to create a class that implements this interface.
+ *
+ * @since 2.5.0
+ */
+interface BP_Email_Delivery {
+
+	/**
+	 * Send email(s).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param BP_Email $email Email to send.
+	 * @return bool|WP_Error Returns true if email send, else a descriptive WP_Error.
+	 */
+	public function bp_email( BP_Email $email );
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email-recipient.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email-recipient.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email-recipient.php	(revision 12265)
@@ -0,0 +1,195 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Represents a recipient that an email will be sent to.
+ *
+ * @since 2.5.0
+ */
+class BP_Email_Recipient {
+
+	/**
+	 * Recipient's email address.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $address = '';
+
+	/**
+	 * Recipient's name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $name = '';
+
+	/**
+	 * Optional. A `WP_User` object relating to this recipient.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var WP_User
+	 */
+	protected $user_object = null;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $email_or_user Either a email address, user ID, WP_User object,
+	 *                                                or an array containing any combination of the above.
+	 * @param string $name Optional. If $email_or_user is a string, this is the recipient's name.
+	 */
+	public function __construct( $email_or_user, $name = '' ) {
+		$name = sanitize_text_field( $name );
+
+		// User ID, email address or WP_User object.
+		if ( is_int( $email_or_user ) || ( is_string( $email_or_user ) && is_email( $email_or_user ) ) || is_object( $email_or_user ) ) {
+			// We already have a WP user.
+			if ( is_object( $email_or_user ) ) {
+				$this->user_object = $email_or_user;
+
+			// Query for WP user by user ID.
+			} elseif ( is_int( $email_or_user ) ) {
+				$this->user_object = get_user_by( 'id', $email_or_user );
+			}
+
+			// Set email address.
+			if ( empty( $this->user_object ) && is_email( $email_or_user ) ) {
+				$address = $email_or_user;
+			}
+
+		// Array or miscellaneous string.
+		} else {
+			if ( ! is_array( $email_or_user ) ) {
+				$email_or_user = array( $email_or_user => $name );
+			}
+
+			// Handle numeric arrays.
+			if ( is_int( key( $email_or_user ) ) ) {
+				$address = current( $email_or_user );
+			} else {
+				$address = key( $email_or_user );
+				$name    = current( $email_or_user );
+			}
+		}
+
+		// Set address if we have one.
+		if ( ! empty( $address ) ) {
+			$this->address = sanitize_email( $address );
+		}
+
+		// Still no user object; try to query user by email address.
+		if ( empty( $this->user_object ) ) {
+			$this->get_user( 'search-email' );
+		}
+
+		// We have a user object; so set address and name from DB.
+		if ( $this->user_object ) {
+			// This is escaped with esc_html in bp_core_get_user_displayname()
+			$wp_name = wp_specialchars_decode( bp_core_get_user_displayname( $this->user_object->ID ), ENT_QUOTES );
+
+			$this->address = $this->user_object->user_email;
+			$this->name    = sanitize_text_field( $wp_name );
+
+		}
+
+		// Custom name override.
+		if ( $name ) {
+			$this->name = $name;
+		}
+
+		/**
+		 * Fires inside __construct() method for BP_Email_Recipient class.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string|array|int|WP_User $email_or_user Either a email address, user ID, WP_User object,
+		 *                                                or an array containing any combination of the above.
+		 * @param string $name If $email_or_user is a string, this is the recipient's name.
+		 * @param BP_Email_Recipient $this Current instance of the email type class.
+		 */
+		do_action( 'bp_email_recipient', $email_or_user, $name, $this );
+	}
+
+	/**
+	 * Get recipient's address.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	public function get_address() {
+
+		/**
+		 * Filters the recipient's address before it's returned.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $address Recipient's address.
+		 * @param BP_Email $recipient $this Current instance of the email recipient class.
+		 */
+		return apply_filters( 'bp_email_recipient_get_address', $this->address, $this );
+	}
+
+	/**
+	 * Get recipient's name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	public function get_name() {
+
+		/**
+		 * Filters the recipient's name before it's returned.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $name Recipient's name.
+		 * @param BP_Email $recipient $this Current instance of the email recipient class.
+		 */
+		return apply_filters( 'bp_email_recipient_get_name', $this->name, $this );
+	}
+
+	/**
+	 * Get WP_User object for this recipient.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'search-email'.
+	 * @return WP_User|null WP_User object, or null if not set.
+	 */
+	public function get_user( $transform = 'raw' ) {
+
+		// If transform "search-email", find the WP_User if not already set.
+		if ( $transform === 'search-email' && ! $this->user_object && $this->address ) {
+			$this->user_object = get_user_by( 'email', $this->address );
+		}
+
+		/**
+		 * Filters the WP_User object for this recipient before it's returned.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param WP_User $name WP_User object for this recipient, or null if not set.
+		 * @param string $transform Optional. How the return value was transformed.
+		 *                          Accepts 'raw' (default) or 'search-email'.
+		 * @param BP_Email $recipient $this Current instance of the email recipient class.
+		 */
+		return apply_filters( 'bp_email_recipient_get_user', $this->user_object, $transform, $this );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-email.php	(revision 12265)
@@ -0,0 +1,963 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Represents an email that will be sent to member(s).
+ *
+ * @since 2.5.0
+ */
+class BP_Email {
+	/**
+	 * Addressee details (BCC).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient[] BCC recipients.
+	 */
+	protected $bcc = array();
+
+	/**
+	 * Addressee details (CC).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient[] CC recipients.
+	 */
+	protected $cc = array();
+
+	/**
+	 * Email content (HTML).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $content_html = '';
+
+	/**
+	 * Email content (plain text).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $content_plaintext = '';
+
+	/**
+	 * The content type to send the email in ("html" or "plaintext").
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $content_type = 'html';
+
+	/**
+	 * Sender details.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient Sender details.
+	 */
+	protected $from = null;
+
+	/**
+	 * Email headers.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string[] Associative pairing of email header name/value.
+	 */
+	protected $headers = array();
+
+	/**
+	 * The Post object (the source of the email's content and subject).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var WP_Post
+	 */
+	protected $post_object = null;
+
+	/**
+	 * Reply To details.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient "Reply to" details.
+	 */
+	protected $reply_to = null;
+
+	/**
+	 * Email subject.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $subject = '';
+
+	/**
+	 * Email template (the HTML wrapper around the email content).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $template = '{{{content}}}';
+
+	/**
+	 * Addressee details (to).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient[] Email recipients.
+	 * }
+	 */
+	protected $to = array();
+
+	/**
+	 * Unique identifier for this particular type of email.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $type = '';
+
+	/**
+	 * Token names and replacement values for this email.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string[] Associative pairing of token name (key) and replacement value (value).
+	 */
+	protected $tokens = array();
+
+	/**
+	 * Constructor.
+	 *
+	 * Set the email type and default "from" and "reply to" name and address.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $email_type Unique identifier for a particular type of email.
+	 */
+	public function __construct( $email_type ) {
+		$this->type = $email_type;
+
+		// SERVER_NAME isn't always set (e.g CLI).
+		if ( ! empty( $_SERVER['SERVER_NAME'] ) ) {
+			$domain = strtolower( $_SERVER['SERVER_NAME'] );
+			if ( substr( $domain, 0, 4 ) === 'www.' ) {
+				$domain = substr( $domain, 4 );
+			}
+
+		} elseif ( function_exists( 'gethostname' ) && gethostname() !== false ) {
+			$domain = gethostname();
+
+		} elseif ( php_uname( 'n' ) !== false ) {
+			$domain = php_uname( 'n' );
+
+		} else {
+			$domain = 'localhost.localdomain';
+		}
+
+		// This was escaped with esc_html on the way into the database in sanitize_option().
+		$from_name    = wp_specialchars_decode( bp_get_option( 'blogname' ), ENT_QUOTES );
+		$from_address = "wordpress@$domain";
+
+		/** This filter is documented in wp-includes/pluggable.php */
+		$from_address = apply_filters( 'wp_mail_from', $from_address );
+
+		/** This filter is documented in wp-includes/pluggable.php */
+		$from_name = apply_filters( 'wp_mail_from_name', $from_name );
+
+		$this->set_from( $from_address, $from_name );
+		$this->set_reply_to( bp_get_option( 'admin_email' ), $from_name );
+
+		/**
+		 * Fires inside __construct() method for BP_Email class.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $email_type Unique identifier for this type of email.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		do_action( 'bp_email', $email_type, $this );
+	}
+
+
+	/*
+	 * Setters/getters.
+	 */
+
+	/**
+	 * Getter function to expose object properties.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $property_name Property to access.
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return mixed Returns null if property does not exist, otherwise the value.
+	 */
+	public function get( $property_name, $transform = 'raw' ) {
+
+		// "content" is replaced by HTML or plain text depending on $content_type.
+		if ( $property_name === 'content' ) {
+			$property_name = 'content_' . $this->get_content_type();
+
+			if ( ! in_array( $property_name, array( 'content_html', 'content_plaintext', ), true ) ) {
+				$property_name = 'content_html';
+			}
+		}
+
+		if ( ! property_exists( $this, $property_name ) ) {
+			return null;
+		}
+
+
+		/**
+		 * Filters the value of the specified email property before transformation.
+		 *
+		 * This is a dynamic filter dependent on the specified key.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param mixed $property_value Property value.
+		 * @param string $property_name
+		 * @param string $transform How to transform the return value.
+		 *                          Accepts 'raw' (default) or 'replace-tokens'.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$retval = apply_filters( "bp_email_get_{$property_name}", $this->$property_name, $property_name, $transform, $this );
+
+		switch ( $transform ) {
+			// Special-case to fill the $template with the email $content.
+			case 'add-content':
+				$retval = str_replace( '{{{content}}}', wpautop( $this->get_content( 'replace-tokens' ) ), $retval );
+				// Fall through.
+
+			case 'replace-tokens':
+				$retval = bp_core_replace_tokens_in_text( $retval, $this->get_tokens( 'raw' ) );
+				// Fall through.
+
+			case 'raw':
+			default:
+				// Do nothing.
+		}
+
+		/**
+		 * Filters the value of the specified email $property after transformation.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $retval Property value.
+		 * @param string $property_name
+		 * @param string $transform How to transform the return value.
+		 *                          Accepts 'raw' (default) or 'replace-tokens'.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		return apply_filters( 'bp_email_get_property', $retval, $property_name, $transform, $this );
+	}
+
+	/**
+	 * Get email headers.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string[] Associative pairing of email header name/value.
+	 */
+	public function get_headers( $transform = 'raw' ) {
+		return $this->get( 'headers', $transform );
+	}
+
+	/**
+	 * Get the email's "bcc" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient[] BCC recipients.
+	 */
+	public function get_bcc( $transform = 'raw' ) {
+		return $this->get( 'bcc', $transform );
+	}
+
+	/**
+	 * Get the email's "cc" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient[] CC recipients.
+	 */
+	public function get_cc( $transform = 'raw' ) {
+		return $this->get( 'cc', $transform );
+	}
+
+	/**
+	 * Get the email content.
+	 *
+	 * HTML or plaintext is returned, depending on the email's $content_type.
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string HTML or plaintext, depending on $content_type.
+	 */
+	public function get_content( $transform = 'raw' ) {
+		return $this->get( 'content', $transform );
+	}
+
+	/**
+	 * Get the email content (in HTML).
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string HTML email content.
+	 */
+	public function get_content_html( $transform = 'raw' ) {
+		return $this->get( 'content_html', $transform );
+	}
+
+	/**
+	 * Get the email content (in plaintext).
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Plain text email content.
+	 */
+	public function get_content_plaintext( $transform = 'raw' ) {
+		return $this->get( 'content_plaintext', $transform );
+	}
+
+	/**
+	 * Get the email content type (HTML or plain text) that the email will be sent in.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Email content type ("html" or "plaintext").
+	 */
+	public function get_content_type( $transform = 'raw' ) {
+		return $this->get( 'content_type', $transform );
+	}
+
+	/**
+	 * Get the email's "from" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient "From" recipient.
+	 */
+	public function get_from( $transform = 'raw' ) {
+		return $this->get( 'from', $transform );
+	}
+
+	/**
+	 * Get the Post associated with the email.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return WP_Post The post.
+	 */
+	public function get_post_object( $transform = 'raw' ) {
+		return $this->get( 'post_object', $transform );
+	}
+
+	/**
+	 * Get the email's "reply to" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient "Reply to" recipient.
+	 */
+	public function get_reply_to( $transform = 'raw' ) {
+		return $this->get( 'reply_to', $transform );
+	}
+
+	/**
+	 * Get the email subject.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Email subject.
+	 */
+	public function get_subject( $transform = 'raw' ) {
+		return $this->get( 'subject', $transform );
+	}
+
+	/**
+	 * Get the email template (the HTML wrapper around the email content).
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Email template. Assumed to be HTML.
+	 */
+	public function get_template( $transform = 'raw' ) {
+		return $this->get( 'template', $transform );
+	}
+
+	/**
+	 * Get the email's "to" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient[] "To" recipients.
+	 */
+	public function get_to( $transform = 'raw' ) {
+		return $this->get( 'to', $transform );
+	}
+
+	/**
+	 * Get token names and replacement values for this email.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string[] Associative pairing of token name (key) and replacement value (value).
+	 */
+	public function get_tokens( $transform = 'raw' ) {
+		return $this->get( 'tokens', $transform );
+	}
+
+	/**
+	 * Set email headers.
+	 *
+	 * Does NOT let you override to/from, etc. Use the methods provided to set those.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string[] $headers Key/value pairs of header name/values (strings).
+	 * @return BP_Email
+	 */
+	public function set_headers( array $headers ) {
+		$new_headers = array();
+
+		foreach ( $headers as $name => $content ) {
+			$content = str_replace( ':', '', $content );
+			$name    = str_replace( ':', '', $name );
+
+			$new_headers[ sanitize_key( $name ) ] = sanitize_text_field( $content );
+		}
+
+		/**
+		 * Filters the new value of the email's "headers" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string[] $new_headers Key/value pairs of new header name/values (strings).
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->headers = apply_filters( 'bp_email_set_headers', $new_headers, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "bcc" address and name.
+	 *
+	 * To set a single address, the first parameter is the address and the second the name.
+	 * You can also pass a user ID or a WP_User object.
+	 *
+	 * To set multiple addresses, for each array item, the key is the email address and
+	 * the value is the name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $bcc_address Either a email address, user ID, WP_User object,
+	 *                                              or an array containing any combination of the above.
+	 * @param string $name Optional. If $bcc_address is a string, this is the recipient's name.
+	 * @param string $operation Optional. If "replace", $to_address replaces current setting (default).
+	 *                          If "add", $to_address is added to the current setting.
+	 * @return BP_Email
+	 */
+	public function set_bcc( $bcc_address, $name = '', $operation = 'replace' ) {
+		$bcc = ( $operation !== 'replace' ) ? $this->bcc : array();
+
+		if ( is_array( $bcc_address ) ) {
+			foreach ( $bcc_address as $address ) {
+				$bcc[] = new BP_Email_Recipient( $address );
+			}
+
+		} else {
+			$bcc[] = new BP_Email_Recipient( $bcc_address, $name );
+		}
+
+		/**
+		 * Filters the new value of the email's "BCC" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient[] $bcc BCC recipients.
+		 * @param string|array|int|WP_User $bcc_address Either a email address, user ID, WP_User object,
+		 *                                              or an array containing any combination of the above.
+		 * @param string $name Optional. If $bcc_address is a string, this is the recipient's name.
+		 * @param string $operation If "replace", $to_address replaced previous recipients. If "add",
+		 *                          $to_address was added to the array of recipients.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->bcc = apply_filters( 'bp_email_set_bcc', $bcc, $bcc_address, $name, $operation, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "cc" address and name.
+	 *
+	 * To set a single address, the first parameter is the address and the second the name.
+	 * You can also pass a user ID or a WP_User object.
+	 *
+	 * To set multiple addresses, for each array item, the key is the email address and
+	 * the value is the name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $cc_address Either a email address, user ID, WP_User object,
+	 *                                             or an array containing any combination of the above.
+	 * @param string $name Optional. If $cc_address is a string, this is the recipient's name.
+	 * @param string $operation Optional. If "replace", $to_address replaces current setting (default).
+	 *                          If "add", $to_address is added to the current setting.
+	 * @return BP_Email
+	 */
+	public function set_cc( $cc_address, $name = '', $operation = 'replace' ) {
+		$cc = ( $operation !== 'replace' ) ? $this->cc : array();
+
+		if ( is_array( $cc_address ) ) {
+			foreach ( $cc_address as $address ) {
+				$cc[] = new BP_Email_Recipient( $address );
+			}
+
+		} else {
+			$cc[] = new BP_Email_Recipient( $cc_address, $name );
+		}
+
+		/**
+		 * Filters the new value of the email's "CC" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient[] $cc CC recipients.
+		 * @param string|array|int|WP_User $cc_address Either a email address, user ID, WP_User object,
+		 *                                             or an array containing any combination of the above.
+		 * @param string $name Optional. If $cc_address is a string, this is the recipient's name.
+		 * @param string $operation If "replace", $to_address replaced previous recipients. If "add",
+		 *                          $to_address was added to the array of recipients.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->cc = apply_filters( 'bp_email_set_cc', $cc, $cc_address, $name, $operation, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email content (HTML).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $content HTML email content.
+	 * @return BP_Email
+	 */
+	public function set_content_html( $content ) {
+
+		/**
+		 * Filters the new value of the email's "content" property (HTML).
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $content HTML email content.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->content_html = apply_filters( 'bp_email_set_content_html', $content, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email content (plain text).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $content Plain text email content.
+	 * @return BP_Email
+	 */
+	public function set_content_plaintext( $content ) {
+
+		/**
+		 * Filters the new value of the email's "content" property (plain text).
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $content Plain text email content.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->content_plaintext = apply_filters( 'bp_email_set_content_plaintext', $content, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the content type (HTML or plain text) to send the email in.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $content_type Email content type ("html" or "plaintext").
+	 * @return BP_Email
+	 */
+	public function set_content_type( $content_type ) {
+		if ( ! in_array( $content_type, array( 'html', 'plaintext', ), true ) ) {
+			$class        = get_class_vars( get_class() );
+			$content_type = $class['content_type'];
+		}
+
+		/**
+		 * Filters the new value of the email's "content type" property.
+		 *
+		 * The content type (HTML or plain text) to send the email in.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $content_type Email content type ("html" or "plaintext").
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->content_type = apply_filters( 'bp_email_set_content_type', $content_type, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "from" address and name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $email_address Either a email address, user ID, or WP_User object.
+	 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_from( $email_address, $name = '' ) {
+		$from = new BP_Email_Recipient( $email_address, $name );
+
+		/**
+		 * Filters the new value of the email's "from" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient $from Sender details.
+		 * @param string|array|int|WP_User $email_address Either a email address, user ID, or WP_User object.
+		 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->from = apply_filters( 'bp_email_set_from', $from, $email_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the Post object containing the email content template.
+	 *
+	 * Also sets the email's subject, content, and template from the Post, for convenience.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param WP_Post $post
+	 * @return BP_Email
+	 */
+	public function set_post_object( WP_Post $post ) {
+
+		/**
+		 * Filters the new value of the email's "post object" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param WP_Post $post A Post.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->post_object = apply_filters( 'bp_email_set_post_object', $post, $this );
+
+		if ( is_a( $this->post_object, 'WP_Post' ) ) {
+			$this->set_subject( $this->post_object->post_title )
+				->set_content_html( $this->post_object->post_content )
+				->set_content_plaintext( $this->post_object->post_excerpt );
+
+			ob_start();
+
+			// Load the template.
+			add_filter( 'bp_locate_template_and_load', '__return_true' );
+
+			bp_locate_template( bp_email_get_template( $this->post_object ), true, false );
+
+			remove_filter( 'bp_locate_template_and_load', '__return_true' );
+
+			$this->set_template( ob_get_contents() );
+
+			ob_end_clean();
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "reply to" address and name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $email_address Either a email address, user ID, WP_User object,
+	 *                                                or an array containing any combination of the above.
+	 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_reply_to( $email_address, $name = '' ) {
+		$reply_to = new BP_Email_Recipient( $email_address, $name );
+
+		/**
+		 * Filters the new value of the email's "reply to" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient $reply_to "Reply to" recipient.
+		 * @param string|array|int|WP_User $email_address Either a email address, user ID, WP_User object,
+		 *                                                or an array containing any combination of the above.
+		 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->reply_to = apply_filters( 'bp_email_set_reply_to', $reply_to, $email_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email subject.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $subject Email subject.
+	 * @return BP_Email
+	 */
+	public function set_subject( $subject ) {
+
+		/**
+		 * Filters the new value of the subject email property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $subject Email subject.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->subject = apply_filters( 'bp_email_set_subject', $subject, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email template (the HTML wrapper around the email content).
+	 *
+	 * This needs to include the string "{{{content}}}" to have the post content added
+	 * when the email template is rendered.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $template Email template. Assumed to be HTML.
+	 * @return BP_Email
+	 */
+	public function set_template( $template ) {
+
+		/**
+		 * Filters the new value of the template email property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $template Email template. Assumed to be HTML.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->template = apply_filters( 'bp_email_set_template', $template, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "to" address and name.
+	 *
+	 * IMPORTANT NOTE: the assumption with all emails sent by (and belonging to) BuddyPress itself
+	 * is that there will only be a single `$to_address`. This is to simplify token and templating
+	 * logic (for example, if multiple recipients, the "unsubscribe" link in the emails will all
+	 * only link to the first recipient).
+	 *
+	 * To set a single address, the first parameter is the address and the second the name.
+	 * You can also pass a user ID or a WP_User object.
+	 *
+	 * To set multiple addresses, for each array item, the key is the email address and
+	 * the value is the name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $to_address Either a email address, user ID, WP_User object,
+	 *                                             or an array containing any combination of the above.
+	 * @param string $name Optional. If $to_address is a string, this is the recipient's name.
+	 * @param string $operation Optional. If "replace", $to_address replaces current setting (default).
+	 *                          If "add", $to_address is added to the current setting.
+	 * @return BP_Email
+	 */
+	public function set_to( $to_address, $name = '', $operation = 'replace' ) {
+		$to = ( $operation !== 'replace' ) ? $this->to : array();
+
+		if ( is_array( $to_address ) ) {
+			foreach ( $to_address as $address ) {
+				$to[] = new BP_Email_Recipient( $address );
+			}
+
+		} else {
+			$to[] = new BP_Email_Recipient( $to_address, $name );
+		}
+
+		/**
+		 * Filters the new value of the email's "to" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient[] "To" recipients.
+		 * @param string $to_address "To" address.
+		 * @param string $name "To" name.
+		 * @param string $operation If "replace", $to_address replaced previous recipients. If "add",
+		 *                          $to_address was added to the array of recipients.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->to = apply_filters( 'bp_email_set_to', $to, $to_address, $name, $operation, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set token names and replacement values for this email.
+	 *
+	 * In templates, tokens are inserted with a Handlebars-like syntax, e.g. `{{token_name}}`.
+	 * { and } are reserved characters. There's no need to specify these brackets in your token names.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string[] $tokens Associative array, contains key/value pairs of token name/value.
+	 *                         Values are a string or a callable function.
+	 * @return BP_Email
+	 */
+	public function set_tokens( array $tokens ) {
+		$formatted_tokens = array();
+
+		foreach ( $tokens as $name => $value ) {
+			$name                      = str_replace( array( '{', '}' ), '', sanitize_text_field( $name ) );
+			$formatted_tokens[ $name ] = $value;
+		}
+
+		/**
+		 * Filters the new value of the email's "tokens" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string[] $formatted_tokens Associative pairing of token names (key)
+		 *                                   and replacement values (value).
+		 * @param string[] $tokens           Associative pairing of unformatted token
+		 *                                   names (key) and replacement values (value).
+		 * @param BP_Email $this             Current instance of the email type class.
+		 */
+		$this->tokens = apply_filters( 'bp_email_set_tokens', $formatted_tokens, $tokens, $this );
+
+		return $this;
+	}
+
+
+	/*
+	 * Sanitisation and validation logic.
+	 */
+
+	/**
+	 * Check that we'd be able to send this email.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return bool|WP_Error Returns true if validation succesful, else a descriptive WP_Error.
+	 */
+	public function validate() {
+		$retval = true;
+
+		// BCC, CC, and token properties are optional.
+		if (
+			! $this->get_from() ||
+			! $this->get_to() ||
+			! $this->get_subject() ||
+			! $this->get_content() ||
+			! $this->get_template()
+		) {
+			$retval = new WP_Error( 'missing_parameter', __CLASS__, $this );
+		}
+
+		/**
+		 * Filters whether the email passes basic validation checks.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param bool|WP_Error $retval Returns true if validation succesful, else a descriptive WP_Error.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		return apply_filters( 'bp_email_validate', $retval, $this );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-embed.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-embed.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-embed.php	(revision 12265)
@@ -0,0 +1,246 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Enable oEmbeds in BuddyPress contexts.
+ *
+ * Extends WP_Embed class for use with BuddyPress.
+ *
+ * @since 1.5.0
+ *
+ * @see WP_Embed
+ */
+class BP_Embed extends WP_Embed {
+
+	/**
+	 * Constructor
+	 *
+	 * @global WP_Embed $wp_embed
+	 */
+	public function __construct() {
+		global $wp_embed;
+
+		// Make sure we populate the WP_Embed handlers array.
+		// These are providers that use a regex callback on the URL in question.
+		// Do not confuse with oEmbed providers, which require an external ping.
+		// Used in WP_Embed::shortcode().
+		$this->handlers = $wp_embed->handlers;
+
+		if ( bp_use_embed_in_activity() ) {
+			add_filter( 'bp_get_activity_content_body', array( &$this, 'autoembed' ), 8 );
+			add_filter( 'bp_get_activity_content_body', array( &$this, 'run_shortcode' ), 7 );
+		}
+
+		if ( bp_use_embed_in_activity_replies() ) {
+			add_filter( 'bp_get_activity_content', array( &$this, 'autoembed' ), 8 );
+			add_filter( 'bp_get_activity_content', array( &$this, 'run_shortcode' ), 7 );
+		}
+
+		if ( bp_use_embed_in_private_messages() ) {
+			add_filter( 'bp_get_the_thread_message_content', array( &$this, 'autoembed' ), 8 );
+			add_filter( 'bp_get_the_thread_message_content', array( &$this, 'run_shortcode' ), 7 );
+		}
+
+		/**
+		 * Filters the BuddyPress Core oEmbed setup.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param BP_Embed $this Current instance of the BP_Embed. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_core_setup_oembed', array( &$this ) );
+	}
+
+	/**
+	 * The {@link do_shortcode()} callback function.
+	 *
+	 * Attempts to convert a URL into embed HTML. Starts by checking the
+	 * URL against the regex of the registered embed handlers. Next, checks
+	 * the URL against the regex of registered {@link WP_oEmbed} providers
+	 * if oEmbed discovery is false. If none of the regex matches and it's
+	 * enabled, then the URL will be passed to {@link BP_Embed::parse_oembed()}
+	 * for oEmbed parsing.
+	 *
+	 *
+	 * @param array  $attr Shortcode attributes.
+	 * @param string $url  The URL attempting to be embeded.
+	 * @return string The embed HTML on success, otherwise the original URL.
+	 */
+	public function shortcode( $attr, $url = '' ) {
+		if ( empty( $url ) )
+			return '';
+
+		$rawattr = $attr;
+		$attr = wp_parse_args( $attr, wp_embed_defaults() );
+
+		// Use kses to convert & into &amp; and we need to undo this
+		// See https://core.trac.wordpress.org/ticket/11311.
+		$url = str_replace( '&amp;', '&', $url );
+
+		// Look for known internal handlers.
+		ksort( $this->handlers );
+		foreach ( $this->handlers as $priority => $handlers ) {
+			foreach ( $handlers as $hid => $handler ) {
+				if ( preg_match( $handler['regex'], $url, $matches ) && is_callable( $handler['callback'] ) ) {
+					if ( false !== $return = call_user_func( $handler['callback'], $matches, $attr, $url, $rawattr ) ) {
+
+						/**
+						 * Filters the oEmbed handler result for the provided URL.
+						 *
+						 * @since 1.5.0
+						 *
+						 * @param string $return Handler callback for the oEmbed.
+						 * @param string $url    URL attempting to be embedded.
+						 * @param array  $attr   Shortcode attributes.
+						 */
+						return apply_filters( 'embed_handler_html', $return, $url, $attr );
+					}
+				}
+			}
+		}
+
+		/**
+		 * Filters the embed object ID.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $value Value of zero.
+		 */
+		$id = apply_filters( 'embed_post_id', 0 );
+
+		$unfiltered_html   = current_user_can( 'unfiltered_html' );
+		$default_discovery = false;
+
+		// Since 4.4, WordPress is now an oEmbed provider.
+		if ( function_exists( 'wp_oembed_register_route' ) ) {
+			$unfiltered_html   = true;
+			$default_discovery = true;
+		}
+
+		/**
+		 * Filters whether or not oEmbed discovery is on.
+		 *
+		 * @since 1.5.0
+		 * @since 2.5.0 Default status of oEmbed discovery has been switched
+		 *              to true to apply changes introduced in WordPress 4.4
+		 *
+		 * @param bool $default_discovery Current status of oEmbed discovery.
+		 */
+		$attr['discover'] = ( apply_filters( 'bp_embed_oembed_discover', $default_discovery ) && $unfiltered_html );
+
+		// Set up a new WP oEmbed object to check URL with registered oEmbed providers.
+		require_once( ABSPATH . WPINC . '/class-oembed.php' );
+		$oembed_obj = _wp_oembed_get_object();
+
+		// If oEmbed discovery is true, skip oEmbed provider check.
+		$is_oembed_link = false;
+		if ( !$attr['discover'] ) {
+			foreach ( (array) $oembed_obj->providers as $provider_matchmask => $provider ) {
+				$regex = ( $is_regex = $provider[1] ) ? $provider_matchmask : '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $provider_matchmask ), '#' ) ) . '#i';
+
+				if ( preg_match( $regex, $url ) )
+					$is_oembed_link = true;
+			}
+
+			// If url doesn't match a WP oEmbed provider, stop parsing.
+			if ( !$is_oembed_link )
+				return $this->maybe_make_link( $url );
+		}
+
+		return $this->parse_oembed( $id, $url, $attr, $rawattr );
+	}
+
+	/**
+	 * Base function so BP components/plugins can parse links to be embedded.
+	 *
+	 * View an example to add support in {@link bp_activity_embed()}.
+	 *
+	 *       on success.
+	 *       oEmbed failure.
+	 *
+	 * @param int    $id      ID to do the caching for.
+	 * @param string $url     The URL attempting to be embedded.
+	 * @param array  $attr    Shortcode attributes from {@link WP_Embed::shortcode()}.
+	 * @param array  $rawattr Untouched shortcode attributes from
+	 *                        {@link WP_Embed::shortcode()}.
+	 * @return string The embed HTML on success, otherwise the original URL.
+	 */
+	public function parse_oembed( $id, $url, $attr, $rawattr ) {
+		$id = intval( $id );
+
+		if ( $id ) {
+			// Setup the cachekey.
+			$cachekey = '_oembed_' . md5( $url . serialize( $attr ) );
+
+			// Let components / plugins grab their cache.
+			$cache = '';
+
+			/**
+			 * Filters the cache value to be used in the oEmbed, if exists.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $cache    Empty initial cache value.
+			 * @param int    $id       ID that the caching is for.
+			 * @param string $cachekey Key to use for the caching in the database.
+			 * @param string $url      The URL attempting to be embedded.
+			 * @param array  $attr     Parsed shortcode attributes.
+			 * @param array  $rawattr  Unparsed shortcode attributes.
+			 */
+			$cache = apply_filters( 'bp_embed_get_cache', $cache, $id, $cachekey, $url, $attr, $rawattr );
+
+			// Grab cache and return it if available.
+			if ( !empty( $cache ) ) {
+
+				/**
+				 * Filters the found cache for the provided URL.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string $cache   Cached HTML markup for embed.
+				 * @param string $url     The URL being embedded.
+				 * @param array  $attr    Parsed shortcode attributes.
+				 * @param array  $rawattr Unparased shortcode attributes.
+				 */
+				return apply_filters( 'bp_embed_oembed_html', $cache, $url, $attr, $rawattr );
+
+			// If no cache, ping the oEmbed provider and cache the result.
+			} else {
+				$html = wp_oembed_get( $url, $attr );
+				$cache = ( $html ) ? $html : $url;
+
+				/**
+				 * Fires if there is no existing cache and triggers cache setting.
+				 *
+				 * Lets components / plugins save their cache.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string $cache    Newly cached HTML markup for embed.
+				 * @param string $cachekey Key to use for the caching in the database.
+				 * @param int    $id       ID to do the caching for.
+				 */
+				do_action( 'bp_embed_update_cache', $cache, $cachekey, $id );
+
+				// If there was a result, return it.
+				if ( $html ) {
+
+					/** This filter is documented in bp-core/classes/class-bp-embed.php */
+					return apply_filters( 'bp_embed_oembed_html', $html, $url, $attr, $rawattr );
+				}
+			}
+		}
+
+		// Still unknown.
+		return $this->maybe_make_link( $url );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-media-extractor.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-media-extractor.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-media-extractor.php	(revision 12265)
@@ -0,0 +1,928 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Extracts media from text. Use {@link extract()}.
+ *
+ * @since 2.3.0
+ *
+ * The supported types are links, mentions, images, shortcodes, embeds, audio, video, and "all".
+ * This is what each type extracts:
+ *
+ * Links:      <a href="http://example.com">
+ * Mentions:   @name
+ *             If the Activity component is enabled, we use it to parse out any at-names. A consequence
+ *             to note is that the "name" mentioned must match a real user account. If it's a made-up
+ *             at-name, then it isn't extracted.
+ *             If the Activity component is disabled, any at-name is extracted (both those matching
+ *             real accounts, and those made-up).
+ * Images:     <img src="image.gif">, [gallery], [gallery ids="2,3"], featured images (Post thumbnails).
+ *             If an extracted image is in the Media Library, then its resolution will be included.
+ * Shortcodes: Extract information about any (registered) shortcodes.
+ *             This includes any shortcodes indirectly covered by any of the other media extraction types.
+ *             For example, [gallery].
+ * Embeds:     Extract any URL matching a registered oEmbed handler.
+ * Audio:      <a href="*.mp3"">, [audio]
+ *             See wp_get_audio_extensions() for supported audio formats.
+ * Video:      [video]
+ *             See wp_get_video_extensions() for supported video formats.
+ *
+ * @see BP_Media_Extractor::extract() Use this to extract media.
+ */
+class BP_Media_Extractor {
+	/**
+	 * Media type.
+	 *
+	 * @since 2.3.0
+	 * @var int
+	 */
+	const ALL        = 255;
+	const LINKS      = 1;
+	const MENTIONS   = 2;
+	const IMAGES     = 4;
+	const SHORTCODES = 8;
+	const EMBEDS     = 16;
+	const AUDIO      = 32;
+	const VIDEOS     = 64;
+
+
+	/**
+	 * Extract media from text.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string|WP_Post $richtext        Content to parse.
+	 * @param int            $what_to_extract Media type to extract (optional).
+	 * @param array          $extra_args      Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $audio
+	 *         @type int $embeds
+	 *         @type int $images
+	 *         @type int $links
+	 *         @type int $mentions
+	 *         @type int $shortcodes
+	 *         @type int $video
+	 *     }
+	 *     @type array $audio Extracted audio. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $source Media source. Either "html" or "shortcodes".
+	 *         @type string $url    Link to audio.
+	 *     }
+	 *     @type array $embeds Extracted oEmbeds. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $url oEmbed link.
+	 *     }
+	 *     @type array $images Extracted images. {
+	 *         Array of extracted media.
+	 *
+	 *         @type int    $gallery_id Gallery ID. Optional, not always set.
+	 *         @type int    $height     Width of image. If unknown, set to 0.
+	 *         @type string $source     Media source. Either "html" or "galleries".
+	 *         @type string $url        Link to image.
+	 *         @type int    $width      Width of image. If unknown, set to 0.
+	 *     }
+	 *     @type array $links Extracted URLs. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $url Link.
+	 *     }
+	 *     @type array $mentions Extracted mentions. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $name    @mention.
+	 *         @type string $user_id User ID. Optional, only set if Activity component enabled.
+	 *     }
+	 *     @type array $shortcodes Extracted shortcodes. {
+	 *         Array of extracted media.
+	 *
+	 *         @type array  $attributes Key/value pairs of the shortcodes attributes (if any).
+	 *         @type string $content    Text wrapped by the shortcode.
+	 *         @type string $type       Shortcode type.
+	 *         @type string $original   The entire shortcode.
+	 *     }
+	 *     @type array $videos Extracted video. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $source Media source. Currently only "shortcodes".
+	 *         @type string $url    Link to audio.
+	 *     }
+	 * }
+	 */
+	public function extract( $richtext, $what_to_extract = self::ALL, $extra_args = array() ) {
+		$media = array();
+
+		// Support passing a WordPress Post for the $richtext parameter.
+		if ( is_a( $richtext, 'WP_Post' ) ) {
+			$extra_args['post'] = $richtext;
+			$richtext           = $extra_args['post']->post_content;
+		}
+
+		$plaintext = $this->strip_markup( $richtext );
+
+
+		// Extract links.
+		if ( self::LINKS & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_links( $richtext, $plaintext, $extra_args ) );
+		}
+
+		// Extract mentions.
+		if ( self::MENTIONS & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_mentions( $richtext, $plaintext, $extra_args ) );
+		}
+
+		// Extract images.
+		if ( self::IMAGES & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_images( $richtext, $plaintext, $extra_args ) );
+		}
+
+		// Extract shortcodes.
+		if ( self::SHORTCODES & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_shortcodes( $richtext, $plaintext, $extra_args ) );
+		}
+
+		// Extract oEmbeds.
+		if ( self::EMBEDS & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_embeds( $richtext, $plaintext, $extra_args ) );
+		}
+
+		// Extract audio.
+		if ( self::AUDIO & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_audio( $richtext, $plaintext, $extra_args ) );
+		}
+
+		// Extract video.
+		if ( self::VIDEOS & $what_to_extract ) {
+			$media = array_merge_recursive( $media, $this->extract_video( $richtext, $plaintext, $extra_args ) );
+		}
+
+		/**
+		 * Filters media extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $media           Extracted media. See {@link BP_Media_Extractor::extract()} for format.
+		 * @param string $richtext        Content to parse.
+		 * @param int    $what_to_extract Media type to extract.
+		 * @param array  $extra_args      Bespoke data for a particular extractor.
+		 * @param string $plaintext       Copy of $richtext without any markup.
+		 */
+		return apply_filters( 'bp_media_extractor_extract', $media, $richtext, $what_to_extract, $extra_args, $plaintext );
+	}
+
+
+	/**
+	 * Content type specific extraction methods.
+	 *
+	 * You shouldn't need to use these directly; just use {@link BP_Media_Extractor::extract()}.
+	 */
+
+	/**
+	 * Extract `<a href>` tags from text.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $links
+	 *     }
+	 *     @type array $links Extracted URLs. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $url Link.
+	 *     }
+	 * }
+	 */
+	protected function extract_links( $richtext, $plaintext, $extra_args = array() ) {
+		$data = array( 'has' => array( 'links' => 0 ), 'links' => array() );
+
+		// Matches: href="text" and href='text'.
+		if ( stripos( $richtext, 'href=' ) !== false ) {
+			preg_match_all( '#href=(["\'])([^"\']+)\1#i', $richtext, $matches );
+
+			if ( ! empty( $matches[2] ) ) {
+				$matches[2] = array_unique( $matches[2] );
+
+				foreach ( $matches[2] as $link_src ) {
+					$link_src = esc_url_raw( $link_src );
+
+					if ( $link_src ) {
+						$data['links'][] = array( 'url' => $link_src );
+					}
+				}
+			}
+		}
+
+		$data['has']['links'] = count( $data['links'] );
+
+		/**
+		 * Filters links extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $data       Extracted links. See {@link BP_Media_Extractor::extract_links()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_links', $data, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract @mentions tags from text.
+	 *
+	 * If the Activity component is enabled, it is used to parse @mentions.
+	 * The mentioned "name" must match a user account, otherwise it is discarded.
+	 *
+	 * If the Activity component is disabled, any @mentions are extracted.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor.
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $mentions
+	 *     }
+	 *     @type array $mentions Extracted mentions. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $name    @mention.
+	 *         @type string $user_id User ID. Optional, only set if Activity component enabled.
+	 *     }
+	 * }
+	 */
+	protected function extract_mentions( $richtext, $plaintext, $extra_args = array() ) {
+		$data     = array( 'has' => array( 'mentions' => 0 ), 'mentions' => array() );
+		$mentions = array();
+
+		// If the Activity component is active, use it to parse @mentions.
+		if ( bp_is_active( 'activity' ) ) {
+			$mentions = bp_activity_find_mentions( $plaintext );
+			if ( ! $mentions ) {
+				$mentions = array();
+			}
+
+		// If the Activity component is disabled, instead do a basic parse.
+		} else {
+			if ( strpos( $plaintext, '@' ) !== false ) {
+				preg_match_all( '/[@]+([A-Za-z0-9-_\.@]+)\b/', $plaintext, $matches );
+
+				if ( ! empty( $matches[1] ) ) {
+					$mentions = array_unique( array_map( 'strtolower', $matches[1] ) );
+				}
+			}
+		}
+
+		// Build results.
+		foreach ( $mentions as $user_id => $mention_name ) {
+			$mention = array( 'name' => strtolower( $mention_name ) );
+
+			// If the Activity component is active, store the User ID, too.
+			if ( bp_is_active( 'activity' ) ) {
+				$mention['user_id'] = (int) $user_id;
+			}
+
+			$data['mentions'][] = $mention;
+		}
+
+		$data['has']['mentions'] = count( $data['mentions'] );
+
+		/**
+		 * Filters @mentions extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $data       Extracted @mentions. See {@link BP_Media_Extractor::extract_mentions()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+		 */
+		return apply_filters( 'bp_media_extractor_mentions', $data, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract images from `<img src>` tags, [galleries], and featured images from a Post.
+	 *
+	 * If an image is in the Media Library, then its resolution is included in the results.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $images
+	 *     }
+	 *     @type array $images Extracted images. {
+	 *         Array of extracted media.
+	 *
+	 *         @type int    $gallery_id Gallery ID. Optional, not always set.
+	 *         @type int    $height     Width of image. If unknown, set to 0.
+	 *         @type string $source     Media source. Either "html" or "galleries".
+	 *         @type string $url        Link to image.
+	 *         @type int    $width      Width of image. If unknown, set to 0.
+	 *     }
+	 * }
+	 */
+	protected function extract_images( $richtext, $plaintext, $extra_args = array() ) {
+		$media = array( 'has' => array( 'images' => 0 ), 'images' => array() );
+
+		$featured_image = $this->extract_images_from_featured_images( $richtext, $plaintext, $extra_args );
+		$galleries      = $this->extract_images_from_galleries( $richtext, $plaintext, $extra_args );
+
+
+		// `<img src>` tags.
+		if ( stripos( $richtext, 'src=' ) !== false ) {
+			preg_match_all( '#src=(["\'])([^"\']+)\1#i', $richtext, $img_srcs );  // Matches src="text" and src='text'.
+
+			// <img>.
+			if ( ! empty( $img_srcs[2] ) ) {
+				$img_srcs[2] = array_unique( $img_srcs[2] );
+
+				foreach ( $img_srcs[2] as $image_src ) {
+					// Skip data URIs.
+					if ( strtolower( substr( $image_src, 0, 5 ) ) === 'data:' ) {
+						continue;
+					}
+
+					$image_src = esc_url_raw( $image_src );
+					if ( ! $image_src ) {
+						continue;
+					}
+
+					$media['images'][] = array(
+						'source' => 'html',
+						'url'    => $image_src,
+
+						// The image resolution isn't available, but we need to set the keys anyway.
+						'height' => 0,
+						'width'  => 0,
+					);
+				}
+			}
+		}
+
+		// Galleries.
+		if ( ! empty( $galleries ) ) {
+			foreach ( $galleries as $gallery ) {
+				foreach ( $gallery as $image ) {
+					$image_url = esc_url_raw( $image['url'] );
+					if ( ! $image_url ) {
+						continue;
+					}
+
+					$media['images'][] = array(
+						'gallery_id' => $image['gallery_id'],
+						'source'     => 'galleries',
+						'url'        => $image_url,
+						'width'      => $image['width'],
+						'height'     => $image['height'],
+					);
+				}
+			}
+
+			$media['has']['galleries'] = count( $galleries );
+		}
+
+		// Featured images (aka thumbnails).
+		if ( ! empty( $featured_image ) ) {
+			$image_url = esc_url_raw( $featured_image[0] );
+
+			if ( $image_url ) {
+				$media['images'][] = array(
+					'source' => 'featured_images',
+					'url'    => $image_url,
+					'width'  => $featured_image[1],
+					'height' => $featured_image[2],
+				);
+
+				$media['has']['featured_images'] = 1;
+			}
+		}
+
+		// Update image count.
+		$media['has']['images'] = count( $media['images'] );
+
+
+		/**
+		 * Filters images extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $media      Extracted images. See {@link BP_Media_Extractor::extract_images()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_images', $media, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract shortcodes from text.
+	 *
+	 * This includes any shortcodes indirectly used by other media extraction types.
+	 * For example, [gallery] and [audio].
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $shortcodes
+	 *     }
+	 *     @type array $shortcodes Extracted shortcodes. {
+	 *         Array of extracted media.
+	 *
+	 *         @type array  $attributes Key/value pairs of the shortcodes attributes (if any).
+	 *         @type string $content    Text wrapped by the shortcode.
+	 *         @type string $type       Shortcode type.
+	 *         @type string $original   The entire shortcode.
+	 *     }
+	 * }
+	 */
+	protected function extract_shortcodes( $richtext, $plaintext, $extra_args = array() ) {
+		$data = array( 'has' => array( 'shortcodes' => 0 ), 'shortcodes' => array() );
+
+		// Match any registered WordPress shortcodes.
+		if ( strpos( $richtext, '[' ) !== false ) {
+			preg_match_all( '/' . get_shortcode_regex() . '/s', $richtext, $matches );
+
+			if ( ! empty( $matches[2] ) ) {
+				foreach ( $matches[2] as $i => $shortcode_name ) {
+					$attrs = shortcode_parse_atts( $matches[3][ $i ] );
+					$attrs = ( ! $attrs ) ? array() : (array) $attrs;
+
+					$shortcode               = array();
+					$shortcode['attributes'] = $attrs;             // Attributes.
+					$shortcode['content']    = $matches[5][ $i ];  // Content.
+					$shortcode['type']       = $shortcode_name;    // Shortcode.
+					$shortcode['original']   = $matches[0][ $i ];  // Entire shortcode.
+
+					$data['shortcodes'][] = $shortcode;
+				}
+			}
+		}
+
+		$data['has']['shortcodes'] = count( $data['shortcodes'] );
+
+		/**
+		 * Filters shortcodes extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $data       Extracted shortcodes.
+		 *                           See {@link BP_Media_Extractor::extract_shortcodes()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_shortcodes', $data, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract any URL, matching a registered oEmbed endpoint, from text.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $embeds
+	 *     }
+	 *     @type array $embeds Extracted oEmbeds. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $url oEmbed link.
+	 *     }
+	 * }
+	 */
+	protected function extract_embeds( $richtext, $plaintext, $extra_args = array() ) {
+		$data   = array( 'has' => array( 'embeds' => 0 ), 'embeds' => array() );
+		$embeds = array();
+
+		if ( ! function_exists( '_wp_oembed_get_object' ) ) {
+			require( ABSPATH . WPINC . '/class-oembed.php' );
+		}
+
+
+		// Matches any links on their own lines. They may be oEmbeds.
+		if ( stripos( $richtext, 'http' ) !== false ) {
+			preg_match_all( '#^\s*(https?://[^\s"]+)\s*$#im', $richtext, $matches );
+
+			if ( ! empty( $matches[1] ) ) {
+				$matches[1] = array_unique( $matches[1] );
+				$oembed     = _wp_oembed_get_object();
+
+				foreach ( $matches[1] as $link ) {
+					// Skip data URIs.
+					if ( strtolower( substr( $link, 0, 5 ) ) === 'data:' ) {
+						continue;
+					}
+
+					foreach ( $oembed->providers as $matchmask => $oembed_data ) {
+						list( , $is_regex ) = $oembed_data;
+
+						// Turn asterisk-type provider URLs into regexs.
+						if ( ! $is_regex ) {
+							$matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
+							$matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
+						}
+
+						// Check whether this "link" is really an oEmbed.
+						if ( preg_match( $matchmask, $link ) ) {
+							$data['embeds'][] = array( 'url' => $link );
+
+							break;
+						}
+					}
+				}
+			}
+		}
+
+		$data['has']['embeds'] = count( $data['embeds'] );
+
+		/**
+		 * Filters embeds extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $data       Extracted embeds. See {@link BP_Media_Extractor::extract_embeds()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_embeds', $data, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract [audio] shortcodes and `<a href="*.mp3">` tags, from text.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @see wp_get_audio_extensions() for supported audio formats.
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $audio
+	 *     }
+	 *     @type array $audio Extracted audio. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $original The entire shortcode.
+	 *         @type string $source   Media source. Either "html" or "shortcodes".
+	 *         @type string $url      Link to audio.
+	 *     }
+	 * }
+	 */
+	protected function extract_audio( $richtext, $plaintext, $extra_args = array() ) {
+		$data   = array( 'has' => array( 'audio' => 0 ), 'audio' => array() );
+		$audios = $this->extract_shortcodes( $richtext, $plaintext, $extra_args );
+		$links  = $this->extract_links( $richtext, $plaintext, $extra_args );
+
+		$audio_types = wp_get_audio_extensions();
+
+
+		// [audio]
+		$audios = wp_list_filter( $audios['shortcodes'], array( 'type' => 'audio' ) );
+		foreach ( $audios as $audio ) {
+
+			// Media URL can appear as the first parameter inside the shortcode brackets.
+			if ( isset( $audio['attributes']['src'] ) ) {
+				$src_param = 'src';
+			} elseif ( isset( $audio['attributes'][0] ) ) {
+				$src_param = 0;
+			} else {
+				continue;
+			}
+
+			$path = untrailingslashit( parse_url( $audio['attributes'][ $src_param ], PHP_URL_PATH ) );
+
+			foreach ( $audio_types as $extension ) {
+				$extension = '.' . $extension;
+
+				// Check this URL's file extension matches that of an accepted audio format.
+				if ( ! $path || substr( $path, -4 ) !== $extension ) {
+					continue;
+				}
+
+				$data['audio'][] = array(
+					'original' => '[audio src="' . esc_url_raw( $audio['attributes'][ $src_param ] ) . '"]',
+					'source'   => 'shortcodes',
+					'url'      => esc_url_raw( $audio['attributes'][ $src_param ] ),
+				);
+			}
+		}
+
+		// <a href="*.mp3"> tags.
+		foreach ( $audio_types as $extension ) {
+			$extension = '.' . $extension;
+
+			foreach ( $links['links'] as $link ) {
+				$path = untrailingslashit( parse_url( $link['url'], PHP_URL_PATH ) );
+
+				// Check this URL's file extension matches that of an accepted audio format.
+				if ( ! $path || substr( $path, -4 ) !== $extension ) {
+					continue;
+				}
+
+				$data['audio'][] = array(
+					'original' => '[audio src="' . esc_url_raw( $link['url'] ) . '"]',  // Build an audio shortcode.
+					'source'   => 'html',
+					'url'      => esc_url_raw( $link['url'] ),
+				);
+			}
+		}
+
+		$data['has']['audio'] = count( $data['audio'] );
+
+		/**
+		 * Filters audio extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $data       Extracted audio. See {@link BP_Media_Extractor::extract_audio()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_audio', $data, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract [video] shortcodes from text.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @see wp_get_video_extensions() for supported video formats.
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array {
+	 *     @type array $has Extracted media counts. {
+	 *         @type int $video
+	 *     }
+	 *     @type array $videos Extracted video. {
+	 *         Array of extracted media.
+	 *
+	 *         @type string $source Media source. Currently only "shortcodes".
+	 *         @type string $url    Link to audio.
+	 *     }
+	 * }
+	 */
+	protected function extract_video( $richtext, $plaintext, $extra_args = array() ) {
+		$data   = array( 'has' => array( 'videos' => 0 ), 'videos' => array() );
+		$videos = $this->extract_shortcodes( $richtext, $plaintext, $extra_args );
+
+		$video_types = wp_get_video_extensions();
+
+
+		// [video]
+		$videos = wp_list_filter( $videos['shortcodes'], array( 'type' => 'video' ) );
+		foreach ( $videos as $video ) {
+
+			// Media URL can appear as the first parameter inside the shortcode brackets.
+			if ( isset( $video['attributes']['src'] ) ) {
+				$src_param = 'src';
+			} elseif ( isset( $video['attributes'][0] ) ) {
+				$src_param = 0;
+			} else {
+				continue;
+			}
+
+			$path = untrailingslashit( parse_url( $video['attributes'][ $src_param ], PHP_URL_PATH ) );
+
+			foreach ( $video_types as $extension ) {
+				$extension = '.' . $extension;
+
+				// Check this URL's file extension matches that of an accepted video format (-5 for webm).
+				if ( ! $path || ( substr( $path, -4 ) !== $extension && substr( $path, -5 ) !== $extension ) ) {
+					continue;
+				}
+
+				$data['videos'][] = array(
+					'original' => $video['original'],  // Entire shortcode.
+					'source'   => 'shortcodes',
+					'url'      => esc_url_raw( $video['attributes'][ $src_param ] ),
+				);
+			}
+		}
+
+		$data['has']['videos'] = count( $data['videos'] );
+
+		/**
+		 * Filters videos extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $data       Extracted videos. See {@link BP_Media_Extractor::extract_videos()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_videos', $data, $richtext, $plaintext, $extra_args );
+	}
+
+
+	/**
+	 * Helpers and utility methods.
+	 */
+
+	/**
+	 * Extract images in [galleries] shortcodes from text.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Bespoke data for a particular extractor (optional).
+	 * @return array
+	 */
+	protected function extract_images_from_galleries( $richtext, $plaintext, $extra_args = array() ) {
+		if ( ! isset( $extra_args['post'] ) || ! is_a( $extra_args['post'], 'WP_Post' ) ) {
+			$post = new WP_Post( (object) array( 'post_content' => $richtext ) );
+		} else {
+			$post = $extra_args['post'];
+		}
+
+		// We're not using get_post_galleries_images() because it returns thumbnails; we want the original image.
+		$galleries      = get_post_galleries( $post, false );
+		$galleries_data = array();
+
+		if ( ! empty( $galleries ) ) {
+			// Validate the size of the images requested.
+			if ( isset( $extra_args['width'] ) ) {
+
+				// A width was specified but not a height, so calculate it assuming a 4:3 ratio.
+				if ( ! isset( $extra_args['height'] ) && ctype_digit( $extra_args['width'] ) ) {
+					$extra_args['height'] = round( ( $extra_args['width'] / 4 ) * 3 );
+				}
+
+				if ( ctype_digit( $extra_args['width'] ) ) {
+					$image_size = array( $extra_args['width'], $extra_args['height'] );
+				} else {
+					$image_size = $extra_args['width'];  // E.g. "thumb", "medium".
+				}
+
+			} else {
+				$image_size = 'full';
+			}
+
+			/**
+			 * There are two variants of gallery shortcode.
+			 *
+			 * One kind specifies the image (post) IDs via an `ids` parameter.
+			 * The other gets the image IDs from post_type=attachment and post_parent=get_the_ID().
+			 */
+
+			foreach ( $galleries as $gallery_id => $gallery ) {
+				$data   = array();
+				$images = array();
+
+				// Gallery ids= variant.
+				if ( isset( $gallery['ids'] ) ) {
+					$images = wp_parse_id_list( $gallery['ids'] );
+
+				// Gallery post_parent variant.
+				} elseif ( isset( $extra_args['post'] ) ) {
+					$images = wp_parse_id_list(
+						get_children( array(
+							'fields'         => 'ids',
+							'order'          => 'ASC',
+							'orderby'        => 'menu_order ID',
+							'post_mime_type' => 'image',
+							'post_parent'    => $extra_args['post']->ID,
+							'post_status'    => 'inherit',
+							'post_type'      => 'attachment',
+						) )
+					);
+				}
+
+				// Extract the data we need from each image in this gallery.
+				foreach ( $images as $image_id ) {
+					$image  = wp_get_attachment_image_src( $image_id, $image_size );
+					$data[] = array(
+						'url'    => $image[0],
+						'width'  => $image[1],
+						'height' => $image[2],
+
+						'gallery_id' => 1 + $gallery_id,
+					);
+				}
+
+				$galleries_data[] = $data;
+			}
+		}
+
+		/**
+		 * Filters image galleries extracted from text.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $galleries_data Galleries. See {@link BP_Media_Extractor::extract_images_from_galleries()}.
+		 * @param string $richtext       Content to parse.
+		 * @param string $plaintext      Copy of $richtext without any markup.
+		 * @param array  $extra_args     Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_galleries', $galleries_data, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Extract the featured image from a Post.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext   Content to parse.
+	 * @param string $plaintext  Sanitized version of the content.
+	 * @param array  $extra_args Contains data that an implementation might need beyond the defaults.
+	 * @return array
+	 */
+	protected function extract_images_from_featured_images( $richtext, $plaintext, $extra_args ) {
+		$image = array();
+		$thumb = 0;
+
+		if ( isset( $extra_args['post'] ) ) {
+			$thumb = (int) get_post_thumbnail_id( $extra_args['post']->ID );
+		}
+
+		if ( $thumb ) {
+			// Validate the size of the images requested.
+			if ( isset( $extra_args['width'] ) ) {
+				if ( ! isset( $extra_args['height'] ) && ctype_digit( $extra_args['width'] ) ) {
+					// A width was specified but not a height, so calculate it assuming a 4:3 ratio.
+					$extra_args['height'] = round( ( $extra_args['width'] / 4 ) * 3 );
+				}
+
+				if ( ctype_digit( $extra_args['width'] ) ) {
+					$image_size = array( $extra_args['width'], $extra_args['height'] );
+				} else {
+					$image_size = $extra_args['width'];  // E.g. "thumb", "medium".
+				}
+			} else {
+				$image_size = 'full';
+			}
+
+			$image = wp_get_attachment_image_src( $thumb, $image_size );
+		}
+
+		/**
+		 * Filters featured images extracted from a WordPress Post.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $image      Extracted images. See {@link BP_Media_Extractor_Post::extract_images()} for format.
+		 * @param string $richtext   Content to parse.
+		 * @param string $plaintext  Copy of $richtext without any markup.
+		 * @param array  $extra_args Bespoke data for a particular extractor.
+		 */
+		return apply_filters( 'bp_media_extractor_featured_images', $image, $richtext, $plaintext, $extra_args );
+	}
+
+	/**
+	 * Sanitize and format raw content to prepare for content extraction.
+	 *
+	 * HTML tags and shortcodes are removed, and HTML entities are decoded.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $richtext Content to sanitize.
+	 * @return string
+	 */
+	protected function strip_markup( $richtext ) {
+		$plaintext = strip_shortcodes( html_entity_decode( strip_tags( $richtext ) ) );
+
+		/**
+		 * Filters the generated plain text version of the content passed to the extractor.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $plaintext Generated plain text.
+		 * @param string $richtext  Original content.
+		 */
+		return apply_filters( 'bp_media_extractor_strip_markup', $plaintext, $richtext );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-members-suggestions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-members-suggestions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-members-suggestions.php	(revision 12265)
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Adds support for user at-mentions to the Suggestions API.
+ *
+ * This class is in the Core component because it's required by a class in the Groups component,
+ * and Groups is loaded before Members (alphabetical order).
+ *
+ * @since 2.1.0
+ */
+class BP_Members_Suggestions extends BP_Suggestions {
+
+	/**
+	 * Default arguments for this suggestions service.
+	 *
+	 * @since 2.1.0
+	 * @var array $args {
+	 *     @type int    $limit        Maximum number of results to display. Default: 16.
+	 *     @type bool   $only_friends If true, only match the current user's friends. Default: false.
+	 *     @type string $term         The suggestion service will try to find results that contain this string.
+	 *                                Mandatory.
+	 * }
+	 */
+	protected $default_args = array(
+		'limit'        => 10,
+		'only_friends' => false,
+		'term'         => '',
+		'type'         => '',
+	);
+
+
+	/**
+	 * Validate and sanitise the parameters for the suggestion service query.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return true|WP_Error If validation fails, return a WP_Error object. On success, return true (bool).
+	 */
+	public function validate() {
+		$this->args['only_friends'] = (bool) $this->args['only_friends'];
+
+		/**
+		 * Filters the members suggestions args for the current user.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array                  $args Array of arguments for the member suggestions.
+		 * @param BP_Members_Suggestions $this Current BP_Members_Suggestions instance.
+		 */
+		$this->args                 = apply_filters( 'bp_members_suggestions_args', $this->args, $this );
+
+		// Check for invalid or missing mandatory parameters.
+		if ( $this->args['only_friends'] && ( ! bp_is_active( 'friends' ) || ! is_user_logged_in() ) ) {
+			return new WP_Error( 'missing_requirement' );
+		}
+
+		/**
+		 * Filters the validation status for the suggestion service query.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param bool|WP_Error          $value Results of validation check.
+		 * @param BP_Members_Suggestions $this  Current BP_Members_Suggestions instance.
+		 */
+		return apply_filters( 'bp_members_suggestions_validate_args', parent::validate(), $this );
+	}
+
+	/**
+	 * Find and return a list of username suggestions that match the query.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return array|WP_Error Array of results. If there were problems, returns a WP_Error object.
+	 */
+	public function get_suggestions() {
+		$user_query = array(
+			'count_total'     => '',  // Prevents total count.
+			'populate_extras' => false,
+			'type'            => 'alphabetical',
+
+			'page'            => 1,
+			'per_page'        => $this->args['limit'],
+			'search_terms'    => $this->args['term'],
+			'search_wildcard' => 'right',
+		);
+
+		// Only return matches of friends of this user.
+		if ( $this->args['only_friends'] && is_user_logged_in() ) {
+			$user_query['user_id'] = get_current_user_id();
+		}
+
+		/**
+		 * Filters the members suggestions query args.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array                  $user_query Array of query arguments.
+		 * @param BP_Members_Suggestions $this       Current BP_Members_Suggestions instance.
+		 */
+		$user_query = apply_filters( 'bp_members_suggestions_query_args', $user_query, $this );
+		if ( is_wp_error( $user_query ) ) {
+			return $user_query;
+		}
+
+
+		$user_query = new BP_User_Query( $user_query );
+		$results    = array();
+
+		foreach ( $user_query->results as $user ) {
+			$result          = new stdClass();
+			$result->ID      = $user->user_nicename;
+			$result->image   = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
+			$result->name    = bp_core_get_user_displayname( $user->ID );
+			$result->user_id = $user->ID;
+
+			$results[] = $result;
+		}
+
+		/**
+		 * Filters the members suggestions results.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array                  $results Array of users to suggest.
+		 * @param BP_Members_Suggestions $this    Current BP_Members_Suggestions instance.
+		 */
+		return apply_filters( 'bp_members_suggestions_get_suggestions', $results, $this );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-phpmailer.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-phpmailer.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-phpmailer.php	(revision 12265)
@@ -0,0 +1,163 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Email delivery implementation using PHPMailer.
+ *
+ * @since 2.5.0
+ */
+class BP_PHPMailer implements BP_Email_Delivery {
+
+	/**
+	 * Send email(s).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param BP_Email $email Email to send.
+	 * @return bool|WP_Error Returns true if email send, else a descriptive WP_Error.
+	 */
+	public function bp_email( BP_Email $email ) {
+		static $phpmailer = null;
+
+		/**
+		 * Filter PHPMailer object to use.
+		 *
+		 * Specify an alternative version of PHPMailer to use instead of WordPress' default.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param null|PHPMailer $phpmailer The phpmailer class.
+		 */
+		$phpmailer = apply_filters( 'bp_phpmailer_object', $phpmailer );
+
+		if ( ! ( $phpmailer instanceof PHPMailer ) ) {
+			if ( ! class_exists( 'PHPMailer' ) ) {
+				require_once ABSPATH . WPINC . '/class-phpmailer.php';
+				require_once ABSPATH . WPINC . '/class-smtp.php';
+			}
+
+			$phpmailer = new PHPMailer( true );
+		}
+
+
+		/*
+		 * Resets.
+		 */
+		$phpmailer->MessageDate = date( 'D, j M Y H:i:s O' );
+		$phpmailer->clearAllRecipients();
+		$phpmailer->clearAttachments();
+		$phpmailer->clearCustomHeaders();
+		$phpmailer->clearReplyTos();
+		$phpmailer->Sender = '';
+
+
+		/*
+		 * Set up.
+		 */
+
+		$phpmailer->IsMail();
+		$phpmailer->CharSet = bp_get_option( 'blog_charset' );
+
+
+		/*
+		 * Content.
+		 */
+
+		$phpmailer->Subject = $email->get_subject( 'replace-tokens' );
+		$content_plaintext  = PHPMailer::normalizeBreaks( $email->get_content_plaintext( 'replace-tokens' ) );
+
+		if ( $email->get( 'content_type' ) === 'html' ) {
+			$phpmailer->msgHTML( $email->get_template( 'add-content' ) );
+			$phpmailer->AltBody = $content_plaintext;
+
+		} else {
+			$phpmailer->IsHTML( false );
+			$phpmailer->Body = $content_plaintext;
+		}
+
+		$recipient = $email->get_from();
+		try {
+			$phpmailer->SetFrom( $recipient->get_address(), $recipient->get_name(), false );
+		} catch ( phpmailerException $e ) {
+		}
+
+		$recipient = $email->get_reply_to();
+		try {
+			$phpmailer->addReplyTo( $recipient->get_address(), $recipient->get_name() );
+		} catch ( phpmailerException $e ) {
+		}
+
+		$recipients = $email->get_to();
+		foreach ( $recipients as $recipient ) {
+			try {
+				$phpmailer->AddAddress( $recipient->get_address(), $recipient->get_name() );
+			} catch ( phpmailerException $e ) {
+			}
+		}
+
+		$recipients = $email->get_cc();
+		foreach ( $recipients as $recipient ) {
+			try {
+				$phpmailer->AddCc( $recipient->get_address(), $recipient->get_name() );
+			} catch ( phpmailerException $e ) {
+			}
+		}
+
+		$recipients = $email->get_bcc();
+		foreach ( $recipients as $recipient ) {
+			try {
+				$phpmailer->AddBcc( $recipient->get_address(), $recipient->get_name() );
+			} catch ( phpmailerException $e ) {
+			}
+		}
+
+		$headers = $email->get_headers();
+		foreach ( $headers as $name => $content ) {
+			$phpmailer->AddCustomHeader( $name, $content );
+		}
+
+
+		/**
+		 * Fires after PHPMailer is initialised.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param PHPMailer $phpmailer The PHPMailer instance.
+		 */
+		do_action( 'bp_phpmailer_init', $phpmailer );
+
+		/** This filter is documented in wp-includes/pluggable.php */
+		do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );
+
+		try {
+			return $phpmailer->Send();
+		} catch ( phpmailerException $e ) {
+			return new WP_Error( $e->getCode(), $e->getMessage(), $email );
+		}
+	}
+
+
+	/*
+	 * Utility/helper functions.
+	 */
+
+	/**
+	 * Get an appropriate hostname for the email. Varies depending on site configuration.
+	 *
+	 * @since 2.5.0
+	 * @deprecated 2.5.3 No longer used.
+	 *
+	 * @return string
+	 */
+	public static function get_hostname() {
+		return '';
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-recursive-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-recursive-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-recursive-query.php	(revision 12265)
@@ -0,0 +1,230 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Base class for creating query classes that generate SQL fragments for filtering results based on recursive query params.
+ *
+ * @since 2.2.0
+ */
+abstract class BP_Recursive_Query {
+
+	/**
+	 * Query arguments passed to the constructor.
+	 *
+	 * @since 2.2.0
+	 * @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' clause, and only passes the 'where' clause.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @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 2.2.0
+	 *
+	 * @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'];
+			} elseif ( 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'][] = '';
+					} elseif ( 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 2.2.0
+	 *
+	 * @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;
+
+			} elseif ( ! is_array( $query ) ) {
+				continue;
+
+			// First-order clause.
+			} elseif ( $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.
+		 */
+		} elseif ( 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 2.2.0
+	 *
+	 * @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 2.2.0
+	 *
+	 * @param array $query Clause to check.
+	 *
+	 * @return bool
+	 */
+	abstract protected function is_first_order_clause( $query );
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-suggestions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-suggestions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-suggestions.php	(revision 12265)
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 2.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Base class for the BuddyPress Suggestions API.
+ *
+ * Originally built to power BuddyPress' at-mentions suggestions, it's flexible enough to be used
+ * for similar kinds of future core requirements, or those desired by third-party developers.
+ *
+ * To implement a new suggestions service, create a new class that extends this one, and update
+ * the list of default services in {@link bp_core_get_suggestions()}. If you're building a plugin,
+ * it's recommend that you use the `bp_suggestions_services` filter to do this. :)
+ *
+ * While the implementation of the query logic is left to you, it should be as quick and efficient
+ * as possible. When implementing the abstract methods in this class, pay close attention to the
+ * recommendations provided in the phpDoc blocks, particularly the expected return types.
+ *
+ * @since 2.1.0
+ */
+abstract class BP_Suggestions {
+
+	/**
+	 * Default arguments common to all suggestions services.
+	 *
+	 * If your custom service requires further defaults, add them here.
+	 *
+	 * @since 2.1.0
+	 * @var array
+	 */
+	protected $default_args = array(
+		'limit' => 16,
+		'term'  => '',
+		'type'  => '',
+	);
+
+	/**
+	 * Holds the arguments for the query (about to made to the suggestions service).
+	 *
+	 * This includes `$default_args`, as well as the user-supplied values.
+	 *
+	 * @since 2.1.0
+	 * @var array
+	 */
+	protected $args = array(
+	);
+
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $args Optional. If set, used as the parameters for the suggestions service query.
+	 */
+	public function __construct( array $args = array() ) {
+		if ( ! empty( $args ) ) {
+			$this->set_query( $args );
+		}
+	}
+
+	/**
+	 * Set the parameters for the suggestions service query.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $args {
+	 *     @type int    $limit Maximum number of results to display. Optional, default: 16.
+	 *     @type string $type  The name of the suggestion service to use for the request. Mandatory.
+	 *     @type string $term  The suggestion service will try to find results that contain this string.
+	 *                         Mandatory.
+	 * }
+	 */
+	public function set_query( array $args = array() ) {
+		$this->args = wp_parse_args( $args, $this->default_args );
+	}
+
+	/**
+	 * Validate and sanitise the parameters for the suggestion service query.
+	 *
+	 * Be sure to call this class' version of this method when implementing it in your own service.
+	 * If validation fails, you must return a WP_Error object.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return true|WP_Error If validation fails, return a WP_Error object. On success, return true (bool).
+	 */
+	public function validate() {
+		$this->args['limit'] = absint( $this->args['limit'] );
+		$this->args['term']  = trim( sanitize_text_field( $this->args['term'] ) );
+
+		/**
+		 * Filters the arguments to be validated for the BP_Suggestions query.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param BP_Suggestions $value Arguments to be validated.
+		 * @param BP_Suggestions $this  Current BP_Suggestions instance.
+		 */
+		$this->args          = apply_filters( 'bp_suggestions_args', $this->args, $this );
+
+		// Check for invalid or missing mandatory parameters.
+		if ( ! $this->args['limit'] || ! $this->args['term'] ) {
+			return new WP_Error( 'missing_parameter' );
+		}
+
+		// Check for blocked users (e.g. deleted accounts, or spammers).
+		if ( is_user_logged_in() && ! bp_is_user_active( get_current_user_id() ) ) {
+			return new WP_Error( 'invalid_user' );
+		}
+
+		/**
+		 * Filters the status of validation for the BP_Suggestions query.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param bool           $value Whether or not the values are valid.
+		 * @param BP_Suggestions $this  Current BP_Suggestions instance.
+		 */
+		return apply_filters( 'bp_suggestions_validate_args', true, $this );
+	}
+
+	/**
+	 * Find and return a list of suggestions that match the query.
+	 *
+	 * The return type is important. If no matches are found, an empty array must be returned.
+	 * Matches must be returned as objects in an array.
+	 *
+	 * The object format for each match must be: { 'ID': string, 'image': string, 'name': string }
+	 * For example: { 'ID': 'admin', 'image': 'http://example.com/logo.png', 'name': 'Name Surname' }
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return array|WP_Error Array of results. If there were problems, returns a WP_Error object.
+	 */
+	abstract public function get_suggestions();
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-theme-compat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-theme-compat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-theme-compat.php	(revision 12265)
@@ -0,0 +1,120 @@
+<?php
+/**
+ * BuddyPress Core Theme Compatibility Base Class.
+ *
+ * @package BuddyPress
+ * @subpackage ThemeCompatibility
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Theme Compatibility base class.
+ *
+ * This is only intended to be extended, and is included here as a basic guide
+ * for future Theme Packs to use. {@link BP_Legacy} is a good example of
+ * extending this class.
+ *
+ * @since 1.7.0
+ *
+ * @todo We should probably do something similar to BP_Component::start().
+ * @todo If this is only intended to be extended, it should be abstract.
+ *
+ * @param array $properties {
+ *     An array of properties describing the theme compat package.
+ *     @type string $id      ID of the package. Must be unique.
+ *     @type string $name    Name of the theme. This should match the name given
+ *                           in style.css.
+ *     @type string $version Theme version. Used for busting script and style
+ *                           browser caches.
+ *     @type string $dir     Filesystem path of the theme.
+ *     @type string $url     Base URL of the theme.
+ * }
+ */
+class BP_Theme_Compat {
+
+	/**
+	 * Template package properties, as passed to the constructor.
+	 *
+	 * @since 1.7.0
+	 * @var array
+	 */
+	protected $_data = array();
+
+	/**
+	 * Pass the $properties to the object on creation.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $properties Array of properties for BP_Theme_Compat.
+	 */
+	public function __construct( Array $properties = array() ) {
+		$this->_data = $properties;
+	}
+
+	/**
+	 * Set up the BuddyPress-specific theme compat methods.
+	 *
+	 * Themes should use this method in their constructor.
+	 *
+	 * @since 1.7.0
+	 */
+	protected function start() {
+		// Sanity check.
+		if ( ! bp_use_theme_compat_with_current_theme() ) {
+			return;
+		}
+
+		// Setup methods.
+		$this->setup_globals();
+		$this->setup_actions();
+	}
+
+	/**
+	 * Set up global data for your template package.
+	 *
+	 * Meant to be overridden in your class. See
+	 * {@link BP_Legacy::setup_globals()} for an example.
+	 *
+	 * @since 1.7.0
+	 */
+	protected function setup_globals() {}
+
+	/**
+	 * Set up theme hooks for your template package.
+	 *
+	 * Meant to be overridden in your class. See
+	 * {@link BP_Legacy::setup_actions()} for an example.
+	 *
+	 * @since 1.7.0
+	 */
+	protected function setup_actions() {}
+
+	/**
+	 * Set a theme's property.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $property Property name.
+	 * @param mixed  $value    Property value.
+	 * @return bool True on success, false on failure.
+	 */
+	public function __set( $property, $value ) {
+		return $this->_data[$property] = $value;
+	}
+
+	/**
+	 * Get a theme's property.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $property Property name.
+	 * @return mixed The value of the property if it exists, otherwise an
+	 *               empty string.
+	 */
+	public function __get( $property ) {
+		return array_key_exists( $property, $this->_data ) ? $this->_data[$property] : '';
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-user-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-user-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-user-query.php	(revision 12265)
@@ -0,0 +1,823 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress User Query class.
+ *
+ * Used for querying users in a BuddyPress context, in situations where WP_User_Query won't do the trick:
+ * Member directories, the Friends component, etc.
+ *
+ * @since 1.7.0
+ *
+ * @param array $query {
+ *     Query arguments. All items are optional.
+ *     @type string            $type                Determines sort order. Select from 'newest', 'active', 'online',
+ *                                                  'random', 'popular', 'alphabetical'. Default: 'newest'.
+ *     @type int               $per_page            Number of results to return. Default: 0 (no limit).
+ *     @type int               $page                Page offset (together with $per_page). Default: 1.
+ *     @type int               $user_id             ID of a user. If present, and if the friends component is activated,
+ *                                                  results will be limited to the friends of that user. Default: 0.
+ *     @type string|bool       $search_terms        Terms to search by. Search happens across xprofile fields. Requires
+ *                                                  XProfile component. Default: false.
+ *     @type string            $search_wildcard     When searching with $search_terms, set where wildcards around the
+ *                                                  term should be positioned. Accepts 'both', 'left', 'right'.
+ *                                                  Default: 'both'.
+ *     @type array|string|bool $include             An array or comma-separated list of user IDs to which query should
+ *                                                  be limited. Default: false.
+ *     @type array|string|bool $exclude             An array or comma-separated list of user IDs that will be excluded
+ *                                                  from query results. Default: false.
+ *     @type array|string|bool $user_ids            An array or comma-separated list of IDs corresponding to the users
+ *                                                  that should be returned. When this parameter is passed, it will
+ *                                                  override all others; BP User objects will be constructed using these
+ *                                                  IDs only. Default: false.
+ *     @type array|string      $member_type         Array or comma-separated list of member types to limit results to.
+ *     @type array|string      $member_type__in     Array or comma-separated list of member types to limit results to.
+ *     @type array|string      $member_type__not_in Array or comma-separated list of member types that will be
+ *			                                             excluded from results.
+ *     @type string|bool       $meta_key            Limit results to users that have usermeta associated with this meta_key.
+ *                                                  Usually used with $meta_value. Default: false.
+ *     @type string|bool       $meta_value          When used with $meta_key, limits results to users whose usermeta value
+ *                                                  associated with $meta_key matches $meta_value. Default: false.
+ *     @type array             $xprofile_query      Filter results by xprofile data. Requires the xprofile component.
+ *                                                  See {@see BP_XProfile_Query} for details.
+ *     @type bool              $populate_extras     True if you want to fetch extra metadata
+ *                                                  about returned users, such as total group and friend counts.
+ *     @type string            $count_total         Determines how BP_User_Query will do a count of total users matching
+ *                                                  the other filter criteria. Default value is 'count_query', which
+ *                                                  does a separate SELECT COUNT query to determine the total.
+ *                                                  'sql_count_found_rows' uses SQL_COUNT_FOUND_ROWS and
+ *                                                  SELECT FOUND_ROWS(). Pass an empty string to skip the total user
+ *                                                  count query.
+ * }
+ */
+class BP_User_Query {
+
+	/** Variables *************************************************************/
+
+	/**
+	 * Unaltered params as passed to the constructor.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	public $query_vars_raw = array();
+
+	/**
+	 * Array of variables to query with.
+	 *
+	 * @since 1.7.0
+	 * @var array
+	 */
+	public $query_vars = array();
+
+	/**
+	 * List of found users and their respective data.
+	 *
+	 * @since 1.7.0
+	 * @var array
+	 */
+	public $results = array();
+
+	/**
+	 * Total number of found users for the current query.
+	 *
+	 * @since 1.7.0
+	 * @var int
+	 */
+	public $total_users = 0;
+
+	/**
+	 * List of found user IDs.
+	 *
+	 * @since 1.7.0
+	 * @var array
+	 */
+	public $user_ids = array();
+
+	/**
+	 * SQL clauses for the user ID query.
+	 *
+	 * @since 1.7.0
+	 * @var array
+	 */
+	public $uid_clauses = array();
+
+	/**
+	 * SQL table where the user ID is being fetched from.
+	 *
+	 * @since 2.2.0
+	 * @var string
+	 */
+	public $uid_table = '';
+
+	/**
+	 * SQL database column name to order by.
+	 *
+	 * @since 1.7.0
+	 * @var string
+	 */
+	public $uid_name = '';
+
+	/**
+	 * Standard response when the query should not return any rows.
+	 *
+	 * @since 1.7.0
+	 * @var string
+	 */
+	protected $no_results = array( 'join' => '', 'where' => '0 = 1' );
+
+
+	/** Methods ***************************************************************/
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string|array|null $query See {@link BP_User_Query}.
+	 */
+	public function __construct( $query = null ) {
+
+		// Store the raw query vars for later access.
+		$this->query_vars_raw = $query;
+
+		// Allow extending classes to register action/filter hooks.
+		$this->setup_hooks();
+
+		if ( ! empty( $this->query_vars_raw ) ) {
+			$this->query_vars = wp_parse_args( $this->query_vars_raw, array(
+				'type'                => 'newest',
+				'per_page'            => 0,
+				'page'                => 1,
+				'user_id'             => 0,
+				'search_terms'        => false,
+				'search_wildcard'     => 'both',
+				'include'             => false,
+				'exclude'             => false,
+				'user_ids'            => false,
+				'member_type'         => '',
+				'member_type__in'     => '',
+				'member_type__not_in' => '',
+				'meta_key'            => false,
+				'meta_value'          => false,
+				'xprofile_query'      => false,
+				'populate_extras'     => true,
+				'count_total'         => 'count_query'
+			) );
+
+			/**
+			 * Fires before the construction of the BP_User_Query query.
+			 *
+			 * @since 1.7.0
+			 *
+			 * @param BP_User_Query $this Current instance of the BP_User_Query. Passed by reference.
+			 */
+			do_action_ref_array( 'bp_pre_user_query_construct', array( &$this ) );
+
+			// Get user ids
+			// If the user_ids param is present, we skip the query.
+			if ( false !== $this->query_vars['user_ids'] ) {
+				$this->user_ids = wp_parse_id_list( $this->query_vars['user_ids'] );
+			} else {
+				$this->prepare_user_ids_query();
+				$this->do_user_ids_query();
+			}
+		}
+
+		// Bail if no user IDs were found.
+		if ( empty( $this->user_ids ) ) {
+			return;
+		}
+
+		// Fetch additional data. First, using WP_User_Query.
+		$this->do_wp_user_query();
+
+		// Get BuddyPress specific user data.
+		$this->populate_extras();
+	}
+
+	/**
+	 * Allow extending classes to set up action/filter hooks.
+	 *
+	 * When extending BP_User_Query, you may need to use some of its
+	 * internal hooks to modify the output. It's not convenient to call
+	 * add_action() or add_filter() in your class constructor, because
+	 * BP_User_Query::__construct() contains a fair amount of logic that
+	 * you may not want to override in your class. Define this method in
+	 * your own class if you need a place where your extending class can
+	 * add its hooks early in the query-building process. See
+	 * {@link BP_Group_Member_Query::setup_hooks()} for an example.
+	 *
+	 * @since 1.8.0
+	 */
+	public function setup_hooks() {}
+
+	/**
+	 * Prepare the query for user_ids.
+	 *
+	 * @since 1.7.0
+	 */
+	public function prepare_user_ids_query() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// Default query variables used here.
+		$type         = '';
+		$per_page     = 0;
+		$page         = 1;
+		$user_id      = 0;
+		$include      = false;
+		$search_terms = false;
+		$exclude      = false;
+		$meta_key     = false;
+		$meta_value   = false;
+
+		extract( $this->query_vars );
+
+		// Setup the main SQL query container.
+		$sql = array(
+			'select'  => '',
+			'where'   => array('1=1'),
+			'orderby' => '',
+			'order'   => '',
+			'limit'   => ''
+		);
+
+		/* TYPE **************************************************************/
+
+		// Determines the sort order, which means it also determines where the
+		// user IDs are drawn from (the SELECT and WHERE statements).
+		switch ( $type ) {
+
+			// 'online' query happens against the last_activity usermeta key
+			// Filter 'bp_user_query_online_interval' to modify the
+			// number of minutes used as an interval.
+			case 'online' :
+				$this->uid_name = 'user_id';
+				$this->uid_table = $bp->members->table_name_last_activity;
+				$sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
+				$sql['where'][] = $wpdb->prepare( "u.component = %s AND u.type = 'last_activity'", buddypress()->members->id );
+
+				/**
+				 * Filters the threshold for activity timestamp minutes since to indicate online status.
+				 *
+				 * @since 1.8.0
+				 *
+				 * @param int $value Amount of minutes for threshold. Default 15.
+				 */
+				$sql['where'][] = $wpdb->prepare( "u.date_recorded >= DATE_SUB( UTC_TIMESTAMP(), INTERVAL %d MINUTE )", apply_filters( 'bp_user_query_online_interval', 15 ) );
+				$sql['orderby'] = "ORDER BY u.date_recorded";
+				$sql['order']   = "DESC";
+
+				break;
+
+			// 'active', 'newest', and 'random' queries
+			// all happen against the last_activity usermeta key.
+			case 'active' :
+			case 'newest' :
+			case 'random' :
+				$this->uid_name = 'user_id';
+				$this->uid_table = $bp->members->table_name_last_activity;
+				$sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
+				$sql['where'][] = $wpdb->prepare( "u.component = %s AND u.type = 'last_activity'", buddypress()->members->id );
+
+				if ( 'newest' == $type ) {
+					$sql['orderby'] = "ORDER BY u.user_id";
+					$sql['order'] = "DESC";
+				} elseif ( 'random' == $type ) {
+					$sql['orderby'] = "ORDER BY rand()";
+				} else {
+					$sql['orderby'] = "ORDER BY u.date_recorded";
+					$sql['order'] = "DESC";
+				}
+
+				break;
+
+			// 'popular' sorts by the 'total_friend_count' usermeta.
+			case 'popular' :
+				$this->uid_name = 'user_id';
+				$this->uid_table = $wpdb->usermeta;
+				$sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
+				$sql['where'][] = $wpdb->prepare( "u.meta_key = %s", bp_get_user_meta_key( 'total_friend_count' ) );
+				$sql['orderby'] = "ORDER BY CONVERT(u.meta_value, SIGNED)";
+				$sql['order']   = "DESC";
+
+				break;
+
+			// 'alphabetical' sorts depend on the xprofile setup.
+			case 'alphabetical' :
+
+				// We prefer to do alphabetical sorts against the display_name field
+				// of wp_users, because the table is smaller and better indexed. We
+				// can do so if xprofile sync is enabled, or if xprofile is inactive.
+				//
+				// @todo remove need for bp_is_active() check.
+				if ( ! bp_disable_profile_sync() || ! bp_is_active( 'xprofile' ) ) {
+					$this->uid_name = 'ID';
+					$this->uid_table = $wpdb->users;
+					$sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
+					$sql['orderby'] = "ORDER BY u.display_name";
+					$sql['order']   = "ASC";
+
+				// When profile sync is disabled, alphabetical sorts must happen against
+				// the xprofile table.
+				} else {
+					$this->uid_name = 'user_id';
+					$this->uid_table = $bp->profile->table_name_data;
+					$sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
+					$sql['where'][] = $wpdb->prepare( "u.field_id = %d", bp_xprofile_fullname_field_id() );
+					$sql['orderby'] = "ORDER BY u.value";
+					$sql['order']   = "ASC";
+				}
+
+				// Alphabetical queries ignore last_activity, while BP uses last_activity
+				// to infer spam/deleted/non-activated users. To ensure that these users
+				// are filtered out, we add an appropriate sub-query.
+				$sql['where'][] = "u.{$this->uid_name} IN ( SELECT ID FROM {$wpdb->users} WHERE " . bp_core_get_status_sql( '' ) . " )";
+
+				break;
+
+			// Any other 'type' falls through.
+			default :
+				$this->uid_name = 'ID';
+				$this->uid_table = $wpdb->users;
+				$sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
+
+				// In this case, we assume that a plugin is
+				// handling order, so we leave those clauses
+				// blank.
+				break;
+		}
+
+		/* WHERE *************************************************************/
+
+		// 'include' - User ids to include in the results.
+		$include     = false !== $include ? wp_parse_id_list( $include ) : array();
+		$include_ids = $this->get_include_ids( $include );
+
+		// An array containing nothing but 0 should always fail.
+		if ( 1 === count( $include_ids ) && 0 == reset( $include_ids ) ) {
+			$sql['where'][] = $this->no_results['where'];
+		} elseif ( ! empty( $include_ids ) ) {
+			$include_ids    = implode( ',', wp_parse_id_list( $include_ids ) );
+			$sql['where'][] = "u.{$this->uid_name} IN ({$include_ids})";
+		}
+
+		// 'exclude' - User ids to exclude from the results.
+		if ( false !== $exclude ) {
+			$exclude_ids    = implode( ',', wp_parse_id_list( $exclude ) );
+			$sql['where'][] = "u.{$this->uid_name} NOT IN ({$exclude_ids})";
+		}
+
+		// 'user_id' - When a user id is passed, limit to the friends of the user
+		// @todo remove need for bp_is_active() check.
+		if ( ! empty( $user_id ) && bp_is_active( 'friends' ) ) {
+			$friend_ids = friends_get_friend_user_ids( $user_id );
+			$friend_ids = implode( ',', wp_parse_id_list( $friend_ids ) );
+
+			if ( ! empty( $friend_ids ) ) {
+				$sql['where'][] = "u.{$this->uid_name} IN ({$friend_ids})";
+
+			// If the user has no friends, the query should always
+			// return no users.
+			} else {
+				$sql['where'][] = $this->no_results['where'];
+			}
+		}
+
+		/* Search Terms ******************************************************/
+
+		// 'search_terms' searches user_login and user_nicename
+		// xprofile field matches happen in bp_xprofile_bp_user_query_search().
+		if ( false !== $search_terms ) {
+			$search_terms = bp_esc_like( wp_kses_normalize_entities( $search_terms ) );
+
+			if ( $search_wildcard === 'left' ) {
+				$search_terms_nospace = '%' . $search_terms;
+				$search_terms_space   = '%' . $search_terms . ' %';
+			} elseif ( $search_wildcard === 'right' ) {
+				$search_terms_nospace =        $search_terms . '%';
+				$search_terms_space   = '% ' . $search_terms . '%';
+			} else {
+				$search_terms_nospace = '%' . $search_terms . '%';
+				$search_terms_space   = '%' . $search_terms . '%';
+			}
+
+			$matched_user_ids = $wpdb->get_col( $wpdb->prepare(
+				"SELECT ID FROM {$wpdb->users} WHERE ( user_login LIKE %s OR user_login LIKE %s OR user_nicename LIKE %s OR user_nicename LIKE %s )",
+				$search_terms_nospace,
+				$search_terms_space,
+				$search_terms_nospace,
+				$search_terms_space
+			) );
+
+			$match_in_clause = empty( $matched_user_ids) ? 'NULL' : implode( ',', $matched_user_ids );
+			$sql['where']['search'] = "u.{$this->uid_name} IN ({$match_in_clause})";
+		}
+
+		// Only use $member_type__in if $member_type is not set.
+		if ( empty( $member_type ) && ! empty( $member_type__in ) ) {
+			$member_type = $member_type__in;
+		}
+
+		// Member types to exclude. Note that this takes precedence over inclusions.
+		if ( ! empty( $member_type__not_in ) ) {
+			$member_type_clause = $this->get_sql_clause_for_member_types( $member_type__not_in, 'NOT IN' );
+
+		// Member types to include.
+		} elseif ( ! empty( $member_type ) ) {
+			$member_type_clause = $this->get_sql_clause_for_member_types( $member_type, 'IN' );
+		}
+
+		if ( ! empty( $member_type_clause ) ) {
+			$sql['where']['member_type'] = $member_type_clause;
+		}
+
+		// 'meta_key', 'meta_value' allow usermeta search
+		// To avoid global joins, do a separate query.
+		if ( false !== $meta_key ) {
+			$meta_sql = $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", $meta_key );
+
+			if ( false !== $meta_value ) {
+				$meta_sql .= $wpdb->prepare( " AND meta_value = %s", $meta_value );
+			}
+
+			$found_user_ids = $wpdb->get_col( $meta_sql );
+
+			if ( ! empty( $found_user_ids ) ) {
+				$sql['where'][] = "u.{$this->uid_name} IN (" . implode( ',', wp_parse_id_list( $found_user_ids ) ) . ")";
+			} else {
+				$sql['where'][] = '1 = 0';
+			}
+		}
+
+		// 'per_page', 'page' - handles LIMIT.
+		if ( !empty( $per_page ) && !empty( $page ) ) {
+			$sql['limit'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $per_page ), intval( $per_page ) );
+		} else {
+			$sql['limit'] = '';
+		}
+
+		/**
+		 * Filters the clauses for the user query.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array         $sql  Array of SQL clauses to be used in the query.
+		 * @param BP_User_Query $this Current BP_User_Query instance.
+		 */
+		$sql = apply_filters_ref_array( 'bp_user_query_uid_clauses', array( $sql, &$this ) );
+
+		// Assemble the query chunks.
+		$this->uid_clauses['select']  = $sql['select'];
+		$this->uid_clauses['where']   = ! empty( $sql['where'] ) ? 'WHERE ' . implode( ' AND ', $sql['where'] ) : '';
+		$this->uid_clauses['orderby'] = $sql['orderby'];
+		$this->uid_clauses['order']   = $sql['order'];
+		$this->uid_clauses['limit']   = $sql['limit'];
+
+		/**
+		 * Fires before the BP_User_Query query is made.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param BP_User_Query $this Current BP_User_Query instance. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_pre_user_query', array( &$this ) );
+	}
+
+	/**
+	 * Query for IDs of users that match the query parameters.
+	 *
+	 * Perform a database query to specifically get only user IDs, using
+	 * existing query variables set previously in the constructor.
+	 *
+	 * Also used to quickly perform user total counts.
+	 *
+	 * @since 1.7.0
+	 */
+	public function do_user_ids_query() {
+		global $wpdb;
+
+		// If counting using SQL_CALC_FOUND_ROWS, set it up here.
+		if ( 'sql_calc_found_rows' == $this->query_vars['count_total'] ) {
+			$this->uid_clauses['select'] = str_replace( 'SELECT', 'SELECT SQL_CALC_FOUND_ROWS', $this->uid_clauses['select'] );
+		}
+
+		// Get the specific user ids.
+		$this->user_ids = $wpdb->get_col( "{$this->uid_clauses['select']} {$this->uid_clauses['where']} {$this->uid_clauses['orderby']} {$this->uid_clauses['order']} {$this->uid_clauses['limit']}" );
+
+		// Get the total user count.
+		if ( 'sql_calc_found_rows' == $this->query_vars['count_total'] ) {
+
+			/**
+			 * Filters the found user SQL statements before query.
+			 *
+			 * If "sql_calc_found_rows" is the provided count_total query var
+			 * then the value will be "SELECT FOUND_ROWS()". Otherwise it will
+			 * use a "SELECT COUNT()" query statement.
+			 *
+			 * @since 1.7.0
+			 *
+			 * @param string        $value SQL statement to select FOUND_ROWS().
+			 * @param BP_User_Query $this  Current BP_User_Query instance.
+			 */
+			$this->total_users = $wpdb->get_var( apply_filters( 'bp_found_user_query', "SELECT FOUND_ROWS()", $this ) );
+		} elseif ( 'count_query' == $this->query_vars['count_total'] ) {
+			$count_select      = preg_replace( '/^SELECT.*?FROM (\S+) u/', "SELECT COUNT(u.{$this->uid_name}) FROM $1 u", $this->uid_clauses['select'] );
+
+			/** This filter is documented in bp-core/classes/class-bp-user-query.php */
+			$this->total_users = $wpdb->get_var( apply_filters( 'bp_found_user_query', "{$count_select} {$this->uid_clauses['where']}", $this ) );
+		}
+	}
+
+	/**
+	 * Use WP_User_Query() to pull data for the user IDs retrieved in the main query.
+	 *
+	 * @since 1.7.0
+	 */
+	public function do_wp_user_query() {
+		$fields = array( 'ID', 'user_login', 'user_pass', 'user_nicename', 'user_email', 'user_url', 'user_registered', 'user_activation_key', 'user_status', 'display_name' );
+
+		if ( is_multisite() ) {
+			$fields[] = 'spam';
+			$fields[] = 'deleted';
+		}
+
+		/**
+		 * Filters the WP User Query arguments before passing into the class.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array         $value Array of arguments for the user query.
+		 * @param BP_User_Query $this  Current BP_User_Query instance.
+		 */
+		$wp_user_query = new WP_User_Query( apply_filters( 'bp_wp_user_query_args', array(
+
+			// Relevant.
+			'fields'      => $fields,
+			'include'     => $this->user_ids,
+
+			// Overrides
+			'blog_id'     => 0,    // BP does not require blog roles.
+			'count_total' => false // We already have a count.
+
+		), $this ) );
+
+		// We calculate total_users using a standalone query, except
+		// when a whitelist of user_ids is passed to the constructor.
+		// This clause covers the latter situation, and ensures that
+		// pagination works when querying by $user_ids.
+		if ( empty( $this->total_users ) ) {
+			$this->total_users = count( $wp_user_query->results );
+		}
+
+		// Reindex for easier matching.
+		$r = array();
+		foreach ( $wp_user_query->results as $u ) {
+			$r[ $u->ID ] = $u;
+		}
+
+		// Match up to the user ids from the main query.
+		foreach ( $this->user_ids as $key => $uid ) {
+			if ( isset( $r[ $uid ] ) ) {
+				$r[ $uid ]->ID = (int) $uid;
+				$r[ $uid ]->user_status = (int) $r[ $uid ]->user_status;
+
+				$this->results[ $uid ] = $r[ $uid ];
+
+				// The BP template functions expect an 'id'
+				// (as opposed to 'ID') property.
+				$this->results[ $uid ]->id = (int) $uid;
+
+			// Remove user ID from original user_ids property.
+			} else {
+				unset( $this->user_ids[ $key ] );
+			}
+		}
+	}
+
+	/**
+	 * Fetch the IDs of users to put in the IN clause of the main query.
+	 *
+	 * By default, returns the value passed to it
+	 * ($this->query_vars['include']). Having this abstracted into a
+	 * standalone method means that extending classes can override the
+	 * logic, parsing together their own user_id limits with the 'include'
+	 * ids passed to the class constructor. See {@link BP_Group_Member_Query}
+	 * for an example.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $include Sanitized array of user IDs, as passed to the 'include'
+	 *                       parameter of the class constructor.
+	 * @return array The list of users to which the main query should be
+	 *               limited.
+	 */
+	public function get_include_ids( $include = array() ) {
+		return $include;
+	}
+
+	/**
+	 * Perform a database query to populate any extra metadata we might need.
+	 *
+	 * Different components will hook into the 'bp_user_query_populate_extras'
+	 * action to loop in the things they want.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @global WPDB $wpdb Global WordPress database access object.
+	 */
+	public function populate_extras() {
+		global $wpdb;
+
+		// Bail if no users.
+		if ( empty( $this->user_ids ) || empty( $this->results ) ) {
+			return;
+		}
+
+		// Bail if the populate_extras flag is set to false
+		// In the case of the 'popular' sort type, we force
+		// populate_extras to true, because we need the friend counts.
+		if ( 'popular' == $this->query_vars['type'] ) {
+			$this->query_vars['populate_extras'] = 1;
+		}
+
+		if ( ! (bool) $this->query_vars['populate_extras'] ) {
+			return;
+		}
+
+		// Turn user ID's into a query-usable, comma separated value.
+		$user_ids_sql = implode( ',', wp_parse_id_list( $this->user_ids ) );
+
+		/**
+		 * Allows users to independently populate custom extras.
+		 *
+		 * Note that anything you add here should query using $user_ids_sql, to
+		 * avoid running multiple queries per user in the loop.
+		 *
+		 * Two BuddyPress components currently do this:
+		 * - XProfile: To override display names.
+		 * - Friends:  To set whether or not a user is the current users friend.
+		 *
+		 * @see bp_xprofile_filter_user_query_populate_extras()
+		 * @see bp_friends_filter_user_query_populate_extras()
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param BP_User_Query $this         Current BP_User_Query instance.
+		 * @param string        $user_ids_sql Comma-separated string of user IDs.
+		 */
+		do_action_ref_array( 'bp_user_query_populate_extras', array( $this, $user_ids_sql ) );
+
+		// Fetch last_active data from the activity table.
+		$last_activities = BP_Core_User::get_last_activity( $this->user_ids );
+
+		// Set a last_activity value for each user, even if it's empty.
+		foreach ( $this->results as $user_id => $user ) {
+			$user_last_activity = isset( $last_activities[ $user_id ] ) ? $last_activities[ $user_id ]['date_recorded'] : '';
+			$this->results[ $user_id ]->last_activity = $user_last_activity;
+		}
+
+		// Fetch usermeta data
+		// We want the three following pieces of info from usermeta:
+		// - friend count
+		// - latest update.
+		$total_friend_count_key = bp_get_user_meta_key( 'total_friend_count' );
+		$bp_latest_update_key   = bp_get_user_meta_key( 'bp_latest_update'   );
+
+		// Total_friend_count must be set for each user, even if its
+		// value is 0.
+		foreach ( $this->results as $uindex => $user ) {
+			$this->results[$uindex]->total_friend_count = 0;
+		}
+
+		// Create, prepare, and run the separate usermeta query.
+		$user_metas = $wpdb->get_results( $wpdb->prepare( "SELECT user_id, meta_key, meta_value FROM {$wpdb->usermeta} WHERE meta_key IN (%s,%s) AND user_id IN ({$user_ids_sql})", $total_friend_count_key, $bp_latest_update_key ) );
+
+		// The $members_template global expects the index key to be different
+		// from the meta_key in some cases, so we rejig things here.
+		foreach ( $user_metas as $user_meta ) {
+			switch ( $user_meta->meta_key ) {
+				case $total_friend_count_key :
+					$key = 'total_friend_count';
+					break;
+
+				case $bp_latest_update_key :
+					$key = 'latest_update';
+					break;
+			}
+
+			if ( isset( $this->results[ $user_meta->user_id ] ) ) {
+				$this->results[ $user_meta->user_id ]->{$key} = $user_meta->meta_value;
+			}
+		}
+
+		// When meta_key or meta_value have been passed to the query,
+		// fetch the resulting values for use in the template functions.
+		if ( ! empty( $this->query_vars['meta_key'] ) ) {
+			$meta_sql = array(
+				'select' => "SELECT user_id, meta_key, meta_value",
+				'from'   => "FROM $wpdb->usermeta",
+				'where'  => $wpdb->prepare( "WHERE meta_key = %s", $this->query_vars['meta_key'] )
+			);
+
+			if ( false !== $this->query_vars['meta_value'] ) {
+				$meta_sql['where'] .= $wpdb->prepare( " AND meta_value = %s", $this->query_vars['meta_value'] );
+			}
+
+			$metas = $wpdb->get_results( "{$meta_sql['select']} {$meta_sql['from']} {$meta_sql['where']}" );
+
+			if ( ! empty( $metas ) ) {
+				foreach ( $metas as $meta ) {
+					if ( isset( $this->results[ $meta->user_id ] ) ) {
+						$this->results[ $meta->user_id ]->meta_key = $meta->meta_key;
+
+						if ( ! empty( $meta->meta_value ) ) {
+							$this->results[ $meta->user_id ]->meta_value = $meta->meta_value;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Get a SQL clause representing member_type include/exclusion.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string|array $member_types Array or comma-separated list of member types.
+	 * @param string       $operator     'IN' or 'NOT IN'.
+	 * @return string
+	 */
+	protected function get_sql_clause_for_member_types( $member_types, $operator ) {
+		global $wpdb;
+
+		// Sanitize.
+		if ( 'NOT IN' !== $operator ) {
+			$operator = 'IN';
+		}
+
+		// Parse and sanitize types.
+		if ( ! is_array( $member_types ) ) {
+			$member_types = preg_split( '/[,\s+]/', $member_types );
+		}
+
+		$types = array();
+		foreach ( $member_types as $mt ) {
+			if ( bp_get_member_type_object( $mt ) ) {
+				$types[] = $mt;
+			}
+		}
+
+		$tax_query = new WP_Tax_Query( array(
+			array(
+				'taxonomy' => bp_get_member_type_tax_name(),
+				'field'    => 'name',
+				'operator' => $operator,
+				'terms'    => $types,
+			),
+		) );
+
+		// Switch to the root blog, where member type taxonomies live.
+		$site_id  = bp_get_taxonomy_term_site_id( bp_get_member_type_tax_name() );
+		$switched = false;
+		if ( $site_id !== get_current_blog_id() ) {
+			switch_to_blog( $site_id );
+			$switched = true;
+		}
+
+		$sql_clauses = $tax_query->get_sql( 'u', $this->uid_name );
+
+		$clause = '';
+
+		// The no_results clauses are the same between IN and NOT IN.
+		if ( false !== strpos( $sql_clauses['where'], '0 = 1' ) ) {
+			$clause = $this->no_results['where'];
+
+		// The tax_query clause generated for NOT IN can be used almost as-is. We just trim the leading 'AND'.
+		} elseif ( 'NOT IN' === $operator ) {
+			$clause = preg_replace( '/^\s*AND\s*/', '', $sql_clauses['where'] );
+
+		// IN clauses must be converted to a subquery.
+		} elseif ( preg_match( '/' . $wpdb->term_relationships . '\.term_taxonomy_id IN \([0-9, ]+\)/', $sql_clauses['where'], $matches ) ) {
+			$clause = "u.{$this->uid_name} IN ( SELECT object_id FROM $wpdb->term_relationships WHERE {$matches[0]} )";
+		}
+
+		if ( $switched ) {
+			restore_current_blog();
+		}
+
+		return $clause;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-category-checklist.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-category-checklist.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-category-checklist.php	(revision 12265)
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Walker class to output an unordered list of category checkbox input elements.
+ *
+ * A modification of WordPress 4.4.1's Walker_Category_Checklist class which prints term description
+ * instead of term name, and makes it work for non-hierarchical taxonomys. Some lines have undergone
+ * slight adjustment to meet modern coding standards, but any improvements should be contributed upstream.
+ *
+ * @since 2.5.0
+ */
+class BP_Walker_Category_Checklist extends Walker {
+	public $tree_type = 'category';
+	public $db_fields = array( 'parent' => 'parent', 'id' => 'term_id' );
+
+	/**
+	 * Starts the list before the elements are added.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $output Passed by reference. Used to append additional content.
+	 * @param int    $depth  Depth of category. Used for tab indentation.
+	 * @param array  $args   An array of arguments.
+	 */
+	public function start_lvl( &$output, $depth = 0, $args = array() ) {
+		$indent  = str_repeat( "\t", $depth );
+		$output .= "$indent<ul class='children'>\n";
+	}
+
+	/**
+	 * Ends the list of after the elements are added.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $output Passed by reference. Used to append additional content.
+	 * @param int    $depth  Depth of category. Used for tab indentation.
+	 * @param array  $args   An array of arguments.
+	 */
+	public function end_lvl( &$output, $depth = 0, $args = array() ) {
+		$indent = str_repeat( "\t", $depth );
+		$output .= "$indent</ul>\n";
+	}
+
+	/**
+	 * Start the element output.
+	 *
+	 * @param string $output   Passed by reference. Used to append additional content.
+	 * @param object $category The current term object.
+	 * @param int    $depth    Depth of the term in reference to parents. Default 0.
+	 * @param array  $args     An array of arguments.
+	 * @param int    $id       ID of the current term.
+	 */
+	public function start_el( &$output, $category, $depth = 0, $args = array(), $id = 0 ) {
+		if ( empty( $args['taxonomy'] ) ) {
+			$taxonomy = 'category';
+		} else {
+			$taxonomy = $args['taxonomy'];
+		}
+
+		if ( $taxonomy == 'category' ) {
+			$name = 'post_category';
+		} else {
+			$name = 'tax_input[' . $taxonomy . ']';
+		}
+
+		$args['popular_cats'] = empty( $args['popular_cats'] ) ? array() : $args['popular_cats'];
+		$class = in_array( $category->term_id, $args['popular_cats'] ) ? ' class="popular-category"' : '';
+
+		$args['selected_cats'] = empty( $args['selected_cats'] ) ? array() : $args['selected_cats'];
+
+		if ( ! empty( $args['list_only'] ) ) {
+			$aria_cheched = 'false';
+			$inner_class = 'category';
+
+			if ( in_array( $category->term_id, $args['selected_cats'] ) ) {
+				$inner_class .= ' selected';
+				$aria_cheched = 'true';
+			}
+
+			/** This filter is documented in wp-includes/category-template.php */
+			$output .= "\n" . '<li' . $class . '>' .
+				'<div class="' . $inner_class . '" data-term-id=' . $category->term_id .
+				' tabindex="0" role="checkbox" aria-checked="' . $aria_cheched . '">' .
+				esc_html( apply_filters( 'the_category', $category->description ) ) . '</div>';
+		} else {
+			/** This filter is documented in wp-includes/category-template.php */
+			$output .= "\n<li id='{$taxonomy}-{$category->term_id}'$class>" .
+				'<label for="in-'.$taxonomy.'-' . $category->term_id . '" class="selectit"><input value="' . $category->slug . '" type="checkbox" name="'.$name.'[]" id="in-'.$taxonomy.'-' . $category->term_id . '"' .
+				checked( in_array( $category->term_id, $args['selected_cats'] ), true, false ) .
+				disabled( empty( $args['disabled'] ), false, false ) . ' /> ' .
+				esc_html( apply_filters( 'the_category', $category->description ) ) . '</label>';
+		}
+	}
+
+	/**
+	 * Ends the element output, if needed.
+	 *
+	 * @see Walker::end_el()
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $output   Passed by reference. Used to append additional content.
+	 * @param object $category The current term object.
+	 * @param int    $depth    Depth of the term in reference to parents. Default 0.
+	 * @param array  $args     An array of arguments. @see wp_terms_checklist()
+	 */
+	public function end_el( &$output, $category, $depth = 0, $args = array() ) {
+		$output .= "</li>\n";
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-nav-menu-checklist.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-nav-menu-checklist.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-nav-menu-checklist.php	(revision 12265)
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Create a set of BuddyPress-specific links for use in the Menus admin UI.
+ *
+ * Borrowed heavily from {@link Walker_Nav_Menu_Checklist}, but modified so as not
+ * to require an actual post type or taxonomy, and to force certain CSS classes.
+ *
+ * @since 1.9.0
+ */
+class BP_Walker_Nav_Menu_Checklist extends Walker_Nav_Menu {
+
+	/**
+	 * Constructor.
+	 *
+	 * @see Walker_Nav_Menu::__construct() for a description of parameters.
+	 *
+	 * @param array|bool $fields See {@link Walker_Nav_Menu::__construct()}.
+	 */
+	public function __construct( $fields = false ) {
+		if ( $fields ) {
+			$this->db_fields = $fields;
+		}
+	}
+
+	/**
+	 * Create the markup to start a tree level.
+	 *
+	 * @see Walker_Nav_Menu::start_lvl() for description of parameters.
+	 *
+	 * @param string $output See {@Walker_Nav_Menu::start_lvl()}.
+	 * @param int    $depth  See {@Walker_Nav_Menu::start_lvl()}.
+	 * @param array  $args   See {@Walker_Nav_Menu::start_lvl()}.
+	 */
+	public function start_lvl( &$output, $depth = 0, $args = array() ) {
+		$indent = str_repeat( "\t", $depth );
+		$output .= "\n$indent<ul class='children'>\n";
+	}
+
+	/**
+	 * Create the markup to end a tree level.
+	 *
+	 * @see Walker_Nav_Menu::end_lvl() for description of parameters.
+	 *
+	 * @param string $output See {@Walker_Nav_Menu::end_lvl()}.
+	 * @param int    $depth  See {@Walker_Nav_Menu::end_lvl()}.
+	 * @param array  $args   See {@Walker_Nav_Menu::end_lvl()}.
+	 */
+	public function end_lvl( &$output, $depth = 0, $args = array() ) {
+		$indent = str_repeat( "\t", $depth );
+		$output .= "\n$indent</ul>";
+	}
+
+	/**
+	 * Create the markup to start an element.
+	 *
+	 * @see Walker::start_el() for description of parameters.
+	 *
+	 * @param string       $output Passed by reference. Used to append additional
+	 *                             content.
+	 * @param object       $item   Menu item data object.
+	 * @param int          $depth  Depth of menu item. Used for padding.
+	 * @param object|array $args   See {@Walker::start_el()}.
+	 * @param int          $id     See {@Walker::start_el()}.
+	 */
+	function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
+		global $_nav_menu_placeholder;
+
+		$_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? intval($_nav_menu_placeholder) - 1 : -1;
+		$possible_object_id = isset( $item->post_type ) && 'nav_menu_item' == $item->post_type ? $item->object_id : $_nav_menu_placeholder;
+		$possible_db_id = ( ! empty( $item->ID ) ) && ( 0 < $possible_object_id ) ? (int) $item->ID : 0;
+
+		$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
+
+		$output .= $indent . '<li>';
+		$output .= '<label class="menu-item-title">';
+		$output .= '<input type="checkbox" class="menu-item-checkbox';
+
+		if ( property_exists( $item, 'label' ) ) {
+			$title = $item->label;
+		}
+
+		$output .= '" name="menu-item[' . $possible_object_id . '][menu-item-object-id]" value="'. esc_attr( $item->object_id ) .'" /> ';
+		$output .= isset( $title ) ? esc_html( $title ) : esc_html( $item->title );
+		$output .= '</label>';
+
+		if ( empty( $item->url ) ) {
+			$item->url = $item->guid;
+		}
+
+		if ( ! in_array( array( 'bp-menu', 'bp-'. $item->post_excerpt .'-nav' ), $item->classes ) ) {
+			$item->classes[] = 'bp-menu';
+			$item->classes[] = 'bp-'. $item->post_excerpt .'-nav';
+		}
+
+		// Menu item hidden fields.
+		$output .= '<input type="hidden" class="menu-item-db-id" name="menu-item[' . $possible_object_id . '][menu-item-db-id]" value="' . $possible_db_id . '" />';
+		$output .= '<input type="hidden" class="menu-item-object" name="menu-item[' . $possible_object_id . '][menu-item-object]" value="'. esc_attr( $item->object ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-parent-id" name="menu-item[' . $possible_object_id . '][menu-item-parent-id]" value="'. esc_attr( $item->menu_item_parent ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-type" name="menu-item[' . $possible_object_id . '][menu-item-type]" value="custom" />';
+		$output .= '<input type="hidden" class="menu-item-title" name="menu-item[' . $possible_object_id . '][menu-item-title]" value="'. esc_attr( $item->title ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-url" name="menu-item[' . $possible_object_id . '][menu-item-url]" value="'. esc_attr( $item->url ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-target" name="menu-item[' . $possible_object_id . '][menu-item-target]" value="'. esc_attr( $item->target ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-attr_title" name="menu-item[' . $possible_object_id . '][menu-item-attr_title]" value="'. esc_attr( $item->attr_title ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-classes" name="menu-item[' . $possible_object_id . '][menu-item-classes]" value="'. esc_attr( implode( ' ', $item->classes ) ) .'" />';
+		$output .= '<input type="hidden" class="menu-item-xfn" name="menu-item[' . $possible_object_id . '][menu-item-xfn]" value="'. esc_attr( $item->xfn ) .'" />';
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-nav-menu.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-nav-menu.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/classes/class-bp-walker-nav-menu.php	(revision 12265)
@@ -0,0 +1,213 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Create HTML list of BP nav items.
+ *
+ * @since 1.7.0
+ */
+class BP_Walker_Nav_Menu extends Walker_Nav_Menu {
+
+	/**
+	 * Description of fields indexes for building markup.
+	 *
+	 * @since 1.7.0
+	 * @var array
+	 */
+	var $db_fields = array( 'id' => 'css_id', 'parent' => 'parent' );
+
+	/**
+	 * Tree type.
+	 *
+	 * @since 1.7.0
+	 * @var string
+	 */
+	var $tree_type = array();
+
+	/**
+	 * Display array of elements hierarchically.
+	 *
+	 * This method is almost identical to the version in {@link Walker::walk()}.
+	 * The only change is on one line which has been commented. An IF was
+	 * comparing 0 to a non-empty string which was preventing child elements
+	 * being grouped under their parent menu element.
+	 *
+	 * This caused a problem for BuddyPress because our primary/secondary
+	 * navigations don't have a unique numerical ID that describes a
+	 * hierarchy (we use a slug). Obviously, WordPress Menus use Posts, and
+	 * those have ID/post_parent.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @see Walker::walk()
+	 *
+	 * @param array $elements  See {@link Walker::walk()}.
+	 * @param int   $max_depth See {@link Walker::walk()}.
+	 * @return string See {@link Walker::walk()}.
+	 */
+	public function walk( $elements, $max_depth ) {
+		$args   = array_slice( func_get_args(), 2 );
+		$output = '';
+
+		if ( $max_depth < -1 ) // Invalid parameter.
+			return $output;
+
+		if ( empty( $elements ) ) // Nothing to walk.
+			return $output;
+
+		$parent_field = $this->db_fields['parent'];
+
+		// Flat display.
+		if ( -1 == $max_depth ) {
+
+			$empty_array = array();
+			foreach ( $elements as $e )
+				$this->display_element( $e, $empty_array, 1, 0, $args, $output );
+
+			return $output;
+		}
+
+		/*
+		 * Need to display in hierarchical order
+		 * separate elements into two buckets: top level and children elements
+		 * children_elements is two dimensional array, eg.
+		 * children_elements[10][] contains all sub-elements whose parent is 10.
+		 */
+		$top_level_elements = array();
+		$children_elements  = array();
+
+		foreach ( $elements as $e ) {
+			// BuddyPress: changed '==' to '==='. This is the only change from version in Walker::walk().
+			if ( 0 === $e->$parent_field )
+				$top_level_elements[] = $e;
+			else
+				$children_elements[$e->$parent_field][] = $e;
+		}
+
+		/*
+		 * When none of the elements is top level
+		 * assume the first one must be root of the sub elements.
+		 */
+		if ( empty( $top_level_elements ) ) {
+
+			$first              = array_slice( $elements, 0, 1 );
+			$root               = $first[0];
+			$top_level_elements = array();
+			$children_elements  = array();
+
+			foreach ( $elements as $e ) {
+				if ( $root->$parent_field == $e->$parent_field )
+					$top_level_elements[] = $e;
+				else
+					$children_elements[$e->$parent_field][] = $e;
+			}
+		}
+
+		foreach ( $top_level_elements as $e )
+			$this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
+
+		/*
+		 * If we are displaying all levels, and remaining children_elements is not empty,
+		 * then we got orphans, which should be displayed regardless.
+		 */
+		if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
+			$empty_array = array();
+
+			foreach ( $children_elements as $orphans )
+				foreach ( $orphans as $op )
+					$this->display_element( $op, $empty_array, 1, 0, $args, $output );
+		 }
+
+		 return $output;
+	}
+
+	/**
+	 * Display the current <li> that we are on.
+	 *
+	 * @see Walker::start_el() for complete description of parameters.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $output Passed by reference. Used to append
+	 *                       additional content.
+	 * @param object $item   Menu item data object.
+	 * @param int    $depth  Depth of menu item. Used for padding. Optional,
+	 *                       defaults to 0.
+	 * @param array  $args   Optional. See {@link Walker::start_el()}.
+	 * @param int    $id     Menu item ID. Optional.
+	 */
+	public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
+		// If we're someway down the tree, indent the HTML with the appropriate number of tabs.
+		$indent = $depth ? str_repeat( "\t", $depth ) : '';
+
+		/**
+		 * Filters the classes to be added to the nav menu markup.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array  $value Array of classes to be added.
+		 * @param object $item  Menu item data object.
+		 * @param array  $args  Array of arguments for the item.
+		 */
+		$class_names = join( ' ', apply_filters( 'bp_nav_menu_css_class', array_filter( $item->class ), $item, $args ) );
+		$class_names = ! empty( $class_names ) ? ' class="' . esc_attr( $class_names ) . '"' : '';
+
+		// Add HTML ID
+		$id = sanitize_html_class( $item->css_id . '-personal-li' );  // Backpat with BP pre-1.7.
+
+		/**
+		 * Filters the value to be used for the nav menu ID attribute.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $id   ID attribute to be added to the menu item.
+		 * @param object $item Menu item data object.
+		 * @param array  $args Array of arguments for the item.
+		 */
+		$id = apply_filters( 'bp_nav_menu_item_id', $id, $item, $args );
+		$id = ! empty( $id ) ? ' id="' . esc_attr( $id ) . '"' : '';
+
+		// Opening tag; closing tag is handled in Walker_Nav_Menu::end_el().
+		$output .= $indent . '<li' . $id . $class_names . '>';
+
+		// Add href attribute.
+		$attributes = ! empty( $item->link ) ? ' href="' . esc_url( $item->link ) . '"' : '';
+
+		// Construct the link.
+		$item_output = $args->before;
+		$item_output .= '<a' . $attributes . '>';
+
+		/**
+		 * Filters the link text to be added to the item output.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $name  Item text to be applied.
+		 * @param int    $value Post ID the title is for.
+		 */
+		$item_output .= $args->link_before . apply_filters( 'the_title', $item->name, 0 ) . $args->link_after;
+		$item_output .= '</a>';
+		$item_output .= $args->after;
+
+		/**
+		 * Filters the final result for the menu item.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $item_output Constructed output for the menu item to append to output.
+		 * @param object $item        Menu item data object.
+		 * @param int    $depth       Depth of menu item. Used for padding.
+		 * @param array  $args        Array of arguments for the item.
+		 */
+		$output .= apply_filters( 'bp_walker_nav_menu_start_el', $item_output, $item, $depth, $args );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/css/admin-bar-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/css/admin-bar-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/css/admin-bar-rtl.css	(revision 12265)
@@ -0,0 +1,70 @@
+/* Displayed User */
+#wpadminbar .quicklinks li#wp-admin-bar-user-admin-with-avatar > a img,
+#wpadminbar .quicklinks li#wp-admin-bar-group-admin-with-avatar > a img {
+	width: 16px;
+	height: 16px;
+	display: inline;
+	border: 1px solid #999;
+	vertical-align: middle;
+	margin: -2px -5px 0 10px;
+	padding: 0;
+	background: #eee;
+	float: none;
+}
+
+/* Displayed Group */
+#wpadminbar .quicklinks li#wp-admin-bar-group-admin-with-avatar ul {
+	right: 0;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-group-admin-with-avatar ul ul {
+	right: 0;
+}
+
+/* Notifications */
+#wpadminbar .quicklinks li#wp-admin-bar-my-account a span.count,
+#wpadminbar .quicklinks li#wp-admin-bar-my-account-with-avatar a span.count,
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications #ab-pending-notifications {
+	background: #21759b;
+	color: #fff;
+	text-shadow: none;
+	display: inline;
+	padding: 2px 5px;
+	font-size: 10px;
+	font-weight: 700;
+	border-radius: 10px;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications #ab-pending-notifications {
+	background: #ddd;
+	color: #333;
+	margin: 0;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications #ab-pending-notifications.alert {
+	background-color: #21759b;
+	color: #fff;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications > a {
+	padding: 0 0.5em;
+}
+
+#wp-admin-bar-user-info img.avatar {
+	height: 64px;
+	width: 64px;
+}
+
+/* Remove additional arrows from appearing on the BuddyPress' `my_account_menu`
+ * on large screens. Add back arrows for screen widths 600px and lower.
+ */
+#wpadminbar .wp-admin-bar-arrow-right {
+	display: none;
+}
+
+@media screen and (max-width: 600px) {
+
+	#wpadminbar .wp-admin-bar-arrow-right {
+		display: block;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/css/admin-bar.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/css/admin-bar.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/css/admin-bar.css	(revision 12265)
@@ -0,0 +1,70 @@
+/* Displayed User */
+#wpadminbar .quicklinks li#wp-admin-bar-user-admin-with-avatar > a img,
+#wpadminbar .quicklinks li#wp-admin-bar-group-admin-with-avatar > a img {
+	width: 16px;
+	height: 16px;
+	display: inline;
+	border: 1px solid #999;
+	vertical-align: middle;
+	margin: -2px 10px 0 -5px;
+	padding: 0;
+	background: #eee;
+	float: none;
+}
+
+/* Displayed Group */
+#wpadminbar .quicklinks li#wp-admin-bar-group-admin-with-avatar ul {
+	left: 0;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-group-admin-with-avatar ul ul {
+	left: 0;
+}
+
+/* Notifications */
+#wpadminbar .quicklinks li#wp-admin-bar-my-account a span.count,
+#wpadminbar .quicklinks li#wp-admin-bar-my-account-with-avatar a span.count,
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications #ab-pending-notifications {
+	background: #21759b;
+	color: #fff;
+	text-shadow: none;
+	display: inline;
+	padding: 2px 5px;
+	font-size: 10px;
+	font-weight: 700;
+	border-radius: 10px;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications #ab-pending-notifications {
+	background: #ddd;
+	color: #333;
+	margin: 0;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications #ab-pending-notifications.alert {
+	background-color: #21759b;
+	color: #fff;
+}
+
+#wpadminbar .quicklinks li#wp-admin-bar-bp-notifications > a {
+	padding: 0 0.5em;
+}
+
+#wp-admin-bar-user-info img.avatar {
+	height: 64px;
+	width: 64px;
+}
+
+/* Remove additional arrows from appearing on the BuddyPress' `my_account_menu`
+ * on large screens. Add back arrows for screen widths 600px and lower.
+ */
+#wpadminbar .wp-admin-bar-arrow-right {
+	display: none;
+}
+
+@media screen and (max-width: 600px) {
+
+	#wpadminbar .wp-admin-bar-arrow-right {
+		display: block;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/css/avatar-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/css/avatar-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/css/avatar-rtl.css	(revision 12265)
@@ -0,0 +1,412 @@
+div.bp-avatar-status,
+div.bp-cover-image-status {
+	clear: both;
+	margin: 1em 0;
+}
+
+div.bp-avatar-status p.updated,
+div.bp-cover-image-status p.updated {
+	display: block;
+	padding: 10px 15px;
+}
+
+div.bp-avatar-status p.success,
+div.bp-cover-image-status p.success {
+	background-color: #efc;
+	border: 1px solid #591;
+	color: #250;
+}
+
+div.bp-avatar-status p.error,
+div.bp-cover-image-status p.error {
+	background-color: #fdc;
+	border: 1px solid #a00;
+	color: #800;
+}
+
+div.bp-avatar-status .bp-progress,
+div.bp-cover-image-status .bp-progress {
+	background: none;
+	border: 1px solid #d1d1d1;
+	float: left;
+	height: 22px;
+	line-height: 2;
+	margin: 6px 0 0 10px;
+	margin-bottom: 2px;
+	padding: 0;
+	overflow: hidden;
+	width: 200px;
+}
+
+div.bp-avatar-status .bp-bar,
+div.bp-cover-image-status .bp-bar {
+	background-color: #c3ff88;
+	width: 0;
+	height: 100%;
+	z-index: 9;
+}
+
+.bp-uploader-progress div.error {
+	background-color: #fdc;
+	border: 1px solid #a00;
+	color: #800;
+	display: block;
+	font-size: 90%;
+	padding: 10px 15px;
+}
+
+/* stylelint-disable selector-id-pattern */
+#buddypress p.warning,
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning {
+	background-color: #ffd;
+	border: 1px solid #cb2;
+	color: #440;
+	display: block;
+	font-size: 90%;
+	margin: 1em 0;
+	padding: 10px 15px;
+}
+/* stylelint-enable */
+
+div.bp-avatar-nav {
+	background: transparent;
+	clear: both;
+	margin: 10px 0;
+	overflow: hidden;
+}
+
+.avatar-nav-items {
+	margin: 0;
+	padding: 0;
+}
+
+.bp-avatar-nav .avatar-nav-items li.avatar-nav-item {
+	float: right !important;
+	margin: 0;
+	list-style: none;
+}
+
+.avatar-nav-items li a {
+	display: block;
+	padding: 5px 10px;
+	text-decoration: none;
+}
+
+.bp-avatar-nav	ul:before,
+.bp-avatar-nav	ul:after {
+	content: " ";
+	display: table;
+}
+
+.bp-avatar-nav	ul:after {
+	clear: both;
+}
+
+.bp-avatar-nav	ul {
+	border-bottom: 1px solid #ccc;
+	margin-bottom: 10px;
+}
+
+.bp-avatar-nav	ul.avatar-nav-items li.current {
+	border: 1px solid #ccc;
+	border-bottom-color: #fff;
+	border-top-right-radius: 4px;
+	border-top-left-radius: 4px;
+	margin-bottom: -1px;
+}
+
+.bp-avatar-nav	li.current a {
+	background: none;
+	color: inherit;
+	font-weight: 700;
+	opacity: 0.8;
+	outline: 0;
+}
+
+#drag-drop-area {
+	border: 4px dashed #bbb;
+	height: 200px;
+}
+
+.drag-drop.drag-over #drag-drop-area {
+	border-color: #83b4d8;
+}
+
+.drag-drop-inside p {
+	display: none;
+}
+
+.drag-drop-inside p.drag-drop-buttons {
+	margin-top: 80px;
+	text-align: center;
+}
+
+.drag-drop .drag-drop-inside p.drag-drop-buttons {
+	margin: auto;
+	text-align: inherit;
+}
+
+.moxie-shim.moxie-shim-html5 input {
+	cursor: pointer;
+}
+
+.drag-drop #drag-drop-area {
+	box-sizing: border-box;
+	display: table;
+	height: 100%;
+	width: 100%;
+}
+
+.drag-drop .drag-drop-inside {
+	display: table-cell;
+	padding: 40px 0;
+	text-align: center;
+	vertical-align: middle;
+}
+
+.drag-drop .drag-drop-inside p,
+.drag-drop-inside p.drag-drop-buttons {
+	display: block;
+}
+
+.drag-drop .drag-drop-inside p {
+	color: #767676;
+	font-size: 110%;
+	margin: 5px 0;
+	text-align: center;
+}
+
+.drag-drop-inside p.drag-drop-info {
+	margin-top: 0;
+}
+
+#avatar-to-crop {
+	margin: 0 auto 20px;
+	text-align: right;
+}
+
+#bp-webcam-avatar #avatar-to-crop {
+	float: right;
+	margin: 0 0 20px;
+}
+
+#avatar-to-crop .jcrop-holder {
+	margin: 0 auto;
+}
+
+.avatar-crop-management {
+	clear: right;
+	overflow: hidden;
+	padding-top: 20px;
+	text-align: center;
+}
+
+#bp-webcam-avatar .avatar-crop-management {
+	clear: none;
+	float: none;
+	overflow: visible;
+	padding-top: 0;
+	width: auto;
+}
+
+#avatar-crop-pane {
+	margin: 0 auto;
+	overflow: hidden;
+}
+
+#bp-webcam-avatar #avatar-to-crop {
+	border: 1px solid #eee;
+	max-width: 100%;
+	width: 100%;
+}
+
+@media screen and (min-width: 801px) {
+
+	#bp-webcam-avatar #avatar-to-crop {
+		max-width: 64%;
+		width: 64%;
+	}
+}
+
+#avatar-crop-actions a {
+	display: block;
+}
+
+#bp-webcam-avatar #avatar-crop-actions {
+	float: right;
+	margin: 0 0 20px;
+	width: 50%;
+}
+
+#avatar-crop-actions a.button {
+	margin-top: 10px;
+}
+
+#bp-webcam-avatar #avatar-crop-actions a.button {
+	display: block;
+	margin: 0 0 5px;
+	padding: 4px 0;
+	width: 100%;
+}
+
+#avatar-to-crop img,
+#avatar-crop-pane img,
+#avatar-crop-pane canvas,
+#avatar-upload-form img,
+#create-group-form img,
+#group-settings-form img {
+	border: none !important;
+	max-width: none !important;
+}
+
+#bp-webcam-avatar video {
+	float: right;
+	margin-bottom: 0;
+	max-width: 100%;
+	width: 100%;
+	transform: scaleX(-1);
+}
+
+#bp-webcam-avatar #avatar-crop-pane {
+	border: 2px dashed #bbb;
+	clear: right;
+	float: left;
+	margin: 0  0 10px 40px;
+	overflow: hidden;
+}
+
+/**
+*  Users can define avatar constants with new dimension values
+*  If they do, we'll ensure the layouts preserved by enforcing a max width
+*  that matches bp default settings.
+*/
+#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+	max-width: 150px;
+	max-height: 150px;
+}
+
+#avatar-crop-pane canvas {
+	height: auto;
+	width: 100%;
+	max-width: 100%;
+}
+
+.group-avatar .bp-avatar .avatar-crop-management {
+	margin-right: 0;
+	padding-top: 0;
+	width: auto;
+}
+
+.bp-avatar .item {
+	overflow: hidden;
+}
+
+.bp-avatar .avatar-crop-management.adjust {
+	float: right;
+	clear: none;
+	padding-top: 0;
+}
+
+.bp-avatar #avatar-to-crop.adjust {
+	float: right;
+	margin-left: 20px;
+}
+
+@media screen and (max-width: 480px) {
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-actions,
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+		float: none;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+		margin: 0 auto 10px;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-actions {
+		width: auto;
+	}
+
+}
+
+@media screen and (min-width: 801px) {
+
+	#bp-webcam-avatar .avatar-crop-management {
+		clear: none;
+		float: left;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+		float: none;
+		margin: 0 auto 10px;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-actions {
+		float: right;
+		width: 100%;
+	}
+
+}
+
+
+/** Admin Profile **/
+/* stylelint-disable selector-id-pattern */
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent {
+	height: 95% !important;
+	width: 95% !important;
+}
+
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent p.updated,
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent p.updated,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning {
+	display: block;
+	padding: 10px 15px;
+}
+
+.wp-admin #TB_window .bp-avatar #avatar-to-crop {
+	float: right;
+	margin: 0;
+}
+
+.wp-admin #TB_window .bp-avatar #bp-webcam-avatar #avatar-to-crop {
+	margin-bottom: 20px;
+}
+
+@media screen and (min-width: 783px) {
+
+	.wp-admin #TB_window .bp-avatar .avatar-crop-management {
+		clear: none;
+		float: right;
+		margin-right: 20px;
+		padding-top: 0;
+		text-align: center;
+
+	}
+
+}
+
+.wp-admin #TB_window .bp-avatar #avatar-to-crop video {
+	width: 100%;
+}
+
+.wp-admin #TB_window .bp-avatar .avatar-crop-management a.button {
+	height: auto;
+	line-height: inherit;
+}
+
+@media screen and (min-width: 810px) {
+
+	.wp-admin #TB_window .bp-avatar #bp-webcam-avatar #avatar-to-crop {
+		max-width: none;
+		width: 76%;
+	}
+
+	.wp-admin #TB_window #bp-webcam-avatar .avatar-crop-management {
+		max-width: none;
+		width: auto;
+	}
+}
+/* stylelint-enable */
Index: /tags/4.0.0-beta1/src/bp-core/css/avatar.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/css/avatar.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/css/avatar.css	(revision 12265)
@@ -0,0 +1,412 @@
+div.bp-avatar-status,
+div.bp-cover-image-status {
+	clear: both;
+	margin: 1em 0;
+}
+
+div.bp-avatar-status p.updated,
+div.bp-cover-image-status p.updated {
+	display: block;
+	padding: 10px 15px;
+}
+
+div.bp-avatar-status p.success,
+div.bp-cover-image-status p.success {
+	background-color: #efc;
+	border: 1px solid #591;
+	color: #250;
+}
+
+div.bp-avatar-status p.error,
+div.bp-cover-image-status p.error {
+	background-color: #fdc;
+	border: 1px solid #a00;
+	color: #800;
+}
+
+div.bp-avatar-status .bp-progress,
+div.bp-cover-image-status .bp-progress {
+	background: none;
+	border: 1px solid #d1d1d1;
+	float: right;
+	height: 22px;
+	line-height: 2;
+	margin: 6px 10px 0 0;
+	margin-bottom: 2px;
+	padding: 0;
+	overflow: hidden;
+	width: 200px;
+}
+
+div.bp-avatar-status .bp-bar,
+div.bp-cover-image-status .bp-bar {
+	background-color: #c3ff88;
+	width: 0;
+	height: 100%;
+	z-index: 9;
+}
+
+.bp-uploader-progress div.error {
+	background-color: #fdc;
+	border: 1px solid #a00;
+	color: #800;
+	display: block;
+	font-size: 90%;
+	padding: 10px 15px;
+}
+
+/* stylelint-disable selector-id-pattern */
+#buddypress p.warning,
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning {
+	background-color: #ffd;
+	border: 1px solid #cb2;
+	color: #440;
+	display: block;
+	font-size: 90%;
+	margin: 1em 0;
+	padding: 10px 15px;
+}
+/* stylelint-enable */
+
+div.bp-avatar-nav {
+	background: transparent;
+	clear: both;
+	margin: 10px 0;
+	overflow: hidden;
+}
+
+.avatar-nav-items {
+	margin: 0;
+	padding: 0;
+}
+
+.bp-avatar-nav .avatar-nav-items li.avatar-nav-item {
+	float: left !important;
+	margin: 0;
+	list-style: none;
+}
+
+.avatar-nav-items li a {
+	display: block;
+	padding: 5px 10px;
+	text-decoration: none;
+}
+
+.bp-avatar-nav	ul:before,
+.bp-avatar-nav	ul:after {
+	content: " ";
+	display: table;
+}
+
+.bp-avatar-nav	ul:after {
+	clear: both;
+}
+
+.bp-avatar-nav	ul {
+	border-bottom: 1px solid #ccc;
+	margin-bottom: 10px;
+}
+
+.bp-avatar-nav	ul.avatar-nav-items li.current {
+	border: 1px solid #ccc;
+	border-bottom-color: #fff;
+	border-top-left-radius: 4px;
+	border-top-right-radius: 4px;
+	margin-bottom: -1px;
+}
+
+.bp-avatar-nav	li.current a {
+	background: none;
+	color: inherit;
+	font-weight: 700;
+	opacity: 0.8;
+	outline: 0;
+}
+
+#drag-drop-area {
+	border: 4px dashed #bbb;
+	height: 200px;
+}
+
+.drag-drop.drag-over #drag-drop-area {
+	border-color: #83b4d8;
+}
+
+.drag-drop-inside p {
+	display: none;
+}
+
+.drag-drop-inside p.drag-drop-buttons {
+	margin-top: 80px;
+	text-align: center;
+}
+
+.drag-drop .drag-drop-inside p.drag-drop-buttons {
+	margin: auto;
+	text-align: inherit;
+}
+
+.moxie-shim.moxie-shim-html5 input {
+	cursor: pointer;
+}
+
+.drag-drop #drag-drop-area {
+	box-sizing: border-box;
+	display: table;
+	height: 100%;
+	width: 100%;
+}
+
+.drag-drop .drag-drop-inside {
+	display: table-cell;
+	padding: 40px 0;
+	text-align: center;
+	vertical-align: middle;
+}
+
+.drag-drop .drag-drop-inside p,
+.drag-drop-inside p.drag-drop-buttons {
+	display: block;
+}
+
+.drag-drop .drag-drop-inside p {
+	color: #767676;
+	font-size: 110%;
+	margin: 5px 0;
+	text-align: center;
+}
+
+.drag-drop-inside p.drag-drop-info {
+	margin-top: 0;
+}
+
+#avatar-to-crop {
+	margin: 0 auto 20px;
+	text-align: left;
+}
+
+#bp-webcam-avatar #avatar-to-crop {
+	float: left;
+	margin: 0 0 20px;
+}
+
+#avatar-to-crop .jcrop-holder {
+	margin: 0 auto;
+}
+
+.avatar-crop-management {
+	clear: left;
+	overflow: hidden;
+	padding-top: 20px;
+	text-align: center;
+}
+
+#bp-webcam-avatar .avatar-crop-management {
+	clear: none;
+	float: none;
+	overflow: visible;
+	padding-top: 0;
+	width: auto;
+}
+
+#avatar-crop-pane {
+	margin: 0 auto;
+	overflow: hidden;
+}
+
+#bp-webcam-avatar #avatar-to-crop {
+	border: 1px solid #eee;
+	max-width: 100%;
+	width: 100%;
+}
+
+@media screen and (min-width: 801px) {
+
+	#bp-webcam-avatar #avatar-to-crop {
+		max-width: 64%;
+		width: 64%;
+	}
+}
+
+#avatar-crop-actions a {
+	display: block;
+}
+
+#bp-webcam-avatar #avatar-crop-actions {
+	float: left;
+	margin: 0 0 20px;
+	width: 50%;
+}
+
+#avatar-crop-actions a.button {
+	margin-top: 10px;
+}
+
+#bp-webcam-avatar #avatar-crop-actions a.button {
+	display: block;
+	margin: 0 0 5px;
+	padding: 4px 0;
+	width: 100%;
+}
+
+#avatar-to-crop img,
+#avatar-crop-pane img,
+#avatar-crop-pane canvas,
+#avatar-upload-form img,
+#create-group-form img,
+#group-settings-form img {
+	border: none !important;
+	max-width: none !important;
+}
+
+#bp-webcam-avatar video {
+	float: left;
+	margin-bottom: 0;
+	max-width: 100%;
+	width: 100%;
+	transform: scaleX(-1);
+}
+
+#bp-webcam-avatar #avatar-crop-pane {
+	border: 2px dashed #bbb;
+	clear: left;
+	float: right;
+	margin: 0  40px 10px 0;
+	overflow: hidden;
+}
+
+/**
+*  Users can define avatar constants with new dimension values
+*  If they do, we'll ensure the layouts preserved by enforcing a max width
+*  that matches bp default settings.
+*/
+#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+	max-width: 150px;
+	max-height: 150px;
+}
+
+#avatar-crop-pane canvas {
+	height: auto;
+	width: 100%;
+	max-width: 100%;
+}
+
+.group-avatar .bp-avatar .avatar-crop-management {
+	margin-left: 0;
+	padding-top: 0;
+	width: auto;
+}
+
+.bp-avatar .item {
+	overflow: hidden;
+}
+
+.bp-avatar .avatar-crop-management.adjust {
+	float: left;
+	clear: none;
+	padding-top: 0;
+}
+
+.bp-avatar #avatar-to-crop.adjust {
+	float: left;
+	margin-right: 20px;
+}
+
+@media screen and (max-width: 480px) {
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-actions,
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+		float: none;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+		margin: 0 auto 10px;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-actions {
+		width: auto;
+	}
+
+}
+
+@media screen and (min-width: 801px) {
+
+	#bp-webcam-avatar .avatar-crop-management {
+		clear: none;
+		float: right;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-pane {
+		float: none;
+		margin: 0 auto 10px;
+	}
+
+	#bp-webcam-avatar .avatar-crop-management #avatar-crop-actions {
+		float: left;
+		width: 100%;
+	}
+
+}
+
+
+/** Admin Profile **/
+/* stylelint-disable selector-id-pattern */
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent {
+	height: 95% !important;
+	width: 95% !important;
+}
+
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent p.updated,
+body.users_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent p.updated,
+body.profile_page_bp-profile-edit.modal-open #TB_ajaxContent p.warning {
+	display: block;
+	padding: 10px 15px;
+}
+
+.wp-admin #TB_window .bp-avatar #avatar-to-crop {
+	float: left;
+	margin: 0;
+}
+
+.wp-admin #TB_window .bp-avatar #bp-webcam-avatar #avatar-to-crop {
+	margin-bottom: 20px;
+}
+
+@media screen and (min-width: 783px) {
+
+	.wp-admin #TB_window .bp-avatar .avatar-crop-management {
+		clear: none;
+		float: left;
+		margin-left: 20px;
+		padding-top: 0;
+		text-align: center;
+
+	}
+
+}
+
+.wp-admin #TB_window .bp-avatar #avatar-to-crop video {
+	width: 100%;
+}
+
+.wp-admin #TB_window .bp-avatar .avatar-crop-management a.button {
+	height: auto;
+	line-height: inherit;
+}
+
+@media screen and (min-width: 810px) {
+
+	.wp-admin #TB_window .bp-avatar #bp-webcam-avatar #avatar-to-crop {
+		max-width: none;
+		width: 76%;
+	}
+
+	.wp-admin #TB_window #bp-webcam-avatar .avatar-crop-management {
+		max-width: none;
+		width: auto;
+	}
+}
+/* stylelint-enable */
Index: /tags/4.0.0-beta1/src/bp-core/css/buddybar-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/css/buddybar-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/css/buddybar-rtl.css	(revision 12265)
@@ -0,0 +1,235 @@
+body:not(.wp-admin) {
+	padding-top: 25px !important;
+}
+
+#wp-admin-bar {
+	position: fixed;
+	top: 0;
+	right: 0;
+	height: 25px;
+	font-size: 11px;
+	width: 100%;
+	z-index: 9999;
+}
+
+#wp-admin-bar .padder {
+	position: relative;
+	padding: 0;
+	width: 100%;
+	margin: 0 auto;
+	background: url(../images/60pc_black.png);
+	height: 25px;
+}
+
+body#bp-default #wp-admin-bar .padder {
+	max-width: 1250px;
+}
+
+#wp-admin-bar * {
+	z-index: 999;
+}
+
+#wp-admin-bar div#admin-bar-logo {
+	position: absolute;
+	top: 5px;
+	right: 10px;
+}
+
+#wp-admin-bar a img {
+	border: none;
+}
+
+#wp-admin-bar li {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+	line-height: 1;
+	text-align: right;
+}
+
+#wp-admin-bar li a {
+	padding: 7px 15px;
+	color: #eee;
+	text-decoration: none;
+	font-size: 11px;
+}
+
+#wp-admin-bar li.alt {
+	border: none;
+}
+
+#wp-admin-bar li.no-arrow a {
+	padding-left: 15px;
+}
+
+#wp-admin-bar ul li ul li a span {
+	display: none;
+}
+
+#wp-admin-bar li:hover,
+#wp-admin-bar li.hover {
+	position: static;
+}
+
+#admin-bar-logo {
+	float: right;
+	font-weight: 700;
+	font-size: 11px;
+	padding: 5px 8px;
+	margin: 0;
+	text-decoration: none;
+	color: #fff;
+}
+
+body#bp-default #admin-bar-logo {
+	padding: 2px 8px;
+}
+
+/* all lists */
+#wp-admin-bar ul {
+	margin: 0;
+	list-style: none;
+	line-height: 1;
+	cursor: pointer;
+	height: auto;
+	padding: 0;
+}
+
+/* all list items */
+#wp-admin-bar ul li {
+	padding: 0;
+	float: right;
+	position: relative;
+	background: url(../images/admin-menu-arrow.gif) 12% 53% no-repeat;
+	padding-left: 11px;
+}
+
+#wp-admin-bar ul li.no-arrow {
+	background: none;
+	padding-left: 0;
+}
+
+#wp-admin-bar ul li ul li {
+	background-image: none;
+}
+
+#wp-admin-bar ul li.align-right {
+	position: absolute;
+	left: 0;
+}
+
+#wp-admin-bar ul li a {
+	display: block;
+}
+
+#wp-admin-bar ul.main-nav li:hover,
+#wp-admin-bar ul.main-nav li.sfhover,
+#wp-admin-bar ul.main-nav li ul li.sfhover {
+	background-color: #333;
+}
+
+/* second-level lists */
+#wp-admin-bar ul li ul {
+	position: absolute;
+	width: 185px;
+	right: -999em;
+	margin-right: 0;
+	background: #333;
+	border: 1px solid #222;
+	-moz-box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+	-webkit-box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+	-moz-border-radius: 3px;
+	-webkit-border-radius: 3px;
+	-moz-border-radius-topright: 0;
+	-webkit-border-top-right-radius: 0;
+	-moz-border-radius-topleft: 0;
+	-webkit-border-top-left-radius: 0;
+}
+
+#wp-admin-bar ul li > ul {
+	border-top: none;
+}
+
+#wp-admin-bar ul li ul a {
+	color: #eee;
+}
+
+#wp-admin-bar ul li ul li {
+	float: right;
+	width: 174px;
+	margin: 0;
+}
+
+#wp-admin-bar ul li ul li:hover a {
+	color: #fff;
+}
+
+#wp-admin-bar ul li div.admin-bar-clear {
+	clear: both;
+}
+
+#wp-admin-bar ul.main-nav li ul li:hover,
+#wp-admin-bar ul.main-nav li ul li.sfhover,
+#wp-admin-bar ul.main-nav li ul li.sfhover {
+	background-color: #222;
+}
+
+/* third-and-above-level lists */
+#wp-admin-bar ul li ul ul {
+	margin: -25px 184px 0 0;
+	-moz-border-radius: 3px;
+	-webkit-border-radius: 3px;
+}
+
+#wp-admin-bar ul li ul li:hover ul li a {
+	color: #eee;
+}
+
+#wp-admin-bar ul li ul li ul li:hover a {
+	color: #fff;
+}
+
+#wp-admin-bar ul li:hover ul,
+#wp-admin-bar ul li ul li:hover ul,
+#wp-admin-bar ul li.sfhover ul,
+#wp-admin-bar ul li ul li.sfhover ul {
+	right: auto;
+}
+
+#wp-admin-bar ul li.align-right:hover ul {
+	left: 0;
+}
+
+#wp-admin-bar ul li:hover ul ul,
+#wp-admin-bar li.sfhover ul li ul {
+	right: -999em;
+}
+
+/* Menu item css */
+#wp-admin-bar img.avatar {
+	float: right;
+	margin-left: 8px;
+}
+
+#wp-admin-bar span.activity {
+	display: block;
+	margin-right: 34px;
+	padding: 0;
+}
+
+#wp-admin-bar ul.author-list li a {
+	height: 17px;
+}
+
+#wp-admin-bar ul li#bp-adminbar-notifications-menu a span {
+	padding: 0 6px;
+	margin-right: 2px;
+	background: #fff;
+	color: #000;
+	border-radius: 3px;
+}
+
+#wp-admin-bar-user-info img.avatar {
+	height: 64px;
+	width: 64px;
+}
Index: /tags/4.0.0-beta1/src/bp-core/css/buddybar.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/css/buddybar.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/css/buddybar.css	(revision 12265)
@@ -0,0 +1,235 @@
+body:not(.wp-admin) {
+	padding-top: 25px !important;
+}
+
+#wp-admin-bar {
+	position: fixed;
+	top: 0;
+	left: 0;
+	height: 25px;
+	font-size: 11px;
+	width: 100%;
+	z-index: 9999;
+}
+
+#wp-admin-bar .padder {
+	position: relative;
+	padding: 0;
+	width: 100%;
+	margin: 0 auto;
+	background: url(../images/60pc_black.png);
+	height: 25px;
+}
+
+body#bp-default #wp-admin-bar .padder {
+	max-width: 1250px;
+}
+
+#wp-admin-bar * {
+	z-index: 999;
+}
+
+#wp-admin-bar div#admin-bar-logo {
+	position: absolute;
+	top: 5px;
+	left: 10px;
+}
+
+#wp-admin-bar a img {
+	border: none;
+}
+
+#wp-admin-bar li {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+	line-height: 1;
+	text-align: left;
+}
+
+#wp-admin-bar li a {
+	padding: 7px 15px;
+	color: #eee;
+	text-decoration: none;
+	font-size: 11px;
+}
+
+#wp-admin-bar li.alt {
+	border: none;
+}
+
+#wp-admin-bar li.no-arrow a {
+	padding-right: 15px;
+}
+
+#wp-admin-bar ul li ul li a span {
+	display: none;
+}
+
+#wp-admin-bar li:hover,
+#wp-admin-bar li.hover {
+	position: static;
+}
+
+#admin-bar-logo {
+	float: left;
+	font-weight: 700;
+	font-size: 11px;
+	padding: 5px 8px;
+	margin: 0;
+	text-decoration: none;
+	color: #fff;
+}
+
+body#bp-default #admin-bar-logo {
+	padding: 2px 8px;
+}
+
+/* all lists */
+#wp-admin-bar ul {
+	margin: 0;
+	list-style: none;
+	line-height: 1;
+	cursor: pointer;
+	height: auto;
+	padding: 0;
+}
+
+/* all list items */
+#wp-admin-bar ul li {
+	padding: 0;
+	float: left;
+	position: relative;
+	background: url(../images/admin-menu-arrow.gif) 88% 53% no-repeat;
+	padding-right: 11px;
+}
+
+#wp-admin-bar ul li.no-arrow {
+	background: none;
+	padding-right: 0;
+}
+
+#wp-admin-bar ul li ul li {
+	background-image: none;
+}
+
+#wp-admin-bar ul li.align-right {
+	position: absolute;
+	right: 0;
+}
+
+#wp-admin-bar ul li a {
+	display: block;
+}
+
+#wp-admin-bar ul.main-nav li:hover,
+#wp-admin-bar ul.main-nav li.sfhover,
+#wp-admin-bar ul.main-nav li ul li.sfhover {
+	background-color: #333;
+}
+
+/* second-level lists */
+#wp-admin-bar ul li ul {
+	position: absolute;
+	width: 185px;
+	left: -999em;
+	margin-left: 0;
+	background: #333;
+	border: 1px solid #222;
+	-moz-box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+	-webkit-box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+	-moz-border-radius: 3px;
+	-webkit-border-radius: 3px;
+	-moz-border-radius-topleft: 0;
+	-webkit-border-top-left-radius: 0;
+	-moz-border-radius-topright: 0;
+	-webkit-border-top-right-radius: 0;
+}
+
+#wp-admin-bar ul li > ul {
+	border-top: none;
+}
+
+#wp-admin-bar ul li ul a {
+	color: #eee;
+}
+
+#wp-admin-bar ul li ul li {
+	float: left;
+	width: 174px;
+	margin: 0;
+}
+
+#wp-admin-bar ul li ul li:hover a {
+	color: #fff;
+}
+
+#wp-admin-bar ul li div.admin-bar-clear {
+	clear: both;
+}
+
+#wp-admin-bar ul.main-nav li ul li:hover,
+#wp-admin-bar ul.main-nav li ul li.sfhover,
+#wp-admin-bar ul.main-nav li ul li.sfhover {
+	background-color: #222;
+}
+
+/* third-and-above-level lists */
+#wp-admin-bar ul li ul ul {
+	margin: -25px 0 0 184px;
+	-moz-border-radius: 3px;
+	-webkit-border-radius: 3px;
+}
+
+#wp-admin-bar ul li ul li:hover ul li a {
+	color: #eee;
+}
+
+#wp-admin-bar ul li ul li ul li:hover a {
+	color: #fff;
+}
+
+#wp-admin-bar ul li:hover ul,
+#wp-admin-bar ul li ul li:hover ul,
+#wp-admin-bar ul li.sfhover ul,
+#wp-admin-bar ul li ul li.sfhover ul {
+	left: auto;
+}
+
+#wp-admin-bar ul li.align-right:hover ul {
+	right: 0;
+}
+
+#wp-admin-bar ul li:hover ul ul,
+#wp-admin-bar li.sfhover ul li ul {
+	left: -999em;
+}
+
+/* Menu item css */
+#wp-admin-bar img.avatar {
+	float: left;
+	margin-right: 8px;
+}
+
+#wp-admin-bar span.activity {
+	display: block;
+	margin-left: 34px;
+	padding: 0;
+}
+
+#wp-admin-bar ul.author-list li a {
+	height: 17px;
+}
+
+#wp-admin-bar ul li#bp-adminbar-notifications-menu a span {
+	padding: 0 6px;
+	margin-left: 2px;
+	background: #fff;
+	color: #000;
+	border-radius: 3px;
+}
+
+#wp-admin-bar-user-info img.avatar {
+	height: 64px;
+	width: 64px;
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/1.2.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/1.2.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/1.2.php	(revision 12265)
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Deprecated Functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+/**
+ * Retrieve sitewide activity
+ *
+ * You should use bp_activity_get() instead
+ *
+ * @since 1.0.0
+ * @deprecated 1.2.0
+ *
+ * @param string|array $args
+ *
+ * @return object $activity The activity/activities object
+ */
+function bp_activity_get_sitewide( $args = '' ) {
+	_deprecated_function( __FUNCTION__, '1.2', 'bp_activity_get()' );
+
+	$defaults = array(
+		'max' => false, // Maximum number of results to return
+		'page' => 1, // page 1 without a per_page will result in no pagination.
+		'per_page' => false, // results per page
+		'sort' => 'DESC', // sort ASC or DESC
+		'display_comments' => false, // false for no comments. 'stream' for within stream display, 'threaded' for below each activity item
+
+		'search_terms' => false, // Pass search terms as a string
+		'show_hidden' => false, // Show activity items that are hidden site-wide?
+
+		/**
+		 * Pass filters as an array:
+		 * array(
+		 * 	'user_id' => false, // user_id to filter on
+		 *	'object' => false, // object to filter on e.g. groups, profile, status, friends
+		 *	'action' => false, // action to filter on e.g. new_wire_post, new_forum_post, profile_updated
+		 *	'primary_id' => false, // object ID to filter on e.g. a group_id or forum_id or blog_id etc.
+		 *	'secondary_id' => false, // secondary object ID to filter on e.g. a post_id
+		 * );
+		 */
+		'filter' => array()
+	);
+
+	$args = wp_parse_args( $args, $defaults );
+
+	return apply_filters( 'bp_activity_get_sitewide', BP_Activity_Activity::get( $args ), $r );
+}
+
+
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/1.5.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/1.5.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/1.5.php	(revision 12265)
@@ -0,0 +1,703 @@
+<?php
+/**
+ * Deprecated Functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @deprecated Since 1.5.0
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/** Loader ********************************************************************/
+
+/**
+ * @deprecated 1.5.0
+ */
+function bp_setup_root_components() {
+	do_action( 'bp_setup_root_components' );
+}
+add_action( 'bp_init', 'bp_setup_root_components', 6 );
+
+/** WP Abstraction ************************************************************/
+
+/**
+ * bp_core_is_multisite()
+ *
+ * This function originally served as a wrapper when WordPress and WordPress MU were separate entities.
+ * Use is_multisite() instead.
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use is_multisite()
+ *
+ * @return bool
+ */
+function bp_core_is_multisite() {
+	_deprecated_function( __FUNCTION__, '1.5', 'is_multisite()' );
+	return is_multisite();
+}
+
+/**
+ * bp_core_is_main_site
+ *
+ * Checks if current blog is root blog of site. Deprecated in 1.5.
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use is_main_site()
+ * @package BuddyPress
+ * @param int|string $blog_id optional blog id to test (default current blog)
+ * @return bool True if not multisite or $blog_id is main site
+ * @since 1.2.6
+ */
+function bp_core_is_main_site( $blog_id = '' ) {
+	_deprecated_function( __FUNCTION__, '1.5', 'is_main_site()' );
+	return is_main_site( $blog_id );
+}
+
+if ( !function_exists( 'is_site_admin' ) ) {
+	/**
+	 * WPMU version of is_super_admin()
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated Use is_super_admin()
+	 * @param int|bool $user_id Optional. Defaults to logged-in user
+	 * @return bool True if is super admin
+	 */
+	function is_site_admin( $user_id = false ) {
+		_deprecated_function( __FUNCTION__, '1.5', 'is_super_admin()' );
+		return is_super_admin( $user_id );
+	}
+}
+
+/** Admin ******************************************************************/
+
+/**
+ * In BuddyPress 1.1 - 1.2.x, this function provided a better version of add_menu_page()
+ * that allowed positioning of menus. Deprecated in 1.5 in favour of a WP core function.
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use add_menu_page().
+ * @since 1.1.0
+ *
+ * @return string
+ */
+function bp_core_add_admin_menu_page( $args = '' ) {
+	global $_registered_pages, $admin_page_hooks, $menu;
+
+	_deprecated_function( __FUNCTION__, '1.5', 'Use add_menu_page()' );
+
+	$defaults = array(
+		'access_level' => 2,
+		'file'         => false,
+		'function'     => false,
+		'icon_url'     => false,
+		'menu_title'   => '',
+		'page_title'   => '',
+		'position'     => 100
+	);
+
+	$r = wp_parse_args( $args, $defaults );
+	extract( $r, EXTR_SKIP );
+
+	$file     = plugin_basename( $file );
+	$hookname = get_plugin_page_hookname( $file, '' );
+
+	$admin_page_hooks[$file] = sanitize_title( $menu_title );
+
+	if ( !empty( $function ) && !empty ( $hookname ) )
+		add_action( $hookname, $function );
+
+	if ( empty( $icon_url ) )
+		$icon_url = 'images/generic.png';
+	elseif ( is_ssl() && 0 === strpos( $icon_url, 'http://' ) )
+		$icon_url = 'https://' . substr( $icon_url, 7 );
+
+	do {
+		$position++;
+	} while ( !empty( $menu[$position] ) );
+
+	$menu[$position] = array ( $menu_title, $access_level, $file, $page_title, 'menu-top ' . $hookname, $hookname, $icon_url );
+	$_registered_pages[$hookname] = true;
+
+	return $hookname;
+}
+/** Activity ******************************************************************/
+
+/**
+ * @deprecated 1.5.0
+ */
+function bp_is_activity_permalink() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_single_activity' );
+	bp_is_single_activity();
+}
+
+/** Core **********************************************************************/
+
+/**
+ * @deprecated 1.5.0
+ */
+function bp_core_get_wp_profile() {
+	_deprecated_function( __FUNCTION__, '1.5' );
+
+	$ud = get_userdata( bp_displayed_user_id() ); ?>
+
+<div class="bp-widget wp-profile">
+	<h4><?php _e( 'My Profile', 'buddypress' ) ?></h4>
+
+	<table class="wp-profile-fields">
+
+		<?php if ( $ud->display_name ) : ?>
+
+			<tr id="wp_displayname">
+				<td class="label"><?php _e( 'Name', 'buddypress' ); ?></td>
+				<td class="data"><?php echo $ud->display_name; ?></td>
+			</tr>
+
+		<?php endif; ?>
+
+		<?php if ( $ud->user_description ) : ?>
+
+			<tr id="wp_desc">
+				<td class="label"><?php _e( 'About Me', 'buddypress' ); ?></td>
+				<td class="data"><?php echo $ud->user_description; ?></td>
+			</tr>
+
+		<?php endif; ?>
+
+		<?php if ( $ud->user_url ) : ?>
+
+			<tr id="wp_website">
+				<td class="label"><?php _e( 'Website', 'buddypress' ); ?></td>
+				<td class="data"><?php echo make_clickable( $ud->user_url ); ?></td>
+			</tr>
+
+		<?php endif; ?>
+
+		<?php if ( $ud->jabber ) : ?>
+
+			<tr id="wp_jabber">
+				<td class="label"><?php _e( 'Jabber', 'buddypress' ); ?></td>
+				<td class="data"><?php echo $ud->jabber; ?></td>
+			</tr>
+
+		<?php endif; ?>
+
+		<?php if ( $ud->aim ) : ?>
+
+			<tr id="wp_aim">
+				<td class="label"><?php _e( 'AOL Messenger', 'buddypress' ); ?></td>
+				<td class="data"><?php echo $ud->aim; ?></td>
+			</tr>
+
+		<?php endif; ?>
+
+		<?php if ( $ud->yim ) : ?>
+
+			<tr id="wp_yim">
+				<td class="label"><?php _e( 'Yahoo Messenger', 'buddypress' ); ?></td>
+				<td class="data"><?php echo $ud->yim; ?></td>
+			</tr>
+
+		<?php endif; ?>
+
+	</table>
+</div>
+
+<?php
+}
+
+/**
+ * @deprecated 1.5.0
+ * @deprecated Use bp_is_my_profile()
+ */
+function bp_is_home() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_my_profile' );
+	return bp_is_my_profile();
+}
+
+/**
+ * Is the user on the front page of the site?
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use is_front_page()
+ * @return bool
+ */
+function bp_is_front_page() {
+	_deprecated_function( __FUNCTION__, '1.5', "is_front_page()" );
+	return is_front_page();
+}
+
+/**
+ * Is the front page of the site set to the Activity component?
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use bp_is_component_front_page( 'activity' )
+ * @return bool
+ */
+function bp_is_activity_front_page() {
+	_deprecated_function( __FUNCTION__, '1.5', "bp_is_component_front_page( 'activity' )" );
+	return bp_is_component_front_page( 'activity' );
+}
+
+/**
+ * @deprecated 1.5.0
+ * @deprecated use bp_is_user()
+ */
+function bp_is_member() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_user' );
+	return bp_is_user();
+}
+
+/**
+ * @deprecated 1.5.0
+ * @deprecated use bp_loggedin_user_link()
+ */
+function bp_loggedinuser_link() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_loggedin_user_link' );
+	bp_loggedin_user_link();
+}
+
+/**
+ * Only show the search form if there are available objects to search for.
+ * Deprecated in 1.5; not used anymore.
+ *
+ * @deprecated 1.5.0
+ * @return bool
+ */
+function bp_search_form_enabled() {
+	_deprecated_function( __FUNCTION__, '1.5', 'No longer required.' );
+	return apply_filters( 'bp_search_form_enabled', true );
+}
+
+/**
+ * Template tag version of bp_get_page_title()
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use wp_title()
+ * @since 1.0.0
+ */
+function bp_page_title() {
+	echo bp_get_page_title();
+}
+	/**
+	 * Prior to BuddyPress 1.5, this was used to generate the page's <title> text.
+	 * Now, just simply use wp_title().
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated Use wp_title()
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_page_title() {
+		_deprecated_function( __FUNCTION__, '1.5', 'wp_title()' );
+		$title = wp_title( '|', false, 'right' ) . get_bloginfo( 'name', 'display' );
+
+		// Backpat for BP 1.2 filter
+		$title = apply_filters( 'bp_page_title', esc_attr( $title ), esc_attr( $title ) );
+
+		return apply_filters( 'bp_get_page_title', $title );
+	}
+
+/**
+ * Generate a link to log out. Last used in BP 1.2-beta. You should be using wp_logout_url().
+ *
+ * @deprecated 1.5.0
+ * @deprecated Use wp_logout_url()
+ * @since 1.0.0
+ */
+function bp_log_out_link() {
+	_deprecated_function( __FUNCTION__, '1.5', 'wp_logout_url()' );
+
+	$logout_link = '<a href="' . wp_logout_url( bp_get_root_domain() ) . '">' . __( 'Log Out', 'buddypress' ) . '</a>';
+	echo apply_filters( 'bp_logout_link', $logout_link );
+}
+
+/**
+ * Send an email and a BP notification on receipt of an @-mention in a group
+ *
+ * @deprecated 1.5.0
+ * @deprecated Deprecated in favor of the more general bp_activity_at_message_notification()
+ */
+function groups_at_message_notification( $content, $poster_user_id, $group_id, $activity_id ) {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_activity_at_message_notification()' );
+
+	/* Scan for @username strings in an activity update. Notify each user. */
+	$pattern = '/[@]+([A-Za-z0-9-_\.@]+)/';
+	preg_match_all( $pattern, $content, $usernames );
+
+	/* Make sure there's only one instance of each username */
+	if ( !$usernames = array_unique( $usernames[1] ) )
+		return false;
+
+	$group = new BP_Groups_Group( $group_id );
+
+	foreach( (array) $usernames as $username ) {
+		if ( !$receiver_user_id = bp_core_get_userid( $username ) )
+			continue;
+
+		/* Check the user is a member of the group before sending the update. */
+		if ( !groups_is_user_member( $receiver_user_id, $group_id ) )
+			continue;
+
+		// Now email the user with the contents of the message (if they have enabled email notifications)
+		if ( 'no' != bp_get_user_meta( $receiver_user_id, 'notification_activity_new_mention', true ) ) {
+			$poster_name = bp_core_get_user_displayname( $poster_user_id );
+
+			$message_link  = bp_activity_get_permalink( $activity_id );
+			$settings_slug = function_exists( 'bp_get_settings_slug' ) ? bp_get_settings_slug() : 'settings';
+			$settings_link = bp_core_get_user_domain( $receiver_user_id ) . $settings_slug . '/notifications/';
+
+			$poster_name = stripslashes( $poster_name );
+			$content = bp_groups_filter_kses( stripslashes( $content ) );
+
+			// Set up and send the message
+			$ud = bp_core_get_core_userdata( $receiver_user_id );
+			$to = $ud->user_email;
+			$subject = bp_get_email_subject( array( 'text' => sprintf( __( '%1$s mentioned you in the group "%2$s"', 'buddypress' ), $poster_name, $group->name ) ) );
+
+$message = sprintf( __(
+'%1$s mentioned you in the group "%2$s":
+
+"%3$s"
+
+To view and respond to the message, log in and visit: %4$s
+
+---------------------
+', 'buddypress' ), $poster_name, $group->name, $content, $message_link );
+
+			/* Send the message */
+			$to = apply_filters( 'groups_at_message_notification_to', $to );
+			$subject = apply_filters( 'groups_at_message_notification_subject', $subject, $group, $poster_name );
+			$message = apply_filters( 'groups_at_message_notification_message', $message, $group, $poster_name, $content, $message_link, $settings_link );
+
+			wp_mail( $to, $subject, $message );
+		}
+	}
+
+	do_action( 'bp_groups_sent_mention_email', $usernames, $subject, $message, $content, $poster_user_id, $group_id, $activity_id );
+}
+
+/**
+ * BP 1.5 simplified notification functions a bit
+ * @deprecated 1.5.0
+ *
+ * @return mixed
+ */
+function bp_core_delete_notifications_for_user_by_type( $user_id, $component_name, $component_action ) {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_core_delete_notifications_by_type()' );
+	return BP_Core_Notification::delete_for_user_by_type( $user_id, $component_name, $component_action );
+}
+
+/**
+ * @return mixed
+ */
+function bp_core_delete_notifications_for_user_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id = false ) {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_core_delete_notifications_by_item_id()' );
+	return BP_Core_Notification::delete_for_user_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id );
+}
+
+/**
+ * Last used by core in BP 1.1. The markup was merged into DTheme's header.php template.
+ * @deprecated 1.5.0
+ */
+function bp_search_form() {
+	_deprecated_function( __FUNCTION__, '1.1', 'No longer required.' );
+
+	$form = '
+		<form action="' . bp_search_form_action() . '" method="post" id="search-form">
+			<input type="text" id="search-terms" name="search-terms" value="" />
+			' . bp_search_form_type_select() . '
+
+			<input type="submit" name="search-submit" id="search-submit" value="' . __( 'Search', 'buddypress' ) . '" />
+			' . wp_nonce_field( 'bp_search_form' ) . '
+		</form>
+	';
+
+	echo apply_filters( 'bp_search_form', $form );
+}
+
+/**
+ * Some _is_ function had their names normalized
+ * @deprecated 1.5.0
+ */
+function bp_is_profile_edit() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_user_profile_edit()' );
+	return bp_is_user_profile_edit();
+}
+
+/**
+ * @deprecated 1.5.0
+ */
+function bp_is_change_avatar() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_user_change_avatar()' );
+	return bp_is_user_change_avatar();
+}
+
+/**
+ * @deprecated 1.5.0
+ */
+function bp_is_friend_requests() {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_user_friend_requests()' );
+	return bp_is_user_friend_requests();
+}
+
+/**
+ * Checks to see if a component's URL should be in the root, not under a member page:
+ * eg: http://example.com/groups/the-group NOT http://example.com/members/andy/groups/the-group
+ * You should be using bp_is_root_component().
+ *
+ * @deprecated 1.5.0
+ * @deprecated bp_is_root_component()
+ * @return bool True if root component, else false.
+ */
+function bp_core_is_root_component( $component_name ) {
+	_deprecated_function( __FUNCTION__, '1.5', 'bp_is_root_component()' );
+	return bp_is_root_component( $component_name );
+}
+
+/** Theme *********************************************************************/
+
+/**
+ * Contains functions which were moved out of BP-Default's functions.php
+ * in BuddyPress 1.5.
+ *
+ * @deprecated 1.5.0
+ * @since 1.5.0
+ */
+function bp_dtheme_deprecated() {
+	if ( !function_exists( 'bp_dtheme_wp_pages_filter' ) ) :
+	/**
+	 * In BuddyPress 1.2.x, this function filtered the dropdown on the
+	 * Settings > Reading screen for selecting the page to show on front to
+	 * include "Activity Stream." As of 1.5.x, it is no longer required.
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated No longer required.
+	 * @param string $page_html A list of pages as a dropdown (select list)
+	 * @return string
+	 * @see wp_dropdown_pages()
+	 * @since 1.2.0
+	 */
+	function bp_dtheme_wp_pages_filter( $page_html ) {
+		_deprecated_function( __FUNCTION__, '1.5', "No longer required." );
+		return $page_html;
+	}
+	endif;
+
+	if ( !function_exists( 'bp_dtheme_page_on_front_update' ) ) :
+	/**
+	 * In BuddyPress 1.2.x, this function hijacked the saving of page on front setting to save the activity stream setting.
+	 * As of 1.5.x, it is no longer required.
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated No longer required.
+	 * @param string $oldvalue Previous value of get_option( 'page_on_front' )
+	 * @param string $oldvalue New value of get_option( 'page_on_front' )
+	 * @return false|string
+	 * @since 1.2.0
+	 */
+	function bp_dtheme_page_on_front_update( $oldvalue, $newvalue ) {
+		_deprecated_function( __FUNCTION__, '1.5', "No longer required." );
+		if ( !is_admin() || !bp_current_user_can( 'bp_moderate' ) )
+			return false;
+
+		return $oldvalue;
+	}
+	endif;
+
+	if ( !function_exists( 'bp_dtheme_page_on_front_template' ) ) :
+	/**
+	 * In BuddyPress 1.2.x, this function loaded the activity stream template if the front page display settings allow.
+	 * As of 1.5.x, it is no longer required.
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated No longer required.
+	 * @param string $template Absolute path to the page template
+	 * @return string
+	 * @since 1.2.0
+	 */
+	function bp_dtheme_page_on_front_template( $template ) {
+		_deprecated_function( __FUNCTION__, '1.5', "No longer required." );
+		return $template;
+	}
+	endif;
+
+	if ( !function_exists( 'bp_dtheme_fix_get_posts_on_activity_front' ) ) :
+	/**
+	 * In BuddyPress 1.2.x, this forced the page ID as a string to stop the get_posts query from kicking up a fuss.
+	 * As of 1.5.x, it is no longer required.
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated No longer required.
+	 * @since 1.2.0
+	 */
+	function bp_dtheme_fix_get_posts_on_activity_front() {
+		_deprecated_function( __FUNCTION__, '1.5', "No longer required." );
+	}
+	endif;
+
+	if ( !function_exists( 'bp_dtheme_fix_the_posts_on_activity_front' ) ) :
+	/**
+	 * In BuddyPress 1.2.x, this was used as part of the code that set the activity stream to be on the front page.
+	 * As of 1.5.x, it is no longer required.
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated No longer required.
+	 * @param array $posts Posts as retrieved by WP_Query
+	 * @return array
+	 * @since 1.2.5
+	 */
+	function bp_dtheme_fix_the_posts_on_activity_front( $posts ) {
+		_deprecated_function( __FUNCTION__, '1.5', "No longer required." );
+		return $posts;
+	}
+	endif;
+
+	if ( !function_exists( 'bp_dtheme_add_blog_comments_js' ) ) :
+	/**
+	 * In BuddyPress 1.2.x, this added the JavaScript needed for blog comment replies.
+	 * As of 1.5.x, we recommend that you enqueue the comment-reply JavaScript in your theme's header.php.
+	 *
+	 * @deprecated 1.5.0
+	 * @deprecated Enqueue the comment-reply script in your theme's header.php.
+	 * @since 1.2.0
+	 */
+	function bp_dtheme_add_blog_comments_js() {
+		_deprecated_function( __FUNCTION__, '1.5', "Enqueue the comment-reply script in your theme's header.php." );
+		if ( is_singular() && bp_is_blog_page() && get_option( 'thread_comments' ) )
+			wp_enqueue_script( 'comment-reply' );
+	}
+	endif;
+}
+add_action( 'after_setup_theme', 'bp_dtheme_deprecated', 15 );
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as the nav structure is set up by the {@link BP_Component} class.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_add_settings_nav() {
+	_deprecated_function( __FUNCTION__, '1.5' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_general_settings() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_general_settings_title() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_general_settings_content() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_notification_settings() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_notification_settings_title() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_notification_settings_content() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_delete_account() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_delete_account_title() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
+
+/**
+ * In BP 1.5, the Settings functions were moved out of the Core and Members
+ * components, and moved into a new Settings component. This function is no
+ * longer needed as new template files for the Settings component were
+ * introduced.
+ *
+ * @deprecated 1.5.0
+ * @since 1.6.0
+ */
+function bp_core_screen_delete_account_content() {
+	_deprecated_function( __FUNCTION__, '1.5', 'Moved into theme template' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/1.6.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/1.6.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/1.6.php	(revision 12265)
@@ -0,0 +1,278 @@
+<?php
+/**
+ * Deprecated Functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @deprecated 1.6.0
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/** Toolbar functions *********************************************************/
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_admin_bar_remove_wp_menus() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_admin_bar_root_site() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_admin_bar_my_sites_menu() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_admin_bar_comments_menu( $wp_admin_bar = '' ) {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_admin_bar_appearance_menu() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_admin_bar_updates_menu() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_members_admin_bar_my_account_logout() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_core_is_user_deleted( $user_id = 0 ) {
+	_deprecated_function( __FUNCTION__, '1.6' );
+	bp_is_user_deleted( $user_id );
+}
+
+/**
+ * @deprecated 1.6.0
+ */
+function bp_core_is_user_spammer( $user_id = 0 ) {
+	_deprecated_function( __FUNCTION__, '1.6' );
+	bp_is_user_spammer( $user_id );
+}
+
+
+/**
+ * Blogs functions
+ */
+
+/**
+ * @deprecated 1.6.0
+ * @deprecated No longer used; see bp_activity_transition_post_type_comment_status()
+ */
+function bp_blogs_manage_comment( $comment_id, $comment_status ) {
+	_deprecated_function( __FUNCTION__, '1.6', 'No longer used' );
+}
+
+/**
+ * Core functions
+ */
+
+/**
+ * @deprecated 1.6.0
+ * @deprecated No longer used; see BP_Admin::admin_menus()
+ */
+function bp_core_add_admin_menu() {
+	_deprecated_function( __FUNCTION__, '1.6', 'No longer used' );
+}
+
+/**
+ * @deprecated 1.6.0
+ * @deprecated No longer used. We do ajax properly now.
+ */
+function bp_core_add_ajax_hook() {
+	_deprecated_function( __FUNCTION__, '1.6', 'No longer used' );
+}
+
+/**
+ * Friends functions
+ */
+
+/**
+ * Displays Friends header tabs
+ *
+ * @deprecated 1.6.0
+ * @deprecated No longer used
+ */
+function bp_friends_header_tabs() {
+	_deprecated_function( __FUNCTION__, '1.6', 'Since BuddyPress 1.2, BP has not supported ordering of friend lists by URL parameters.' );
+?>
+
+	<li<?php if ( !bp_action_variable( 0 ) || bp_is_action_variable( 'recently-active', 0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_friends_slug() . '/my-friends/recently-active' ) ?>"><?php _e( 'Recently Active', 'buddypress' ) ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'newest', 0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_friends_slug() . '/my-friends/newest' ) ?>"><?php _e( 'Newest', 'buddypress' ) ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'alphabetically', 0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_friends_slug() . '/my-friends/alphabetically' ) ?>"><?php _e( 'Alphabetically', 'buddypress' ) ?></a></li>
+
+<?php
+	do_action( 'friends_header_tabs' );
+}
+
+/**
+ * Filters the title for the Friends component
+ *
+ * @deprecated 1.6.0
+ * @deprecated No longer used
+ */
+function bp_friends_filter_title() {
+	_deprecated_function( __FUNCTION__, '1.6', 'Since BuddyPress 1.2, BP has not supported ordering of friend lists by URL parameters.' );
+
+	$current_filter = bp_action_variable( 0 );
+
+	switch ( $current_filter ) {
+		case 'recently-active': default:
+			_e( 'Recently Active', 'buddypress' );
+			break;
+		case 'newest':
+			_e( 'Newest', 'buddypress' );
+			break;
+		case 'alphabetically':
+			_e( 'Alphabetically', 'buddypress' );
+			break;
+	}
+}
+
+
+/** Groups functions **********************************************************/
+
+/**
+ * @deprecated 1.6.0
+ * @deprecated Renamed to groups_get_id() for greater consistency
+ */
+function groups_check_group_exists( $group_slug ) {
+	_deprecated_function( __FUNCTION__, '1.6', 'groups_get_id()' );
+	return groups_get_id( $group_slug );
+}
+
+/** Admin functions ***********************************************************/
+
+/**
+ * Loads admin panel styles and scripts.
+ *
+ * @deprecated 1.6.0
+ */
+function bp_core_add_admin_menu_styles() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/** Activity functions ********************************************************/
+
+/**
+ * @deprecated 1.6.0
+ */
+function updates_register_activity_actions() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+}
+
+/**
+ * Sets the "From" address in emails sent
+ *
+ * @deprecated 1.6.0
+ * @return string email address
+ */
+function bp_core_email_from_address_filter() {
+	_deprecated_function( __FUNCTION__, '1.6' );
+
+	$domain = (array) explode( '/', site_url() );
+	return apply_filters( 'bp_core_email_from_address_filter', 'noreply@' . $domain[2] );
+}
+
+/**
+ * Backward compatibility for AJAX callbacks that do not die() on their own
+ *
+ * In BuddyPress 1.6, BP was altered so that it uses admin-ajax.php (instead of wp-load.php) for
+ * AJAX requests. admin-ajax.php dies with an output of '0' (to signify an error), so that if an
+ * AJAX callback does not kill PHP execution, a '0' character will be erroneously appended to the
+ * output. All bp-default AJAX callbacks (/bp-themes/bp-default/_inc/ajax.php) have been updated
+ * for BP 1.6 so that they die() properly; any theme that dynamically includes this file will
+ * inherit the fixes. However, any theme that contains a copy of BP's pre-1.5 ajax.php file will
+ * continue to witness the 'trailing "0"' problem.
+ *
+ * This function provides a backward compatible workaround for these themes, by hooking to the
+ * BP wp_ajax_ actions that were problematic prior to BP 1.6, and killing PHP execution with die().
+ *
+ * Note that this hack only runs if the function bp_dtheme_register_actions() is not found (this
+ * function was introduced in BP 1.6 for related backward compatibility reasons).
+ *
+ * @deprecated 1.6.0
+ */
+if ( !function_exists( 'bp_dtheme_register_actions' ) ) :
+	function bp_die_legacy_ajax_callbacks() {
+
+		// This is a list of the BP wp_ajax_ hook suffixes whose associated functions did
+		// not die properly before BP 1.6
+		$actions = array(
+			// Directory template loaders
+			'members_filter',
+			'groups_filter',
+			'blogs_filter',
+			'forums_filter',
+			'messages_filter',
+
+			// Activity
+			'activity_widget_filter',
+			'activity_get_older_updates',
+			'post_update',
+			'new_activity_comment',
+			'delete_activity',
+			'delete_activity_comment',
+			'spam_activity',
+			'spam_activity_comment',
+			'activity_mark_fav',
+			'activity_mark_unfav',
+
+			// Groups
+			'groups_invite_user',
+			'joinleave_group',
+
+			// Members
+			'addremove_friend',
+			'accept_friendship',
+			'reject_friendship',
+
+			// Messages
+			'messages_close_notice',
+			'messages_send_reply',
+			'messages_markunread',
+			'messages_markread',
+			'messages_delete',
+			'messages_autocomplete_results'
+		);
+
+		// For each of the problematic hooks, exit at the very end of execution
+		foreach( $actions as $action ) {
+			add_action( 'wp_ajax_' . $action, function() {
+				exit;
+			}, 9999 );
+
+			add_action( 'wp_ajax_nopriv_' . $action, function() {
+				exit;
+			}, 9999 );
+		}
+	}
+	add_action( 'after_setup_theme', 'bp_die_legacy_ajax_callbacks', 20 );
+endif;
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/1.7.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/1.7.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/1.7.php	(revision 12265)
@@ -0,0 +1,162 @@
+<?php
+/**
+ * Deprecated Functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @deprecated Since 1.7.0
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the BuddyPress maintenance mode
+ *
+ * @since 1.6.0
+ * @deprecated 1.7.0
+ */
+function bp_maintenance_mode() {
+	echo bp_get_maintenance_mode();
+}
+	/**
+	 * Return the BuddyPress maintenance mode
+	 *
+	 * @since 1.6.0
+	 * @deprecated 1.7.0
+	 * @return string The BuddyPress maintenance mode
+	 */
+	function bp_get_maintenance_mode() {
+		return buddypress()->maintenance_mode;
+	}
+
+/**
+ * @deprecated 1.7.0
+ */
+function xprofile_get_profile() {
+	_deprecated_function( __FUNCTION__, '1.7' );
+	bp_locate_template( array( 'profile/profile-loop.php' ), true );
+}
+
+/**
+ * @deprecated 1.7.0
+ */
+function bp_get_profile_header() {
+	_deprecated_function( __FUNCTION__, '1.7' );
+	bp_locate_template( array( 'profile/profile-header.php' ), true );
+}
+
+/**
+ * @deprecated 1.7.0
+ * @param string $component_name
+ * @return boolean
+ */
+function bp_exists( $component_name ) {
+	_deprecated_function( __FUNCTION__, '1.7' );
+	if ( function_exists( $component_name . '_install' ) )
+		return true;
+
+	return false;
+}
+
+/**
+ * @deprecated 1.7.0
+ */
+function bp_get_plugin_sidebar() {
+	_deprecated_function( __FUNCTION__, '1.7' );
+	bp_locate_template( array( 'plugin-sidebar.php' ), true );
+}
+
+/**
+ * On multiblog installations you must first allow themes to be activated and
+ * show up on the theme selection screen. This function will let the BuddyPress
+ * bundled themes show up on the root blog selection screen and bypass this
+ * step. It also means that the themes won't show for selection on other blogs.
+ *
+ * @deprecated 1.7.0
+ * @return array
+ */
+function bp_core_allow_default_theme( $themes ) {
+	_deprecated_function( __FUNCTION__, '1.7' );
+
+	if ( !bp_current_user_can( 'bp_moderate' ) )
+		return $themes;
+
+	if ( bp_get_root_blog_id() != get_current_blog_id() )
+		return $themes;
+
+	if ( isset( $themes['bp-default'] ) )
+		return $themes;
+
+	$themes['bp-default'] = true;
+
+	return $themes;
+}
+
+/**
+ * No longer used by BuddyPress core
+ *
+ * @deprecated 1.7.0
+ * @param string $page
+ * @return boolean True if is BuddyPress page
+ */
+function bp_is_page( $page = '' ) {
+	_deprecated_function( __FUNCTION__, '1.7' );
+
+	if ( !bp_is_user() && bp_is_current_component( $page )  )
+		return true;
+
+	if ( 'home' == $page )
+		return is_front_page();
+
+	return false;
+}
+
+/** Admin *********************************************************************/
+
+/**
+ * This function was originally used to update pre-1.1 schemas, but that was
+ * before we had a legitimate update process.
+ *
+ * @deprecated 1.7.0
+ * @global WPDB $wpdb
+ */
+function bp_update_db_stuff() {
+	global $wpdb;
+
+	$bp        = buddypress();
+	$bp_prefix = bp_core_get_table_prefix();
+
+	// Rename the old user activity cached table if needed.
+	if ( $wpdb->get_var( "SHOW TABLES LIKE '%{$bp_prefix}bp_activity_user_activity_cached%'" ) ) {
+		$wpdb->query( "RENAME TABLE {$bp_prefix}bp_activity_user_activity_cached TO {$bp->activity->table_name}" );
+	}
+
+	// Rename fields from pre BP 1.2
+	if ( $wpdb->get_var( "SHOW TABLES LIKE '%{$bp->activity->table_name}%'" ) ) {
+		if ( $wpdb->get_var( "SHOW COLUMNS FROM {$bp->activity->table_name} LIKE 'component_action'" ) ) {
+			$wpdb->query( "ALTER TABLE {$bp->activity->table_name} CHANGE component_action type varchar(75) NOT NULL" );
+		}
+
+		if ( $wpdb->get_var( "SHOW COLUMNS FROM {$bp->activity->table_name} LIKE 'component_name'" ) ) {
+			$wpdb->query( "ALTER TABLE {$bp->activity->table_name} CHANGE component_name component varchar(75) NOT NULL" );
+		}
+	}
+
+	// On first installation - record all existing blogs in the system.
+	if ( !(int) $bp->site_options['bp-blogs-first-install'] ) {
+		bp_blogs_record_existing_blogs();
+		bp_update_option( 'bp-blogs-first-install', 1 );
+	}
+
+	if ( is_multisite() ) {
+		bp_core_add_illegal_names();
+	}
+
+	// Update and remove the message threads table if it exists
+	if ( $wpdb->get_var( "SHOW TABLES LIKE '%{$bp_prefix}bp_messages_threads%'" ) ) {
+		if ( BP_Messages_Thread::update_tables() ) {
+			$wpdb->query( "DROP TABLE {$bp_prefix}bp_messages_threads" );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/1.9.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/1.9.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/1.9.php	(revision 12265)
@@ -0,0 +1,267 @@
+<?php
+/**
+ * BuddyPress Member Notifications
+ *
+ * Backwards compatibility functions and filters used for member notifications.
+ * Use bp-notifications instead.
+ *
+ * @package BuddyPress
+ * @subpackage MembersNotifications
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add a notification for a specific user, from a specific component.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_add_notification() instead.
+ *
+ * @since 1.0.0
+ * @param string       $item_id
+ * @param int          $user_id
+ * @param string       $component_name
+ * @param string       $component_action
+ * @param int          $secondary_item_id
+ * @param false|string $date_notified
+ * @param int          $is_new
+ * @return int|boolean True on success, false on failure.
+ */
+function bp_core_add_notification( $item_id, $user_id, $component_name, $component_action, $secondary_item_id = 0, $date_notified = false, $is_new = 1 ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_add_notification()' );
+
+	// Notifications must always have a time
+	if ( false === $date_notified ) {
+		$date_notified = bp_core_current_time();
+	}
+
+	// Add the notification
+	return bp_notifications_add_notification( array(
+		'item_id'           => $item_id,
+		'user_id'           => $user_id,
+		'component_name'    => $component_name,
+		'component_action'  => $component_action,
+		'secondary_item_id' => $secondary_item_id,
+		'date_notified'     => $date_notified,
+		'is_new'            => $is_new
+	) );
+}
+
+/**
+ * Delete a specific notification by its ID.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_delete_notification() instead.
+ *
+ * @since 1.0.0
+ *
+ * @param int $id ID of notification.
+ * @return false|integer True on success, false on failure.
+ */
+function bp_core_delete_notification( $id ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_delete_notification()' );
+
+	return BP_Notifications_Notification::delete_by_id( $id );
+}
+
+/**
+ * Get a specific notification by its ID.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_get_notification() instead.
+ *
+ * @since 1.0.0
+ * @param int $id ID of notification.
+ * @return false|BP_Core_Notification
+ */
+function bp_core_get_notification( $id ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_get_notification()' );
+
+	return bp_notifications_get_notification( $id );
+}
+
+/**
+ * Get notifications for a specific user.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_get_notifications_for_user() instead.
+ *
+ * @since 1.0.0
+ * @param int $user_id ID of user.
+ * @param string $format
+ * @return boolean Object or array on success, false on failure.
+ */
+function bp_core_get_notifications_for_user( $user_id, $format = 'string' ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_get_notifications_for_user()' );
+
+	return bp_notifications_get_notifications_for_user( $user_id, $format );
+}
+
+/** Delete ********************************************************************/
+
+/**
+ * Delete notifications for a user by type.
+ *
+ * Used when clearing out notifications for a specific component when the user
+ * has visited that component.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_delete_notifications_by_type() instead.
+ *
+ * @since 1.0.0
+ * @param int $user_id
+ * @param string $component_name
+ * @param string $component_action
+ * @return false|int True on success, false on failure.
+ */
+function bp_core_delete_notifications_by_type( $user_id, $component_name, $component_action ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_delete_notifications_by_type()' );
+
+	return bp_notifications_delete_notifications_by_type( $user_id, $component_name, $component_action );
+}
+
+/**
+ * Delete notifications for an item ID.
+ *
+ * Used when clearing out notifications for a specific component when the user
+ * has visited that component.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_delete_notifications_by_item_id() instead.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id
+ * @param string $component_name
+ * @param string $component_action
+ * @return false|int True on success, false on failure.
+ */
+function bp_core_delete_notifications_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id = false ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_delete_notifications_by_item_id()' );
+
+	return bp_notifications_delete_notifications_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id );
+}
+
+/**
+ * Delete all notifications for by type.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_delete_all_notifications_by_type() instead.
+ *
+ * @since 1.0.0
+ *
+ * @param int          $user_id
+ * @param string       $component_name
+ * @param false|string $component_action
+ * @return boolean True on success, false on failure.
+ */
+function bp_core_delete_all_notifications_by_type( $item_id, $component_name, $component_action = false, $secondary_item_id = false ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_delete_all_notifications_by_type()' );
+
+	bp_notifications_delete_all_notifications_by_type( $item_id, $component_name, $component_action, $secondary_item_id );
+}
+
+/**
+ * Delete all notifications for a user.
+ *
+ * Used when clearing out all notifications for a user, when deleted or spammed
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_delete_notifications_from_user() instead.
+ *
+ * @since 1.0.0
+ * @param int $user_id
+ * @param string $component_name
+ * @param string $component_action
+ * @return false|int True on success, false on failure.
+ */
+function bp_core_delete_notifications_from_user( $user_id, $component_name, $component_action ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_delete_notifications_from_user()' );
+
+	return bp_notifications_delete_notifications_from_user( $user_id, $component_name, $component_action );
+}
+
+/** Helpers *******************************************************************/
+
+/**
+ * Check if a user has access to a specific notification.
+ *
+ * Used before deleting a notification for a user.
+ *
+ * @deprecated Deprecated since BuddyPress 1.9.0. Use
+ *             bp_notifications_check_notification_access() instead.
+ *
+ * @since 1.0.0
+ * @param int $user_id
+ * @param int $notification_id
+ * @return boolean True on success, false on failure.
+ */
+function bp_core_check_notification_access( $user_id, $notification_id ) {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	// Trigger the deprecated function notice
+	_deprecated_function( __FUNCTION__, '1.9', 'bp_notifications_check_notification_access()' );
+
+	return bp_notifications_check_notification_access( $user_id, $notification_id );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.0.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.0.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.0.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Deprecated Functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @deprecated 2.0.0
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * @deprecated 2.0.0
+ */
+function bp_activity_clear_meta_cache_for_activity() {
+	_deprecated_function( __FUNCTION__, '2.0.0', 'Use WP metadata API instead' );
+}
+
+/**
+ * @deprecated 2.0.0
+ */
+function bp_blogs_catch_published_post() {
+	_deprecated_function( __FUNCTION__, '2.0', 'bp_blogs_catch_transition_post_status()' );
+}
+
+/**
+ * @deprecated 2.0.0
+ */
+function bp_messages_screen_inbox_mark_notifications() {
+	_deprecated_function( __FUNCTION__, '2.0', 'bp_messages_screen_conversation_mark_notifications()' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.1.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.1.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.1.php	(revision 12265)
@@ -0,0 +1,599 @@
+<?php
+/**
+ * Deprecated functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @deprecated 2.1.0
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register (not enqueue) scripts that used to be used by BuddyPress.
+ *
+ * @since 2.1.0
+ */
+function bp_core_register_deprecated_scripts() {
+	// Scripts undeprecated as of 2.5.0.
+}
+
+/**
+ * Register (not enqueue) styles that used to be used by BuddyPress.
+ *
+ * @since 2.1.0
+ */
+function bp_core_register_deprecated_styles() {
+	// Scripts undeprecated as of 2.5.0.
+}
+
+/** BuddyBar *****************************************************************/
+
+/**
+ * Add a Sites menu to the BuddyBar.
+ *
+ * @since 1.0.0
+ * @deprecated 2.1.0
+ *
+ * @return false|null Returns false on failure. Otherwise echoes the menu item.
+ */
+function bp_adminbar_blogs_menu() {
+
+	if ( ! is_user_logged_in() || ! bp_is_active( 'blogs' ) ) {
+		return false;
+	}
+
+	if ( ! is_multisite() ) {
+		return false;
+	}
+
+	$blogs = wp_cache_get( 'bp_blogs_of_user_' . bp_loggedin_user_id() . '_inc_hidden', 'bp' );
+	if ( empty( $blogs ) ) {
+		$blogs = bp_blogs_get_blogs_for_user( bp_loggedin_user_id(), true );
+		wp_cache_set( 'bp_blogs_of_user_' . bp_loggedin_user_id() . '_inc_hidden', $blogs, 'bp' );
+	}
+
+	$counter = 0;
+	if ( is_array( $blogs['blogs'] ) && (int) $blogs['count'] ) {
+
+		echo '<li id="bp-adminbar-blogs-menu"><a href="' . trailingslashit( bp_loggedin_user_domain() . bp_get_blogs_slug() ) . '">';
+
+		_e( 'My Sites', 'buddypress' );
+
+		echo '</a>';
+		echo '<ul>';
+
+		foreach ( (array) $blogs['blogs'] as $blog ) {
+			$alt      = ( 0 == $counter % 2 ) ? ' class="alt"' : '';
+			$site_url = esc_attr( $blog->siteurl );
+
+			echo '<li' . $alt . '>';
+			echo '<a href="' . $site_url . '">' . esc_html( $blog->name ) . '</a>';
+			echo '<ul>';
+			echo '<li class="alt"><a href="' . $site_url . 'wp-admin/">' . __( 'Dashboard', 'buddypress' ) . '</a></li>';
+			echo '<li><a href="' . $site_url . 'wp-admin/post-new.php">' . __( 'New Post', 'buddypress' ) . '</a></li>';
+			echo '<li class="alt"><a href="' . $site_url . 'wp-admin/edit.php">' . __( 'Manage Posts', 'buddypress' ) . '</a></li>';
+			echo '<li><a href="' . $site_url . 'wp-admin/edit-comments.php">' . __( 'Manage Comments', 'buddypress' ) . '</a></li>';
+			echo '</ul>';
+
+			do_action( 'bp_adminbar_blog_items', $blog );
+
+			echo '</li>';
+			$counter++;
+		}
+
+		$alt = ( 0 == $counter % 2 ) ? ' class="alt"' : '';
+
+		if ( bp_blog_signup_enabled() ) {
+			echo '<li' . $alt . '>';
+			echo '<a href="' . trailingslashit( bp_get_blogs_directory_permalink() . 'create' ) . '">' . __( 'Create a Site!', 'buddypress' ) . '</a>';
+			echo '</li>';
+		}
+
+		echo '</ul>';
+		echo '</li>';
+	}
+}
+
+/**
+ * If user has upgraded to 1.6 and chose to retain their BuddyBar, offer then a switch to change over
+ * to the WP Toolbar.
+ *
+ * @since 1.6.0
+ * @deprecated 2.1.0
+ */
+function bp_admin_setting_callback_force_buddybar() {
+?>
+
+	<input id="_bp_force_buddybar" name="_bp_force_buddybar" type="checkbox" value="1" <?php checked( ! bp_force_buddybar( true ) ); ?> />
+	<label for="_bp_force_buddybar"><?php _e( 'Switch to WordPress Toolbar', 'buddypress' ); ?></label>
+
+<?php
+}
+
+
+/**
+ * Sanitization for _bp_force_buddybar
+ *
+ * If upgraded to 1.6 and you chose to keep the BuddyBar, a checkbox asks if you want to switch to
+ * the WP Toolbar. The option we store is 1 if the BuddyBar is forced on, so we use this function
+ * to flip the boolean before saving the intval.
+ *
+ * @since 1.6.0
+ * @deprecated 2.1.0
+ * @access Private
+ */
+function bp_admin_sanitize_callback_force_buddybar( $value = false ) {
+	return $value ? 0 : 1;
+}
+
+/**
+ * Wrapper function for rendering the BuddyBar.
+ *
+ * @return false|null Returns false if the BuddyBar is disabled.
+ * @deprecated 2.1.0
+ */
+function bp_core_admin_bar() {
+	$bp = buddypress();
+
+	if ( defined( 'BP_DISABLE_ADMIN_BAR' ) && BP_DISABLE_ADMIN_BAR ) {
+		return false;
+	}
+
+	if ( (int) bp_get_option( 'hide-loggedout-adminbar' ) && !is_user_logged_in() ) {
+		return false;
+	}
+
+	$bp->doing_admin_bar = true;
+
+	echo '<div id="wp-admin-bar"><div class="padder">';
+
+	// **** Do bp-adminbar-logo Actions ********
+	do_action( 'bp_adminbar_logo' );
+
+	echo '<ul class="main-nav">';
+
+	// **** Do bp-adminbar-menus Actions ********
+	do_action( 'bp_adminbar_menus' );
+
+	echo '</ul>';
+	echo "</div></div><!-- #wp-admin-bar -->\n\n";
+
+	$bp->doing_admin_bar = false;
+}
+
+/**
+ * Output the BuddyBar logo.
+ *
+ * @deprecated 2.1.0
+ */
+function bp_adminbar_logo() {
+	echo '<a href="' . bp_get_root_domain() . '" id="admin-bar-logo">' . get_blog_option( bp_get_root_blog_id(), 'blogname' ) . '</a>';
+}
+
+/**
+ * Output the "Log In" and "Sign Up" names to the BuddyBar.
+ *
+ * Visible only to visitors who are not logged in.
+ *
+ * @deprecated 2.1.0
+ *
+ * @return false|null Returns false if the current user is logged in.
+ */
+function bp_adminbar_login_menu() {
+
+	if ( is_user_logged_in() ) {
+		return false;
+	}
+
+	echo '<li class="bp-login no-arrow"><a href="' . wp_login_url() . '">' . __( 'Log In', 'buddypress' ) . '</a></li>';
+
+	// Show "Sign Up" link if user registrations are allowed
+	if ( bp_get_signup_allowed() ) {
+		echo '<li class="bp-signup no-arrow"><a href="' . bp_get_signup_page() . '">' . __( 'Sign Up', 'buddypress' ) . '</a></li>';
+	}
+}
+
+/**
+ * Output the My Account BuddyBar menu.
+ *
+ * @deprecated 2.1.0
+ *
+ * @return false|null Returns false on failure.
+ */
+function bp_adminbar_account_menu() {
+	$bp = buddypress();
+
+	if ( empty( $bp->bp_nav ) || ! is_user_logged_in() ) {
+		return false;
+	}
+
+	echo '<li id="bp-adminbar-account-menu"><a href="' . bp_loggedin_user_domain() . '">';
+	echo __( 'My Account', 'buddypress' ) . '</a>';
+	echo '<ul>';
+
+	// Loop through each navigation item
+	$counter = 0;
+	foreach( (array) $bp->bp_nav as $nav_item ) {
+		$alt = ( 0 == $counter % 2 ) ? ' class="alt"' : '';
+
+		if ( -1 == $nav_item['position'] ) {
+			continue;
+		}
+
+		echo '<li' . $alt . '>';
+		echo '<a id="bp-admin-' . $nav_item['css_id'] . '" href="' . $nav_item['link'] . '">' . $nav_item['name'] . '</a>';
+
+		if ( isset( $bp->bp_options_nav[$nav_item['slug']] ) && is_array( $bp->bp_options_nav[$nav_item['slug']] ) ) {
+			echo '<ul>';
+			$sub_counter = 0;
+
+			foreach( (array) $bp->bp_options_nav[$nav_item['slug']] as $subnav_item ) {
+				$link = $subnav_item['link'];
+				$name = $subnav_item['name'];
+
+				if ( bp_displayed_user_domain() ) {
+					$link = str_replace( bp_displayed_user_domain(), bp_loggedin_user_domain(), $subnav_item['link'] );
+				}
+
+				if ( isset( $bp->displayed_user->userdata->user_login ) ) {
+					$name = str_replace( $bp->displayed_user->userdata->user_login, $bp->loggedin_user->userdata->user_login, $subnav_item['name'] );
+				}
+
+				$alt = ( 0 == $sub_counter % 2 ) ? ' class="alt"' : '';
+				echo '<li' . $alt . '><a id="bp-admin-' . $subnav_item['css_id'] . '" href="' . $link . '">' . $name . '</a></li>';
+				$sub_counter++;
+			}
+			echo '</ul>';
+		}
+
+		echo '</li>';
+
+		$counter++;
+	}
+
+	$alt = ( 0 == $counter % 2 ) ? ' class="alt"' : '';
+
+	echo '<li' . $alt . '><a id="bp-admin-logout" class="logout" href="' . wp_logout_url( home_url() ) . '">' . __( 'Log Out', 'buddypress' ) . '</a></li>';
+	echo '</ul>';
+	echo '</li>';
+}
+
+function bp_adminbar_thisblog_menu() {
+	if ( current_user_can( 'edit_posts' ) ) {
+		echo '<li id="bp-adminbar-thisblog-menu"><a href="' . admin_url() . '">';
+		_e( 'Dashboard', 'buddypress' );
+		echo '</a>';
+		echo '<ul>';
+
+		echo '<li class="alt"><a href="' . admin_url() . 'post-new.php">' . __( 'New Post', 'buddypress' ) . '</a></li>';
+		echo '<li><a href="' . admin_url() . 'edit.php">' . __( 'Manage Posts', 'buddypress' ) . '</a></li>';
+		echo '<li class="alt"><a href="' . admin_url() . 'edit-comments.php">' . __( 'Manage Comments', 'buddypress' ) . '</a></li>';
+
+		do_action( 'bp_adminbar_thisblog_items' );
+
+		echo '</ul>';
+		echo '</li>';
+	}
+}
+
+/**
+ * Output the Random BuddyBar menu.
+ *
+ * Not visible for logged-in users.
+ *
+ * @deprecated 2.1.0
+ */
+function bp_adminbar_random_menu() {
+?>
+
+	<li class="align-right" id="bp-adminbar-visitrandom-menu">
+		<a href="#"><?php _e( 'Visit', 'buddypress' ) ?></a>
+		<ul class="random-list">
+			<li><a href="<?php bp_members_directory_permalink(); ?>?random-member" rel="nofollow"><?php _e( 'Random Member', 'buddypress' ) ?></a></li>
+
+			<?php if ( bp_is_active( 'groups' ) ) : ?>
+
+				<li class="alt"><a href="<?php bp_groups_directory_permalink(); ?>?random-group"  rel="nofollow"><?php _e( 'Random Group', 'buddypress' ) ?></a></li>
+
+			<?php endif; ?>
+
+			<?php if ( is_multisite() && bp_is_active( 'blogs' ) ) : ?>
+
+				<li><a href="<?php bp_blogs_directory_permalink(); ?>?random-blog"  rel="nofollow"><?php _e( 'Random Site', 'buddypress' ) ?></a></li>
+
+			<?php endif; ?>
+
+			<?php do_action( 'bp_adminbar_random_menu' ) ?>
+
+		</ul>
+	</li>
+
+	<?php
+}
+
+/**
+ * Enqueue the BuddyBar CSS.
+ *
+ * @deprecated 2.1.0
+ */
+function bp_core_load_buddybar_css() {
+
+	if ( bp_use_wp_admin_bar() || ( (int) bp_get_option( 'hide-loggedout-adminbar' ) && !is_user_logged_in() ) || ( defined( 'BP_DISABLE_ADMIN_BAR' ) && BP_DISABLE_ADMIN_BAR ) ) {
+		return;
+	}
+
+	$min = bp_core_get_minified_asset_suffix();
+
+	if ( file_exists( get_stylesheet_directory() . '/_inc/css/adminbar.css' ) ) { // Backwards compatibility
+		$stylesheet = get_stylesheet_directory_uri() . '/_inc/css/adminbar.css';
+	} else {
+		$stylesheet = buddypress()->plugin_url . "bp-core/css/buddybar{$min}.css";
+	}
+
+	wp_enqueue_style( 'bp-admin-bar', apply_filters( 'bp_core_buddybar_rtl_css', $stylesheet ), array(), bp_get_version() );
+
+	wp_style_add_data( 'bp-admin-bar', 'rtl', true );
+	if ( $min ) {
+		wp_style_add_data( 'bp-admin-bar', 'suffix', $min );
+	}
+}
+add_action( 'bp_init', 'bp_core_load_buddybar_css' );
+
+/**
+ * Add menu items to the BuddyBar.
+ *
+ * @since 1.0.0
+ *
+ * @deprecated 2.1.0
+ */
+function bp_groups_adminbar_admin_menu() {
+	$bp = buddypress();
+
+	if ( empty( $bp->groups->current_group ) ) {
+		return false;
+	}
+
+	// Only group admins and site admins can see this menu
+	if ( !current_user_can( 'edit_users' ) && !bp_current_user_can( 'bp_moderate' ) && !bp_is_item_admin() ) {
+		return false;
+	} ?>
+
+	<li id="bp-adminbar-adminoptions-menu">
+		<a href="<?php bp_groups_action_link( 'admin' ); ?>"><?php _e( 'Admin Options', 'buddypress' ); ?></a>
+
+		<ul>
+			<li><a href="<?php bp_groups_action_link( 'admin/edit-details' ); ?>"><?php _e( 'Edit Details', 'buddypress' ); ?></a></li>
+
+			<li><a href="<?php bp_groups_action_link( 'admin/group-settings' );  ?>"><?php _e( 'Group Settings', 'buddypress' ); ?></a></li>
+
+			<?php if ( !(int)bp_get_option( 'bp-disable-avatar-uploads' ) && $bp->avatar->show_avatars ) : ?>
+
+				<li><a href="<?php bp_groups_action_link( 'admin/group-avatar' ); ?>"><?php _e( 'Group Profile Photo', 'buddypress' ); ?></a></li>
+
+			<?php endif; ?>
+
+			<?php if ( bp_is_active( 'friends' ) ) : ?>
+
+				<li><a href="<?php bp_groups_action_link( 'send-invites' ); ?>"><?php _e( 'Manage Invitations', 'buddypress' ); ?></a></li>
+
+			<?php endif; ?>
+
+			<li><a href="<?php bp_groups_action_link( 'admin/manage-members' ); ?>"><?php _e( 'Manage Members', 'buddypress' ); ?></a></li>
+
+			<?php if ( $bp->groups->current_group->status == 'private' ) : ?>
+
+				<li><a href="<?php bp_groups_action_link( 'admin/membership-requests' ); ?>"><?php _e( 'Membership Requests', 'buddypress' ); ?></a></li>
+
+			<?php endif; ?>
+
+			<li><a class="confirm" href="<?php echo wp_nonce_url( bp_get_group_permalink( $bp->groups->current_group ) . 'admin/delete-group/', 'groups_delete_group' ); ?>&amp;delete-group-button=1&amp;delete-group-understand=1"><?php _e( "Delete Group", 'buddypress' ) ?></a></li>
+
+			<?php do_action( 'bp_groups_adminbar_admin_menu' ) ?>
+
+		</ul>
+	</li>
+
+	<?php
+}
+add_action( 'bp_adminbar_menus', 'bp_groups_adminbar_admin_menu', 20 );
+
+/**
+ * Add the Notifications menu to the BuddyBar.
+ *
+ * @deprecated 2.1.0
+ */
+function bp_adminbar_notifications_menu() {
+
+	// Bail if notifications is not active
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	bp_notifications_buddybar_menu();
+}
+add_action( 'bp_adminbar_menus', 'bp_adminbar_notifications_menu', 8 );
+
+/**
+ * Add the Blog Authors menu to the BuddyBar (visible when not logged in).
+ *
+ * @deprecated 2.1.0
+ */
+function bp_adminbar_authors_menu() {
+	global $wpdb;
+
+	// Only for multisite
+	if ( ! is_multisite() ) {
+		return false;
+	}
+
+	// Hide on root blog
+	if ( bp_is_root_blog( $wpdb->blogid ) || ! bp_is_active( 'blogs' ) ) {
+		return false;
+	}
+
+	$blog_prefix = $wpdb->get_blog_prefix( $wpdb->blogid );
+	$authors     = $wpdb->get_results( "SELECT user_id, user_login, user_nicename, display_name, user_email, meta_value as caps FROM $wpdb->users u, $wpdb->usermeta um WHERE u.ID = um.user_id AND meta_key = '{$blog_prefix}capabilities' ORDER BY um.user_id" );
+
+	if ( !empty( $authors ) ) {
+		// This is a blog, render a menu with links to all authors
+		echo '<li id="bp-adminbar-authors-menu"><a href="/">';
+		_e('Blog Authors', 'buddypress');
+		echo '</a>';
+
+		echo '<ul class="author-list">';
+		foreach( (array) $authors as $author ) {
+			$caps = maybe_unserialize( $author->caps );
+			if ( isset( $caps['subscriber'] ) || isset( $caps['contributor'] ) ) {
+				continue;
+			}
+
+			echo '<li>';
+			echo '<a href="' . bp_core_get_user_domain( $author->user_id, $author->user_nicename, $author->user_login ) . '">';
+			echo bp_core_fetch_avatar( array(
+				'item_id' => $author->user_id,
+				'email'   => $author->user_email,
+				'width'   => 15,
+				'height'  => 15,
+				'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), $author->display_name )
+			) );
+			echo ' ' . $author->display_name . '</a>';
+			echo '<div class="admin-bar-clear"></div>';
+			echo '</li>';
+		}
+		echo '</ul>';
+		echo '</li>';
+	}
+}
+add_action( 'bp_adminbar_menus', 'bp_adminbar_authors_menu', 12 );
+
+/**
+ * Add a member admin menu to the BuddyBar.
+ *
+ * Adds an Toolbar menu to any profile page providing site moderator actions
+ * that allow capable users to clean up a users account.
+ *
+ * @deprecated 2.1.0
+ */
+function bp_members_adminbar_admin_menu() {
+
+	// Only show if viewing a user
+	if ( ! bp_displayed_user_id() ) {
+		return false;
+	}
+
+	// Don't show this menu to non site admins or if you're viewing your own profile
+	if ( !current_user_can( 'edit_users' ) || bp_is_my_profile() ) {
+		return false;
+	} ?>
+
+	<li id="bp-adminbar-adminoptions-menu">
+
+		<a href=""><?php _e( 'Admin Options', 'buddypress' ) ?></a>
+
+		<ul>
+			<?php if ( bp_is_active( 'xprofile' ) ) : ?>
+
+				<li><a href="<?php bp_members_component_link( 'profile', 'edit' ); ?>"><?php printf( __( "Edit %s's Profile", 'buddypress' ), esc_attr( bp_get_displayed_user_fullname() ) ) ?></a></li>
+
+			<?php endif ?>
+
+			<li><a href="<?php bp_members_component_link( 'profile', 'change-avatar' ); ?>"><?php printf( __( "Edit %s's Profile Photo", 'buddypress' ), esc_attr( bp_get_displayed_user_fullname() ) ) ?></a></li>
+
+			<li><a href="<?php bp_members_component_link( 'settings', 'capabilities' ); ?>"><?php _e( 'User Capabilities', 'buddypress' ); ?></a></li>
+
+			<li><a href="<?php bp_members_component_link( 'settings', 'delete-account' ); ?>"><?php printf( __( "Delete %s's Account", 'buddypress' ), esc_attr( bp_get_displayed_user_fullname() ) ); ?></a></li>
+
+			<?php do_action( 'bp_members_adminbar_admin_menu' ) ?>
+
+		</ul>
+	</li>
+
+	<?php
+}
+add_action( 'bp_adminbar_menus', 'bp_members_adminbar_admin_menu', 20 );
+
+/**
+ * Create the Notifications menu for the BuddyBar.
+ *
+ * @since 1.9.0
+ * @deprecated 2.1.0
+ */
+function bp_notifications_buddybar_menu() {
+
+	if ( ! is_user_logged_in() ) {
+		return false;
+	}
+
+	echo '<li id="bp-adminbar-notifications-menu"><a href="' . esc_url( bp_loggedin_user_domain() ) . '">';
+	_e( 'Notifications', 'buddypress' );
+
+	$notification_count = bp_notifications_get_unread_notification_count( bp_loggedin_user_id() );
+	$notifications      = bp_notifications_get_notifications_for_user( bp_loggedin_user_id() );
+
+	if ( ! empty( $notification_count ) ) : ?>
+		<span><?php echo bp_core_number_format( $notification_count ); ?></span>
+	<?php
+	endif;
+
+	echo '</a>';
+	echo '<ul>';
+
+	if ( ! empty( $notifications ) ) {
+		$counter = 0;
+		for ( $i = 0, $count = count( $notifications ); $i < $count; ++$i ) {
+			$alt = ( 0 == $counter % 2 ) ? ' class="alt"' : ''; ?>
+
+			<li<?php echo $alt ?>><?php echo $notifications[$i] ?></li>
+
+			<?php $counter++;
+		}
+	} else { ?>
+
+		<li><a href="<?php echo esc_url( bp_loggedin_user_domain() ); ?>"><?php _e( 'No new notifications.', 'buddypress' ); ?></a></li>
+
+	<?php
+	}
+
+	echo '</ul>';
+	echo '</li>';
+}
+add_action( 'bp_adminbar_menus', 'bp_adminbar_notifications_menu', 8 );
+
+/**
+ * Output the base URL for subdomain installations of WordPress Multisite.
+ *
+ * @since 1.6.0
+ *
+ * @deprecated 2.1.0
+ */
+function bp_blogs_subdomain_base() {
+	_deprecated_function( __FUNCTION__, '2.1', 'bp_signup_subdomain_base()' );
+	echo bp_signup_get_subdomain_base();
+}
+
+/**
+ * Return the base URL for subdomain installations of WordPress Multisite.
+ *
+ * @since 1.6.0
+ *
+ * @return string The base URL - eg, 'example.com' for site_url() example.com or www.example.com.
+ *
+ * @deprecated 2.1.0
+ */
+function bp_blogs_get_subdomain_base() {
+	_deprecated_function( __FUNCTION__, '2.1', 'bp_signup_get_subdomain_base()' );
+	return bp_signup_get_subdomain_base();
+}
+
+/**
+ * Allegedly output an avatar upload form, but it hasn't done that since 2009.
+ *
+ * @since 1.0.0
+ * @deprecated 2.1.0
+ */
+function bp_avatar_upload_form() {
+	_deprecated_function(__FUNCTION__, '2.1', 'No longer used' );
+}
+
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.2.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.2.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.2.php	(revision 12265)
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Deprecated functions
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @deprecated 2.2.0
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Detect a change in post status, and initiate an activity update if necessary.
+ *
+ * Posts get new activity updates when (a) they are being published, and (b)
+ * they have not already been published. This enables proper posting for
+ * regular posts as well as scheduled posts, while preventing post bumping.
+ *
+ * See #4090, #3746, #2546 for background.
+ *
+ * @since 2.0.0
+ * @deprecated 2.2.0
+ *
+ * @todo Support untrashing better
+ *
+ * @param string $new_status New status for the post.
+ * @param string $old_status Old status for the post.
+ * @param object $post Post data.
+ */
+function bp_blogs_catch_transition_post_status( $new_status, $old_status, $post ) {
+	_deprecated_function( __FUNCTION__, '2.2', 'bp_activity_catch_transition_post_type_status()' );
+	bp_activity_catch_transition_post_type_status( $new_status, $old_status, $post );
+}
+
+/**
+ * Record a new blog post in the BuddyPress activity stream.
+ *
+ * @deprecated 2.2.0
+ *
+ * @param int $post_id ID of the post being recorded.
+ * @param object $post The WP post object passed to the 'save_post' action.
+ * @param int $user_id Optional. The user to whom the activity item will be
+ *        associated. Defaults to the post_author.
+ * @return bool|null Returns false on failure.
+ */
+function bp_blogs_record_post( $post_id, $post, $user_id = 0 ) {
+	_deprecated_function( __FUNCTION__, '2.2', 'bp_activity_post_type_publish()' );
+	bp_activity_post_type_publish( $post_id, $post, $user_id );
+}
+
+/**
+ * Updates a blog post's corresponding activity entry during a post edit.
+ *
+ * @since 2.0.0
+ * @deprecated 2.2.0
+ *
+ * @see bp_blogs_catch_transition_post_status()
+ *
+ * @param WP_Post $post
+ */
+function bp_blogs_update_post( $post ) {
+	_deprecated_function( __FUNCTION__, '2.2', 'bp_activity_post_type_update()' );
+	bp_activity_post_type_update( $post );
+}
+
+/**
+ * Clear cache when a new blog is created.
+ *
+ * @since 1.0.0
+ * @deprecated 2.2.0
+ *
+ * @param BP_Blogs_Blog $recorded_blog_obj The recorded blog, passed by
+ *        'bp_blogs_new_blog'.
+ */
+function bp_blogs_format_clear_blog_cache( $recorded_blog_obj ) {
+	_deprecated_function( __FUNCTION__, '2.2', 'bp_blogs_clear_blog_object_cache()' );
+	bp_blogs_clear_blog_object_cache( false, $recorded_blog_obj->user_id );
+}
+
+/**
+ * Format 'new_member' activity actions.
+ *
+ * @since 2.0.0
+ * @deprecated 2.2.0
+ *
+ * @param string $action Static activity action.
+ * @param object $activity Activity object.
+ * @return string
+ */
+function bp_xprofile_format_activity_action_new_member( $action, $activity ) {
+	_deprecated_function( __FUNCTION__, '2.2', 'bp_members_format_activity_action_new_member()' );
+
+	$action = apply_filters( 'bp_xprofile_format_activity_action_new_member', $action, $activity );
+	return bp_members_format_activity_action_new_member( $action, $activity );
+}
+
+/**
+ * Add 'bp' to global group of network wide cachable objects.
+ *
+ * @since 1.1.0
+ * @deprecated 2.2.0
+ */
+function bp_core_add_global_group() {
+	_deprecated_function( __FUNCTION__, '2.2', 'This function has no replacement' );
+}
+
+/**
+ * Add a piece of message metadata.
+ *
+ * @deprecated 2.2.2
+ */
+function bp_message_add_meta( $message_id, $meta_key, $meta_value, $unique = false ) {
+	_deprecated_function( __FUNCTION__, '2.3.0', 'bp_messages_add_meta()' );
+	return bp_messages_add_meta( $message_id, $meta_key, $meta_value, $unique );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.3.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.3.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.3.php	(revision 12265)
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Return the referrer URL without the http(s)://
+ *
+ * @deprecated 2.3.0
+ *
+ * @return string The referrer URL.
+ */
+function bp_core_referrer() {
+	_deprecated_function( __FUNCTION__, '2.3.0', 'bp_get_referer_path()' );
+	$referer = explode( '/', wp_get_referer() );
+	unset( $referer[0], $referer[1], $referer[2] );
+	return implode( '/', $referer );
+}
+
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.4.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.4.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.4.php	(revision 12265)
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.4.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * @deprecated 2.4.0
+ */
+function bp_group_avatar_edit_form() {
+	_deprecated_function( __FUNCTION__, '2.4' );
+}
+
+/**
+ * @deprecated 2.4.0
+ *
+ * @param bool $deprecated Param deprecated 1.1.0
+ */
+function groups_avatar_upload( $deprecated = true ) {
+	_deprecated_function( __FUNCTION__, '2.4' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.5.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.5.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.5.php	(revision 12265)
@@ -0,0 +1,934 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set "From" name in outgoing email to the site name.
+ *
+ * @since 1.0.0
+ * @deprecated 2.5.0 Not used. Was hooked to WordPress' "wp_mail_from_name" action.
+ *                   Use the "bp_email_get_from" action instead.
+ *
+ * @return string The blog name for the root blog.
+ */
+function bp_core_email_from_name_filter() {
+	_deprecated_function( __FUNCTION__, '2.5' );
+
+	/**
+	 * Filters the "From" name in outgoing email to the site name.
+	 *
+	 * @since 1.2.0
+	 * @deprecated 2.5.0 Not used.
+	 *
+	 * @param string $value Value to set the "From" name to.
+	 */
+	return apply_filters( 'bp_core_email_from_name_filter', bp_get_option( 'blogname', 'WordPress' ) );
+}
+
+/**
+ * Add support for pre-2.5 email filters.
+ *
+ * @since 2.5.0
+ *
+ * @param mixed $value
+ * @param string $property Name of property.
+ * @param string $transform Return value transformation. Unused.
+ * @param BP_Email $email Email object reference.
+ * @return mixed
+ */
+function bp_core_deprecated_email_filters( $value, $property, $transform, $email ) {
+	$pre_2_5_emails = array(
+		'activity-at-message',
+		'activity-comment',
+		'activity-comment-author',
+		'core-user-registration',
+		'core-user-registration-with-blog',
+		'friends-request',
+		'friends-request-accepted',
+		'groups-at-message',
+		'groups-details-updated',
+		'groups-invitation',
+		'groups-member-promoted',
+		'groups-membership-request',
+		'groups-membership-request-accepted',
+		'groups-membership-request-rejected',
+		'messages-unread',
+		'settings-verify-email-change',
+	);
+
+	remove_filter( 'bp_email_get_property', 'bp_core_deprecated_email_filters', 20 );
+	$email_type = $email->get( 'type' );
+	$tokens     = $email->get( 'tokens' );
+	add_filter( 'bp_email_get_property', 'bp_core_deprecated_email_filters', 20, 4 );
+
+	// Backpat for pre-2.5 emails only.
+	if ( ! in_array( $email_type, $pre_2_5_emails, true ) ) {
+		return $value;
+	}
+
+	$original_value = $value;
+	$to_changed     = false;
+
+	if ( $property === 'to' && $original_value ) {
+		$to_changed     = true;
+
+		$value          = array_shift( $original_value );
+		$recipient_name = $value->get_name();     // Value - name
+		$value          = $value->get_address();  // Key   - email
+	}
+
+	if ( $email_type === 'activity-comment' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the new comment notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value
+			 */
+			$value = apply_filters( 'bp_activity_new_comment_notification_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the new comment notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value       Email notification subject text.
+			 * @param string $poster_name Name of the person who made the comment.
+			 */
+			$value = apply_filters( 'bp_activity_new_comment_notification_subject', $value, $tokens['poster.name'] );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the new comment notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string $value         Email notification message text.
+			 * @param string $poster_name   Name of the person who made the comment.
+			 * @param string $content       Content of the comment.
+			 * @param string $thread_link   URL permalink for the activity thread.
+			 * @param string $deprecated    Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters( 'bp_activity_new_comment_notification_message', $value, $tokens['poster.name'], $tokens['content'], $tokens['thread.url'], '' );
+		}
+
+	} elseif ( $email_type === 'activity-comment-author' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the new comment reply notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value
+			 */
+			$value = apply_filters( 'bp_activity_new_comment_notification_comment_author_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the new comment reply notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value       Email notification subject text.
+			 * @param string $poster_name Name of the person who made the comment.
+			 */
+			$value = apply_filters( 'bp_activity_new_comment_notification_comment_author_subject', $value, $tokens['poster.name'] );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the new comment reply notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string $value         Email notification message text.
+			 * @param string $poster_name   Name of the person who made the comment.
+			 * @param string $content       Content of the comment.
+			 * @param string $deprecated    Removed in 2.5; now an empty string.
+			 * @param string $thread_link   URL permalink for the activity thread.
+			 */
+			$value = apply_filters( 'bp_activity_new_comment_notification_comment_author_message', $value, $tokens['poster.name'], $tokens['content'], '', $tokens['thread.url'] );
+		}
+
+	} elseif ( $email_type === 'activity-at-message' || $email_type === 'groups-at-message' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the @mention notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value
+			 */
+			$value = apply_filters( 'bp_activity_at_message_notification_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the @mention notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value       Email notification subject text.
+			 * @param string $poster_name Name of the person who made the @mention.
+			 */
+			$value = apply_filters( 'bp_activity_at_message_notification_subject', $value, $tokens['poster.name'] );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the @mention notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string $message       Email notification message text.
+			 * @param string $poster_name   Name of the person who made the @mention.
+			 * @param string $content       Content of the @mention.
+			 * @param string $message_link  URL permalink for the activity message.
+			 * @param string $deprecated    Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters( 'bp_activity_at_message_notification_message', $value, $tokens['poster.name'], $tokens['content'], $tokens['mentioned.url'], '' );
+		}
+
+	} elseif ( $email_type === 'core-user-registration' ) {
+		// Filters are different according to the WordPress config.
+		if ( is_multisite() ) {
+			$registration_filters = array(
+				'to'      => 'bp_core_activation_signup_user_notification_to',
+				'subject' => 'bp_core_activation_signup_user_notification_subject',
+				'content' => 'bp_core_activation_signup_user_notification_message',
+			);
+		} else {
+			$registration_filters = array(
+				'to'      => 'bp_core_signup_send_validation_email_to',
+				'subject' => 'bp_core_signup_send_validation_email_subject',
+				'content' => 'bp_core_signup_send_validation_email_message',
+			);
+		}
+
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the email that the notification is going to upon successful registration without blog.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $value              User email the notification is being sent to.
+			 * @param string $recipient_username The user's login name.
+			 * @param array $value               User email the notification is being sent to (again).
+			 *                                   Array key is email address, value is the name.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array $meta                Removed in 2.5; now an empty array.
+			 */
+			$value = apply_filters( $registration_filters['to'], $value, $tokens['recipient.username'], $value, $tokens['key'], array() );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the subject that the notification uses upon successful registration without blog.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $value              Email notification subject text.
+			 * @param string $recipient_username The user's login name.
+			 * @param string $recipient_email    The user's email address.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array $meta                Removed in 2.5; now an empty array.
+			 */
+			$value = apply_filters( $registration_filters['subject'], $value, $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key'], array() );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the message that the notification uses upon successful registration without blog.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $value              The message to use.
+			 * @param string $recipient_username The user's login name.
+			 * @param string $recipient_email    The user's email address.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array $meta                Removed in 2.5; now an empty array.
+			 */
+			$value = apply_filters( $registration_filters['content'], $value, $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key'], array() );
+		}
+
+	} elseif ( $email_type === 'core-user-registration-with-blog' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the email that the notification is going to upon successful registration with blog.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $value              User email the notification is being sent to.
+			 * @param string $domain             The new blog domain.
+			 * @param string $path               The new blog path.
+			 * @param string $title              The site title.
+			 * @param string $recipient_username The user's login name.
+			 * @param string $recipient_email    The user's email address.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array  $meta               Removed in 2.5; now an empty array.
+			 */
+			$value = apply_filters( 'bp_core_activation_signup_blog_notification_to', $value, $tokens['domain'], $tokens['path'], $tokens['title'], $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key_blog'], array() );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the subject that the notification uses upon successful registration with blog.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $value              The subject to use.
+			 * @param string $domain             The new blog domain.
+			 * @param string $path               The new blog path.
+			 * @param string $title              The site title.
+			 * @param string $recipient_username The user's login name.
+			 * @param string $recipient_email    The user's email address.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array  $meta               Removed in 2.5; now an empty array.
+			 */
+			$value = apply_filters( 'bp_core_activation_signup_blog_notification_subject', $value, $tokens['domain'], $tokens['path'], $tokens['title'], $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key_blog'], array() );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the message that the notification uses upon successful registration with blog.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $value              The message to use.
+			 * @param string $domain             The new blog domain.
+			 * @param string $path               The new blog path.
+			 * @param string $title              The site title.
+			 * @param string $recipient_username The user's login name.
+			 * @param string $recipient_email    The user's email address.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array  $meta               Removed in 2.5; now an empty array.
+			 */
+			$value = apply_filters( 'bp_core_activation_signup_blog_notification_message', $value, $tokens['domain'], $tokens['path'], $tokens['title'], $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key_blog'], array() );
+		}
+
+	} elseif ( $email_type === 'friends-request' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the email address for who is getting the friend request.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value
+			 */
+			$value = apply_filters( 'friends_notification_new_request_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the subject for the friend request email.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value          Subject line to be used in friend request email.
+			 * @param string $initiator_name Name of the person requesting friendship.
+			 */
+			$value = apply_filters( 'friends_notification_new_request_subject', $value, $tokens['initiator.name'] );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the message for the friend request email.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string $value             Message to be used in friend request email.
+			 * @param string $initiator_name    Name of the person requesting friendship.
+			 * @param string $initiator_link    Profile link of person requesting friendship.
+			 * @param string $all_requests_link User's friends request management link.
+			 * @param string $settings_link     Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters( 'friends_notification_new_request_message', $value, $tokens['initiator.name'], $tokens['initiator.url'], $tokens['friend-requests.url'], '' );
+		}
+
+	} elseif ( $email_type === 'friends-request-accepted' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the email address for whose friend request got accepted.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value Email address for whose friend request got accepted.
+			 */
+			$value = apply_filters( 'friends_notification_accepted_request_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the subject for the friend request accepted email.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value       Subject line to be used in friend request accepted email.
+			 * @param string $friend_name Name of the person who accepted the friendship request.
+			 */
+			$value = apply_filters( 'friends_notification_accepted_request_subject', $value, $tokens['friend.name'] );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the message for the friend request accepted email.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string $value         Message to be used in friend request email.
+			 * @param string $friend_name   Name of the person who accepted the friendship request.
+			 * @param string $friend_link   Profile link of person who accepted the friendship request.
+			 * @param string $settings_link Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters( 'friends_notification_accepted_request_message', $value, $tokens['friend.name'], $tokens['friendship.url'], '' );
+		}
+
+	} elseif ( $email_type === 'groups-details-updated' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the group update notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value User email the notification is being sent to.
+			 */
+			$value = apply_filters( 'groups_notification_group_updated_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the group update notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string          $value Email notification subject text.
+			 * @param BP_Groups_Group $group Object holding the current group instance. Passed by reference.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_group_updated_subject', array( $value, &$tokens['group'] ) );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the group update notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string          $value         Email notification message text.
+			 * @param BP_Groups_Group $group         Object holding the current group instance. Passed by reference.
+			 * @param string          $group_link    URL permalink to the group that was updated.
+			 * @param string          $settings_link Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_group_updated_message', array( $value, &$tokens['group'], $tokens['group.url'], '' ) );
+		}
+
+	} elseif ( $email_type === 'groups-invitation' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the group invite notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value User email the invite notification is being sent to.
+			 */
+			$value = apply_filters( 'groups_notification_group_invites_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the group invite notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string          $value Invite notification email subject text.
+			 * @param BP_Groups_Group $group Object holding the current group instance. Passed by reference.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_group_invites_subject', array( $value, &$tokens['group'] ) );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the group invite notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string          $value         Invite notification email message text.
+			 * @param BP_Groups_Group $group         Object holding the current group instance. Passed by reference.
+			 * @param string          $inviter_name  Username for the person doing the inviting.
+			 * @param string          $inviter_link  Profile link for the person doing the inviting.
+			 * @param string          $invites_link  URL permalink for the invited user's invite management screen.
+			 * @param string          $group_link    URL permalink for the group that the invite was related to.
+			 * @param string          $settings_link Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_group_invites_message', array( $value, &$tokens['group'], $tokens['inviter.name'], $tokens['inviter.url'], $tokens['invites.url'], $tokens['group.url'], '' ) );
+		}
+
+	} elseif ( $email_type === 'groups-member-promoted' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the group promotion notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value User email the promotion notification is being sent to.
+			 */
+			$value = apply_filters( 'groups_notification_promoted_member_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the group promotion notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string          $value Promotion notification email subject text.
+			 * @param BP_Groups_Group $group Object holding the current group instance. Passed by reference.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_promoted_member_subject', array( $value, &$tokens['group'] ) );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the group promotion notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string          $value         Promotion notification email message text.
+			 * @param BP_Groups_Group $group         Object holding the current group instance. Passed by reference.
+			 * @param string          $promoted_to   Role that the user was promoted to within the group.
+			 * @param string          $group_link    URL permalink for the group that the promotion was related to.
+			 * @param string          $settings_link Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_promoted_member_message', array( $value, &$tokens['group'], $tokens['promoted_to'], $tokens['group.url'], '' ) );
+		}
+
+	} elseif ( $email_type === 'groups-membership-request' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the group membership request will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value User email the request is being sent to.
+			 */
+			$value = apply_filters( 'groups_notification_new_membership_request_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the group membership request subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string          $value Membership request email subject text.
+			 * @param BP_Groups_Group $group Object holding the current group instance. Passed by reference.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_new_membership_request_subject', array( $value, &$tokens['group'] ) );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the group membership request message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string          $value                Membership request email message text.
+			 * @param BP_Groups_Group $group                Object holding the current group instance. Passed by reference.
+			 * @param string          $requesting_user_name Username of who is requesting membership.
+			 * @param string          $profile_link         URL permalink for the profile for the user requesting membership.
+			 * @param string          $group_requests       URL permalink for the group requests screen for group being requested membership to.
+			 * @param string          $deprecated           Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_new_membership_request_message', array( $value, &$tokens['group'], $tokens['requesting-user.name'], $tokens['profile.url'], $tokens['group-requests.url'], '' ) );
+		}
+
+	} elseif ( $email_type === 'groups-membership-request-accepted' || $email_type === 'groups-membership-request-rejected' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the group membership request result will be sent to.
+			 *
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string $value User email the request is being sent to.
+			 */
+			$value = apply_filters( 'groups_notification_membership_request_completed_to', $value );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the group membership request result subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *
+			 * @param string          $value Membership request result email subject text.
+			 * @param BP_Groups_Group $group Object holding the current group instance. Passed by reference.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_membership_request_completed_subject', array( $value, &$tokens['group'] ) );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the group membership request result message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $settings_link argument unset and deprecated.
+			 *
+			 * @param string          $value      Membership request result email message text.
+			 * @param BP_Groups_Group $group      Object holding the current group instance. Passed by reference.
+			 * @param string          $group_link URL permalink for the group that was requested membership for.
+			 * @param string          $deprecated Removed in 2.5; now an empty string.
+			 */
+			$value = apply_filters_ref_array( 'groups_notification_membership_request_completed_message', array( $value, &$tokens['group'], $tokens['group.url'], '' ) );
+		}
+
+	} elseif ( $email_type === 'messages-unread' ) {
+		if ( $property === 'to' ) {
+			/**
+			 * Filters the user email that the message notification will be sent to.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $ud argument unset and deprecated.
+			 *
+			 * @param string $value      User email the message notification is being sent to.
+			 * @param bool   $deprecated Removed in 2.5; now a bool (false).
+			 */
+			$value = apply_filters( 'messages_notification_new_message_to', $value, false );
+
+		} elseif ( $property === 'subject' ) {
+			/**
+			 * Filters the message notification subject that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $ud argument unset and deprecated.
+			 *
+			 * @param string $value       Email notification subject text.
+			 * @param string $sender_name Name of the person who sent the message.
+			 * @param bool   $deprecated  Removed in 2.5; now a bool (false).
+			 */
+			$value = apply_filters( 'messages_notification_new_message_subject', $value, $tokens['sender.name'], false );
+
+		} elseif ( $property === 'content' ) {
+			/**
+			 * Filters the message notification message that will be sent to user.
+			 *
+			 * @since 1.2.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email.
+			 *                   $settings_link and $ud arguments unset and deprecated.
+			 *
+			 * @param string $value        Email notification message text.
+			 * @param string $sender_name  Name of the person who sent the message.
+			 * @param string $usersubjec   Email notification subject text.
+			 * @param string $content      Content of the message.
+			 * @param string $message_link URL permalink for the message.
+			 * @param string $deprecated   Removed in 2.5; now an empty string.
+			 * @param bool   $deprecated   Removed in 2.5; now a bool (false).
+			 */
+			$value = apply_filters( 'messages_notification_new_message_message', $value, $tokens['sender.name'], $tokens['usersubject'], $tokens['content'], $tokens['message.url'], '', false );
+		}
+
+	} elseif ( $email_type === 'settings-verify-email-change' ) {
+		if ( $property === 'content' ) {
+			/**
+			 * Filter the email text sent when a user changes emails.
+			 *
+			 * @since 2.1.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $update_user argument unset and deprecated.
+			 *
+			 * @param string  $value           Text of the email.
+			 * @param string  $recipient_email New user email that the current user has changed to.
+			 * @param string  $old_user_email  Existing email address for the current user.
+			 * @param bool    $deprecated      Removed in 2.5; now a bool (false).
+			 */
+			$value = apply_filters( 'bp_new_user_email_content', $value, $tokens['recipient.email'], $tokens['old-user.email'], false );
+		}
+	}
+
+	if ( $property === 'to' && $to_changed ) {
+		// We normally apart $to, so we often need to rebuild it.
+		array_unshift( $original_value, new BP_Email_Recipient( $value, $recipient_name ) );
+		$value = $original_value;
+	}
+
+	return $value;
+}
+add_filter( 'bp_email_get_property', 'bp_core_deprecated_email_filters', 20, 4 );
+
+/**
+ * Add support for pre-2.5 email actions.
+ *
+ * @since 2.5.0
+ *
+ * @param bool|WP_Error $delivery_status Bool if the email was sent or not.
+ *                                       If a WP_Error, there was a failure.
+ * @param BP_Email $email Email object reference.
+ * @return mixed
+ */
+function bp_core_deprecated_email_actions( $delivery_status, $email ) {
+	$pre_2_5_emails = array(
+		'activity-comment',
+		'activity-comment-author',
+		'core-user-registration',
+		'core-user-registration-with-blog',
+		'friends-request',
+		'friends-request-accepted',
+		'groups-details-updated',
+		'groups-invitation',
+		'groups-member-promoted',
+		'groups-membership-request',
+		'groups-membership-request-accepted',
+		'groups-membership-request-rejected',
+		'messages-unread',
+		'settings-verify-email-change',
+	);
+
+	remove_action( 'bp_send_email_success', 'bp_core_deprecated_email_actions', 20 );
+	$email_content = $email->get( 'content' );
+	$email_subject = $email->get( 'subject' );
+	$email_type    = $email->get( 'type' );
+	$tokens        = $email->get( 'tokens' );
+	add_action( 'bp_send_email_success', 'bp_core_deprecated_email_actions', 20, 2 );
+
+	// Backpat for pre-2.5 emails only.
+	if ( ! in_array( $email_type, $pre_2_5_emails, true ) ) {
+		return;
+	}
+
+	if ( $email_type === 'activity-comment' ) {
+		/**
+		 * Fires after the sending of a reply to an update email notification.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email. $params argument unset and deprecated.
+		 *
+		 * @param int    $user_id       ID of the original activity item author.
+		 * @param string $email_subject Email notification subject text.
+		 * @param string $email_content Email notification message text.
+		 * @param int    $comment_id    ID for the newly received comment.
+		 * @param int    $commenter_id  ID of the user who made the comment.
+		 * @param array  $deprecated    Removed in 2.5; now an empty array.
+		 */
+		do_action( 'bp_activity_sent_reply_to_update_email', $tokens['original_activity.user_id'], $email_subject, $email_content, $tokens['comment.id'], $tokens['commenter.id'], array() );
+
+	} elseif ( $email_type === 'activity-comment-author' ) {
+		/**
+		 * Fires after the sending of a reply to a reply email notification.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email. $params argument unset and deprecated.
+		 *
+		 * @param int    $user_id       ID of the parent activity item author.
+		 * @param string $email_subject Email notification subject text.
+		 * @param string $email_content Email notification message text.
+		 * @param int    $comment_id    ID for the newly received comment.
+		 * @param int    $commenter_id  ID of the user who made the comment.
+		 * @param array  $deprecated    Removed in 2.5; now an empty array.
+		 */
+		do_action( 'bp_activity_sent_reply_to_reply_email', $tokens['parent-comment-user.id'], $email_subject, $email_content, $tokens['comment.id'], $tokens['commenter.id'], array() );
+
+	} elseif ( $email_type === 'core-user-registration' ) {
+		if ( ! empty( $tokens['user.id'] ) ) {
+			/**
+			 * Fires after the sending of activation email to a newly registered user.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $email_subject   Subject for the sent email.
+			 * @param string $email_content   Message for the sent email.
+			 * @param int    $user_id         ID of the new user.
+			 * @param string $recipient_email Email address of the new user.
+			 * @param string $key             Activation key.
+			 */
+			do_action( 'bp_core_sent_user_validation_email', $email_subject, $email_content, $tokens['user.id'], $tokens['recipient.email'], $tokens['key'] );
+
+		} else {
+			/**
+			 * Fires after the sending of the notification to new users for successful registration without blog.
+			 *
+			 * @since 1.5.0
+			 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+			 *
+			 * @param string $admin_email        Admin Email address for the site.
+			 * @param string $email_subject      Subject used in the notification email.
+			 * @param string $email_content      Message used in the notification email.
+			 * @param string $recipient_username The user's login name.
+			 * @param string $recipient_email    The user's email address.
+			 * @param string $key                The activation key created in wpmu_signup_blog().
+			 * @param array  $meta               Removed in 2.5; now an empty array.
+			 */
+			do_action( 'bp_core_sent_user_signup_email', bp_get_option( 'admin_email' ), $email_subject, $email_content, $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key'], array() );
+		}
+
+	} elseif ( $email_type === 'core-user-registration-with-blog' ) {
+		/**
+		 * Fires after the sending of the notification to new users for successful registration with blog.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email. $meta argument unset and deprecated.
+		 *
+		 * @param string $admin_email        Admin Email address for the site.
+		 * @param string $email_subject      Subject used in the notification email.
+		 * @param string $email_content      Message used in the notification email.
+		 * @param string $domain             The new blog domain.
+		 * @param string $path               The new blog path.
+		 * @param string $title              The site title.
+		 * @param string $recipient_username The user's login name.
+		 * @param string $recipient_email    The user's email address.
+		 * @param string $key                The activation key created in wpmu_signup_blog().
+		 * @param array  $meta               Removed in 2.5; now an empty array.
+		 */
+		do_action( 'bp_core_sent_blog_signup_email', bp_get_option( 'admin_email' ), $email_subject, $email_content, $tokens['domain'], $tokens['path'], $tokens['title'], $tokens['recipient.username'], $tokens['recipient.email'], $tokens['key_blog'], array() );
+
+	} elseif ( $email_type === 'friends-request' ) {
+		/**
+		 * Fires after the new friend request email is sent.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email.
+		 *
+		 * @param int    $friend_id     ID of the request recipient.
+		 * @param string $email_subject Text for the friend request subject field.
+		 * @param string $email_content Text for the friend request message field.
+		 * @param int    $friendship_id ID of the friendship object.
+		 * @param int    $initiator_id  ID of the friendship requester.
+		 */
+		do_action( 'bp_friends_sent_request_email', $tokens['friend.id'], $email_subject, $email_content, $tokens['friendship.id'], $tokens['initiator.id'] );
+
+	} elseif ( $email_type === 'friends-request-accepted' ) {
+		/**
+		 * Fires after the friend request accepted email is sent.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email.
+		 *
+		 * @param int    $initiator_id  ID of the friendship requester.
+		 * @param string $email_subject Text for the friend request subject field.
+		 * @param string $email_content Text for the friend request message field.
+		 * @param int    $friendship_id ID of the friendship object.
+		 * @param int    $friend_id     ID of the request recipient.
+		 */
+		do_action( 'bp_friends_sent_accepted_email', $tokens['initiator.id'], $email_subject, $email_content, $tokens['friendship.id'], $tokens['friend.id'] );
+
+	} elseif ( $email_type === 'groups-invitation' ) {
+		/**
+		 * Fires after the notification is sent that a member has been invited to a group.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email.
+		 *
+		 * @param int             $invited_user_id  ID of the user who was invited.
+		 * @param string          $email_subject    Email notification subject text.
+		 * @param string          $email_content    Email notification message text.
+		 * @param BP_Groups_Group $group            Group object.
+		 */
+		do_action( 'bp_groups_sent_invited_email', $tokens['inviter.id'], $email_subject, $email_content, $tokens['group'] );
+
+	} elseif ( $email_type === 'groups-member-promoted' ) {
+		/**
+		 * Fires after the notification is sent that a member has been promoted.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email.
+		 *
+		 * @param int    $user_id       ID of the user who was promoted.
+		 * @param string $email_subject Email notification subject text.
+		 * @param string $email_content Email notification message text.
+		 * @param int    $group_id      ID of the group that the user is a member of.
+		 */
+		do_action( 'bp_groups_sent_promoted_email', $tokens['user.id'], $email_subject, $email_content, $tokens['group.id'] );
+
+	} elseif ( $email_type === 'groups-membership-request' ) {
+		/**
+		 * Fires after the notification is sent that a member has requested group membership.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email.
+		 *
+		 * @param int    $admin_id           ID of the group administrator.
+		 * @param string $email_subject      Email notification subject text.
+		 * @param string $email_content      Email notification message text.
+		 * @param int    $requesting_user_id ID of the user requesting membership.
+		 * @param int    $group_id           ID of the group receiving membership request.
+		 * @param int    $membership_id      ID of the group membership object.
+		 */
+		do_action( 'bp_groups_sent_membership_request_email', $tokens['admin.id'], $email_subject, $email_content, $tokens['requesting-user.id'], $tokens['group.id'], $tokens['membership.id'] );
+
+	} elseif ( $email_type === 'groups-membership-request-accepted' || $email_type === 'groups-membership-request-rejected' ) {
+		/**
+		 * Fires after the notification is sent that a membership has been approved.
+		 *
+		 * @since 1.5.0
+		 * @deprecated 2.5.0 Use the filters in BP_Email.
+		 *
+		 * @param int    $requesting_user_id ID of the user whose membership was approved.
+		 * @param string $email_subject      Email notification subject text.
+		 * @param string $email_content      Email notification message text.
+		 * @param int    $group_id           ID of the group that was joined.
+		 */
+		do_action( 'bp_groups_sent_membership_approved_email', $tokens['requesting-user.id'], $email_subject, $email_content, $tokens['group.id'] );
+	}
+}
+add_action( 'bp_send_email_success', 'bp_core_deprecated_email_actions', 20, 2 );
+
+/**
+ * When a blog comment status transition occurs, update the relevant activity's status.
+ *
+ * @since 1.6.0
+ * @deprecated 2.5.0
+ *
+ * @param string $new_status New comment status.
+ * @param string $old_status Previous comment status.
+ * @param object $comment Comment data.
+ */
+function bp_blogs_transition_activity_status( $new_status, $old_status, $comment ) {
+	_deprecated_function( __FUNCTION__, '2.5.0', 'bp_activity_transition_post_type_comment_status()' );
+	bp_activity_transition_post_type_comment_status( $new_status, $old_status, $comment );
+}
+
+/**
+ * Record a new blog comment in the BuddyPress activity stream.
+ *
+ * Only posts the item if blog is public and post is not password-protected.
+ *
+ * @deprecated 2.5.0
+ *
+ * @param int $comment_id ID of the comment being recorded.
+ * @param bool|string $is_approved Optional. The $is_approved value passed to
+ *        the 'comment_post' action. Default: true.
+ * @return bool|object|null Returns false on failure, the comment object on success.
+ */
+function bp_blogs_record_comment( $comment_id, $is_approved = true ) {
+	_deprecated_function( __FUNCTION__, '2.5.0', 'bp_activity_post_type_comment()' );
+	bp_activity_post_type_comment( $comment_id, $is_approved );
+}
+
+/**
+ * Remove a blog comment activity item from the activity stream.
+ *
+ * @deprecated 2.5.0
+ *
+ * @param int $comment_id ID of the comment to be removed.
+ */
+function bp_blogs_remove_comment( $comment_id ) {
+	_deprecated_function( __FUNCTION__, '2.5.0', 'bp_activity_post_type_remove_comment()' );
+	bp_activity_post_type_remove_comment( $comment_id );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.6.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.6.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.6.php	(revision 12265)
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Print the generation time in the footer of the site.
+ *
+ * @since 1.0.0
+ * @deprecated 2.6.0
+ */
+function bp_core_print_generation_time() {
+?>
+
+<!-- Generated in <?php timer_stop(1); ?> seconds. (<?php echo get_num_queries(); ?> q) -->
+
+	<?php
+}
+
+/**
+ * Sort the navigation menu items.
+ *
+ * The sorting is split into a separate function because it can only happen
+ * after all plugins have had a chance to register their navigation items.
+ *
+ * @since 1.0.0
+ * @deprecated 2.6.0
+ *
+ * @return bool|null Returns false on failure.
+ */
+function bp_core_sort_nav_items() {
+	_deprecated_function( __FUNCTION__, '2.6' );
+}
+
+/**
+ * Sort all subnavigation arrays.
+ *
+ * @since 1.1.0
+ * @deprecated 2.6.0
+ *
+ * @return bool|null Returns false on failure.
+ */
+function bp_core_sort_subnav_items() {
+	_deprecated_function( __FUNCTION__, '2.6' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.7.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.7.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.7.php	(revision 12265)
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Get the DB schema to use for BuddyPress components.
+ *
+ * @since 1.1.0
+ * @deprecated 2.7.0
+ *
+ * @return string The default database character-set, if set.
+ */
+function bp_core_set_charset() {
+	global $wpdb;
+
+	_deprecated_function( __FUNCTION__, '2.7', 'wpdb::get_charset_collate()' );
+
+	require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
+	return !empty( $wpdb->charset ) ? "DEFAULT CHARACTER SET {$wpdb->charset}" : '';
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.8.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.8.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.8.php	(revision 12265)
@@ -0,0 +1,203 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.8.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Determines whether the current installation is running PHP 5.3 or greater.
+ *
+ * BuddyPress 2.8 introduces a minimum PHP requirement of PHP 5.3.
+ *
+ * @since 2.7.0
+ * @deprecated 2.8.0
+ *
+ * @return bool
+ */
+function bp_core_admin_is_running_php53_or_greater() {
+	_deprecated_function( __FUNCTION__, '2.8' );
+	return version_compare( PHP_VERSION, '5.3', '>=' );
+}
+
+/**
+ * Replaces WP's default update notice on plugins.php with an error message, when site is not running PHP 5.3 or greater.
+ *
+ * Originally hooked to 'load-plugins.php' with priority 100.
+ *
+ * @since 2.7.0
+ * @deprecated 2.8.0
+ */
+function bp_core_admin_maybe_disable_update_row_for_php53_requirement() {
+	if ( bp_core_admin_is_running_php53_or_greater() ) {
+		return;
+	}
+
+	$loader = basename( constant( 'BP_PLUGIN_DIR' ) ) . '/bp-loader.php';
+
+	remove_action( "after_plugin_row_{$loader}", 'wp_plugin_update_row', 10 );
+	add_action( "after_plugin_row_{$loader}", 'bp_core_admin_php52_plugin_row', 10, 2 );
+}
+
+/**
+ * On the "Dashboard > Updates" page, remove BuddyPress from plugins list if PHP < 5.3.
+ *
+ * Originally hooked to 'load-update-core.php'.
+ *
+ * @since 2.7.0
+ * @deprecated 2.8.0
+ */
+function bp_core_admin_maybe_remove_from_update_core() {
+	if ( bp_core_admin_is_running_php53_or_greater() ) {
+		return;
+	}
+
+	// Add filter to remove BP from the update plugins list.
+	add_filter( 'site_transient_update_plugins', 'bp_core_admin_remove_buddypress_from_update_transient' );
+}
+
+/**
+ * Filter callback to remove BuddyPress from the update plugins list.
+ *
+ * Attached to the 'site_transient_update_plugins' filter.
+ *
+ * @since 2.7.0
+ * @deprecated 2.8.0
+ *
+ * @param  object $retval Object of plugin update data.
+ * @return object
+ */
+function bp_core_admin_remove_buddypress_from_update_transient( $retval ) {
+	_deprecated_function( __FUNCTION__, '2.8' );
+
+	$loader = basename( constant( 'BP_PLUGIN_DIR' ) ) . '/bp-loader.php';
+
+	// Remove BP from update plugins list.
+	if ( isset( $retval->response[ $loader ] ) ) {
+		unset( $retval->response[ $loader ] );
+	}
+
+	return $retval;
+}
+
+/**
+ * Outputs a replacement for WP's default update notice, when site is not running PHP 5.3 or greater.
+ *
+ * When we see that a site is not running PHP 5.3 and is trying to update to
+ * BP 2.8+, we replace WP's default notice with our own, which both provides a
+ * link to our documentation of the requirement, and removes the link that
+ * allows a single plugin to be updated.
+ *
+ * @since 2.7.0
+ * @deprecated 2.8.0
+ *
+ * @param string $file        Plugin filename. buddypress/bp-loader.php.
+ * @param array  $plugin_data Data about the BuddyPress plugin, as returned by the
+ *                            plugins API.
+ */
+function bp_core_admin_php52_plugin_row( $file, $plugin_data ) {
+	_deprecated_function( __FUNCTION__, '2.8' );
+
+	if ( is_multisite() && ! is_network_admin() ) {
+		return;
+	}
+
+	$current = get_site_transient( 'update_plugins' );
+	if ( ! isset( $current->response[ $file ] ) ) {
+		return false;
+	}
+
+	$response = $current->response[ $file ];
+
+	// No need to do this if update is for < BP 2.8.
+	if ( version_compare( $response->new_version, '2.8', '<' ) ) {
+		return false;
+	}
+
+	$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
+
+	if ( is_network_admin() ) {
+		$active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
+	} else {
+		$active_class = is_plugin_active( $file ) ? ' active' : '';
+	}
+
+	// WP 4.6 uses different markup for the plugin row notice.
+	if ( function_exists( 'wp_get_ext_types' ) ) {
+		$p = '<p>%s</p>';
+
+	// WP < 4.6.
+	} else {
+		$p = '%s';
+
+		// Ugh.
+		$active_class .= ' not-shiny';
+	}
+
+	echo '<tr class="plugin-update-tr' . $active_class . '" id="' . esc_attr( $response->slug . '-update' ) . '" data-slug="' . esc_attr( $response->slug ) . '" data-plugin="' . esc_attr( $file ) . '"><td colspan="' . esc_attr( $wp_list_table->get_column_count() ) . '" class="plugin-update colspanchange"><div class="update-message inline notice notice-error notice-alt">';
+
+	printf( $p,
+		esc_html__( 'A BuddyPress update is available, but your system is not compatible.', 'buddypress' ) . ' ' .
+		sprintf( __( 'See <a href="%s">the Codex guide</a> for more information.', 'buddypress' ), 'https://codex.buddypress.org/getting-started/buddypress-2-8-will-require-php-5-3/' )
+	);
+
+	echo '</div></td></tr>';
+
+	/*
+	 * JavaScript to disable the bulk upgrade checkbox.
+	 * See WP_Plugins_List_Table::single_row().
+	 */
+	$checkbox_id = 'checkbox_' . md5( $plugin_data['Name'] );
+	echo "<script type='text/javascript'>document.getElementById('$checkbox_id').disabled = true;</script>";
+}
+
+/**
+ * Add an admin notice to installations that are not running PHP 5.3+.
+ *
+ * @since 2.7.0
+ * @deprecated 2.8.0
+ */
+function bp_core_admin_php53_admin_notice() {
+	_deprecated_function( __FUNCTION__, '2.8' );
+
+	// If not on the Plugins page, stop now.
+	if ( 'plugins' !== get_current_screen()->parent_base ) {
+		return;
+	}
+
+	if ( ! current_user_can( 'update_core' ) ) {
+		return;
+	}
+
+	if ( bp_core_admin_is_running_php53_or_greater() ) {
+		return;
+	}
+
+	$notice_id = 'bp28-php53';
+	if ( bp_get_option( "bp-dismissed-notice-$notice_id" ) ) {
+		return;
+	}
+
+	$bp  = buddypress();
+	$min = bp_core_get_minified_asset_suffix();
+
+	wp_enqueue_script(
+		'bp-dismissible-admin-notices',
+		"{$bp->plugin_url}bp-core/admin/js/dismissible-admin-notices{$min}.js",
+		array( 'jquery' ),
+		bp_get_version(),
+		true
+	);
+	?>
+
+	<div id="message" class="error notice is-dismissible bp-is-dismissible" data-noticeid="<?php echo esc_attr( $notice_id ); ?>">
+		<p><strong><?php esc_html_e( 'Your site is not ready for BuddyPress 2.8.', 'buddypress' ); ?></strong></p>
+		<p><?php printf( esc_html__( 'Your site is currently running PHP version %s, while BuddyPress 2.8 will require version 5.3+.', 'buddypress' ), esc_html( phpversion() ) ); ?> <?php printf( __( 'See <a href="%s">the Codex guide</a> for more information.', 'buddypress' ), 'https://codex.buddypress.org/getting-started/buddypress-2-8-will-require-php-5-3/' ); ?></p>
+		<?php wp_nonce_field( "bp-dismissible-notice-$notice_id", "bp-dismissible-nonce-$notice_id" ); ?>
+	</div>
+	<?php
+}
+
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/2.9.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/2.9.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/2.9.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 2.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Replace default WordPress avatars with BP avatars, if available.
+ *
+ * See 'get_avatar' filter description in wp-includes/pluggable.php.
+ *
+ * @since 1.1.0
+ * @since 2.4.0 Added $args parameter to coincide with WordPress 4.2.0.
+ *
+ * @param string            $avatar  The avatar path passed to 'get_avatar'.
+ * @param int|string|object $user    A user ID, email address, or comment object.
+ * @param int               $size    Size of the avatar image ('thumb' or 'full').
+ * @param string            $default URL to a default image to use if no avatar is available.
+ * @param string            $alt     Alternate text to use in image tag. Default: ''.
+ * @param array             $args    Arguments passed to get_avatar_data(), after processing.
+ * @return string BP avatar path, if found; else the original avatar path.
+ */
+function bp_core_fetch_avatar_filter( $avatar, $user, $size, $default, $alt = '', $args = array() ) {
+	_deprecated_function( __FUNCTION__, '2.9' );
+	return $avatar;
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/3.0.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/3.0.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/3.0.php	(revision 12265)
@@ -0,0 +1,187 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Check whether bbPress plugin-powered Group Forums are enabled.
+ *
+ * @since 1.6.0
+ * @since 3.0.0 $default argument's default value changed from true to false.
+ * @deprecated 3.0.0 No longer used in core, but supported for third-party code.
+ *
+ * @param bool $default Optional. Fallback value if not found in the database.
+ *                      Default: false.
+ * @return bool True if group forums are active, otherwise false.
+ */
+function bp_is_group_forums_active( $default = false ) {
+	_deprecated_function( __FUNCTION__, '3.0', 'groups_get_group( $id )->enable_forum' );
+
+	$is_active = function_exists( 'bbp_is_group_forums_active' ) ? bbp_is_group_forums_active( $default ) : $default;
+
+	/**
+	 * Filters whether or not bbPress plugin-powered Group Forums are enabled.
+	 *
+	 * @since 1.6.0
+	 * @deprecated 3.0.0 No longer used in core, but supported for third-party code.
+	 *
+	 * @param bool $value Whether or not bbPress plugin-powered Group Forums are enabled.
+	 */
+	return (bool) apply_filters( 'bp_is_group_forums_active', $is_active );
+}
+
+/**
+ * Is this a user's forums page?
+ *
+ * Eg http://example.com/members/joe/forums/ (or a subpage thereof).
+ *
+ * @since 1.5.0
+ * @deprecated 3.0.0 No longer used in core, but supported for third-party code.
+ *
+ * @return false
+ */
+function bp_is_user_forums() {
+	_deprecated_function( __FUNCTION__, '3.0', 'legacy forum support removed' );
+	return false;
+}
+
+/**
+ * Is the current page a group's (legacy bbPress) forum page?
+ *
+ * @since 1.1.0
+ * @since 3.0.0 Always returns false.
+ * @deprecated 3.0.0 No longer used in core, but supported for custom theme templates.
+ *
+ * @return bool
+ */
+function bp_is_group_forum() {
+	_deprecated_function( __FUNCTION__, '3.0', 'legacy forum support removed' );
+	return false;
+}
+
+
+/**
+ * Output a 'New Topic' button for a group.
+ *
+ * @since 1.2.7
+ * @deprecated 3.0.0 No longer used in core, but supported for third-party code.
+ *
+ * @param BP_Groups_Group|bool $group The BP Groups_Group object if passed, boolean false if not passed.
+ */
+function bp_group_new_topic_button( $group = false ) {
+	_deprecated_function( __FUNCTION__, '3.0', 'legacy forum support removed' );
+}
+
+	/**
+	 * Return a 'New Topic' button for a group.
+	 *
+	 * @since 1.2.7
+	 * @deprecated 3.0.0 No longer used in core, but supported for third-party code.
+	 *
+	 * @param BP_Groups_Group|bool $group The BP Groups_Group object if passed, boolean false if not passed.
+	 *
+	 * @return false
+	 */
+	function bp_get_group_new_topic_button( $group = false ) {
+		_deprecated_function( __FUNCTION__, '3.0', 'legacy forum support removed' );
+		return false;
+	}
+
+/**
+ * Catch a "Mark as Spammer/Not Spammer" click from the toolbar.
+ *
+ * When a site admin selects "Mark as Spammer/Not Spammer" from the admin menu
+ * this action will fire and mark or unmark the user and their blogs as spam.
+ * Must be a site admin for this function to run.
+ *
+ * Note: no longer used in the current state. See the Settings component.
+ *
+ * @since 1.1.0
+ * @since 1.6.0 No longer used, unhooked.
+ * @since 3.0.0 Formally marked as deprecated.
+ *
+ * @param int $user_id Optional. User ID to mark as spam. Defaults to displayed user.
+ */
+function bp_core_action_set_spammer_status( $user_id = 0 ) {
+	_deprecated_function( __FUNCTION__, '3.0' );
+
+	// Only super admins can currently spam users (but they can't spam
+	// themselves).
+	if ( ! is_super_admin() || bp_is_my_profile() ) {
+		return;
+	}
+
+	// Use displayed user if it's not yourself.
+	if ( empty( $user_id ) )
+		$user_id = bp_displayed_user_id();
+
+	if ( bp_is_current_component( 'admin' ) && ( in_array( bp_current_action(), array( 'mark-spammer', 'unmark-spammer' ) ) ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'mark-unmark-spammer' );
+
+		// To spam or not to spam.
+		$status = bp_is_current_action( 'mark-spammer' ) ? 'spam' : 'ham';
+
+		// The heavy lifting.
+		bp_core_process_spammer_status( $user_id, $status );
+
+		// Add feedback message. @todo - Error reporting.
+		if ( 'spam' == $status ) {
+			bp_core_add_message( __( 'User marked as spammer. Spam users are visible only to site admins.', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'User removed as spammer.', 'buddypress' ) );
+		}
+
+		// Deprecated. Use bp_core_process_spammer_status.
+		$is_spam = 'spam' == $status;
+		do_action( 'bp_core_action_set_spammer_status', bp_displayed_user_id(), $is_spam );
+
+		// Redirect back to where we came from.
+		bp_core_redirect( wp_get_referer() );
+	}
+}
+
+/**
+ * Process user deletion requests.
+ *
+ * Note: no longer used in the current state. See the Settings component.
+ *
+ * @since 1.1.0
+ * @since 1.6.0 No longer used, unhooked.
+ * @since 3.0.0 Formally marked as deprecated.
+ */
+function bp_core_action_delete_user() {
+	_deprecated_function( __FUNCTION__, '3.0' );
+
+	if ( !bp_current_user_can( 'bp_moderate' ) || bp_is_my_profile() || !bp_displayed_user_id() )
+		return false;
+
+	if ( bp_is_current_component( 'admin' ) && bp_is_current_action( 'delete-user' ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'delete-user' );
+
+		$errors = false;
+		do_action( 'bp_core_before_action_delete_user', $errors );
+
+		if ( bp_core_delete_account( bp_displayed_user_id() ) ) {
+			bp_core_add_message( sprintf( __( '%s has been deleted from the system.', 'buddypress' ), bp_get_displayed_user_fullname() ) );
+		} else {
+			bp_core_add_message( sprintf( __( 'There was an error deleting %s from the system. Please try again.', 'buddypress' ), bp_get_displayed_user_fullname() ), 'error' );
+			$errors = true;
+		}
+
+		do_action( 'bp_core_action_delete_user', $errors );
+
+		if ( $errors )
+			bp_core_redirect( bp_displayed_user_domain() );
+		else
+			bp_core_redirect( bp_loggedin_user_domain() );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-core/deprecated/4.0.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/deprecated/4.0.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/deprecated/4.0.php	(revision 12265)
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Deprecated functions.
+ *
+ * @deprecated 4.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Print moment.js config in page footer.
+ *
+ * @since 2.7.0
+ * @since 4.0.0 Deprecated as BP requires WP >= 4.6.
+ *
+ * @access private
+ */
+function _bp_core_moment_js_config_footer() {
+	_deprecated_function( __FUNCTION__, '4.0' );
+}
Index: /tags/4.0.0-beta1/src/bp-core/js/avatar.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/avatar.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/avatar.js	(revision 12265)
@@ -0,0 +1,682 @@
+/* global bp, BP_Uploader, _, Backbone */
+
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Uploader === 'undefined' ) {
+		return;
+	}
+
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+
+	bp.Avatar = {
+		start: function() {
+			var self = this;
+
+			/**
+			 * Remove the bp-legacy UI
+			 *
+			 * bp.Avatar successfully loaded, we can now
+			 * safely remove the Legacy UI.
+			 */
+			this.removeLegacyUI();
+
+			// Init some vars
+			this.views    = new Backbone.Collection();
+			this.jcropapi = {};
+			this.warning = null;
+
+			// Set up nav
+			this.setupNav();
+
+			// Avatars are uploaded files
+			this.avatars = bp.Uploader.filesUploaded;
+
+			// The Avatar Attachment object.
+			this.Attachment = new Backbone.Model();
+
+			// Wait till the queue is reset
+			bp.Uploader.filesQueue.on( 'reset', this.cropView, this );
+
+			/**
+			 * In Administration screens we're using Thickbox
+			 * We need to make sure to reset the views if it's closed or opened
+			 */
+			$( 'body.wp-admin' ).on( 'tb_unload', '#TB_window', function() {
+				self.resetViews();
+			} );
+
+			$( 'body.wp-admin' ).on( 'click', '.bp-xprofile-avatar-user-edit', function() {
+				self.resetViews();
+			} );
+		},
+
+		removeLegacyUI: function() {
+			// User
+			if ( $( '#avatar-upload-form' ).length ) {
+				$( '#avatar-upload' ).remove();
+				$( '#avatar-upload-form p' ).remove();
+
+			// Group Manage
+			} else if ( $( '#group-settings-form' ).length ) {
+				$( '#group-settings-form p' ).each( function( i ) {
+					if ( 0 !== i ) {
+						$( this ).remove();
+					}
+				} );
+
+				if ( $( '#delete-group-avatar-button' ).length ) {
+					$( '#delete-group-avatar-button' ).remove();
+				}
+
+			// Group Create
+			} else if ( $( '#group-create-body' ).length ) {
+				$( '.main-column p #file' ).remove();
+				$( '.main-column p #upload' ).remove();
+
+			// Admin Extended Profile
+			} else if ( $( '#bp_xprofile_user_admin_avatar a.bp-xprofile-avatar-user-admin' ).length ) {
+				$( '#bp_xprofile_user_admin_avatar a.bp-xprofile-avatar-user-admin' ).remove();
+			}
+		},
+
+		setView: function( view ) {
+			// Clear views
+			if ( ! _.isUndefined( this.views.models ) ) {
+				_.each( this.views.models, function( model ) {
+					model.get( 'view' ).remove();
+				}, this );
+			}
+
+			// Reset Views
+			this.views.reset();
+
+			// Reset Avatars (file uploaded)
+			if ( ! _.isUndefined( this.avatars ) ) {
+				this.avatars.reset();
+			}
+
+			// Reset the Jcrop API
+			if ( ! _.isEmpty( this.jcropapi ) ) {
+				this.jcropapi.destroy();
+				this.jcropapi = {};
+			}
+
+			// Load the required view
+			switch ( view ) {
+				case 'upload':
+					this.uploaderView();
+					break;
+
+				case 'delete':
+					this.deleteView();
+					break;
+			}
+		},
+
+		resetViews: function() {
+			// Reset to the uploader view
+			this.nav.trigger( 'bp-avatar-view:changed', 'upload' );
+
+			// Reset to the uploader nav
+			_.each( this.navItems.models, function( model ) {
+				if ( model.id === 'upload' ) {
+					model.set( { active: 1 } );
+				} else {
+					model.set( { active: 0 } );
+				}
+			} );
+		},
+
+		setupNav: function() {
+			var self = this,
+			    initView, activeView;
+
+			this.navItems = new Backbone.Collection();
+
+			_.each( BP_Uploader.settings.nav, function( item, index ) {
+				if ( ! _.isObject( item ) ) {
+					return;
+				}
+
+				// Reset active View
+				activeView = 0;
+
+				if ( 0 === index ) {
+					initView = item.id;
+					activeView = 1;
+				}
+
+				self.navItems.add( {
+					id     : item.id,
+					name   : item.caption,
+					href   : '#',
+					active : activeView,
+					hide   : _.isUndefined( item.hide ) ? 0 : item.hide
+				} );
+			} );
+
+			this.nav = new bp.Views.Nav( { collection: this.navItems } );
+			this.nav.inject( '.bp-avatar-nav' );
+
+			// Activate the initial view (uploader)
+			this.setView( initView );
+
+			// Listen to nav changes (it's like a do_action!)
+			this.nav.on( 'bp-avatar-view:changed', _.bind( this.setView, this ) );
+		},
+
+		uploaderView: function() {
+			// Listen to the Queued uploads
+			bp.Uploader.filesQueue.on( 'add', this.uploadProgress, this );
+
+			// Create the BuddyPress Uploader
+			var uploader = new bp.Views.Uploader();
+
+			// Add it to views
+			this.views.add( { id: 'upload', view: uploader } );
+
+			// Display it
+			uploader.inject( '.bp-avatar' );
+		},
+
+		uploadProgress: function() {
+			// Create the Uploader status view
+			var avatarStatus = new bp.Views.uploaderStatus( { collection: bp.Uploader.filesQueue } );
+
+			if ( ! _.isUndefined( this.views.get( 'status' ) ) ) {
+				this.views.set( { id: 'status', view: avatarStatus } );
+			} else {
+				this.views.add( { id: 'status', view: avatarStatus } );
+			}
+
+			// Display it
+			avatarStatus.inject( '.bp-avatar-status' );
+		},
+
+		cropView: function() {
+			var status;
+
+			// Bail there was an error during the Upload
+			if ( _.isEmpty( this.avatars.models ) ) {
+				return;
+			}
+
+			// Make sure to remove the uploads status
+			if ( ! _.isUndefined( this.views.get( 'status' ) ) ) {
+				status = this.views.get( 'status' );
+				status.get( 'view' ).remove();
+				this.views.remove( { id: 'status', view: status } );
+			}
+
+			// Create the Avatars view
+			var avatar = new bp.Views.Avatars( { collection: this.avatars } );
+			this.views.add( { id: 'crop', view: avatar } );
+
+			avatar.inject( '.bp-avatar' );
+		},
+
+		setAvatar: function( avatar ) {
+			var self = this,
+				crop;
+
+			// Remove the crop view
+			if ( ! _.isUndefined( this.views.get( 'crop' ) ) ) {
+				// Remove the JCrop API
+				if ( ! _.isEmpty( this.jcropapi ) ) {
+					this.jcropapi.destroy();
+					this.jcropapi = {};
+				}
+				crop = this.views.get( 'crop' );
+				crop.get( 'view' ).remove();
+				this.views.remove( { id: 'crop', view: crop } );
+			}
+
+			// Set the avatar !
+			bp.ajax.post( 'bp_avatar_set', {
+				json:          true,
+				original_file: avatar.get( 'url' ),
+				crop_w:        avatar.get( 'w' ),
+				crop_h:        avatar.get( 'h' ),
+				crop_x:        avatar.get( 'x' ),
+				crop_y:        avatar.get( 'y' ),
+				item_id:       avatar.get( 'item_id' ),
+				object:        avatar.get( 'object' ),
+				type:          _.isUndefined( avatar.get( 'type' ) ) ? 'crop' : avatar.get( 'type' ),
+				nonce:         avatar.get( 'nonces' ).set
+			} ).done( function( response ) {
+				var avatarStatus = new bp.Views.AvatarStatus( {
+					value : BP_Uploader.strings.feedback_messages[ response.feedback_code ],
+					type : 'success'
+				} );
+
+				self.views.add( {
+					id   : 'status',
+					view : avatarStatus
+				} );
+
+				avatarStatus.inject( '.bp-avatar-status' );
+
+				// Update each avatars of the page
+				$( '.' + avatar.get( 'object' ) + '-' + response.item_id + '-avatar' ).each( function() {
+					$(this).prop( 'src', response.avatar );
+				} );
+
+				// Inject the Delete nav
+				bp.Avatar.navItems.get( 'delete' ).set( { hide: 0 } );
+
+				/**
+				 * Set the Attachment object
+				 *
+				 * You can run extra actions once the avatar is set using:
+				 * bp.Avatar.Attachment.on( 'change:url', function( data ) { your code } );
+				 *
+				 * In this case data.attributes will include the url to the newly
+				 * uploaded avatar, the object and the item_id concerned.
+				 */
+				self.Attachment.set( _.extend(
+					_.pick( avatar.attributes, ['object', 'item_id'] ),
+					{ url: response.avatar, action: 'uploaded' }
+				) );
+
+			} ).fail( function( response ) {
+				var feedback = BP_Uploader.strings.default_error;
+				if ( ! _.isUndefined( response ) ) {
+					feedback = BP_Uploader.strings.feedback_messages[ response.feedback_code ];
+				}
+
+				var avatarStatus = new bp.Views.AvatarStatus( {
+					value : feedback,
+					type : 'error'
+				} );
+
+				self.views.add( {
+					id   : 'status',
+					view : avatarStatus
+				} );
+
+				avatarStatus.inject( '.bp-avatar-status' );
+			} );
+		},
+
+		deleteView:function() {
+			// Create the delete model
+			var delete_model = new Backbone.Model( _.pick( BP_Uploader.settings.defaults.multipart_params.bp_params,
+				'object',
+				'item_id',
+				'nonces'
+			) );
+
+			// Create the delete view
+			var deleteView = new bp.Views.DeleteAvatar( { model: delete_model } );
+
+			// Add it to views
+			this.views.add( { id: 'delete', view: deleteView } );
+
+			// Display it
+			deleteView.inject( '.bp-avatar' );
+		},
+
+		deleteAvatar: function( model ) {
+			var self = this,
+				deleteView;
+
+			// Remove the delete view
+			if ( ! _.isUndefined( this.views.get( 'delete' ) ) ) {
+				deleteView = this.views.get( 'delete' );
+				deleteView.get( 'view' ).remove();
+				this.views.remove( { id: 'delete', view: deleteView } );
+			}
+
+			// Remove the avatar !
+			bp.ajax.post( 'bp_avatar_delete', {
+				json:          true,
+				item_id:       model.get( 'item_id' ),
+				object:        model.get( 'object' ),
+				nonce:         model.get( 'nonces' ).remove
+			} ).done( function( response ) {
+				var avatarStatus = new bp.Views.AvatarStatus( {
+					value : BP_Uploader.strings.feedback_messages[ response.feedback_code ],
+					type : 'success'
+				} );
+
+				self.views.add( {
+					id   : 'status',
+					view : avatarStatus
+				} );
+
+				avatarStatus.inject( '.bp-avatar-status' );
+
+				// Update each avatars of the page
+				$( '.' + model.get( 'object' ) + '-' + response.item_id + '-avatar').each( function() {
+					$( this ).prop( 'src', response.avatar );
+				} );
+
+				// Remove the Delete nav
+				bp.Avatar.navItems.get( 'delete' ).set( { active: 0, hide: 1 } );
+
+				/**
+				 * Reset the Attachment object
+				 *
+				 * You can run extra actions once the avatar is set using:
+				 * bp.Avatar.Attachment.on( 'change:url', function( data ) { your code } );
+				 *
+				 * In this case data.attributes will include the url to the gravatar,
+				 * the object and the item_id concerned.
+				 */
+				self.Attachment.set( _.extend(
+					_.pick( model.attributes, ['object', 'item_id'] ),
+					{ url: response.avatar, action: 'deleted' }
+				) );
+
+			} ).fail( function( response ) {
+				var feedback = BP_Uploader.strings.default_error;
+				if ( ! _.isUndefined( response ) ) {
+					feedback = BP_Uploader.strings.feedback_messages[ response.feedback_code ];
+				}
+
+				var avatarStatus = new bp.Views.AvatarStatus( {
+					value : feedback,
+					type : 'error'
+				} );
+
+				self.views.add( {
+					id   : 'status',
+					view : avatarStatus
+				} );
+
+				avatarStatus.inject( '.bp-avatar-status' );
+			} );
+		},
+
+		removeWarning: function() {
+			if ( ! _.isNull( this.warning ) ) {
+				this.warning.remove();
+			}
+		},
+
+		displayWarning: function( message ) {
+			this.removeWarning();
+
+			this.warning = new bp.Views.uploaderWarning( {
+				value: message
+			} );
+
+			this.warning.inject( '.bp-avatar-status' );
+		}
+	};
+
+	// Main Nav view
+	bp.Views.Nav = bp.View.extend( {
+		tagName:    'ul',
+		className:  'avatar-nav-items',
+
+		events: {
+			'click .bp-avatar-nav-item' : 'toggleView'
+		},
+
+		initialize: function() {
+			var hasAvatar = _.findWhere( this.collection.models, { id: 'delete' } );
+
+			// Display a message to inform about the delete tab
+			if ( 1 !== hasAvatar.get( 'hide' ) ) {
+				bp.Avatar.displayWarning( BP_Uploader.strings.has_avatar_warning );
+			}
+
+			_.each( this.collection.models, this.addNavItem, this );
+			this.collection.on( 'change:hide', this.showHideNavItem, this );
+		},
+
+		addNavItem: function( item ) {
+			/**
+			 * The delete nav is not added if no avatar
+			 * is set for the object
+			 */
+			if ( 1 === item.get( 'hide' ) ) {
+				return;
+			}
+
+			this.views.add( new bp.Views.NavItem( { model: item } ) );
+		},
+
+		showHideNavItem: function( item ) {
+			var isRendered = null;
+
+			/**
+			 * Loop in views to show/hide the nav item
+			 * BuddyPress is only using this for the delete nav
+			 */
+			_.each( this.views._views[''], function( view ) {
+				if ( 1 === view.model.get( 'hide' ) ) {
+					view.remove();
+				}
+
+				// Check to see if the nav is not already rendered
+				if ( item.get( 'id' ) === view.model.get( 'id' ) ) {
+					isRendered = true;
+				}
+			} );
+
+			// Add the Delete nav if not rendered
+			if ( ! _.isBoolean( isRendered ) ) {
+				this.addNavItem( item );
+			}
+		},
+
+		toggleView: function( event ) {
+			event.preventDefault();
+
+			// First make sure to remove all warnings
+			bp.Avatar.removeWarning();
+
+			var active = $( event.target ).data( 'nav' );
+
+			_.each( this.collection.models, function( model ) {
+				if ( model.id === active ) {
+					model.set( { active: 1 } );
+					this.trigger( 'bp-avatar-view:changed', model.id );
+				} else {
+					model.set( { active: 0 } );
+				}
+			}, this );
+		}
+	} );
+
+	// Nav item view
+	bp.Views.NavItem = bp.View.extend( {
+		tagName:    'li',
+		className:  'avatar-nav-item',
+		template: bp.template( 'bp-avatar-nav' ),
+
+		initialize: function() {
+			if ( 1 === this.model.get( 'active' ) ) {
+				this.el.className += ' current';
+			}
+			this.el.id += 'bp-avatar-' + this.model.get( 'id' );
+
+			this.model.on( 'change:active', this.setCurrentNav, this );
+		},
+
+		setCurrentNav: function( model ) {
+			if ( 1 === model.get( 'active' ) ) {
+				this.$el.addClass( 'current' );
+			} else {
+				this.$el.removeClass( 'current' );
+			}
+		}
+	} );
+
+	// Avatars view
+	bp.Views.Avatars = bp.View.extend( {
+		className: 'items',
+
+		initialize: function() {
+			_.each( this.collection.models, this.addItemView, this );
+		},
+
+		addItemView: function( item ) {
+			// Defaults to 150
+			var full_d = { full_h: 150, full_w: 150 };
+
+			// Make sure to take in account bp_core_avatar_full_height or bp_core_avatar_full_width php filters
+			if ( ! _.isUndefined( BP_Uploader.settings.crop.full_h ) && ! _.isUndefined( BP_Uploader.settings.crop.full_w ) ) {
+				full_d.full_h = BP_Uploader.settings.crop.full_h;
+				full_d.full_w = BP_Uploader.settings.crop.full_w;
+			}
+
+			// Set the avatar model
+			item.set( _.extend( _.pick( BP_Uploader.settings.defaults.multipart_params.bp_params,
+				'object',
+				'item_id',
+				'nonces'
+			), full_d ) );
+
+			// Add the view
+			this.views.add( new bp.Views.Avatar( { model: item } ) );
+		}
+	} );
+
+	// Avatar view
+	bp.Views.Avatar = bp.View.extend( {
+		className: 'item',
+		template: bp.template( 'bp-avatar-item' ),
+
+		events: {
+			'click .avatar-crop-submit': 'cropAvatar'
+		},
+
+		initialize: function() {
+			_.defaults( this.options, {
+				full_h:  BP_Uploader.settings.crop.full_h,
+				full_w:  BP_Uploader.settings.crop.full_w,
+				aspectRatio : 1
+			} );
+
+			// Display a warning if the image is smaller than minimum advised
+			if ( false !== this.model.get( 'feedback' ) ) {
+				bp.Avatar.displayWarning( this.model.get( 'feedback' ) );
+			}
+
+			this.on( 'ready', this.initCropper );
+		},
+
+		initCropper: function() {
+			var self = this,
+				tocrop = this.$el.find( '#avatar-to-crop img' ),
+				availableWidth = this.$el.width(),
+				selection = {}, crop_top, crop_bottom, crop_left, crop_right, nh, nw;
+
+			if ( ! _.isUndefined( this.options.full_h ) && ! _.isUndefined( this.options.full_w ) ) {
+				this.options.aspectRatio = this.options.full_w / this.options.full_h;
+			}
+
+			selection.w = this.model.get( 'width' );
+			selection.h = this.model.get( 'height' );
+
+			/**
+			 * Make sure the crop preview is at the right of the avatar
+			 * if the available width allowes it.
+			 */
+			if ( this.options.full_w + selection.w + 20 < availableWidth ) {
+				$( '#avatar-to-crop' ).addClass( 'adjust' );
+				this.$el.find( '.avatar-crop-management' ).addClass( 'adjust' );
+			}
+
+			if ( selection.h <= selection.w ) {
+				crop_top    = Math.round( selection.h / 4 );
+				nh = nw     = Math.round( selection.h / 2 );
+				crop_bottom = nh + crop_top;
+				crop_left   = ( selection.w - nw ) / 2;
+				crop_right  = nw + crop_left;
+			} else {
+				crop_left   = Math.round( selection.w / 4 );
+				nh = nw     = Math.round( selection.w / 2 );
+				crop_right  = nw + crop_left;
+				crop_top    = ( selection.h - nh ) / 2;
+				crop_bottom = nh + crop_top;
+			}
+
+			// Add the cropping interface
+			tocrop.Jcrop( {
+				onChange: _.bind( self.showPreview, self ),
+				onSelect: _.bind( self.showPreview, self ),
+				aspectRatio: self.options.aspectRatio,
+				setSelect: [ crop_left, crop_top, crop_right, crop_bottom ]
+			}, function() {
+				// Get the Jcrop API
+				bp.Avatar.jcropapi = this;
+			} );
+		},
+
+		cropAvatar: function( event ) {
+			event.preventDefault();
+
+			bp.Avatar.setAvatar( this.model );
+		},
+
+		showPreview: function( coords ) {
+			if ( ! coords.w || ! coords.h ) {
+				return;
+			}
+
+			if ( parseInt( coords.w, 10 ) > 0 ) {
+				var fw = this.options.full_w;
+				var fh = this.options.full_h;
+				var rx = fw / coords.w;
+				var ry = fh / coords.h;
+
+				// Update the model
+				this.model.set( { x: coords.x, y: coords.y, w: coords.w, h: coords.h } );
+
+				$( '#avatar-crop-preview' ).css( {
+					maxWidth:'none',
+					width: Math.round( rx *  this.model.get( 'width' ) )+ 'px',
+					height: Math.round( ry * this.model.get( 'height' ) )+ 'px',
+					marginLeft: '-' + Math.round( rx * this.model.get( 'x' ) ) + 'px',
+					marginTop: '-' + Math.round( ry * this.model.get( 'y' ) ) + 'px'
+				} );
+			}
+		}
+	} );
+
+	// BuddyPress Avatar Feedback view
+	bp.Views.AvatarStatus = bp.View.extend( {
+		tagName: 'p',
+		className: 'updated',
+		id: 'bp-avatar-feedback',
+
+		initialize: function() {
+			this.el.className += ' ' + this.options.type;
+			this.value = this.options.value;
+		},
+
+		render: function() {
+			this.$el.html( this.value );
+			return this;
+		}
+	} );
+
+	// BuddyPress Avatar Delete view
+	bp.Views.DeleteAvatar = bp.View.extend( {
+		tagName: 'div',
+		id: 'bp-delete-avatar-container',
+		template: bp.template( 'bp-avatar-delete' ),
+
+		events: {
+			'click #bp-delete-avatar': 'deleteAvatar'
+		},
+
+		deleteAvatar: function( event ) {
+			event.preventDefault();
+
+			bp.Avatar.deleteAvatar( this.model );
+		}
+	} );
+
+	bp.Avatar.start();
+
+})( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-core/js/bp-plupload.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/bp-plupload.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/bp-plupload.js	(revision 12265)
@@ -0,0 +1,419 @@
+/* global bp, plupload, BP_Uploader, _, JSON, Backbone */
+
+window.wp = window.wp || {};
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Uploader === 'undefined' ) {
+		return;
+	}
+
+	/**
+	 * Extend the bp global with what we need from the wp one.
+	 * and make sure previously defined BuddyPress attributes
+	 * are not removed (eg: bp.mentions)
+	 */
+	_.extend( bp, _.pick( wp, 'Backbone', 'ajax', 'template' ) );
+
+	// Init Models, Collections, Views and the BuddyPress Uploader
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+	bp.Uploader    = {};
+
+	/**
+	 * BuddyPress Uploader.
+	 *
+	 * This is an adapted version of wp.Uploader
+	 */
+	bp.Uploader.uploader = function() {
+		var self = this,
+			isIE = navigator.userAgent.indexOf('Trident/') !== -1 || navigator.userAgent.indexOf('MSIE ') !== -1;
+
+		this.params  = BP_Uploader.settings;
+		this.strings = BP_Uploader.strings;
+
+		this.supports = {
+			upload: this.params.browser.supported
+		};
+
+		this.supported = this.supports.upload;
+
+		if ( ! this.supported ) {
+			/*jshint -W020 */
+			BP_Uploader = undefined;
+			return;
+		}
+
+		// Make sure flash sends cookies (seems in IE it does without switching to urlstream mode)
+		if ( ! isIE && 'flash' === plupload.predictRuntime( this.params.defaults ) &&
+			( ! this.params.defaults.required_features || ! this.params.defaults.required_features.hasOwnProperty( 'send_binary_string' ) ) ) {
+
+			this.params.defaults.required_features = this.params.defaults.required_features || {};
+			this.params.defaults.required_features.send_binary_string = true;
+		}
+
+		this.uploader = new plupload.Uploader( this.params.defaults );
+
+		/**
+		 * After the Uploader has been initialized, initialize some behaviors for the dropzone.
+		 *
+		 * @event Init
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 */
+		this.uploader.bind( 'Init', function( uploader ) {
+			var container    = $( '#' + self.params.defaults.container ),
+			    drop_element = $( '#' + self.params.defaults.drop_element );
+
+			if ( 'html4' === uploader.runtime ) {
+				uploader.settings.multipart_params.html4 = true;
+			}
+
+			/**
+			 * Avatars need to be cropped, by default we are using an original
+			 * max width of 450px, but there can be cases when this max width
+			 * is larger than the one of the Avatar UI (eg: on mobile). To avoid any
+			 * difficulties, we're adding a ui_available_width argument to the bp_params
+			 * object and set it according to the container width. This value will be
+			 * checked during the upload process to eventually adapt the resized avatar.
+			 */
+			if ( 'bp_avatar_upload' ===  uploader.settings.multipart_params.action ) {
+				 uploader.settings.multipart_params.bp_params.ui_available_width = container.width();
+			}
+
+			if ( uploader.features.dragdrop && ! self.params.browser.mobile ) {
+				container.addClass( 'drag-drop' );
+				drop_element.bind( 'dragover.wp-uploader', function() {
+					container.addClass( 'drag-over' );
+				} ).bind( 'dragleave.wp-uploader, drop.wp-uploader', function() {
+					container.removeClass( 'drag-over' );
+				} );
+			} else {
+				container.removeClass( 'drag-drop' );
+				drop_element.unbind( '.wp-uploader' );
+			}
+
+		} );
+
+		// See https://core.trac.wordpress.org/ticket/37039
+		this.uploader.bind( 'postinit', function( up ) {
+			up.refresh();
+		});
+
+		// Init BuddyPress Uploader
+		this.uploader.init();
+
+		/**
+		 * Feedback callback.
+		 *
+		 * Add a new message to the errors collection, so it's possible
+		 * to give some feedback to the user
+		 *
+		 * @param  {string}        message
+		 * @param  {object}        data
+		 * @param  {plupload.File} file     File that was uploaded.
+		 */
+		this.feedback = function( message, data, file ) {
+			if ( ! _.isNull( file ) && file.item ) {
+				file.item.clear();
+			}
+
+			bp.Uploader.filesError.unshift( {
+				message: message,
+				data:    data,
+				file:    file
+			} );
+		};
+
+		/**
+		 * After files were filtered and added to the queue, create a model for each.
+		 *
+		 * @event FilesAdded
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 * @param {Array}             files    Array of file objects that were added to queue by the user.
+		 */
+		this.uploader.bind( 'FilesAdded', function( uploader, files ) {
+			var hundredmb = 100 * 1024 * 1024, max = parseInt( uploader.settings.max_file_size, 10 ),
+			    _this = this;
+
+			/**
+			 * In case the multiple selection is false (eg: avatar) stop the process and send
+			 * and event containing a warning
+			 */
+			if ( ! uploader.settings.multi_selection && files.length > 1 ) {
+				for ( var i in files ) {
+					uploader.removeFile( files[i] );
+				}
+
+				$( self ).trigger( 'bp-uploader-warning', self.strings.unique_file_warning );
+				return;
+			}
+
+			_.each( files, function( file ) {
+				var attributes;
+
+				// Ignore failed uploads.
+				if ( plupload.FAILED === file.status ) {
+					return;
+				}
+
+				if ( max > hundredmb && file.size > hundredmb && uploader.runtime !== 'html5' ) {
+					_this.uploadSizeError( uploader, file, true );
+				} else {
+					attributes = _.extend( {
+						id:        file.id,
+						file:      file,
+						uploading: true,
+						date:      new Date(),
+						filename:  file.name
+					}, _.pick( file, 'loaded', 'size', 'percent' ) );
+
+					file.item = new bp.Models.File( attributes );
+					bp.Uploader.filesQueue.add( file.item );
+				}
+
+			} );
+
+			uploader.refresh();
+			uploader.start();
+		} );
+
+		/**
+		 * Update each file item on progress
+		 *
+		 * @event UploadProgress
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 * @param {Object}            file
+		 */
+		this.uploader.bind( 'UploadProgress', function( uploader, file ) {
+			file.item.set( _.pick( file, 'loaded', 'percent' ) );
+		} );
+
+		/**
+		 * After a file is successfully uploaded, update its model.
+		 *
+		 * @event FileUploaded
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 * @param {plupload.File}     file     File that was uploaded.
+		 * @param {Object}            response Object with response properties.
+		 * @return {mixed}
+		 */
+		this.uploader.bind( 'FileUploaded', function( uploader, file, response ) {
+			var message = self.strings.default_error;
+
+			try {
+				response = JSON.parse( response.response );
+			} catch ( e ) {
+				return self.feedback( message, e, file );
+			}
+
+			if ( ! _.isObject( response ) || _.isUndefined( response.success ) ) {
+				return self.feedback( message, null, file );
+			} else if ( ! response.success ) {
+				if ( response.data && response.data.message ) {
+					message = response.data.message;
+				}
+
+				return self.feedback( message, response.data, file );
+			}
+
+			_.each(['file','loaded','size','percent'], function( key ) {
+				file.item.unset( key );
+			} );
+
+			file.item.set( _.extend( response.data, { uploading: false } ) );
+
+			//  Add the file to the Uploaded ones
+			bp.Uploader.filesUploaded.add( file.item );
+
+		} );
+
+		/**
+		 * Trigger an event to inform a new upload is being processed
+		 *
+		 * Mainly used to remove an eventual warning
+		 *
+		 * @event BeforeUpload
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 * @param {Array}             files    Array of file objects that were added to queue by the user.
+		 */
+		this.uploader.bind( 'BeforeUpload', function( uploader, files ) {
+			$( self ).trigger( 'bp-uploader-new-upload', uploader, files );
+		} );
+
+		/**
+		 * Reset the filesQueue once the upload is complete
+		 *
+		 * @event BeforeUpload
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 * @param {Array}             files    Array of file objects that were added to queue by the user.
+		 */
+		this.uploader.bind( 'UploadComplete', function( uploader, files ) {
+			$( self ).trigger( 'bp-uploader-upload-complete', uploader, files );
+			bp.Uploader.filesQueue.reset();
+		} );
+
+		/**
+		 * Map Plupload errors & Create a warning when plupload failed
+		 *
+		 * @event Error
+		 * @param {plupload.Uploader} uploader Uploader instance.
+		 * @param {Object}            pluploadError Plupload error
+		 */
+		this.uploader.bind( 'Error', function( uploader, pluploadError ) {
+			var message = self.strings.default_error,
+				key,
+				errors = {
+					'FAILED':                 self.strings.upload_failed,
+					'FILE_EXTENSION_ERROR':   self.strings.invalid_filetype,
+					'IMAGE_FORMAT_ERROR':     self.strings.not_an_image,
+					'IMAGE_MEMORY_ERROR':     self.strings.image_memory_exceeded,
+					'IMAGE_DIMENSIONS_ERROR': self.strings.image_dimensions_exceeded,
+					'GENERIC_ERROR':          self.strings.upload_failed,
+					'IO_ERROR':               self.strings.io_error,
+					'HTTP_ERROR':             self.strings.http_error,
+					'SECURITY_ERROR':         self.strings.security_error,
+					'FILE_SIZE_ERROR':        self.strings.file_exceeds_size_limit.replace( '%s' , $( '<span />' ).text( pluploadError.file.name ).html() )
+				};
+
+			// Check for plupload errors.
+			for ( key in errors ) {
+				if ( pluploadError.code === plupload[ key ] ) {
+					message = errors[ key ];
+					break;
+				}
+			}
+
+			$( self ).trigger( 'bp-uploader-warning', message );
+			uploader.refresh();
+		} );
+	};
+
+	// Create a very generic Model for files
+	bp.Models.File = Backbone.Model.extend( {
+		file: {}
+	} );
+
+	// Add Collections to store queue, uploaded files and errors
+	$.extend( bp.Uploader, {
+		filesQueue    : new Backbone.Collection(),
+		filesUploaded : new Backbone.Collection(),
+		filesError    : new Backbone.Collection()
+	} );
+
+	// Extend wp.Backbone.View with .prepare() and .inject()
+	bp.View = bp.Backbone.View.extend( {
+		inject: function( selector ) {
+			this.render();
+			$(selector).html( this.el );
+			this.views.ready();
+		},
+
+		prepare: function() {
+			if ( ! _.isUndefined( this.model ) && _.isFunction( this.model.toJSON ) ) {
+				return this.model.toJSON();
+			} else {
+				return {};
+			}
+		}
+	} );
+
+	// BuddyPress Uploader main view
+	bp.Views.Uploader = bp.View.extend( {
+		className: 'bp-uploader-window',
+		template: bp.template( 'upload-window' ),
+
+		defaults: _.pick( BP_Uploader.settings.defaults, 'container', 'drop_element', 'browse_button' ),
+
+		initialize: function() {
+			this.warnings = [];
+			this.model    = new Backbone.Model( this.defaults );
+			this.on( 'ready', this.initUploader );
+		},
+
+		initUploader: function() {
+			this.uploader = new bp.Uploader.uploader();
+			$( this.uploader ).on( 'bp-uploader-warning', _.bind( this.setWarning, this ) );
+			$( this.uploader ).on( 'bp-uploader-new-upload', _.bind( this.resetWarning, this ) );
+		},
+
+		setWarning: function( event, message ) {
+			if ( _.isUndefined( message ) ) {
+				return;
+			}
+
+			var warning = new bp.Views.uploaderWarning( {
+				value: message
+			} ).render();
+
+			this.warnings.push( warning );
+
+			this.$el.after( warning.el );
+		},
+
+		resetWarning: function() {
+			if ( 0 === this.warnings.length ) {
+				return;
+			}
+
+			// Remove all warning views
+			_.each( this.warnings, function( view ) {
+				view.remove();
+			} );
+
+			// Reset Warnings
+			this.warnings = [];
+		}
+	} );
+
+	// BuddyPress Uploader warning view
+	bp.Views.uploaderWarning = bp.View.extend( {
+		tagName: 'p',
+		className: 'warning',
+
+		initialize: function() {
+			this.value = this.options.value;
+		},
+
+		render: function() {
+			this.$el.html( this.value );
+			return this;
+		}
+	} );
+
+	// BuddyPress Uploader Files view
+	bp.Views.uploaderStatus = bp.View.extend( {
+		className: 'files',
+
+		initialize: function() {
+			_.each( this.collection.models, this.addFile, this );
+			this.collection.on( 'change:percent', this.progress, this );
+			bp.Uploader.filesError.on( 'add', this.feedback, this );
+		},
+
+		addFile: function( file ) {
+			this.views.add( new bp.Views.uploaderProgress( { model: file } ) );
+		},
+
+		progress:function( model ) {
+			if ( ! _.isUndefined( model.get( 'percent' ) ) ) {
+				$( '#' + model.get('id') + ' .bp-progress .bp-bar' ).css( 'width', model.get('percent') + '%' );
+			}
+		},
+
+		feedback: function( model ) {
+			if ( ! _.isUndefined( model.get( 'message' ) ) && ! _.isUndefined( model.get( 'file' ) ) ) {
+				$( '#' + model.get( 'file' ).id ).html( model.get( 'message' ) ).addClass( 'error' );
+			}
+		}
+	} );
+
+	// BuddyPress Uploader File progress view
+	bp.Views.uploaderProgress = bp.View.extend( {
+		className: 'bp-uploader-progress',
+		template: bp.template( 'progress-window' )
+	} );
+
+})( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-core/js/confirm.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/confirm.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/confirm.js	(revision 12265)
@@ -0,0 +1,12 @@
+/* jshint devel: true */
+/* global BP_Confirm */
+
+jQuery( document ).ready( function() {
+	jQuery( '#buddypress' ).on( 'click', 'a.confirm', function() {
+		if ( confirm( BP_Confirm.are_you_sure ) ) {
+			return true;
+		} else {
+			return false;
+		}
+	} );
+} );
Index: /tags/4.0.0-beta1/src/bp-core/js/cover-image.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/cover-image.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/cover-image.js	(revision 12265)
@@ -0,0 +1,277 @@
+/* global bp, BP_Uploader, _, Backbone */
+
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Uploader === 'undefined' ) {
+		return;
+	}
+
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+
+	bp.CoverImage = {
+		start: function() {
+
+			// Init some vars
+			this.views   = new Backbone.Collection();
+			this.warning = null;
+
+			// The Cover Image Attachment object.
+			this.Attachment = new Backbone.Model();
+
+			// Set up views
+			this.uploaderView();
+
+			// Inform about the needed dimensions
+			this.displayWarning( BP_Uploader.strings.cover_image_warnings.dimensions );
+
+			// Set up the delete view if needed
+			if ( true === BP_Uploader.settings.defaults.multipart_params.bp_params.has_cover_image ) {
+				this.deleteView();
+			}
+		},
+
+		uploaderView: function() {
+			// Listen to the Queued uploads
+			bp.Uploader.filesQueue.on( 'add', this.uploadProgress, this );
+
+			// Create the BuddyPress Uploader
+			var uploader = new bp.Views.Uploader();
+
+			// Add it to views
+			this.views.add( { id: 'upload', view: uploader } );
+
+			// Display it
+			uploader.inject( '.bp-cover-image' );
+		},
+
+		uploadProgress: function() {
+			// Create the Uploader status view
+			var coverImageUploadProgress = new bp.Views.coverImageUploadProgress( { collection: bp.Uploader.filesQueue } );
+
+			if ( ! _.isUndefined( this.views.get( 'status' ) ) ) {
+				this.views.set( { id: 'status', view: coverImageUploadProgress } );
+			} else {
+				this.views.add( { id: 'status', view: coverImageUploadProgress } );
+			}
+
+			// Display it
+			coverImageUploadProgress.inject( '.bp-cover-image-status' );
+		},
+
+		deleteView: function() {
+			// Create the delete model
+			var delete_model = new Backbone.Model( _.pick( BP_Uploader.settings.defaults.multipart_params.bp_params,
+				['object', 'item_id', 'nonces']
+			) );
+
+			// Do not add it if already there!
+			if ( ! _.isUndefined( this.views.get( 'delete' ) ) ) {
+				return;
+			}
+
+			// Create the delete view
+			var deleteView = new bp.Views.DeleteCoverImage( { model: delete_model } );
+
+			// Add it to views
+			this.views.add( { id: 'delete', view: deleteView } );
+
+			// Display it
+			deleteView.inject( '.bp-cover-image-manage' );
+		},
+
+		deleteCoverImage: function( model ) {
+			var self = this,
+				deleteView;
+
+			// Remove the delete view
+			if ( ! _.isUndefined( this.views.get( 'delete' ) ) ) {
+				deleteView = this.views.get( 'delete' );
+				deleteView.get( 'view' ).remove();
+				this.views.remove( { id: 'delete', view: deleteView } );
+			}
+
+			// Remove the cover image !
+			bp.ajax.post( 'bp_cover_image_delete', {
+				json:          true,
+				item_id:       model.get( 'item_id' ),
+				object:        model.get( 'object' ),
+				nonce:         model.get( 'nonces' ).remove
+			} ).done( function( response ) {
+				var coverImageStatus = new bp.Views.CoverImageStatus( {
+					value : BP_Uploader.strings.feedback_messages[ response.feedback_code ],
+					type : 'success'
+				} );
+
+				self.views.add( {
+					id   : 'status',
+					view : coverImageStatus
+				} );
+
+				coverImageStatus.inject( '.bp-cover-image-status' );
+
+				// Reset the header of the page
+				if ( '' === response.reset_url ) {
+					$( '#header-cover-image' ).css( {
+						'background-image': 'none'
+					} );
+				} else {
+					$( '#header-cover-image' ).css( {
+						'background-image': 'url( ' + response.reset_url + ' )'
+					} );
+				}
+
+				// Reset the has_cover_image bp_param
+				BP_Uploader.settings.defaults.multipart_params.bp_params.has_cover_image = false;
+
+				/**
+				 * Reset the Attachment object
+				 *
+				 * You can run extra actions once the cover image is set using:
+				 * bp.CoverImage.Attachment.on( 'change:url', function( data ) { your code } );
+				 *
+				 * In this case data.attributes will include the default url for the
+				 * cover image (most of the time: ''), the object and the item_id concerned.
+				 */
+				self.Attachment.set( _.extend(
+					_.pick( model.attributes, ['object', 'item_id'] ),
+					{ url: response.reset_url, action: 'deleted' }
+				) );
+
+			} ).fail( function( response ) {
+				var feedback = BP_Uploader.strings.default_error;
+				if ( ! _.isUndefined( response ) ) {
+					feedback = BP_Uploader.strings.feedback_messages[ response.feedback_code ];
+				}
+
+				var coverImageStatus = new bp.Views.CoverImageStatus( {
+					value : feedback,
+					type : 'error'
+				} );
+
+				self.views.add( {
+					id   : 'status',
+					view : coverImageStatus
+				} );
+
+				coverImageStatus.inject( '.bp-cover-image-status' );
+
+				// Put back the delete view
+				bp.CoverImage.deleteView();
+			} );
+		},
+
+		removeWarning: function() {
+			if ( ! _.isNull( this.warning ) ) {
+				this.warning.remove();
+			}
+		},
+
+		displayWarning: function( message ) {
+			this.removeWarning();
+
+			this.warning = new bp.Views.uploaderWarning( {
+				value: message
+			} );
+
+			this.warning.inject( '.bp-cover-image-status' );
+		}
+	};
+
+	// Custom Uploader Files view
+	bp.Views.coverImageUploadProgress = bp.Views.uploaderStatus.extend( {
+		className: 'files',
+
+		initialize: function() {
+			bp.Views.uploaderStatus.prototype.initialize.apply( this, arguments );
+
+			this.collection.on( 'change:url', this.uploadResult, this );
+		},
+
+		uploadResult: function( model ) {
+			var message, type;
+
+			if ( ! _.isUndefined( model.get( 'url' ) ) ) {
+
+				// Image is too small
+				if ( 0 === model.get( 'feedback_code' ) ) {
+					message = BP_Uploader.strings.cover_image_warnings.dimensions;
+					type    = 'warning';
+
+				// Success, Rock n roll!
+				} else {
+					message = BP_Uploader.strings.feedback_messages[ model.get( 'feedback_code' ) ];
+					type = 'success';
+				}
+
+				this.views.set( '.bp-uploader-progress', new bp.Views.CoverImageStatus( {
+					value : message,
+					type  : type
+				} ) );
+
+				// Update the header of the page
+				$( '#header-cover-image' ).css( {
+					'background-image': 'url( ' + model.get( 'url' ) + ' )'
+				} );
+
+				// Add the delete view
+				bp.CoverImage.deleteView();
+
+				/**
+				 * Set the Attachment object
+				 *
+				 * You can run extra actions once the cover image is set using:
+				 * bp.CoverImage.Attachment.on( 'change:url', function( data ) { your code } );
+				 *
+				 * In this case data.attributes will include the url to the newly
+				 * uploaded cover image, the object and the item_id concerned.
+				 */
+				bp.CoverImage.Attachment.set( _.extend(
+					_.pick( BP_Uploader.settings.defaults.multipart_params.bp_params, ['object', 'item_id'] ),
+					{ url: model.get( 'url' ), action: 'uploaded' }
+				) );
+			}
+		}
+	} );
+
+	// BuddyPress Cover Image Feedback view
+	bp.Views.CoverImageStatus = bp.View.extend( {
+		tagName: 'p',
+		className: 'updated',
+		id: 'bp-cover-image-feedback',
+
+		initialize: function() {
+			this.el.className += ' ' + this.options.type;
+			this.value = this.options.value;
+		},
+
+		render: function() {
+			this.$el.html( this.value );
+			return this;
+		}
+	} );
+
+	// BuddyPress Cover Image Delete view
+	bp.Views.DeleteCoverImage = bp.View.extend( {
+		tagName: 'div',
+		id: 'bp-delete-cover-image-container',
+		template: bp.template( 'bp-cover-image-delete' ),
+
+		events: {
+			'click #bp-delete-cover-image': 'deleteCoverImage'
+		},
+
+		deleteCoverImage: function( event ) {
+			event.preventDefault();
+
+			bp.CoverImage.deleteCoverImage( this.model );
+		}
+	} );
+
+	bp.CoverImage.start();
+
+})( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-core/js/jquery-query.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/jquery-query.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/jquery-query.js	(revision 12265)
@@ -0,0 +1,6 @@
+/* jshint unused: false */
+
+function bp_get_querystring( n ) {
+	var half = location.search.split( n + '=' )[1];
+	return half ? decodeURIComponent( half.split('&')[0] ) : null;
+}
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery-cookie.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery-cookie.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery-cookie.js	(revision 12265)
@@ -0,0 +1,120 @@
+/* jshint undef: false */
+
+/*!
+ * jQuery Cookie Plugin v1.4.1
+ * https://github.com/carhartl/jquery-cookie
+ *
+ * Copyright 2013 Klaus Hartl
+ * Released under the MIT license
+ */
+(function(factory) {
+	// AMD
+	if (typeof define === 'function' && define.amd) {
+		define(['jquery'], factory);
+	// CommonJS
+	} else if (typeof exports === 'object') {
+		factory(require('jquery'));
+	// Browser globals
+	} else {
+		factory(jQuery);
+	}
+}(function($) {
+
+	var pluses = /\+/g;
+
+	function encode(s) {
+		return config.raw ? s : encodeURIComponent(s);
+	}
+
+	function decode(s) {
+		return config.raw ? s : decodeURIComponent(s);
+	}
+
+	function stringifyCookieValue(value) {
+		return encode(config.json ? JSON.stringify(value) : String(value));
+	}
+
+	function parseCookieValue(s) {
+		if (s.indexOf('"') === 0) {
+			// This is a quoted cookie as according to RFC2068, unescape...
+			s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
+		}
+
+		try {
+			// Replace server-side written pluses with spaces.
+			// If we can't decode the cookie, ignore it, it's unusable.
+			// If we can't parse the cookie, ignore it, it's unusable.
+			s = decodeURIComponent(s.replace(pluses, ' '));
+			return config.json ? JSON.parse(s) : s;
+		} catch (e) {
+		}
+	}
+
+	function read(s, converter) {
+		var value = config.raw ? s : parseCookieValue(s);
+		return $.isFunction(converter) ? converter(value) : value;
+	}
+
+	var config = $.cookie = function(key, value, options) {
+
+		// Write
+
+		if (value !== undefined && !$.isFunction(value)) {
+			options = $.extend({}, config.defaults, options);
+
+			if (typeof options.expires === 'number') {
+				var days = options.expires, t = options.expires = new Date();
+				t.setTime(+t + days * 864e+5);
+			}
+
+			return (document.cookie = [
+				encode(key), '=', stringifyCookieValue(value),
+				options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+				options.path ? '; path=' + options.path : '',
+				options.domain ? '; domain=' + options.domain : '',
+				options.secure ? '; secure' : ''
+			].join(''));
+		}
+
+		// Read
+
+		var result = key ? undefined : {};
+
+		// To prevent the for loop in the first place assign an empty array
+		// in case there are no cookies at all. Also prevents odd result when
+		// calling $.cookie().
+		var cookies = document.cookie ? document.cookie.split('; ') : [];
+
+		for (var i = 0, l = cookies.length; i < l; i++) {
+			var parts = cookies[i].split('=');
+			var name = decode(parts.shift());
+			var cookie = parts.join('=');
+
+			if (key && key === name) {
+				// If second argument (value) is a function it's a converter...
+				result = read(cookie, value);
+				break;
+			}
+
+			// Prevent storing a cookie that we couldn't decode.
+			if (!key && (cookie = read(cookie)) !== undefined) {
+				result[name] = cookie;
+			}
+		}
+
+		return result;
+	};
+
+	config.defaults = {};
+
+	$.removeCookie = function(key, options) {
+		if ($.cookie(key) === undefined) {
+			return false;
+		}
+
+		// Must not alter options, thus extending a fresh object...
+		$.cookie(key, '', $.extend({}, options, {expires: -1}));
+		return !$.cookie(key);
+	};
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery-scroll-to.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery-scroll-to.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery-scroll-to.js	(revision 12265)
@@ -0,0 +1,208 @@
+/* jshint undef: false */
+/* jshint -W065 */
+
+/*!
+ * jQuery.ScrollTo
+ * Copyright (c) 2007-2014 Ariel Flesler - aflesler<a>gmail<d>com | http://flesler.blogspot.com
+ * Licensed under MIT
+ * http://flesler.blogspot.com/2007/10/jqueryscrollto.html
+ * @projectDescription Easy element scrolling using jQuery.
+ * @author Ariel Flesler
+ * @version 1.4.12
+ */
+
+(function(factory) {
+	// AMD
+	if (typeof define === 'function' && define.amd) {
+		define(['jquery'], factory);
+	// CommonJS
+	} else if (typeof exports === 'object') {
+		factory(require('jquery'));
+	// Browser globals
+	} else {
+		factory(jQuery);
+	}
+}(function($) {
+
+	var $scrollTo = $.scrollTo = function(target, duration, settings) {
+		return $(window).scrollTo(target, duration, settings);
+	};
+
+	$scrollTo.defaults = {
+		axis: 'xy',
+		duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1,
+		limit: true
+	};
+
+	// Returns the element that needs to be animated to scroll the window.
+	// Kept for backwards compatibility (specially for localScroll & serialScroll)
+	$scrollTo.window = function() {
+		return $(window)._scrollable();
+	};
+
+	// Hack, hack, hack :)
+	// Returns the real elements to scroll (supports window/iframes, documents and regular nodes)
+	$.fn._scrollable = function() {
+		return this.map(function() {
+			var elem = this,
+					isWin = !elem.nodeName || $.inArray(elem.nodeName.toLowerCase(), ['iframe', '#document', 'html', 'body']) !== -1;
+
+			if (!isWin) {
+				return elem;
+			}
+
+			var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem;
+
+			return /webkit/i.test(navigator.userAgent) || doc.compatMode === 'BackCompat' ?
+					doc.body :
+					doc.documentElement;
+		});
+	};
+
+	$.fn.scrollTo = function(target, duration, settings) {
+		if (typeof duration === 'object') {
+			settings = duration;
+			duration = 0;
+		}
+		if (typeof settings === 'function') {
+			settings = {onAfter: settings};
+		}
+
+		if (target === 'max') {
+			target = 9e9;
+		}
+
+		settings = $.extend({}, $scrollTo.defaults, settings);
+		// Speed is still recognized for backwards compatibility
+		duration = duration || settings.duration;
+		// Make sure the settings are given right
+		settings.queue = settings.queue && settings.axis.length > 1;
+
+		// Let's keep the overall duration
+		if (settings.queue) {
+			duration /= 2;
+		}
+
+		settings.offset = both(settings.offset);
+		settings.over = both(settings.over);
+
+		return this._scrollable().each(function() {
+
+			// Null target yields nothing, just like jQuery does
+			if (target === null) {
+				return;
+			}
+
+			var elem = this,
+					$elem = $(elem),
+					targ = target, toff, attr = {},
+					win = $elem.is('html,body');
+
+			switch (typeof targ) {
+				// A number will pass the regex
+				case 'number':
+				case 'string':
+					if (/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)) {
+						targ = both(targ);
+						// We are done
+						break;
+					}
+					// Relative/Absolute selector, no break!
+					targ = win ? $(targ) : $(targ, this);
+					if (!targ.length) {
+						return;
+					}
+					/* falls through */
+				case 'object':
+					// DOMElement / jQuery
+					if (targ.is || targ.style) {
+						// Get the real position of the target
+						toff = (targ = $(targ)).offset();
+					}
+			}
+
+			var offset = $.isFunction(settings.offset) && settings.offset(elem, targ) || settings.offset;
+
+			$.each(settings.axis.split(''), function(i, axis) {
+				var Pos = axis === 'x' ? 'Left' : 'Top',
+						pos = Pos.toLowerCase(),
+						key = 'scroll' + Pos,
+						old = elem[key],
+						max = $scrollTo.max(elem, axis);
+
+				if (toff) {// jQuery / DOMElement
+					attr[key] = toff[pos] + (win ? 0 : old - $elem.offset()[pos]);
+
+					// If it's a dom element, reduce the margin
+					if (settings.margin) {
+						attr[key] -= parseInt(targ.css('margin' + Pos)) || 0;
+						attr[key] -= parseInt(targ.css('border' + Pos + 'Width')) || 0;
+					}
+
+					attr[key] += offset[pos] || 0;
+
+					// Scroll to a fraction of its width/height
+					if (settings.over[pos]) {
+						attr[key] += targ[axis === 'x' ? 'width' : 'height']() * settings.over[pos];
+					}
+				} else {
+					var val = targ[pos];
+					// Handle percentage values
+					attr[key] = val.slice && val.slice(-1) === '%' ?
+							parseFloat(val) / 100 * max
+							: val;
+				}
+
+				// Number or 'number'
+				if (settings.limit && /^\d+$/.test(attr[key])) {
+					// Check the limits
+					attr[key] = attr[key] <= 0 ? 0 : Math.min(attr[key], max);
+				}
+
+				// Queueing axes
+				if (!i && settings.queue) {
+					// Don't waste time animating, if there's no need.
+					if (old !== attr[key]) {
+						// Intermediate animation
+						animate(settings.onAfterFirst);
+					}
+					// Don't animate this axis again in the next iteration.
+					delete attr[key];
+				}
+			});
+
+			animate(settings.onAfter);
+
+			function animate(callback) {
+				$elem.animate(attr, duration, settings.easing, callback && function() {
+					callback.call(this, targ, settings);
+				});
+			}
+
+		}).end();
+	};
+
+	// Max scrolling position, works on quirks mode
+	// It only fails (not too badly) on IE, quirks mode.
+	$scrollTo.max = function(elem, axis) {
+		var Dim = axis === 'x' ? 'Width' : 'Height',
+				scroll = 'scroll' + Dim;
+
+		if (!$(elem).is('html,body')) {
+			return elem[scroll] - $(elem)[Dim.toLowerCase()]();
+		}
+
+		var size = 'client' + Dim,
+				html = elem.ownerDocument.documentElement,
+				body = elem.ownerDocument.body;
+
+		return Math.max(html[scroll], body[scroll]) - Math.min(html[size], body[size]);
+	};
+
+	function both(val) {
+		return $.isFunction(val) || typeof val === 'object' ? val : {top: val, left: val};
+	}
+
+	// AMD requirement
+	return $scrollTo;
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.atwho.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.atwho.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.atwho.js	(revision 12265)
@@ -0,0 +1,1212 @@
+/**
+ * at.js - 1.5.4
+ * Copyright (c) 2017 chord.luo <chord.luo@gmail.com>;
+ * Homepage: http://ichord.github.com/At.js
+ * License: MIT
+ */
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module unless amdModuleId is set
+    define(["jquery"], function (a0) {
+      return (factory(a0));
+    });
+  } else if (typeof exports === 'object') {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like environments that support module.exports,
+    // like Node.
+    module.exports = factory(require("jquery"));
+  } else {
+    factory(jQuery);
+  }
+}(this, function ($) {
+var DEFAULT_CALLBACKS, KEY_CODE;
+
+KEY_CODE = {
+  ESC: 27,
+  TAB: 9,
+  ENTER: 13,
+  CTRL: 17,
+  A: 65,
+  P: 80,
+  N: 78,
+  LEFT: 37,
+  UP: 38,
+  RIGHT: 39,
+  DOWN: 40,
+  BACKSPACE: 8,
+  SPACE: 32
+};
+
+DEFAULT_CALLBACKS = {
+  beforeSave: function(data) {
+    return Controller.arrayToDefaultHash(data);
+  },
+  matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+    var _a, _y, match, regexp, space;
+    flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+    if (should_startWithSpace) {
+      flag = '(?:^|\\s)' + flag;
+    }
+    _a = decodeURI("%C3%80");
+    _y = decodeURI("%C3%BF");
+    space = acceptSpaceBar ? "\ " : "";
+    regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
+    match = regexp.exec(subtext);
+    if (match) {
+      return match[2] || match[1];
+    } else {
+      return null;
+    }
+  },
+  filter: function(query, data, searchKey) {
+    var _results, i, item, len;
+    _results = [];
+    for (i = 0, len = data.length; i < len; i++) {
+      item = data[i];
+      if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) {
+        _results.push(item);
+      }
+    }
+    return _results;
+  },
+  remoteFilter: null,
+  sorter: function(query, items, searchKey) {
+    var _results, i, item, len;
+    if (!query) {
+      return items;
+    }
+    _results = [];
+    for (i = 0, len = items.length; i < len; i++) {
+      item = items[i];
+      item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
+      if (item.atwho_order > -1) {
+        _results.push(item);
+      }
+    }
+    return _results.sort(function(a, b) {
+      return a.atwho_order - b.atwho_order;
+    });
+  },
+  tplEval: function(tpl, map) {
+    var error, error1, template;
+    template = tpl;
+    try {
+      if (typeof tpl !== 'string') {
+        template = tpl(map);
+      }
+      return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+        return map[key];
+      });
+    } catch (error1) {
+      error = error1;
+      return "";
+    }
+  },
+  highlighter: function(li, query) {
+    var regexp;
+    if (!query) {
+      return li;
+    }
+    regexp = new RegExp(">\\s*([^\<]*?)(" + query.replace("+", "\\+") + ")([^\<]*)\\s*<", 'ig');
+    return li.replace(regexp, function(str, $1, $2, $3) {
+      return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
+    });
+  },
+  beforeInsert: function(value, $li, e) {
+    return value;
+  },
+  beforeReposition: function(offset) {
+    return offset;
+  },
+  afterMatchFailed: function(at, el) {}
+};
+
+var App;
+
+App = (function() {
+  function App(inputor) {
+    this.currentFlag = null;
+    this.controllers = {};
+    this.aliasMaps = {};
+    this.$inputor = $(inputor);
+    this.setupRootElement();
+    this.listen();
+  }
+
+  App.prototype.createContainer = function(doc) {
+    var ref;
+    if ((ref = this.$el) != null) {
+      ref.remove();
+    }
+    return $(doc.body).append(this.$el = $("<div class='atwho-container'></div>"));
+  };
+
+  App.prototype.setupRootElement = function(iframe, asRoot) {
+    var error, error1;
+    if (asRoot == null) {
+      asRoot = false;
+    }
+    if (iframe) {
+      this.window = iframe.contentWindow;
+      this.document = iframe.contentDocument || this.window.document;
+      this.iframe = iframe;
+    } else {
+      this.document = this.$inputor[0].ownerDocument;
+      this.window = this.document.defaultView || this.document.parentWindow;
+      try {
+        this.iframe = this.window.frameElement;
+      } catch (error1) {
+        error = error1;
+        this.iframe = null;
+        if ($.fn.atwho.debug) {
+          throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error);
+        }
+      }
+    }
+    return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document);
+  };
+
+  App.prototype.controller = function(at) {
+    var c, current, currentFlag, ref;
+    if (this.aliasMaps[at]) {
+      current = this.controllers[this.aliasMaps[at]];
+    } else {
+      ref = this.controllers;
+      for (currentFlag in ref) {
+        c = ref[currentFlag];
+        if (currentFlag === at) {
+          current = c;
+          break;
+        }
+      }
+    }
+    if (current) {
+      return current;
+    } else {
+      return this.controllers[this.currentFlag];
+    }
+  };
+
+  App.prototype.setContextFor = function(at) {
+    this.currentFlag = at;
+    return this;
+  };
+
+  App.prototype.reg = function(flag, setting) {
+    var base, controller;
+    controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag));
+    if (setting.alias) {
+      this.aliasMaps[setting.alias] = flag;
+    }
+    controller.init(setting);
+    return this;
+  };
+
+  App.prototype.listen = function() {
+    return this.$inputor.on('compositionstart', (function(_this) {
+      return function(e) {
+        var ref;
+        if ((ref = _this.controller()) != null) {
+          ref.view.hide();
+        }
+        _this.isComposing = true;
+        return null;
+      };
+    })(this)).on('compositionend', (function(_this) {
+      return function(e) {
+        _this.isComposing = false;
+        setTimeout(function(e) {
+          return _this.dispatch(e);
+        });
+        return null;
+      };
+    })(this)).on('keyup.atwhoInner', (function(_this) {
+      return function(e) {
+        return _this.onKeyup(e);
+      };
+    })(this)).on('keydown.atwhoInner', (function(_this) {
+      return function(e) {
+        return _this.onKeydown(e);
+      };
+    })(this)).on('blur.atwhoInner', (function(_this) {
+      return function(e) {
+        var c;
+        if (c = _this.controller()) {
+          c.expectedQueryCBId = null;
+          return c.view.hide(e, c.getOpt("displayTimeout"));
+        }
+      };
+    })(this)).on('click.atwhoInner', (function(_this) {
+      return function(e) {
+        return _this.dispatch(e);
+      };
+    })(this)).on('scroll.atwhoInner', (function(_this) {
+      return function() {
+        var lastScrollTop;
+        lastScrollTop = _this.$inputor.scrollTop();
+        return function(e) {
+          var currentScrollTop, ref;
+          currentScrollTop = e.target.scrollTop;
+          if (lastScrollTop !== currentScrollTop) {
+            if ((ref = _this.controller()) != null) {
+              ref.view.hide(e);
+            }
+          }
+          lastScrollTop = currentScrollTop;
+          return true;
+        };
+      };
+    })(this)());
+  };
+
+  App.prototype.shutdown = function() {
+    var _, c, ref;
+    ref = this.controllers;
+    for (_ in ref) {
+      c = ref[_];
+      c.destroy();
+      delete this.controllers[_];
+    }
+    this.$inputor.off('.atwhoInner');
+    return this.$el.remove();
+  };
+
+  App.prototype.dispatch = function(e) {
+    var _, c, ref, results;
+    ref = this.controllers;
+    results = [];
+    for (_ in ref) {
+      c = ref[_];
+      results.push(c.lookUp(e));
+    }
+    return results;
+  };
+
+  App.prototype.onKeyup = function(e) {
+    var ref;
+    switch (e.keyCode) {
+      case KEY_CODE.ESC:
+        e.preventDefault();
+        if ((ref = this.controller()) != null) {
+          ref.view.hide();
+        }
+        break;
+      case KEY_CODE.DOWN:
+      case KEY_CODE.UP:
+      case KEY_CODE.CTRL:
+      case KEY_CODE.ENTER:
+        $.noop();
+        break;
+      case KEY_CODE.P:
+      case KEY_CODE.N:
+        if (!e.ctrlKey) {
+          this.dispatch(e);
+        }
+        break;
+      default:
+        this.dispatch(e);
+    }
+  };
+
+  App.prototype.onKeydown = function(e) {
+    var ref, view;
+    view = (ref = this.controller()) != null ? ref.view : void 0;
+    if (!(view && view.visible())) {
+      return;
+    }
+    switch (e.keyCode) {
+      case KEY_CODE.ESC:
+        e.preventDefault();
+        view.hide(e);
+        break;
+      case KEY_CODE.UP:
+        e.preventDefault();
+        view.prev();
+        break;
+      case KEY_CODE.DOWN:
+        e.preventDefault();
+        view.next();
+        break;
+      case KEY_CODE.P:
+        if (!e.ctrlKey) {
+          return;
+        }
+        e.preventDefault();
+        view.prev();
+        break;
+      case KEY_CODE.N:
+        if (!e.ctrlKey) {
+          return;
+        }
+        e.preventDefault();
+        view.next();
+        break;
+      case KEY_CODE.TAB:
+      case KEY_CODE.ENTER:
+      case KEY_CODE.SPACE:
+        if (!view.visible()) {
+          return;
+        }
+        if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) {
+          return;
+        }
+        if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) {
+          return;
+        }
+        if (view.highlighted()) {
+          e.preventDefault();
+          view.choose(e);
+        } else {
+          view.hide(e);
+        }
+        break;
+      default:
+        $.noop();
+    }
+  };
+
+  return App;
+
+})();
+
+var Controller,
+  slice = [].slice;
+
+Controller = (function() {
+  Controller.prototype.uid = function() {
+    return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
+  };
+
+  function Controller(app, at1) {
+    this.app = app;
+    this.at = at1;
+    this.$inputor = this.app.$inputor;
+    this.id = this.$inputor[0].id || this.uid();
+    this.expectedQueryCBId = null;
+    this.setting = null;
+    this.query = null;
+    this.pos = 0;
+    this.range = null;
+    if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) {
+      this.app.$el.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
+    }
+    this.model = new Model(this);
+    this.view = new View(this);
+  }
+
+  Controller.prototype.init = function(setting) {
+    this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
+    this.view.init();
+    return this.model.reload(this.setting.data);
+  };
+
+  Controller.prototype.destroy = function() {
+    this.trigger('beforeDestroy');
+    this.model.destroy();
+    this.view.destroy();
+    return this.$el.remove();
+  };
+
+  Controller.prototype.callDefault = function() {
+    var args, error, error1, funcName;
+    funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+    try {
+      return DEFAULT_CALLBACKS[funcName].apply(this, args);
+    } catch (error1) {
+      error = error1;
+      return $.error(error + " Or maybe At.js doesn't have function " + funcName);
+    }
+  };
+
+  Controller.prototype.trigger = function(name, data) {
+    var alias, eventName;
+    if (data == null) {
+      data = [];
+    }
+    data.push(this);
+    alias = this.getOpt('alias');
+    eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho";
+    return this.$inputor.trigger(eventName, data);
+  };
+
+  Controller.prototype.callbacks = function(funcName) {
+    return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName];
+  };
+
+  Controller.prototype.getOpt = function(at, default_value) {
+    var e, error1;
+    try {
+      return this.setting[at];
+    } catch (error1) {
+      e = error1;
+      return null;
+    }
+  };
+
+  Controller.prototype.insertContentFor = function($li) {
+    var data, tpl;
+    tpl = this.getOpt('insertTpl');
+    data = $.extend({}, $li.data('item-data'), {
+      'atwho-at': this.at
+    });
+    return this.callbacks("tplEval").call(this, tpl, data, "onInsert");
+  };
+
+  Controller.prototype.renderView = function(data) {
+    var searchKey;
+    searchKey = this.getOpt("searchKey");
+    data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey);
+    return this.view.render(data.slice(0, this.getOpt('limit')));
+  };
+
+  Controller.arrayToDefaultHash = function(data) {
+    var i, item, len, results;
+    if (!$.isArray(data)) {
+      return data;
+    }
+    results = [];
+    for (i = 0, len = data.length; i < len; i++) {
+      item = data[i];
+      if ($.isPlainObject(item)) {
+        results.push(item);
+      } else {
+        results.push({
+          name: item
+        });
+      }
+    }
+    return results;
+  };
+
+  Controller.prototype.lookUp = function(e) {
+    var query, wait;
+    if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) {
+      return;
+    }
+    if (this.getOpt('suspendOnComposing') && this.app.isComposing) {
+      return;
+    }
+    query = this.catchQuery(e);
+    if (!query) {
+      this.expectedQueryCBId = null;
+      return query;
+    }
+    this.app.setContextFor(this.at);
+    if (wait = this.getOpt('delay')) {
+      this._delayLookUp(query, wait);
+    } else {
+      this._lookUp(query);
+    }
+    return query;
+  };
+
+  Controller.prototype._delayLookUp = function(query, wait) {
+    var now, remaining;
+    now = Date.now ? Date.now() : new Date().getTime();
+    this.previousCallTime || (this.previousCallTime = now);
+    remaining = wait - (now - this.previousCallTime);
+    if ((0 < remaining && remaining < wait)) {
+      this.previousCallTime = now;
+      this._stopDelayedCall();
+      return this.delayedCallTimeout = setTimeout((function(_this) {
+        return function() {
+          _this.previousCallTime = 0;
+          _this.delayedCallTimeout = null;
+          return _this._lookUp(query);
+        };
+      })(this), wait);
+    } else {
+      this._stopDelayedCall();
+      if (this.previousCallTime !== now) {
+        this.previousCallTime = 0;
+      }
+      return this._lookUp(query);
+    }
+  };
+
+  Controller.prototype._stopDelayedCall = function() {
+    if (this.delayedCallTimeout) {
+      clearTimeout(this.delayedCallTimeout);
+      return this.delayedCallTimeout = null;
+    }
+  };
+
+  Controller.prototype._generateQueryCBId = function() {
+    return {};
+  };
+
+  Controller.prototype._lookUp = function(query) {
+    var _callback;
+    _callback = function(queryCBId, data) {
+      if (queryCBId !== this.expectedQueryCBId) {
+        return;
+      }
+      if (data && data.length > 0) {
+        return this.renderView(this.constructor.arrayToDefaultHash(data));
+      } else {
+        return this.view.hide();
+      }
+    };
+    this.expectedQueryCBId = this._generateQueryCBId();
+    return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId));
+  };
+
+  return Controller;
+
+})();
+
+var TextareaController,
+  extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+  hasProp = {}.hasOwnProperty;
+
+TextareaController = (function(superClass) {
+  extend(TextareaController, superClass);
+
+  function TextareaController() {
+    return TextareaController.__super__.constructor.apply(this, arguments);
+  }
+
+  TextareaController.prototype.catchQuery = function() {
+    var caretPos, content, end, isString, query, start, subtext;
+    content = this.$inputor.val();
+    caretPos = this.$inputor.caret('pos', {
+      iframe: this.app.iframe
+    });
+    subtext = content.slice(0, caretPos);
+    query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+    isString = typeof query === 'string';
+    if (isString && query.length < this.getOpt('minLen', 0)) {
+      return;
+    }
+    if (isString && query.length <= this.getOpt('maxLen', 20)) {
+      start = caretPos - query.length;
+      end = start + query.length;
+      this.pos = start;
+      query = {
+        'text': query,
+        'headPos': start,
+        'endPos': end
+      };
+      this.trigger("matched", [this.at, query.text]);
+    } else {
+      query = null;
+      this.view.hide();
+    }
+    return this.query = query;
+  };
+
+  TextareaController.prototype.rect = function() {
+    var c, iframeOffset, scaleBottom;
+    if (!(c = this.$inputor.caret('offset', this.pos - 1, {
+      iframe: this.app.iframe
+    }))) {
+      return;
+    }
+    if (this.app.iframe && !this.app.iframeAsRoot) {
+      iframeOffset = $(this.app.iframe).offset();
+      c.left += iframeOffset.left;
+      c.top += iframeOffset.top;
+    }
+    scaleBottom = this.app.document.selection ? 0 : 2;
+    return {
+      left: c.left,
+      top: c.top,
+      bottom: c.top + c.height + scaleBottom
+    };
+  };
+
+  TextareaController.prototype.insert = function(content, $li) {
+    var $inputor, source, startStr, suffix, text;
+    $inputor = this.$inputor;
+    source = $inputor.val();
+    startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0));
+    suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " ";
+    content += suffix;
+    text = "" + startStr + content + (source.slice(this.query['endPos'] || 0));
+    $inputor.val(text);
+    $inputor.caret('pos', startStr.length + content.length, {
+      iframe: this.app.iframe
+    });
+    if (!$inputor.is(':focus')) {
+      $inputor.focus();
+    }
+    return $inputor.change();
+  };
+
+  return TextareaController;
+
+})(Controller);
+
+var EditableController,
+  extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+  hasProp = {}.hasOwnProperty;
+
+EditableController = (function(superClass) {
+  extend(EditableController, superClass);
+
+  function EditableController() {
+    return EditableController.__super__.constructor.apply(this, arguments);
+  }
+
+  EditableController.prototype._getRange = function() {
+    var sel;
+    sel = this.app.window.getSelection();
+    if (sel.rangeCount > 0) {
+      return sel.getRangeAt(0);
+    }
+  };
+
+  EditableController.prototype._setRange = function(position, node, range) {
+    if (range == null) {
+      range = this._getRange();
+    }
+    if (!(range && node)) {
+      return;
+    }
+    node = $(node)[0];
+    if (position === 'after') {
+      range.setEndAfter(node);
+      range.setStartAfter(node);
+    } else {
+      range.setEndBefore(node);
+      range.setStartBefore(node);
+    }
+    range.collapse(false);
+    return this._clearRange(range);
+  };
+
+  EditableController.prototype._clearRange = function(range) {
+    var sel;
+    if (range == null) {
+      range = this._getRange();
+    }
+    sel = this.app.window.getSelection();
+    if (this.ctrl_a_pressed == null) {
+      sel.removeAllRanges();
+      return sel.addRange(range);
+    }
+  };
+
+  EditableController.prototype._movingEvent = function(e) {
+    var ref;
+    return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN);
+  };
+
+  EditableController.prototype._unwrap = function(node) {
+    var next;
+    node = $(node).unwrap().get(0);
+    if ((next = node.nextSibling) && next.nodeValue) {
+      node.nodeValue += next.nodeValue;
+      $(next).remove();
+    }
+    return node;
+  };
+
+  EditableController.prototype.catchQuery = function(e) {
+    var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range;
+    if (!(range = this._getRange())) {
+      return;
+    }
+    if (!range.collapsed) {
+      return;
+    }
+    if (e.which === KEY_CODE.ENTER) {
+      ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap();
+      if ($query.is(':empty')) {
+        $query.remove();
+      }
+      ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap();
+      this._clearRange();
+      return;
+    }
+    if (/firefox/i.test(navigator.userAgent)) {
+      if ($(range.startContainer).is(this.$inputor)) {
+        this._clearRange();
+        return;
+      }
+      if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) {
+        _range = range.cloneRange();
+        _range.setStart(range.startContainer, offset);
+        if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) {
+          inserted = $(range.startContainer).contents().get(offset);
+          this._setRange('after', $(inserted).contents().last());
+        }
+      } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) {
+        $inserted = $(range.startContainer.previousSibling);
+        if ($inserted.is('.atwho-inserted') && range.startOffset === 0) {
+          this._setRange('after', $inserted.contents().last());
+        }
+      }
+    }
+    $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query');
+    if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) {
+      $query.remove();
+    }
+    if (!this._movingEvent(e)) {
+      $query.removeClass('atwho-inserted');
+    }
+    if ($query.length > 0) {
+      switch (e.which) {
+        case KEY_CODE.LEFT:
+          this._setRange('before', $query.get(0), range);
+          $query.removeClass('atwho-query');
+          return;
+        case KEY_CODE.RIGHT:
+          this._setRange('after', $query.get(0).nextSibling, range);
+          $query.removeClass('atwho-query');
+          return;
+      }
+    }
+    if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) {
+      $query.empty().html(query_content).attr('data-atwho-at-query', null);
+      this._setRange('after', $query.get(0), range);
+    }
+    _range = range.cloneRange();
+    _range.setStart(range.startContainer, 0);
+    matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+    isString = typeof matched === 'string';
+    if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) {
+      range.setStart(range.startContainer, index);
+      $query = $('<span/>', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query');
+      range.surroundContents($query.get(0));
+      lastNode = $query.contents().last().get(0);
+      if (lastNode) {
+        if (/firefox/i.test(navigator.userAgent)) {
+          range.setStart(lastNode, lastNode.length);
+          range.setEnd(lastNode, lastNode.length);
+          this._clearRange(range);
+        } else {
+          this._setRange('after', lastNode, range);
+        }
+      }
+    }
+    if (isString && matched.length < this.getOpt('minLen', 0)) {
+      return;
+    }
+    if (isString && matched.length <= this.getOpt('maxLen', 20)) {
+      query = {
+        text: matched,
+        el: $query
+      };
+      this.trigger("matched", [this.at, query.text]);
+      return this.query = query;
+    } else {
+      this.view.hide();
+      this.query = {
+        el: $query
+      };
+      if ($query.text().indexOf(this.at) >= 0) {
+        if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) {
+          $query.removeClass('atwho-query');
+        } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) {
+          this._setRange("after", this._unwrap($query.text($query.text()).contents().first()));
+        }
+      }
+      return null;
+    }
+  };
+
+  EditableController.prototype.rect = function() {
+    var $iframe, iframeOffset, rect;
+    rect = this.query.el.offset();
+    if (!(rect && this.query.el[0].getClientRects().length)) {
+      return;
+    }
+    if (this.app.iframe && !this.app.iframeAsRoot) {
+      iframeOffset = ($iframe = $(this.app.iframe)).offset();
+      rect.left += iframeOffset.left - this.$inputor.scrollLeft();
+      rect.top += iframeOffset.top - this.$inputor.scrollTop();
+    }
+    rect.bottom = rect.top + this.query.el.height();
+    return rect;
+  };
+
+  EditableController.prototype.insert = function(content, $li) {
+    var data, overrides, range, suffix, suffixNode;
+    if (!this.$inputor.is(':focus')) {
+      this.$inputor.focus();
+    }
+    overrides = this.getOpt('functionOverrides');
+    if (overrides.insert) {
+      return overrides.insert.call(this, content, $li);
+    }
+    suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0";
+    data = $li.data('item-data');
+    this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text).attr('contenteditable', "false");
+    if (range = this._getRange()) {
+      if (this.query.el.length) {
+        range.setEndAfter(this.query.el[0]);
+      }
+      range.collapse(false);
+      range.insertNode(suffixNode = this.app.document.createTextNode("" + suffix));
+      this._setRange('after', suffixNode, range);
+    }
+    if (!this.$inputor.is(':focus')) {
+      this.$inputor.focus();
+    }
+    return this.$inputor.change();
+  };
+
+  return EditableController;
+
+})(Controller);
+
+var Model;
+
+Model = (function() {
+  function Model(context) {
+    this.context = context;
+    this.at = this.context.at;
+    this.storage = this.context.$inputor;
+  }
+
+  Model.prototype.destroy = function() {
+    return this.storage.data(this.at, null);
+  };
+
+  Model.prototype.saved = function() {
+    return this.fetch() > 0;
+  };
+
+  Model.prototype.query = function(query, callback) {
+    var _remoteFilter, data, searchKey;
+    data = this.fetch();
+    searchKey = this.context.getOpt("searchKey");
+    data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || [];
+    _remoteFilter = this.context.callbacks('remoteFilter');
+    if (data.length > 0 || (!_remoteFilter && data.length === 0)) {
+      return callback(data);
+    } else {
+      return _remoteFilter.call(this.context, query, callback);
+    }
+  };
+
+  Model.prototype.fetch = function() {
+    return this.storage.data(this.at) || [];
+  };
+
+  Model.prototype.save = function(data) {
+    return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || []));
+  };
+
+  Model.prototype.load = function(data) {
+    if (!(this.saved() || !data)) {
+      return this._load(data);
+    }
+  };
+
+  Model.prototype.reload = function(data) {
+    return this._load(data);
+  };
+
+  Model.prototype._load = function(data) {
+    if (typeof data === "string") {
+      return $.ajax(data, {
+        dataType: "json"
+      }).done((function(_this) {
+        return function(data) {
+          return _this.save(data);
+        };
+      })(this));
+    } else {
+      return this.save(data);
+    }
+  };
+
+  return Model;
+
+})();
+
+var View;
+
+View = (function() {
+  function View(context) {
+    this.context = context;
+    this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
+    this.$elUl = this.$el.children();
+    this.timeoutID = null;
+    this.context.$el.append(this.$el);
+    this.bindEvent();
+  }
+
+  View.prototype.init = function() {
+    var header_tpl, id;
+    id = this.context.getOpt("alias") || this.context.at.charCodeAt(0);
+    header_tpl = this.context.getOpt("headerTpl");
+    if (header_tpl && this.$el.children().length === 1) {
+      this.$el.prepend(header_tpl);
+    }
+    return this.$el.attr({
+      'id': "at-view-" + id
+    });
+  };
+
+  View.prototype.destroy = function() {
+    return this.$el.remove();
+  };
+
+  View.prototype.bindEvent = function() {
+    var $menu, lastCoordX, lastCoordY;
+    $menu = this.$el.find('ul');
+    lastCoordX = 0;
+    lastCoordY = 0;
+    return $menu.on('mousemove.atwho-view', 'li', (function(_this) {
+      return function(e) {
+        var $cur;
+        if (lastCoordX === e.clientX && lastCoordY === e.clientY) {
+          return;
+        }
+        lastCoordX = e.clientX;
+        lastCoordY = e.clientY;
+        $cur = $(e.currentTarget);
+        if ($cur.hasClass('cur')) {
+          return;
+        }
+        $menu.find('.cur').removeClass('cur');
+        return $cur.addClass('cur');
+      };
+    })(this)).on('click.atwho-view', 'li', (function(_this) {
+      return function(e) {
+        $menu.find('.cur').removeClass('cur');
+        $(e.currentTarget).addClass('cur');
+        _this.choose(e);
+        return e.preventDefault();
+      };
+    })(this));
+  };
+
+  View.prototype.visible = function() {
+    return $.expr.filters.visible(this.$el[0]);
+  };
+
+  View.prototype.highlighted = function() {
+    return this.$el.find(".cur").length > 0;
+  };
+
+  View.prototype.choose = function(e) {
+    var $li, content;
+    if (($li = this.$el.find(".cur")).length) {
+      content = this.context.insertContentFor($li);
+      this.context._stopDelayedCall();
+      this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li);
+      this.context.trigger("inserted", [$li, e]);
+      this.hide(e);
+    }
+    if (this.context.getOpt("hideWithoutSuffix")) {
+      return this.stopShowing = true;
+    }
+  };
+
+  View.prototype.reposition = function(rect) {
+    var _window, offset, overflowOffset, ref;
+    _window = this.context.app.iframeAsRoot ? this.context.app.window : window;
+    if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) {
+      rect.bottom = rect.top - this.$el.height();
+    }
+    if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) {
+      rect.left = overflowOffset;
+    }
+    offset = {
+      left: rect.left,
+      top: rect.bottom
+    };
+    if ((ref = this.context.callbacks("beforeReposition")) != null) {
+      ref.call(this.context, offset);
+    }
+    this.$el.offset(offset);
+    return this.context.trigger("reposition", [offset]);
+  };
+
+  View.prototype.next = function() {
+    var cur, next, nextEl, offset;
+    cur = this.$el.find('.cur').removeClass('cur');
+    next = cur.next();
+    if (!next.length) {
+      next = this.$el.find('li:first');
+    }
+    next.addClass('cur');
+    nextEl = next[0];
+    offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0);
+    return this.scrollTop(Math.max(0, offset - this.$el.height()));
+  };
+
+  View.prototype.prev = function() {
+    var cur, offset, prev, prevEl;
+    cur = this.$el.find('.cur').removeClass('cur');
+    prev = cur.prev();
+    if (!prev.length) {
+      prev = this.$el.find('li:last');
+    }
+    prev.addClass('cur');
+    prevEl = prev[0];
+    offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0);
+    return this.scrollTop(Math.max(0, offset - this.$el.height()));
+  };
+
+  View.prototype.scrollTop = function(scrollTop) {
+    var scrollDuration;
+    scrollDuration = this.context.getOpt('scrollDuration');
+    if (scrollDuration) {
+      return this.$elUl.animate({
+        scrollTop: scrollTop
+      }, scrollDuration);
+    } else {
+      return this.$elUl.scrollTop(scrollTop);
+    }
+  };
+
+  View.prototype.show = function() {
+    var rect;
+    if (this.stopShowing) {
+      this.stopShowing = false;
+      return;
+    }
+    if (!this.visible()) {
+      this.$el.show();
+      this.$el.scrollTop(0);
+      this.context.trigger('shown');
+    }
+    if (rect = this.context.rect()) {
+      return this.reposition(rect);
+    }
+  };
+
+  View.prototype.hide = function(e, time) {
+    var callback;
+    if (!this.visible()) {
+      return;
+    }
+    if (isNaN(time)) {
+      this.$el.hide();
+      return this.context.trigger('hidden', [e]);
+    } else {
+      callback = (function(_this) {
+        return function() {
+          return _this.hide();
+        };
+      })(this);
+      clearTimeout(this.timeoutID);
+      return this.timeoutID = setTimeout(callback, time);
+    }
+  };
+
+  View.prototype.render = function(list) {
+    var $li, $ul, i, item, len, li, tpl;
+    if (!($.isArray(list) && list.length > 0)) {
+      this.hide();
+      return;
+    }
+    this.$el.find('ul').empty();
+    $ul = this.$el.find('ul');
+    tpl = this.context.getOpt('displayTpl');
+    for (i = 0, len = list.length; i < len; i++) {
+      item = list[i];
+      item = $.extend({}, item, {
+        'atwho-at': this.context.at
+      });
+      li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay");
+      $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
+      $li.data("item-data", item);
+      $ul.append($li);
+    }
+    this.show();
+    if (this.context.getOpt('highlightFirst')) {
+      return $ul.find("li:first").addClass("cur");
+    }
+  };
+
+  return View;
+
+})();
+
+var Api;
+
+Api = {
+  load: function(at, data) {
+    var c;
+    if (c = this.controller(at)) {
+      return c.model.load(data);
+    }
+  },
+  isSelecting: function() {
+    var ref;
+    return !!((ref = this.controller()) != null ? ref.view.visible() : void 0);
+  },
+  hide: function() {
+    var ref;
+    return (ref = this.controller()) != null ? ref.view.hide() : void 0;
+  },
+  reposition: function() {
+    var c;
+    if (c = this.controller()) {
+      return c.view.reposition(c.rect());
+    }
+  },
+  setIframe: function(iframe, asRoot) {
+    this.setupRootElement(iframe, asRoot);
+    return null;
+  },
+  run: function() {
+    return this.dispatch();
+  },
+  destroy: function() {
+    this.shutdown();
+    return this.$inputor.data('atwho', null);
+  }
+};
+
+$.fn.atwho = function(method) {
+  var _args, result;
+  _args = arguments;
+  result = null;
+  this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() {
+    var $this, app;
+    if (!(app = ($this = $(this)).data("atwho"))) {
+      $this.data('atwho', (app = new App(this)));
+    }
+    if (typeof method === 'object' || !method) {
+      return app.reg(method.at, method);
+    } else if (Api[method] && app) {
+      return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
+    } else {
+      return $.error("Method " + method + " does not exist on jQuery.atwho");
+    }
+  });
+  if (result != null) {
+    return result;
+  } else {
+    return this;
+  }
+};
+
+$.fn.atwho["default"] = {
+  at: void 0,
+  alias: void 0,
+  data: null,
+  displayTpl: "<li>${name}</li>",
+  insertTpl: "${atwho-at}${name}",
+  headerTpl: null,
+  callbacks: DEFAULT_CALLBACKS,
+  functionOverrides: {},
+  searchKey: "name",
+  suffix: void 0,
+  hideWithoutSuffix: false,
+  startWithSpace: true,
+  acceptSpaceBar: false,
+  highlightFirst: true,
+  limit: 5,
+  maxLen: 20,
+  minLen: 0,
+  displayTimeout: 300,
+  delay: null,
+  spaceSelectsMatch: false,
+  tabSelectsMatch: true,
+  editableAtwhoQueryAttrs: {},
+  scrollDuration: 150,
+  suspendOnComposing: true,
+  lookUpOnClick: true
+};
+
+$.fn.atwho.debug = false;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.atwho.txt
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.atwho.txt	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.atwho.txt	(revision 12265)
@@ -0,0 +1,22 @@
+Copyright (c) 2013 chord.luo@gmail.com
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.caret.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.caret.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.caret.js	(revision 12265)
@@ -0,0 +1,436 @@
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(["jquery"], function ($) {
+      return (root.returnExportsGlobal = factory($));
+    });
+  } else if (typeof exports === 'object') {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like enviroments that support module.exports,
+    // like Node.
+    module.exports = factory(require("jquery"));
+  } else {
+    factory(jQuery);
+  }
+}(this, function ($) {
+
+/*
+  Implement Github like autocomplete mentions
+  http://ichord.github.com/At.js
+
+  Copyright (c) 2013 chord.luo@gmail.com
+  Licensed under the MIT license.
+*/
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置，我设置
+插入符的位置.
+*/
+
+"use strict";
+var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
+
+pluginName = 'caret';
+
+EditableCaret = (function() {
+  function EditableCaret($inputor) {
+    this.$inputor = $inputor;
+    this.domInputor = this.$inputor[0];
+  }
+
+  EditableCaret.prototype.setPos = function(pos) {
+    var fn, found, offset, sel;
+    if (sel = oWindow.getSelection()) {
+      offset = 0;
+      found = false;
+      (fn = function(pos, parent) {
+        var node, range, _i, _len, _ref, _results;
+        _ref = parent.childNodes;
+        _results = [];
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          node = _ref[_i];
+          if (found) {
+            break;
+          }
+          if (node.nodeType === 3) {
+            if (offset + node.length >= pos) {
+              found = true;
+              range = oDocument.createRange();
+              range.setStart(node, pos - offset);
+              sel.removeAllRanges();
+              sel.addRange(range);
+              break;
+            } else {
+              _results.push(offset += node.length);
+            }
+          } else {
+            _results.push(fn(pos, node));
+          }
+        }
+        return _results;
+      })(pos, this.domInputor);
+    }
+    return this.domInputor;
+  };
+
+  EditableCaret.prototype.getIEPosition = function() {
+    return this.getPosition();
+  };
+
+  EditableCaret.prototype.getPosition = function() {
+    var inputor_offset, offset;
+    offset = this.getOffset();
+    inputor_offset = this.$inputor.offset();
+    offset.left -= inputor_offset.left;
+    offset.top -= inputor_offset.top;
+    return offset;
+  };
+
+  EditableCaret.prototype.getOldIEPos = function() {
+    var preCaretTextRange, textRange;
+    textRange = oDocument.selection.createRange();
+    preCaretTextRange = oDocument.body.createTextRange();
+    preCaretTextRange.moveToElementText(this.domInputor);
+    preCaretTextRange.setEndPoint("EndToEnd", textRange);
+    return preCaretTextRange.text.length;
+  };
+
+  EditableCaret.prototype.getPos = function() {
+    var clonedRange, pos, range;
+    if (range = this.range()) {
+      clonedRange = range.cloneRange();
+      clonedRange.selectNodeContents(this.domInputor);
+      clonedRange.setEnd(range.endContainer, range.endOffset);
+      pos = clonedRange.toString().length;
+      clonedRange.detach();
+      return pos;
+    } else if (oDocument.selection) {
+      return this.getOldIEPos();
+    }
+  };
+
+  EditableCaret.prototype.getOldIEOffset = function() {
+    var range, rect;
+    range = oDocument.selection.createRange().duplicate();
+    range.moveStart("character", -1);
+    rect = range.getBoundingClientRect();
+    return {
+      height: rect.bottom - rect.top,
+      left: rect.left,
+      top: rect.top
+    };
+  };
+
+  EditableCaret.prototype.getOffset = function(pos) {
+    var clonedRange, offset, range, rect, shadowCaret;
+    if (oWindow.getSelection && (range = this.range())) {
+      if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) {
+        clonedRange = range.cloneRange();
+        clonedRange.setStart(range.endContainer, range.endOffset - 1);
+        clonedRange.setEnd(range.endContainer, range.endOffset);
+        rect = clonedRange.getBoundingClientRect();
+        offset = {
+          height: rect.height,
+          left: rect.left + rect.width,
+          top: rect.top
+        };
+        clonedRange.detach();
+      }
+      if (!offset || (offset != null ? offset.height : void 0) === 0) {
+        clonedRange = range.cloneRange();
+        shadowCaret = $(oDocument.createTextNode("|"));
+        clonedRange.insertNode(shadowCaret[0]);
+        clonedRange.selectNode(shadowCaret[0]);
+        rect = clonedRange.getBoundingClientRect();
+        offset = {
+          height: rect.height,
+          left: rect.left,
+          top: rect.top
+        };
+        shadowCaret.remove();
+        clonedRange.detach();
+      }
+    } else if (oDocument.selection) {
+      offset = this.getOldIEOffset();
+    }
+    if (offset) {
+      offset.top += $(oWindow).scrollTop();
+      offset.left += $(oWindow).scrollLeft();
+    }
+    return offset;
+  };
+
+  EditableCaret.prototype.range = function() {
+    var sel;
+    if (!oWindow.getSelection) {
+      return;
+    }
+    sel = oWindow.getSelection();
+    if (sel.rangeCount > 0) {
+      return sel.getRangeAt(0);
+    } else {
+      return null;
+    }
+  };
+
+  return EditableCaret;
+
+})();
+
+InputCaret = (function() {
+  function InputCaret($inputor) {
+    this.$inputor = $inputor;
+    this.domInputor = this.$inputor[0];
+  }
+
+  InputCaret.prototype.getIEPos = function() {
+    var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
+    inputor = this.domInputor;
+    range = oDocument.selection.createRange();
+    pos = 0;
+    if (range && range.parentElement() === inputor) {
+      normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+      len = normalizedValue.length;
+      textInputRange = inputor.createTextRange();
+      textInputRange.moveToBookmark(range.getBookmark());
+      endRange = inputor.createTextRange();
+      endRange.collapse(false);
+      if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+        pos = len;
+      } else {
+        pos = -textInputRange.moveStart("character", -len);
+      }
+    }
+    return pos;
+  };
+
+  InputCaret.prototype.getPos = function() {
+    if (oDocument.selection) {
+      return this.getIEPos();
+    } else {
+      return this.domInputor.selectionStart;
+    }
+  };
+
+  InputCaret.prototype.setPos = function(pos) {
+    var inputor, range;
+    inputor = this.domInputor;
+    if (oDocument.selection) {
+      range = inputor.createTextRange();
+      range.move("character", pos);
+      range.select();
+    } else if (inputor.setSelectionRange) {
+      inputor.setSelectionRange(pos, pos);
+    }
+    return inputor;
+  };
+
+  InputCaret.prototype.getIEOffset = function(pos) {
+    var h, textRange, x, y;
+    textRange = this.domInputor.createTextRange();
+    pos || (pos = this.getPos());
+    textRange.move('character', pos);
+    x = textRange.boundingLeft;
+    y = textRange.boundingTop;
+    h = textRange.boundingHeight;
+    return {
+      left: x,
+      top: y,
+      height: h
+    };
+  };
+
+  InputCaret.prototype.getOffset = function(pos) {
+    var $inputor, offset, position;
+    $inputor = this.$inputor;
+    if (oDocument.selection) {
+      offset = this.getIEOffset(pos);
+      offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
+      offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
+      return offset;
+    } else {
+      offset = $inputor.offset();
+      position = this.getPosition(pos);
+      return offset = {
+        left: offset.left + position.left - $inputor.scrollLeft(),
+        top: offset.top + position.top - $inputor.scrollTop(),
+        height: position.height
+      };
+    }
+  };
+
+  InputCaret.prototype.getPosition = function(pos) {
+    var $inputor, at_rect, end_range, format, html, mirror, start_range;
+    $inputor = this.$inputor;
+    format = function(value) {
+      value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "<br/>");
+      if (/firefox/i.test(navigator.userAgent)) {
+        value = value.replace(/\s/g, '&nbsp;');
+      }
+      return value;
+    };
+    if (pos === void 0) {
+      pos = this.getPos();
+    }
+    start_range = $inputor.val().slice(0, pos);
+    end_range = $inputor.val().slice(pos);
+    html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
+    html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
+    html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
+    mirror = new Mirror($inputor);
+    return at_rect = mirror.create(html).rect();
+  };
+
+  InputCaret.prototype.getIEPosition = function(pos) {
+    var h, inputorOffset, offset, x, y;
+    offset = this.getIEOffset(pos);
+    inputorOffset = this.$inputor.offset();
+    x = offset.left - inputorOffset.left;
+    y = offset.top - inputorOffset.top;
+    h = offset.height;
+    return {
+      left: x,
+      top: y,
+      height: h
+    };
+  };
+
+  return InputCaret;
+
+})();
+
+Mirror = (function() {
+  Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"];
+
+  function Mirror($inputor) {
+    this.$inputor = $inputor;
+  }
+
+  Mirror.prototype.mirrorCss = function() {
+    var css,
+      _this = this;
+    css = {
+      position: 'absolute',
+      left: -9999,
+      top: 0,
+      zIndex: -20000
+    };
+    if (this.$inputor.prop('tagName') === 'TEXTAREA') {
+      this.css_attr.push('width');
+    }
+    $.each(this.css_attr, function(i, p) {
+      return css[p] = _this.$inputor.css(p);
+    });
+    return css;
+  };
+
+  Mirror.prototype.create = function(html) {
+    this.$mirror = $('<div></div>');
+    this.$mirror.css(this.mirrorCss());
+    this.$mirror.html(html);
+    this.$inputor.after(this.$mirror);
+    return this;
+  };
+
+  Mirror.prototype.rect = function() {
+    var $flag, pos, rect;
+    $flag = this.$mirror.find("#caret");
+    pos = $flag.position();
+    rect = {
+      left: pos.left,
+      top: pos.top,
+      height: $flag.height()
+    };
+    this.$mirror.remove();
+    return rect;
+  };
+
+  return Mirror;
+
+})();
+
+Utils = {
+  contentEditable: function($inputor) {
+    return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
+  }
+};
+
+methods = {
+  pos: function(pos) {
+    if (pos || pos === 0) {
+      return this.setPos(pos);
+    } else {
+      return this.getPos();
+    }
+  },
+  position: function(pos) {
+    if (oDocument.selection) {
+      return this.getIEPosition(pos);
+    } else {
+      return this.getPosition(pos);
+    }
+  },
+  offset: function(pos) {
+    var offset;
+    offset = this.getOffset(pos);
+    return offset;
+  }
+};
+
+oDocument = null;
+
+oWindow = null;
+
+oFrame = null;
+
+setContextBy = function(settings) {
+  var iframe;
+  if (iframe = settings != null ? settings.iframe : void 0) {
+    oFrame = iframe;
+    oWindow = iframe.contentWindow;
+    return oDocument = iframe.contentDocument || oWindow.document;
+  } else {
+    oFrame = void 0;
+    oWindow = window;
+    return oDocument = document;
+  }
+};
+
+discoveryIframeOf = function($dom) {
+  var error;
+  oDocument = $dom[0].ownerDocument;
+  oWindow = oDocument.defaultView || oDocument.parentWindow;
+  try {
+    return oFrame = oWindow.frameElement;
+  } catch (_error) {
+    error = _error;
+  }
+};
+
+$.fn.caret = function(method, value, settings) {
+  var caret;
+  if (methods[method]) {
+    if ($.isPlainObject(value)) {
+      setContextBy(value);
+      value = void 0;
+    } else {
+      setContextBy(settings);
+    }
+    caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
+    return methods[method].apply(caret, [value]);
+  } else {
+    return $.error("Method " + method + " does not exist on jQuery.caret");
+  }
+};
+
+$.fn.caret.EditableCaret = EditableCaret;
+
+$.fn.caret.InputCaret = InputCaret;
+
+$.fn.caret.Utils = Utils;
+
+$.fn.caret.apis = methods;
+
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.caret.txt
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.caret.txt	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/jquery.caret.txt	(revision 12265)
@@ -0,0 +1,22 @@
+Copyright (c) 2013 chord.luo@gmail.com
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/livestamp.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/livestamp.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/livestamp.js	(revision 12265)
@@ -0,0 +1,129 @@
+// Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License
+(function($, moment) {
+  var updateInterval = 1e3,
+      paused = false,
+      $livestamps = $([]),
+
+  init = function() {
+    livestampGlobal.resume();
+  },
+
+  prep = function($el, timestamp) {
+    var oldData = $el.data('livestampdata');
+    if (typeof timestamp == 'number')
+      timestamp *= 1e3;
+
+    $el.removeAttr('data-livestamp')
+      .removeData('livestamp');
+
+    timestamp = moment(timestamp);
+    if (moment.isMoment(timestamp) && !isNaN(+timestamp)) {
+      var newData = $.extend({ }, { 'original': $el.contents() }, oldData);
+      newData.moment = moment(timestamp);
+
+      $el.data('livestampdata', newData).empty();
+      $livestamps.push($el[0]);
+    }
+  },
+
+  run = function() {
+    if (paused) return;
+    livestampGlobal.update();
+    setTimeout(run, updateInterval);
+  },
+
+  livestampGlobal = {
+    update: function() {
+      $('[data-livestamp]').each(function() {
+        var $this = $(this);
+        prep($this, $this.data('livestamp'));
+      });
+
+      var toRemove = [];
+      $livestamps.each(function() {
+        var $this = $(this),
+            data = $this.data('livestampdata');
+
+        if (data === undefined)
+          toRemove.push(this);
+        else if (moment.isMoment(data.moment)) {
+          var from = $this.html(),
+              to = data.moment.fromNow();
+
+          if (from != to) {
+            var e = $.Event('change.livestamp');
+            $this.trigger(e, [from, to]);
+            if (!e.isDefaultPrevented())
+              $this.html(to);
+          }
+        }
+      });
+
+      $livestamps = $livestamps.not(toRemove);
+    },
+
+    pause: function() {
+      paused = true;
+    },
+
+    resume: function() {
+      paused = false;
+      run();
+    },
+
+    interval: function(interval) {
+      if (interval === undefined)
+        return updateInterval;
+      updateInterval = interval;
+    }
+  },
+
+  livestampLocal = {
+    add: function($el, timestamp) {
+      if (typeof timestamp == 'number')
+        timestamp *= 1e3;
+      timestamp = moment(timestamp);
+
+      if (moment.isMoment(timestamp) && !isNaN(+timestamp)) {
+        $el.each(function() {
+          prep($(this), timestamp);
+        });
+        livestampGlobal.update();
+      }
+
+      return $el;
+    },
+
+    destroy: function($el) {
+      $livestamps = $livestamps.not($el);
+      $el.each(function() {
+        var $this = $(this),
+            data = $this.data('livestampdata');
+
+        if (data === undefined)
+          return $el;
+
+        $this
+          .html(data.original ? data.original : '')
+          .removeData('livestampdata');
+      });
+
+      return $el;
+    },
+
+    isLivestamp: function($el) {
+      return $el.data('livestampdata') !== undefined;
+    }
+  };
+
+  $.livestamp = livestampGlobal;
+  $(init);
+  $.fn.livestamp = function(method, options) {
+    if (!livestampLocal[method]) {
+      options = method;
+      method = 'add';
+    }
+
+    return livestampLocal[method](this, options);
+  };
+})(jQuery, moment);
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/af.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/af.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/af.js	(revision 12265)
@@ -0,0 +1,73 @@
+//! moment.js locale configuration
+//! locale : Afrikaans [af]
+//! author : Werner Mollentze : https://github.com/wernerm
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var af = moment.defineLocale('af', {
+        months : 'Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember'.split('_'),
+        monthsShort : 'Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des'.split('_'),
+        weekdays : 'Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag'.split('_'),
+        weekdaysShort : 'Son_Maa_Din_Woe_Don_Vry_Sat'.split('_'),
+        weekdaysMin : 'So_Ma_Di_Wo_Do_Vr_Sa'.split('_'),
+        meridiemParse: /vm|nm/i,
+        isPM : function (input) {
+            return /^nm$/i.test(input);
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 12) {
+                return isLower ? 'vm' : 'VM';
+            } else {
+                return isLower ? 'nm' : 'NM';
+            }
+        },
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Vandag om] LT',
+            nextDay : '[Môre om] LT',
+            nextWeek : 'dddd [om] LT',
+            lastDay : '[Gister om] LT',
+            lastWeek : '[Laas] dddd [om] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'oor %s',
+            past : '%s gelede',
+            s : '\'n paar sekondes',
+            m : '\'n minuut',
+            mm : '%d minute',
+            h : '\'n uur',
+            hh : '%d ure',
+            d : '\'n dag',
+            dd : '%d dae',
+            M : '\'n maand',
+            MM : '%d maande',
+            y : '\'n jaar',
+            yy : '%d jaar'
+        },
+        ordinalParse: /\d{1,2}(ste|de)/,
+        ordinal : function (number) {
+            return number + ((number === 1 || number === 8 || number >= 20) ? 'ste' : 'de'); // Thanks to Joris Röling : https://github.com/jjupiter
+        },
+        week : {
+            dow : 1, // Maandag is die eerste dag van die week.
+            doy : 4  // Die week wat die 4de Januarie bevat is die eerste week van die jaar.
+        }
+    });
+
+    return af;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-ly.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-ly.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-ly.js	(revision 12265)
@@ -0,0 +1,122 @@
+//! moment.js locale configuration
+//! locale : Arabic (Lybia) [ar-ly]
+//! author : Ali Hmer: https://github.com/kikoanis
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '1',
+        '2': '2',
+        '3': '3',
+        '4': '4',
+        '5': '5',
+        '6': '6',
+        '7': '7',
+        '8': '8',
+        '9': '9',
+        '0': '0'
+    }, pluralForm = function (n) {
+        return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5;
+    }, plurals = {
+        s : ['أقل من ثانية', 'ثانية واحدة', ['ثانيتان', 'ثانيتين'], '%d ثوان', '%d ثانية', '%d ثانية'],
+        m : ['أقل من دقيقة', 'دقيقة واحدة', ['دقيقتان', 'دقيقتين'], '%d دقائق', '%d دقيقة', '%d دقيقة'],
+        h : ['أقل من ساعة', 'ساعة واحدة', ['ساعتان', 'ساعتين'], '%d ساعات', '%d ساعة', '%d ساعة'],
+        d : ['أقل من يوم', 'يوم واحد', ['يومان', 'يومين'], '%d أيام', '%d يومًا', '%d يوم'],
+        M : ['أقل من شهر', 'شهر واحد', ['شهران', 'شهرين'], '%d أشهر', '%d شهرا', '%d شهر'],
+        y : ['أقل من عام', 'عام واحد', ['عامان', 'عامين'], '%d أعوام', '%d عامًا', '%d عام']
+    }, pluralize = function (u) {
+        return function (number, withoutSuffix, string, isFuture) {
+            var f = pluralForm(number),
+                str = plurals[u][pluralForm(number)];
+            if (f === 2) {
+                str = str[withoutSuffix ? 0 : 1];
+            }
+            return str.replace(/%d/i, number);
+        };
+    }, months = [
+        'يناير',
+        'فبراير',
+        'مارس',
+        'أبريل',
+        'مايو',
+        'يونيو',
+        'يوليو',
+        'أغسطس',
+        'سبتمبر',
+        'أكتوبر',
+        'نوفمبر',
+        'ديسمبر'
+    ];
+
+    var ar_ly = moment.defineLocale('ar-ly', {
+        months : months,
+        monthsShort : months,
+        weekdays : 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'),
+        weekdaysShort : 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'),
+        weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'D/\u200FM/\u200FYYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        meridiemParse: /ص|م/,
+        isPM : function (input) {
+            return 'م' === input;
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'ص';
+            } else {
+                return 'م';
+            }
+        },
+        calendar : {
+            sameDay: '[اليوم عند الساعة] LT',
+            nextDay: '[غدًا عند الساعة] LT',
+            nextWeek: 'dddd [عند الساعة] LT',
+            lastDay: '[أمس عند الساعة] LT',
+            lastWeek: 'dddd [عند الساعة] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'بعد %s',
+            past : 'منذ %s',
+            s : pluralize('s'),
+            m : pluralize('m'),
+            mm : pluralize('m'),
+            h : pluralize('h'),
+            hh : pluralize('h'),
+            d : pluralize('d'),
+            dd : pluralize('d'),
+            M : pluralize('M'),
+            MM : pluralize('M'),
+            y : pluralize('y'),
+            yy : pluralize('y')
+        },
+        preparse: function (string) {
+            return string.replace(/\u200f/g, '').replace(/،/g, ',');
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            }).replace(/,/g, '،');
+        },
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ar_ly;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-ma.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-ma.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-ma.js	(revision 12265)
@@ -0,0 +1,60 @@
+//! moment.js locale configuration
+//! locale : Arabic (Morocco) [ar-ma]
+//! author : ElFadili Yassine : https://github.com/ElFadiliY
+//! author : Abdel Said : https://github.com/abdelsaid
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ar_ma = moment.defineLocale('ar-ma', {
+        months : 'يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر'.split('_'),
+        monthsShort : 'يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر'.split('_'),
+        weekdays : 'الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'),
+        weekdaysShort : 'احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت'.split('_'),
+        weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[اليوم على الساعة] LT',
+            nextDay: '[غدا على الساعة] LT',
+            nextWeek: 'dddd [على الساعة] LT',
+            lastDay: '[أمس على الساعة] LT',
+            lastWeek: 'dddd [على الساعة] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'في %s',
+            past : 'منذ %s',
+            s : 'ثوان',
+            m : 'دقيقة',
+            mm : '%d دقائق',
+            h : 'ساعة',
+            hh : '%d ساعات',
+            d : 'يوم',
+            dd : '%d أيام',
+            M : 'شهر',
+            MM : '%d أشهر',
+            y : 'سنة',
+            yy : '%d سنوات'
+        },
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ar_ma;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-sa.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-sa.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-sa.js	(revision 12265)
@@ -0,0 +1,104 @@
+//! moment.js locale configuration
+//! locale : Arabic (Saudi Arabia) [ar-sa]
+//! author : Suhail Alkowaileet : https://github.com/xsoh
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '١',
+        '2': '٢',
+        '3': '٣',
+        '4': '٤',
+        '5': '٥',
+        '6': '٦',
+        '7': '٧',
+        '8': '٨',
+        '9': '٩',
+        '0': '٠'
+    }, numberMap = {
+        '١': '1',
+        '٢': '2',
+        '٣': '3',
+        '٤': '4',
+        '٥': '5',
+        '٦': '6',
+        '٧': '7',
+        '٨': '8',
+        '٩': '9',
+        '٠': '0'
+    };
+
+    var ar_sa = moment.defineLocale('ar-sa', {
+        months : 'يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'),
+        monthsShort : 'يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'),
+        weekdays : 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'),
+        weekdaysShort : 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'),
+        weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        meridiemParse: /ص|م/,
+        isPM : function (input) {
+            return 'م' === input;
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'ص';
+            } else {
+                return 'م';
+            }
+        },
+        calendar : {
+            sameDay: '[اليوم على الساعة] LT',
+            nextDay: '[غدا على الساعة] LT',
+            nextWeek: 'dddd [على الساعة] LT',
+            lastDay: '[أمس على الساعة] LT',
+            lastWeek: 'dddd [على الساعة] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'في %s',
+            past : 'منذ %s',
+            s : 'ثوان',
+            m : 'دقيقة',
+            mm : '%d دقائق',
+            h : 'ساعة',
+            hh : '%d ساعات',
+            d : 'يوم',
+            dd : '%d أيام',
+            M : 'شهر',
+            MM : '%d أشهر',
+            y : 'سنة',
+            yy : '%d سنوات'
+        },
+        preparse: function (string) {
+            return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) {
+                return numberMap[match];
+            }).replace(/،/g, ',');
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            }).replace(/,/g, '،');
+        },
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ar_sa;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-tn.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-tn.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar-tn.js	(revision 12265)
@@ -0,0 +1,59 @@
+//! moment.js locale configuration
+//! locale  :  Arabic (Tunisia) [ar-tn]
+//! author : Nader Toukabri : https://github.com/naderio
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ar_tn = moment.defineLocale('ar-tn', {
+        months: 'جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'),
+        monthsShort: 'جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'),
+        weekdays: 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'),
+        weekdaysShort: 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'),
+        weekdaysMin: 'ح_ن_ث_ر_خ_ج_س'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat: {
+            LT: 'HH:mm',
+            LTS: 'HH:mm:ss',
+            L: 'DD/MM/YYYY',
+            LL: 'D MMMM YYYY',
+            LLL: 'D MMMM YYYY HH:mm',
+            LLLL: 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar: {
+            sameDay: '[اليوم على الساعة] LT',
+            nextDay: '[غدا على الساعة] LT',
+            nextWeek: 'dddd [على الساعة] LT',
+            lastDay: '[أمس على الساعة] LT',
+            lastWeek: 'dddd [على الساعة] LT',
+            sameElse: 'L'
+        },
+        relativeTime: {
+            future: 'في %s',
+            past: 'منذ %s',
+            s: 'ثوان',
+            m: 'دقيقة',
+            mm: '%d دقائق',
+            h: 'ساعة',
+            hh: '%d ساعات',
+            d: 'يوم',
+            dd: '%d أيام',
+            M: 'شهر',
+            MM: '%d أشهر',
+            y: 'سنة',
+            yy: '%d سنوات'
+        },
+        week: {
+            dow: 1, // Monday is the first day of the week.
+            doy: 4 // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return ar_tn;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ar.js	(revision 12265)
@@ -0,0 +1,137 @@
+//! moment.js locale configuration
+//! locale : Arabic [ar]
+//! author : Abdel Said: https://github.com/abdelsaid
+//! author : Ahmed Elkhatib
+//! author : forabi https://github.com/forabi
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '١',
+        '2': '٢',
+        '3': '٣',
+        '4': '٤',
+        '5': '٥',
+        '6': '٦',
+        '7': '٧',
+        '8': '٨',
+        '9': '٩',
+        '0': '٠'
+    }, numberMap = {
+        '١': '1',
+        '٢': '2',
+        '٣': '3',
+        '٤': '4',
+        '٥': '5',
+        '٦': '6',
+        '٧': '7',
+        '٨': '8',
+        '٩': '9',
+        '٠': '0'
+    }, pluralForm = function (n) {
+        return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5;
+    }, plurals = {
+        s : ['أقل من ثانية', 'ثانية واحدة', ['ثانيتان', 'ثانيتين'], '%d ثوان', '%d ثانية', '%d ثانية'],
+        m : ['أقل من دقيقة', 'دقيقة واحدة', ['دقيقتان', 'دقيقتين'], '%d دقائق', '%d دقيقة', '%d دقيقة'],
+        h : ['أقل من ساعة', 'ساعة واحدة', ['ساعتان', 'ساعتين'], '%d ساعات', '%d ساعة', '%d ساعة'],
+        d : ['أقل من يوم', 'يوم واحد', ['يومان', 'يومين'], '%d أيام', '%d يومًا', '%d يوم'],
+        M : ['أقل من شهر', 'شهر واحد', ['شهران', 'شهرين'], '%d أشهر', '%d شهرا', '%d شهر'],
+        y : ['أقل من عام', 'عام واحد', ['عامان', 'عامين'], '%d أعوام', '%d عامًا', '%d عام']
+    }, pluralize = function (u) {
+        return function (number, withoutSuffix, string, isFuture) {
+            var f = pluralForm(number),
+                str = plurals[u][pluralForm(number)];
+            if (f === 2) {
+                str = str[withoutSuffix ? 0 : 1];
+            }
+            return str.replace(/%d/i, number);
+        };
+    }, months = [
+        'كانون الثاني يناير',
+        'شباط فبراير',
+        'آذار مارس',
+        'نيسان أبريل',
+        'أيار مايو',
+        'حزيران يونيو',
+        'تموز يوليو',
+        'آب أغسطس',
+        'أيلول سبتمبر',
+        'تشرين الأول أكتوبر',
+        'تشرين الثاني نوفمبر',
+        'كانون الأول ديسمبر'
+    ];
+
+    var ar = moment.defineLocale('ar', {
+        months : months,
+        monthsShort : months,
+        weekdays : 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'),
+        weekdaysShort : 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'),
+        weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'D/\u200FM/\u200FYYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        meridiemParse: /ص|م/,
+        isPM : function (input) {
+            return 'م' === input;
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'ص';
+            } else {
+                return 'م';
+            }
+        },
+        calendar : {
+            sameDay: '[اليوم عند الساعة] LT',
+            nextDay: '[غدًا عند الساعة] LT',
+            nextWeek: 'dddd [عند الساعة] LT',
+            lastDay: '[أمس عند الساعة] LT',
+            lastWeek: 'dddd [عند الساعة] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'بعد %s',
+            past : 'منذ %s',
+            s : pluralize('s'),
+            m : pluralize('m'),
+            mm : pluralize('m'),
+            h : pluralize('h'),
+            hh : pluralize('h'),
+            d : pluralize('d'),
+            dd : pluralize('d'),
+            M : pluralize('M'),
+            MM : pluralize('M'),
+            y : pluralize('y'),
+            yy : pluralize('y')
+        },
+        preparse: function (string) {
+            return string.replace(/\u200f/g, '').replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) {
+                return numberMap[match];
+            }).replace(/،/g, ',');
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            }).replace(/,/g, '،');
+        },
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ar;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/az.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/az.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/az.js	(revision 12265)
@@ -0,0 +1,105 @@
+//! moment.js locale configuration
+//! locale : Azerbaijani [az]
+//! author : topchiyev : https://github.com/topchiyev
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var suffixes = {
+        1: '-inci',
+        5: '-inci',
+        8: '-inci',
+        70: '-inci',
+        80: '-inci',
+        2: '-nci',
+        7: '-nci',
+        20: '-nci',
+        50: '-nci',
+        3: '-üncü',
+        4: '-üncü',
+        100: '-üncü',
+        6: '-ncı',
+        9: '-uncu',
+        10: '-uncu',
+        30: '-uncu',
+        60: '-ıncı',
+        90: '-ıncı'
+    };
+
+    var az = moment.defineLocale('az', {
+        months : 'yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr'.split('_'),
+        monthsShort : 'yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek'.split('_'),
+        weekdays : 'Bazar_Bazar ertəsi_Çərşənbə axşamı_Çərşənbə_Cümə axşamı_Cümə_Şənbə'.split('_'),
+        weekdaysShort : 'Baz_BzE_ÇAx_Çər_CAx_Cüm_Şən'.split('_'),
+        weekdaysMin : 'Bz_BE_ÇA_Çə_CA_Cü_Şə'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[bugün saat] LT',
+            nextDay : '[sabah saat] LT',
+            nextWeek : '[gələn həftə] dddd [saat] LT',
+            lastDay : '[dünən] LT',
+            lastWeek : '[keçən həftə] dddd [saat] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s sonra',
+            past : '%s əvvəl',
+            s : 'birneçə saniyyə',
+            m : 'bir dəqiqə',
+            mm : '%d dəqiqə',
+            h : 'bir saat',
+            hh : '%d saat',
+            d : 'bir gün',
+            dd : '%d gün',
+            M : 'bir ay',
+            MM : '%d ay',
+            y : 'bir il',
+            yy : '%d il'
+        },
+        meridiemParse: /gecə|səhər|gündüz|axşam/,
+        isPM : function (input) {
+            return /^(gündüz|axşam)$/.test(input);
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'gecə';
+            } else if (hour < 12) {
+                return 'səhər';
+            } else if (hour < 17) {
+                return 'gündüz';
+            } else {
+                return 'axşam';
+            }
+        },
+        ordinalParse: /\d{1,2}-(ıncı|inci|nci|üncü|ncı|uncu)/,
+        ordinal : function (number) {
+            if (number === 0) {  // special case for zero
+                return number + '-ıncı';
+            }
+            var a = number % 10,
+                b = number % 100 - a,
+                c = number >= 100 ? 100 : null;
+            return number + (suffixes[a] || suffixes[b] || suffixes[c]);
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return az;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/be.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/be.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/be.js	(revision 12265)
@@ -0,0 +1,134 @@
+//! moment.js locale configuration
+//! locale : Belarusian [be]
+//! author : Dmitry Demidov : https://github.com/demidov91
+//! author: Praleska: http://praleska.pro/
+//! Author : Menelion Elensúle : https://github.com/Oire
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function plural(word, num) {
+        var forms = word.split('_');
+        return num % 10 === 1 && num % 100 !== 11 ? forms[0] : (num % 10 >= 2 && num % 10 <= 4 && (num % 100 < 10 || num % 100 >= 20) ? forms[1] : forms[2]);
+    }
+    function relativeTimeWithPlural(number, withoutSuffix, key) {
+        var format = {
+            'mm': withoutSuffix ? 'хвіліна_хвіліны_хвілін' : 'хвіліну_хвіліны_хвілін',
+            'hh': withoutSuffix ? 'гадзіна_гадзіны_гадзін' : 'гадзіну_гадзіны_гадзін',
+            'dd': 'дзень_дні_дзён',
+            'MM': 'месяц_месяцы_месяцаў',
+            'yy': 'год_гады_гадоў'
+        };
+        if (key === 'm') {
+            return withoutSuffix ? 'хвіліна' : 'хвіліну';
+        }
+        else if (key === 'h') {
+            return withoutSuffix ? 'гадзіна' : 'гадзіну';
+        }
+        else {
+            return number + ' ' + plural(format[key], +number);
+        }
+    }
+
+    var be = moment.defineLocale('be', {
+        months : {
+            format: 'студзеня_лютага_сакавіка_красавіка_траўня_чэрвеня_ліпеня_жніўня_верасня_кастрычніка_лістапада_снежня'.split('_'),
+            standalone: 'студзень_люты_сакавік_красавік_травень_чэрвень_ліпень_жнівень_верасень_кастрычнік_лістапад_снежань'.split('_')
+        },
+        monthsShort : 'студ_лют_сак_крас_трав_чэрв_ліп_жнів_вер_каст_ліст_снеж'.split('_'),
+        weekdays : {
+            format: 'нядзелю_панядзелак_аўторак_сераду_чацвер_пятніцу_суботу'.split('_'),
+            standalone: 'нядзеля_панядзелак_аўторак_серада_чацвер_пятніца_субота'.split('_'),
+            isFormat: /\[ ?[Вв] ?(?:мінулую|наступную)? ?\] ?dddd/
+        },
+        weekdaysShort : 'нд_пн_ат_ср_чц_пт_сб'.split('_'),
+        weekdaysMin : 'нд_пн_ат_ср_чц_пт_сб'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY г.',
+            LLL : 'D MMMM YYYY г., HH:mm',
+            LLLL : 'dddd, D MMMM YYYY г., HH:mm'
+        },
+        calendar : {
+            sameDay: '[Сёння ў] LT',
+            nextDay: '[Заўтра ў] LT',
+            lastDay: '[Учора ў] LT',
+            nextWeek: function () {
+                return '[У] dddd [ў] LT';
+            },
+            lastWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                    case 3:
+                    case 5:
+                    case 6:
+                        return '[У мінулую] dddd [ў] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                        return '[У мінулы] dddd [ў] LT';
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'праз %s',
+            past : '%s таму',
+            s : 'некалькі секунд',
+            m : relativeTimeWithPlural,
+            mm : relativeTimeWithPlural,
+            h : relativeTimeWithPlural,
+            hh : relativeTimeWithPlural,
+            d : 'дзень',
+            dd : relativeTimeWithPlural,
+            M : 'месяц',
+            MM : relativeTimeWithPlural,
+            y : 'год',
+            yy : relativeTimeWithPlural
+        },
+        meridiemParse: /ночы|раніцы|дня|вечара/,
+        isPM : function (input) {
+            return /^(дня|вечара)$/.test(input);
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'ночы';
+            } else if (hour < 12) {
+                return 'раніцы';
+            } else if (hour < 17) {
+                return 'дня';
+            } else {
+                return 'вечара';
+            }
+        },
+        ordinalParse: /\d{1,2}-(і|ы|га)/,
+        ordinal: function (number, period) {
+            switch (period) {
+                case 'M':
+                case 'd':
+                case 'DDD':
+                case 'w':
+                case 'W':
+                    return (number % 10 === 2 || number % 10 === 3) && (number % 100 !== 12 && number % 100 !== 13) ? number + '-і' : number + '-ы';
+                case 'D':
+                    return number + '-га';
+                default:
+                    return number;
+            }
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return be;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bg.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bg.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bg.js	(revision 12265)
@@ -0,0 +1,90 @@
+//! moment.js locale configuration
+//! locale : Bulgarian [bg]
+//! author : Krasen Borisov : https://github.com/kraz
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var bg = moment.defineLocale('bg', {
+        months : 'януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември'.split('_'),
+        monthsShort : 'янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек'.split('_'),
+        weekdays : 'неделя_понеделник_вторник_сряда_четвъртък_петък_събота'.split('_'),
+        weekdaysShort : 'нед_пон_вто_сря_чет_пет_съб'.split('_'),
+        weekdaysMin : 'нд_пн_вт_ср_чт_пт_сб'.split('_'),
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'D.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY H:mm',
+            LLLL : 'dddd, D MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay : '[Днес в] LT',
+            nextDay : '[Утре в] LT',
+            nextWeek : 'dddd [в] LT',
+            lastDay : '[Вчера в] LT',
+            lastWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                    case 3:
+                    case 6:
+                        return '[В изминалата] dddd [в] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[В изминалия] dddd [в] LT';
+                }
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'след %s',
+            past : 'преди %s',
+            s : 'няколко секунди',
+            m : 'минута',
+            mm : '%d минути',
+            h : 'час',
+            hh : '%d часа',
+            d : 'ден',
+            dd : '%d дни',
+            M : 'месец',
+            MM : '%d месеца',
+            y : 'година',
+            yy : '%d години'
+        },
+        ordinalParse: /\d{1,2}-(ев|ен|ти|ви|ри|ми)/,
+        ordinal : function (number) {
+            var lastDigit = number % 10,
+                last2Digits = number % 100;
+            if (number === 0) {
+                return number + '-ев';
+            } else if (last2Digits === 0) {
+                return number + '-ен';
+            } else if (last2Digits > 10 && last2Digits < 20) {
+                return number + '-ти';
+            } else if (lastDigit === 1) {
+                return number + '-ви';
+            } else if (lastDigit === 2) {
+                return number + '-ри';
+            } else if (lastDigit === 7 || lastDigit === 8) {
+                return number + '-ми';
+            } else {
+                return number + '-ти';
+            }
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return bg;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bn.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bn.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bn.js	(revision 12265)
@@ -0,0 +1,119 @@
+//! moment.js locale configuration
+//! locale : Bengali [bn]
+//! author : Kaushik Gandhi : https://github.com/kaushikgandhi
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '১',
+        '2': '২',
+        '3': '৩',
+        '4': '৪',
+        '5': '৫',
+        '6': '৬',
+        '7': '৭',
+        '8': '৮',
+        '9': '৯',
+        '0': '০'
+    },
+    numberMap = {
+        '১': '1',
+        '২': '2',
+        '৩': '3',
+        '৪': '4',
+        '৫': '5',
+        '৬': '6',
+        '৭': '7',
+        '৮': '8',
+        '৯': '9',
+        '০': '0'
+    };
+
+    var bn = moment.defineLocale('bn', {
+        months : 'জানুয়ারী_ফেব্রুয়ারি_মার্চ_এপ্রিল_মে_জুন_জুলাই_আগস্ট_সেপ্টেম্বর_অক্টোবর_নভেম্বর_ডিসেম্বর'.split('_'),
+        monthsShort : 'জানু_ফেব_মার্চ_এপ্র_মে_জুন_জুল_আগ_সেপ্ট_অক্টো_নভে_ডিসে'.split('_'),
+        weekdays : 'রবিবার_সোমবার_মঙ্গলবার_বুধবার_বৃহস্পতিবার_শুক্রবার_শনিবার'.split('_'),
+        weekdaysShort : 'রবি_সোম_মঙ্গল_বুধ_বৃহস্পতি_শুক্র_শনি'.split('_'),
+        weekdaysMin : 'রবি_সোম_মঙ্গ_বুধ_বৃহঃ_শুক্র_শনি'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm সময়',
+            LTS : 'A h:mm:ss সময়',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm সময়',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm সময়'
+        },
+        calendar : {
+            sameDay : '[আজ] LT',
+            nextDay : '[আগামীকাল] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[গতকাল] LT',
+            lastWeek : '[গত] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s পরে',
+            past : '%s আগে',
+            s : 'কয়েক সেকেন্ড',
+            m : 'এক মিনিট',
+            mm : '%d মিনিট',
+            h : 'এক ঘন্টা',
+            hh : '%d ঘন্টা',
+            d : 'এক দিন',
+            dd : '%d দিন',
+            M : 'এক মাস',
+            MM : '%d মাস',
+            y : 'এক বছর',
+            yy : '%d বছর'
+        },
+        preparse: function (string) {
+            return string.replace(/[১২৩৪৫৬৭৮৯০]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        meridiemParse: /রাত|সকাল|দুপুর|বিকাল|রাত/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if ((meridiem === 'রাত' && hour >= 4) ||
+                    (meridiem === 'দুপুর' && hour < 5) ||
+                    meridiem === 'বিকাল') {
+                return hour + 12;
+            } else {
+                return hour;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'রাত';
+            } else if (hour < 10) {
+                return 'সকাল';
+            } else if (hour < 17) {
+                return 'দুপুর';
+            } else if (hour < 20) {
+                return 'বিকাল';
+            } else {
+                return 'রাত';
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return bn;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bo.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bo.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bo.js	(revision 12265)
@@ -0,0 +1,119 @@
+//! moment.js locale configuration
+//! locale : Tibetan [bo]
+//! author : Thupten N. Chakrishar : https://github.com/vajradog
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '༡',
+        '2': '༢',
+        '3': '༣',
+        '4': '༤',
+        '5': '༥',
+        '6': '༦',
+        '7': '༧',
+        '8': '༨',
+        '9': '༩',
+        '0': '༠'
+    },
+    numberMap = {
+        '༡': '1',
+        '༢': '2',
+        '༣': '3',
+        '༤': '4',
+        '༥': '5',
+        '༦': '6',
+        '༧': '7',
+        '༨': '8',
+        '༩': '9',
+        '༠': '0'
+    };
+
+    var bo = moment.defineLocale('bo', {
+        months : 'ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ'.split('_'),
+        monthsShort : 'ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ'.split('_'),
+        weekdays : 'གཟའ་ཉི་མ་_གཟའ་ཟླ་བ་_གཟའ་མིག་དམར་_གཟའ་ལྷག་པ་_གཟའ་ཕུར་བུ_གཟའ་པ་སངས་_གཟའ་སྤེན་པ་'.split('_'),
+        weekdaysShort : 'ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་'.split('_'),
+        weekdaysMin : 'ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm',
+            LTS : 'A h:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm'
+        },
+        calendar : {
+            sameDay : '[དི་རིང] LT',
+            nextDay : '[སང་ཉིན] LT',
+            nextWeek : '[བདུན་ཕྲག་རྗེས་མ], LT',
+            lastDay : '[ཁ་སང] LT',
+            lastWeek : '[བདུན་ཕྲག་མཐའ་མ] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s ལ་',
+            past : '%s སྔན་ལ',
+            s : 'ལམ་སང',
+            m : 'སྐར་མ་གཅིག',
+            mm : '%d སྐར་མ',
+            h : 'ཆུ་ཚོད་གཅིག',
+            hh : '%d ཆུ་ཚོད',
+            d : 'ཉིན་གཅིག',
+            dd : '%d ཉིན་',
+            M : 'ཟླ་བ་གཅིག',
+            MM : '%d ཟླ་བ',
+            y : 'ལོ་གཅིག',
+            yy : '%d ལོ'
+        },
+        preparse: function (string) {
+            return string.replace(/[༡༢༣༤༥༦༧༨༩༠]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        meridiemParse: /མཚན་མོ|ཞོགས་ཀས|ཉིན་གུང|དགོང་དག|མཚན་མོ/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if ((meridiem === 'མཚན་མོ' && hour >= 4) ||
+                    (meridiem === 'ཉིན་གུང' && hour < 5) ||
+                    meridiem === 'དགོང་དག') {
+                return hour + 12;
+            } else {
+                return hour;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'མཚན་མོ';
+            } else if (hour < 10) {
+                return 'ཞོགས་ཀས';
+            } else if (hour < 17) {
+                return 'ཉིན་གུང';
+            } else if (hour < 20) {
+                return 'དགོང་དག';
+            } else {
+                return 'མཚན་མོ';
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return bo;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/br.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/br.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/br.js	(revision 12265)
@@ -0,0 +1,108 @@
+//! moment.js locale configuration
+//! locale : Breton [br]
+//! author : Jean-Baptiste Le Duigou : https://github.com/jbleduigou
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function relativeTimeWithMutation(number, withoutSuffix, key) {
+        var format = {
+            'mm': 'munutenn',
+            'MM': 'miz',
+            'dd': 'devezh'
+        };
+        return number + ' ' + mutation(format[key], number);
+    }
+    function specialMutationForYears(number) {
+        switch (lastNumber(number)) {
+            case 1:
+            case 3:
+            case 4:
+            case 5:
+            case 9:
+                return number + ' bloaz';
+            default:
+                return number + ' vloaz';
+        }
+    }
+    function lastNumber(number) {
+        if (number > 9) {
+            return lastNumber(number % 10);
+        }
+        return number;
+    }
+    function mutation(text, number) {
+        if (number === 2) {
+            return softMutation(text);
+        }
+        return text;
+    }
+    function softMutation(text) {
+        var mutationTable = {
+            'm': 'v',
+            'b': 'v',
+            'd': 'z'
+        };
+        if (mutationTable[text.charAt(0)] === undefined) {
+            return text;
+        }
+        return mutationTable[text.charAt(0)] + text.substring(1);
+    }
+
+    var br = moment.defineLocale('br', {
+        months : 'Genver_C\'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu'.split('_'),
+        monthsShort : 'Gen_C\'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker'.split('_'),
+        weekdays : 'Sul_Lun_Meurzh_Merc\'her_Yaou_Gwener_Sadorn'.split('_'),
+        weekdaysShort : 'Sul_Lun_Meu_Mer_Yao_Gwe_Sad'.split('_'),
+        weekdaysMin : 'Su_Lu_Me_Mer_Ya_Gw_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'h[e]mm A',
+            LTS : 'h[e]mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D [a viz] MMMM YYYY',
+            LLL : 'D [a viz] MMMM YYYY h[e]mm A',
+            LLLL : 'dddd, D [a viz] MMMM YYYY h[e]mm A'
+        },
+        calendar : {
+            sameDay : '[Hiziv da] LT',
+            nextDay : '[Warc\'hoazh da] LT',
+            nextWeek : 'dddd [da] LT',
+            lastDay : '[Dec\'h da] LT',
+            lastWeek : 'dddd [paset da] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'a-benn %s',
+            past : '%s \'zo',
+            s : 'un nebeud segondennoù',
+            m : 'ur vunutenn',
+            mm : relativeTimeWithMutation,
+            h : 'un eur',
+            hh : '%d eur',
+            d : 'un devezh',
+            dd : relativeTimeWithMutation,
+            M : 'ur miz',
+            MM : relativeTimeWithMutation,
+            y : 'ur bloaz',
+            yy : specialMutationForYears
+        },
+        ordinalParse: /\d{1,2}(añ|vet)/,
+        ordinal : function (number) {
+            var output = (number === 1) ? 'añ' : 'vet';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return br;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bs.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bs.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/bs.js	(revision 12265)
@@ -0,0 +1,143 @@
+//! moment.js locale configuration
+//! locale : Bosnian [bs]
+//! author : Nedim Cholich : https://github.com/frontyard
+//! based on (hr) translation by Bojan Marković
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function translate(number, withoutSuffix, key) {
+        var result = number + ' ';
+        switch (key) {
+            case 'm':
+                return withoutSuffix ? 'jedna minuta' : 'jedne minute';
+            case 'mm':
+                if (number === 1) {
+                    result += 'minuta';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'minute';
+                } else {
+                    result += 'minuta';
+                }
+                return result;
+            case 'h':
+                return withoutSuffix ? 'jedan sat' : 'jednog sata';
+            case 'hh':
+                if (number === 1) {
+                    result += 'sat';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'sata';
+                } else {
+                    result += 'sati';
+                }
+                return result;
+            case 'dd':
+                if (number === 1) {
+                    result += 'dan';
+                } else {
+                    result += 'dana';
+                }
+                return result;
+            case 'MM':
+                if (number === 1) {
+                    result += 'mjesec';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'mjeseca';
+                } else {
+                    result += 'mjeseci';
+                }
+                return result;
+            case 'yy':
+                if (number === 1) {
+                    result += 'godina';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'godine';
+                } else {
+                    result += 'godina';
+                }
+                return result;
+        }
+    }
+
+    var bs = moment.defineLocale('bs', {
+        months : 'januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar'.split('_'),
+        monthsShort : 'jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'),
+        weekdaysShort : 'ned._pon._uto._sri._čet._pet._sub.'.split('_'),
+        weekdaysMin : 'ne_po_ut_sr_če_pe_su'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY H:mm',
+            LLLL : 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay  : '[danas u] LT',
+            nextDay  : '[sutra u] LT',
+            nextWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[u] [nedjelju] [u] LT';
+                    case 3:
+                        return '[u] [srijedu] [u] LT';
+                    case 6:
+                        return '[u] [subotu] [u] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[u] dddd [u] LT';
+                }
+            },
+            lastDay  : '[jučer u] LT',
+            lastWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                    case 3:
+                        return '[prošlu] dddd [u] LT';
+                    case 6:
+                        return '[prošle] [subote] [u] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[prošli] dddd [u] LT';
+                }
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past   : 'prije %s',
+            s      : 'par sekundi',
+            m      : translate,
+            mm     : translate,
+            h      : translate,
+            hh     : translate,
+            d      : 'dan',
+            dd     : translate,
+            M      : 'mjesec',
+            MM     : translate,
+            y      : 'godinu',
+            yy     : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return bs;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ca.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ca.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ca.js	(revision 12265)
@@ -0,0 +1,81 @@
+//! moment.js locale configuration
+//! locale : Catalan [ca]
+//! author : Juan G. Hurtado : https://github.com/juanghurtado
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ca = moment.defineLocale('ca', {
+        months : 'gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre'.split('_'),
+        monthsShort : 'gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte'.split('_'),
+        weekdaysShort : 'dg._dl._dt._dc._dj._dv._ds.'.split('_'),
+        weekdaysMin : 'Dg_Dl_Dt_Dc_Dj_Dv_Ds'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY H:mm',
+            LLLL : 'dddd D MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay : function () {
+                return '[avui a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
+            },
+            nextDay : function () {
+                return '[demà a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
+            },
+            nextWeek : function () {
+                return 'dddd [a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
+            },
+            lastDay : function () {
+                return '[ahir a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
+            },
+            lastWeek : function () {
+                return '[el] dddd [passat a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'en %s',
+            past : 'fa %s',
+            s : 'uns segons',
+            m : 'un minut',
+            mm : '%d minuts',
+            h : 'una hora',
+            hh : '%d hores',
+            d : 'un dia',
+            dd : '%d dies',
+            M : 'un mes',
+            MM : '%d mesos',
+            y : 'un any',
+            yy : '%d anys'
+        },
+        ordinalParse: /\d{1,2}(r|n|t|è|a)/,
+        ordinal : function (number, period) {
+            var output = (number === 1) ? 'r' :
+                (number === 2) ? 'n' :
+                (number === 3) ? 'r' :
+                (number === 4) ? 't' : 'è';
+            if (period === 'w' || period === 'W') {
+                output = 'a';
+            }
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return ca;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cs.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cs.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cs.js	(revision 12265)
@@ -0,0 +1,172 @@
+//! moment.js locale configuration
+//! locale : Czech [cs]
+//! author : petrbela : https://github.com/petrbela
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var months = 'leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec'.split('_'),
+        monthsShort = 'led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro'.split('_');
+    function plural(n) {
+        return (n > 1) && (n < 5) && (~~(n / 10) !== 1);
+    }
+    function translate(number, withoutSuffix, key, isFuture) {
+        var result = number + ' ';
+        switch (key) {
+            case 's':  // a few seconds / in a few seconds / a few seconds ago
+                return (withoutSuffix || isFuture) ? 'pár sekund' : 'pár sekundami';
+            case 'm':  // a minute / in a minute / a minute ago
+                return withoutSuffix ? 'minuta' : (isFuture ? 'minutu' : 'minutou');
+            case 'mm': // 9 minutes / in 9 minutes / 9 minutes ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'minuty' : 'minut');
+                } else {
+                    return result + 'minutami';
+                }
+                break;
+            case 'h':  // an hour / in an hour / an hour ago
+                return withoutSuffix ? 'hodina' : (isFuture ? 'hodinu' : 'hodinou');
+            case 'hh': // 9 hours / in 9 hours / 9 hours ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'hodiny' : 'hodin');
+                } else {
+                    return result + 'hodinami';
+                }
+                break;
+            case 'd':  // a day / in a day / a day ago
+                return (withoutSuffix || isFuture) ? 'den' : 'dnem';
+            case 'dd': // 9 days / in 9 days / 9 days ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'dny' : 'dní');
+                } else {
+                    return result + 'dny';
+                }
+                break;
+            case 'M':  // a month / in a month / a month ago
+                return (withoutSuffix || isFuture) ? 'měsíc' : 'měsícem';
+            case 'MM': // 9 months / in 9 months / 9 months ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'měsíce' : 'měsíců');
+                } else {
+                    return result + 'měsíci';
+                }
+                break;
+            case 'y':  // a year / in a year / a year ago
+                return (withoutSuffix || isFuture) ? 'rok' : 'rokem';
+            case 'yy': // 9 years / in 9 years / 9 years ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'roky' : 'let');
+                } else {
+                    return result + 'lety';
+                }
+                break;
+        }
+    }
+
+    var cs = moment.defineLocale('cs', {
+        months : months,
+        monthsShort : monthsShort,
+        monthsParse : (function (months, monthsShort) {
+            var i, _monthsParse = [];
+            for (i = 0; i < 12; i++) {
+                // use custom parser to solve problem with July (červenec)
+                _monthsParse[i] = new RegExp('^' + months[i] + '$|^' + monthsShort[i] + '$', 'i');
+            }
+            return _monthsParse;
+        }(months, monthsShort)),
+        shortMonthsParse : (function (monthsShort) {
+            var i, _shortMonthsParse = [];
+            for (i = 0; i < 12; i++) {
+                _shortMonthsParse[i] = new RegExp('^' + monthsShort[i] + '$', 'i');
+            }
+            return _shortMonthsParse;
+        }(monthsShort)),
+        longMonthsParse : (function (months) {
+            var i, _longMonthsParse = [];
+            for (i = 0; i < 12; i++) {
+                _longMonthsParse[i] = new RegExp('^' + months[i] + '$', 'i');
+            }
+            return _longMonthsParse;
+        }(months)),
+        weekdays : 'neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota'.split('_'),
+        weekdaysShort : 'ne_po_út_st_čt_pá_so'.split('_'),
+        weekdaysMin : 'ne_po_út_st_čt_pá_so'.split('_'),
+        longDateFormat : {
+            LT: 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY H:mm',
+            LLLL : 'dddd D. MMMM YYYY H:mm',
+            l : 'D. M. YYYY'
+        },
+        calendar : {
+            sameDay: '[dnes v] LT',
+            nextDay: '[zítra v] LT',
+            nextWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[v neděli v] LT';
+                    case 1:
+                    case 2:
+                        return '[v] dddd [v] LT';
+                    case 3:
+                        return '[ve středu v] LT';
+                    case 4:
+                        return '[ve čtvrtek v] LT';
+                    case 5:
+                        return '[v pátek v] LT';
+                    case 6:
+                        return '[v sobotu v] LT';
+                }
+            },
+            lastDay: '[včera v] LT',
+            lastWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[minulou neděli v] LT';
+                    case 1:
+                    case 2:
+                        return '[minulé] dddd [v] LT';
+                    case 3:
+                        return '[minulou středu v] LT';
+                    case 4:
+                    case 5:
+                        return '[minulý] dddd [v] LT';
+                    case 6:
+                        return '[minulou sobotu v] LT';
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past : 'před %s',
+            s : translate,
+            m : translate,
+            mm : translate,
+            h : translate,
+            hh : translate,
+            d : translate,
+            dd : translate,
+            M : translate,
+            MM : translate,
+            y : translate,
+            yy : translate
+        },
+        ordinalParse : /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return cs;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cv.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cv.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cv.js	(revision 12265)
@@ -0,0 +1,63 @@
+//! moment.js locale configuration
+//! locale : Chuvash [cv]
+//! author : Anatoly Mironov : https://github.com/mirontoli
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var cv = moment.defineLocale('cv', {
+        months : 'кӑрлач_нарӑс_пуш_ака_май_ҫӗртме_утӑ_ҫурла_авӑн_юпа_чӳк_раштав'.split('_'),
+        monthsShort : 'кӑр_нар_пуш_ака_май_ҫӗр_утӑ_ҫур_авн_юпа_чӳк_раш'.split('_'),
+        weekdays : 'вырсарникун_тунтикун_ытларикун_юнкун_кӗҫнерникун_эрнекун_шӑматкун'.split('_'),
+        weekdaysShort : 'выр_тун_ытл_юн_кӗҫ_эрн_шӑм'.split('_'),
+        weekdaysMin : 'вр_тн_ыт_юн_кҫ_эр_шм'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD-MM-YYYY',
+            LL : 'YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ]',
+            LLL : 'YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ], HH:mm',
+            LLLL : 'dddd, YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ], HH:mm'
+        },
+        calendar : {
+            sameDay: '[Паян] LT [сехетре]',
+            nextDay: '[Ыран] LT [сехетре]',
+            lastDay: '[Ӗнер] LT [сехетре]',
+            nextWeek: '[Ҫитес] dddd LT [сехетре]',
+            lastWeek: '[Иртнӗ] dddd LT [сехетре]',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : function (output) {
+                var affix = /сехет$/i.exec(output) ? 'рен' : /ҫул$/i.exec(output) ? 'тан' : 'ран';
+                return output + affix;
+            },
+            past : '%s каялла',
+            s : 'пӗр-ик ҫеккунт',
+            m : 'пӗр минут',
+            mm : '%d минут',
+            h : 'пӗр сехет',
+            hh : '%d сехет',
+            d : 'пӗр кун',
+            dd : '%d кун',
+            M : 'пӗр уйӑх',
+            MM : '%d уйӑх',
+            y : 'пӗр ҫул',
+            yy : '%d ҫул'
+        },
+        ordinalParse: /\d{1,2}-мӗш/,
+        ordinal : '%d-мӗш',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return cv;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cy.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cy.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/cy.js	(revision 12265)
@@ -0,0 +1,81 @@
+//! moment.js locale configuration
+//! locale : Welsh [cy]
+//! author : Robert Allen : https://github.com/robgallen
+//! author : https://github.com/ryangreaves
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var cy = moment.defineLocale('cy', {
+        months: 'Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr'.split('_'),
+        monthsShort: 'Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag'.split('_'),
+        weekdays: 'Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn'.split('_'),
+        weekdaysShort: 'Sul_Llun_Maw_Mer_Iau_Gwe_Sad'.split('_'),
+        weekdaysMin: 'Su_Ll_Ma_Me_Ia_Gw_Sa'.split('_'),
+        weekdaysParseExact : true,
+        // time formats are the same as en-gb
+        longDateFormat: {
+            LT: 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L: 'DD/MM/YYYY',
+            LL: 'D MMMM YYYY',
+            LLL: 'D MMMM YYYY HH:mm',
+            LLLL: 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar: {
+            sameDay: '[Heddiw am] LT',
+            nextDay: '[Yfory am] LT',
+            nextWeek: 'dddd [am] LT',
+            lastDay: '[Ddoe am] LT',
+            lastWeek: 'dddd [diwethaf am] LT',
+            sameElse: 'L'
+        },
+        relativeTime: {
+            future: 'mewn %s',
+            past: '%s yn ôl',
+            s: 'ychydig eiliadau',
+            m: 'munud',
+            mm: '%d munud',
+            h: 'awr',
+            hh: '%d awr',
+            d: 'diwrnod',
+            dd: '%d diwrnod',
+            M: 'mis',
+            MM: '%d mis',
+            y: 'blwyddyn',
+            yy: '%d flynedd'
+        },
+        ordinalParse: /\d{1,2}(fed|ain|af|il|ydd|ed|eg)/,
+        // traditional ordinal numbers above 31 are not commonly used in colloquial Welsh
+        ordinal: function (number) {
+            var b = number,
+                output = '',
+                lookup = [
+                    '', 'af', 'il', 'ydd', 'ydd', 'ed', 'ed', 'ed', 'fed', 'fed', 'fed', // 1af to 10fed
+                    'eg', 'fed', 'eg', 'eg', 'fed', 'eg', 'eg', 'fed', 'eg', 'fed' // 11eg to 20fed
+                ];
+            if (b > 20) {
+                if (b === 40 || b === 50 || b === 60 || b === 80 || b === 100) {
+                    output = 'fed'; // not 30ain, 70ain or 90ain
+                } else {
+                    output = 'ain';
+                }
+            } else if (b > 0) {
+                output = lookup[b];
+            }
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return cy;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/da.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/da.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/da.js	(revision 12265)
@@ -0,0 +1,60 @@
+//! moment.js locale configuration
+//! locale : Danish [da]
+//! author : Ulrik Nielsen : https://github.com/mrbase
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var da = moment.defineLocale('da', {
+        months : 'januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec'.split('_'),
+        weekdays : 'søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag'.split('_'),
+        weekdaysShort : 'søn_man_tir_ons_tor_fre_lør'.split('_'),
+        weekdaysMin : 'sø_ma_ti_on_to_fr_lø'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY HH:mm',
+            LLLL : 'dddd [d.] D. MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[I dag kl.] LT',
+            nextDay : '[I morgen kl.] LT',
+            nextWeek : 'dddd [kl.] LT',
+            lastDay : '[I går kl.] LT',
+            lastWeek : '[sidste] dddd [kl] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'om %s',
+            past : '%s siden',
+            s : 'få sekunder',
+            m : 'et minut',
+            mm : '%d minutter',
+            h : 'en time',
+            hh : '%d timer',
+            d : 'en dag',
+            dd : '%d dage',
+            M : 'en måned',
+            MM : '%d måneder',
+            y : 'et år',
+            yy : '%d år'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return da;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/de-at.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/de-at.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/de-at.js	(revision 12265)
@@ -0,0 +1,79 @@
+//! moment.js locale configuration
+//! locale : German (Austria) [de-at]
+//! author : lluchs : https://github.com/lluchs
+//! author: Menelion Elensúle: https://github.com/Oire
+//! author : Martin Groller : https://github.com/MadMG
+//! author : Mikolaj Dadela : https://github.com/mik01aj
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function processRelativeTime(number, withoutSuffix, key, isFuture) {
+        var format = {
+            'm': ['eine Minute', 'einer Minute'],
+            'h': ['eine Stunde', 'einer Stunde'],
+            'd': ['ein Tag', 'einem Tag'],
+            'dd': [number + ' Tage', number + ' Tagen'],
+            'M': ['ein Monat', 'einem Monat'],
+            'MM': [number + ' Monate', number + ' Monaten'],
+            'y': ['ein Jahr', 'einem Jahr'],
+            'yy': [number + ' Jahre', number + ' Jahren']
+        };
+        return withoutSuffix ? format[key][0] : format[key][1];
+    }
+
+    var de_at = moment.defineLocale('de-at', {
+        months : 'Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'),
+        monthsShort : 'Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag'.split('_'),
+        weekdaysShort : 'So._Mo._Di._Mi._Do._Fr._Sa.'.split('_'),
+        weekdaysMin : 'So_Mo_Di_Mi_Do_Fr_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT: 'HH:mm',
+            LTS: 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY HH:mm',
+            LLLL : 'dddd, D. MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[heute um] LT [Uhr]',
+            sameElse: 'L',
+            nextDay: '[morgen um] LT [Uhr]',
+            nextWeek: 'dddd [um] LT [Uhr]',
+            lastDay: '[gestern um] LT [Uhr]',
+            lastWeek: '[letzten] dddd [um] LT [Uhr]'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : 'vor %s',
+            s : 'ein paar Sekunden',
+            m : processRelativeTime,
+            mm : '%d Minuten',
+            h : processRelativeTime,
+            hh : '%d Stunden',
+            d : processRelativeTime,
+            dd : processRelativeTime,
+            M : processRelativeTime,
+            MM : processRelativeTime,
+            y : processRelativeTime,
+            yy : processRelativeTime
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return de_at;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/de.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/de.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/de.js	(revision 12265)
@@ -0,0 +1,78 @@
+//! moment.js locale configuration
+//! locale : German [de]
+//! author : lluchs : https://github.com/lluchs
+//! author: Menelion Elensúle: https://github.com/Oire
+//! author : Mikolaj Dadela : https://github.com/mik01aj
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function processRelativeTime(number, withoutSuffix, key, isFuture) {
+        var format = {
+            'm': ['eine Minute', 'einer Minute'],
+            'h': ['eine Stunde', 'einer Stunde'],
+            'd': ['ein Tag', 'einem Tag'],
+            'dd': [number + ' Tage', number + ' Tagen'],
+            'M': ['ein Monat', 'einem Monat'],
+            'MM': [number + ' Monate', number + ' Monaten'],
+            'y': ['ein Jahr', 'einem Jahr'],
+            'yy': [number + ' Jahre', number + ' Jahren']
+        };
+        return withoutSuffix ? format[key][0] : format[key][1];
+    }
+
+    var de = moment.defineLocale('de', {
+        months : 'Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'),
+        monthsShort : 'Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag'.split('_'),
+        weekdaysShort : 'So._Mo._Di._Mi._Do._Fr._Sa.'.split('_'),
+        weekdaysMin : 'So_Mo_Di_Mi_Do_Fr_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT: 'HH:mm',
+            LTS: 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY HH:mm',
+            LLLL : 'dddd, D. MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[heute um] LT [Uhr]',
+            sameElse: 'L',
+            nextDay: '[morgen um] LT [Uhr]',
+            nextWeek: 'dddd [um] LT [Uhr]',
+            lastDay: '[gestern um] LT [Uhr]',
+            lastWeek: '[letzten] dddd [um] LT [Uhr]'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : 'vor %s',
+            s : 'ein paar Sekunden',
+            m : processRelativeTime,
+            mm : '%d Minuten',
+            h : processRelativeTime,
+            hh : '%d Stunden',
+            d : processRelativeTime,
+            dd : processRelativeTime,
+            M : processRelativeTime,
+            MM : processRelativeTime,
+            y : processRelativeTime,
+            yy : processRelativeTime
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return de;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/dv.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/dv.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/dv.js	(revision 12265)
@@ -0,0 +1,99 @@
+//! moment.js locale configuration
+//! locale : Maldivian [dv]
+//! author : Jawish Hameed : https://github.com/jawish
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var months = [
+        'ޖެނުއަރީ',
+        'ފެބްރުއަރީ',
+        'މާރިޗު',
+        'އޭޕްރީލު',
+        'މޭ',
+        'ޖޫން',
+        'ޖުލައި',
+        'އޯގަސްޓު',
+        'ސެޕްޓެމްބަރު',
+        'އޮކްޓޯބަރު',
+        'ނޮވެމްބަރު',
+        'ޑިސެމްބަރު'
+    ], weekdays = [
+        'އާދިއްތަ',
+        'ހޯމަ',
+        'އަންގާރަ',
+        'ބުދަ',
+        'ބުރާސްފަތި',
+        'ހުކުރު',
+        'ހޮނިހިރު'
+    ];
+
+    var dv = moment.defineLocale('dv', {
+        months : months,
+        monthsShort : months,
+        weekdays : weekdays,
+        weekdaysShort : weekdays,
+        weekdaysMin : 'އާދި_ހޯމަ_އަން_ބުދަ_ބުރާ_ހުކު_ހޮނި'.split('_'),
+        longDateFormat : {
+
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'D/M/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        meridiemParse: /މކ|މފ/,
+        isPM : function (input) {
+            return 'މފ' === input;
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'މކ';
+            } else {
+                return 'މފ';
+            }
+        },
+        calendar : {
+            sameDay : '[މިއަދު] LT',
+            nextDay : '[މާދަމާ] LT',
+            nextWeek : 'dddd LT',
+            lastDay : '[އިއްޔެ] LT',
+            lastWeek : '[ފާއިތުވި] dddd LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'ތެރޭގައި %s',
+            past : 'ކުރިން %s',
+            s : 'ސިކުންތުކޮޅެއް',
+            m : 'މިނިޓެއް',
+            mm : 'މިނިޓު %d',
+            h : 'ގަޑިއިރެއް',
+            hh : 'ގަޑިއިރު %d',
+            d : 'ދުވަހެއް',
+            dd : 'ދުވަސް %d',
+            M : 'މަހެއް',
+            MM : 'މަސް %d',
+            y : 'އަހަރެއް',
+            yy : 'އަހަރު %d'
+        },
+        preparse: function (string) {
+            return string.replace(/،/g, ',');
+        },
+        postformat: function (string) {
+            return string.replace(/,/g, '،');
+        },
+        week : {
+            dow : 7,  // Sunday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return dv;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/el.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/el.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/el.js	(revision 12265)
@@ -0,0 +1,98 @@
+//! moment.js locale configuration
+//! locale : Greek [el]
+//! author : Aggelos Karalias : https://github.com/mehiel
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+    function isFunction(input) {
+        return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]';
+    }
+
+
+    var el = moment.defineLocale('el', {
+        monthsNominativeEl : 'Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος'.split('_'),
+        monthsGenitiveEl : 'Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου'.split('_'),
+        months : function (momentToFormat, format) {
+            if (/D/.test(format.substring(0, format.indexOf('MMMM')))) { // if there is a day number before 'MMMM'
+                return this._monthsGenitiveEl[momentToFormat.month()];
+            } else {
+                return this._monthsNominativeEl[momentToFormat.month()];
+            }
+        },
+        monthsShort : 'Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ'.split('_'),
+        weekdays : 'Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο'.split('_'),
+        weekdaysShort : 'Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ'.split('_'),
+        weekdaysMin : 'Κυ_Δε_Τρ_Τε_Πε_Πα_Σα'.split('_'),
+        meridiem : function (hours, minutes, isLower) {
+            if (hours > 11) {
+                return isLower ? 'μμ' : 'ΜΜ';
+            } else {
+                return isLower ? 'πμ' : 'ΠΜ';
+            }
+        },
+        isPM : function (input) {
+            return ((input + '').toLowerCase()[0] === 'μ');
+        },
+        meridiemParse : /[ΠΜ]\.?Μ?\.?/i,
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY h:mm A',
+            LLLL : 'dddd, D MMMM YYYY h:mm A'
+        },
+        calendarEl : {
+            sameDay : '[Σήμερα {}] LT',
+            nextDay : '[Αύριο {}] LT',
+            nextWeek : 'dddd [{}] LT',
+            lastDay : '[Χθες {}] LT',
+            lastWeek : function () {
+                switch (this.day()) {
+                    case 6:
+                        return '[το προηγούμενο] dddd [{}] LT';
+                    default:
+                        return '[την προηγούμενη] dddd [{}] LT';
+                }
+            },
+            sameElse : 'L'
+        },
+        calendar : function (key, mom) {
+            var output = this._calendarEl[key],
+                hours = mom && mom.hours();
+            if (isFunction(output)) {
+                output = output.apply(mom);
+            }
+            return output.replace('{}', (hours % 12 === 1 ? 'στη' : 'στις'));
+        },
+        relativeTime : {
+            future : 'σε %s',
+            past : '%s πριν',
+            s : 'λίγα δευτερόλεπτα',
+            m : 'ένα λεπτό',
+            mm : '%d λεπτά',
+            h : 'μία ώρα',
+            hh : '%d ώρες',
+            d : 'μία μέρα',
+            dd : '%d μέρες',
+            M : 'ένας μήνας',
+            MM : '%d μήνες',
+            y : 'ένας χρόνος',
+            yy : '%d χρόνια'
+        },
+        ordinalParse: /\d{1,2}η/,
+        ordinal: '%dη',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4st is the first week of the year.
+        }
+    });
+
+    return el;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-au.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-au.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-au.js	(revision 12265)
@@ -0,0 +1,67 @@
+//! moment.js locale configuration
+//! locale : English (Australia) [en-au]
+//! author : Jared Morse : https://github.com/jarcoal
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var en_au = moment.defineLocale('en-au', {
+        months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+        weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+        weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+        weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY h:mm A',
+            LLLL : 'dddd, D MMMM YYYY h:mm A'
+        },
+        calendar : {
+            sameDay : '[Today at] LT',
+            nextDay : '[Tomorrow at] LT',
+            nextWeek : 'dddd [at] LT',
+            lastDay : '[Yesterday at] LT',
+            lastWeek : '[Last] dddd [at] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : '%s ago',
+            s : 'a few seconds',
+            m : 'a minute',
+            mm : '%d minutes',
+            h : 'an hour',
+            hh : '%d hours',
+            d : 'a day',
+            dd : '%d days',
+            M : 'a month',
+            MM : '%d months',
+            y : 'a year',
+            yy : '%d years'
+        },
+        ordinalParse: /\d{1,2}(st|nd|rd|th)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return en_au;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-ca.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-ca.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-ca.js	(revision 12265)
@@ -0,0 +1,63 @@
+//! moment.js locale configuration
+//! locale : English (Canada) [en-ca]
+//! author : Jonathan Abourbih : https://github.com/jonbca
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var en_ca = moment.defineLocale('en-ca', {
+        months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+        weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+        weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+        weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'YYYY-MM-DD',
+            LL : 'MMMM D, YYYY',
+            LLL : 'MMMM D, YYYY h:mm A',
+            LLLL : 'dddd, MMMM D, YYYY h:mm A'
+        },
+        calendar : {
+            sameDay : '[Today at] LT',
+            nextDay : '[Tomorrow at] LT',
+            nextWeek : 'dddd [at] LT',
+            lastDay : '[Yesterday at] LT',
+            lastWeek : '[Last] dddd [at] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : '%s ago',
+            s : 'a few seconds',
+            m : 'a minute',
+            mm : '%d minutes',
+            h : 'an hour',
+            hh : '%d hours',
+            d : 'a day',
+            dd : '%d days',
+            M : 'a month',
+            MM : '%d months',
+            y : 'a year',
+            yy : '%d years'
+        },
+        ordinalParse: /\d{1,2}(st|nd|rd|th)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        }
+    });
+
+    return en_ca;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-gb.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-gb.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-gb.js	(revision 12265)
@@ -0,0 +1,67 @@
+//! moment.js locale configuration
+//! locale : English (United Kingdom) [en-gb]
+//! author : Chris Gedrim : https://github.com/chrisgedrim
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var en_gb = moment.defineLocale('en-gb', {
+        months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+        weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+        weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+        weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Today at] LT',
+            nextDay : '[Tomorrow at] LT',
+            nextWeek : 'dddd [at] LT',
+            lastDay : '[Yesterday at] LT',
+            lastWeek : '[Last] dddd [at] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : '%s ago',
+            s : 'a few seconds',
+            m : 'a minute',
+            mm : '%d minutes',
+            h : 'an hour',
+            hh : '%d hours',
+            d : 'a day',
+            dd : '%d days',
+            M : 'a month',
+            MM : '%d months',
+            y : 'a year',
+            yy : '%d years'
+        },
+        ordinalParse: /\d{1,2}(st|nd|rd|th)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return en_gb;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-ie.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-ie.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-ie.js	(revision 12265)
@@ -0,0 +1,67 @@
+//! moment.js locale configuration
+//! locale : English (Ireland) [en-ie]
+//! author : Chris Cartlidge : https://github.com/chriscartlidge
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var en_ie = moment.defineLocale('en-ie', {
+        months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+        weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+        weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+        weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD-MM-YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Today at] LT',
+            nextDay : '[Tomorrow at] LT',
+            nextWeek : 'dddd [at] LT',
+            lastDay : '[Yesterday at] LT',
+            lastWeek : '[Last] dddd [at] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : '%s ago',
+            s : 'a few seconds',
+            m : 'a minute',
+            mm : '%d minutes',
+            h : 'an hour',
+            hh : '%d hours',
+            d : 'a day',
+            dd : '%d days',
+            M : 'a month',
+            MM : '%d months',
+            y : 'a year',
+            yy : '%d years'
+        },
+        ordinalParse: /\d{1,2}(st|nd|rd|th)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return en_ie;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-nz.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-nz.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/en-nz.js	(revision 12265)
@@ -0,0 +1,67 @@
+//! moment.js locale configuration
+//! locale : English (New Zealand) [en-nz]
+//! author : Luke McGregor : https://github.com/lukemcgregor
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var en_nz = moment.defineLocale('en-nz', {
+        months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+        weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+        weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+        weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY h:mm A',
+            LLLL : 'dddd, D MMMM YYYY h:mm A'
+        },
+        calendar : {
+            sameDay : '[Today at] LT',
+            nextDay : '[Tomorrow at] LT',
+            nextWeek : 'dddd [at] LT',
+            lastDay : '[Yesterday at] LT',
+            lastWeek : '[Last] dddd [at] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'in %s',
+            past : '%s ago',
+            s : 'a few seconds',
+            m : 'a minute',
+            mm : '%d minutes',
+            h : 'an hour',
+            hh : '%d hours',
+            d : 'a day',
+            dd : '%d days',
+            M : 'a month',
+            MM : '%d months',
+            y : 'a year',
+            yy : '%d years'
+        },
+        ordinalParse: /\d{1,2}(st|nd|rd|th)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return en_nz;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/eo.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/eo.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/eo.js	(revision 12265)
@@ -0,0 +1,73 @@
+//! moment.js locale configuration
+//! locale : Esperanto [eo]
+//! author : Colin Dean : https://github.com/colindean
+//! komento: Mi estas malcerta se mi korekte traktis akuzativojn en tiu traduko.
+//!          Se ne, bonvolu korekti kaj avizi min por ke mi povas lerni!
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var eo = moment.defineLocale('eo', {
+        months : 'januaro_februaro_marto_aprilo_majo_junio_julio_aŭgusto_septembro_oktobro_novembro_decembro'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_maj_jun_jul_aŭg_sep_okt_nov_dec'.split('_'),
+        weekdays : 'Dimanĉo_Lundo_Mardo_Merkredo_Ĵaŭdo_Vendredo_Sabato'.split('_'),
+        weekdaysShort : 'Dim_Lun_Mard_Merk_Ĵaŭ_Ven_Sab'.split('_'),
+        weekdaysMin : 'Di_Lu_Ma_Me_Ĵa_Ve_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'YYYY-MM-DD',
+            LL : 'D[-an de] MMMM, YYYY',
+            LLL : 'D[-an de] MMMM, YYYY HH:mm',
+            LLLL : 'dddd, [la] D[-an de] MMMM, YYYY HH:mm'
+        },
+        meridiemParse: /[ap]\.t\.m/i,
+        isPM: function (input) {
+            return input.charAt(0).toLowerCase() === 'p';
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours > 11) {
+                return isLower ? 'p.t.m.' : 'P.T.M.';
+            } else {
+                return isLower ? 'a.t.m.' : 'A.T.M.';
+            }
+        },
+        calendar : {
+            sameDay : '[Hodiaŭ je] LT',
+            nextDay : '[Morgaŭ je] LT',
+            nextWeek : 'dddd [je] LT',
+            lastDay : '[Hieraŭ je] LT',
+            lastWeek : '[pasinta] dddd [je] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'je %s',
+            past : 'antaŭ %s',
+            s : 'sekundoj',
+            m : 'minuto',
+            mm : '%d minutoj',
+            h : 'horo',
+            hh : '%d horoj',
+            d : 'tago',//ne 'diurno', ĉar estas uzita por proksimumo
+            dd : '%d tagoj',
+            M : 'monato',
+            MM : '%d monatoj',
+            y : 'jaro',
+            yy : '%d jaroj'
+        },
+        ordinalParse: /\d{1,2}a/,
+        ordinal : '%da',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return eo;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/es-do.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/es-do.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/es-do.js	(revision 12265)
@@ -0,0 +1,80 @@
+//! moment.js locale configuration
+//! locale : Spanish (Dominican Republic) [es-do]
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'),
+        monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_');
+
+    var es_do = moment.defineLocale('es-do', {
+        months : 'enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre'.split('_'),
+        monthsShort : function (m, format) {
+            if (/-MMM-/.test(format)) {
+                return monthsShort[m.month()];
+            } else {
+                return monthsShortDot[m.month()];
+            }
+        },
+        monthsParseExact : true,
+        weekdays : 'domingo_lunes_martes_miércoles_jueves_viernes_sábado'.split('_'),
+        weekdaysShort : 'dom._lun._mar._mié._jue._vie._sáb.'.split('_'),
+        weekdaysMin : 'do_lu_ma_mi_ju_vi_sá'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D [de] MMMM [de] YYYY',
+            LLL : 'D [de] MMMM [de] YYYY h:mm A',
+            LLLL : 'dddd, D [de] MMMM [de] YYYY h:mm A'
+        },
+        calendar : {
+            sameDay : function () {
+                return '[hoy a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            nextDay : function () {
+                return '[mañana a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            nextWeek : function () {
+                return 'dddd [a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            lastDay : function () {
+                return '[ayer a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            lastWeek : function () {
+                return '[el] dddd [pasado a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'en %s',
+            past : 'hace %s',
+            s : 'unos segundos',
+            m : 'un minuto',
+            mm : '%d minutos',
+            h : 'una hora',
+            hh : '%d horas',
+            d : 'un día',
+            dd : '%d días',
+            M : 'un mes',
+            MM : '%d meses',
+            y : 'un año',
+            yy : '%d años'
+        },
+        ordinalParse : /\d{1,2}º/,
+        ordinal : '%dº',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return es_do;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/es.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/es.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/es.js	(revision 12265)
@@ -0,0 +1,81 @@
+//! moment.js locale configuration
+//! locale : Spanish [es]
+//! author : Julio Napurí : https://github.com/julionc
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'),
+        monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_');
+
+    var es = moment.defineLocale('es', {
+        months : 'enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre'.split('_'),
+        monthsShort : function (m, format) {
+            if (/-MMM-/.test(format)) {
+                return monthsShort[m.month()];
+            } else {
+                return monthsShortDot[m.month()];
+            }
+        },
+        monthsParseExact : true,
+        weekdays : 'domingo_lunes_martes_miércoles_jueves_viernes_sábado'.split('_'),
+        weekdaysShort : 'dom._lun._mar._mié._jue._vie._sáb.'.split('_'),
+        weekdaysMin : 'do_lu_ma_mi_ju_vi_sá'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D [de] MMMM [de] YYYY',
+            LLL : 'D [de] MMMM [de] YYYY H:mm',
+            LLLL : 'dddd, D [de] MMMM [de] YYYY H:mm'
+        },
+        calendar : {
+            sameDay : function () {
+                return '[hoy a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            nextDay : function () {
+                return '[mañana a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            nextWeek : function () {
+                return 'dddd [a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            lastDay : function () {
+                return '[ayer a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            lastWeek : function () {
+                return '[el] dddd [pasado a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'en %s',
+            past : 'hace %s',
+            s : 'unos segundos',
+            m : 'un minuto',
+            mm : '%d minutos',
+            h : 'una hora',
+            hh : '%d horas',
+            d : 'un día',
+            dd : '%d días',
+            M : 'un mes',
+            MM : '%d meses',
+            y : 'un año',
+            yy : '%d años'
+        },
+        ordinalParse : /\d{1,2}º/,
+        ordinal : '%dº',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return es;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/et.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/et.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/et.js	(revision 12265)
@@ -0,0 +1,80 @@
+//! moment.js locale configuration
+//! locale : Estonian [et]
+//! author : Henry Kehlmann : https://github.com/madhenry
+//! improvements : Illimar Tambek : https://github.com/ragulka
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function processRelativeTime(number, withoutSuffix, key, isFuture) {
+        var format = {
+            's' : ['mõne sekundi', 'mõni sekund', 'paar sekundit'],
+            'm' : ['ühe minuti', 'üks minut'],
+            'mm': [number + ' minuti', number + ' minutit'],
+            'h' : ['ühe tunni', 'tund aega', 'üks tund'],
+            'hh': [number + ' tunni', number + ' tundi'],
+            'd' : ['ühe päeva', 'üks päev'],
+            'M' : ['kuu aja', 'kuu aega', 'üks kuu'],
+            'MM': [number + ' kuu', number + ' kuud'],
+            'y' : ['ühe aasta', 'aasta', 'üks aasta'],
+            'yy': [number + ' aasta', number + ' aastat']
+        };
+        if (withoutSuffix) {
+            return format[key][2] ? format[key][2] : format[key][1];
+        }
+        return isFuture ? format[key][0] : format[key][1];
+    }
+
+    var et = moment.defineLocale('et', {
+        months        : 'jaanuar_veebruar_märts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember'.split('_'),
+        monthsShort   : 'jaan_veebr_märts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets'.split('_'),
+        weekdays      : 'pühapäev_esmaspäev_teisipäev_kolmapäev_neljapäev_reede_laupäev'.split('_'),
+        weekdaysShort : 'P_E_T_K_N_R_L'.split('_'),
+        weekdaysMin   : 'P_E_T_K_N_R_L'.split('_'),
+        longDateFormat : {
+            LT   : 'H:mm',
+            LTS : 'H:mm:ss',
+            L    : 'DD.MM.YYYY',
+            LL   : 'D. MMMM YYYY',
+            LLL  : 'D. MMMM YYYY H:mm',
+            LLLL : 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay  : '[Täna,] LT',
+            nextDay  : '[Homme,] LT',
+            nextWeek : '[Järgmine] dddd LT',
+            lastDay  : '[Eile,] LT',
+            lastWeek : '[Eelmine] dddd LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s pärast',
+            past   : '%s tagasi',
+            s      : processRelativeTime,
+            m      : processRelativeTime,
+            mm     : processRelativeTime,
+            h      : processRelativeTime,
+            hh     : processRelativeTime,
+            d      : processRelativeTime,
+            dd     : '%d päeva',
+            M      : processRelativeTime,
+            MM     : processRelativeTime,
+            y      : processRelativeTime,
+            yy     : processRelativeTime
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return et;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/eu.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/eu.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/eu.js	(revision 12265)
@@ -0,0 +1,66 @@
+//! moment.js locale configuration
+//! locale : Basque [eu]
+//! author : Eneko Illarramendi : https://github.com/eillarra
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var eu = moment.defineLocale('eu', {
+        months : 'urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua'.split('_'),
+        monthsShort : 'urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata'.split('_'),
+        weekdaysShort : 'ig._al._ar._az._og._ol._lr.'.split('_'),
+        weekdaysMin : 'ig_al_ar_az_og_ol_lr'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'YYYY-MM-DD',
+            LL : 'YYYY[ko] MMMM[ren] D[a]',
+            LLL : 'YYYY[ko] MMMM[ren] D[a] HH:mm',
+            LLLL : 'dddd, YYYY[ko] MMMM[ren] D[a] HH:mm',
+            l : 'YYYY-M-D',
+            ll : 'YYYY[ko] MMM D[a]',
+            lll : 'YYYY[ko] MMM D[a] HH:mm',
+            llll : 'ddd, YYYY[ko] MMM D[a] HH:mm'
+        },
+        calendar : {
+            sameDay : '[gaur] LT[etan]',
+            nextDay : '[bihar] LT[etan]',
+            nextWeek : 'dddd LT[etan]',
+            lastDay : '[atzo] LT[etan]',
+            lastWeek : '[aurreko] dddd LT[etan]',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s barru',
+            past : 'duela %s',
+            s : 'segundo batzuk',
+            m : 'minutu bat',
+            mm : '%d minutu',
+            h : 'ordu bat',
+            hh : '%d ordu',
+            d : 'egun bat',
+            dd : '%d egun',
+            M : 'hilabete bat',
+            MM : '%d hilabete',
+            y : 'urte bat',
+            yy : '%d urte'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return eu;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fa.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fa.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fa.js	(revision 12265)
@@ -0,0 +1,106 @@
+//! moment.js locale configuration
+//! locale : Persian [fa]
+//! author : Ebrahim Byagowi : https://github.com/ebraminio
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '۱',
+        '2': '۲',
+        '3': '۳',
+        '4': '۴',
+        '5': '۵',
+        '6': '۶',
+        '7': '۷',
+        '8': '۸',
+        '9': '۹',
+        '0': '۰'
+    }, numberMap = {
+        '۱': '1',
+        '۲': '2',
+        '۳': '3',
+        '۴': '4',
+        '۵': '5',
+        '۶': '6',
+        '۷': '7',
+        '۸': '8',
+        '۹': '9',
+        '۰': '0'
+    };
+
+    var fa = moment.defineLocale('fa', {
+        months : 'ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر'.split('_'),
+        monthsShort : 'ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر'.split('_'),
+        weekdays : 'یک\u200cشنبه_دوشنبه_سه\u200cشنبه_چهارشنبه_پنج\u200cشنبه_جمعه_شنبه'.split('_'),
+        weekdaysShort : 'یک\u200cشنبه_دوشنبه_سه\u200cشنبه_چهارشنبه_پنج\u200cشنبه_جمعه_شنبه'.split('_'),
+        weekdaysMin : 'ی_د_س_چ_پ_ج_ش'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        meridiemParse: /قبل از ظهر|بعد از ظهر/,
+        isPM: function (input) {
+            return /بعد از ظهر/.test(input);
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'قبل از ظهر';
+            } else {
+                return 'بعد از ظهر';
+            }
+        },
+        calendar : {
+            sameDay : '[امروز ساعت] LT',
+            nextDay : '[فردا ساعت] LT',
+            nextWeek : 'dddd [ساعت] LT',
+            lastDay : '[دیروز ساعت] LT',
+            lastWeek : 'dddd [پیش] [ساعت] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'در %s',
+            past : '%s پیش',
+            s : 'چندین ثانیه',
+            m : 'یک دقیقه',
+            mm : '%d دقیقه',
+            h : 'یک ساعت',
+            hh : '%d ساعت',
+            d : 'یک روز',
+            dd : '%d روز',
+            M : 'یک ماه',
+            MM : '%d ماه',
+            y : 'یک سال',
+            yy : '%d سال'
+        },
+        preparse: function (string) {
+            return string.replace(/[۰-۹]/g, function (match) {
+                return numberMap[match];
+            }).replace(/،/g, ',');
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            }).replace(/,/g, '،');
+        },
+        ordinalParse: /\d{1,2}م/,
+        ordinal : '%dم',
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12 // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return fa;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fi.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fi.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fi.js	(revision 12265)
@@ -0,0 +1,107 @@
+//! moment.js locale configuration
+//! locale : Finnish [fi]
+//! author : Tarmo Aidantausta : https://github.com/bleadof
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var numbersPast = 'nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän'.split(' '),
+        numbersFuture = [
+            'nolla', 'yhden', 'kahden', 'kolmen', 'neljän', 'viiden', 'kuuden',
+            numbersPast[7], numbersPast[8], numbersPast[9]
+        ];
+    function translate(number, withoutSuffix, key, isFuture) {
+        var result = '';
+        switch (key) {
+            case 's':
+                return isFuture ? 'muutaman sekunnin' : 'muutama sekunti';
+            case 'm':
+                return isFuture ? 'minuutin' : 'minuutti';
+            case 'mm':
+                result = isFuture ? 'minuutin' : 'minuuttia';
+                break;
+            case 'h':
+                return isFuture ? 'tunnin' : 'tunti';
+            case 'hh':
+                result = isFuture ? 'tunnin' : 'tuntia';
+                break;
+            case 'd':
+                return isFuture ? 'päivän' : 'päivä';
+            case 'dd':
+                result = isFuture ? 'päivän' : 'päivää';
+                break;
+            case 'M':
+                return isFuture ? 'kuukauden' : 'kuukausi';
+            case 'MM':
+                result = isFuture ? 'kuukauden' : 'kuukautta';
+                break;
+            case 'y':
+                return isFuture ? 'vuoden' : 'vuosi';
+            case 'yy':
+                result = isFuture ? 'vuoden' : 'vuotta';
+                break;
+        }
+        result = verbalNumber(number, isFuture) + ' ' + result;
+        return result;
+    }
+    function verbalNumber(number, isFuture) {
+        return number < 10 ? (isFuture ? numbersFuture[number] : numbersPast[number]) : number;
+    }
+
+    var fi = moment.defineLocale('fi', {
+        months : 'tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu'.split('_'),
+        monthsShort : 'tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu'.split('_'),
+        weekdays : 'sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai'.split('_'),
+        weekdaysShort : 'su_ma_ti_ke_to_pe_la'.split('_'),
+        weekdaysMin : 'su_ma_ti_ke_to_pe_la'.split('_'),
+        longDateFormat : {
+            LT : 'HH.mm',
+            LTS : 'HH.mm.ss',
+            L : 'DD.MM.YYYY',
+            LL : 'Do MMMM[ta] YYYY',
+            LLL : 'Do MMMM[ta] YYYY, [klo] HH.mm',
+            LLLL : 'dddd, Do MMMM[ta] YYYY, [klo] HH.mm',
+            l : 'D.M.YYYY',
+            ll : 'Do MMM YYYY',
+            lll : 'Do MMM YYYY, [klo] HH.mm',
+            llll : 'ddd, Do MMM YYYY, [klo] HH.mm'
+        },
+        calendar : {
+            sameDay : '[tänään] [klo] LT',
+            nextDay : '[huomenna] [klo] LT',
+            nextWeek : 'dddd [klo] LT',
+            lastDay : '[eilen] [klo] LT',
+            lastWeek : '[viime] dddd[na] [klo] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s päästä',
+            past : '%s sitten',
+            s : translate,
+            m : translate,
+            mm : translate,
+            h : translate,
+            hh : translate,
+            d : translate,
+            dd : translate,
+            M : translate,
+            MM : translate,
+            y : translate,
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return fi;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fo.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fo.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fo.js	(revision 12265)
@@ -0,0 +1,60 @@
+//! moment.js locale configuration
+//! locale : Faroese [fo]
+//! author : Ragnar Johannesen : https://github.com/ragnar123
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var fo = moment.defineLocale('fo', {
+        months : 'januar_februar_mars_apríl_mai_juni_juli_august_september_oktober_november_desember'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des'.split('_'),
+        weekdays : 'sunnudagur_mánadagur_týsdagur_mikudagur_hósdagur_fríggjadagur_leygardagur'.split('_'),
+        weekdaysShort : 'sun_mán_týs_mik_hós_frí_ley'.split('_'),
+        weekdaysMin : 'su_má_tý_mi_hó_fr_le'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D. MMMM, YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Í dag kl.] LT',
+            nextDay : '[Í morgin kl.] LT',
+            nextWeek : 'dddd [kl.] LT',
+            lastDay : '[Í gjár kl.] LT',
+            lastWeek : '[síðstu] dddd [kl] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'um %s',
+            past : '%s síðani',
+            s : 'fá sekund',
+            m : 'ein minutt',
+            mm : '%d minuttir',
+            h : 'ein tími',
+            hh : '%d tímar',
+            d : 'ein dagur',
+            dd : '%d dagar',
+            M : 'ein mánaði',
+            MM : '%d mánaðir',
+            y : 'eitt ár',
+            yy : '%d ár'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return fo;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr-ca.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr-ca.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr-ca.js	(revision 12265)
@@ -0,0 +1,60 @@
+//! moment.js locale configuration
+//! locale : French (Canada) [fr-ca]
+//! author : Jonathan Abourbih : https://github.com/jonbca
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var fr_ca = moment.defineLocale('fr-ca', {
+        months : 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'),
+        monthsShort : 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi'.split('_'),
+        weekdaysShort : 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'),
+        weekdaysMin : 'Di_Lu_Ma_Me_Je_Ve_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'YYYY-MM-DD',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Aujourd\'hui à] LT',
+            nextDay: '[Demain à] LT',
+            nextWeek: 'dddd [à] LT',
+            lastDay: '[Hier à] LT',
+            lastWeek: 'dddd [dernier à] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'dans %s',
+            past : 'il y a %s',
+            s : 'quelques secondes',
+            m : 'une minute',
+            mm : '%d minutes',
+            h : 'une heure',
+            hh : '%d heures',
+            d : 'un jour',
+            dd : '%d jours',
+            M : 'un mois',
+            MM : '%d mois',
+            y : 'un an',
+            yy : '%d ans'
+        },
+        ordinalParse: /\d{1,2}(er|e)/,
+        ordinal : function (number) {
+            return number + (number === 1 ? 'er' : 'e');
+        }
+    });
+
+    return fr_ca;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr-ch.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr-ch.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr-ch.js	(revision 12265)
@@ -0,0 +1,64 @@
+//! moment.js locale configuration
+//! locale : French (Switzerland) [fr-ch]
+//! author : Gaspard Bucher : https://github.com/gaspard
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var fr_ch = moment.defineLocale('fr-ch', {
+        months : 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'),
+        monthsShort : 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi'.split('_'),
+        weekdaysShort : 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'),
+        weekdaysMin : 'Di_Lu_Ma_Me_Je_Ve_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Aujourd\'hui à] LT',
+            nextDay: '[Demain à] LT',
+            nextWeek: 'dddd [à] LT',
+            lastDay: '[Hier à] LT',
+            lastWeek: 'dddd [dernier à] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'dans %s',
+            past : 'il y a %s',
+            s : 'quelques secondes',
+            m : 'une minute',
+            mm : '%d minutes',
+            h : 'une heure',
+            hh : '%d heures',
+            d : 'un jour',
+            dd : '%d jours',
+            M : 'un mois',
+            MM : '%d mois',
+            y : 'un an',
+            yy : '%d ans'
+        },
+        ordinalParse: /\d{1,2}(er|e)/,
+        ordinal : function (number) {
+            return number + (number === 1 ? 'er' : 'e');
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return fr_ch;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fr.js	(revision 12265)
@@ -0,0 +1,64 @@
+//! moment.js locale configuration
+//! locale : French [fr]
+//! author : John Fischer : https://github.com/jfroffice
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var fr = moment.defineLocale('fr', {
+        months : 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'),
+        monthsShort : 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi'.split('_'),
+        weekdaysShort : 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'),
+        weekdaysMin : 'Di_Lu_Ma_Me_Je_Ve_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Aujourd\'hui à] LT',
+            nextDay: '[Demain à] LT',
+            nextWeek: 'dddd [à] LT',
+            lastDay: '[Hier à] LT',
+            lastWeek: 'dddd [dernier à] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'dans %s',
+            past : 'il y a %s',
+            s : 'quelques secondes',
+            m : 'une minute',
+            mm : '%d minutes',
+            h : 'une heure',
+            hh : '%d heures',
+            d : 'un jour',
+            dd : '%d jours',
+            M : 'un mois',
+            MM : '%d mois',
+            y : 'un an',
+            yy : '%d ans'
+        },
+        ordinalParse: /\d{1,2}(er|)/,
+        ordinal : function (number) {
+            return number + (number === 1 ? 'er' : '');
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return fr;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fy.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fy.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/fy.js	(revision 12265)
@@ -0,0 +1,73 @@
+//! moment.js locale configuration
+//! locale : Frisian [fy]
+//! author : Robin van der Vliet : https://github.com/robin0van0der0v
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var monthsShortWithDots = 'jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.'.split('_'),
+        monthsShortWithoutDots = 'jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des'.split('_');
+
+    var fy = moment.defineLocale('fy', {
+        months : 'jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber'.split('_'),
+        monthsShort : function (m, format) {
+            if (/-MMM-/.test(format)) {
+                return monthsShortWithoutDots[m.month()];
+            } else {
+                return monthsShortWithDots[m.month()];
+            }
+        },
+        monthsParseExact : true,
+        weekdays : 'snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon'.split('_'),
+        weekdaysShort : 'si._mo._ti._wo._to._fr._so.'.split('_'),
+        weekdaysMin : 'Si_Mo_Ti_Wo_To_Fr_So'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD-MM-YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[hjoed om] LT',
+            nextDay: '[moarn om] LT',
+            nextWeek: 'dddd [om] LT',
+            lastDay: '[juster om] LT',
+            lastWeek: '[ôfrûne] dddd [om] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'oer %s',
+            past : '%s lyn',
+            s : 'in pear sekonden',
+            m : 'ien minút',
+            mm : '%d minuten',
+            h : 'ien oere',
+            hh : '%d oeren',
+            d : 'ien dei',
+            dd : '%d dagen',
+            M : 'ien moanne',
+            MM : '%d moannen',
+            y : 'ien jier',
+            yy : '%d jierren'
+        },
+        ordinalParse: /\d{1,2}(ste|de)/,
+        ordinal : function (number) {
+            return number + ((number === 1 || number === 8 || number >= 20) ? 'ste' : 'de');
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return fy;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/gd.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/gd.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/gd.js	(revision 12265)
@@ -0,0 +1,76 @@
+//! moment.js locale configuration
+//! locale : Scottish Gaelic [gd]
+//! author : Jon Ashdown : https://github.com/jonashdown
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var months = [
+        'Am Faoilleach', 'An Gearran', 'Am Màrt', 'An Giblean', 'An Cèitean', 'An t-Ògmhios', 'An t-Iuchar', 'An Lùnastal', 'An t-Sultain', 'An Dàmhair', 'An t-Samhain', 'An Dùbhlachd'
+    ];
+
+    var monthsShort = ['Faoi', 'Gear', 'Màrt', 'Gibl', 'Cèit', 'Ògmh', 'Iuch', 'Lùn', 'Sult', 'Dàmh', 'Samh', 'Dùbh'];
+
+    var weekdays = ['Didòmhnaich', 'Diluain', 'Dimàirt', 'Diciadain', 'Diardaoin', 'Dihaoine', 'Disathairne'];
+
+    var weekdaysShort = ['Did', 'Dil', 'Dim', 'Dic', 'Dia', 'Dih', 'Dis'];
+
+    var weekdaysMin = ['Dò', 'Lu', 'Mà', 'Ci', 'Ar', 'Ha', 'Sa'];
+
+    var gd = moment.defineLocale('gd', {
+        months : months,
+        monthsShort : monthsShort,
+        monthsParseExact : true,
+        weekdays : weekdays,
+        weekdaysShort : weekdaysShort,
+        weekdaysMin : weekdaysMin,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[An-diugh aig] LT',
+            nextDay : '[A-màireach aig] LT',
+            nextWeek : 'dddd [aig] LT',
+            lastDay : '[An-dè aig] LT',
+            lastWeek : 'dddd [seo chaidh] [aig] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'ann an %s',
+            past : 'bho chionn %s',
+            s : 'beagan diogan',
+            m : 'mionaid',
+            mm : '%d mionaidean',
+            h : 'uair',
+            hh : '%d uairean',
+            d : 'latha',
+            dd : '%d latha',
+            M : 'mìos',
+            MM : '%d mìosan',
+            y : 'bliadhna',
+            yy : '%d bliadhna'
+        },
+        ordinalParse : /\d{1,2}(d|na|mh)/,
+        ordinal : function (number) {
+            var output = number === 1 ? 'd' : number % 10 === 2 ? 'na' : 'mh';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return gd;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/gl.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/gl.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/gl.js	(revision 12265)
@@ -0,0 +1,77 @@
+//! moment.js locale configuration
+//! locale : Galician [gl]
+//! author : Juan G. Hurtado : https://github.com/juanghurtado
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var gl = moment.defineLocale('gl', {
+        months : 'xaneiro_febreiro_marzo_abril_maio_xuño_xullo_agosto_setembro_outubro_novembro_decembro'.split('_'),
+        monthsShort : 'xan._feb._mar._abr._mai._xuñ._xul._ago._set._out._nov._dec.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'domingo_luns_martes_mércores_xoves_venres_sábado'.split('_'),
+        weekdaysShort : 'dom._lun._mar._mér._xov._ven._sáb.'.split('_'),
+        weekdaysMin : 'do_lu_ma_mé_xo_ve_sá'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D [de] MMMM [de] YYYY',
+            LLL : 'D [de] MMMM [de] YYYY H:mm',
+            LLLL : 'dddd, D [de] MMMM [de] YYYY H:mm'
+        },
+        calendar : {
+            sameDay : function () {
+                return '[hoxe ' + ((this.hours() !== 1) ? 'ás' : 'á') + '] LT';
+            },
+            nextDay : function () {
+                return '[mañá ' + ((this.hours() !== 1) ? 'ás' : 'á') + '] LT';
+            },
+            nextWeek : function () {
+                return 'dddd [' + ((this.hours() !== 1) ? 'ás' : 'a') + '] LT';
+            },
+            lastDay : function () {
+                return '[onte ' + ((this.hours() !== 1) ? 'á' : 'a') + '] LT';
+            },
+            lastWeek : function () {
+                return '[o] dddd [pasado ' + ((this.hours() !== 1) ? 'ás' : 'a') + '] LT';
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : function (str) {
+                if (str.indexOf('un') === 0) {
+                    return 'n' + str;
+                }
+                return 'en ' + str;
+            },
+            past : 'hai %s',
+            s : 'uns segundos',
+            m : 'un minuto',
+            mm : '%d minutos',
+            h : 'unha hora',
+            hh : '%d horas',
+            d : 'un día',
+            dd : '%d días',
+            M : 'un mes',
+            MM : '%d meses',
+            y : 'un ano',
+            yy : '%d anos'
+        },
+        ordinalParse : /\d{1,2}º/,
+        ordinal : '%dº',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return gl;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/he.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/he.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/he.js	(revision 12265)
@@ -0,0 +1,99 @@
+//! moment.js locale configuration
+//! locale : Hebrew [he]
+//! author : Tomer Cohen : https://github.com/tomer
+//! author : Moshe Simantov : https://github.com/DevelopmentIL
+//! author : Tal Ater : https://github.com/TalAter
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var he = moment.defineLocale('he', {
+        months : 'ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר'.split('_'),
+        monthsShort : 'ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳'.split('_'),
+        weekdays : 'ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת'.split('_'),
+        weekdaysShort : 'א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳'.split('_'),
+        weekdaysMin : 'א_ב_ג_ד_ה_ו_ש'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D [ב]MMMM YYYY',
+            LLL : 'D [ב]MMMM YYYY HH:mm',
+            LLLL : 'dddd, D [ב]MMMM YYYY HH:mm',
+            l : 'D/M/YYYY',
+            ll : 'D MMM YYYY',
+            lll : 'D MMM YYYY HH:mm',
+            llll : 'ddd, D MMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[היום ב־]LT',
+            nextDay : '[מחר ב־]LT',
+            nextWeek : 'dddd [בשעה] LT',
+            lastDay : '[אתמול ב־]LT',
+            lastWeek : '[ביום] dddd [האחרון בשעה] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'בעוד %s',
+            past : 'לפני %s',
+            s : 'מספר שניות',
+            m : 'דקה',
+            mm : '%d דקות',
+            h : 'שעה',
+            hh : function (number) {
+                if (number === 2) {
+                    return 'שעתיים';
+                }
+                return number + ' שעות';
+            },
+            d : 'יום',
+            dd : function (number) {
+                if (number === 2) {
+                    return 'יומיים';
+                }
+                return number + ' ימים';
+            },
+            M : 'חודש',
+            MM : function (number) {
+                if (number === 2) {
+                    return 'חודשיים';
+                }
+                return number + ' חודשים';
+            },
+            y : 'שנה',
+            yy : function (number) {
+                if (number === 2) {
+                    return 'שנתיים';
+                } else if (number % 10 === 0 && number !== 10) {
+                    return number + ' שנה';
+                }
+                return number + ' שנים';
+            }
+        },
+        meridiemParse: /אחה"צ|לפנה"צ|אחרי הצהריים|לפני הצהריים|לפנות בוקר|בבוקר|בערב/i,
+        isPM : function (input) {
+            return /^(אחה"צ|אחרי הצהריים|בערב)$/.test(input);
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 5) {
+                return 'לפנות בוקר';
+            } else if (hour < 10) {
+                return 'בבוקר';
+            } else if (hour < 12) {
+                return isLower ? 'לפנה"צ' : 'לפני הצהריים';
+            } else if (hour < 18) {
+                return isLower ? 'אחה"צ' : 'אחרי הצהריים';
+            } else {
+                return 'בערב';
+            }
+        }
+    });
+
+    return he;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hi.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hi.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hi.js	(revision 12265)
@@ -0,0 +1,124 @@
+//! moment.js locale configuration
+//! locale : Hindi [hi]
+//! author : Mayank Singhal : https://github.com/mayanksinghal
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '१',
+        '2': '२',
+        '3': '३',
+        '4': '४',
+        '5': '५',
+        '6': '६',
+        '7': '७',
+        '8': '८',
+        '9': '९',
+        '0': '०'
+    },
+    numberMap = {
+        '१': '1',
+        '२': '2',
+        '३': '3',
+        '४': '4',
+        '५': '5',
+        '६': '6',
+        '७': '7',
+        '८': '8',
+        '९': '9',
+        '०': '0'
+    };
+
+    var hi = moment.defineLocale('hi', {
+        months : 'जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर'.split('_'),
+        monthsShort : 'जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार'.split('_'),
+        weekdaysShort : 'रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि'.split('_'),
+        weekdaysMin : 'र_सो_मं_बु_गु_शु_श'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm बजे',
+            LTS : 'A h:mm:ss बजे',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm बजे',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm बजे'
+        },
+        calendar : {
+            sameDay : '[आज] LT',
+            nextDay : '[कल] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[कल] LT',
+            lastWeek : '[पिछले] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s में',
+            past : '%s पहले',
+            s : 'कुछ ही क्षण',
+            m : 'एक मिनट',
+            mm : '%d मिनट',
+            h : 'एक घंटा',
+            hh : '%d घंटे',
+            d : 'एक दिन',
+            dd : '%d दिन',
+            M : 'एक महीने',
+            MM : '%d महीने',
+            y : 'एक वर्ष',
+            yy : '%d वर्ष'
+        },
+        preparse: function (string) {
+            return string.replace(/[१२३४५६७८९०]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        // Hindi notation for meridiems are quite fuzzy in practice. While there exists
+        // a rigid notion of a 'Pahar' it is not used as rigidly in modern Hindi.
+        meridiemParse: /रात|सुबह|दोपहर|शाम/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'रात') {
+                return hour < 4 ? hour : hour + 12;
+            } else if (meridiem === 'सुबह') {
+                return hour;
+            } else if (meridiem === 'दोपहर') {
+                return hour >= 10 ? hour : hour + 12;
+            } else if (meridiem === 'शाम') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'रात';
+            } else if (hour < 10) {
+                return 'सुबह';
+            } else if (hour < 17) {
+                return 'दोपहर';
+            } else if (hour < 20) {
+                return 'शाम';
+            } else {
+                return 'रात';
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return hi;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hr.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hr.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hr.js	(revision 12265)
@@ -0,0 +1,145 @@
+//! moment.js locale configuration
+//! locale : Croatian [hr]
+//! author : Bojan Marković : https://github.com/bmarkovic
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function translate(number, withoutSuffix, key) {
+        var result = number + ' ';
+        switch (key) {
+            case 'm':
+                return withoutSuffix ? 'jedna minuta' : 'jedne minute';
+            case 'mm':
+                if (number === 1) {
+                    result += 'minuta';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'minute';
+                } else {
+                    result += 'minuta';
+                }
+                return result;
+            case 'h':
+                return withoutSuffix ? 'jedan sat' : 'jednog sata';
+            case 'hh':
+                if (number === 1) {
+                    result += 'sat';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'sata';
+                } else {
+                    result += 'sati';
+                }
+                return result;
+            case 'dd':
+                if (number === 1) {
+                    result += 'dan';
+                } else {
+                    result += 'dana';
+                }
+                return result;
+            case 'MM':
+                if (number === 1) {
+                    result += 'mjesec';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'mjeseca';
+                } else {
+                    result += 'mjeseci';
+                }
+                return result;
+            case 'yy':
+                if (number === 1) {
+                    result += 'godina';
+                } else if (number === 2 || number === 3 || number === 4) {
+                    result += 'godine';
+                } else {
+                    result += 'godina';
+                }
+                return result;
+        }
+    }
+
+    var hr = moment.defineLocale('hr', {
+        months : {
+            format: 'siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca'.split('_'),
+            standalone: 'siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac'.split('_')
+        },
+        monthsShort : 'sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'),
+        weekdaysShort : 'ned._pon._uto._sri._čet._pet._sub.'.split('_'),
+        weekdaysMin : 'ne_po_ut_sr_če_pe_su'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY H:mm',
+            LLLL : 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay  : '[danas u] LT',
+            nextDay  : '[sutra u] LT',
+            nextWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[u] [nedjelju] [u] LT';
+                    case 3:
+                        return '[u] [srijedu] [u] LT';
+                    case 6:
+                        return '[u] [subotu] [u] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[u] dddd [u] LT';
+                }
+            },
+            lastDay  : '[jučer u] LT',
+            lastWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                    case 3:
+                        return '[prošlu] dddd [u] LT';
+                    case 6:
+                        return '[prošle] [subote] [u] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[prošli] dddd [u] LT';
+                }
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past   : 'prije %s',
+            s      : 'par sekundi',
+            m      : translate,
+            mm     : translate,
+            h      : translate,
+            hh     : translate,
+            d      : 'dan',
+            dd     : translate,
+            M      : 'mjesec',
+            MM     : translate,
+            y      : 'godinu',
+            yy     : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return hr;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hu.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hu.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hu.js	(revision 12265)
@@ -0,0 +1,109 @@
+//! moment.js locale configuration
+//! locale : Hungarian [hu]
+//! author : Adam Brunner : https://github.com/adambrunner
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var weekEndings = 'vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton'.split(' ');
+    function translate(number, withoutSuffix, key, isFuture) {
+        var num = number,
+            suffix;
+        switch (key) {
+            case 's':
+                return (isFuture || withoutSuffix) ? 'néhány másodperc' : 'néhány másodperce';
+            case 'm':
+                return 'egy' + (isFuture || withoutSuffix ? ' perc' : ' perce');
+            case 'mm':
+                return num + (isFuture || withoutSuffix ? ' perc' : ' perce');
+            case 'h':
+                return 'egy' + (isFuture || withoutSuffix ? ' óra' : ' órája');
+            case 'hh':
+                return num + (isFuture || withoutSuffix ? ' óra' : ' órája');
+            case 'd':
+                return 'egy' + (isFuture || withoutSuffix ? ' nap' : ' napja');
+            case 'dd':
+                return num + (isFuture || withoutSuffix ? ' nap' : ' napja');
+            case 'M':
+                return 'egy' + (isFuture || withoutSuffix ? ' hónap' : ' hónapja');
+            case 'MM':
+                return num + (isFuture || withoutSuffix ? ' hónap' : ' hónapja');
+            case 'y':
+                return 'egy' + (isFuture || withoutSuffix ? ' év' : ' éve');
+            case 'yy':
+                return num + (isFuture || withoutSuffix ? ' év' : ' éve');
+        }
+        return '';
+    }
+    function week(isFuture) {
+        return (isFuture ? '' : '[múlt] ') + '[' + weekEndings[this.day()] + '] LT[-kor]';
+    }
+
+    var hu = moment.defineLocale('hu', {
+        months : 'január_február_március_április_május_június_július_augusztus_szeptember_október_november_december'.split('_'),
+        monthsShort : 'jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec'.split('_'),
+        weekdays : 'vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat'.split('_'),
+        weekdaysShort : 'vas_hét_kedd_sze_csüt_pén_szo'.split('_'),
+        weekdaysMin : 'v_h_k_sze_cs_p_szo'.split('_'),
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'YYYY.MM.DD.',
+            LL : 'YYYY. MMMM D.',
+            LLL : 'YYYY. MMMM D. H:mm',
+            LLLL : 'YYYY. MMMM D., dddd H:mm'
+        },
+        meridiemParse: /de|du/i,
+        isPM: function (input) {
+            return input.charAt(1).toLowerCase() === 'u';
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 12) {
+                return isLower === true ? 'de' : 'DE';
+            } else {
+                return isLower === true ? 'du' : 'DU';
+            }
+        },
+        calendar : {
+            sameDay : '[ma] LT[-kor]',
+            nextDay : '[holnap] LT[-kor]',
+            nextWeek : function () {
+                return week.call(this, true);
+            },
+            lastDay : '[tegnap] LT[-kor]',
+            lastWeek : function () {
+                return week.call(this, false);
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s múlva',
+            past : '%s',
+            s : translate,
+            m : translate,
+            mm : translate,
+            h : translate,
+            hh : translate,
+            d : translate,
+            dd : translate,
+            M : translate,
+            MM : translate,
+            y : translate,
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return hu;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hy-am.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hy-am.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/hy-am.js	(revision 12265)
@@ -0,0 +1,95 @@
+//! moment.js locale configuration
+//! locale : Armenian [hy-am]
+//! author : Armendarabyan : https://github.com/armendarabyan
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var hy_am = moment.defineLocale('hy-am', {
+        months : {
+            format: 'հունվարի_փետրվարի_մարտի_ապրիլի_մայիսի_հունիսի_հուլիսի_օգոստոսի_սեպտեմբերի_հոկտեմբերի_նոյեմբերի_դեկտեմբերի'.split('_'),
+            standalone: 'հունվար_փետրվար_մարտ_ապրիլ_մայիս_հունիս_հուլիս_օգոստոս_սեպտեմբեր_հոկտեմբեր_նոյեմբեր_դեկտեմբեր'.split('_')
+        },
+        monthsShort : 'հնվ_փտր_մրտ_ապր_մյս_հնս_հլս_օգս_սպտ_հկտ_նմբ_դկտ'.split('_'),
+        weekdays : 'կիրակի_երկուշաբթի_երեքշաբթի_չորեքշաբթի_հինգշաբթի_ուրբաթ_շաբաթ'.split('_'),
+        weekdaysShort : 'կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ'.split('_'),
+        weekdaysMin : 'կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY թ.',
+            LLL : 'D MMMM YYYY թ., HH:mm',
+            LLLL : 'dddd, D MMMM YYYY թ., HH:mm'
+        },
+        calendar : {
+            sameDay: '[այսօր] LT',
+            nextDay: '[վաղը] LT',
+            lastDay: '[երեկ] LT',
+            nextWeek: function () {
+                return 'dddd [օրը ժամը] LT';
+            },
+            lastWeek: function () {
+                return '[անցած] dddd [օրը ժամը] LT';
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : '%s հետո',
+            past : '%s առաջ',
+            s : 'մի քանի վայրկյան',
+            m : 'րոպե',
+            mm : '%d րոպե',
+            h : 'ժամ',
+            hh : '%d ժամ',
+            d : 'օր',
+            dd : '%d օր',
+            M : 'ամիս',
+            MM : '%d ամիս',
+            y : 'տարի',
+            yy : '%d տարի'
+        },
+        meridiemParse: /գիշերվա|առավոտվա|ցերեկվա|երեկոյան/,
+        isPM: function (input) {
+            return /^(ցերեկվա|երեկոյան)$/.test(input);
+        },
+        meridiem : function (hour) {
+            if (hour < 4) {
+                return 'գիշերվա';
+            } else if (hour < 12) {
+                return 'առավոտվա';
+            } else if (hour < 17) {
+                return 'ցերեկվա';
+            } else {
+                return 'երեկոյան';
+            }
+        },
+        ordinalParse: /\d{1,2}|\d{1,2}-(ին|րդ)/,
+        ordinal: function (number, period) {
+            switch (period) {
+                case 'DDD':
+                case 'w':
+                case 'W':
+                case 'DDDo':
+                    if (number === 1) {
+                        return number + '-ին';
+                    }
+                    return number + '-րդ';
+                default:
+                    return number;
+            }
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return hy_am;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/id.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/id.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/id.js	(revision 12265)
@@ -0,0 +1,83 @@
+//! moment.js locale configuration
+//! locale : Indonesian [id]
+//! author : Mohammad Satrio Utomo : https://github.com/tyok
+//! reference: http://id.wikisource.org/wiki/Pedoman_Umum_Ejaan_Bahasa_Indonesia_yang_Disempurnakan
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var id = moment.defineLocale('id', {
+        months : 'Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des'.split('_'),
+        weekdays : 'Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu'.split('_'),
+        weekdaysShort : 'Min_Sen_Sel_Rab_Kam_Jum_Sab'.split('_'),
+        weekdaysMin : 'Mg_Sn_Sl_Rb_Km_Jm_Sb'.split('_'),
+        longDateFormat : {
+            LT : 'HH.mm',
+            LTS : 'HH.mm.ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY [pukul] HH.mm',
+            LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm'
+        },
+        meridiemParse: /pagi|siang|sore|malam/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'pagi') {
+                return hour;
+            } else if (meridiem === 'siang') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === 'sore' || meridiem === 'malam') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 11) {
+                return 'pagi';
+            } else if (hours < 15) {
+                return 'siang';
+            } else if (hours < 19) {
+                return 'sore';
+            } else {
+                return 'malam';
+            }
+        },
+        calendar : {
+            sameDay : '[Hari ini pukul] LT',
+            nextDay : '[Besok pukul] LT',
+            nextWeek : 'dddd [pukul] LT',
+            lastDay : '[Kemarin pukul] LT',
+            lastWeek : 'dddd [lalu pukul] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'dalam %s',
+            past : '%s yang lalu',
+            s : 'beberapa detik',
+            m : 'semenit',
+            mm : '%d menit',
+            h : 'sejam',
+            hh : '%d jam',
+            d : 'sehari',
+            dd : '%d hari',
+            M : 'sebulan',
+            MM : '%d bulan',
+            y : 'setahun',
+            yy : '%d tahun'
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return id;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/is.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/is.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/is.js	(revision 12265)
@@ -0,0 +1,127 @@
+//! moment.js locale configuration
+//! locale : Icelandic [is]
+//! author : Hinrik Örn Sigurðsson : https://github.com/hinrik
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function plural(n) {
+        if (n % 100 === 11) {
+            return true;
+        } else if (n % 10 === 1) {
+            return false;
+        }
+        return true;
+    }
+    function translate(number, withoutSuffix, key, isFuture) {
+        var result = number + ' ';
+        switch (key) {
+            case 's':
+                return withoutSuffix || isFuture ? 'nokkrar sekúndur' : 'nokkrum sekúndum';
+            case 'm':
+                return withoutSuffix ? 'mínúta' : 'mínútu';
+            case 'mm':
+                if (plural(number)) {
+                    return result + (withoutSuffix || isFuture ? 'mínútur' : 'mínútum');
+                } else if (withoutSuffix) {
+                    return result + 'mínúta';
+                }
+                return result + 'mínútu';
+            case 'hh':
+                if (plural(number)) {
+                    return result + (withoutSuffix || isFuture ? 'klukkustundir' : 'klukkustundum');
+                }
+                return result + 'klukkustund';
+            case 'd':
+                if (withoutSuffix) {
+                    return 'dagur';
+                }
+                return isFuture ? 'dag' : 'degi';
+            case 'dd':
+                if (plural(number)) {
+                    if (withoutSuffix) {
+                        return result + 'dagar';
+                    }
+                    return result + (isFuture ? 'daga' : 'dögum');
+                } else if (withoutSuffix) {
+                    return result + 'dagur';
+                }
+                return result + (isFuture ? 'dag' : 'degi');
+            case 'M':
+                if (withoutSuffix) {
+                    return 'mánuður';
+                }
+                return isFuture ? 'mánuð' : 'mánuði';
+            case 'MM':
+                if (plural(number)) {
+                    if (withoutSuffix) {
+                        return result + 'mánuðir';
+                    }
+                    return result + (isFuture ? 'mánuði' : 'mánuðum');
+                } else if (withoutSuffix) {
+                    return result + 'mánuður';
+                }
+                return result + (isFuture ? 'mánuð' : 'mánuði');
+            case 'y':
+                return withoutSuffix || isFuture ? 'ár' : 'ári';
+            case 'yy':
+                if (plural(number)) {
+                    return result + (withoutSuffix || isFuture ? 'ár' : 'árum');
+                }
+                return result + (withoutSuffix || isFuture ? 'ár' : 'ári');
+        }
+    }
+
+    var is = moment.defineLocale('is', {
+        months : 'janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des'.split('_'),
+        weekdays : 'sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur'.split('_'),
+        weekdaysShort : 'sun_mán_þri_mið_fim_fös_lau'.split('_'),
+        weekdaysMin : 'Su_Má_Þr_Mi_Fi_Fö_La'.split('_'),
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY [kl.] H:mm',
+            LLLL : 'dddd, D. MMMM YYYY [kl.] H:mm'
+        },
+        calendar : {
+            sameDay : '[í dag kl.] LT',
+            nextDay : '[á morgun kl.] LT',
+            nextWeek : 'dddd [kl.] LT',
+            lastDay : '[í gær kl.] LT',
+            lastWeek : '[síðasta] dddd [kl.] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'eftir %s',
+            past : 'fyrir %s síðan',
+            s : translate,
+            m : translate,
+            mm : translate,
+            h : 'klukkustund',
+            hh : translate,
+            d : translate,
+            dd : translate,
+            M : translate,
+            MM : translate,
+            y : translate,
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return is;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/it.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/it.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/it.js	(revision 12265)
@@ -0,0 +1,70 @@
+//! moment.js locale configuration
+//! locale : Italian [it]
+//! author : Lorenzo : https://github.com/aliem
+//! author: Mattia Larentis: https://github.com/nostalgiaz
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var it = moment.defineLocale('it', {
+        months : 'gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre'.split('_'),
+        monthsShort : 'gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic'.split('_'),
+        weekdays : 'Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato'.split('_'),
+        weekdaysShort : 'Dom_Lun_Mar_Mer_Gio_Ven_Sab'.split('_'),
+        weekdaysMin : 'Do_Lu_Ma_Me_Gi_Ve_Sa'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Oggi alle] LT',
+            nextDay: '[Domani alle] LT',
+            nextWeek: 'dddd [alle] LT',
+            lastDay: '[Ieri alle] LT',
+            lastWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[la scorsa] dddd [alle] LT';
+                    default:
+                        return '[lo scorso] dddd [alle] LT';
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : function (s) {
+                return ((/^[0-9].+$/).test(s) ? 'tra' : 'in') + ' ' + s;
+            },
+            past : '%s fa',
+            s : 'alcuni secondi',
+            m : 'un minuto',
+            mm : '%d minuti',
+            h : 'un\'ora',
+            hh : '%d ore',
+            d : 'un giorno',
+            dd : '%d giorni',
+            M : 'un mese',
+            MM : '%d mesi',
+            y : 'un anno',
+            yy : '%d anni'
+        },
+        ordinalParse : /\d{1,2}º/,
+        ordinal: '%dº',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return it;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ja.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ja.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ja.js	(revision 12265)
@@ -0,0 +1,76 @@
+//! moment.js locale configuration
+//! locale : Japanese [ja]
+//! author : LI Long : https://github.com/baryon
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ja = moment.defineLocale('ja', {
+        months : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
+        monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
+        weekdays : '日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日'.split('_'),
+        weekdaysShort : '日_月_火_水_木_金_土'.split('_'),
+        weekdaysMin : '日_月_火_水_木_金_土'.split('_'),
+        longDateFormat : {
+            LT : 'Ah時m分',
+            LTS : 'Ah時m分s秒',
+            L : 'YYYY/MM/DD',
+            LL : 'YYYY年M月D日',
+            LLL : 'YYYY年M月D日Ah時m分',
+            LLLL : 'YYYY年M月D日Ah時m分 dddd'
+        },
+        meridiemParse: /午前|午後/i,
+        isPM : function (input) {
+            return input === '午後';
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return '午前';
+            } else {
+                return '午後';
+            }
+        },
+        calendar : {
+            sameDay : '[今日] LT',
+            nextDay : '[明日] LT',
+            nextWeek : '[来週]dddd LT',
+            lastDay : '[昨日] LT',
+            lastWeek : '[前週]dddd LT',
+            sameElse : 'L'
+        },
+        ordinalParse : /\d{1,2}日/,
+        ordinal : function (number, period) {
+            switch (period) {
+                case 'd':
+                case 'D':
+                case 'DDD':
+                    return number + '日';
+                default:
+                    return number;
+            }
+        },
+        relativeTime : {
+            future : '%s後',
+            past : '%s前',
+            s : '数秒',
+            m : '1分',
+            mm : '%d分',
+            h : '1時間',
+            hh : '%d時間',
+            d : '1日',
+            dd : '%d日',
+            M : '1ヶ月',
+            MM : '%dヶ月',
+            y : '1年',
+            yy : '%d年'
+        }
+    });
+
+    return ja;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/jv.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/jv.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/jv.js	(revision 12265)
@@ -0,0 +1,83 @@
+//! moment.js locale configuration
+//! locale : Japanese [jv]
+//! author : Rony Lantip : https://github.com/lantip
+//! reference: http://jv.wikipedia.org/wiki/Basa_Jawa
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var jv = moment.defineLocale('jv', {
+        months : 'Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_Nopember_Desember'.split('_'),
+        monthsShort : 'Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nop_Des'.split('_'),
+        weekdays : 'Minggu_Senen_Seloso_Rebu_Kemis_Jemuwah_Septu'.split('_'),
+        weekdaysShort : 'Min_Sen_Sel_Reb_Kem_Jem_Sep'.split('_'),
+        weekdaysMin : 'Mg_Sn_Sl_Rb_Km_Jm_Sp'.split('_'),
+        longDateFormat : {
+            LT : 'HH.mm',
+            LTS : 'HH.mm.ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY [pukul] HH.mm',
+            LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm'
+        },
+        meridiemParse: /enjing|siyang|sonten|ndalu/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'enjing') {
+                return hour;
+            } else if (meridiem === 'siyang') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === 'sonten' || meridiem === 'ndalu') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 11) {
+                return 'enjing';
+            } else if (hours < 15) {
+                return 'siyang';
+            } else if (hours < 19) {
+                return 'sonten';
+            } else {
+                return 'ndalu';
+            }
+        },
+        calendar : {
+            sameDay : '[Dinten puniko pukul] LT',
+            nextDay : '[Mbenjang pukul] LT',
+            nextWeek : 'dddd [pukul] LT',
+            lastDay : '[Kala wingi pukul] LT',
+            lastWeek : 'dddd [kepengker pukul] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'wonten ing %s',
+            past : '%s ingkang kepengker',
+            s : 'sawetawis detik',
+            m : 'setunggal menit',
+            mm : '%d menit',
+            h : 'setunggal jam',
+            hh : '%d jam',
+            d : 'sedinten',
+            dd : '%d dinten',
+            M : 'sewulan',
+            MM : '%d wulan',
+            y : 'setaun',
+            yy : '%d taun'
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return jv;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ka.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ka.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ka.js	(revision 12265)
@@ -0,0 +1,89 @@
+//! moment.js locale configuration
+//! locale : Georgian [ka]
+//! author : Irakli Janiashvili : https://github.com/irakli-janiashvili
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ka = moment.defineLocale('ka', {
+        months : {
+            standalone: 'იანვარი_თებერვალი_მარტი_აპრილი_მაისი_ივნისი_ივლისი_აგვისტო_სექტემბერი_ოქტომბერი_ნოემბერი_დეკემბერი'.split('_'),
+            format: 'იანვარს_თებერვალს_მარტს_აპრილის_მაისს_ივნისს_ივლისს_აგვისტს_სექტემბერს_ოქტომბერს_ნოემბერს_დეკემბერს'.split('_')
+        },
+        monthsShort : 'იან_თებ_მარ_აპრ_მაი_ივნ_ივლ_აგვ_სექ_ოქტ_ნოე_დეკ'.split('_'),
+        weekdays : {
+            standalone: 'კვირა_ორშაბათი_სამშაბათი_ოთხშაბათი_ხუთშაბათი_პარასკევი_შაბათი'.split('_'),
+            format: 'კვირას_ორშაბათს_სამშაბათს_ოთხშაბათს_ხუთშაბათს_პარასკევს_შაბათს'.split('_'),
+            isFormat: /(წინა|შემდეგ)/
+        },
+        weekdaysShort : 'კვი_ორშ_სამ_ოთხ_ხუთ_პარ_შაბ'.split('_'),
+        weekdaysMin : 'კვ_ორ_სა_ოთ_ხუ_პა_შა'.split('_'),
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY h:mm A',
+            LLLL : 'dddd, D MMMM YYYY h:mm A'
+        },
+        calendar : {
+            sameDay : '[დღეს] LT[-ზე]',
+            nextDay : '[ხვალ] LT[-ზე]',
+            lastDay : '[გუშინ] LT[-ზე]',
+            nextWeek : '[შემდეგ] dddd LT[-ზე]',
+            lastWeek : '[წინა] dddd LT-ზე',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : function (s) {
+                return (/(წამი|წუთი|საათი|წელი)/).test(s) ?
+                    s.replace(/ი$/, 'ში') :
+                    s + 'ში';
+            },
+            past : function (s) {
+                if ((/(წამი|წუთი|საათი|დღე|თვე)/).test(s)) {
+                    return s.replace(/(ი|ე)$/, 'ის წინ');
+                }
+                if ((/წელი/).test(s)) {
+                    return s.replace(/წელი$/, 'წლის წინ');
+                }
+            },
+            s : 'რამდენიმე წამი',
+            m : 'წუთი',
+            mm : '%d წუთი',
+            h : 'საათი',
+            hh : '%d საათი',
+            d : 'დღე',
+            dd : '%d დღე',
+            M : 'თვე',
+            MM : '%d თვე',
+            y : 'წელი',
+            yy : '%d წელი'
+        },
+        ordinalParse: /0|1-ლი|მე-\d{1,2}|\d{1,2}-ე/,
+        ordinal : function (number) {
+            if (number === 0) {
+                return number;
+            }
+            if (number === 1) {
+                return number + '-ლი';
+            }
+            if ((number < 20) || (number <= 100 && (number % 20 === 0)) || (number % 100 === 0)) {
+                return 'მე-' + number;
+            }
+            return number + '-ე';
+        },
+        week : {
+            dow : 1,
+            doy : 7
+        }
+    });
+
+    return ka;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/kk.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/kk.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/kk.js	(revision 12265)
@@ -0,0 +1,87 @@
+//! moment.js locale configuration
+//! locale : Kazakh [kk]
+//! authors : Nurlan Rakhimzhanov : https://github.com/nurlan
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var suffixes = {
+        0: '-ші',
+        1: '-ші',
+        2: '-ші',
+        3: '-ші',
+        4: '-ші',
+        5: '-ші',
+        6: '-шы',
+        7: '-ші',
+        8: '-ші',
+        9: '-шы',
+        10: '-шы',
+        20: '-шы',
+        30: '-шы',
+        40: '-шы',
+        50: '-ші',
+        60: '-шы',
+        70: '-ші',
+        80: '-ші',
+        90: '-шы',
+        100: '-ші'
+    };
+
+    var kk = moment.defineLocale('kk', {
+        months : 'қаңтар_ақпан_наурыз_сәуір_мамыр_маусым_шілде_тамыз_қыркүйек_қазан_қараша_желтоқсан'.split('_'),
+        monthsShort : 'қаң_ақп_нау_сәу_мам_мау_шіл_там_қыр_қаз_қар_жел'.split('_'),
+        weekdays : 'жексенбі_дүйсенбі_сейсенбі_сәрсенбі_бейсенбі_жұма_сенбі'.split('_'),
+        weekdaysShort : 'жек_дүй_сей_сәр_бей_жұм_сен'.split('_'),
+        weekdaysMin : 'жк_дй_сй_ср_бй_жм_сн'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Бүгін сағат] LT',
+            nextDay : '[Ертең сағат] LT',
+            nextWeek : 'dddd [сағат] LT',
+            lastDay : '[Кеше сағат] LT',
+            lastWeek : '[Өткен аптаның] dddd [сағат] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s ішінде',
+            past : '%s бұрын',
+            s : 'бірнеше секунд',
+            m : 'бір минут',
+            mm : '%d минут',
+            h : 'бір сағат',
+            hh : '%d сағат',
+            d : 'бір күн',
+            dd : '%d күн',
+            M : 'бір ай',
+            MM : '%d ай',
+            y : 'бір жыл',
+            yy : '%d жыл'
+        },
+        ordinalParse: /\d{1,2}-(ші|шы)/,
+        ordinal : function (number) {
+            var a = number % 10,
+                b = number >= 100 ? 100 : null;
+            return number + (suffixes[number] || suffixes[a] || suffixes[b]);
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return kk;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/km.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/km.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/km.js	(revision 12265)
@@ -0,0 +1,58 @@
+//! moment.js locale configuration
+//! locale : Cambodian [km]
+//! author : Kruy Vanna : https://github.com/kruyvanna
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var km = moment.defineLocale('km', {
+        months: 'មករា_កុម្ភៈ_មីនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ'.split('_'),
+        monthsShort: 'មករា_កុម្ភៈ_មីនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ'.split('_'),
+        weekdays: 'អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍'.split('_'),
+        weekdaysShort: 'អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍'.split('_'),
+        weekdaysMin: 'អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍'.split('_'),
+        longDateFormat: {
+            LT: 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L: 'DD/MM/YYYY',
+            LL: 'D MMMM YYYY',
+            LLL: 'D MMMM YYYY HH:mm',
+            LLLL: 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar: {
+            sameDay: '[ថ្ងៃនេះ ម៉ោង] LT',
+            nextDay: '[ស្អែក ម៉ោង] LT',
+            nextWeek: 'dddd [ម៉ោង] LT',
+            lastDay: '[ម្សិលមិញ ម៉ោង] LT',
+            lastWeek: 'dddd [សប្តាហ៍មុន] [ម៉ោង] LT',
+            sameElse: 'L'
+        },
+        relativeTime: {
+            future: '%sទៀត',
+            past: '%sមុន',
+            s: 'ប៉ុន្មានវិនាទី',
+            m: 'មួយនាទី',
+            mm: '%d នាទី',
+            h: 'មួយម៉ោង',
+            hh: '%d ម៉ោង',
+            d: 'មួយថ្ងៃ',
+            dd: '%d ថ្ងៃ',
+            M: 'មួយខែ',
+            MM: '%d ខែ',
+            y: 'មួយឆ្នាំ',
+            yy: '%d ឆ្នាំ'
+        },
+        week: {
+            dow: 1, // Monday is the first day of the week.
+            doy: 4 // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return km;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ko.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ko.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ko.js	(revision 12265)
@@ -0,0 +1,65 @@
+//! moment.js locale configuration
+//! locale : Korean [ko]
+//! author : Kyungwook, Park : https://github.com/kyungw00k
+//! author : Jeeeyul Lee <jeeeyul@gmail.com>
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ko = moment.defineLocale('ko', {
+        months : '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'),
+        monthsShort : '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'),
+        weekdays : '일요일_월요일_화요일_수요일_목요일_금요일_토요일'.split('_'),
+        weekdaysShort : '일_월_화_수_목_금_토'.split('_'),
+        weekdaysMin : '일_월_화_수_목_금_토'.split('_'),
+        longDateFormat : {
+            LT : 'A h시 m분',
+            LTS : 'A h시 m분 s초',
+            L : 'YYYY.MM.DD',
+            LL : 'YYYY년 MMMM D일',
+            LLL : 'YYYY년 MMMM D일 A h시 m분',
+            LLLL : 'YYYY년 MMMM D일 dddd A h시 m분'
+        },
+        calendar : {
+            sameDay : '오늘 LT',
+            nextDay : '내일 LT',
+            nextWeek : 'dddd LT',
+            lastDay : '어제 LT',
+            lastWeek : '지난주 dddd LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s 후',
+            past : '%s 전',
+            s : '몇 초',
+            ss : '%d초',
+            m : '일분',
+            mm : '%d분',
+            h : '한 시간',
+            hh : '%d시간',
+            d : '하루',
+            dd : '%d일',
+            M : '한 달',
+            MM : '%d달',
+            y : '일 년',
+            yy : '%d년'
+        },
+        ordinalParse : /\d{1,2}일/,
+        ordinal : '%d일',
+        meridiemParse : /오전|오후/,
+        isPM : function (token) {
+            return token === '오후';
+        },
+        meridiem : function (hour, minute, isUpper) {
+            return hour < 12 ? '오전' : '오후';
+        }
+    });
+
+    return ko;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ky.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ky.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ky.js	(revision 12265)
@@ -0,0 +1,88 @@
+//! moment.js locale configuration
+//! locale : Kyrgyz [ky]
+//! author : Chyngyz Arystan uulu : https://github.com/chyngyz
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+
+    var suffixes = {
+        0: '-чү',
+        1: '-чи',
+        2: '-чи',
+        3: '-чү',
+        4: '-чү',
+        5: '-чи',
+        6: '-чы',
+        7: '-чи',
+        8: '-чи',
+        9: '-чу',
+        10: '-чу',
+        20: '-чы',
+        30: '-чу',
+        40: '-чы',
+        50: '-чү',
+        60: '-чы',
+        70: '-чи',
+        80: '-чи',
+        90: '-чу',
+        100: '-чү'
+    };
+
+    var ky = moment.defineLocale('ky', {
+        months : 'январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь'.split('_'),
+        monthsShort : 'янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек'.split('_'),
+        weekdays : 'Жекшемби_Дүйшөмбү_Шейшемби_Шаршемби_Бейшемби_Жума_Ишемби'.split('_'),
+        weekdaysShort : 'Жек_Дүй_Шей_Шар_Бей_Жум_Ише'.split('_'),
+        weekdaysMin : 'Жк_Дй_Шй_Шр_Бй_Жм_Иш'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Бүгүн саат] LT',
+            nextDay : '[Эртең саат] LT',
+            nextWeek : 'dddd [саат] LT',
+            lastDay : '[Кече саат] LT',
+            lastWeek : '[Өткен аптанын] dddd [күнү] [саат] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s ичинде',
+            past : '%s мурун',
+            s : 'бирнече секунд',
+            m : 'бир мүнөт',
+            mm : '%d мүнөт',
+            h : 'бир саат',
+            hh : '%d саат',
+            d : 'бир күн',
+            dd : '%d күн',
+            M : 'бир ай',
+            MM : '%d ай',
+            y : 'бир жыл',
+            yy : '%d жыл'
+        },
+        ordinalParse: /\d{1,2}-(чи|чы|чү|чу)/,
+        ordinal : function (number) {
+            var a = number % 10,
+                b = number >= 100 ? 100 : null;
+            return number + (suffixes[number] || suffixes[a] || suffixes[b]);
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ky;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lb.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lb.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lb.js	(revision 12265)
@@ -0,0 +1,137 @@
+//! moment.js locale configuration
+//! locale : Luxembourgish [lb]
+//! author : mweimerskirch : https://github.com/mweimerskirch
+//! author : David Raison : https://github.com/kwisatz
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function processRelativeTime(number, withoutSuffix, key, isFuture) {
+        var format = {
+            'm': ['eng Minutt', 'enger Minutt'],
+            'h': ['eng Stonn', 'enger Stonn'],
+            'd': ['een Dag', 'engem Dag'],
+            'M': ['ee Mount', 'engem Mount'],
+            'y': ['ee Joer', 'engem Joer']
+        };
+        return withoutSuffix ? format[key][0] : format[key][1];
+    }
+    function processFutureTime(string) {
+        var number = string.substr(0, string.indexOf(' '));
+        if (eifelerRegelAppliesToNumber(number)) {
+            return 'a ' + string;
+        }
+        return 'an ' + string;
+    }
+    function processPastTime(string) {
+        var number = string.substr(0, string.indexOf(' '));
+        if (eifelerRegelAppliesToNumber(number)) {
+            return 'viru ' + string;
+        }
+        return 'virun ' + string;
+    }
+    /**
+     * Returns true if the word before the given number loses the '-n' ending.
+     * e.g. 'an 10 Deeg' but 'a 5 Deeg'
+     *
+     * @param number {integer}
+     * @returns {boolean}
+     */
+    function eifelerRegelAppliesToNumber(number) {
+        number = parseInt(number, 10);
+        if (isNaN(number)) {
+            return false;
+        }
+        if (number < 0) {
+            // Negative Number --> always true
+            return true;
+        } else if (number < 10) {
+            // Only 1 digit
+            if (4 <= number && number <= 7) {
+                return true;
+            }
+            return false;
+        } else if (number < 100) {
+            // 2 digits
+            var lastDigit = number % 10, firstDigit = number / 10;
+            if (lastDigit === 0) {
+                return eifelerRegelAppliesToNumber(firstDigit);
+            }
+            return eifelerRegelAppliesToNumber(lastDigit);
+        } else if (number < 10000) {
+            // 3 or 4 digits --> recursively check first digit
+            while (number >= 10) {
+                number = number / 10;
+            }
+            return eifelerRegelAppliesToNumber(number);
+        } else {
+            // Anything larger than 4 digits: recursively check first n-3 digits
+            number = number / 1000;
+            return eifelerRegelAppliesToNumber(number);
+        }
+    }
+
+    var lb = moment.defineLocale('lb', {
+        months: 'Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'),
+        monthsShort: 'Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.'.split('_'),
+        monthsParseExact : true,
+        weekdays: 'Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg'.split('_'),
+        weekdaysShort: 'So._Mé._Dë._Më._Do._Fr._Sa.'.split('_'),
+        weekdaysMin: 'So_Mé_Dë_Më_Do_Fr_Sa'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat: {
+            LT: 'H:mm [Auer]',
+            LTS: 'H:mm:ss [Auer]',
+            L: 'DD.MM.YYYY',
+            LL: 'D. MMMM YYYY',
+            LLL: 'D. MMMM YYYY H:mm [Auer]',
+            LLLL: 'dddd, D. MMMM YYYY H:mm [Auer]'
+        },
+        calendar: {
+            sameDay: '[Haut um] LT',
+            sameElse: 'L',
+            nextDay: '[Muer um] LT',
+            nextWeek: 'dddd [um] LT',
+            lastDay: '[Gëschter um] LT',
+            lastWeek: function () {
+                // Different date string for 'Dënschdeg' (Tuesday) and 'Donneschdeg' (Thursday) due to phonological rule
+                switch (this.day()) {
+                    case 2:
+                    case 4:
+                        return '[Leschten] dddd [um] LT';
+                    default:
+                        return '[Leschte] dddd [um] LT';
+                }
+            }
+        },
+        relativeTime : {
+            future : processFutureTime,
+            past : processPastTime,
+            s : 'e puer Sekonnen',
+            m : processRelativeTime,
+            mm : '%d Minutten',
+            h : processRelativeTime,
+            hh : '%d Stonnen',
+            d : processRelativeTime,
+            dd : '%d Deeg',
+            M : processRelativeTime,
+            MM : '%d Méint',
+            y : processRelativeTime,
+            yy : '%d Joer'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal: '%d.',
+        week: {
+            dow: 1, // Monday is the first day of the week.
+            doy: 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return lb;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lo.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lo.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lo.js	(revision 12265)
@@ -0,0 +1,70 @@
+//! moment.js locale configuration
+//! locale : Lao [lo]
+//! author : Ryan Hart : https://github.com/ryanhart2
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var lo = moment.defineLocale('lo', {
+        months : 'ມັງກອນ_ກຸມພາ_ມີນາ_ເມສາ_ພຶດສະພາ_ມິຖຸນາ_ກໍລະກົດ_ສິງຫາ_ກັນຍາ_ຕຸລາ_ພະຈິກ_ທັນວາ'.split('_'),
+        monthsShort : 'ມັງກອນ_ກຸມພາ_ມີນາ_ເມສາ_ພຶດສະພາ_ມິຖຸນາ_ກໍລະກົດ_ສິງຫາ_ກັນຍາ_ຕຸລາ_ພະຈິກ_ທັນວາ'.split('_'),
+        weekdays : 'ອາທິດ_ຈັນ_ອັງຄານ_ພຸດ_ພະຫັດ_ສຸກ_ເສົາ'.split('_'),
+        weekdaysShort : 'ທິດ_ຈັນ_ອັງຄານ_ພຸດ_ພະຫັດ_ສຸກ_ເສົາ'.split('_'),
+        weekdaysMin : 'ທ_ຈ_ອຄ_ພ_ພຫ_ສກ_ສ'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'ວັນdddd D MMMM YYYY HH:mm'
+        },
+        meridiemParse: /ຕອນເຊົ້າ|ຕອນແລງ/,
+        isPM: function (input) {
+            return input === 'ຕອນແລງ';
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'ຕອນເຊົ້າ';
+            } else {
+                return 'ຕອນແລງ';
+            }
+        },
+        calendar : {
+            sameDay : '[ມື້ນີ້ເວລາ] LT',
+            nextDay : '[ມື້ອື່ນເວລາ] LT',
+            nextWeek : '[ວັນ]dddd[ໜ້າເວລາ] LT',
+            lastDay : '[ມື້ວານນີ້ເວລາ] LT',
+            lastWeek : '[ວັນ]dddd[ແລ້ວນີ້ເວລາ] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'ອີກ %s',
+            past : '%sຜ່ານມາ',
+            s : 'ບໍ່ເທົ່າໃດວິນາທີ',
+            m : '1 ນາທີ',
+            mm : '%d ນາທີ',
+            h : '1 ຊົ່ວໂມງ',
+            hh : '%d ຊົ່ວໂມງ',
+            d : '1 ມື້',
+            dd : '%d ມື້',
+            M : '1 ເດືອນ',
+            MM : '%d ເດືອນ',
+            y : '1 ປີ',
+            yy : '%d ປີ'
+        },
+        ordinalParse: /(ທີ່)\d{1,2}/,
+        ordinal : function (number) {
+            return 'ທີ່' + number;
+        }
+    });
+
+    return lo;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lt.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lt.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lt.js	(revision 12265)
@@ -0,0 +1,117 @@
+//! moment.js locale configuration
+//! locale : Lithuanian [lt]
+//! author : Mindaugas Mozūras : https://github.com/mmozuras
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var units = {
+        'm' : 'minutė_minutės_minutę',
+        'mm': 'minutės_minučių_minutes',
+        'h' : 'valanda_valandos_valandą',
+        'hh': 'valandos_valandų_valandas',
+        'd' : 'diena_dienos_dieną',
+        'dd': 'dienos_dienų_dienas',
+        'M' : 'mėnuo_mėnesio_mėnesį',
+        'MM': 'mėnesiai_mėnesių_mėnesius',
+        'y' : 'metai_metų_metus',
+        'yy': 'metai_metų_metus'
+    };
+    function translateSeconds(number, withoutSuffix, key, isFuture) {
+        if (withoutSuffix) {
+            return 'kelios sekundės';
+        } else {
+            return isFuture ? 'kelių sekundžių' : 'kelias sekundes';
+        }
+    }
+    function translateSingular(number, withoutSuffix, key, isFuture) {
+        return withoutSuffix ? forms(key)[0] : (isFuture ? forms(key)[1] : forms(key)[2]);
+    }
+    function special(number) {
+        return number % 10 === 0 || (number > 10 && number < 20);
+    }
+    function forms(key) {
+        return units[key].split('_');
+    }
+    function translate(number, withoutSuffix, key, isFuture) {
+        var result = number + ' ';
+        if (number === 1) {
+            return result + translateSingular(number, withoutSuffix, key[0], isFuture);
+        } else if (withoutSuffix) {
+            return result + (special(number) ? forms(key)[1] : forms(key)[0]);
+        } else {
+            if (isFuture) {
+                return result + forms(key)[1];
+            } else {
+                return result + (special(number) ? forms(key)[1] : forms(key)[2]);
+            }
+        }
+    }
+    var lt = moment.defineLocale('lt', {
+        months : {
+            format: 'sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio'.split('_'),
+            standalone: 'sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis'.split('_'),
+            isFormat: /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?|MMMM?(\[[^\[\]]*\]|\s+)+D[oD]?/
+        },
+        monthsShort : 'sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd'.split('_'),
+        weekdays : {
+            format: 'sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį'.split('_'),
+            standalone: 'sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis'.split('_'),
+            isFormat: /dddd HH:mm/
+        },
+        weekdaysShort : 'Sek_Pir_Ant_Tre_Ket_Pen_Šeš'.split('_'),
+        weekdaysMin : 'S_P_A_T_K_Pn_Š'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'YYYY-MM-DD',
+            LL : 'YYYY [m.] MMMM D [d.]',
+            LLL : 'YYYY [m.] MMMM D [d.], HH:mm [val.]',
+            LLLL : 'YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]',
+            l : 'YYYY-MM-DD',
+            ll : 'YYYY [m.] MMMM D [d.]',
+            lll : 'YYYY [m.] MMMM D [d.], HH:mm [val.]',
+            llll : 'YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]'
+        },
+        calendar : {
+            sameDay : '[Šiandien] LT',
+            nextDay : '[Rytoj] LT',
+            nextWeek : 'dddd LT',
+            lastDay : '[Vakar] LT',
+            lastWeek : '[Praėjusį] dddd LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'po %s',
+            past : 'prieš %s',
+            s : translateSeconds,
+            m : translateSingular,
+            mm : translate,
+            h : translateSingular,
+            hh : translate,
+            d : translateSingular,
+            dd : translate,
+            M : translateSingular,
+            MM : translate,
+            y : translateSingular,
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}-oji/,
+        ordinal : function (number) {
+            return number + '-oji';
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return lt;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lv.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lv.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/lv.js	(revision 12265)
@@ -0,0 +1,97 @@
+//! moment.js locale configuration
+//! locale : Latvian [lv]
+//! author : Kristaps Karlsons : https://github.com/skakri
+//! author : Jānis Elmeris : https://github.com/JanisE
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var units = {
+        'm': 'minūtes_minūtēm_minūte_minūtes'.split('_'),
+        'mm': 'minūtes_minūtēm_minūte_minūtes'.split('_'),
+        'h': 'stundas_stundām_stunda_stundas'.split('_'),
+        'hh': 'stundas_stundām_stunda_stundas'.split('_'),
+        'd': 'dienas_dienām_diena_dienas'.split('_'),
+        'dd': 'dienas_dienām_diena_dienas'.split('_'),
+        'M': 'mēneša_mēnešiem_mēnesis_mēneši'.split('_'),
+        'MM': 'mēneša_mēnešiem_mēnesis_mēneši'.split('_'),
+        'y': 'gada_gadiem_gads_gadi'.split('_'),
+        'yy': 'gada_gadiem_gads_gadi'.split('_')
+    };
+    /**
+     * @param withoutSuffix boolean true = a length of time; false = before/after a period of time.
+     */
+    function format(forms, number, withoutSuffix) {
+        if (withoutSuffix) {
+            // E.g. "21 minūte", "3 minūtes".
+            return number % 10 === 1 && number % 100 !== 11 ? forms[2] : forms[3];
+        } else {
+            // E.g. "21 minūtes" as in "pēc 21 minūtes".
+            // E.g. "3 minūtēm" as in "pēc 3 minūtēm".
+            return number % 10 === 1 && number % 100 !== 11 ? forms[0] : forms[1];
+        }
+    }
+    function relativeTimeWithPlural(number, withoutSuffix, key) {
+        return number + ' ' + format(units[key], number, withoutSuffix);
+    }
+    function relativeTimeWithSingular(number, withoutSuffix, key) {
+        return format(units[key], number, withoutSuffix);
+    }
+    function relativeSeconds(number, withoutSuffix) {
+        return withoutSuffix ? 'dažas sekundes' : 'dažām sekundēm';
+    }
+
+    var lv = moment.defineLocale('lv', {
+        months : 'janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec'.split('_'),
+        weekdays : 'svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena'.split('_'),
+        weekdaysShort : 'Sv_P_O_T_C_Pk_S'.split('_'),
+        weekdaysMin : 'Sv_P_O_T_C_Pk_S'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY.',
+            LL : 'YYYY. [gada] D. MMMM',
+            LLL : 'YYYY. [gada] D. MMMM, HH:mm',
+            LLLL : 'YYYY. [gada] D. MMMM, dddd, HH:mm'
+        },
+        calendar : {
+            sameDay : '[Šodien pulksten] LT',
+            nextDay : '[Rīt pulksten] LT',
+            nextWeek : 'dddd [pulksten] LT',
+            lastDay : '[Vakar pulksten] LT',
+            lastWeek : '[Pagājušā] dddd [pulksten] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'pēc %s',
+            past : 'pirms %s',
+            s : relativeSeconds,
+            m : relativeTimeWithSingular,
+            mm : relativeTimeWithPlural,
+            h : relativeTimeWithSingular,
+            hh : relativeTimeWithPlural,
+            d : relativeTimeWithSingular,
+            dd : relativeTimeWithPlural,
+            M : relativeTimeWithSingular,
+            MM : relativeTimeWithPlural,
+            y : relativeTimeWithSingular,
+            yy : relativeTimeWithPlural
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return lv;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/me.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/me.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/me.js	(revision 12265)
@@ -0,0 +1,111 @@
+//! moment.js locale configuration
+//! locale : Montenegrin [me]
+//! author : Miodrag Nikač <miodrag@restartit.me> : https://github.com/miodragnikac
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var translator = {
+        words: { //Different grammatical cases
+            m: ['jedan minut', 'jednog minuta'],
+            mm: ['minut', 'minuta', 'minuta'],
+            h: ['jedan sat', 'jednog sata'],
+            hh: ['sat', 'sata', 'sati'],
+            dd: ['dan', 'dana', 'dana'],
+            MM: ['mjesec', 'mjeseca', 'mjeseci'],
+            yy: ['godina', 'godine', 'godina']
+        },
+        correctGrammaticalCase: function (number, wordKey) {
+            return number === 1 ? wordKey[0] : (number >= 2 && number <= 4 ? wordKey[1] : wordKey[2]);
+        },
+        translate: function (number, withoutSuffix, key) {
+            var wordKey = translator.words[key];
+            if (key.length === 1) {
+                return withoutSuffix ? wordKey[0] : wordKey[1];
+            } else {
+                return number + ' ' + translator.correctGrammaticalCase(number, wordKey);
+            }
+        }
+    };
+
+    var me = moment.defineLocale('me', {
+        months: 'januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar'.split('_'),
+        monthsShort: 'jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.'.split('_'),
+        monthsParseExact : true,
+        weekdays: 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'),
+        weekdaysShort: 'ned._pon._uto._sri._čet._pet._sub.'.split('_'),
+        weekdaysMin: 'ne_po_ut_sr_če_pe_su'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat: {
+            LT: 'H:mm',
+            LTS : 'H:mm:ss',
+            L: 'DD.MM.YYYY',
+            LL: 'D. MMMM YYYY',
+            LLL: 'D. MMMM YYYY H:mm',
+            LLLL: 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar: {
+            sameDay: '[danas u] LT',
+            nextDay: '[sjutra u] LT',
+
+            nextWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[u] [nedjelju] [u] LT';
+                    case 3:
+                        return '[u] [srijedu] [u] LT';
+                    case 6:
+                        return '[u] [subotu] [u] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[u] dddd [u] LT';
+                }
+            },
+            lastDay  : '[juče u] LT',
+            lastWeek : function () {
+                var lastWeekDays = [
+                    '[prošle] [nedjelje] [u] LT',
+                    '[prošlog] [ponedjeljka] [u] LT',
+                    '[prošlog] [utorka] [u] LT',
+                    '[prošle] [srijede] [u] LT',
+                    '[prošlog] [četvrtka] [u] LT',
+                    '[prošlog] [petka] [u] LT',
+                    '[prošle] [subote] [u] LT'
+                ];
+                return lastWeekDays[this.day()];
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past   : 'prije %s',
+            s      : 'nekoliko sekundi',
+            m      : translator.translate,
+            mm     : translator.translate,
+            h      : translator.translate,
+            hh     : translator.translate,
+            d      : 'dan',
+            dd     : translator.translate,
+            M      : 'mjesec',
+            MM     : translator.translate,
+            y      : 'godinu',
+            yy     : translator.translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return me;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mi.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mi.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mi.js	(revision 12265)
@@ -0,0 +1,64 @@
+//! moment.js locale configuration
+//! locale : Maori [mi]
+//! author : John Corrigan <robbiecloset@gmail.com> : https://github.com/johnideal
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var mi = moment.defineLocale('mi', {
+        months: 'Kohi-tāte_Hui-tanguru_Poutū-te-rangi_Paenga-whāwhā_Haratua_Pipiri_Hōngoingoi_Here-turi-kōkā_Mahuru_Whiringa-ā-nuku_Whiringa-ā-rangi_Hakihea'.split('_'),
+        monthsShort: 'Kohi_Hui_Pou_Pae_Hara_Pipi_Hōngoi_Here_Mahu_Whi-nu_Whi-ra_Haki'.split('_'),
+        monthsRegex: /(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,
+        monthsStrictRegex: /(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,
+        monthsShortRegex: /(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,
+        monthsShortStrictRegex: /(?:['a-z\u0101\u014D\u016B]+\-?){1,2}/i,
+        weekdays: 'Rātapu_Mane_Tūrei_Wenerei_Tāite_Paraire_Hātarei'.split('_'),
+        weekdaysShort: 'Ta_Ma_Tū_We_Tāi_Pa_Hā'.split('_'),
+        weekdaysMin: 'Ta_Ma_Tū_We_Tāi_Pa_Hā'.split('_'),
+        longDateFormat: {
+            LT: 'HH:mm',
+            LTS: 'HH:mm:ss',
+            L: 'DD/MM/YYYY',
+            LL: 'D MMMM YYYY',
+            LLL: 'D MMMM YYYY [i] HH:mm',
+            LLLL: 'dddd, D MMMM YYYY [i] HH:mm'
+        },
+        calendar: {
+            sameDay: '[i teie mahana, i] LT',
+            nextDay: '[apopo i] LT',
+            nextWeek: 'dddd [i] LT',
+            lastDay: '[inanahi i] LT',
+            lastWeek: 'dddd [whakamutunga i] LT',
+            sameElse: 'L'
+        },
+        relativeTime: {
+            future: 'i roto i %s',
+            past: '%s i mua',
+            s: 'te hēkona ruarua',
+            m: 'he meneti',
+            mm: '%d meneti',
+            h: 'te haora',
+            hh: '%d haora',
+            d: 'he ra',
+            dd: '%d ra',
+            M: 'he marama',
+            MM: '%d marama',
+            y: 'he tau',
+            yy: '%d tau'
+        },
+        ordinalParse: /\d{1,2}º/,
+        ordinal: '%dº',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return mi;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mk.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mk.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mk.js	(revision 12265)
@@ -0,0 +1,90 @@
+//! moment.js locale configuration
+//! locale : Macedonian [mk]
+//! author : Borislav Mickov : https://github.com/B0k0
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var mk = moment.defineLocale('mk', {
+        months : 'јануари_февруари_март_април_мај_јуни_јули_август_септември_октомври_ноември_декември'.split('_'),
+        monthsShort : 'јан_фев_мар_апр_мај_јун_јул_авг_сеп_окт_ное_дек'.split('_'),
+        weekdays : 'недела_понеделник_вторник_среда_четврток_петок_сабота'.split('_'),
+        weekdaysShort : 'нед_пон_вто_сре_чет_пет_саб'.split('_'),
+        weekdaysMin : 'нe_пo_вт_ср_че_пе_сa'.split('_'),
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'D.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY H:mm',
+            LLLL : 'dddd, D MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay : '[Денес во] LT',
+            nextDay : '[Утре во] LT',
+            nextWeek : '[Во] dddd [во] LT',
+            lastDay : '[Вчера во] LT',
+            lastWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                    case 3:
+                    case 6:
+                        return '[Изминатата] dddd [во] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[Изминатиот] dddd [во] LT';
+                }
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'после %s',
+            past : 'пред %s',
+            s : 'неколку секунди',
+            m : 'минута',
+            mm : '%d минути',
+            h : 'час',
+            hh : '%d часа',
+            d : 'ден',
+            dd : '%d дена',
+            M : 'месец',
+            MM : '%d месеци',
+            y : 'година',
+            yy : '%d години'
+        },
+        ordinalParse: /\d{1,2}-(ев|ен|ти|ви|ри|ми)/,
+        ordinal : function (number) {
+            var lastDigit = number % 10,
+                last2Digits = number % 100;
+            if (number === 0) {
+                return number + '-ев';
+            } else if (last2Digits === 0) {
+                return number + '-ен';
+            } else if (last2Digits > 10 && last2Digits < 20) {
+                return number + '-ти';
+            } else if (lastDigit === 1) {
+                return number + '-ви';
+            } else if (lastDigit === 2) {
+                return number + '-ри';
+            } else if (lastDigit === 7 || lastDigit === 8) {
+                return number + '-ми';
+            } else {
+                return number + '-ти';
+            }
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return mk;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ml.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ml.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ml.js	(revision 12265)
@@ -0,0 +1,81 @@
+//! moment.js locale configuration
+//! locale : Malayalam [ml]
+//! author : Floyd Pink : https://github.com/floydpink
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ml = moment.defineLocale('ml', {
+        months : 'ജനുവരി_ഫെബ്രുവരി_മാർച്ച്_ഏപ്രിൽ_മേയ്_ജൂൺ_ജൂലൈ_ഓഗസ്റ്റ്_സെപ്റ്റംബർ_ഒക്ടോബർ_നവംബർ_ഡിസംബർ'.split('_'),
+        monthsShort : 'ജനു._ഫെബ്രു._മാർ._ഏപ്രി._മേയ്_ജൂൺ_ജൂലൈ._ഓഗ._സെപ്റ്റ._ഒക്ടോ._നവം._ഡിസം.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'ഞായറാഴ്ച_തിങ്കളാഴ്ച_ചൊവ്വാഴ്ച_ബുധനാഴ്ച_വ്യാഴാഴ്ച_വെള്ളിയാഴ്ച_ശനിയാഴ്ച'.split('_'),
+        weekdaysShort : 'ഞായർ_തിങ്കൾ_ചൊവ്വ_ബുധൻ_വ്യാഴം_വെള്ളി_ശനി'.split('_'),
+        weekdaysMin : 'ഞാ_തി_ചൊ_ബു_വ്യാ_വെ_ശ'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm -നു',
+            LTS : 'A h:mm:ss -നു',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm -നു',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm -നു'
+        },
+        calendar : {
+            sameDay : '[ഇന്ന്] LT',
+            nextDay : '[നാളെ] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[ഇന്നലെ] LT',
+            lastWeek : '[കഴിഞ്ഞ] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s കഴിഞ്ഞ്',
+            past : '%s മുൻപ്',
+            s : 'അൽപ നിമിഷങ്ങൾ',
+            m : 'ഒരു മിനിറ്റ്',
+            mm : '%d മിനിറ്റ്',
+            h : 'ഒരു മണിക്കൂർ',
+            hh : '%d മണിക്കൂർ',
+            d : 'ഒരു ദിവസം',
+            dd : '%d ദിവസം',
+            M : 'ഒരു മാസം',
+            MM : '%d മാസം',
+            y : 'ഒരു വർഷം',
+            yy : '%d വർഷം'
+        },
+        meridiemParse: /രാത്രി|രാവിലെ|ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി/i,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if ((meridiem === 'രാത്രി' && hour >= 4) ||
+                    meridiem === 'ഉച്ച കഴിഞ്ഞ്' ||
+                    meridiem === 'വൈകുന്നേരം') {
+                return hour + 12;
+            } else {
+                return hour;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'രാത്രി';
+            } else if (hour < 12) {
+                return 'രാവിലെ';
+            } else if (hour < 17) {
+                return 'ഉച്ച കഴിഞ്ഞ്';
+            } else if (hour < 20) {
+                return 'വൈകുന്നേരം';
+            } else {
+                return 'രാത്രി';
+            }
+        }
+    });
+
+    return ml;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mr.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mr.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/mr.js	(revision 12265)
@@ -0,0 +1,159 @@
+//! moment.js locale configuration
+//! locale : Marathi [mr]
+//! author : Harshad Kale : https://github.com/kalehv
+//! author : Vivek Athalye : https://github.com/vnathalye
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '१',
+        '2': '२',
+        '3': '३',
+        '4': '४',
+        '5': '५',
+        '6': '६',
+        '7': '७',
+        '8': '८',
+        '9': '९',
+        '0': '०'
+    },
+    numberMap = {
+        '१': '1',
+        '२': '2',
+        '३': '3',
+        '४': '4',
+        '५': '5',
+        '६': '6',
+        '७': '7',
+        '८': '8',
+        '९': '9',
+        '०': '0'
+    };
+
+    function relativeTimeMr(number, withoutSuffix, string, isFuture)
+    {
+        var output = '';
+        if (withoutSuffix) {
+            switch (string) {
+                case 's': output = 'काही सेकंद'; break;
+                case 'm': output = 'एक मिनिट'; break;
+                case 'mm': output = '%d मिनिटे'; break;
+                case 'h': output = 'एक तास'; break;
+                case 'hh': output = '%d तास'; break;
+                case 'd': output = 'एक दिवस'; break;
+                case 'dd': output = '%d दिवस'; break;
+                case 'M': output = 'एक महिना'; break;
+                case 'MM': output = '%d महिने'; break;
+                case 'y': output = 'एक वर्ष'; break;
+                case 'yy': output = '%d वर्षे'; break;
+            }
+        }
+        else {
+            switch (string) {
+                case 's': output = 'काही सेकंदां'; break;
+                case 'm': output = 'एका मिनिटा'; break;
+                case 'mm': output = '%d मिनिटां'; break;
+                case 'h': output = 'एका तासा'; break;
+                case 'hh': output = '%d तासां'; break;
+                case 'd': output = 'एका दिवसा'; break;
+                case 'dd': output = '%d दिवसां'; break;
+                case 'M': output = 'एका महिन्या'; break;
+                case 'MM': output = '%d महिन्यां'; break;
+                case 'y': output = 'एका वर्षा'; break;
+                case 'yy': output = '%d वर्षां'; break;
+            }
+        }
+        return output.replace(/%d/i, number);
+    }
+
+    var mr = moment.defineLocale('mr', {
+        months : 'जानेवारी_फेब्रुवारी_मार्च_एप्रिल_मे_जून_जुलै_ऑगस्ट_सप्टेंबर_ऑक्टोबर_नोव्हेंबर_डिसेंबर'.split('_'),
+        monthsShort: 'जाने._फेब्रु._मार्च._एप्रि._मे._जून._जुलै._ऑग._सप्टें._ऑक्टो._नोव्हें._डिसें.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'रविवार_सोमवार_मंगळवार_बुधवार_गुरूवार_शुक्रवार_शनिवार'.split('_'),
+        weekdaysShort : 'रवि_सोम_मंगळ_बुध_गुरू_शुक्र_शनि'.split('_'),
+        weekdaysMin : 'र_सो_मं_बु_गु_शु_श'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm वाजता',
+            LTS : 'A h:mm:ss वाजता',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm वाजता',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm वाजता'
+        },
+        calendar : {
+            sameDay : '[आज] LT',
+            nextDay : '[उद्या] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[काल] LT',
+            lastWeek: '[मागील] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future: '%sमध्ये',
+            past: '%sपूर्वी',
+            s: relativeTimeMr,
+            m: relativeTimeMr,
+            mm: relativeTimeMr,
+            h: relativeTimeMr,
+            hh: relativeTimeMr,
+            d: relativeTimeMr,
+            dd: relativeTimeMr,
+            M: relativeTimeMr,
+            MM: relativeTimeMr,
+            y: relativeTimeMr,
+            yy: relativeTimeMr
+        },
+        preparse: function (string) {
+            return string.replace(/[१२३४५६७८९०]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        meridiemParse: /रात्री|सकाळी|दुपारी|सायंकाळी/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'रात्री') {
+                return hour < 4 ? hour : hour + 12;
+            } else if (meridiem === 'सकाळी') {
+                return hour;
+            } else if (meridiem === 'दुपारी') {
+                return hour >= 10 ? hour : hour + 12;
+            } else if (meridiem === 'सायंकाळी') {
+                return hour + 12;
+            }
+        },
+        meridiem: function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'रात्री';
+            } else if (hour < 10) {
+                return 'सकाळी';
+            } else if (hour < 17) {
+                return 'दुपारी';
+            } else if (hour < 20) {
+                return 'सायंकाळी';
+            } else {
+                return 'रात्री';
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return mr;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ms-my.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ms-my.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ms-my.js	(revision 12265)
@@ -0,0 +1,83 @@
+//! moment.js locale configuration
+//! locale : Malay [ms-my]
+//! note : DEPRECATED, the correct one is [ms]
+//! author : Weldan Jamili : https://github.com/weldan
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ms_my = moment.defineLocale('ms-my', {
+        months : 'Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember'.split('_'),
+        monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis'.split('_'),
+        weekdays : 'Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu'.split('_'),
+        weekdaysShort : 'Ahd_Isn_Sel_Rab_Kha_Jum_Sab'.split('_'),
+        weekdaysMin : 'Ah_Is_Sl_Rb_Km_Jm_Sb'.split('_'),
+        longDateFormat : {
+            LT : 'HH.mm',
+            LTS : 'HH.mm.ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY [pukul] HH.mm',
+            LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm'
+        },
+        meridiemParse: /pagi|tengahari|petang|malam/,
+        meridiemHour: function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'pagi') {
+                return hour;
+            } else if (meridiem === 'tengahari') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === 'petang' || meridiem === 'malam') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 11) {
+                return 'pagi';
+            } else if (hours < 15) {
+                return 'tengahari';
+            } else if (hours < 19) {
+                return 'petang';
+            } else {
+                return 'malam';
+            }
+        },
+        calendar : {
+            sameDay : '[Hari ini pukul] LT',
+            nextDay : '[Esok pukul] LT',
+            nextWeek : 'dddd [pukul] LT',
+            lastDay : '[Kelmarin pukul] LT',
+            lastWeek : 'dddd [lepas pukul] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'dalam %s',
+            past : '%s yang lepas',
+            s : 'beberapa saat',
+            m : 'seminit',
+            mm : '%d minit',
+            h : 'sejam',
+            hh : '%d jam',
+            d : 'sehari',
+            dd : '%d hari',
+            M : 'sebulan',
+            MM : '%d bulan',
+            y : 'setahun',
+            yy : '%d tahun'
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ms_my;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ms.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ms.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ms.js	(revision 12265)
@@ -0,0 +1,82 @@
+//! moment.js locale configuration
+//! locale : Malay [ms]
+//! author : Weldan Jamili : https://github.com/weldan
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var ms = moment.defineLocale('ms', {
+        months : 'Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember'.split('_'),
+        monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis'.split('_'),
+        weekdays : 'Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu'.split('_'),
+        weekdaysShort : 'Ahd_Isn_Sel_Rab_Kha_Jum_Sab'.split('_'),
+        weekdaysMin : 'Ah_Is_Sl_Rb_Km_Jm_Sb'.split('_'),
+        longDateFormat : {
+            LT : 'HH.mm',
+            LTS : 'HH.mm.ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY [pukul] HH.mm',
+            LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm'
+        },
+        meridiemParse: /pagi|tengahari|petang|malam/,
+        meridiemHour: function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'pagi') {
+                return hour;
+            } else if (meridiem === 'tengahari') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === 'petang' || meridiem === 'malam') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 11) {
+                return 'pagi';
+            } else if (hours < 15) {
+                return 'tengahari';
+            } else if (hours < 19) {
+                return 'petang';
+            } else {
+                return 'malam';
+            }
+        },
+        calendar : {
+            sameDay : '[Hari ini pukul] LT',
+            nextDay : '[Esok pukul] LT',
+            nextWeek : 'dddd [pukul] LT',
+            lastDay : '[Kelmarin pukul] LT',
+            lastWeek : 'dddd [lepas pukul] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'dalam %s',
+            past : '%s yang lepas',
+            s : 'beberapa saat',
+            m : 'seminit',
+            mm : '%d minit',
+            h : 'sejam',
+            hh : '%d jam',
+            d : 'sehari',
+            dd : '%d hari',
+            M : 'sebulan',
+            MM : '%d bulan',
+            y : 'setahun',
+            yy : '%d tahun'
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ms;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/my.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/my.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/my.js	(revision 12265)
@@ -0,0 +1,95 @@
+//! moment.js locale configuration
+//! locale : Burmese [my]
+//! author : Squar team, mysquar.com
+//! author : David Rossellat : https://github.com/gholadr
+//! author : Tin Aung Lin : https://github.com/thanyawzinmin
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '၁',
+        '2': '၂',
+        '3': '၃',
+        '4': '၄',
+        '5': '၅',
+        '6': '၆',
+        '7': '၇',
+        '8': '၈',
+        '9': '၉',
+        '0': '၀'
+    }, numberMap = {
+        '၁': '1',
+        '၂': '2',
+        '၃': '3',
+        '၄': '4',
+        '၅': '5',
+        '၆': '6',
+        '၇': '7',
+        '၈': '8',
+        '၉': '9',
+        '၀': '0'
+    };
+
+    var my = moment.defineLocale('my', {
+        months: 'ဇန်နဝါရီ_ဖေဖော်ဝါရီ_မတ်_ဧပြီ_မေ_ဇွန်_ဇူလိုင်_သြဂုတ်_စက်တင်ဘာ_အောက်တိုဘာ_နိုဝင်ဘာ_ဒီဇင်ဘာ'.split('_'),
+        monthsShort: 'ဇန်_ဖေ_မတ်_ပြီ_မေ_ဇွန်_လိုင်_သြ_စက်_အောက်_နို_ဒီ'.split('_'),
+        weekdays: 'တနင်္ဂနွေ_တနင်္လာ_အင်္ဂါ_ဗုဒ္ဓဟူး_ကြာသပတေး_သောကြာ_စနေ'.split('_'),
+        weekdaysShort: 'နွေ_လာ_ဂါ_ဟူး_ကြာ_သော_နေ'.split('_'),
+        weekdaysMin: 'နွေ_လာ_ဂါ_ဟူး_ကြာ_သော_နေ'.split('_'),
+
+        longDateFormat: {
+            LT: 'HH:mm',
+            LTS: 'HH:mm:ss',
+            L: 'DD/MM/YYYY',
+            LL: 'D MMMM YYYY',
+            LLL: 'D MMMM YYYY HH:mm',
+            LLLL: 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar: {
+            sameDay: '[ယနေ.] LT [မှာ]',
+            nextDay: '[မနက်ဖြန်] LT [မှာ]',
+            nextWeek: 'dddd LT [မှာ]',
+            lastDay: '[မနေ.က] LT [မှာ]',
+            lastWeek: '[ပြီးခဲ့သော] dddd LT [မှာ]',
+            sameElse: 'L'
+        },
+        relativeTime: {
+            future: 'လာမည့် %s မှာ',
+            past: 'လွန်ခဲ့သော %s က',
+            s: 'စက္ကန်.အနည်းငယ်',
+            m: 'တစ်မိနစ်',
+            mm: '%d မိနစ်',
+            h: 'တစ်နာရီ',
+            hh: '%d နာရီ',
+            d: 'တစ်ရက်',
+            dd: '%d ရက်',
+            M: 'တစ်လ',
+            MM: '%d လ',
+            y: 'တစ်နှစ်',
+            yy: '%d နှစ်'
+        },
+        preparse: function (string) {
+            return string.replace(/[၁၂၃၄၅၆၇၈၉၀]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        week: {
+            dow: 1, // Monday is the first day of the week.
+            doy: 4 // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return my;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nb.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nb.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nb.js	(revision 12265)
@@ -0,0 +1,63 @@
+//! moment.js locale configuration
+//! locale : Norwegian Bokmål [nb]
+//! authors : Espen Hovlandsdal : https://github.com/rexxars
+//!           Sigurd Gartmann : https://github.com/sigurdga
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var nb = moment.defineLocale('nb', {
+        months : 'januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember'.split('_'),
+        monthsShort : 'jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag'.split('_'),
+        weekdaysShort : 'sø._ma._ti._on._to._fr._lø.'.split('_'),
+        weekdaysMin : 'sø_ma_ti_on_to_fr_lø'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY [kl.] HH:mm',
+            LLLL : 'dddd D. MMMM YYYY [kl.] HH:mm'
+        },
+        calendar : {
+            sameDay: '[i dag kl.] LT',
+            nextDay: '[i morgen kl.] LT',
+            nextWeek: 'dddd [kl.] LT',
+            lastDay: '[i går kl.] LT',
+            lastWeek: '[forrige] dddd [kl.] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'om %s',
+            past : '%s siden',
+            s : 'noen sekunder',
+            m : 'ett minutt',
+            mm : '%d minutter',
+            h : 'en time',
+            hh : '%d timer',
+            d : 'en dag',
+            dd : '%d dager',
+            M : 'en måned',
+            MM : '%d måneder',
+            y : 'ett år',
+            yy : '%d år'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return nb;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ne.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ne.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ne.js	(revision 12265)
@@ -0,0 +1,123 @@
+//! moment.js locale configuration
+//! locale : Nepalese [ne]
+//! author : suvash : https://github.com/suvash
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '१',
+        '2': '२',
+        '3': '३',
+        '4': '४',
+        '5': '५',
+        '6': '६',
+        '7': '७',
+        '8': '८',
+        '9': '९',
+        '0': '०'
+    },
+    numberMap = {
+        '१': '1',
+        '२': '2',
+        '३': '3',
+        '४': '4',
+        '५': '5',
+        '६': '6',
+        '७': '7',
+        '८': '8',
+        '९': '9',
+        '०': '0'
+    };
+
+    var ne = moment.defineLocale('ne', {
+        months : 'जनवरी_फेब्रुवरी_मार्च_अप्रिल_मई_जुन_जुलाई_अगष्ट_सेप्टेम्बर_अक्टोबर_नोभेम्बर_डिसेम्बर'.split('_'),
+        monthsShort : 'जन._फेब्रु._मार्च_अप्रि._मई_जुन_जुलाई._अग._सेप्ट._अक्टो._नोभे._डिसे.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'आइतबार_सोमबार_मङ्गलबार_बुधबार_बिहिबार_शुक्रबार_शनिबार'.split('_'),
+        weekdaysShort : 'आइत._सोम._मङ्गल._बुध._बिहि._शुक्र._शनि.'.split('_'),
+        weekdaysMin : 'आ._सो._मं._बु._बि._शु._श.'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'Aको h:mm बजे',
+            LTS : 'Aको h:mm:ss बजे',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, Aको h:mm बजे',
+            LLLL : 'dddd, D MMMM YYYY, Aको h:mm बजे'
+        },
+        preparse: function (string) {
+            return string.replace(/[१२३४५६७८९०]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        meridiemParse: /राति|बिहान|दिउँसो|साँझ/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'राति') {
+                return hour < 4 ? hour : hour + 12;
+            } else if (meridiem === 'बिहान') {
+                return hour;
+            } else if (meridiem === 'दिउँसो') {
+                return hour >= 10 ? hour : hour + 12;
+            } else if (meridiem === 'साँझ') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 3) {
+                return 'राति';
+            } else if (hour < 12) {
+                return 'बिहान';
+            } else if (hour < 16) {
+                return 'दिउँसो';
+            } else if (hour < 20) {
+                return 'साँझ';
+            } else {
+                return 'राति';
+            }
+        },
+        calendar : {
+            sameDay : '[आज] LT',
+            nextDay : '[भोलि] LT',
+            nextWeek : '[आउँदो] dddd[,] LT',
+            lastDay : '[हिजो] LT',
+            lastWeek : '[गएको] dddd[,] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%sमा',
+            past : '%s अगाडि',
+            s : 'केही क्षण',
+            m : 'एक मिनेट',
+            mm : '%d मिनेट',
+            h : 'एक घण्टा',
+            hh : '%d घण्टा',
+            d : 'एक दिन',
+            dd : '%d दिन',
+            M : 'एक महिना',
+            MM : '%d महिना',
+            y : 'एक बर्ष',
+            yy : '%d बर्ष'
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ne;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nl.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nl.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nl.js	(revision 12265)
@@ -0,0 +1,86 @@
+//! moment.js locale configuration
+//! locale : Dutch [nl]
+//! author : Joris Röling : https://github.com/jorisroling
+//! author : Jacob Middag : https://github.com/middagj
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var monthsShortWithDots = 'jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.'.split('_'),
+        monthsShortWithoutDots = 'jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec'.split('_');
+
+    var monthsParse = [/^jan/i, /^feb/i, /^maart|mrt.?$/i, /^apr/i, /^mei$/i, /^jun[i.]?$/i, /^jul[i.]?$/i, /^aug/i, /^sep/i, /^okt/i, /^nov/i, /^dec/i];
+    var monthsRegex = /^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;
+
+    var nl = moment.defineLocale('nl', {
+        months : 'januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december'.split('_'),
+        monthsShort : function (m, format) {
+            if (/-MMM-/.test(format)) {
+                return monthsShortWithoutDots[m.month()];
+            } else {
+                return monthsShortWithDots[m.month()];
+            }
+        },
+
+        monthsRegex: monthsRegex,
+        monthsShortRegex: monthsRegex,
+        monthsStrictRegex: /^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,
+        monthsShortStrictRegex: /^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,
+
+        monthsParse : monthsParse,
+        longMonthsParse : monthsParse,
+        shortMonthsParse : monthsParse,
+
+        weekdays : 'zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag'.split('_'),
+        weekdaysShort : 'zo._ma._di._wo._do._vr._za.'.split('_'),
+        weekdaysMin : 'Zo_Ma_Di_Wo_Do_Vr_Za'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD-MM-YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[vandaag om] LT',
+            nextDay: '[morgen om] LT',
+            nextWeek: 'dddd [om] LT',
+            lastDay: '[gisteren om] LT',
+            lastWeek: '[afgelopen] dddd [om] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'over %s',
+            past : '%s geleden',
+            s : 'een paar seconden',
+            m : 'één minuut',
+            mm : '%d minuten',
+            h : 'één uur',
+            hh : '%d uur',
+            d : 'één dag',
+            dd : '%d dagen',
+            M : 'één maand',
+            MM : '%d maanden',
+            y : 'één jaar',
+            yy : '%d jaar'
+        },
+        ordinalParse: /\d{1,2}(ste|de)/,
+        ordinal : function (number) {
+            return number + ((number === 1 || number === 8 || number >= 20) ? 'ste' : 'de');
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return nl;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nn.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nn.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/nn.js	(revision 12265)
@@ -0,0 +1,60 @@
+//! moment.js locale configuration
+//! locale : Nynorsk [nn]
+//! author : https://github.com/mechuwind
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var nn = moment.defineLocale('nn', {
+        months : 'januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des'.split('_'),
+        weekdays : 'sundag_måndag_tysdag_onsdag_torsdag_fredag_laurdag'.split('_'),
+        weekdaysShort : 'sun_mån_tys_ons_tor_fre_lau'.split('_'),
+        weekdaysMin : 'su_må_ty_on_to_fr_lø'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY [kl.] H:mm',
+            LLLL : 'dddd D. MMMM YYYY [kl.] HH:mm'
+        },
+        calendar : {
+            sameDay: '[I dag klokka] LT',
+            nextDay: '[I morgon klokka] LT',
+            nextWeek: 'dddd [klokka] LT',
+            lastDay: '[I går klokka] LT',
+            lastWeek: '[Føregåande] dddd [klokka] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'om %s',
+            past : '%s sidan',
+            s : 'nokre sekund',
+            m : 'eit minutt',
+            mm : '%d minutt',
+            h : 'ein time',
+            hh : '%d timar',
+            d : 'ein dag',
+            dd : '%d dagar',
+            M : 'ein månad',
+            MM : '%d månader',
+            y : 'eit år',
+            yy : '%d år'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return nn;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pa-in.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pa-in.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pa-in.js	(revision 12265)
@@ -0,0 +1,124 @@
+//! moment.js locale configuration
+//! locale : Punjabi (India) [pa-in]
+//! author : Harpreet Singh : https://github.com/harpreetkhalsagtbit
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '੧',
+        '2': '੨',
+        '3': '੩',
+        '4': '੪',
+        '5': '੫',
+        '6': '੬',
+        '7': '੭',
+        '8': '੮',
+        '9': '੯',
+        '0': '੦'
+    },
+    numberMap = {
+        '੧': '1',
+        '੨': '2',
+        '੩': '3',
+        '੪': '4',
+        '੫': '5',
+        '੬': '6',
+        '੭': '7',
+        '੮': '8',
+        '੯': '9',
+        '੦': '0'
+    };
+
+    var pa_in = moment.defineLocale('pa-in', {
+        // There are months name as per Nanakshahi Calender but they are not used as rigidly in modern Punjabi.
+        months : 'ਜਨਵਰੀ_ਫ਼ਰਵਰੀ_ਮਾਰਚ_ਅਪ੍ਰੈਲ_ਮਈ_ਜੂਨ_ਜੁਲਾਈ_ਅਗਸਤ_ਸਤੰਬਰ_ਅਕਤੂਬਰ_ਨਵੰਬਰ_ਦਸੰਬਰ'.split('_'),
+        monthsShort : 'ਜਨਵਰੀ_ਫ਼ਰਵਰੀ_ਮਾਰਚ_ਅਪ੍ਰੈਲ_ਮਈ_ਜੂਨ_ਜੁਲਾਈ_ਅਗਸਤ_ਸਤੰਬਰ_ਅਕਤੂਬਰ_ਨਵੰਬਰ_ਦਸੰਬਰ'.split('_'),
+        weekdays : 'ਐਤਵਾਰ_ਸੋਮਵਾਰ_ਮੰਗਲਵਾਰ_ਬੁਧਵਾਰ_ਵੀਰਵਾਰ_ਸ਼ੁੱਕਰਵਾਰ_ਸ਼ਨੀਚਰਵਾਰ'.split('_'),
+        weekdaysShort : 'ਐਤ_ਸੋਮ_ਮੰਗਲ_ਬੁਧ_ਵੀਰ_ਸ਼ੁਕਰ_ਸ਼ਨੀ'.split('_'),
+        weekdaysMin : 'ਐਤ_ਸੋਮ_ਮੰਗਲ_ਬੁਧ_ਵੀਰ_ਸ਼ੁਕਰ_ਸ਼ਨੀ'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm ਵਜੇ',
+            LTS : 'A h:mm:ss ਵਜੇ',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm ਵਜੇ',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm ਵਜੇ'
+        },
+        calendar : {
+            sameDay : '[ਅਜ] LT',
+            nextDay : '[ਕਲ] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[ਕਲ] LT',
+            lastWeek : '[ਪਿਛਲੇ] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s ਵਿੱਚ',
+            past : '%s ਪਿਛਲੇ',
+            s : 'ਕੁਝ ਸਕਿੰਟ',
+            m : 'ਇਕ ਮਿੰਟ',
+            mm : '%d ਮਿੰਟ',
+            h : 'ਇੱਕ ਘੰਟਾ',
+            hh : '%d ਘੰਟੇ',
+            d : 'ਇੱਕ ਦਿਨ',
+            dd : '%d ਦਿਨ',
+            M : 'ਇੱਕ ਮਹੀਨਾ',
+            MM : '%d ਮਹੀਨੇ',
+            y : 'ਇੱਕ ਸਾਲ',
+            yy : '%d ਸਾਲ'
+        },
+        preparse: function (string) {
+            return string.replace(/[੧੨੩੪੫੬੭੮੯੦]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        // Punjabi notation for meridiems are quite fuzzy in practice. While there exists
+        // a rigid notion of a 'Pahar' it is not used as rigidly in modern Punjabi.
+        meridiemParse: /ਰਾਤ|ਸਵੇਰ|ਦੁਪਹਿਰ|ਸ਼ਾਮ/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'ਰਾਤ') {
+                return hour < 4 ? hour : hour + 12;
+            } else if (meridiem === 'ਸਵੇਰ') {
+                return hour;
+            } else if (meridiem === 'ਦੁਪਹਿਰ') {
+                return hour >= 10 ? hour : hour + 12;
+            } else if (meridiem === 'ਸ਼ਾਮ') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'ਰਾਤ';
+            } else if (hour < 10) {
+                return 'ਸਵੇਰ';
+            } else if (hour < 17) {
+                return 'ਦੁਪਹਿਰ';
+            } else if (hour < 20) {
+                return 'ਸ਼ਾਮ';
+            } else {
+                return 'ਰਾਤ';
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return pa_in;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pl.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pl.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pl.js	(revision 12265)
@@ -0,0 +1,105 @@
+//! moment.js locale configuration
+//! locale : Polish [pl]
+//! author : Rafal Hirsz : https://github.com/evoL
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var monthsNominative = 'styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień'.split('_'),
+        monthsSubjective = 'stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia'.split('_');
+    function plural(n) {
+        return (n % 10 < 5) && (n % 10 > 1) && ((~~(n / 10) % 10) !== 1);
+    }
+    function translate(number, withoutSuffix, key) {
+        var result = number + ' ';
+        switch (key) {
+            case 'm':
+                return withoutSuffix ? 'minuta' : 'minutę';
+            case 'mm':
+                return result + (plural(number) ? 'minuty' : 'minut');
+            case 'h':
+                return withoutSuffix  ? 'godzina'  : 'godzinę';
+            case 'hh':
+                return result + (plural(number) ? 'godziny' : 'godzin');
+            case 'MM':
+                return result + (plural(number) ? 'miesiące' : 'miesięcy');
+            case 'yy':
+                return result + (plural(number) ? 'lata' : 'lat');
+        }
+    }
+
+    var pl = moment.defineLocale('pl', {
+        months : function (momentToFormat, format) {
+            if (format === '') {
+                // Hack: if format empty we know this is used to generate
+                // RegExp by moment. Give then back both valid forms of months
+                // in RegExp ready format.
+                return '(' + monthsSubjective[momentToFormat.month()] + '|' + monthsNominative[momentToFormat.month()] + ')';
+            } else if (/D MMMM/.test(format)) {
+                return monthsSubjective[momentToFormat.month()];
+            } else {
+                return monthsNominative[momentToFormat.month()];
+            }
+        },
+        monthsShort : 'sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru'.split('_'),
+        weekdays : 'niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota'.split('_'),
+        weekdaysShort : 'nie_pon_wt_śr_czw_pt_sb'.split('_'),
+        weekdaysMin : 'Nd_Pn_Wt_Śr_Cz_Pt_So'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Dziś o] LT',
+            nextDay: '[Jutro o] LT',
+            nextWeek: '[W] dddd [o] LT',
+            lastDay: '[Wczoraj o] LT',
+            lastWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[W zeszłą niedzielę o] LT';
+                    case 3:
+                        return '[W zeszłą środę o] LT';
+                    case 6:
+                        return '[W zeszłą sobotę o] LT';
+                    default:
+                        return '[W zeszły] dddd [o] LT';
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past : '%s temu',
+            s : 'kilka sekund',
+            m : translate,
+            mm : translate,
+            h : translate,
+            hh : translate,
+            d : '1 dzień',
+            dd : '%d dni',
+            M : 'miesiąc',
+            MM : translate,
+            y : 'rok',
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return pl;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pt-br.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pt-br.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pt-br.js	(revision 12265)
@@ -0,0 +1,61 @@
+//! moment.js locale configuration
+//! locale : Portuguese (Brazil) [pt-br]
+//! author : Caio Ribeiro Pereira : https://github.com/caio-ribeiro-pereira
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var pt_br = moment.defineLocale('pt-br', {
+        months : 'Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro'.split('_'),
+        monthsShort : 'Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez'.split('_'),
+        weekdays : 'Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado'.split('_'),
+        weekdaysShort : 'Dom_Seg_Ter_Qua_Qui_Sex_Sáb'.split('_'),
+        weekdaysMin : 'Dom_2ª_3ª_4ª_5ª_6ª_Sáb'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D [de] MMMM [de] YYYY',
+            LLL : 'D [de] MMMM [de] YYYY [às] HH:mm',
+            LLLL : 'dddd, D [de] MMMM [de] YYYY [às] HH:mm'
+        },
+        calendar : {
+            sameDay: '[Hoje às] LT',
+            nextDay: '[Amanhã às] LT',
+            nextWeek: 'dddd [às] LT',
+            lastDay: '[Ontem às] LT',
+            lastWeek: function () {
+                return (this.day() === 0 || this.day() === 6) ?
+                    '[Último] dddd [às] LT' : // Saturday + Sunday
+                    '[Última] dddd [às] LT'; // Monday - Friday
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'em %s',
+            past : '%s atrás',
+            s : 'poucos segundos',
+            m : 'um minuto',
+            mm : '%d minutos',
+            h : 'uma hora',
+            hh : '%d horas',
+            d : 'um dia',
+            dd : '%d dias',
+            M : 'um mês',
+            MM : '%d meses',
+            y : 'um ano',
+            yy : '%d anos'
+        },
+        ordinalParse: /\d{1,2}º/,
+        ordinal : '%dº'
+    });
+
+    return pt_br;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pt.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pt.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/pt.js	(revision 12265)
@@ -0,0 +1,65 @@
+//! moment.js locale configuration
+//! locale : Portuguese [pt]
+//! author : Jefferson : https://github.com/jalex79
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var pt = moment.defineLocale('pt', {
+        months : 'Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro'.split('_'),
+        monthsShort : 'Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez'.split('_'),
+        weekdays : 'Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado'.split('_'),
+        weekdaysShort : 'Dom_Seg_Ter_Qua_Qui_Sex_Sáb'.split('_'),
+        weekdaysMin : 'Dom_2ª_3ª_4ª_5ª_6ª_Sáb'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D [de] MMMM [de] YYYY',
+            LLL : 'D [de] MMMM [de] YYYY HH:mm',
+            LLLL : 'dddd, D [de] MMMM [de] YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Hoje às] LT',
+            nextDay: '[Amanhã às] LT',
+            nextWeek: 'dddd [às] LT',
+            lastDay: '[Ontem às] LT',
+            lastWeek: function () {
+                return (this.day() === 0 || this.day() === 6) ?
+                    '[Último] dddd [às] LT' : // Saturday + Sunday
+                    '[Última] dddd [às] LT'; // Monday - Friday
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'em %s',
+            past : 'há %s',
+            s : 'segundos',
+            m : 'um minuto',
+            mm : '%d minutos',
+            h : 'uma hora',
+            hh : '%d horas',
+            d : 'um dia',
+            dd : '%d dias',
+            M : 'um mês',
+            MM : '%d meses',
+            y : 'um ano',
+            yy : '%d anos'
+        },
+        ordinalParse: /\d{1,2}º/,
+        ordinal : '%dº',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return pt;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ro.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ro.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ro.js	(revision 12265)
@@ -0,0 +1,75 @@
+//! moment.js locale configuration
+//! locale : Romanian [ro]
+//! author : Vlad Gurdiga : https://github.com/gurdiga
+//! author : Valentin Agachi : https://github.com/avaly
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function relativeTimeWithPlural(number, withoutSuffix, key) {
+        var format = {
+                'mm': 'minute',
+                'hh': 'ore',
+                'dd': 'zile',
+                'MM': 'luni',
+                'yy': 'ani'
+            },
+            separator = ' ';
+        if (number % 100 >= 20 || (number >= 100 && number % 100 === 0)) {
+            separator = ' de ';
+        }
+        return number + separator + format[key];
+    }
+
+    var ro = moment.defineLocale('ro', {
+        months : 'ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie'.split('_'),
+        monthsShort : 'ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'duminică_luni_marți_miercuri_joi_vineri_sâmbătă'.split('_'),
+        weekdaysShort : 'Dum_Lun_Mar_Mie_Joi_Vin_Sâm'.split('_'),
+        weekdaysMin : 'Du_Lu_Ma_Mi_Jo_Vi_Sâ'.split('_'),
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY H:mm',
+            LLLL : 'dddd, D MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay: '[azi la] LT',
+            nextDay: '[mâine la] LT',
+            nextWeek: 'dddd [la] LT',
+            lastDay: '[ieri la] LT',
+            lastWeek: '[fosta] dddd [la] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'peste %s',
+            past : '%s în urmă',
+            s : 'câteva secunde',
+            m : 'un minut',
+            mm : relativeTimeWithPlural,
+            h : 'o oră',
+            hh : relativeTimeWithPlural,
+            d : 'o zi',
+            dd : relativeTimeWithPlural,
+            M : 'o lună',
+            MM : relativeTimeWithPlural,
+            y : 'un an',
+            yy : relativeTimeWithPlural
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ro;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ru.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ru.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ru.js	(revision 12265)
@@ -0,0 +1,183 @@
+//! moment.js locale configuration
+//! locale : Russian [ru]
+//! author : Viktorminator : https://github.com/Viktorminator
+//! Author : Menelion Elensúle : https://github.com/Oire
+//! author : Коренберг Марк : https://github.com/socketpair
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function plural(word, num) {
+        var forms = word.split('_');
+        return num % 10 === 1 && num % 100 !== 11 ? forms[0] : (num % 10 >= 2 && num % 10 <= 4 && (num % 100 < 10 || num % 100 >= 20) ? forms[1] : forms[2]);
+    }
+    function relativeTimeWithPlural(number, withoutSuffix, key) {
+        var format = {
+            'mm': withoutSuffix ? 'минута_минуты_минут' : 'минуту_минуты_минут',
+            'hh': 'час_часа_часов',
+            'dd': 'день_дня_дней',
+            'MM': 'месяц_месяца_месяцев',
+            'yy': 'год_года_лет'
+        };
+        if (key === 'm') {
+            return withoutSuffix ? 'минута' : 'минуту';
+        }
+        else {
+            return number + ' ' + plural(format[key], +number);
+        }
+    }
+    var monthsParse = [/^янв/i, /^фев/i, /^мар/i, /^апр/i, /^ма[йя]/i, /^июн/i, /^июл/i, /^авг/i, /^сен/i, /^окт/i, /^ноя/i, /^дек/i];
+
+    // http://new.gramota.ru/spravka/rules/139-prop : § 103
+    // Сокращения месяцев: http://new.gramota.ru/spravka/buro/search-answer?s=242637
+    // CLDR data:          http://www.unicode.org/cldr/charts/28/summary/ru.html#1753
+    var ru = moment.defineLocale('ru', {
+        months : {
+            format: 'января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря'.split('_'),
+            standalone: 'январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь'.split('_')
+        },
+        monthsShort : {
+            // по CLDR именно "июл." и "июн.", но какой смысл менять букву на точку ?
+            format: 'янв._февр._мар._апр._мая_июня_июля_авг._сент._окт._нояб._дек.'.split('_'),
+            standalone: 'янв._февр._март_апр._май_июнь_июль_авг._сент._окт._нояб._дек.'.split('_')
+        },
+        weekdays : {
+            standalone: 'воскресенье_понедельник_вторник_среда_четверг_пятница_суббота'.split('_'),
+            format: 'воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу'.split('_'),
+            isFormat: /\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/
+        },
+        weekdaysShort : 'вс_пн_вт_ср_чт_пт_сб'.split('_'),
+        weekdaysMin : 'вс_пн_вт_ср_чт_пт_сб'.split('_'),
+        monthsParse : monthsParse,
+        longMonthsParse : monthsParse,
+        shortMonthsParse : monthsParse,
+
+        // полные названия с падежами, по три буквы, для некоторых, по 4 буквы, сокращения с точкой и без точки
+        monthsRegex: /^(январ[ья]|янв\.?|феврал[ья]|февр?\.?|марта?|мар\.?|апрел[ья]|апр\.?|ма[йя]|июн[ья]|июн\.?|июл[ья]|июл\.?|августа?|авг\.?|сентябр[ья]|сент?\.?|октябр[ья]|окт\.?|ноябр[ья]|нояб?\.?|декабр[ья]|дек\.?)/i,
+
+        // копия предыдущего
+        monthsShortRegex: /^(январ[ья]|янв\.?|феврал[ья]|февр?\.?|марта?|мар\.?|апрел[ья]|апр\.?|ма[йя]|июн[ья]|июн\.?|июл[ья]|июл\.?|августа?|авг\.?|сентябр[ья]|сент?\.?|октябр[ья]|окт\.?|ноябр[ья]|нояб?\.?|декабр[ья]|дек\.?)/i,
+
+        // полные названия с падежами
+        monthsStrictRegex: /^(январ[яь]|феврал[яь]|марта?|апрел[яь]|ма[яй]|июн[яь]|июл[яь]|августа?|сентябр[яь]|октябр[яь]|ноябр[яь]|декабр[яь])/i,
+
+        // Выражение, которое соотвествует только сокращённым формам
+        monthsShortStrictRegex: /^(янв\.|февр?\.|мар[т.]|апр\.|ма[яй]|июн[ья.]|июл[ья.]|авг\.|сент?\.|окт\.|нояб?\.|дек\.)/i,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY г.',
+            LLL : 'D MMMM YYYY г., HH:mm',
+            LLLL : 'dddd, D MMMM YYYY г., HH:mm'
+        },
+        calendar : {
+            sameDay: '[Сегодня в] LT',
+            nextDay: '[Завтра в] LT',
+            lastDay: '[Вчера в] LT',
+            nextWeek: function (now) {
+                if (now.week() !== this.week()) {
+                    switch (this.day()) {
+                        case 0:
+                            return '[В следующее] dddd [в] LT';
+                        case 1:
+                        case 2:
+                        case 4:
+                            return '[В следующий] dddd [в] LT';
+                        case 3:
+                        case 5:
+                        case 6:
+                            return '[В следующую] dddd [в] LT';
+                    }
+                } else {
+                    if (this.day() === 2) {
+                        return '[Во] dddd [в] LT';
+                    } else {
+                        return '[В] dddd [в] LT';
+                    }
+                }
+            },
+            lastWeek: function (now) {
+                if (now.week() !== this.week()) {
+                    switch (this.day()) {
+                        case 0:
+                            return '[В прошлое] dddd [в] LT';
+                        case 1:
+                        case 2:
+                        case 4:
+                            return '[В прошлый] dddd [в] LT';
+                        case 3:
+                        case 5:
+                        case 6:
+                            return '[В прошлую] dddd [в] LT';
+                    }
+                } else {
+                    if (this.day() === 2) {
+                        return '[Во] dddd [в] LT';
+                    } else {
+                        return '[В] dddd [в] LT';
+                    }
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'через %s',
+            past : '%s назад',
+            s : 'несколько секунд',
+            m : relativeTimeWithPlural,
+            mm : relativeTimeWithPlural,
+            h : 'час',
+            hh : relativeTimeWithPlural,
+            d : 'день',
+            dd : relativeTimeWithPlural,
+            M : 'месяц',
+            MM : relativeTimeWithPlural,
+            y : 'год',
+            yy : relativeTimeWithPlural
+        },
+        meridiemParse: /ночи|утра|дня|вечера/i,
+        isPM : function (input) {
+            return /^(дня|вечера)$/.test(input);
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'ночи';
+            } else if (hour < 12) {
+                return 'утра';
+            } else if (hour < 17) {
+                return 'дня';
+            } else {
+                return 'вечера';
+            }
+        },
+        ordinalParse: /\d{1,2}-(й|го|я)/,
+        ordinal: function (number, period) {
+            switch (period) {
+                case 'M':
+                case 'd':
+                case 'DDD':
+                    return number + '-й';
+                case 'D':
+                    return number + '-го';
+                case 'w':
+                case 'W':
+                    return number + '-я';
+                default:
+                    return number;
+            }
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ru;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/se.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/se.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/se.js	(revision 12265)
@@ -0,0 +1,61 @@
+//! moment.js locale configuration
+//! locale : Northern Sami [se]
+//! authors : Bård Rolstad Henriksen : https://github.com/karamell
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+
+    var se = moment.defineLocale('se', {
+        months : 'ođđajagemánnu_guovvamánnu_njukčamánnu_cuoŋománnu_miessemánnu_geassemánnu_suoidnemánnu_borgemánnu_čakčamánnu_golggotmánnu_skábmamánnu_juovlamánnu'.split('_'),
+        monthsShort : 'ođđj_guov_njuk_cuo_mies_geas_suoi_borg_čakč_golg_skáb_juov'.split('_'),
+        weekdays : 'sotnabeaivi_vuossárga_maŋŋebárga_gaskavahkku_duorastat_bearjadat_lávvardat'.split('_'),
+        weekdaysShort : 'sotn_vuos_maŋ_gask_duor_bear_láv'.split('_'),
+        weekdaysMin : 's_v_m_g_d_b_L'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'MMMM D. [b.] YYYY',
+            LLL : 'MMMM D. [b.] YYYY [ti.] HH:mm',
+            LLLL : 'dddd, MMMM D. [b.] YYYY [ti.] HH:mm'
+        },
+        calendar : {
+            sameDay: '[otne ti] LT',
+            nextDay: '[ihttin ti] LT',
+            nextWeek: 'dddd [ti] LT',
+            lastDay: '[ikte ti] LT',
+            lastWeek: '[ovddit] dddd [ti] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : '%s geažes',
+            past : 'maŋit %s',
+            s : 'moadde sekunddat',
+            m : 'okta minuhta',
+            mm : '%d minuhtat',
+            h : 'okta diimmu',
+            hh : '%d diimmut',
+            d : 'okta beaivi',
+            dd : '%d beaivvit',
+            M : 'okta mánnu',
+            MM : '%d mánut',
+            y : 'okta jahki',
+            yy : '%d jagit'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return se;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/si.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/si.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/si.js	(revision 12265)
@@ -0,0 +1,71 @@
+//! moment.js locale configuration
+//! locale : Sinhalese [si]
+//! author : Sampath Sitinamaluwa : https://github.com/sampathsris
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    /*jshint -W100*/
+    var si = moment.defineLocale('si', {
+        months : 'ජනවාරි_පෙබරවාරි_මාර්තු_අප්‍රේල්_මැයි_ජූනි_ජූලි_අගෝස්තු_සැප්තැම්බර්_ඔක්තෝබර්_නොවැම්බර්_දෙසැම්බර්'.split('_'),
+        monthsShort : 'ජන_පෙබ_මාර්_අප්_මැයි_ජූනි_ජූලි_අගෝ_සැප්_ඔක්_නොවැ_දෙසැ'.split('_'),
+        weekdays : 'ඉරිදා_සඳුදා_අඟහරුවාදා_බදාදා_බ්‍රහස්පතින්දා_සිකුරාදා_සෙනසුරාදා'.split('_'),
+        weekdaysShort : 'ඉරි_සඳු_අඟ_බදා_බ්‍රහ_සිකු_සෙන'.split('_'),
+        weekdaysMin : 'ඉ_ස_අ_බ_බ්‍ර_සි_සෙ'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'a h:mm',
+            LTS : 'a h:mm:ss',
+            L : 'YYYY/MM/DD',
+            LL : 'YYYY MMMM D',
+            LLL : 'YYYY MMMM D, a h:mm',
+            LLLL : 'YYYY MMMM D [වැනි] dddd, a h:mm:ss'
+        },
+        calendar : {
+            sameDay : '[අද] LT[ට]',
+            nextDay : '[හෙට] LT[ට]',
+            nextWeek : 'dddd LT[ට]',
+            lastDay : '[ඊයේ] LT[ට]',
+            lastWeek : '[පසුගිය] dddd LT[ට]',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%sකින්',
+            past : '%sකට පෙර',
+            s : 'තත්පර කිහිපය',
+            m : 'මිනිත්තුව',
+            mm : 'මිනිත්තු %d',
+            h : 'පැය',
+            hh : 'පැය %d',
+            d : 'දිනය',
+            dd : 'දින %d',
+            M : 'මාසය',
+            MM : 'මාස %d',
+            y : 'වසර',
+            yy : 'වසර %d'
+        },
+        ordinalParse: /\d{1,2} වැනි/,
+        ordinal : function (number) {
+            return number + ' වැනි';
+        },
+        meridiemParse : /පෙර වරු|පස් වරු|පෙ.ව|ප.ව./,
+        isPM : function (input) {
+            return input === 'ප.ව.' || input === 'පස් වරු';
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours > 11) {
+                return isLower ? 'ප.ව.' : 'පස් වරු';
+            } else {
+                return isLower ? 'පෙ.ව.' : 'පෙර වරු';
+            }
+        }
+    });
+
+    return si;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sk.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sk.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sk.js	(revision 12265)
@@ -0,0 +1,150 @@
+//! moment.js locale configuration
+//! locale : Slovak [sk]
+//! author : Martin Minka : https://github.com/k2s
+//! based on work of petrbela : https://github.com/petrbela
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var months = 'január_február_marec_apríl_máj_jún_júl_august_september_október_november_december'.split('_'),
+        monthsShort = 'jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec'.split('_');
+    function plural(n) {
+        return (n > 1) && (n < 5);
+    }
+    function translate(number, withoutSuffix, key, isFuture) {
+        var result = number + ' ';
+        switch (key) {
+            case 's':  // a few seconds / in a few seconds / a few seconds ago
+                return (withoutSuffix || isFuture) ? 'pár sekúnd' : 'pár sekundami';
+            case 'm':  // a minute / in a minute / a minute ago
+                return withoutSuffix ? 'minúta' : (isFuture ? 'minútu' : 'minútou');
+            case 'mm': // 9 minutes / in 9 minutes / 9 minutes ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'minúty' : 'minút');
+                } else {
+                    return result + 'minútami';
+                }
+                break;
+            case 'h':  // an hour / in an hour / an hour ago
+                return withoutSuffix ? 'hodina' : (isFuture ? 'hodinu' : 'hodinou');
+            case 'hh': // 9 hours / in 9 hours / 9 hours ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'hodiny' : 'hodín');
+                } else {
+                    return result + 'hodinami';
+                }
+                break;
+            case 'd':  // a day / in a day / a day ago
+                return (withoutSuffix || isFuture) ? 'deň' : 'dňom';
+            case 'dd': // 9 days / in 9 days / 9 days ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'dni' : 'dní');
+                } else {
+                    return result + 'dňami';
+                }
+                break;
+            case 'M':  // a month / in a month / a month ago
+                return (withoutSuffix || isFuture) ? 'mesiac' : 'mesiacom';
+            case 'MM': // 9 months / in 9 months / 9 months ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'mesiace' : 'mesiacov');
+                } else {
+                    return result + 'mesiacmi';
+                }
+                break;
+            case 'y':  // a year / in a year / a year ago
+                return (withoutSuffix || isFuture) ? 'rok' : 'rokom';
+            case 'yy': // 9 years / in 9 years / 9 years ago
+                if (withoutSuffix || isFuture) {
+                    return result + (plural(number) ? 'roky' : 'rokov');
+                } else {
+                    return result + 'rokmi';
+                }
+                break;
+        }
+    }
+
+    var sk = moment.defineLocale('sk', {
+        months : months,
+        monthsShort : monthsShort,
+        weekdays : 'nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota'.split('_'),
+        weekdaysShort : 'ne_po_ut_st_št_pi_so'.split('_'),
+        weekdaysMin : 'ne_po_ut_st_št_pi_so'.split('_'),
+        longDateFormat : {
+            LT: 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY H:mm',
+            LLLL : 'dddd D. MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay: '[dnes o] LT',
+            nextDay: '[zajtra o] LT',
+            nextWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[v nedeľu o] LT';
+                    case 1:
+                    case 2:
+                        return '[v] dddd [o] LT';
+                    case 3:
+                        return '[v stredu o] LT';
+                    case 4:
+                        return '[vo štvrtok o] LT';
+                    case 5:
+                        return '[v piatok o] LT';
+                    case 6:
+                        return '[v sobotu o] LT';
+                }
+            },
+            lastDay: '[včera o] LT',
+            lastWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[minulú nedeľu o] LT';
+                    case 1:
+                    case 2:
+                        return '[minulý] dddd [o] LT';
+                    case 3:
+                        return '[minulú stredu o] LT';
+                    case 4:
+                    case 5:
+                        return '[minulý] dddd [o] LT';
+                    case 6:
+                        return '[minulú sobotu o] LT';
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past : 'pred %s',
+            s : translate,
+            m : translate,
+            mm : translate,
+            h : translate,
+            hh : translate,
+            d : translate,
+            dd : translate,
+            M : translate,
+            MM : translate,
+            y : translate,
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return sk;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sl.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sl.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sl.js	(revision 12265)
@@ -0,0 +1,162 @@
+//! moment.js locale configuration
+//! locale : Slovenian [sl]
+//! author : Robert Sedovšek : https://github.com/sedovsek
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function processRelativeTime(number, withoutSuffix, key, isFuture) {
+        var result = number + ' ';
+        switch (key) {
+            case 's':
+                return withoutSuffix || isFuture ? 'nekaj sekund' : 'nekaj sekundami';
+            case 'm':
+                return withoutSuffix ? 'ena minuta' : 'eno minuto';
+            case 'mm':
+                if (number === 1) {
+                    result += withoutSuffix ? 'minuta' : 'minuto';
+                } else if (number === 2) {
+                    result += withoutSuffix || isFuture ? 'minuti' : 'minutama';
+                } else if (number < 5) {
+                    result += withoutSuffix || isFuture ? 'minute' : 'minutami';
+                } else {
+                    result += withoutSuffix || isFuture ? 'minut' : 'minutami';
+                }
+                return result;
+            case 'h':
+                return withoutSuffix ? 'ena ura' : 'eno uro';
+            case 'hh':
+                if (number === 1) {
+                    result += withoutSuffix ? 'ura' : 'uro';
+                } else if (number === 2) {
+                    result += withoutSuffix || isFuture ? 'uri' : 'urama';
+                } else if (number < 5) {
+                    result += withoutSuffix || isFuture ? 'ure' : 'urami';
+                } else {
+                    result += withoutSuffix || isFuture ? 'ur' : 'urami';
+                }
+                return result;
+            case 'd':
+                return withoutSuffix || isFuture ? 'en dan' : 'enim dnem';
+            case 'dd':
+                if (number === 1) {
+                    result += withoutSuffix || isFuture ? 'dan' : 'dnem';
+                } else if (number === 2) {
+                    result += withoutSuffix || isFuture ? 'dni' : 'dnevoma';
+                } else {
+                    result += withoutSuffix || isFuture ? 'dni' : 'dnevi';
+                }
+                return result;
+            case 'M':
+                return withoutSuffix || isFuture ? 'en mesec' : 'enim mesecem';
+            case 'MM':
+                if (number === 1) {
+                    result += withoutSuffix || isFuture ? 'mesec' : 'mesecem';
+                } else if (number === 2) {
+                    result += withoutSuffix || isFuture ? 'meseca' : 'mesecema';
+                } else if (number < 5) {
+                    result += withoutSuffix || isFuture ? 'mesece' : 'meseci';
+                } else {
+                    result += withoutSuffix || isFuture ? 'mesecev' : 'meseci';
+                }
+                return result;
+            case 'y':
+                return withoutSuffix || isFuture ? 'eno leto' : 'enim letom';
+            case 'yy':
+                if (number === 1) {
+                    result += withoutSuffix || isFuture ? 'leto' : 'letom';
+                } else if (number === 2) {
+                    result += withoutSuffix || isFuture ? 'leti' : 'letoma';
+                } else if (number < 5) {
+                    result += withoutSuffix || isFuture ? 'leta' : 'leti';
+                } else {
+                    result += withoutSuffix || isFuture ? 'let' : 'leti';
+                }
+                return result;
+        }
+    }
+
+    var sl = moment.defineLocale('sl', {
+        months : 'januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december'.split('_'),
+        monthsShort : 'jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota'.split('_'),
+        weekdaysShort : 'ned._pon._tor._sre._čet._pet._sob.'.split('_'),
+        weekdaysMin : 'ne_po_to_sr_če_pe_so'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM YYYY',
+            LLL : 'D. MMMM YYYY H:mm',
+            LLLL : 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar : {
+            sameDay  : '[danes ob] LT',
+            nextDay  : '[jutri ob] LT',
+
+            nextWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[v] [nedeljo] [ob] LT';
+                    case 3:
+                        return '[v] [sredo] [ob] LT';
+                    case 6:
+                        return '[v] [soboto] [ob] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[v] dddd [ob] LT';
+                }
+            },
+            lastDay  : '[včeraj ob] LT',
+            lastWeek : function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[prejšnjo] [nedeljo] [ob] LT';
+                    case 3:
+                        return '[prejšnjo] [sredo] [ob] LT';
+                    case 6:
+                        return '[prejšnjo] [soboto] [ob] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[prejšnji] dddd [ob] LT';
+                }
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'čez %s',
+            past   : 'pred %s',
+            s      : processRelativeTime,
+            m      : processRelativeTime,
+            mm     : processRelativeTime,
+            h      : processRelativeTime,
+            hh     : processRelativeTime,
+            d      : processRelativeTime,
+            dd     : processRelativeTime,
+            M      : processRelativeTime,
+            MM     : processRelativeTime,
+            y      : processRelativeTime,
+            yy     : processRelativeTime
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return sl;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sq.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sq.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sq.js	(revision 12265)
@@ -0,0 +1,70 @@
+//! moment.js locale configuration
+//! locale : Albanian [sq]
+//! author : Flakërim Ismani : https://github.com/flakerimi
+//! author : Menelion Elensúle : https://github.com/Oire
+//! author : Oerd Cukalla : https://github.com/oerd
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var sq = moment.defineLocale('sq', {
+        months : 'Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_Nëntor_Dhjetor'.split('_'),
+        monthsShort : 'Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_Nën_Dhj'.split('_'),
+        weekdays : 'E Diel_E Hënë_E Martë_E Mërkurë_E Enjte_E Premte_E Shtunë'.split('_'),
+        weekdaysShort : 'Die_Hën_Mar_Mër_Enj_Pre_Sht'.split('_'),
+        weekdaysMin : 'D_H_Ma_Më_E_P_Sh'.split('_'),
+        weekdaysParseExact : true,
+        meridiemParse: /PD|MD/,
+        isPM: function (input) {
+            return input.charAt(0) === 'M';
+        },
+        meridiem : function (hours, minutes, isLower) {
+            return hours < 12 ? 'PD' : 'MD';
+        },
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[Sot në] LT',
+            nextDay : '[Nesër në] LT',
+            nextWeek : 'dddd [në] LT',
+            lastDay : '[Dje në] LT',
+            lastWeek : 'dddd [e kaluar në] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'në %s',
+            past : '%s më parë',
+            s : 'disa sekonda',
+            m : 'një minutë',
+            mm : '%d minuta',
+            h : 'një orë',
+            hh : '%d orë',
+            d : 'një ditë',
+            dd : '%d ditë',
+            M : 'një muaj',
+            MM : '%d muaj',
+            y : 'një vit',
+            yy : '%d vite'
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return sq;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sr-cyrl.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sr-cyrl.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sr-cyrl.js	(revision 12265)
@@ -0,0 +1,110 @@
+//! moment.js locale configuration
+//! locale : Serbian Cyrillic [sr-cyrl]
+//! author : Milan Janačković<milanjanackovic@gmail.com> : https://github.com/milan-j
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var translator = {
+        words: { //Different grammatical cases
+            m: ['један минут', 'једне минуте'],
+            mm: ['минут', 'минуте', 'минута'],
+            h: ['један сат', 'једног сата'],
+            hh: ['сат', 'сата', 'сати'],
+            dd: ['дан', 'дана', 'дана'],
+            MM: ['месец', 'месеца', 'месеци'],
+            yy: ['година', 'године', 'година']
+        },
+        correctGrammaticalCase: function (number, wordKey) {
+            return number === 1 ? wordKey[0] : (number >= 2 && number <= 4 ? wordKey[1] : wordKey[2]);
+        },
+        translate: function (number, withoutSuffix, key) {
+            var wordKey = translator.words[key];
+            if (key.length === 1) {
+                return withoutSuffix ? wordKey[0] : wordKey[1];
+            } else {
+                return number + ' ' + translator.correctGrammaticalCase(number, wordKey);
+            }
+        }
+    };
+
+    var sr_cyrl = moment.defineLocale('sr-cyrl', {
+        months: 'јануар_фебруар_март_април_мај_јун_јул_август_септембар_октобар_новембар_децембар'.split('_'),
+        monthsShort: 'јан._феб._мар._апр._мај_јун_јул_авг._сеп._окт._нов._дец.'.split('_'),
+        monthsParseExact: true,
+        weekdays: 'недеља_понедељак_уторак_среда_четвртак_петак_субота'.split('_'),
+        weekdaysShort: 'нед._пон._уто._сре._чет._пет._суб.'.split('_'),
+        weekdaysMin: 'не_по_ут_ср_че_пе_су'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat: {
+            LT: 'H:mm',
+            LTS : 'H:mm:ss',
+            L: 'DD.MM.YYYY',
+            LL: 'D. MMMM YYYY',
+            LLL: 'D. MMMM YYYY H:mm',
+            LLLL: 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar: {
+            sameDay: '[данас у] LT',
+            nextDay: '[сутра у] LT',
+            nextWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[у] [недељу] [у] LT';
+                    case 3:
+                        return '[у] [среду] [у] LT';
+                    case 6:
+                        return '[у] [суботу] [у] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[у] dddd [у] LT';
+                }
+            },
+            lastDay  : '[јуче у] LT',
+            lastWeek : function () {
+                var lastWeekDays = [
+                    '[прошле] [недеље] [у] LT',
+                    '[прошлог] [понедељка] [у] LT',
+                    '[прошлог] [уторка] [у] LT',
+                    '[прошле] [среде] [у] LT',
+                    '[прошлог] [четвртка] [у] LT',
+                    '[прошлог] [петка] [у] LT',
+                    '[прошле] [суботе] [у] LT'
+                ];
+                return lastWeekDays[this.day()];
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'за %s',
+            past   : 'пре %s',
+            s      : 'неколико секунди',
+            m      : translator.translate,
+            mm     : translator.translate,
+            h      : translator.translate,
+            hh     : translator.translate,
+            d      : 'дан',
+            dd     : translator.translate,
+            M      : 'месец',
+            MM     : translator.translate,
+            y      : 'годину',
+            yy     : translator.translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return sr_cyrl;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sr.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sr.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sr.js	(revision 12265)
@@ -0,0 +1,110 @@
+//! moment.js locale configuration
+//! locale : Serbian [sr]
+//! author : Milan Janačković<milanjanackovic@gmail.com> : https://github.com/milan-j
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var translator = {
+        words: { //Different grammatical cases
+            m: ['jedan minut', 'jedne minute'],
+            mm: ['minut', 'minute', 'minuta'],
+            h: ['jedan sat', 'jednog sata'],
+            hh: ['sat', 'sata', 'sati'],
+            dd: ['dan', 'dana', 'dana'],
+            MM: ['mesec', 'meseca', 'meseci'],
+            yy: ['godina', 'godine', 'godina']
+        },
+        correctGrammaticalCase: function (number, wordKey) {
+            return number === 1 ? wordKey[0] : (number >= 2 && number <= 4 ? wordKey[1] : wordKey[2]);
+        },
+        translate: function (number, withoutSuffix, key) {
+            var wordKey = translator.words[key];
+            if (key.length === 1) {
+                return withoutSuffix ? wordKey[0] : wordKey[1];
+            } else {
+                return number + ' ' + translator.correctGrammaticalCase(number, wordKey);
+            }
+        }
+    };
+
+    var sr = moment.defineLocale('sr', {
+        months: 'januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar'.split('_'),
+        monthsShort: 'jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.'.split('_'),
+        monthsParseExact: true,
+        weekdays: 'nedelja_ponedeljak_utorak_sreda_četvrtak_petak_subota'.split('_'),
+        weekdaysShort: 'ned._pon._uto._sre._čet._pet._sub.'.split('_'),
+        weekdaysMin: 'ne_po_ut_sr_če_pe_su'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat: {
+            LT: 'H:mm',
+            LTS : 'H:mm:ss',
+            L: 'DD.MM.YYYY',
+            LL: 'D. MMMM YYYY',
+            LLL: 'D. MMMM YYYY H:mm',
+            LLLL: 'dddd, D. MMMM YYYY H:mm'
+        },
+        calendar: {
+            sameDay: '[danas u] LT',
+            nextDay: '[sutra u] LT',
+            nextWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                        return '[u] [nedelju] [u] LT';
+                    case 3:
+                        return '[u] [sredu] [u] LT';
+                    case 6:
+                        return '[u] [subotu] [u] LT';
+                    case 1:
+                    case 2:
+                    case 4:
+                    case 5:
+                        return '[u] dddd [u] LT';
+                }
+            },
+            lastDay  : '[juče u] LT',
+            lastWeek : function () {
+                var lastWeekDays = [
+                    '[prošle] [nedelje] [u] LT',
+                    '[prošlog] [ponedeljka] [u] LT',
+                    '[prošlog] [utorka] [u] LT',
+                    '[prošle] [srede] [u] LT',
+                    '[prošlog] [četvrtka] [u] LT',
+                    '[prošlog] [petka] [u] LT',
+                    '[prošle] [subote] [u] LT'
+                ];
+                return lastWeekDays[this.day()];
+            },
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'za %s',
+            past   : 'pre %s',
+            s      : 'nekoliko sekundi',
+            m      : translator.translate,
+            mm     : translator.translate,
+            h      : translator.translate,
+            hh     : translator.translate,
+            d      : 'dan',
+            dd     : translator.translate,
+            M      : 'mesec',
+            MM     : translator.translate,
+            y      : 'godinu',
+            yy     : translator.translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return sr;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ss.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ss.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ss.js	(revision 12265)
@@ -0,0 +1,89 @@
+//! moment.js locale configuration
+//! locale : siSwati [ss]
+//! author : Nicolai Davies<mail@nicolai.io> : https://github.com/nicolaidavies
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+
+    var ss = moment.defineLocale('ss', {
+        months : "Bhimbidvwane_Indlovana_Indlov'lenkhulu_Mabasa_Inkhwekhweti_Inhlaba_Kholwane_Ingci_Inyoni_Imphala_Lweti_Ingongoni".split('_'),
+        monthsShort : 'Bhi_Ina_Inu_Mab_Ink_Inh_Kho_Igc_Iny_Imp_Lwe_Igo'.split('_'),
+        weekdays : 'Lisontfo_Umsombuluko_Lesibili_Lesitsatfu_Lesine_Lesihlanu_Umgcibelo'.split('_'),
+        weekdaysShort : 'Lis_Umb_Lsb_Les_Lsi_Lsh_Umg'.split('_'),
+        weekdaysMin : 'Li_Us_Lb_Lt_Ls_Lh_Ug'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'h:mm A',
+            LTS : 'h:mm:ss A',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY h:mm A',
+            LLLL : 'dddd, D MMMM YYYY h:mm A'
+        },
+        calendar : {
+            sameDay : '[Namuhla nga] LT',
+            nextDay : '[Kusasa nga] LT',
+            nextWeek : 'dddd [nga] LT',
+            lastDay : '[Itolo nga] LT',
+            lastWeek : 'dddd [leliphelile] [nga] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'nga %s',
+            past : 'wenteka nga %s',
+            s : 'emizuzwana lomcane',
+            m : 'umzuzu',
+            mm : '%d emizuzu',
+            h : 'lihora',
+            hh : '%d emahora',
+            d : 'lilanga',
+            dd : '%d emalanga',
+            M : 'inyanga',
+            MM : '%d tinyanga',
+            y : 'umnyaka',
+            yy : '%d iminyaka'
+        },
+        meridiemParse: /ekuseni|emini|entsambama|ebusuku/,
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 11) {
+                return 'ekuseni';
+            } else if (hours < 15) {
+                return 'emini';
+            } else if (hours < 19) {
+                return 'entsambama';
+            } else {
+                return 'ebusuku';
+            }
+        },
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'ekuseni') {
+                return hour;
+            } else if (meridiem === 'emini') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === 'entsambama' || meridiem === 'ebusuku') {
+                if (hour === 0) {
+                    return 0;
+                }
+                return hour + 12;
+            }
+        },
+        ordinalParse: /\d{1,2}/,
+        ordinal : '%d',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return ss;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sv.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sv.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sv.js	(revision 12265)
@@ -0,0 +1,69 @@
+//! moment.js locale configuration
+//! locale : Swedish [sv]
+//! author : Jens Alm : https://github.com/ulmus
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var sv = moment.defineLocale('sv', {
+        months : 'januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december'.split('_'),
+        monthsShort : 'jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec'.split('_'),
+        weekdays : 'söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag'.split('_'),
+        weekdaysShort : 'sön_mån_tis_ons_tor_fre_lör'.split('_'),
+        weekdaysMin : 'sö_må_ti_on_to_fr_lö'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'YYYY-MM-DD',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY [kl.] HH:mm',
+            LLLL : 'dddd D MMMM YYYY [kl.] HH:mm',
+            lll : 'D MMM YYYY HH:mm',
+            llll : 'ddd D MMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Idag] LT',
+            nextDay: '[Imorgon] LT',
+            lastDay: '[Igår] LT',
+            nextWeek: '[På] dddd LT',
+            lastWeek: '[I] dddd[s] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'om %s',
+            past : 'för %s sedan',
+            s : 'några sekunder',
+            m : 'en minut',
+            mm : '%d minuter',
+            h : 'en timme',
+            hh : '%d timmar',
+            d : 'en dag',
+            dd : '%d dagar',
+            M : 'en månad',
+            MM : '%d månader',
+            y : 'ett år',
+            yy : '%d år'
+        },
+        ordinalParse: /\d{1,2}(e|a)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'e' :
+                (b === 1) ? 'a' :
+                (b === 2) ? 'a' :
+                (b === 3) ? 'e' : 'e';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return sv;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sw.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sw.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/sw.js	(revision 12265)
@@ -0,0 +1,59 @@
+//! moment.js locale configuration
+//! locale : Swahili [sw]
+//! author : Fahad Kassim : https://github.com/fadsel
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var sw = moment.defineLocale('sw', {
+        months : 'Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba'.split('_'),
+        monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des'.split('_'),
+        weekdays : 'Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi'.split('_'),
+        weekdaysShort : 'Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos'.split('_'),
+        weekdaysMin : 'J2_J3_J4_J5_Al_Ij_J1'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[leo saa] LT',
+            nextDay : '[kesho saa] LT',
+            nextWeek : '[wiki ijayo] dddd [saat] LT',
+            lastDay : '[jana] LT',
+            lastWeek : '[wiki iliyopita] dddd [saat] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s baadaye',
+            past : 'tokea %s',
+            s : 'hivi punde',
+            m : 'dakika moja',
+            mm : 'dakika %d',
+            h : 'saa limoja',
+            hh : 'masaa %d',
+            d : 'siku moja',
+            dd : 'masiku %d',
+            M : 'mwezi mmoja',
+            MM : 'miezi %d',
+            y : 'mwaka mmoja',
+            yy : 'miaka %d'
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return sw;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ta.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ta.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/ta.js	(revision 12265)
@@ -0,0 +1,129 @@
+//! moment.js locale configuration
+//! locale : Tamil [ta]
+//! author : Arjunkumar Krishnamoorthy : https://github.com/tk120404
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var symbolMap = {
+        '1': '௧',
+        '2': '௨',
+        '3': '௩',
+        '4': '௪',
+        '5': '௫',
+        '6': '௬',
+        '7': '௭',
+        '8': '௮',
+        '9': '௯',
+        '0': '௦'
+    }, numberMap = {
+        '௧': '1',
+        '௨': '2',
+        '௩': '3',
+        '௪': '4',
+        '௫': '5',
+        '௬': '6',
+        '௭': '7',
+        '௮': '8',
+        '௯': '9',
+        '௦': '0'
+    };
+
+    var ta = moment.defineLocale('ta', {
+        months : 'ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்'.split('_'),
+        monthsShort : 'ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்'.split('_'),
+        weekdays : 'ஞாயிற்றுக்கிழமை_திங்கட்கிழமை_செவ்வாய்கிழமை_புதன்கிழமை_வியாழக்கிழமை_வெள்ளிக்கிழமை_சனிக்கிழமை'.split('_'),
+        weekdaysShort : 'ஞாயிறு_திங்கள்_செவ்வாய்_புதன்_வியாழன்_வெள்ளி_சனி'.split('_'),
+        weekdaysMin : 'ஞா_தி_செ_பு_வி_வெ_ச'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, HH:mm',
+            LLLL : 'dddd, D MMMM YYYY, HH:mm'
+        },
+        calendar : {
+            sameDay : '[இன்று] LT',
+            nextDay : '[நாளை] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[நேற்று] LT',
+            lastWeek : '[கடந்த வாரம்] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s இல்',
+            past : '%s முன்',
+            s : 'ஒரு சில விநாடிகள்',
+            m : 'ஒரு நிமிடம்',
+            mm : '%d நிமிடங்கள்',
+            h : 'ஒரு மணி நேரம்',
+            hh : '%d மணி நேரம்',
+            d : 'ஒரு நாள்',
+            dd : '%d நாட்கள்',
+            M : 'ஒரு மாதம்',
+            MM : '%d மாதங்கள்',
+            y : 'ஒரு வருடம்',
+            yy : '%d ஆண்டுகள்'
+        },
+        ordinalParse: /\d{1,2}வது/,
+        ordinal : function (number) {
+            return number + 'வது';
+        },
+        preparse: function (string) {
+            return string.replace(/[௧௨௩௪௫௬௭௮௯௦]/g, function (match) {
+                return numberMap[match];
+            });
+        },
+        postformat: function (string) {
+            return string.replace(/\d/g, function (match) {
+                return symbolMap[match];
+            });
+        },
+        // refer http://ta.wikipedia.org/s/1er1
+        meridiemParse: /யாமம்|வைகறை|காலை|நண்பகல்|எற்பாடு|மாலை/,
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 2) {
+                return ' யாமம்';
+            } else if (hour < 6) {
+                return ' வைகறை';  // வைகறை
+            } else if (hour < 10) {
+                return ' காலை'; // காலை
+            } else if (hour < 14) {
+                return ' நண்பகல்'; // நண்பகல்
+            } else if (hour < 18) {
+                return ' எற்பாடு'; // எற்பாடு
+            } else if (hour < 22) {
+                return ' மாலை'; // மாலை
+            } else {
+                return ' யாமம்';
+            }
+        },
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'யாமம்') {
+                return hour < 2 ? hour : hour + 12;
+            } else if (meridiem === 'வைகறை' || meridiem === 'காலை') {
+                return hour;
+            } else if (meridiem === 'நண்பகல்') {
+                return hour >= 10 ? hour : hour + 12;
+            } else {
+                return hour + 12;
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return ta;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/te.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/te.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/te.js	(revision 12265)
@@ -0,0 +1,89 @@
+//! moment.js locale configuration
+//! locale : Telugu [te]
+//! author : Krishna Chaitanya Thota : https://github.com/kcthota
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var te = moment.defineLocale('te', {
+        months : 'జనవరి_ఫిబ్రవరి_మార్చి_ఏప్రిల్_మే_జూన్_జూలై_ఆగస్టు_సెప్టెంబర్_అక్టోబర్_నవంబర్_డిసెంబర్'.split('_'),
+        monthsShort : 'జన._ఫిబ్ర._మార్చి_ఏప్రి._మే_జూన్_జూలై_ఆగ._సెప్._అక్టో._నవ._డిసె.'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'ఆదివారం_సోమవారం_మంగళవారం_బుధవారం_గురువారం_శుక్రవారం_శనివారం'.split('_'),
+        weekdaysShort : 'ఆది_సోమ_మంగళ_బుధ_గురు_శుక్ర_శని'.split('_'),
+        weekdaysMin : 'ఆ_సో_మం_బు_గు_శు_శ'.split('_'),
+        longDateFormat : {
+            LT : 'A h:mm',
+            LTS : 'A h:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY, A h:mm',
+            LLLL : 'dddd, D MMMM YYYY, A h:mm'
+        },
+        calendar : {
+            sameDay : '[నేడు] LT',
+            nextDay : '[రేపు] LT',
+            nextWeek : 'dddd, LT',
+            lastDay : '[నిన్న] LT',
+            lastWeek : '[గత] dddd, LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s లో',
+            past : '%s క్రితం',
+            s : 'కొన్ని క్షణాలు',
+            m : 'ఒక నిమిషం',
+            mm : '%d నిమిషాలు',
+            h : 'ఒక గంట',
+            hh : '%d గంటలు',
+            d : 'ఒక రోజు',
+            dd : '%d రోజులు',
+            M : 'ఒక నెల',
+            MM : '%d నెలలు',
+            y : 'ఒక సంవత్సరం',
+            yy : '%d సంవత్సరాలు'
+        },
+        ordinalParse : /\d{1,2}వ/,
+        ordinal : '%dవ',
+        meridiemParse: /రాత్రి|ఉదయం|మధ్యాహ్నం|సాయంత్రం/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === 'రాత్రి') {
+                return hour < 4 ? hour : hour + 12;
+            } else if (meridiem === 'ఉదయం') {
+                return hour;
+            } else if (meridiem === 'మధ్యాహ్నం') {
+                return hour >= 10 ? hour : hour + 12;
+            } else if (meridiem === 'సాయంత్రం') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'రాత్రి';
+            } else if (hour < 10) {
+                return 'ఉదయం';
+            } else if (hour < 17) {
+                return 'మధ్యాహ్నం';
+            } else if (hour < 20) {
+                return 'సాయంత్రం';
+            } else {
+                return 'రాత్రి';
+            }
+        },
+        week : {
+            dow : 0, // Sunday is the first day of the week.
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return te;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/th.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/th.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/th.js	(revision 12265)
@@ -0,0 +1,67 @@
+//! moment.js locale configuration
+//! locale : Thai [th]
+//! author : Kridsada Thanabulpong : https://github.com/sirn
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var th = moment.defineLocale('th', {
+        months : 'มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม'.split('_'),
+        monthsShort : 'ม.ค._ก.พ._มี.ค._เม.ย._พ.ค._มิ.ย._ก.ค._ส.ค._ก.ย._ต.ค._พ.ย._ธ.ค.'.split('_'),
+        monthsParseExact: true,
+        weekdays : 'อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์'.split('_'),
+        weekdaysShort : 'อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์'.split('_'), // yes, three characters difference
+        weekdaysMin : 'อา._จ._อ._พ._พฤ._ศ._ส.'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'H:mm',
+            LTS : 'H:mm:ss',
+            L : 'YYYY/MM/DD',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY เวลา H:mm',
+            LLLL : 'วันddddที่ D MMMM YYYY เวลา H:mm'
+        },
+        meridiemParse: /ก่อนเที่ยง|หลังเที่ยง/,
+        isPM: function (input) {
+            return input === 'หลังเที่ยง';
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 12) {
+                return 'ก่อนเที่ยง';
+            } else {
+                return 'หลังเที่ยง';
+            }
+        },
+        calendar : {
+            sameDay : '[วันนี้ เวลา] LT',
+            nextDay : '[พรุ่งนี้ เวลา] LT',
+            nextWeek : 'dddd[หน้า เวลา] LT',
+            lastDay : '[เมื่อวานนี้ เวลา] LT',
+            lastWeek : '[วัน]dddd[ที่แล้ว เวลา] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'อีก %s',
+            past : '%sที่แล้ว',
+            s : 'ไม่กี่วินาที',
+            m : '1 นาที',
+            mm : '%d นาที',
+            h : '1 ชั่วโมง',
+            hh : '%d ชั่วโมง',
+            d : '1 วัน',
+            dd : '%d วัน',
+            M : '1 เดือน',
+            MM : '%d เดือน',
+            y : '1 ปี',
+            yy : '%d ปี'
+        }
+    });
+
+    return th;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tl-ph.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tl-ph.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tl-ph.js	(revision 12265)
@@ -0,0 +1,62 @@
+//! moment.js locale configuration
+//! locale : Tagalog (Philippines) [tl-ph]
+//! author : Dan Hagman : https://github.com/hagmandan
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var tl_ph = moment.defineLocale('tl-ph', {
+        months : 'Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre'.split('_'),
+        monthsShort : 'Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis'.split('_'),
+        weekdays : 'Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado'.split('_'),
+        weekdaysShort : 'Lin_Lun_Mar_Miy_Huw_Biy_Sab'.split('_'),
+        weekdaysMin : 'Li_Lu_Ma_Mi_Hu_Bi_Sab'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'MM/D/YYYY',
+            LL : 'MMMM D, YYYY',
+            LLL : 'MMMM D, YYYY HH:mm',
+            LLLL : 'dddd, MMMM DD, YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Ngayon sa] LT',
+            nextDay: '[Bukas sa] LT',
+            nextWeek: 'dddd [sa] LT',
+            lastDay: '[Kahapon sa] LT',
+            lastWeek: 'dddd [huling linggo] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'sa loob ng %s',
+            past : '%s ang nakalipas',
+            s : 'ilang segundo',
+            m : 'isang minuto',
+            mm : '%d minuto',
+            h : 'isang oras',
+            hh : '%d oras',
+            d : 'isang araw',
+            dd : '%d araw',
+            M : 'isang buwan',
+            MM : '%d buwan',
+            y : 'isang taon',
+            yy : '%d taon'
+        },
+        ordinalParse: /\d{1,2}/,
+        ordinal : function (number) {
+            return number;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return tl_ph;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tlh.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tlh.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tlh.js	(revision 12265)
@@ -0,0 +1,120 @@
+//! moment.js locale configuration
+//! locale : Klingon [tlh]
+//! author : Dominika Kruk : https://github.com/amaranthrose
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var numbersNouns = 'pagh_wa’_cha’_wej_loS_vagh_jav_Soch_chorgh_Hut'.split('_');
+
+    function translateFuture(output) {
+        var time = output;
+        time = (output.indexOf('jaj') !== -1) ?
+        time.slice(0, -3) + 'leS' :
+        (output.indexOf('jar') !== -1) ?
+        time.slice(0, -3) + 'waQ' :
+        (output.indexOf('DIS') !== -1) ?
+        time.slice(0, -3) + 'nem' :
+        time + ' pIq';
+        return time;
+    }
+
+    function translatePast(output) {
+        var time = output;
+        time = (output.indexOf('jaj') !== -1) ?
+        time.slice(0, -3) + 'Hu’' :
+        (output.indexOf('jar') !== -1) ?
+        time.slice(0, -3) + 'wen' :
+        (output.indexOf('DIS') !== -1) ?
+        time.slice(0, -3) + 'ben' :
+        time + ' ret';
+        return time;
+    }
+
+    function translate(number, withoutSuffix, string, isFuture) {
+        var numberNoun = numberAsNoun(number);
+        switch (string) {
+            case 'mm':
+                return numberNoun + ' tup';
+            case 'hh':
+                return numberNoun + ' rep';
+            case 'dd':
+                return numberNoun + ' jaj';
+            case 'MM':
+                return numberNoun + ' jar';
+            case 'yy':
+                return numberNoun + ' DIS';
+        }
+    }
+
+    function numberAsNoun(number) {
+        var hundred = Math.floor((number % 1000) / 100),
+        ten = Math.floor((number % 100) / 10),
+        one = number % 10,
+        word = '';
+        if (hundred > 0) {
+            word += numbersNouns[hundred] + 'vatlh';
+        }
+        if (ten > 0) {
+            word += ((word !== '') ? ' ' : '') + numbersNouns[ten] + 'maH';
+        }
+        if (one > 0) {
+            word += ((word !== '') ? ' ' : '') + numbersNouns[one];
+        }
+        return (word === '') ? 'pagh' : word;
+    }
+
+    var tlh = moment.defineLocale('tlh', {
+        months : 'tera’ jar wa’_tera’ jar cha’_tera’ jar wej_tera’ jar loS_tera’ jar vagh_tera’ jar jav_tera’ jar Soch_tera’ jar chorgh_tera’ jar Hut_tera’ jar wa’maH_tera’ jar wa’maH wa’_tera’ jar wa’maH cha’'.split('_'),
+        monthsShort : 'jar wa’_jar cha’_jar wej_jar loS_jar vagh_jar jav_jar Soch_jar chorgh_jar Hut_jar wa’maH_jar wa’maH wa’_jar wa’maH cha’'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'.split('_'),
+        weekdaysShort : 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'.split('_'),
+        weekdaysMin : 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[DaHjaj] LT',
+            nextDay: '[wa’leS] LT',
+            nextWeek: 'LLL',
+            lastDay: '[wa’Hu’] LT',
+            lastWeek: 'LLL',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : translateFuture,
+            past : translatePast,
+            s : 'puS lup',
+            m : 'wa’ tup',
+            mm : translate,
+            h : 'wa’ rep',
+            hh : translate,
+            d : 'wa’ jaj',
+            dd : translate,
+            M : 'wa’ jar',
+            MM : translate,
+            y : 'wa’ DIS',
+            yy : translate
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return tlh;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tr.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tr.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tr.js	(revision 12265)
@@ -0,0 +1,90 @@
+//! moment.js locale configuration
+//! locale : Turkish [tr]
+//! authors : Erhan Gundogan : https://github.com/erhangundogan,
+//!           Burak Yiğit Kaya: https://github.com/BYK
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var suffixes = {
+        1: '\'inci',
+        5: '\'inci',
+        8: '\'inci',
+        70: '\'inci',
+        80: '\'inci',
+        2: '\'nci',
+        7: '\'nci',
+        20: '\'nci',
+        50: '\'nci',
+        3: '\'üncü',
+        4: '\'üncü',
+        100: '\'üncü',
+        6: '\'ncı',
+        9: '\'uncu',
+        10: '\'uncu',
+        30: '\'uncu',
+        60: '\'ıncı',
+        90: '\'ıncı'
+    };
+
+    var tr = moment.defineLocale('tr', {
+        months : 'Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık'.split('_'),
+        monthsShort : 'Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara'.split('_'),
+        weekdays : 'Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi'.split('_'),
+        weekdaysShort : 'Paz_Pts_Sal_Çar_Per_Cum_Cts'.split('_'),
+        weekdaysMin : 'Pz_Pt_Sa_Ça_Pe_Cu_Ct'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[bugün saat] LT',
+            nextDay : '[yarın saat] LT',
+            nextWeek : '[haftaya] dddd [saat] LT',
+            lastDay : '[dün] LT',
+            lastWeek : '[geçen hafta] dddd [saat] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : '%s sonra',
+            past : '%s önce',
+            s : 'birkaç saniye',
+            m : 'bir dakika',
+            mm : '%d dakika',
+            h : 'bir saat',
+            hh : '%d saat',
+            d : 'bir gün',
+            dd : '%d gün',
+            M : 'bir ay',
+            MM : '%d ay',
+            y : 'bir yıl',
+            yy : '%d yıl'
+        },
+        ordinalParse: /\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,
+        ordinal : function (number) {
+            if (number === 0) {  // special case for zero
+                return number + '\'ıncı';
+            }
+            var a = number % 10,
+                b = number % 100 - a,
+                c = number >= 100 ? 100 : null;
+            return number + (suffixes[a] || suffixes[b] || suffixes[c]);
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return tr;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzl.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzl.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzl.js	(revision 12265)
@@ -0,0 +1,91 @@
+//! moment.js locale configuration
+//! locale : Talossan [tzl]
+//! author : Robin van der Vliet : https://github.com/robin0van0der0v
+//! author : Iustì Canun
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    // After the year there should be a slash and the amount of years since December 26, 1979 in Roman numerals.
+    // This is currently too difficult (maybe even impossible) to add.
+    var tzl = moment.defineLocale('tzl', {
+        months : 'Januar_Fevraglh_Març_Avrïu_Mai_Gün_Julia_Guscht_Setemvar_Listopäts_Noemvar_Zecemvar'.split('_'),
+        monthsShort : 'Jan_Fev_Mar_Avr_Mai_Gün_Jul_Gus_Set_Lis_Noe_Zec'.split('_'),
+        weekdays : 'Súladi_Lúneçi_Maitzi_Márcuri_Xhúadi_Viénerçi_Sáturi'.split('_'),
+        weekdaysShort : 'Súl_Lún_Mai_Már_Xhú_Vié_Sát'.split('_'),
+        weekdaysMin : 'Sú_Lú_Ma_Má_Xh_Vi_Sá'.split('_'),
+        longDateFormat : {
+            LT : 'HH.mm',
+            LTS : 'HH.mm.ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D. MMMM [dallas] YYYY',
+            LLL : 'D. MMMM [dallas] YYYY HH.mm',
+            LLLL : 'dddd, [li] D. MMMM [dallas] YYYY HH.mm'
+        },
+        meridiemParse: /d\'o|d\'a/i,
+        isPM : function (input) {
+            return 'd\'o' === input.toLowerCase();
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours > 11) {
+                return isLower ? 'd\'o' : 'D\'O';
+            } else {
+                return isLower ? 'd\'a' : 'D\'A';
+            }
+        },
+        calendar : {
+            sameDay : '[oxhi à] LT',
+            nextDay : '[demà à] LT',
+            nextWeek : 'dddd [à] LT',
+            lastDay : '[ieiri à] LT',
+            lastWeek : '[sür el] dddd [lasteu à] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'osprei %s',
+            past : 'ja%s',
+            s : processRelativeTime,
+            m : processRelativeTime,
+            mm : processRelativeTime,
+            h : processRelativeTime,
+            hh : processRelativeTime,
+            d : processRelativeTime,
+            dd : processRelativeTime,
+            M : processRelativeTime,
+            MM : processRelativeTime,
+            y : processRelativeTime,
+            yy : processRelativeTime
+        },
+        ordinalParse: /\d{1,2}\./,
+        ordinal : '%d.',
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    function processRelativeTime(number, withoutSuffix, key, isFuture) {
+        var format = {
+            's': ['viensas secunds', '\'iensas secunds'],
+            'm': ['\'n míut', '\'iens míut'],
+            'mm': [number + ' míuts', '' + number + ' míuts'],
+            'h': ['\'n þora', '\'iensa þora'],
+            'hh': [number + ' þoras', '' + number + ' þoras'],
+            'd': ['\'n ziua', '\'iensa ziua'],
+            'dd': [number + ' ziuas', '' + number + ' ziuas'],
+            'M': ['\'n mes', '\'iens mes'],
+            'MM': [number + ' mesen', '' + number + ' mesen'],
+            'y': ['\'n ar', '\'iens ar'],
+            'yy': [number + ' ars', '' + number + ' ars']
+        };
+        return isFuture ? format[key][0] : (withoutSuffix ? format[key][0] : format[key][1]);
+    }
+
+    return tzl;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzm-latn.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzm-latn.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzm-latn.js	(revision 12265)
@@ -0,0 +1,58 @@
+//! moment.js locale configuration
+//! locale : Central Atlas Tamazight Latin [tzm-latn]
+//! author : Abdel Said : https://github.com/abdelsaid
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var tzm_latn = moment.defineLocale('tzm-latn', {
+        months : 'innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir'.split('_'),
+        monthsShort : 'innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir'.split('_'),
+        weekdays : 'asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas'.split('_'),
+        weekdaysShort : 'asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas'.split('_'),
+        weekdaysMin : 'asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[asdkh g] LT',
+            nextDay: '[aska g] LT',
+            nextWeek: 'dddd [g] LT',
+            lastDay: '[assant g] LT',
+            lastWeek: 'dddd [g] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'dadkh s yan %s',
+            past : 'yan %s',
+            s : 'imik',
+            m : 'minuḍ',
+            mm : '%d minuḍ',
+            h : 'saɛa',
+            hh : '%d tassaɛin',
+            d : 'ass',
+            dd : '%d ossan',
+            M : 'ayowr',
+            MM : '%d iyyirn',
+            y : 'asgas',
+            yy : '%d isgasn'
+        },
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return tzm_latn;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzm.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzm.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/tzm.js	(revision 12265)
@@ -0,0 +1,58 @@
+//! moment.js locale configuration
+//! locale : Central Atlas Tamazight [tzm]
+//! author : Abdel Said : https://github.com/abdelsaid
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var tzm = moment.defineLocale('tzm', {
+        months : 'ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ'.split('_'),
+        monthsShort : 'ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ'.split('_'),
+        weekdays : 'ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ'.split('_'),
+        weekdaysShort : 'ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ'.split('_'),
+        weekdaysMin : 'ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS: 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[ⴰⵙⴷⵅ ⴴ] LT',
+            nextDay: '[ⴰⵙⴽⴰ ⴴ] LT',
+            nextWeek: 'dddd [ⴴ] LT',
+            lastDay: '[ⴰⵚⴰⵏⵜ ⴴ] LT',
+            lastWeek: 'dddd [ⴴ] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'ⴷⴰⴷⵅ ⵙ ⵢⴰⵏ %s',
+            past : 'ⵢⴰⵏ %s',
+            s : 'ⵉⵎⵉⴽ',
+            m : 'ⵎⵉⵏⵓⴺ',
+            mm : '%d ⵎⵉⵏⵓⴺ',
+            h : 'ⵙⴰⵄⴰ',
+            hh : '%d ⵜⴰⵙⵙⴰⵄⵉⵏ',
+            d : 'ⴰⵙⵙ',
+            dd : '%d oⵙⵙⴰⵏ',
+            M : 'ⴰⵢoⵓⵔ',
+            MM : '%d ⵉⵢⵢⵉⵔⵏ',
+            y : 'ⴰⵙⴳⴰⵙ',
+            yy : '%d ⵉⵙⴳⴰⵙⵏ'
+        },
+        week : {
+            dow : 6, // Saturday is the first day of the week.
+            doy : 12  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return tzm;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/uk.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/uk.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/uk.js	(revision 12265)
@@ -0,0 +1,146 @@
+//! moment.js locale configuration
+//! locale : Ukrainian [uk]
+//! author : zemlanin : https://github.com/zemlanin
+//! Author : Menelion Elensúle : https://github.com/Oire
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    function plural(word, num) {
+        var forms = word.split('_');
+        return num % 10 === 1 && num % 100 !== 11 ? forms[0] : (num % 10 >= 2 && num % 10 <= 4 && (num % 100 < 10 || num % 100 >= 20) ? forms[1] : forms[2]);
+    }
+    function relativeTimeWithPlural(number, withoutSuffix, key) {
+        var format = {
+            'mm': withoutSuffix ? 'хвилина_хвилини_хвилин' : 'хвилину_хвилини_хвилин',
+            'hh': withoutSuffix ? 'година_години_годин' : 'годину_години_годин',
+            'dd': 'день_дні_днів',
+            'MM': 'місяць_місяці_місяців',
+            'yy': 'рік_роки_років'
+        };
+        if (key === 'm') {
+            return withoutSuffix ? 'хвилина' : 'хвилину';
+        }
+        else if (key === 'h') {
+            return withoutSuffix ? 'година' : 'годину';
+        }
+        else {
+            return number + ' ' + plural(format[key], +number);
+        }
+    }
+    function weekdaysCaseReplace(m, format) {
+        var weekdays = {
+            'nominative': 'неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота'.split('_'),
+            'accusative': 'неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу'.split('_'),
+            'genitive': 'неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи'.split('_')
+        },
+        nounCase = (/(\[[ВвУу]\]) ?dddd/).test(format) ?
+            'accusative' :
+            ((/\[?(?:минулої|наступної)? ?\] ?dddd/).test(format) ?
+                'genitive' :
+                'nominative');
+        return weekdays[nounCase][m.day()];
+    }
+    function processHoursFunction(str) {
+        return function () {
+            return str + 'о' + (this.hours() === 11 ? 'б' : '') + '] LT';
+        };
+    }
+
+    var uk = moment.defineLocale('uk', {
+        months : {
+            'format': 'січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня'.split('_'),
+            'standalone': 'січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень'.split('_')
+        },
+        monthsShort : 'січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд'.split('_'),
+        weekdays : weekdaysCaseReplace,
+        weekdaysShort : 'нд_пн_вт_ср_чт_пт_сб'.split('_'),
+        weekdaysMin : 'нд_пн_вт_ср_чт_пт_сб'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD.MM.YYYY',
+            LL : 'D MMMM YYYY р.',
+            LLL : 'D MMMM YYYY р., HH:mm',
+            LLLL : 'dddd, D MMMM YYYY р., HH:mm'
+        },
+        calendar : {
+            sameDay: processHoursFunction('[Сьогодні '),
+            nextDay: processHoursFunction('[Завтра '),
+            lastDay: processHoursFunction('[Вчора '),
+            nextWeek: processHoursFunction('[У] dddd ['),
+            lastWeek: function () {
+                switch (this.day()) {
+                    case 0:
+                    case 3:
+                    case 5:
+                    case 6:
+                        return processHoursFunction('[Минулої] dddd [').call(this);
+                    case 1:
+                    case 2:
+                    case 4:
+                        return processHoursFunction('[Минулого] dddd [').call(this);
+                }
+            },
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : 'за %s',
+            past : '%s тому',
+            s : 'декілька секунд',
+            m : relativeTimeWithPlural,
+            mm : relativeTimeWithPlural,
+            h : 'годину',
+            hh : relativeTimeWithPlural,
+            d : 'день',
+            dd : relativeTimeWithPlural,
+            M : 'місяць',
+            MM : relativeTimeWithPlural,
+            y : 'рік',
+            yy : relativeTimeWithPlural
+        },
+        // M. E.: those two are virtually unused but a user might want to implement them for his/her website for some reason
+        meridiemParse: /ночі|ранку|дня|вечора/,
+        isPM: function (input) {
+            return /^(дня|вечора)$/.test(input);
+        },
+        meridiem : function (hour, minute, isLower) {
+            if (hour < 4) {
+                return 'ночі';
+            } else if (hour < 12) {
+                return 'ранку';
+            } else if (hour < 17) {
+                return 'дня';
+            } else {
+                return 'вечора';
+            }
+        },
+        ordinalParse: /\d{1,2}-(й|го)/,
+        ordinal: function (number, period) {
+            switch (period) {
+                case 'M':
+                case 'd':
+                case 'DDD':
+                case 'w':
+                case 'W':
+                    return number + '-й';
+                case 'D':
+                    return number + '-го';
+                default:
+                    return number;
+            }
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 1st is the first week of the year.
+        }
+    });
+
+    return uk;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/uz.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/uz.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/uz.js	(revision 12265)
@@ -0,0 +1,58 @@
+//! moment.js locale configuration
+//! locale : Uzbek [uz]
+//! author : Sardor Muminov : https://github.com/muminoff
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var uz = moment.defineLocale('uz', {
+        months : 'январ_феврал_март_апрел_май_июн_июл_август_сентябр_октябр_ноябр_декабр'.split('_'),
+        monthsShort : 'янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек'.split('_'),
+        weekdays : 'Якшанба_Душанба_Сешанба_Чоршанба_Пайшанба_Жума_Шанба'.split('_'),
+        weekdaysShort : 'Якш_Душ_Сеш_Чор_Пай_Жум_Шан'.split('_'),
+        weekdaysMin : 'Як_Ду_Се_Чо_Па_Жу_Ша'.split('_'),
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'D MMMM YYYY, dddd HH:mm'
+        },
+        calendar : {
+            sameDay : '[Бугун соат] LT [да]',
+            nextDay : '[Эртага] LT [да]',
+            nextWeek : 'dddd [куни соат] LT [да]',
+            lastDay : '[Кеча соат] LT [да]',
+            lastWeek : '[Утган] dddd [куни соат] LT [да]',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'Якин %s ичида',
+            past : 'Бир неча %s олдин',
+            s : 'фурсат',
+            m : 'бир дакика',
+            mm : '%d дакика',
+            h : 'бир соат',
+            hh : '%d соат',
+            d : 'бир кун',
+            dd : '%d кун',
+            M : 'бир ой',
+            MM : '%d ой',
+            y : 'бир йил',
+            yy : '%d йил'
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 7  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return uz;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/vi.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/vi.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/vi.js	(revision 12265)
@@ -0,0 +1,79 @@
+//! moment.js locale configuration
+//! locale : Vietnamese [vi]
+//! author : Bang Nguyen : https://github.com/bangnk
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var vi = moment.defineLocale('vi', {
+        months : 'tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12'.split('_'),
+        monthsShort : 'Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy'.split('_'),
+        weekdaysShort : 'CN_T2_T3_T4_T5_T6_T7'.split('_'),
+        weekdaysMin : 'CN_T2_T3_T4_T5_T6_T7'.split('_'),
+        weekdaysParseExact : true,
+        meridiemParse: /sa|ch/i,
+        isPM : function (input) {
+            return /^ch$/i.test(input);
+        },
+        meridiem : function (hours, minutes, isLower) {
+            if (hours < 12) {
+                return isLower ? 'sa' : 'SA';
+            } else {
+                return isLower ? 'ch' : 'CH';
+            }
+        },
+        longDateFormat : {
+            LT : 'HH:mm',
+            LTS : 'HH:mm:ss',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM [năm] YYYY',
+            LLL : 'D MMMM [năm] YYYY HH:mm',
+            LLLL : 'dddd, D MMMM [năm] YYYY HH:mm',
+            l : 'DD/M/YYYY',
+            ll : 'D MMM YYYY',
+            lll : 'D MMM YYYY HH:mm',
+            llll : 'ddd, D MMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay: '[Hôm nay lúc] LT',
+            nextDay: '[Ngày mai lúc] LT',
+            nextWeek: 'dddd [tuần tới lúc] LT',
+            lastDay: '[Hôm qua lúc] LT',
+            lastWeek: 'dddd [tuần rồi lúc] LT',
+            sameElse: 'L'
+        },
+        relativeTime : {
+            future : '%s tới',
+            past : '%s trước',
+            s : 'vài giây',
+            m : 'một phút',
+            mm : '%d phút',
+            h : 'một giờ',
+            hh : '%d giờ',
+            d : 'một ngày',
+            dd : '%d ngày',
+            M : 'một tháng',
+            MM : '%d tháng',
+            y : 'một năm',
+            yy : '%d năm'
+        },
+        ordinalParse: /\d{1,2}/,
+        ordinal : function (number) {
+            return number;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return vi;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/x-pseudo.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/x-pseudo.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/x-pseudo.js	(revision 12265)
@@ -0,0 +1,68 @@
+//! moment.js locale configuration
+//! locale : Pseudo [x-pseudo]
+//! author : Andrew Hood : https://github.com/andrewhood125
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var x_pseudo = moment.defineLocale('x-pseudo', {
+        months : 'J~áñúá~rý_F~ébrú~árý_~Márc~h_Áp~ríl_~Máý_~Júñé~_Júl~ý_Áú~gúst~_Sép~témb~ér_Ó~ctób~ér_Ñ~óvém~bér_~Décé~mbér'.split('_'),
+        monthsShort : 'J~áñ_~Féb_~Már_~Ápr_~Máý_~Júñ_~Júl_~Áúg_~Sép_~Óct_~Ñóv_~Déc'.split('_'),
+        monthsParseExact : true,
+        weekdays : 'S~úñdá~ý_Mó~ñdáý~_Túé~sdáý~_Wéd~ñésd~áý_T~húrs~dáý_~Fríd~áý_S~átúr~dáý'.split('_'),
+        weekdaysShort : 'S~úñ_~Móñ_~Túé_~Wéd_~Thú_~Frí_~Sát'.split('_'),
+        weekdaysMin : 'S~ú_Mó~_Tú_~Wé_T~h_Fr~_Sá'.split('_'),
+        weekdaysParseExact : true,
+        longDateFormat : {
+            LT : 'HH:mm',
+            L : 'DD/MM/YYYY',
+            LL : 'D MMMM YYYY',
+            LLL : 'D MMMM YYYY HH:mm',
+            LLLL : 'dddd, D MMMM YYYY HH:mm'
+        },
+        calendar : {
+            sameDay : '[T~ódá~ý át] LT',
+            nextDay : '[T~ómó~rró~w át] LT',
+            nextWeek : 'dddd [át] LT',
+            lastDay : '[Ý~ést~érdá~ý át] LT',
+            lastWeek : '[L~ást] dddd [át] LT',
+            sameElse : 'L'
+        },
+        relativeTime : {
+            future : 'í~ñ %s',
+            past : '%s á~gó',
+            s : 'á ~féw ~sécó~ñds',
+            m : 'á ~míñ~úté',
+            mm : '%d m~íñú~tés',
+            h : 'á~ñ hó~úr',
+            hh : '%d h~óúrs',
+            d : 'á ~dáý',
+            dd : '%d d~áýs',
+            M : 'á ~móñ~th',
+            MM : '%d m~óñt~hs',
+            y : 'á ~ýéár',
+            yy : '%d ý~éárs'
+        },
+        ordinalParse: /\d{1,2}(th|st|nd|rd)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (~~(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        },
+        week : {
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return x_pseudo;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-cn.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-cn.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-cn.js	(revision 12265)
@@ -0,0 +1,127 @@
+//! moment.js locale configuration
+//! locale : Chinese (China) [zh-cn]
+//! author : suupic : https://github.com/suupic
+//! author : Zeno Zeng : https://github.com/zenozeng
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var zh_cn = moment.defineLocale('zh-cn', {
+        months : '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
+        monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
+        weekdays : '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
+        weekdaysShort : '周日_周一_周二_周三_周四_周五_周六'.split('_'),
+        weekdaysMin : '日_一_二_三_四_五_六'.split('_'),
+        longDateFormat : {
+            LT : 'Ah点mm分',
+            LTS : 'Ah点m分s秒',
+            L : 'YYYY-MM-DD',
+            LL : 'YYYY年MMMD日',
+            LLL : 'YYYY年MMMD日Ah点mm分',
+            LLLL : 'YYYY年MMMD日ddddAh点mm分',
+            l : 'YYYY-MM-DD',
+            ll : 'YYYY年MMMD日',
+            lll : 'YYYY年MMMD日Ah点mm分',
+            llll : 'YYYY年MMMD日ddddAh点mm分'
+        },
+        meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
+        meridiemHour: function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === '凌晨' || meridiem === '早上' ||
+                    meridiem === '上午') {
+                return hour;
+            } else if (meridiem === '下午' || meridiem === '晚上') {
+                return hour + 12;
+            } else {
+                // '中午'
+                return hour >= 11 ? hour : hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            var hm = hour * 100 + minute;
+            if (hm < 600) {
+                return '凌晨';
+            } else if (hm < 900) {
+                return '早上';
+            } else if (hm < 1130) {
+                return '上午';
+            } else if (hm < 1230) {
+                return '中午';
+            } else if (hm < 1800) {
+                return '下午';
+            } else {
+                return '晚上';
+            }
+        },
+        calendar : {
+            sameDay : function () {
+                return this.minutes() === 0 ? '[今天]Ah[点整]' : '[今天]LT';
+            },
+            nextDay : function () {
+                return this.minutes() === 0 ? '[明天]Ah[点整]' : '[明天]LT';
+            },
+            lastDay : function () {
+                return this.minutes() === 0 ? '[昨天]Ah[点整]' : '[昨天]LT';
+            },
+            nextWeek : function () {
+                var startOfWeek, prefix;
+                startOfWeek = moment().startOf('week');
+                prefix = this.diff(startOfWeek, 'days') >= 7 ? '[下]' : '[本]';
+                return this.minutes() === 0 ? prefix + 'dddAh点整' : prefix + 'dddAh点mm';
+            },
+            lastWeek : function () {
+                var startOfWeek, prefix;
+                startOfWeek = moment().startOf('week');
+                prefix = this.unix() < startOfWeek.unix()  ? '[上]' : '[本]';
+                return this.minutes() === 0 ? prefix + 'dddAh点整' : prefix + 'dddAh点mm';
+            },
+            sameElse : 'LL'
+        },
+        ordinalParse: /\d{1,2}(日|月|周)/,
+        ordinal : function (number, period) {
+            switch (period) {
+                case 'd':
+                case 'D':
+                case 'DDD':
+                    return number + '日';
+                case 'M':
+                    return number + '月';
+                case 'w':
+                case 'W':
+                    return number + '周';
+                default:
+                    return number;
+            }
+        },
+        relativeTime : {
+            future : '%s内',
+            past : '%s前',
+            s : '几秒',
+            m : '1 分钟',
+            mm : '%d 分钟',
+            h : '1 小时',
+            hh : '%d 小时',
+            d : '1 天',
+            dd : '%d 天',
+            M : '1 个月',
+            MM : '%d 个月',
+            y : '1 年',
+            yy : '%d 年'
+        },
+        week : {
+            // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
+            dow : 1, // Monday is the first day of the week.
+            doy : 4  // The week that contains Jan 4th is the first week of the year.
+        }
+    });
+
+    return zh_cn;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-hk.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-hk.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-hk.js	(revision 12265)
@@ -0,0 +1,105 @@
+//! moment.js locale configuration
+//! locale : Chinese (Hong Kong) [zh-hk]
+//! author : Ben : https://github.com/ben-lin
+//! author : Chris Lam : https://github.com/hehachris
+//! author : Konstantin : https://github.com/skfd
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var zh_hk = moment.defineLocale('zh-hk', {
+        months : '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
+        monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
+        weekdays : '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
+        weekdaysShort : '週日_週一_週二_週三_週四_週五_週六'.split('_'),
+        weekdaysMin : '日_一_二_三_四_五_六'.split('_'),
+        longDateFormat : {
+            LT : 'Ah點mm分',
+            LTS : 'Ah點m分s秒',
+            L : 'YYYY年MMMD日',
+            LL : 'YYYY年MMMD日',
+            LLL : 'YYYY年MMMD日Ah點mm分',
+            LLLL : 'YYYY年MMMD日ddddAh點mm分',
+            l : 'YYYY年MMMD日',
+            ll : 'YYYY年MMMD日',
+            lll : 'YYYY年MMMD日Ah點mm分',
+            llll : 'YYYY年MMMD日ddddAh點mm分'
+        },
+        meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === '凌晨' || meridiem === '早上' || meridiem === '上午') {
+                return hour;
+            } else if (meridiem === '中午') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === '下午' || meridiem === '晚上') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            var hm = hour * 100 + minute;
+            if (hm < 600) {
+                return '凌晨';
+            } else if (hm < 900) {
+                return '早上';
+            } else if (hm < 1130) {
+                return '上午';
+            } else if (hm < 1230) {
+                return '中午';
+            } else if (hm < 1800) {
+                return '下午';
+            } else {
+                return '晚上';
+            }
+        },
+        calendar : {
+            sameDay : '[今天]LT',
+            nextDay : '[明天]LT',
+            nextWeek : '[下]ddddLT',
+            lastDay : '[昨天]LT',
+            lastWeek : '[上]ddddLT',
+            sameElse : 'L'
+        },
+        ordinalParse: /\d{1,2}(日|月|週)/,
+        ordinal : function (number, period) {
+            switch (period) {
+                case 'd' :
+                case 'D' :
+                case 'DDD' :
+                    return number + '日';
+                case 'M' :
+                    return number + '月';
+                case 'w' :
+                case 'W' :
+                    return number + '週';
+                default :
+                    return number;
+            }
+        },
+        relativeTime : {
+            future : '%s內',
+            past : '%s前',
+            s : '幾秒',
+            m : '1 分鐘',
+            mm : '%d 分鐘',
+            h : '1 小時',
+            hh : '%d 小時',
+            d : '1 天',
+            dd : '%d 天',
+            M : '1 個月',
+            MM : '%d 個月',
+            y : '1 年',
+            yy : '%d 年'
+        }
+    });
+
+    return zh_hk;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-tw.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-tw.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/locale/zh-tw.js	(revision 12265)
@@ -0,0 +1,104 @@
+//! moment.js locale configuration
+//! locale : Chinese (Taiwan) [zh-tw]
+//! author : Ben : https://github.com/ben-lin
+//! author : Chris Lam : https://github.com/hehachris
+
+;(function (global, factory) {
+   typeof exports === 'object' && typeof module !== 'undefined'
+       && typeof require === 'function' ? factory(require('../moment')) :
+   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
+   factory(global.moment)
+}(this, function (moment) { 'use strict';
+
+
+    var zh_tw = moment.defineLocale('zh-tw', {
+        months : '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
+        monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
+        weekdays : '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
+        weekdaysShort : '週日_週一_週二_週三_週四_週五_週六'.split('_'),
+        weekdaysMin : '日_一_二_三_四_五_六'.split('_'),
+        longDateFormat : {
+            LT : 'Ah點mm分',
+            LTS : 'Ah點m分s秒',
+            L : 'YYYY年MMMD日',
+            LL : 'YYYY年MMMD日',
+            LLL : 'YYYY年MMMD日Ah點mm分',
+            LLLL : 'YYYY年MMMD日ddddAh點mm分',
+            l : 'YYYY年MMMD日',
+            ll : 'YYYY年MMMD日',
+            lll : 'YYYY年MMMD日Ah點mm分',
+            llll : 'YYYY年MMMD日ddddAh點mm分'
+        },
+        meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
+        meridiemHour : function (hour, meridiem) {
+            if (hour === 12) {
+                hour = 0;
+            }
+            if (meridiem === '凌晨' || meridiem === '早上' || meridiem === '上午') {
+                return hour;
+            } else if (meridiem === '中午') {
+                return hour >= 11 ? hour : hour + 12;
+            } else if (meridiem === '下午' || meridiem === '晚上') {
+                return hour + 12;
+            }
+        },
+        meridiem : function (hour, minute, isLower) {
+            var hm = hour * 100 + minute;
+            if (hm < 600) {
+                return '凌晨';
+            } else if (hm < 900) {
+                return '早上';
+            } else if (hm < 1130) {
+                return '上午';
+            } else if (hm < 1230) {
+                return '中午';
+            } else if (hm < 1800) {
+                return '下午';
+            } else {
+                return '晚上';
+            }
+        },
+        calendar : {
+            sameDay : '[今天]LT',
+            nextDay : '[明天]LT',
+            nextWeek : '[下]ddddLT',
+            lastDay : '[昨天]LT',
+            lastWeek : '[上]ddddLT',
+            sameElse : 'L'
+        },
+        ordinalParse: /\d{1,2}(日|月|週)/,
+        ordinal : function (number, period) {
+            switch (period) {
+                case 'd' :
+                case 'D' :
+                case 'DDD' :
+                    return number + '日';
+                case 'M' :
+                    return number + '月';
+                case 'w' :
+                case 'W' :
+                    return number + '週';
+                default :
+                    return number;
+            }
+        },
+        relativeTime : {
+            future : '%s內',
+            past : '%s前',
+            s : '幾秒',
+            m : '1 分鐘',
+            mm : '%d 分鐘',
+            h : '1 小時',
+            hh : '%d 小時',
+            d : '1 天',
+            dd : '%d 天',
+            M : '1 個月',
+            MM : '%d 個月',
+            y : '1 年',
+            yy : '%d 年'
+        }
+    });
+
+    return zh_tw;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/moment.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/moment.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/vendor/moment-js/moment.js	(revision 12265)
@@ -0,0 +1,4234 @@
+//! moment.js
+//! version : 2.15.1
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+
+;(function (global, factory) {
+    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+    typeof define === 'function' && define.amd ? define(factory) :
+    global.moment = factory()
+}(this, function () { 'use strict';
+
+    var hookCallback;
+
+    function utils_hooks__hooks () {
+        return hookCallback.apply(null, arguments);
+    }
+
+    // This is done to register the method called with moment()
+    // without creating circular dependencies.
+    function setHookCallback (callback) {
+        hookCallback = callback;
+    }
+
+    function isArray(input) {
+        return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]';
+    }
+
+    function isObject(input) {
+        // IE8 will treat undefined and null as object if it wasn't for
+        // input != null
+        return input != null && Object.prototype.toString.call(input) === '[object Object]';
+    }
+
+    function isObjectEmpty(obj) {
+        var k;
+        for (k in obj) {
+            // even if its not own property I'd still call it non-empty
+            return false;
+        }
+        return true;
+    }
+
+    function isDate(input) {
+        return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
+    }
+
+    function map(arr, fn) {
+        var res = [], i;
+        for (i = 0; i < arr.length; ++i) {
+            res.push(fn(arr[i], i));
+        }
+        return res;
+    }
+
+    function hasOwnProp(a, b) {
+        return Object.prototype.hasOwnProperty.call(a, b);
+    }
+
+    function extend(a, b) {
+        for (var i in b) {
+            if (hasOwnProp(b, i)) {
+                a[i] = b[i];
+            }
+        }
+
+        if (hasOwnProp(b, 'toString')) {
+            a.toString = b.toString;
+        }
+
+        if (hasOwnProp(b, 'valueOf')) {
+            a.valueOf = b.valueOf;
+        }
+
+        return a;
+    }
+
+    function create_utc__createUTC (input, format, locale, strict) {
+        return createLocalOrUTC(input, format, locale, strict, true).utc();
+    }
+
+    function defaultParsingFlags() {
+        // We need to deep clone this object.
+        return {
+            empty           : false,
+            unusedTokens    : [],
+            unusedInput     : [],
+            overflow        : -2,
+            charsLeftOver   : 0,
+            nullInput       : false,
+            invalidMonth    : null,
+            invalidFormat   : false,
+            userInvalidated : false,
+            iso             : false,
+            parsedDateParts : [],
+            meridiem        : null
+        };
+    }
+
+    function getParsingFlags(m) {
+        if (m._pf == null) {
+            m._pf = defaultParsingFlags();
+        }
+        return m._pf;
+    }
+
+    var some;
+    if (Array.prototype.some) {
+        some = Array.prototype.some;
+    } else {
+        some = function (fun) {
+            var t = Object(this);
+            var len = t.length >>> 0;
+
+            for (var i = 0; i < len; i++) {
+                if (i in t && fun.call(this, t[i], i, t)) {
+                    return true;
+                }
+            }
+
+            return false;
+        };
+    }
+
+    function valid__isValid(m) {
+        if (m._isValid == null) {
+            var flags = getParsingFlags(m);
+            var parsedParts = some.call(flags.parsedDateParts, function (i) {
+                return i != null;
+            });
+            var isNowValid = !isNaN(m._d.getTime()) &&
+                flags.overflow < 0 &&
+                !flags.empty &&
+                !flags.invalidMonth &&
+                !flags.invalidWeekday &&
+                !flags.nullInput &&
+                !flags.invalidFormat &&
+                !flags.userInvalidated &&
+                (!flags.meridiem || (flags.meridiem && parsedParts));
+
+            if (m._strict) {
+                isNowValid = isNowValid &&
+                    flags.charsLeftOver === 0 &&
+                    flags.unusedTokens.length === 0 &&
+                    flags.bigHour === undefined;
+            }
+
+            if (Object.isFrozen == null || !Object.isFrozen(m)) {
+                m._isValid = isNowValid;
+            }
+            else {
+                return isNowValid;
+            }
+        }
+        return m._isValid;
+    }
+
+    function valid__createInvalid (flags) {
+        var m = create_utc__createUTC(NaN);
+        if (flags != null) {
+            extend(getParsingFlags(m), flags);
+        }
+        else {
+            getParsingFlags(m).userInvalidated = true;
+        }
+
+        return m;
+    }
+
+    function isUndefined(input) {
+        return input === void 0;
+    }
+
+    // Plugins that add properties should also add the key here (null value),
+    // so we can properly clone ourselves.
+    var momentProperties = utils_hooks__hooks.momentProperties = [];
+
+    function copyConfig(to, from) {
+        var i, prop, val;
+
+        if (!isUndefined(from._isAMomentObject)) {
+            to._isAMomentObject = from._isAMomentObject;
+        }
+        if (!isUndefined(from._i)) {
+            to._i = from._i;
+        }
+        if (!isUndefined(from._f)) {
+            to._f = from._f;
+        }
+        if (!isUndefined(from._l)) {
+            to._l = from._l;
+        }
+        if (!isUndefined(from._strict)) {
+            to._strict = from._strict;
+        }
+        if (!isUndefined(from._tzm)) {
+            to._tzm = from._tzm;
+        }
+        if (!isUndefined(from._isUTC)) {
+            to._isUTC = from._isUTC;
+        }
+        if (!isUndefined(from._offset)) {
+            to._offset = from._offset;
+        }
+        if (!isUndefined(from._pf)) {
+            to._pf = getParsingFlags(from);
+        }
+        if (!isUndefined(from._locale)) {
+            to._locale = from._locale;
+        }
+
+        if (momentProperties.length > 0) {
+            for (i in momentProperties) {
+                prop = momentProperties[i];
+                val = from[prop];
+                if (!isUndefined(val)) {
+                    to[prop] = val;
+                }
+            }
+        }
+
+        return to;
+    }
+
+    var updateInProgress = false;
+
+    // Moment prototype object
+    function Moment(config) {
+        copyConfig(this, config);
+        this._d = new Date(config._d != null ? config._d.getTime() : NaN);
+        // Prevent infinite loop in case updateOffset creates new moment
+        // objects.
+        if (updateInProgress === false) {
+            updateInProgress = true;
+            utils_hooks__hooks.updateOffset(this);
+            updateInProgress = false;
+        }
+    }
+
+    function isMoment (obj) {
+        return obj instanceof Moment || (obj != null && obj._isAMomentObject != null);
+    }
+
+    function absFloor (number) {
+        if (number < 0) {
+            // -0 -> 0
+            return Math.ceil(number) || 0;
+        } else {
+            return Math.floor(number);
+        }
+    }
+
+    function toInt(argumentForCoercion) {
+        var coercedNumber = +argumentForCoercion,
+            value = 0;
+
+        if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+            value = absFloor(coercedNumber);
+        }
+
+        return value;
+    }
+
+    // compare two arrays, return the number of differences
+    function compareArrays(array1, array2, dontConvert) {
+        var len = Math.min(array1.length, array2.length),
+            lengthDiff = Math.abs(array1.length - array2.length),
+            diffs = 0,
+            i;
+        for (i = 0; i < len; i++) {
+            if ((dontConvert && array1[i] !== array2[i]) ||
+                (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+                diffs++;
+            }
+        }
+        return diffs + lengthDiff;
+    }
+
+    function warn(msg) {
+        if (utils_hooks__hooks.suppressDeprecationWarnings === false &&
+                (typeof console !==  'undefined') && console.warn) {
+            console.warn('Deprecation warning: ' + msg);
+        }
+    }
+
+    function deprecate(msg, fn) {
+        var firstTime = true;
+
+        return extend(function () {
+            if (utils_hooks__hooks.deprecationHandler != null) {
+                utils_hooks__hooks.deprecationHandler(null, msg);
+            }
+            if (firstTime) {
+                var args = [];
+                var arg;
+                for (var i = 0; i < arguments.length; i++) {
+                    arg = '';
+                    if (typeof arguments[i] === 'object') {
+                        arg += '\n[' + i + '] ';
+                        for (var key in arguments[0]) {
+                            arg += key + ': ' + arguments[0][key] + ', ';
+                        }
+                        arg = arg.slice(0, -2); // Remove trailing comma and space
+                    } else {
+                        arg = arguments[i];
+                    }
+                    args.push(arg);
+                }
+                warn(msg + '\nArguments: ' + Array.prototype.slice.call(args).join('') + '\n' + (new Error()).stack);
+                firstTime = false;
+            }
+            return fn.apply(this, arguments);
+        }, fn);
+    }
+
+    var deprecations = {};
+
+    function deprecateSimple(name, msg) {
+        if (utils_hooks__hooks.deprecationHandler != null) {
+            utils_hooks__hooks.deprecationHandler(name, msg);
+        }
+        if (!deprecations[name]) {
+            warn(msg);
+            deprecations[name] = true;
+        }
+    }
+
+    utils_hooks__hooks.suppressDeprecationWarnings = false;
+    utils_hooks__hooks.deprecationHandler = null;
+
+    function isFunction(input) {
+        return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]';
+    }
+
+    function locale_set__set (config) {
+        var prop, i;
+        for (i in config) {
+            prop = config[i];
+            if (isFunction(prop)) {
+                this[i] = prop;
+            } else {
+                this['_' + i] = prop;
+            }
+        }
+        this._config = config;
+        // Lenient ordinal parsing accepts just a number in addition to
+        // number + (possibly) stuff coming from _ordinalParseLenient.
+        this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source);
+    }
+
+    function mergeConfigs(parentConfig, childConfig) {
+        var res = extend({}, parentConfig), prop;
+        for (prop in childConfig) {
+            if (hasOwnProp(childConfig, prop)) {
+                if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) {
+                    res[prop] = {};
+                    extend(res[prop], parentConfig[prop]);
+                    extend(res[prop], childConfig[prop]);
+                } else if (childConfig[prop] != null) {
+                    res[prop] = childConfig[prop];
+                } else {
+                    delete res[prop];
+                }
+            }
+        }
+        for (prop in parentConfig) {
+            if (hasOwnProp(parentConfig, prop) &&
+                    !hasOwnProp(childConfig, prop) &&
+                    isObject(parentConfig[prop])) {
+                // make sure changes to properties don't modify parent config
+                res[prop] = extend({}, res[prop]);
+            }
+        }
+        return res;
+    }
+
+    function Locale(config) {
+        if (config != null) {
+            this.set(config);
+        }
+    }
+
+    var keys;
+
+    if (Object.keys) {
+        keys = Object.keys;
+    } else {
+        keys = function (obj) {
+            var i, res = [];
+            for (i in obj) {
+                if (hasOwnProp(obj, i)) {
+                    res.push(i);
+                }
+            }
+            return res;
+        };
+    }
+
+    var defaultCalendar = {
+        sameDay : '[Today at] LT',
+        nextDay : '[Tomorrow at] LT',
+        nextWeek : 'dddd [at] LT',
+        lastDay : '[Yesterday at] LT',
+        lastWeek : '[Last] dddd [at] LT',
+        sameElse : 'L'
+    };
+
+    function locale_calendar__calendar (key, mom, now) {
+        var output = this._calendar[key] || this._calendar['sameElse'];
+        return isFunction(output) ? output.call(mom, now) : output;
+    }
+
+    var defaultLongDateFormat = {
+        LTS  : 'h:mm:ss A',
+        LT   : 'h:mm A',
+        L    : 'MM/DD/YYYY',
+        LL   : 'MMMM D, YYYY',
+        LLL  : 'MMMM D, YYYY h:mm A',
+        LLLL : 'dddd, MMMM D, YYYY h:mm A'
+    };
+
+    function longDateFormat (key) {
+        var format = this._longDateFormat[key],
+            formatUpper = this._longDateFormat[key.toUpperCase()];
+
+        if (format || !formatUpper) {
+            return format;
+        }
+
+        this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) {
+            return val.slice(1);
+        });
+
+        return this._longDateFormat[key];
+    }
+
+    var defaultInvalidDate = 'Invalid date';
+
+    function invalidDate () {
+        return this._invalidDate;
+    }
+
+    var defaultOrdinal = '%d';
+    var defaultOrdinalParse = /\d{1,2}/;
+
+    function ordinal (number) {
+        return this._ordinal.replace('%d', number);
+    }
+
+    var defaultRelativeTime = {
+        future : 'in %s',
+        past   : '%s ago',
+        s  : 'a few seconds',
+        m  : 'a minute',
+        mm : '%d minutes',
+        h  : 'an hour',
+        hh : '%d hours',
+        d  : 'a day',
+        dd : '%d days',
+        M  : 'a month',
+        MM : '%d months',
+        y  : 'a year',
+        yy : '%d years'
+    };
+
+    function relative__relativeTime (number, withoutSuffix, string, isFuture) {
+        var output = this._relativeTime[string];
+        return (isFunction(output)) ?
+            output(number, withoutSuffix, string, isFuture) :
+            output.replace(/%d/i, number);
+    }
+
+    function pastFuture (diff, output) {
+        var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+        return isFunction(format) ? format(output) : format.replace(/%s/i, output);
+    }
+
+    var aliases = {};
+
+    function addUnitAlias (unit, shorthand) {
+        var lowerCase = unit.toLowerCase();
+        aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit;
+    }
+
+    function normalizeUnits(units) {
+        return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined;
+    }
+
+    function normalizeObjectUnits(inputObject) {
+        var normalizedInput = {},
+            normalizedProp,
+            prop;
+
+        for (prop in inputObject) {
+            if (hasOwnProp(inputObject, prop)) {
+                normalizedProp = normalizeUnits(prop);
+                if (normalizedProp) {
+                    normalizedInput[normalizedProp] = inputObject[prop];
+                }
+            }
+        }
+
+        return normalizedInput;
+    }
+
+    var priorities = {};
+
+    function addUnitPriority(unit, priority) {
+        priorities[unit] = priority;
+    }
+
+    function getPrioritizedUnits(unitsObj) {
+        var units = [];
+        for (var u in unitsObj) {
+            units.push({unit: u, priority: priorities[u]});
+        }
+        units.sort(function (a, b) {
+            return a.priority - b.priority;
+        });
+        return units;
+    }
+
+    function makeGetSet (unit, keepTime) {
+        return function (value) {
+            if (value != null) {
+                get_set__set(this, unit, value);
+                utils_hooks__hooks.updateOffset(this, keepTime);
+                return this;
+            } else {
+                return get_set__get(this, unit);
+            }
+        };
+    }
+
+    function get_set__get (mom, unit) {
+        return mom.isValid() ?
+            mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN;
+    }
+
+    function get_set__set (mom, unit, value) {
+        if (mom.isValid()) {
+            mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+        }
+    }
+
+    // MOMENTS
+
+    function stringGet (units) {
+        units = normalizeUnits(units);
+        if (isFunction(this[units])) {
+            return this[units]();
+        }
+        return this;
+    }
+
+
+    function stringSet (units, value) {
+        if (typeof units === 'object') {
+            units = normalizeObjectUnits(units);
+            var prioritized = getPrioritizedUnits(units);
+            for (var i = 0; i < prioritized.length; i++) {
+                this[prioritized[i].unit](units[prioritized[i].unit]);
+            }
+        } else {
+            units = normalizeUnits(units);
+            if (isFunction(this[units])) {
+                return this[units](value);
+            }
+        }
+        return this;
+    }
+
+    function zeroFill(number, targetLength, forceSign) {
+        var absNumber = '' + Math.abs(number),
+            zerosToFill = targetLength - absNumber.length,
+            sign = number >= 0;
+        return (sign ? (forceSign ? '+' : '') : '-') +
+            Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber;
+    }
+
+    var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;
+
+    var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g;
+
+    var formatFunctions = {};
+
+    var formatTokenFunctions = {};
+
+    // token:    'M'
+    // padded:   ['MM', 2]
+    // ordinal:  'Mo'
+    // callback: function () { this.month() + 1 }
+    function addFormatToken (token, padded, ordinal, callback) {
+        var func = callback;
+        if (typeof callback === 'string') {
+            func = function () {
+                return this[callback]();
+            };
+        }
+        if (token) {
+            formatTokenFunctions[token] = func;
+        }
+        if (padded) {
+            formatTokenFunctions[padded[0]] = function () {
+                return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
+            };
+        }
+        if (ordinal) {
+            formatTokenFunctions[ordinal] = function () {
+                return this.localeData().ordinal(func.apply(this, arguments), token);
+            };
+        }
+    }
+
+    function removeFormattingTokens(input) {
+        if (input.match(/\[[\s\S]/)) {
+            return input.replace(/^\[|\]$/g, '');
+        }
+        return input.replace(/\\/g, '');
+    }
+
+    function makeFormatFunction(format) {
+        var array = format.match(formattingTokens), i, length;
+
+        for (i = 0, length = array.length; i < length; i++) {
+            if (formatTokenFunctions[array[i]]) {
+                array[i] = formatTokenFunctions[array[i]];
+            } else {
+                array[i] = removeFormattingTokens(array[i]);
+            }
+        }
+
+        return function (mom) {
+            var output = '', i;
+            for (i = 0; i < length; i++) {
+                output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+            }
+            return output;
+        };
+    }
+
+    // format date using native date object
+    function formatMoment(m, format) {
+        if (!m.isValid()) {
+            return m.localeData().invalidDate();
+        }
+
+        format = expandFormat(format, m.localeData());
+        formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format);
+
+        return formatFunctions[format](m);
+    }
+
+    function expandFormat(format, locale) {
+        var i = 5;
+
+        function replaceLongDateFormatTokens(input) {
+            return locale.longDateFormat(input) || input;
+        }
+
+        localFormattingTokens.lastIndex = 0;
+        while (i >= 0 && localFormattingTokens.test(format)) {
+            format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+            localFormattingTokens.lastIndex = 0;
+            i -= 1;
+        }
+
+        return format;
+    }
+
+    var match1         = /\d/;            //       0 - 9
+    var match2         = /\d\d/;          //      00 - 99
+    var match3         = /\d{3}/;         //     000 - 999
+    var match4         = /\d{4}/;         //    0000 - 9999
+    var match6         = /[+-]?\d{6}/;    // -999999 - 999999
+    var match1to2      = /\d\d?/;         //       0 - 99
+    var match3to4      = /\d\d\d\d?/;     //     999 - 9999
+    var match5to6      = /\d\d\d\d\d\d?/; //   99999 - 999999
+    var match1to3      = /\d{1,3}/;       //       0 - 999
+    var match1to4      = /\d{1,4}/;       //       0 - 9999
+    var match1to6      = /[+-]?\d{1,6}/;  // -999999 - 999999
+
+    var matchUnsigned  = /\d+/;           //       0 - inf
+    var matchSigned    = /[+-]?\d+/;      //    -inf - inf
+
+    var matchOffset    = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z
+    var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z
+
+    var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123
+
+    // any word (or two) characters or numbers including two/three word month in arabic.
+    // includes scottish gaelic two word and hyphenated months
+    var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
+
+
+    var regexes = {};
+
+    function addRegexToken (token, regex, strictRegex) {
+        regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) {
+            return (isStrict && strictRegex) ? strictRegex : regex;
+        };
+    }
+
+    function getParseRegexForToken (token, config) {
+        if (!hasOwnProp(regexes, token)) {
+            return new RegExp(unescapeFormat(token));
+        }
+
+        return regexes[token](config._strict, config._locale);
+    }
+
+    // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+    function unescapeFormat(s) {
+        return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+            return p1 || p2 || p3 || p4;
+        }));
+    }
+
+    function regexEscape(s) {
+        return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+    }
+
+    var tokens = {};
+
+    function addParseToken (token, callback) {
+        var i, func = callback;
+        if (typeof token === 'string') {
+            token = [token];
+        }
+        if (typeof callback === 'number') {
+            func = function (input, array) {
+                array[callback] = toInt(input);
+            };
+        }
+        for (i = 0; i < token.length; i++) {
+            tokens[token[i]] = func;
+        }
+    }
+
+    function addWeekParseToken (token, callback) {
+        addParseToken(token, function (input, array, config, token) {
+            config._w = config._w || {};
+            callback(input, config._w, config, token);
+        });
+    }
+
+    function addTimeToArrayFromToken(token, input, config) {
+        if (input != null && hasOwnProp(tokens, token)) {
+            tokens[token](input, config._a, config, token);
+        }
+    }
+
+    var YEAR = 0;
+    var MONTH = 1;
+    var DATE = 2;
+    var HOUR = 3;
+    var MINUTE = 4;
+    var SECOND = 5;
+    var MILLISECOND = 6;
+    var WEEK = 7;
+    var WEEKDAY = 8;
+
+    var indexOf;
+
+    if (Array.prototype.indexOf) {
+        indexOf = Array.prototype.indexOf;
+    } else {
+        indexOf = function (o) {
+            // I know
+            var i;
+            for (i = 0; i < this.length; ++i) {
+                if (this[i] === o) {
+                    return i;
+                }
+            }
+            return -1;
+        };
+    }
+
+    function daysInMonth(year, month) {
+        return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+    }
+
+    // FORMATTING
+
+    addFormatToken('M', ['MM', 2], 'Mo', function () {
+        return this.month() + 1;
+    });
+
+    addFormatToken('MMM', 0, 0, function (format) {
+        return this.localeData().monthsShort(this, format);
+    });
+
+    addFormatToken('MMMM', 0, 0, function (format) {
+        return this.localeData().months(this, format);
+    });
+
+    // ALIASES
+
+    addUnitAlias('month', 'M');
+
+    // PRIORITY
+
+    addUnitPriority('month', 8);
+
+    // PARSING
+
+    addRegexToken('M',    match1to2);
+    addRegexToken('MM',   match1to2, match2);
+    addRegexToken('MMM',  function (isStrict, locale) {
+        return locale.monthsShortRegex(isStrict);
+    });
+    addRegexToken('MMMM', function (isStrict, locale) {
+        return locale.monthsRegex(isStrict);
+    });
+
+    addParseToken(['M', 'MM'], function (input, array) {
+        array[MONTH] = toInt(input) - 1;
+    });
+
+    addParseToken(['MMM', 'MMMM'], function (input, array, config, token) {
+        var month = config._locale.monthsParse(input, token, config._strict);
+        // if we didn't find a month name, mark the date as invalid.
+        if (month != null) {
+            array[MONTH] = month;
+        } else {
+            getParsingFlags(config).invalidMonth = input;
+        }
+    });
+
+    // LOCALES
+
+    var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/;
+    var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_');
+    function localeMonths (m, format) {
+        if (!m) {
+            return this._months;
+        }
+        return isArray(this._months) ? this._months[m.month()] :
+            this._months[(this._months.isFormat || MONTHS_IN_FORMAT).test(format) ? 'format' : 'standalone'][m.month()];
+    }
+
+    var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_');
+    function localeMonthsShort (m, format) {
+        if (!m) {
+            return this._monthsShort;
+        }
+        return isArray(this._monthsShort) ? this._monthsShort[m.month()] :
+            this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()];
+    }
+
+    function units_month__handleStrictParse(monthName, format, strict) {
+        var i, ii, mom, llc = monthName.toLocaleLowerCase();
+        if (!this._monthsParse) {
+            // this is not used
+            this._monthsParse = [];
+            this._longMonthsParse = [];
+            this._shortMonthsParse = [];
+            for (i = 0; i < 12; ++i) {
+                mom = create_utc__createUTC([2000, i]);
+                this._shortMonthsParse[i] = this.monthsShort(mom, '').toLocaleLowerCase();
+                this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase();
+            }
+        }
+
+        if (strict) {
+            if (format === 'MMM') {
+                ii = indexOf.call(this._shortMonthsParse, llc);
+                return ii !== -1 ? ii : null;
+            } else {
+                ii = indexOf.call(this._longMonthsParse, llc);
+                return ii !== -1 ? ii : null;
+            }
+        } else {
+            if (format === 'MMM') {
+                ii = indexOf.call(this._shortMonthsParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._longMonthsParse, llc);
+                return ii !== -1 ? ii : null;
+            } else {
+                ii = indexOf.call(this._longMonthsParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._shortMonthsParse, llc);
+                return ii !== -1 ? ii : null;
+            }
+        }
+    }
+
+    function localeMonthsParse (monthName, format, strict) {
+        var i, mom, regex;
+
+        if (this._monthsParseExact) {
+            return units_month__handleStrictParse.call(this, monthName, format, strict);
+        }
+
+        if (!this._monthsParse) {
+            this._monthsParse = [];
+            this._longMonthsParse = [];
+            this._shortMonthsParse = [];
+        }
+
+        // TODO: add sorting
+        // Sorting makes sure if one month (or abbr) is a prefix of another
+        // see sorting in computeMonthsParse
+        for (i = 0; i < 12; i++) {
+            // make the regex if we don't have it already
+            mom = create_utc__createUTC([2000, i]);
+            if (strict && !this._longMonthsParse[i]) {
+                this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i');
+                this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i');
+            }
+            if (!strict && !this._monthsParse[i]) {
+                regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+                this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+            }
+            // test the regex
+            if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) {
+                return i;
+            } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) {
+                return i;
+            } else if (!strict && this._monthsParse[i].test(monthName)) {
+                return i;
+            }
+        }
+    }
+
+    // MOMENTS
+
+    function setMonth (mom, value) {
+        var dayOfMonth;
+
+        if (!mom.isValid()) {
+            // No op
+            return mom;
+        }
+
+        if (typeof value === 'string') {
+            if (/^\d+$/.test(value)) {
+                value = toInt(value);
+            } else {
+                value = mom.localeData().monthsParse(value);
+                // TODO: Another silent failure?
+                if (typeof value !== 'number') {
+                    return mom;
+                }
+            }
+        }
+
+        dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value));
+        mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+        return mom;
+    }
+
+    function getSetMonth (value) {
+        if (value != null) {
+            setMonth(this, value);
+            utils_hooks__hooks.updateOffset(this, true);
+            return this;
+        } else {
+            return get_set__get(this, 'Month');
+        }
+    }
+
+    function getDaysInMonth () {
+        return daysInMonth(this.year(), this.month());
+    }
+
+    var defaultMonthsShortRegex = matchWord;
+    function monthsShortRegex (isStrict) {
+        if (this._monthsParseExact) {
+            if (!hasOwnProp(this, '_monthsRegex')) {
+                computeMonthsParse.call(this);
+            }
+            if (isStrict) {
+                return this._monthsShortStrictRegex;
+            } else {
+                return this._monthsShortRegex;
+            }
+        } else {
+            if (!hasOwnProp(this, '_monthsShortRegex')) {
+                this._monthsShortRegex = defaultMonthsShortRegex;
+            }
+            return this._monthsShortStrictRegex && isStrict ?
+                this._monthsShortStrictRegex : this._monthsShortRegex;
+        }
+    }
+
+    var defaultMonthsRegex = matchWord;
+    function monthsRegex (isStrict) {
+        if (this._monthsParseExact) {
+            if (!hasOwnProp(this, '_monthsRegex')) {
+                computeMonthsParse.call(this);
+            }
+            if (isStrict) {
+                return this._monthsStrictRegex;
+            } else {
+                return this._monthsRegex;
+            }
+        } else {
+            if (!hasOwnProp(this, '_monthsRegex')) {
+                this._monthsRegex = defaultMonthsRegex;
+            }
+            return this._monthsStrictRegex && isStrict ?
+                this._monthsStrictRegex : this._monthsRegex;
+        }
+    }
+
+    function computeMonthsParse () {
+        function cmpLenRev(a, b) {
+            return b.length - a.length;
+        }
+
+        var shortPieces = [], longPieces = [], mixedPieces = [],
+            i, mom;
+        for (i = 0; i < 12; i++) {
+            // make the regex if we don't have it already
+            mom = create_utc__createUTC([2000, i]);
+            shortPieces.push(this.monthsShort(mom, ''));
+            longPieces.push(this.months(mom, ''));
+            mixedPieces.push(this.months(mom, ''));
+            mixedPieces.push(this.monthsShort(mom, ''));
+        }
+        // Sorting makes sure if one month (or abbr) is a prefix of another it
+        // will match the longer piece.
+        shortPieces.sort(cmpLenRev);
+        longPieces.sort(cmpLenRev);
+        mixedPieces.sort(cmpLenRev);
+        for (i = 0; i < 12; i++) {
+            shortPieces[i] = regexEscape(shortPieces[i]);
+            longPieces[i] = regexEscape(longPieces[i]);
+        }
+        for (i = 0; i < 24; i++) {
+            mixedPieces[i] = regexEscape(mixedPieces[i]);
+        }
+
+        this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+        this._monthsShortRegex = this._monthsRegex;
+        this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i');
+        this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i');
+    }
+
+    // FORMATTING
+
+    addFormatToken('Y', 0, 0, function () {
+        var y = this.year();
+        return y <= 9999 ? '' + y : '+' + y;
+    });
+
+    addFormatToken(0, ['YY', 2], 0, function () {
+        return this.year() % 100;
+    });
+
+    addFormatToken(0, ['YYYY',   4],       0, 'year');
+    addFormatToken(0, ['YYYYY',  5],       0, 'year');
+    addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');
+
+    // ALIASES
+
+    addUnitAlias('year', 'y');
+
+    // PRIORITIES
+
+    addUnitPriority('year', 1);
+
+    // PARSING
+
+    addRegexToken('Y',      matchSigned);
+    addRegexToken('YY',     match1to2, match2);
+    addRegexToken('YYYY',   match1to4, match4);
+    addRegexToken('YYYYY',  match1to6, match6);
+    addRegexToken('YYYYYY', match1to6, match6);
+
+    addParseToken(['YYYYY', 'YYYYYY'], YEAR);
+    addParseToken('YYYY', function (input, array) {
+        array[YEAR] = input.length === 2 ? utils_hooks__hooks.parseTwoDigitYear(input) : toInt(input);
+    });
+    addParseToken('YY', function (input, array) {
+        array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input);
+    });
+    addParseToken('Y', function (input, array) {
+        array[YEAR] = parseInt(input, 10);
+    });
+
+    // HELPERS
+
+    function daysInYear(year) {
+        return isLeapYear(year) ? 366 : 365;
+    }
+
+    function isLeapYear(year) {
+        return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+    }
+
+    // HOOKS
+
+    utils_hooks__hooks.parseTwoDigitYear = function (input) {
+        return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+    };
+
+    // MOMENTS
+
+    var getSetYear = makeGetSet('FullYear', true);
+
+    function getIsLeapYear () {
+        return isLeapYear(this.year());
+    }
+
+    function createDate (y, m, d, h, M, s, ms) {
+        //can't just apply() to create a date:
+        //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
+        var date = new Date(y, m, d, h, M, s, ms);
+
+        //the date constructor remaps years 0-99 to 1900-1999
+        if (y < 100 && y >= 0 && isFinite(date.getFullYear())) {
+            date.setFullYear(y);
+        }
+        return date;
+    }
+
+    function createUTCDate (y) {
+        var date = new Date(Date.UTC.apply(null, arguments));
+
+        //the Date.UTC function remaps years 0-99 to 1900-1999
+        if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) {
+            date.setUTCFullYear(y);
+        }
+        return date;
+    }
+
+    // start-of-first-week - start-of-year
+    function firstWeekOffset(year, dow, doy) {
+        var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
+            fwd = 7 + dow - doy,
+            // first-week day local weekday -- which local weekday is fwd
+            fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7;
+
+        return -fwdlw + fwd - 1;
+    }
+
+    //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+    function dayOfYearFromWeeks(year, week, weekday, dow, doy) {
+        var localWeekday = (7 + weekday - dow) % 7,
+            weekOffset = firstWeekOffset(year, dow, doy),
+            dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset,
+            resYear, resDayOfYear;
+
+        if (dayOfYear <= 0) {
+            resYear = year - 1;
+            resDayOfYear = daysInYear(resYear) + dayOfYear;
+        } else if (dayOfYear > daysInYear(year)) {
+            resYear = year + 1;
+            resDayOfYear = dayOfYear - daysInYear(year);
+        } else {
+            resYear = year;
+            resDayOfYear = dayOfYear;
+        }
+
+        return {
+            year: resYear,
+            dayOfYear: resDayOfYear
+        };
+    }
+
+    function weekOfYear(mom, dow, doy) {
+        var weekOffset = firstWeekOffset(mom.year(), dow, doy),
+            week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1,
+            resWeek, resYear;
+
+        if (week < 1) {
+            resYear = mom.year() - 1;
+            resWeek = week + weeksInYear(resYear, dow, doy);
+        } else if (week > weeksInYear(mom.year(), dow, doy)) {
+            resWeek = week - weeksInYear(mom.year(), dow, doy);
+            resYear = mom.year() + 1;
+        } else {
+            resYear = mom.year();
+            resWeek = week;
+        }
+
+        return {
+            week: resWeek,
+            year: resYear
+        };
+    }
+
+    function weeksInYear(year, dow, doy) {
+        var weekOffset = firstWeekOffset(year, dow, doy),
+            weekOffsetNext = firstWeekOffset(year + 1, dow, doy);
+        return (daysInYear(year) - weekOffset + weekOffsetNext) / 7;
+    }
+
+    // FORMATTING
+
+    addFormatToken('w', ['ww', 2], 'wo', 'week');
+    addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek');
+
+    // ALIASES
+
+    addUnitAlias('week', 'w');
+    addUnitAlias('isoWeek', 'W');
+
+    // PRIORITIES
+
+    addUnitPriority('week', 5);
+    addUnitPriority('isoWeek', 5);
+
+    // PARSING
+
+    addRegexToken('w',  match1to2);
+    addRegexToken('ww', match1to2, match2);
+    addRegexToken('W',  match1to2);
+    addRegexToken('WW', match1to2, match2);
+
+    addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) {
+        week[token.substr(0, 1)] = toInt(input);
+    });
+
+    // HELPERS
+
+    // LOCALES
+
+    function localeWeek (mom) {
+        return weekOfYear(mom, this._week.dow, this._week.doy).week;
+    }
+
+    var defaultLocaleWeek = {
+        dow : 0, // Sunday is the first day of the week.
+        doy : 6  // The week that contains Jan 1st is the first week of the year.
+    };
+
+    function localeFirstDayOfWeek () {
+        return this._week.dow;
+    }
+
+    function localeFirstDayOfYear () {
+        return this._week.doy;
+    }
+
+    // MOMENTS
+
+    function getSetWeek (input) {
+        var week = this.localeData().week(this);
+        return input == null ? week : this.add((input - week) * 7, 'd');
+    }
+
+    function getSetISOWeek (input) {
+        var week = weekOfYear(this, 1, 4).week;
+        return input == null ? week : this.add((input - week) * 7, 'd');
+    }
+
+    // FORMATTING
+
+    addFormatToken('d', 0, 'do', 'day');
+
+    addFormatToken('dd', 0, 0, function (format) {
+        return this.localeData().weekdaysMin(this, format);
+    });
+
+    addFormatToken('ddd', 0, 0, function (format) {
+        return this.localeData().weekdaysShort(this, format);
+    });
+
+    addFormatToken('dddd', 0, 0, function (format) {
+        return this.localeData().weekdays(this, format);
+    });
+
+    addFormatToken('e', 0, 0, 'weekday');
+    addFormatToken('E', 0, 0, 'isoWeekday');
+
+    // ALIASES
+
+    addUnitAlias('day', 'd');
+    addUnitAlias('weekday', 'e');
+    addUnitAlias('isoWeekday', 'E');
+
+    // PRIORITY
+    addUnitPriority('day', 11);
+    addUnitPriority('weekday', 11);
+    addUnitPriority('isoWeekday', 11);
+
+    // PARSING
+
+    addRegexToken('d',    match1to2);
+    addRegexToken('e',    match1to2);
+    addRegexToken('E',    match1to2);
+    addRegexToken('dd',   function (isStrict, locale) {
+        return locale.weekdaysMinRegex(isStrict);
+    });
+    addRegexToken('ddd',   function (isStrict, locale) {
+        return locale.weekdaysShortRegex(isStrict);
+    });
+    addRegexToken('dddd',   function (isStrict, locale) {
+        return locale.weekdaysRegex(isStrict);
+    });
+
+    addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) {
+        var weekday = config._locale.weekdaysParse(input, token, config._strict);
+        // if we didn't get a weekday name, mark the date as invalid
+        if (weekday != null) {
+            week.d = weekday;
+        } else {
+            getParsingFlags(config).invalidWeekday = input;
+        }
+    });
+
+    addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) {
+        week[token] = toInt(input);
+    });
+
+    // HELPERS
+
+    function parseWeekday(input, locale) {
+        if (typeof input !== 'string') {
+            return input;
+        }
+
+        if (!isNaN(input)) {
+            return parseInt(input, 10);
+        }
+
+        input = locale.weekdaysParse(input);
+        if (typeof input === 'number') {
+            return input;
+        }
+
+        return null;
+    }
+
+    function parseIsoWeekday(input, locale) {
+        if (typeof input === 'string') {
+            return locale.weekdaysParse(input) % 7 || 7;
+        }
+        return isNaN(input) ? null : input;
+    }
+
+    // LOCALES
+
+    var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_');
+    function localeWeekdays (m, format) {
+        if (!m) {
+            return this._weekdays;
+        }
+        return isArray(this._weekdays) ? this._weekdays[m.day()] :
+            this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()];
+    }
+
+    var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_');
+    function localeWeekdaysShort (m) {
+        return (m) ? this._weekdaysShort[m.day()] : this._weekdaysShort;
+    }
+
+    var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_');
+    function localeWeekdaysMin (m) {
+        return (m) ? this._weekdaysMin[m.day()] : this._weekdaysMin;
+    }
+
+    function day_of_week__handleStrictParse(weekdayName, format, strict) {
+        var i, ii, mom, llc = weekdayName.toLocaleLowerCase();
+        if (!this._weekdaysParse) {
+            this._weekdaysParse = [];
+            this._shortWeekdaysParse = [];
+            this._minWeekdaysParse = [];
+
+            for (i = 0; i < 7; ++i) {
+                mom = create_utc__createUTC([2000, 1]).day(i);
+                this._minWeekdaysParse[i] = this.weekdaysMin(mom, '').toLocaleLowerCase();
+                this._shortWeekdaysParse[i] = this.weekdaysShort(mom, '').toLocaleLowerCase();
+                this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase();
+            }
+        }
+
+        if (strict) {
+            if (format === 'dddd') {
+                ii = indexOf.call(this._weekdaysParse, llc);
+                return ii !== -1 ? ii : null;
+            } else if (format === 'ddd') {
+                ii = indexOf.call(this._shortWeekdaysParse, llc);
+                return ii !== -1 ? ii : null;
+            } else {
+                ii = indexOf.call(this._minWeekdaysParse, llc);
+                return ii !== -1 ? ii : null;
+            }
+        } else {
+            if (format === 'dddd') {
+                ii = indexOf.call(this._weekdaysParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._shortWeekdaysParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._minWeekdaysParse, llc);
+                return ii !== -1 ? ii : null;
+            } else if (format === 'ddd') {
+                ii = indexOf.call(this._shortWeekdaysParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._weekdaysParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._minWeekdaysParse, llc);
+                return ii !== -1 ? ii : null;
+            } else {
+                ii = indexOf.call(this._minWeekdaysParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._weekdaysParse, llc);
+                if (ii !== -1) {
+                    return ii;
+                }
+                ii = indexOf.call(this._shortWeekdaysParse, llc);
+                return ii !== -1 ? ii : null;
+            }
+        }
+    }
+
+    function localeWeekdaysParse (weekdayName, format, strict) {
+        var i, mom, regex;
+
+        if (this._weekdaysParseExact) {
+            return day_of_week__handleStrictParse.call(this, weekdayName, format, strict);
+        }
+
+        if (!this._weekdaysParse) {
+            this._weekdaysParse = [];
+            this._minWeekdaysParse = [];
+            this._shortWeekdaysParse = [];
+            this._fullWeekdaysParse = [];
+        }
+
+        for (i = 0; i < 7; i++) {
+            // make the regex if we don't have it already
+
+            mom = create_utc__createUTC([2000, 1]).day(i);
+            if (strict && !this._fullWeekdaysParse[i]) {
+                this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i');
+                this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i');
+                this._minWeekdaysParse[i] = new RegExp('^' + this.weekdaysMin(mom, '').replace('.', '\.?') + '$', 'i');
+            }
+            if (!this._weekdaysParse[i]) {
+                regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+                this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+            }
+            // test the regex
+            if (strict && format === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName)) {
+                return i;
+            } else if (strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName)) {
+                return i;
+            } else if (strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName)) {
+                return i;
+            } else if (!strict && this._weekdaysParse[i].test(weekdayName)) {
+                return i;
+            }
+        }
+    }
+
+    // MOMENTS
+
+    function getSetDayOfWeek (input) {
+        if (!this.isValid()) {
+            return input != null ? this : NaN;
+        }
+        var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+        if (input != null) {
+            input = parseWeekday(input, this.localeData());
+            return this.add(input - day, 'd');
+        } else {
+            return day;
+        }
+    }
+
+    function getSetLocaleDayOfWeek (input) {
+        if (!this.isValid()) {
+            return input != null ? this : NaN;
+        }
+        var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+        return input == null ? weekday : this.add(input - weekday, 'd');
+    }
+
+    function getSetISODayOfWeek (input) {
+        if (!this.isValid()) {
+            return input != null ? this : NaN;
+        }
+
+        // behaves the same as moment#day except
+        // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+        // as a setter, sunday should belong to the previous week.
+
+        if (input != null) {
+            var weekday = parseIsoWeekday(input, this.localeData());
+            return this.day(this.day() % 7 ? weekday : weekday - 7);
+        } else {
+            return this.day() || 7;
+        }
+    }
+
+    var defaultWeekdaysRegex = matchWord;
+    function weekdaysRegex (isStrict) {
+        if (this._weekdaysParseExact) {
+            if (!hasOwnProp(this, '_weekdaysRegex')) {
+                computeWeekdaysParse.call(this);
+            }
+            if (isStrict) {
+                return this._weekdaysStrictRegex;
+            } else {
+                return this._weekdaysRegex;
+            }
+        } else {
+            if (!hasOwnProp(this, '_weekdaysRegex')) {
+                this._weekdaysRegex = defaultWeekdaysRegex;
+            }
+            return this._weekdaysStrictRegex && isStrict ?
+                this._weekdaysStrictRegex : this._weekdaysRegex;
+        }
+    }
+
+    var defaultWeekdaysShortRegex = matchWord;
+    function weekdaysShortRegex (isStrict) {
+        if (this._weekdaysParseExact) {
+            if (!hasOwnProp(this, '_weekdaysRegex')) {
+                computeWeekdaysParse.call(this);
+            }
+            if (isStrict) {
+                return this._weekdaysShortStrictRegex;
+            } else {
+                return this._weekdaysShortRegex;
+            }
+        } else {
+            if (!hasOwnProp(this, '_weekdaysShortRegex')) {
+                this._weekdaysShortRegex = defaultWeekdaysShortRegex;
+            }
+            return this._weekdaysShortStrictRegex && isStrict ?
+                this._weekdaysShortStrictRegex : this._weekdaysShortRegex;
+        }
+    }
+
+    var defaultWeekdaysMinRegex = matchWord;
+    function weekdaysMinRegex (isStrict) {
+        if (this._weekdaysParseExact) {
+            if (!hasOwnProp(this, '_weekdaysRegex')) {
+                computeWeekdaysParse.call(this);
+            }
+            if (isStrict) {
+                return this._weekdaysMinStrictRegex;
+            } else {
+                return this._weekdaysMinRegex;
+            }
+        } else {
+            if (!hasOwnProp(this, '_weekdaysMinRegex')) {
+                this._weekdaysMinRegex = defaultWeekdaysMinRegex;
+            }
+            return this._weekdaysMinStrictRegex && isStrict ?
+                this._weekdaysMinStrictRegex : this._weekdaysMinRegex;
+        }
+    }
+
+
+    function computeWeekdaysParse () {
+        function cmpLenRev(a, b) {
+            return b.length - a.length;
+        }
+
+        var minPieces = [], shortPieces = [], longPieces = [], mixedPieces = [],
+            i, mom, minp, shortp, longp;
+        for (i = 0; i < 7; i++) {
+            // make the regex if we don't have it already
+            mom = create_utc__createUTC([2000, 1]).day(i);
+            minp = this.weekdaysMin(mom, '');
+            shortp = this.weekdaysShort(mom, '');
+            longp = this.weekdays(mom, '');
+            minPieces.push(minp);
+            shortPieces.push(shortp);
+            longPieces.push(longp);
+            mixedPieces.push(minp);
+            mixedPieces.push(shortp);
+            mixedPieces.push(longp);
+        }
+        // Sorting makes sure if one weekday (or abbr) is a prefix of another it
+        // will match the longer piece.
+        minPieces.sort(cmpLenRev);
+        shortPieces.sort(cmpLenRev);
+        longPieces.sort(cmpLenRev);
+        mixedPieces.sort(cmpLenRev);
+        for (i = 0; i < 7; i++) {
+            shortPieces[i] = regexEscape(shortPieces[i]);
+            longPieces[i] = regexEscape(longPieces[i]);
+            mixedPieces[i] = regexEscape(mixedPieces[i]);
+        }
+
+        this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+        this._weekdaysShortRegex = this._weekdaysRegex;
+        this._weekdaysMinRegex = this._weekdaysRegex;
+
+        this._weekdaysStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i');
+        this._weekdaysShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i');
+        this._weekdaysMinStrictRegex = new RegExp('^(' + minPieces.join('|') + ')', 'i');
+    }
+
+    // FORMATTING
+
+    function hFormat() {
+        return this.hours() % 12 || 12;
+    }
+
+    function kFormat() {
+        return this.hours() || 24;
+    }
+
+    addFormatToken('H', ['HH', 2], 0, 'hour');
+    addFormatToken('h', ['hh', 2], 0, hFormat);
+    addFormatToken('k', ['kk', 2], 0, kFormat);
+
+    addFormatToken('hmm', 0, 0, function () {
+        return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2);
+    });
+
+    addFormatToken('hmmss', 0, 0, function () {
+        return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) +
+            zeroFill(this.seconds(), 2);
+    });
+
+    addFormatToken('Hmm', 0, 0, function () {
+        return '' + this.hours() + zeroFill(this.minutes(), 2);
+    });
+
+    addFormatToken('Hmmss', 0, 0, function () {
+        return '' + this.hours() + zeroFill(this.minutes(), 2) +
+            zeroFill(this.seconds(), 2);
+    });
+
+    function meridiem (token, lowercase) {
+        addFormatToken(token, 0, 0, function () {
+            return this.localeData().meridiem(this.hours(), this.minutes(), lowercase);
+        });
+    }
+
+    meridiem('a', true);
+    meridiem('A', false);
+
+    // ALIASES
+
+    addUnitAlias('hour', 'h');
+
+    // PRIORITY
+    addUnitPriority('hour', 13);
+
+    // PARSING
+
+    function matchMeridiem (isStrict, locale) {
+        return locale._meridiemParse;
+    }
+
+    addRegexToken('a',  matchMeridiem);
+    addRegexToken('A',  matchMeridiem);
+    addRegexToken('H',  match1to2);
+    addRegexToken('h',  match1to2);
+    addRegexToken('HH', match1to2, match2);
+    addRegexToken('hh', match1to2, match2);
+
+    addRegexToken('hmm', match3to4);
+    addRegexToken('hmmss', match5to6);
+    addRegexToken('Hmm', match3to4);
+    addRegexToken('Hmmss', match5to6);
+
+    addParseToken(['H', 'HH'], HOUR);
+    addParseToken(['a', 'A'], function (input, array, config) {
+        config._isPm = config._locale.isPM(input);
+        config._meridiem = input;
+    });
+    addParseToken(['h', 'hh'], function (input, array, config) {
+        array[HOUR] = toInt(input);
+        getParsingFlags(config).bigHour = true;
+    });
+    addParseToken('hmm', function (input, array, config) {
+        var pos = input.length - 2;
+        array[HOUR] = toInt(input.substr(0, pos));
+        array[MINUTE] = toInt(input.substr(pos));
+        getParsingFlags(config).bigHour = true;
+    });
+    addParseToken('hmmss', function (input, array, config) {
+        var pos1 = input.length - 4;
+        var pos2 = input.length - 2;
+        array[HOUR] = toInt(input.substr(0, pos1));
+        array[MINUTE] = toInt(input.substr(pos1, 2));
+        array[SECOND] = toInt(input.substr(pos2));
+        getParsingFlags(config).bigHour = true;
+    });
+    addParseToken('Hmm', function (input, array, config) {
+        var pos = input.length - 2;
+        array[HOUR] = toInt(input.substr(0, pos));
+        array[MINUTE] = toInt(input.substr(pos));
+    });
+    addParseToken('Hmmss', function (input, array, config) {
+        var pos1 = input.length - 4;
+        var pos2 = input.length - 2;
+        array[HOUR] = toInt(input.substr(0, pos1));
+        array[MINUTE] = toInt(input.substr(pos1, 2));
+        array[SECOND] = toInt(input.substr(pos2));
+    });
+
+    // LOCALES
+
+    function localeIsPM (input) {
+        // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+        // Using charAt should be more compatible.
+        return ((input + '').toLowerCase().charAt(0) === 'p');
+    }
+
+    var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i;
+    function localeMeridiem (hours, minutes, isLower) {
+        if (hours > 11) {
+            return isLower ? 'pm' : 'PM';
+        } else {
+            return isLower ? 'am' : 'AM';
+        }
+    }
+
+
+    // MOMENTS
+
+    // Setting the hour should keep the time, because the user explicitly
+    // specified which hour he wants. So trying to maintain the same hour (in
+    // a new timezone) makes sense. Adding/subtracting hours does not follow
+    // this rule.
+    var getSetHour = makeGetSet('Hours', true);
+
+    var baseConfig = {
+        calendar: defaultCalendar,
+        longDateFormat: defaultLongDateFormat,
+        invalidDate: defaultInvalidDate,
+        ordinal: defaultOrdinal,
+        ordinalParse: defaultOrdinalParse,
+        relativeTime: defaultRelativeTime,
+
+        months: defaultLocaleMonths,
+        monthsShort: defaultLocaleMonthsShort,
+
+        week: defaultLocaleWeek,
+
+        weekdays: defaultLocaleWeekdays,
+        weekdaysMin: defaultLocaleWeekdaysMin,
+        weekdaysShort: defaultLocaleWeekdaysShort,
+
+        meridiemParse: defaultLocaleMeridiemParse
+    };
+
+    // internal storage for locale config files
+    var locales = {};
+    var globalLocale;
+
+    function normalizeLocale(key) {
+        return key ? key.toLowerCase().replace('_', '-') : key;
+    }
+
+    // pick the locale from the array
+    // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+    // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+    function chooseLocale(names) {
+        var i = 0, j, next, locale, split;
+
+        while (i < names.length) {
+            split = normalizeLocale(names[i]).split('-');
+            j = split.length;
+            next = normalizeLocale(names[i + 1]);
+            next = next ? next.split('-') : null;
+            while (j > 0) {
+                locale = loadLocale(split.slice(0, j).join('-'));
+                if (locale) {
+                    return locale;
+                }
+                if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+                    //the next array item is better than a shallower substring of this one
+                    break;
+                }
+                j--;
+            }
+            i++;
+        }
+        return null;
+    }
+
+    function loadLocale(name) {
+        var oldLocale = null;
+        // TODO: Find a better way to register and load all the locales in Node
+        if (!locales[name] && (typeof module !== 'undefined') &&
+                module && module.exports) {
+            try {
+                oldLocale = globalLocale._abbr;
+                require('./locale/' + name);
+                // because defineLocale currently also sets the global locale, we
+                // want to undo that for lazy loaded locales
+                locale_locales__getSetGlobalLocale(oldLocale);
+            } catch (e) { }
+        }
+        return locales[name];
+    }
+
+    // This function will load locale and then set the global locale.  If
+    // no arguments are passed in, it will simply return the current global
+    // locale key.
+    function locale_locales__getSetGlobalLocale (key, values) {
+        var data;
+        if (key) {
+            if (isUndefined(values)) {
+                data = locale_locales__getLocale(key);
+            }
+            else {
+                data = defineLocale(key, values);
+            }
+
+            if (data) {
+                // moment.duration._locale = moment._locale = data;
+                globalLocale = data;
+            }
+        }
+
+        return globalLocale._abbr;
+    }
+
+    function defineLocale (name, config) {
+        if (config !== null) {
+            var parentConfig = baseConfig;
+            config.abbr = name;
+            if (locales[name] != null) {
+                deprecateSimple('defineLocaleOverride',
+                        'use moment.updateLocale(localeName, config) to change ' +
+                        'an existing locale. moment.defineLocale(localeName, ' +
+                        'config) should only be used for creating a new locale ' +
+                        'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.');
+                parentConfig = locales[name]._config;
+            } else if (config.parentLocale != null) {
+                if (locales[config.parentLocale] != null) {
+                    parentConfig = locales[config.parentLocale]._config;
+                } else {
+                    // treat as if there is no base config
+                    deprecateSimple('parentLocaleUndefined',
+                            'specified parentLocale is not defined yet. See http://momentjs.com/guides/#/warnings/parent-locale/');
+                }
+            }
+            locales[name] = new Locale(mergeConfigs(parentConfig, config));
+
+            // backwards compat for now: also set the locale
+            locale_locales__getSetGlobalLocale(name);
+
+            return locales[name];
+        } else {
+            // useful for testing
+            delete locales[name];
+            return null;
+        }
+    }
+
+    function updateLocale(name, config) {
+        if (config != null) {
+            var locale, parentConfig = baseConfig;
+            // MERGE
+            if (locales[name] != null) {
+                parentConfig = locales[name]._config;
+            }
+            config = mergeConfigs(parentConfig, config);
+            locale = new Locale(config);
+            locale.parentLocale = locales[name];
+            locales[name] = locale;
+
+            // backwards compat for now: also set the locale
+            locale_locales__getSetGlobalLocale(name);
+        } else {
+            // pass null for config to unupdate, useful for tests
+            if (locales[name] != null) {
+                if (locales[name].parentLocale != null) {
+                    locales[name] = locales[name].parentLocale;
+                } else if (locales[name] != null) {
+                    delete locales[name];
+                }
+            }
+        }
+        return locales[name];
+    }
+
+    // returns locale data
+    function locale_locales__getLocale (key) {
+        var locale;
+
+        if (key && key._locale && key._locale._abbr) {
+            key = key._locale._abbr;
+        }
+
+        if (!key) {
+            return globalLocale;
+        }
+
+        if (!isArray(key)) {
+            //short-circuit everything else
+            locale = loadLocale(key);
+            if (locale) {
+                return locale;
+            }
+            key = [key];
+        }
+
+        return chooseLocale(key);
+    }
+
+    function locale_locales__listLocales() {
+        return keys(locales);
+    }
+
+    function checkOverflow (m) {
+        var overflow;
+        var a = m._a;
+
+        if (a && getParsingFlags(m).overflow === -2) {
+            overflow =
+                a[MONTH]       < 0 || a[MONTH]       > 11  ? MONTH :
+                a[DATE]        < 1 || a[DATE]        > daysInMonth(a[YEAR], a[MONTH]) ? DATE :
+                a[HOUR]        < 0 || a[HOUR]        > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR :
+                a[MINUTE]      < 0 || a[MINUTE]      > 59  ? MINUTE :
+                a[SECOND]      < 0 || a[SECOND]      > 59  ? SECOND :
+                a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND :
+                -1;
+
+            if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+                overflow = DATE;
+            }
+            if (getParsingFlags(m)._overflowWeeks && overflow === -1) {
+                overflow = WEEK;
+            }
+            if (getParsingFlags(m)._overflowWeekday && overflow === -1) {
+                overflow = WEEKDAY;
+            }
+
+            getParsingFlags(m).overflow = overflow;
+        }
+
+        return m;
+    }
+
+    // iso 8601 regex
+    // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
+    var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/;
+    var basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/;
+
+    var tzRegex = /Z|[+-]\d\d(?::?\d\d)?/;
+
+    var isoDates = [
+        ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/],
+        ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/],
+        ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/],
+        ['GGGG-[W]WW', /\d{4}-W\d\d/, false],
+        ['YYYY-DDD', /\d{4}-\d{3}/],
+        ['YYYY-MM', /\d{4}-\d\d/, false],
+        ['YYYYYYMMDD', /[+-]\d{10}/],
+        ['YYYYMMDD', /\d{8}/],
+        // YYYYMM is NOT allowed by the standard
+        ['GGGG[W]WWE', /\d{4}W\d{3}/],
+        ['GGGG[W]WW', /\d{4}W\d{2}/, false],
+        ['YYYYDDD', /\d{7}/]
+    ];
+
+    // iso time formats and regexes
+    var isoTimes = [
+        ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/],
+        ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/],
+        ['HH:mm:ss', /\d\d:\d\d:\d\d/],
+        ['HH:mm', /\d\d:\d\d/],
+        ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/],
+        ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/],
+        ['HHmmss', /\d\d\d\d\d\d/],
+        ['HHmm', /\d\d\d\d/],
+        ['HH', /\d\d/]
+    ];
+
+    var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i;
+
+    // date from iso format
+    function configFromISO(config) {
+        var i, l,
+            string = config._i,
+            match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string),
+            allowTime, dateFormat, timeFormat, tzFormat;
+
+        if (match) {
+            getParsingFlags(config).iso = true;
+
+            for (i = 0, l = isoDates.length; i < l; i++) {
+                if (isoDates[i][1].exec(match[1])) {
+                    dateFormat = isoDates[i][0];
+                    allowTime = isoDates[i][2] !== false;
+                    break;
+                }
+            }
+            if (dateFormat == null) {
+                config._isValid = false;
+                return;
+            }
+            if (match[3]) {
+                for (i = 0, l = isoTimes.length; i < l; i++) {
+                    if (isoTimes[i][1].exec(match[3])) {
+                        // match[2] should be 'T' or space
+                        timeFormat = (match[2] || ' ') + isoTimes[i][0];
+                        break;
+                    }
+                }
+                if (timeFormat == null) {
+                    config._isValid = false;
+                    return;
+                }
+            }
+            if (!allowTime && timeFormat != null) {
+                config._isValid = false;
+                return;
+            }
+            if (match[4]) {
+                if (tzRegex.exec(match[4])) {
+                    tzFormat = 'Z';
+                } else {
+                    config._isValid = false;
+                    return;
+                }
+            }
+            config._f = dateFormat + (timeFormat || '') + (tzFormat || '');
+            configFromStringAndFormat(config);
+        } else {
+            config._isValid = false;
+        }
+    }
+
+    // date from iso format or fallback
+    function configFromString(config) {
+        var matched = aspNetJsonRegex.exec(config._i);
+
+        if (matched !== null) {
+            config._d = new Date(+matched[1]);
+            return;
+        }
+
+        configFromISO(config);
+        if (config._isValid === false) {
+            delete config._isValid;
+            utils_hooks__hooks.createFromInputFallback(config);
+        }
+    }
+
+    utils_hooks__hooks.createFromInputFallback = deprecate(
+        'value provided is not in a recognized ISO format. moment construction falls back to js Date(), ' +
+        'which is not reliable across all browsers and versions. Non ISO date formats are ' +
+        'discouraged and will be removed in an upcoming major release. Please refer to ' +
+        'http://momentjs.com/guides/#/warnings/js-date/ for more info.',
+        function (config) {
+            config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+        }
+    );
+
+    // Pick the first defined of two or three arguments.
+    function defaults(a, b, c) {
+        if (a != null) {
+            return a;
+        }
+        if (b != null) {
+            return b;
+        }
+        return c;
+    }
+
+    function currentDateArray(config) {
+        // hooks is actually the exported moment object
+        var nowValue = new Date(utils_hooks__hooks.now());
+        if (config._useUTC) {
+            return [nowValue.getUTCFullYear(), nowValue.getUTCMonth(), nowValue.getUTCDate()];
+        }
+        return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()];
+    }
+
+    // convert an array to a date.
+    // the array should mirror the parameters below
+    // note: all values past the year are optional and will default to the lowest possible value.
+    // [year, month, day , hour, minute, second, millisecond]
+    function configFromArray (config) {
+        var i, date, input = [], currentDate, yearToUse;
+
+        if (config._d) {
+            return;
+        }
+
+        currentDate = currentDateArray(config);
+
+        //compute day of the year from weeks and weekdays
+        if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+            dayOfYearFromWeekInfo(config);
+        }
+
+        //if the day of the year is set, figure out what it is
+        if (config._dayOfYear) {
+            yearToUse = defaults(config._a[YEAR], currentDate[YEAR]);
+
+            if (config._dayOfYear > daysInYear(yearToUse)) {
+                getParsingFlags(config)._overflowDayOfYear = true;
+            }
+
+            date = createUTCDate(yearToUse, 0, config._dayOfYear);
+            config._a[MONTH] = date.getUTCMonth();
+            config._a[DATE] = date.getUTCDate();
+        }
+
+        // Default to current date.
+        // * if no year, month, day of month are given, default to today
+        // * if day of month is given, default month and year
+        // * if month is given, default only year
+        // * if year is given, don't default anything
+        for (i = 0; i < 3 && config._a[i] == null; ++i) {
+            config._a[i] = input[i] = currentDate[i];
+        }
+
+        // Zero out whatever was not defaulted, including time
+        for (; i < 7; i++) {
+            config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+        }
+
+        // Check for 24:00:00.000
+        if (config._a[HOUR] === 24 &&
+                config._a[MINUTE] === 0 &&
+                config._a[SECOND] === 0 &&
+                config._a[MILLISECOND] === 0) {
+            config._nextDay = true;
+            config._a[HOUR] = 0;
+        }
+
+        config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
+        // Apply timezone offset from input. The actual utcOffset can be changed
+        // with parseZone.
+        if (config._tzm != null) {
+            config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+        }
+
+        if (config._nextDay) {
+            config._a[HOUR] = 24;
+        }
+    }
+
+    function dayOfYearFromWeekInfo(config) {
+        var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow;
+
+        w = config._w;
+        if (w.GG != null || w.W != null || w.E != null) {
+            dow = 1;
+            doy = 4;
+
+            // TODO: We need to take the current isoWeekYear, but that depends on
+            // how we interpret now (local, utc, fixed offset). So create
+            // a now version of current config (take local/utc/offset flags, and
+            // create now).
+            weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year);
+            week = defaults(w.W, 1);
+            weekday = defaults(w.E, 1);
+            if (weekday < 1 || weekday > 7) {
+                weekdayOverflow = true;
+            }
+        } else {
+            dow = config._locale._week.dow;
+            doy = config._locale._week.doy;
+
+            weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year);
+            week = defaults(w.w, 1);
+
+            if (w.d != null) {
+                // weekday -- low day numbers are considered next week
+                weekday = w.d;
+                if (weekday < 0 || weekday > 6) {
+                    weekdayOverflow = true;
+                }
+            } else if (w.e != null) {
+                // local weekday -- counting starts from begining of week
+                weekday = w.e + dow;
+                if (w.e < 0 || w.e > 6) {
+                    weekdayOverflow = true;
+                }
+            } else {
+                // default to begining of week
+                weekday = dow;
+            }
+        }
+        if (week < 1 || week > weeksInYear(weekYear, dow, doy)) {
+            getParsingFlags(config)._overflowWeeks = true;
+        } else if (weekdayOverflow != null) {
+            getParsingFlags(config)._overflowWeekday = true;
+        } else {
+            temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy);
+            config._a[YEAR] = temp.year;
+            config._dayOfYear = temp.dayOfYear;
+        }
+    }
+
+    // constant that refers to the ISO standard
+    utils_hooks__hooks.ISO_8601 = function () {};
+
+    // date from string and format string
+    function configFromStringAndFormat(config) {
+        // TODO: Move this to another part of the creation flow to prevent circular deps
+        if (config._f === utils_hooks__hooks.ISO_8601) {
+            configFromISO(config);
+            return;
+        }
+
+        config._a = [];
+        getParsingFlags(config).empty = true;
+
+        // This array is used to make a Date, either with `new Date` or `Date.UTC`
+        var string = '' + config._i,
+            i, parsedInput, tokens, token, skipped,
+            stringLength = string.length,
+            totalParsedInputLength = 0;
+
+        tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
+
+        for (i = 0; i < tokens.length; i++) {
+            token = tokens[i];
+            parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+            // console.log('token', token, 'parsedInput', parsedInput,
+            //         'regex', getParseRegexForToken(token, config));
+            if (parsedInput) {
+                skipped = string.substr(0, string.indexOf(parsedInput));
+                if (skipped.length > 0) {
+                    getParsingFlags(config).unusedInput.push(skipped);
+                }
+                string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+                totalParsedInputLength += parsedInput.length;
+            }
+            // don't parse if it's not a known token
+            if (formatTokenFunctions[token]) {
+                if (parsedInput) {
+                    getParsingFlags(config).empty = false;
+                }
+                else {
+                    getParsingFlags(config).unusedTokens.push(token);
+                }
+                addTimeToArrayFromToken(token, parsedInput, config);
+            }
+            else if (config._strict && !parsedInput) {
+                getParsingFlags(config).unusedTokens.push(token);
+            }
+        }
+
+        // add remaining unparsed input length to the string
+        getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
+        if (string.length > 0) {
+            getParsingFlags(config).unusedInput.push(string);
+        }
+
+        // clear _12h flag if hour is <= 12
+        if (config._a[HOUR] <= 12 &&
+            getParsingFlags(config).bigHour === true &&
+            config._a[HOUR] > 0) {
+            getParsingFlags(config).bigHour = undefined;
+        }
+
+        getParsingFlags(config).parsedDateParts = config._a.slice(0);
+        getParsingFlags(config).meridiem = config._meridiem;
+        // handle meridiem
+        config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);
+
+        configFromArray(config);
+        checkOverflow(config);
+    }
+
+
+    function meridiemFixWrap (locale, hour, meridiem) {
+        var isPm;
+
+        if (meridiem == null) {
+            // nothing to do
+            return hour;
+        }
+        if (locale.meridiemHour != null) {
+            return locale.meridiemHour(hour, meridiem);
+        } else if (locale.isPM != null) {
+            // Fallback
+            isPm = locale.isPM(meridiem);
+            if (isPm && hour < 12) {
+                hour += 12;
+            }
+            if (!isPm && hour === 12) {
+                hour = 0;
+            }
+            return hour;
+        } else {
+            // this is not supposed to happen
+            return hour;
+        }
+    }
+
+    // date from string and array of format strings
+    function configFromStringAndArray(config) {
+        var tempConfig,
+            bestMoment,
+
+            scoreToBeat,
+            i,
+            currentScore;
+
+        if (config._f.length === 0) {
+            getParsingFlags(config).invalidFormat = true;
+            config._d = new Date(NaN);
+            return;
+        }
+
+        for (i = 0; i < config._f.length; i++) {
+            currentScore = 0;
+            tempConfig = copyConfig({}, config);
+            if (config._useUTC != null) {
+                tempConfig._useUTC = config._useUTC;
+            }
+            tempConfig._f = config._f[i];
+            configFromStringAndFormat(tempConfig);
+
+            if (!valid__isValid(tempConfig)) {
+                continue;
+            }
+
+            // if there is any input that was not parsed add a penalty for that format
+            currentScore += getParsingFlags(tempConfig).charsLeftOver;
+
+            //or tokens
+            currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10;
+
+            getParsingFlags(tempConfig).score = currentScore;
+
+            if (scoreToBeat == null || currentScore < scoreToBeat) {
+                scoreToBeat = currentScore;
+                bestMoment = tempConfig;
+            }
+        }
+
+        extend(config, bestMoment || tempConfig);
+    }
+
+    function configFromObject(config) {
+        if (config._d) {
+            return;
+        }
+
+        var i = normalizeObjectUnits(config._i);
+        config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) {
+            return obj && parseInt(obj, 10);
+        });
+
+        configFromArray(config);
+    }
+
+    function createFromConfig (config) {
+        var res = new Moment(checkOverflow(prepareConfig(config)));
+        if (res._nextDay) {
+            // Adding is smart enough around DST
+            res.add(1, 'd');
+            res._nextDay = undefined;
+        }
+
+        return res;
+    }
+
+    function prepareConfig (config) {
+        var input = config._i,
+            format = config._f;
+
+        config._locale = config._locale || locale_locales__getLocale(config._l);
+
+        if (input === null || (format === undefined && input === '')) {
+            return valid__createInvalid({nullInput: true});
+        }
+
+        if (typeof input === 'string') {
+            config._i = input = config._locale.preparse(input);
+        }
+
+        if (isMoment(input)) {
+            return new Moment(checkOverflow(input));
+        } else if (isArray(format)) {
+            configFromStringAndArray(config);
+        } else if (isDate(input)) {
+            config._d = input;
+        } else if (format) {
+            configFromStringAndFormat(config);
+        }  else {
+            configFromInput(config);
+        }
+
+        if (!valid__isValid(config)) {
+            config._d = null;
+        }
+
+        return config;
+    }
+
+    function configFromInput(config) {
+        var input = config._i;
+        if (input === undefined) {
+            config._d = new Date(utils_hooks__hooks.now());
+        } else if (isDate(input)) {
+            config._d = new Date(input.valueOf());
+        } else if (typeof input === 'string') {
+            configFromString(config);
+        } else if (isArray(input)) {
+            config._a = map(input.slice(0), function (obj) {
+                return parseInt(obj, 10);
+            });
+            configFromArray(config);
+        } else if (typeof(input) === 'object') {
+            configFromObject(config);
+        } else if (typeof(input) === 'number') {
+            // from milliseconds
+            config._d = new Date(input);
+        } else {
+            utils_hooks__hooks.createFromInputFallback(config);
+        }
+    }
+
+    function createLocalOrUTC (input, format, locale, strict, isUTC) {
+        var c = {};
+
+        if (typeof(locale) === 'boolean') {
+            strict = locale;
+            locale = undefined;
+        }
+
+        if ((isObject(input) && isObjectEmpty(input)) ||
+                (isArray(input) && input.length === 0)) {
+            input = undefined;
+        }
+        // object construction must be done this way.
+        // https://github.com/moment/moment/issues/1423
+        c._isAMomentObject = true;
+        c._useUTC = c._isUTC = isUTC;
+        c._l = locale;
+        c._i = input;
+        c._f = format;
+        c._strict = strict;
+
+        return createFromConfig(c);
+    }
+
+    function local__createLocal (input, format, locale, strict) {
+        return createLocalOrUTC(input, format, locale, strict, false);
+    }
+
+    var prototypeMin = deprecate(
+        'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/',
+        function () {
+            var other = local__createLocal.apply(null, arguments);
+            if (this.isValid() && other.isValid()) {
+                return other < this ? this : other;
+            } else {
+                return valid__createInvalid();
+            }
+        }
+    );
+
+    var prototypeMax = deprecate(
+        'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/',
+        function () {
+            var other = local__createLocal.apply(null, arguments);
+            if (this.isValid() && other.isValid()) {
+                return other > this ? this : other;
+            } else {
+                return valid__createInvalid();
+            }
+        }
+    );
+
+    // Pick a moment m from moments so that m[fn](other) is true for all
+    // other. This relies on the function fn to be transitive.
+    //
+    // moments should either be an array of moment objects or an array, whose
+    // first element is an array of moment objects.
+    function pickBy(fn, moments) {
+        var res, i;
+        if (moments.length === 1 && isArray(moments[0])) {
+            moments = moments[0];
+        }
+        if (!moments.length) {
+            return local__createLocal();
+        }
+        res = moments[0];
+        for (i = 1; i < moments.length; ++i) {
+            if (!moments[i].isValid() || moments[i][fn](res)) {
+                res = moments[i];
+            }
+        }
+        return res;
+    }
+
+    // TODO: Use [].sort instead?
+    function min () {
+        var args = [].slice.call(arguments, 0);
+
+        return pickBy('isBefore', args);
+    }
+
+    function max () {
+        var args = [].slice.call(arguments, 0);
+
+        return pickBy('isAfter', args);
+    }
+
+    var now = function () {
+        return Date.now ? Date.now() : +(new Date());
+    };
+
+    function Duration (duration) {
+        var normalizedInput = normalizeObjectUnits(duration),
+            years = normalizedInput.year || 0,
+            quarters = normalizedInput.quarter || 0,
+            months = normalizedInput.month || 0,
+            weeks = normalizedInput.week || 0,
+            days = normalizedInput.day || 0,
+            hours = normalizedInput.hour || 0,
+            minutes = normalizedInput.minute || 0,
+            seconds = normalizedInput.second || 0,
+            milliseconds = normalizedInput.millisecond || 0;
+
+        // representation for dateAddRemove
+        this._milliseconds = +milliseconds +
+            seconds * 1e3 + // 1000
+            minutes * 6e4 + // 1000 * 60
+            hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
+        // Because of dateAddRemove treats 24 hours as different from a
+        // day when working around DST, we need to store them separately
+        this._days = +days +
+            weeks * 7;
+        // It is impossible translate months into days without knowing
+        // which months you are are talking about, so we have to store
+        // it separately.
+        this._months = +months +
+            quarters * 3 +
+            years * 12;
+
+        this._data = {};
+
+        this._locale = locale_locales__getLocale();
+
+        this._bubble();
+    }
+
+    function isDuration (obj) {
+        return obj instanceof Duration;
+    }
+
+    function absRound (number) {
+        if (number < 0) {
+            return Math.round(-1 * number) * -1;
+        } else {
+            return Math.round(number);
+        }
+    }
+
+    // FORMATTING
+
+    function offset (token, separator) {
+        addFormatToken(token, 0, 0, function () {
+            var offset = this.utcOffset();
+            var sign = '+';
+            if (offset < 0) {
+                offset = -offset;
+                sign = '-';
+            }
+            return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2);
+        });
+    }
+
+    offset('Z', ':');
+    offset('ZZ', '');
+
+    // PARSING
+
+    addRegexToken('Z',  matchShortOffset);
+    addRegexToken('ZZ', matchShortOffset);
+    addParseToken(['Z', 'ZZ'], function (input, array, config) {
+        config._useUTC = true;
+        config._tzm = offsetFromString(matchShortOffset, input);
+    });
+
+    // HELPERS
+
+    // timezone chunker
+    // '+10:00' > ['10',  '00']
+    // '-1530'  > ['-15', '30']
+    var chunkOffset = /([\+\-]|\d\d)/gi;
+
+    function offsetFromString(matcher, string) {
+        var matches = ((string || '').match(matcher) || []);
+        var chunk   = matches[matches.length - 1] || [];
+        var parts   = (chunk + '').match(chunkOffset) || ['-', 0, 0];
+        var minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+        return parts[0] === '+' ? minutes : -minutes;
+    }
+
+    // Return a moment from input, that is local/utc/zone equivalent to model.
+    function cloneWithOffset(input, model) {
+        var res, diff;
+        if (model._isUTC) {
+            res = model.clone();
+            diff = (isMoment(input) || isDate(input) ? input.valueOf() : local__createLocal(input).valueOf()) - res.valueOf();
+            // Use low-level api, because this fn is low-level api.
+            res._d.setTime(res._d.valueOf() + diff);
+            utils_hooks__hooks.updateOffset(res, false);
+            return res;
+        } else {
+            return local__createLocal(input).local();
+        }
+    }
+
+    function getDateOffset (m) {
+        // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+        // https://github.com/moment/moment/pull/1871
+        return -Math.round(m._d.getTimezoneOffset() / 15) * 15;
+    }
+
+    // HOOKS
+
+    // This function will be called whenever a moment is mutated.
+    // It is intended to keep the offset in sync with the timezone.
+    utils_hooks__hooks.updateOffset = function () {};
+
+    // MOMENTS
+
+    // keepLocalTime = true means only change the timezone, without
+    // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+    // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+    // +0200, so we adjust the time as needed, to be valid.
+    //
+    // Keeping the time actually adds/subtracts (one hour)
+    // from the actual represented time. That is why we call updateOffset
+    // a second time. In case it wants us to change the offset again
+    // _changeInProgress == true case, then we have to adjust, because
+    // there is no such time in the given timezone.
+    function getSetOffset (input, keepLocalTime) {
+        var offset = this._offset || 0,
+            localAdjust;
+        if (!this.isValid()) {
+            return input != null ? this : NaN;
+        }
+        if (input != null) {
+            if (typeof input === 'string') {
+                input = offsetFromString(matchShortOffset, input);
+            } else if (Math.abs(input) < 16) {
+                input = input * 60;
+            }
+            if (!this._isUTC && keepLocalTime) {
+                localAdjust = getDateOffset(this);
+            }
+            this._offset = input;
+            this._isUTC = true;
+            if (localAdjust != null) {
+                this.add(localAdjust, 'm');
+            }
+            if (offset !== input) {
+                if (!keepLocalTime || this._changeInProgress) {
+                    add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false);
+                } else if (!this._changeInProgress) {
+                    this._changeInProgress = true;
+                    utils_hooks__hooks.updateOffset(this, true);
+                    this._changeInProgress = null;
+                }
+            }
+            return this;
+        } else {
+            return this._isUTC ? offset : getDateOffset(this);
+        }
+    }
+
+    function getSetZone (input, keepLocalTime) {
+        if (input != null) {
+            if (typeof input !== 'string') {
+                input = -input;
+            }
+
+            this.utcOffset(input, keepLocalTime);
+
+            return this;
+        } else {
+            return -this.utcOffset();
+        }
+    }
+
+    function setOffsetToUTC (keepLocalTime) {
+        return this.utcOffset(0, keepLocalTime);
+    }
+
+    function setOffsetToLocal (keepLocalTime) {
+        if (this._isUTC) {
+            this.utcOffset(0, keepLocalTime);
+            this._isUTC = false;
+
+            if (keepLocalTime) {
+                this.subtract(getDateOffset(this), 'm');
+            }
+        }
+        return this;
+    }
+
+    function setOffsetToParsedOffset () {
+        if (this._tzm) {
+            this.utcOffset(this._tzm);
+        } else if (typeof this._i === 'string') {
+            var tZone = offsetFromString(matchOffset, this._i);
+
+            if (tZone === 0) {
+                this.utcOffset(0, true);
+            } else {
+                this.utcOffset(offsetFromString(matchOffset, this._i));
+            }
+        }
+        return this;
+    }
+
+    function hasAlignedHourOffset (input) {
+        if (!this.isValid()) {
+            return false;
+        }
+        input = input ? local__createLocal(input).utcOffset() : 0;
+
+        return (this.utcOffset() - input) % 60 === 0;
+    }
+
+    function isDaylightSavingTime () {
+        return (
+            this.utcOffset() > this.clone().month(0).utcOffset() ||
+            this.utcOffset() > this.clone().month(5).utcOffset()
+        );
+    }
+
+    function isDaylightSavingTimeShifted () {
+        if (!isUndefined(this._isDSTShifted)) {
+            return this._isDSTShifted;
+        }
+
+        var c = {};
+
+        copyConfig(c, this);
+        c = prepareConfig(c);
+
+        if (c._a) {
+            var other = c._isUTC ? create_utc__createUTC(c._a) : local__createLocal(c._a);
+            this._isDSTShifted = this.isValid() &&
+                compareArrays(c._a, other.toArray()) > 0;
+        } else {
+            this._isDSTShifted = false;
+        }
+
+        return this._isDSTShifted;
+    }
+
+    function isLocal () {
+        return this.isValid() ? !this._isUTC : false;
+    }
+
+    function isUtcOffset () {
+        return this.isValid() ? this._isUTC : false;
+    }
+
+    function isUtc () {
+        return this.isValid() ? this._isUTC && this._offset === 0 : false;
+    }
+
+    // ASP.NET json date format regex
+    var aspNetRegex = /^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/;
+
+    // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+    // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+    // and further modified to allow for strings containing both week and day
+    var isoRegex = /^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;
+
+    function create__createDuration (input, key) {
+        var duration = input,
+            // matching against regexp is expensive, do it on demand
+            match = null,
+            sign,
+            ret,
+            diffRes;
+
+        if (isDuration(input)) {
+            duration = {
+                ms : input._milliseconds,
+                d  : input._days,
+                M  : input._months
+            };
+        } else if (typeof input === 'number') {
+            duration = {};
+            if (key) {
+                duration[key] = input;
+            } else {
+                duration.milliseconds = input;
+            }
+        } else if (!!(match = aspNetRegex.exec(input))) {
+            sign = (match[1] === '-') ? -1 : 1;
+            duration = {
+                y  : 0,
+                d  : toInt(match[DATE])                         * sign,
+                h  : toInt(match[HOUR])                         * sign,
+                m  : toInt(match[MINUTE])                       * sign,
+                s  : toInt(match[SECOND])                       * sign,
+                ms : toInt(absRound(match[MILLISECOND] * 1000)) * sign // the millisecond decimal point is included in the match
+            };
+        } else if (!!(match = isoRegex.exec(input))) {
+            sign = (match[1] === '-') ? -1 : 1;
+            duration = {
+                y : parseIso(match[2], sign),
+                M : parseIso(match[3], sign),
+                w : parseIso(match[4], sign),
+                d : parseIso(match[5], sign),
+                h : parseIso(match[6], sign),
+                m : parseIso(match[7], sign),
+                s : parseIso(match[8], sign)
+            };
+        } else if (duration == null) {// checks for null or undefined
+            duration = {};
+        } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) {
+            diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to));
+
+            duration = {};
+            duration.ms = diffRes.milliseconds;
+            duration.M = diffRes.months;
+        }
+
+        ret = new Duration(duration);
+
+        if (isDuration(input) && hasOwnProp(input, '_locale')) {
+            ret._locale = input._locale;
+        }
+
+        return ret;
+    }
+
+    create__createDuration.fn = Duration.prototype;
+
+    function parseIso (inp, sign) {
+        // We'd normally use ~~inp for this, but unfortunately it also
+        // converts floats to ints.
+        // inp may be undefined, so careful calling replace on it.
+        var res = inp && parseFloat(inp.replace(',', '.'));
+        // apply sign while we're at it
+        return (isNaN(res) ? 0 : res) * sign;
+    }
+
+    function positiveMomentsDifference(base, other) {
+        var res = {milliseconds: 0, months: 0};
+
+        res.months = other.month() - base.month() +
+            (other.year() - base.year()) * 12;
+        if (base.clone().add(res.months, 'M').isAfter(other)) {
+            --res.months;
+        }
+
+        res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
+
+        return res;
+    }
+
+    function momentsDifference(base, other) {
+        var res;
+        if (!(base.isValid() && other.isValid())) {
+            return {milliseconds: 0, months: 0};
+        }
+
+        other = cloneWithOffset(other, base);
+        if (base.isBefore(other)) {
+            res = positiveMomentsDifference(base, other);
+        } else {
+            res = positiveMomentsDifference(other, base);
+            res.milliseconds = -res.milliseconds;
+            res.months = -res.months;
+        }
+
+        return res;
+    }
+
+    // TODO: remove 'name' arg after deprecation is removed
+    function createAdder(direction, name) {
+        return function (val, period) {
+            var dur, tmp;
+            //invert the arguments, but complain about it
+            if (period !== null && !isNaN(+period)) {
+                deprecateSimple(name, 'moment().' + name  + '(period, number) is deprecated. Please use moment().' + name + '(number, period). ' +
+                'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.');
+                tmp = val; val = period; period = tmp;
+            }
+
+            val = typeof val === 'string' ? +val : val;
+            dur = create__createDuration(val, period);
+            add_subtract__addSubtract(this, dur, direction);
+            return this;
+        };
+    }
+
+    function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) {
+        var milliseconds = duration._milliseconds,
+            days = absRound(duration._days),
+            months = absRound(duration._months);
+
+        if (!mom.isValid()) {
+            // No op
+            return;
+        }
+
+        updateOffset = updateOffset == null ? true : updateOffset;
+
+        if (milliseconds) {
+            mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding);
+        }
+        if (days) {
+            get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding);
+        }
+        if (months) {
+            setMonth(mom, get_set__get(mom, 'Month') + months * isAdding);
+        }
+        if (updateOffset) {
+            utils_hooks__hooks.updateOffset(mom, days || months);
+        }
+    }
+
+    var add_subtract__add      = createAdder(1, 'add');
+    var add_subtract__subtract = createAdder(-1, 'subtract');
+
+    function getCalendarFormat(myMoment, now) {
+        var diff = myMoment.diff(now, 'days', true);
+        return diff < -6 ? 'sameElse' :
+                diff < -1 ? 'lastWeek' :
+                diff < 0 ? 'lastDay' :
+                diff < 1 ? 'sameDay' :
+                diff < 2 ? 'nextDay' :
+                diff < 7 ? 'nextWeek' : 'sameElse';
+    }
+
+    function moment_calendar__calendar (time, formats) {
+        // We want to compare the start of today, vs this.
+        // Getting start-of-today depends on whether we're local/utc/offset or not.
+        var now = time || local__createLocal(),
+            sod = cloneWithOffset(now, this).startOf('day'),
+            format = utils_hooks__hooks.calendarFormat(this, sod) || 'sameElse';
+
+        var output = formats && (isFunction(formats[format]) ? formats[format].call(this, now) : formats[format]);
+
+        return this.format(output || this.localeData().calendar(format, this, local__createLocal(now)));
+    }
+
+    function clone () {
+        return new Moment(this);
+    }
+
+    function isAfter (input, units) {
+        var localInput = isMoment(input) ? input : local__createLocal(input);
+        if (!(this.isValid() && localInput.isValid())) {
+            return false;
+        }
+        units = normalizeUnits(!isUndefined(units) ? units : 'millisecond');
+        if (units === 'millisecond') {
+            return this.valueOf() > localInput.valueOf();
+        } else {
+            return localInput.valueOf() < this.clone().startOf(units).valueOf();
+        }
+    }
+
+    function isBefore (input, units) {
+        var localInput = isMoment(input) ? input : local__createLocal(input);
+        if (!(this.isValid() && localInput.isValid())) {
+            return false;
+        }
+        units = normalizeUnits(!isUndefined(units) ? units : 'millisecond');
+        if (units === 'millisecond') {
+            return this.valueOf() < localInput.valueOf();
+        } else {
+            return this.clone().endOf(units).valueOf() < localInput.valueOf();
+        }
+    }
+
+    function isBetween (from, to, units, inclusivity) {
+        inclusivity = inclusivity || '()';
+        return (inclusivity[0] === '(' ? this.isAfter(from, units) : !this.isBefore(from, units)) &&
+            (inclusivity[1] === ')' ? this.isBefore(to, units) : !this.isAfter(to, units));
+    }
+
+    function isSame (input, units) {
+        var localInput = isMoment(input) ? input : local__createLocal(input),
+            inputMs;
+        if (!(this.isValid() && localInput.isValid())) {
+            return false;
+        }
+        units = normalizeUnits(units || 'millisecond');
+        if (units === 'millisecond') {
+            return this.valueOf() === localInput.valueOf();
+        } else {
+            inputMs = localInput.valueOf();
+            return this.clone().startOf(units).valueOf() <= inputMs && inputMs <= this.clone().endOf(units).valueOf();
+        }
+    }
+
+    function isSameOrAfter (input, units) {
+        return this.isSame(input, units) || this.isAfter(input,units);
+    }
+
+    function isSameOrBefore (input, units) {
+        return this.isSame(input, units) || this.isBefore(input,units);
+    }
+
+    function diff (input, units, asFloat) {
+        var that,
+            zoneDelta,
+            delta, output;
+
+        if (!this.isValid()) {
+            return NaN;
+        }
+
+        that = cloneWithOffset(input, this);
+
+        if (!that.isValid()) {
+            return NaN;
+        }
+
+        zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4;
+
+        units = normalizeUnits(units);
+
+        if (units === 'year' || units === 'month' || units === 'quarter') {
+            output = monthDiff(this, that);
+            if (units === 'quarter') {
+                output = output / 3;
+            } else if (units === 'year') {
+                output = output / 12;
+            }
+        } else {
+            delta = this - that;
+            output = units === 'second' ? delta / 1e3 : // 1000
+                units === 'minute' ? delta / 6e4 : // 1000 * 60
+                units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60
+                units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+                units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+                delta;
+        }
+        return asFloat ? output : absFloor(output);
+    }
+
+    function monthDiff (a, b) {
+        // difference in months
+        var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()),
+            // b is in (anchor - 1 month, anchor + 1 month)
+            anchor = a.clone().add(wholeMonthDiff, 'months'),
+            anchor2, adjust;
+
+        if (b - anchor < 0) {
+            anchor2 = a.clone().add(wholeMonthDiff - 1, 'months');
+            // linear across the month
+            adjust = (b - anchor) / (anchor - anchor2);
+        } else {
+            anchor2 = a.clone().add(wholeMonthDiff + 1, 'months');
+            // linear across the month
+            adjust = (b - anchor) / (anchor2 - anchor);
+        }
+
+        //check for negative zero, return zero if negative zero
+        return -(wholeMonthDiff + adjust) || 0;
+    }
+
+    utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ';
+    utils_hooks__hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]';
+
+    function toString () {
+        return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+    }
+
+    function moment_format__toISOString () {
+        var m = this.clone().utc();
+        if (0 < m.year() && m.year() <= 9999) {
+            if (isFunction(Date.prototype.toISOString)) {
+                // native implementation is ~50x faster, use it when we can
+                return this.toDate().toISOString();
+            } else {
+                return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+            }
+        } else {
+            return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+        }
+    }
+
+    function format (inputString) {
+        if (!inputString) {
+            inputString = this.isUtc() ? utils_hooks__hooks.defaultFormatUtc : utils_hooks__hooks.defaultFormat;
+        }
+        var output = formatMoment(this, inputString);
+        return this.localeData().postformat(output);
+    }
+
+    function from (time, withoutSuffix) {
+        if (this.isValid() &&
+                ((isMoment(time) && time.isValid()) ||
+                 local__createLocal(time).isValid())) {
+            return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
+        } else {
+            return this.localeData().invalidDate();
+        }
+    }
+
+    function fromNow (withoutSuffix) {
+        return this.from(local__createLocal(), withoutSuffix);
+    }
+
+    function to (time, withoutSuffix) {
+        if (this.isValid() &&
+                ((isMoment(time) && time.isValid()) ||
+                 local__createLocal(time).isValid())) {
+            return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix);
+        } else {
+            return this.localeData().invalidDate();
+        }
+    }
+
+    function toNow (withoutSuffix) {
+        return this.to(local__createLocal(), withoutSuffix);
+    }
+
+    // If passed a locale key, it will set the locale for this
+    // instance.  Otherwise, it will return the locale configuration
+    // variables for this instance.
+    function locale (key) {
+        var newLocaleData;
+
+        if (key === undefined) {
+            return this._locale._abbr;
+        } else {
+            newLocaleData = locale_locales__getLocale(key);
+            if (newLocaleData != null) {
+                this._locale = newLocaleData;
+            }
+            return this;
+        }
+    }
+
+    var lang = deprecate(
+        'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+        function (key) {
+            if (key === undefined) {
+                return this.localeData();
+            } else {
+                return this.locale(key);
+            }
+        }
+    );
+
+    function localeData () {
+        return this._locale;
+    }
+
+    function startOf (units) {
+        units = normalizeUnits(units);
+        // the following switch intentionally omits break keywords
+        // to utilize falling through the cases.
+        switch (units) {
+            case 'year':
+                this.month(0);
+                /* falls through */
+            case 'quarter':
+            case 'month':
+                this.date(1);
+                /* falls through */
+            case 'week':
+            case 'isoWeek':
+            case 'day':
+            case 'date':
+                this.hours(0);
+                /* falls through */
+            case 'hour':
+                this.minutes(0);
+                /* falls through */
+            case 'minute':
+                this.seconds(0);
+                /* falls through */
+            case 'second':
+                this.milliseconds(0);
+        }
+
+        // weeks are a special case
+        if (units === 'week') {
+            this.weekday(0);
+        }
+        if (units === 'isoWeek') {
+            this.isoWeekday(1);
+        }
+
+        // quarters are also special
+        if (units === 'quarter') {
+            this.month(Math.floor(this.month() / 3) * 3);
+        }
+
+        return this;
+    }
+
+    function endOf (units) {
+        units = normalizeUnits(units);
+        if (units === undefined || units === 'millisecond') {
+            return this;
+        }
+
+        // 'date' is an alias for 'day', so it should be considered as such.
+        if (units === 'date') {
+            units = 'day';
+        }
+
+        return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
+    }
+
+    function to_type__valueOf () {
+        return this._d.valueOf() - ((this._offset || 0) * 60000);
+    }
+
+    function unix () {
+        return Math.floor(this.valueOf() / 1000);
+    }
+
+    function toDate () {
+        return new Date(this.valueOf());
+    }
+
+    function toArray () {
+        var m = this;
+        return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()];
+    }
+
+    function toObject () {
+        var m = this;
+        return {
+            years: m.year(),
+            months: m.month(),
+            date: m.date(),
+            hours: m.hours(),
+            minutes: m.minutes(),
+            seconds: m.seconds(),
+            milliseconds: m.milliseconds()
+        };
+    }
+
+    function toJSON () {
+        // new Date(NaN).toJSON() === null
+        return this.isValid() ? this.toISOString() : null;
+    }
+
+    function moment_valid__isValid () {
+        return valid__isValid(this);
+    }
+
+    function parsingFlags () {
+        return extend({}, getParsingFlags(this));
+    }
+
+    function invalidAt () {
+        return getParsingFlags(this).overflow;
+    }
+
+    function creationData() {
+        return {
+            input: this._i,
+            format: this._f,
+            locale: this._locale,
+            isUTC: this._isUTC,
+            strict: this._strict
+        };
+    }
+
+    // FORMATTING
+
+    addFormatToken(0, ['gg', 2], 0, function () {
+        return this.weekYear() % 100;
+    });
+
+    addFormatToken(0, ['GG', 2], 0, function () {
+        return this.isoWeekYear() % 100;
+    });
+
+    function addWeekYearFormatToken (token, getter) {
+        addFormatToken(0, [token, token.length], 0, getter);
+    }
+
+    addWeekYearFormatToken('gggg',     'weekYear');
+    addWeekYearFormatToken('ggggg',    'weekYear');
+    addWeekYearFormatToken('GGGG',  'isoWeekYear');
+    addWeekYearFormatToken('GGGGG', 'isoWeekYear');
+
+    // ALIASES
+
+    addUnitAlias('weekYear', 'gg');
+    addUnitAlias('isoWeekYear', 'GG');
+
+    // PRIORITY
+
+    addUnitPriority('weekYear', 1);
+    addUnitPriority('isoWeekYear', 1);
+
+
+    // PARSING
+
+    addRegexToken('G',      matchSigned);
+    addRegexToken('g',      matchSigned);
+    addRegexToken('GG',     match1to2, match2);
+    addRegexToken('gg',     match1to2, match2);
+    addRegexToken('GGGG',   match1to4, match4);
+    addRegexToken('gggg',   match1to4, match4);
+    addRegexToken('GGGGG',  match1to6, match6);
+    addRegexToken('ggggg',  match1to6, match6);
+
+    addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) {
+        week[token.substr(0, 2)] = toInt(input);
+    });
+
+    addWeekParseToken(['gg', 'GG'], function (input, week, config, token) {
+        week[token] = utils_hooks__hooks.parseTwoDigitYear(input);
+    });
+
+    // MOMENTS
+
+    function getSetWeekYear (input) {
+        return getSetWeekYearHelper.call(this,
+                input,
+                this.week(),
+                this.weekday(),
+                this.localeData()._week.dow,
+                this.localeData()._week.doy);
+    }
+
+    function getSetISOWeekYear (input) {
+        return getSetWeekYearHelper.call(this,
+                input, this.isoWeek(), this.isoWeekday(), 1, 4);
+    }
+
+    function getISOWeeksInYear () {
+        return weeksInYear(this.year(), 1, 4);
+    }
+
+    function getWeeksInYear () {
+        var weekInfo = this.localeData()._week;
+        return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+    }
+
+    function getSetWeekYearHelper(input, week, weekday, dow, doy) {
+        var weeksTarget;
+        if (input == null) {
+            return weekOfYear(this, dow, doy).year;
+        } else {
+            weeksTarget = weeksInYear(input, dow, doy);
+            if (week > weeksTarget) {
+                week = weeksTarget;
+            }
+            return setWeekAll.call(this, input, week, weekday, dow, doy);
+        }
+    }
+
+    function setWeekAll(weekYear, week, weekday, dow, doy) {
+        var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy),
+            date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear);
+
+        this.year(date.getUTCFullYear());
+        this.month(date.getUTCMonth());
+        this.date(date.getUTCDate());
+        return this;
+    }
+
+    // FORMATTING
+
+    addFormatToken('Q', 0, 'Qo', 'quarter');
+
+    // ALIASES
+
+    addUnitAlias('quarter', 'Q');
+
+    // PRIORITY
+
+    addUnitPriority('quarter', 7);
+
+    // PARSING
+
+    addRegexToken('Q', match1);
+    addParseToken('Q', function (input, array) {
+        array[MONTH] = (toInt(input) - 1) * 3;
+    });
+
+    // MOMENTS
+
+    function getSetQuarter (input) {
+        return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+    }
+
+    // FORMATTING
+
+    addFormatToken('D', ['DD', 2], 'Do', 'date');
+
+    // ALIASES
+
+    addUnitAlias('date', 'D');
+
+    // PRIOROITY
+    addUnitPriority('date', 9);
+
+    // PARSING
+
+    addRegexToken('D',  match1to2);
+    addRegexToken('DD', match1to2, match2);
+    addRegexToken('Do', function (isStrict, locale) {
+        return isStrict ? locale._ordinalParse : locale._ordinalParseLenient;
+    });
+
+    addParseToken(['D', 'DD'], DATE);
+    addParseToken('Do', function (input, array) {
+        array[DATE] = toInt(input.match(match1to2)[0], 10);
+    });
+
+    // MOMENTS
+
+    var getSetDayOfMonth = makeGetSet('Date', true);
+
+    // FORMATTING
+
+    addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear');
+
+    // ALIASES
+
+    addUnitAlias('dayOfYear', 'DDD');
+
+    // PRIORITY
+    addUnitPriority('dayOfYear', 4);
+
+    // PARSING
+
+    addRegexToken('DDD',  match1to3);
+    addRegexToken('DDDD', match3);
+    addParseToken(['DDD', 'DDDD'], function (input, array, config) {
+        config._dayOfYear = toInt(input);
+    });
+
+    // HELPERS
+
+    // MOMENTS
+
+    function getSetDayOfYear (input) {
+        var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1;
+        return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
+    }
+
+    // FORMATTING
+
+    addFormatToken('m', ['mm', 2], 0, 'minute');
+
+    // ALIASES
+
+    addUnitAlias('minute', 'm');
+
+    // PRIORITY
+
+    addUnitPriority('minute', 14);
+
+    // PARSING
+
+    addRegexToken('m',  match1to2);
+    addRegexToken('mm', match1to2, match2);
+    addParseToken(['m', 'mm'], MINUTE);
+
+    // MOMENTS
+
+    var getSetMinute = makeGetSet('Minutes', false);
+
+    // FORMATTING
+
+    addFormatToken('s', ['ss', 2], 0, 'second');
+
+    // ALIASES
+
+    addUnitAlias('second', 's');
+
+    // PRIORITY
+
+    addUnitPriority('second', 15);
+
+    // PARSING
+
+    addRegexToken('s',  match1to2);
+    addRegexToken('ss', match1to2, match2);
+    addParseToken(['s', 'ss'], SECOND);
+
+    // MOMENTS
+
+    var getSetSecond = makeGetSet('Seconds', false);
+
+    // FORMATTING
+
+    addFormatToken('S', 0, 0, function () {
+        return ~~(this.millisecond() / 100);
+    });
+
+    addFormatToken(0, ['SS', 2], 0, function () {
+        return ~~(this.millisecond() / 10);
+    });
+
+    addFormatToken(0, ['SSS', 3], 0, 'millisecond');
+    addFormatToken(0, ['SSSS', 4], 0, function () {
+        return this.millisecond() * 10;
+    });
+    addFormatToken(0, ['SSSSS', 5], 0, function () {
+        return this.millisecond() * 100;
+    });
+    addFormatToken(0, ['SSSSSS', 6], 0, function () {
+        return this.millisecond() * 1000;
+    });
+    addFormatToken(0, ['SSSSSSS', 7], 0, function () {
+        return this.millisecond() * 10000;
+    });
+    addFormatToken(0, ['SSSSSSSS', 8], 0, function () {
+        return this.millisecond() * 100000;
+    });
+    addFormatToken(0, ['SSSSSSSSS', 9], 0, function () {
+        return this.millisecond() * 1000000;
+    });
+
+
+    // ALIASES
+
+    addUnitAlias('millisecond', 'ms');
+
+    // PRIORITY
+
+    addUnitPriority('millisecond', 16);
+
+    // PARSING
+
+    addRegexToken('S',    match1to3, match1);
+    addRegexToken('SS',   match1to3, match2);
+    addRegexToken('SSS',  match1to3, match3);
+
+    var token;
+    for (token = 'SSSS'; token.length <= 9; token += 'S') {
+        addRegexToken(token, matchUnsigned);
+    }
+
+    function parseMs(input, array) {
+        array[MILLISECOND] = toInt(('0.' + input) * 1000);
+    }
+
+    for (token = 'S'; token.length <= 9; token += 'S') {
+        addParseToken(token, parseMs);
+    }
+    // MOMENTS
+
+    var getSetMillisecond = makeGetSet('Milliseconds', false);
+
+    // FORMATTING
+
+    addFormatToken('z',  0, 0, 'zoneAbbr');
+    addFormatToken('zz', 0, 0, 'zoneName');
+
+    // MOMENTS
+
+    function getZoneAbbr () {
+        return this._isUTC ? 'UTC' : '';
+    }
+
+    function getZoneName () {
+        return this._isUTC ? 'Coordinated Universal Time' : '';
+    }
+
+    var momentPrototype__proto = Moment.prototype;
+
+    momentPrototype__proto.add               = add_subtract__add;
+    momentPrototype__proto.calendar          = moment_calendar__calendar;
+    momentPrototype__proto.clone             = clone;
+    momentPrototype__proto.diff              = diff;
+    momentPrototype__proto.endOf             = endOf;
+    momentPrototype__proto.format            = format;
+    momentPrototype__proto.from              = from;
+    momentPrototype__proto.fromNow           = fromNow;
+    momentPrototype__proto.to                = to;
+    momentPrototype__proto.toNow             = toNow;
+    momentPrototype__proto.get               = stringGet;
+    momentPrototype__proto.invalidAt         = invalidAt;
+    momentPrototype__proto.isAfter           = isAfter;
+    momentPrototype__proto.isBefore          = isBefore;
+    momentPrototype__proto.isBetween         = isBetween;
+    momentPrototype__proto.isSame            = isSame;
+    momentPrototype__proto.isSameOrAfter     = isSameOrAfter;
+    momentPrototype__proto.isSameOrBefore    = isSameOrBefore;
+    momentPrototype__proto.isValid           = moment_valid__isValid;
+    momentPrototype__proto.lang              = lang;
+    momentPrototype__proto.locale            = locale;
+    momentPrototype__proto.localeData        = localeData;
+    momentPrototype__proto.max               = prototypeMax;
+    momentPrototype__proto.min               = prototypeMin;
+    momentPrototype__proto.parsingFlags      = parsingFlags;
+    momentPrototype__proto.set               = stringSet;
+    momentPrototype__proto.startOf           = startOf;
+    momentPrototype__proto.subtract          = add_subtract__subtract;
+    momentPrototype__proto.toArray           = toArray;
+    momentPrototype__proto.toObject          = toObject;
+    momentPrototype__proto.toDate            = toDate;
+    momentPrototype__proto.toISOString       = moment_format__toISOString;
+    momentPrototype__proto.toJSON            = toJSON;
+    momentPrototype__proto.toString          = toString;
+    momentPrototype__proto.unix              = unix;
+    momentPrototype__proto.valueOf           = to_type__valueOf;
+    momentPrototype__proto.creationData      = creationData;
+
+    // Year
+    momentPrototype__proto.year       = getSetYear;
+    momentPrototype__proto.isLeapYear = getIsLeapYear;
+
+    // Week Year
+    momentPrototype__proto.weekYear    = getSetWeekYear;
+    momentPrototype__proto.isoWeekYear = getSetISOWeekYear;
+
+    // Quarter
+    momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter;
+
+    // Month
+    momentPrototype__proto.month       = getSetMonth;
+    momentPrototype__proto.daysInMonth = getDaysInMonth;
+
+    // Week
+    momentPrototype__proto.week           = momentPrototype__proto.weeks        = getSetWeek;
+    momentPrototype__proto.isoWeek        = momentPrototype__proto.isoWeeks     = getSetISOWeek;
+    momentPrototype__proto.weeksInYear    = getWeeksInYear;
+    momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear;
+
+    // Day
+    momentPrototype__proto.date       = getSetDayOfMonth;
+    momentPrototype__proto.day        = momentPrototype__proto.days             = getSetDayOfWeek;
+    momentPrototype__proto.weekday    = getSetLocaleDayOfWeek;
+    momentPrototype__proto.isoWeekday = getSetISODayOfWeek;
+    momentPrototype__proto.dayOfYear  = getSetDayOfYear;
+
+    // Hour
+    momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour;
+
+    // Minute
+    momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute;
+
+    // Second
+    momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond;
+
+    // Millisecond
+    momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond;
+
+    // Offset
+    momentPrototype__proto.utcOffset            = getSetOffset;
+    momentPrototype__proto.utc                  = setOffsetToUTC;
+    momentPrototype__proto.local                = setOffsetToLocal;
+    momentPrototype__proto.parseZone            = setOffsetToParsedOffset;
+    momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset;
+    momentPrototype__proto.isDST                = isDaylightSavingTime;
+    momentPrototype__proto.isLocal              = isLocal;
+    momentPrototype__proto.isUtcOffset          = isUtcOffset;
+    momentPrototype__proto.isUtc                = isUtc;
+    momentPrototype__proto.isUTC                = isUtc;
+
+    // Timezone
+    momentPrototype__proto.zoneAbbr = getZoneAbbr;
+    momentPrototype__proto.zoneName = getZoneName;
+
+    // Deprecations
+    momentPrototype__proto.dates  = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth);
+    momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth);
+    momentPrototype__proto.years  = deprecate('years accessor is deprecated. Use year instead', getSetYear);
+    momentPrototype__proto.zone   = deprecate('moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', getSetZone);
+    momentPrototype__proto.isDSTShifted = deprecate('isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', isDaylightSavingTimeShifted);
+
+    var momentPrototype = momentPrototype__proto;
+
+    function moment__createUnix (input) {
+        return local__createLocal(input * 1000);
+    }
+
+    function moment__createInZone () {
+        return local__createLocal.apply(null, arguments).parseZone();
+    }
+
+    function preParsePostFormat (string) {
+        return string;
+    }
+
+    var prototype__proto = Locale.prototype;
+
+    prototype__proto.calendar        = locale_calendar__calendar;
+    prototype__proto.longDateFormat  = longDateFormat;
+    prototype__proto.invalidDate     = invalidDate;
+    prototype__proto.ordinal         = ordinal;
+    prototype__proto.preparse        = preParsePostFormat;
+    prototype__proto.postformat      = preParsePostFormat;
+    prototype__proto.relativeTime    = relative__relativeTime;
+    prototype__proto.pastFuture      = pastFuture;
+    prototype__proto.set             = locale_set__set;
+
+    // Month
+    prototype__proto.months            =        localeMonths;
+    prototype__proto.monthsShort       =        localeMonthsShort;
+    prototype__proto.monthsParse       =        localeMonthsParse;
+    prototype__proto.monthsRegex       = monthsRegex;
+    prototype__proto.monthsShortRegex  = monthsShortRegex;
+
+    // Week
+    prototype__proto.week = localeWeek;
+    prototype__proto.firstDayOfYear = localeFirstDayOfYear;
+    prototype__proto.firstDayOfWeek = localeFirstDayOfWeek;
+
+    // Day of Week
+    prototype__proto.weekdays       =        localeWeekdays;
+    prototype__proto.weekdaysMin    =        localeWeekdaysMin;
+    prototype__proto.weekdaysShort  =        localeWeekdaysShort;
+    prototype__proto.weekdaysParse  =        localeWeekdaysParse;
+
+    prototype__proto.weekdaysRegex       =        weekdaysRegex;
+    prototype__proto.weekdaysShortRegex  =        weekdaysShortRegex;
+    prototype__proto.weekdaysMinRegex    =        weekdaysMinRegex;
+
+    // Hours
+    prototype__proto.isPM = localeIsPM;
+    prototype__proto.meridiem = localeMeridiem;
+
+    function lists__get (format, index, field, setter) {
+        var locale = locale_locales__getLocale();
+        var utc = create_utc__createUTC().set(setter, index);
+        return locale[field](utc, format);
+    }
+
+    function listMonthsImpl (format, index, field) {
+        if (typeof format === 'number') {
+            index = format;
+            format = undefined;
+        }
+
+        format = format || '';
+
+        if (index != null) {
+            return lists__get(format, index, field, 'month');
+        }
+
+        var i;
+        var out = [];
+        for (i = 0; i < 12; i++) {
+            out[i] = lists__get(format, i, field, 'month');
+        }
+        return out;
+    }
+
+    // ()
+    // (5)
+    // (fmt, 5)
+    // (fmt)
+    // (true)
+    // (true, 5)
+    // (true, fmt, 5)
+    // (true, fmt)
+    function listWeekdaysImpl (localeSorted, format, index, field) {
+        if (typeof localeSorted === 'boolean') {
+            if (typeof format === 'number') {
+                index = format;
+                format = undefined;
+            }
+
+            format = format || '';
+        } else {
+            format = localeSorted;
+            index = format;
+            localeSorted = false;
+
+            if (typeof format === 'number') {
+                index = format;
+                format = undefined;
+            }
+
+            format = format || '';
+        }
+
+        var locale = locale_locales__getLocale(),
+            shift = localeSorted ? locale._week.dow : 0;
+
+        if (index != null) {
+            return lists__get(format, (index + shift) % 7, field, 'day');
+        }
+
+        var i;
+        var out = [];
+        for (i = 0; i < 7; i++) {
+            out[i] = lists__get(format, (i + shift) % 7, field, 'day');
+        }
+        return out;
+    }
+
+    function lists__listMonths (format, index) {
+        return listMonthsImpl(format, index, 'months');
+    }
+
+    function lists__listMonthsShort (format, index) {
+        return listMonthsImpl(format, index, 'monthsShort');
+    }
+
+    function lists__listWeekdays (localeSorted, format, index) {
+        return listWeekdaysImpl(localeSorted, format, index, 'weekdays');
+    }
+
+    function lists__listWeekdaysShort (localeSorted, format, index) {
+        return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort');
+    }
+
+    function lists__listWeekdaysMin (localeSorted, format, index) {
+        return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin');
+    }
+
+    locale_locales__getSetGlobalLocale('en', {
+        ordinalParse: /\d{1,2}(th|st|nd|rd)/,
+        ordinal : function (number) {
+            var b = number % 10,
+                output = (toInt(number % 100 / 10) === 1) ? 'th' :
+                (b === 1) ? 'st' :
+                (b === 2) ? 'nd' :
+                (b === 3) ? 'rd' : 'th';
+            return number + output;
+        }
+    });
+
+    // Side effect imports
+    utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale);
+    utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale);
+
+    var mathAbs = Math.abs;
+
+    function duration_abs__abs () {
+        var data           = this._data;
+
+        this._milliseconds = mathAbs(this._milliseconds);
+        this._days         = mathAbs(this._days);
+        this._months       = mathAbs(this._months);
+
+        data.milliseconds  = mathAbs(data.milliseconds);
+        data.seconds       = mathAbs(data.seconds);
+        data.minutes       = mathAbs(data.minutes);
+        data.hours         = mathAbs(data.hours);
+        data.months        = mathAbs(data.months);
+        data.years         = mathAbs(data.years);
+
+        return this;
+    }
+
+    function duration_add_subtract__addSubtract (duration, input, value, direction) {
+        var other = create__createDuration(input, value);
+
+        duration._milliseconds += direction * other._milliseconds;
+        duration._days         += direction * other._days;
+        duration._months       += direction * other._months;
+
+        return duration._bubble();
+    }
+
+    // supports only 2.0-style add(1, 's') or add(duration)
+    function duration_add_subtract__add (input, value) {
+        return duration_add_subtract__addSubtract(this, input, value, 1);
+    }
+
+    // supports only 2.0-style subtract(1, 's') or subtract(duration)
+    function duration_add_subtract__subtract (input, value) {
+        return duration_add_subtract__addSubtract(this, input, value, -1);
+    }
+
+    function absCeil (number) {
+        if (number < 0) {
+            return Math.floor(number);
+        } else {
+            return Math.ceil(number);
+        }
+    }
+
+    function bubble () {
+        var milliseconds = this._milliseconds;
+        var days         = this._days;
+        var months       = this._months;
+        var data         = this._data;
+        var seconds, minutes, hours, years, monthsFromDays;
+
+        // if we have a mix of positive and negative values, bubble down first
+        // check: https://github.com/moment/moment/issues/2166
+        if (!((milliseconds >= 0 && days >= 0 && months >= 0) ||
+                (milliseconds <= 0 && days <= 0 && months <= 0))) {
+            milliseconds += absCeil(monthsToDays(months) + days) * 864e5;
+            days = 0;
+            months = 0;
+        }
+
+        // The following code bubbles up values, see the tests for
+        // examples of what that means.
+        data.milliseconds = milliseconds % 1000;
+
+        seconds           = absFloor(milliseconds / 1000);
+        data.seconds      = seconds % 60;
+
+        minutes           = absFloor(seconds / 60);
+        data.minutes      = minutes % 60;
+
+        hours             = absFloor(minutes / 60);
+        data.hours        = hours % 24;
+
+        days += absFloor(hours / 24);
+
+        // convert days to months
+        monthsFromDays = absFloor(daysToMonths(days));
+        months += monthsFromDays;
+        days -= absCeil(monthsToDays(monthsFromDays));
+
+        // 12 months -> 1 year
+        years = absFloor(months / 12);
+        months %= 12;
+
+        data.days   = days;
+        data.months = months;
+        data.years  = years;
+
+        return this;
+    }
+
+    function daysToMonths (days) {
+        // 400 years have 146097 days (taking into account leap year rules)
+        // 400 years have 12 months === 4800
+        return days * 4800 / 146097;
+    }
+
+    function monthsToDays (months) {
+        // the reverse of daysToMonths
+        return months * 146097 / 4800;
+    }
+
+    function as (units) {
+        var days;
+        var months;
+        var milliseconds = this._milliseconds;
+
+        units = normalizeUnits(units);
+
+        if (units === 'month' || units === 'year') {
+            days   = this._days   + milliseconds / 864e5;
+            months = this._months + daysToMonths(days);
+            return units === 'month' ? months : months / 12;
+        } else {
+            // handle milliseconds separately because of floating point math errors (issue #1867)
+            days = this._days + Math.round(monthsToDays(this._months));
+            switch (units) {
+                case 'week'   : return days / 7     + milliseconds / 6048e5;
+                case 'day'    : return days         + milliseconds / 864e5;
+                case 'hour'   : return days * 24    + milliseconds / 36e5;
+                case 'minute' : return days * 1440  + milliseconds / 6e4;
+                case 'second' : return days * 86400 + milliseconds / 1000;
+                // Math.floor prevents floating point math errors here
+                case 'millisecond': return Math.floor(days * 864e5) + milliseconds;
+                default: throw new Error('Unknown unit ' + units);
+            }
+        }
+    }
+
+    // TODO: Use this.as('ms')?
+    function duration_as__valueOf () {
+        return (
+            this._milliseconds +
+            this._days * 864e5 +
+            (this._months % 12) * 2592e6 +
+            toInt(this._months / 12) * 31536e6
+        );
+    }
+
+    function makeAs (alias) {
+        return function () {
+            return this.as(alias);
+        };
+    }
+
+    var asMilliseconds = makeAs('ms');
+    var asSeconds      = makeAs('s');
+    var asMinutes      = makeAs('m');
+    var asHours        = makeAs('h');
+    var asDays         = makeAs('d');
+    var asWeeks        = makeAs('w');
+    var asMonths       = makeAs('M');
+    var asYears        = makeAs('y');
+
+    function duration_get__get (units) {
+        units = normalizeUnits(units);
+        return this[units + 's']();
+    }
+
+    function makeGetter(name) {
+        return function () {
+            return this._data[name];
+        };
+    }
+
+    var milliseconds = makeGetter('milliseconds');
+    var seconds      = makeGetter('seconds');
+    var minutes      = makeGetter('minutes');
+    var hours        = makeGetter('hours');
+    var days         = makeGetter('days');
+    var months       = makeGetter('months');
+    var years        = makeGetter('years');
+
+    function weeks () {
+        return absFloor(this.days() / 7);
+    }
+
+    var round = Math.round;
+    var thresholds = {
+        s: 45,  // seconds to minute
+        m: 45,  // minutes to hour
+        h: 22,  // hours to day
+        d: 26,  // days to month
+        M: 11   // months to year
+    };
+
+    // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+    function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+        return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+    }
+
+    function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) {
+        var duration = create__createDuration(posNegDuration).abs();
+        var seconds  = round(duration.as('s'));
+        var minutes  = round(duration.as('m'));
+        var hours    = round(duration.as('h'));
+        var days     = round(duration.as('d'));
+        var months   = round(duration.as('M'));
+        var years    = round(duration.as('y'));
+
+        var a = seconds < thresholds.s && ['s', seconds]  ||
+                minutes <= 1           && ['m']           ||
+                minutes < thresholds.m && ['mm', minutes] ||
+                hours   <= 1           && ['h']           ||
+                hours   < thresholds.h && ['hh', hours]   ||
+                days    <= 1           && ['d']           ||
+                days    < thresholds.d && ['dd', days]    ||
+                months  <= 1           && ['M']           ||
+                months  < thresholds.M && ['MM', months]  ||
+                years   <= 1           && ['y']           || ['yy', years];
+
+        a[2] = withoutSuffix;
+        a[3] = +posNegDuration > 0;
+        a[4] = locale;
+        return substituteTimeAgo.apply(null, a);
+    }
+
+    // This function allows you to set the rounding function for relative time strings
+    function duration_humanize__getSetRelativeTimeRounding (roundingFunction) {
+        if (roundingFunction === undefined) {
+            return round;
+        }
+        if (typeof(roundingFunction) === 'function') {
+            round = roundingFunction;
+            return true;
+        }
+        return false;
+    }
+
+    // This function allows you to set a threshold for relative time strings
+    function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) {
+        if (thresholds[threshold] === undefined) {
+            return false;
+        }
+        if (limit === undefined) {
+            return thresholds[threshold];
+        }
+        thresholds[threshold] = limit;
+        return true;
+    }
+
+    function humanize (withSuffix) {
+        var locale = this.localeData();
+        var output = duration_humanize__relativeTime(this, !withSuffix, locale);
+
+        if (withSuffix) {
+            output = locale.pastFuture(+this, output);
+        }
+
+        return locale.postformat(output);
+    }
+
+    var iso_string__abs = Math.abs;
+
+    function iso_string__toISOString() {
+        // for ISO strings we do not use the normal bubbling rules:
+        //  * milliseconds bubble up until they become hours
+        //  * days do not bubble at all
+        //  * months bubble up until they become years
+        // This is because there is no context-free conversion between hours and days
+        // (think of clock changes)
+        // and also not between days and months (28-31 days per month)
+        var seconds = iso_string__abs(this._milliseconds) / 1000;
+        var days         = iso_string__abs(this._days);
+        var months       = iso_string__abs(this._months);
+        var minutes, hours, years;
+
+        // 3600 seconds -> 60 minutes -> 1 hour
+        minutes           = absFloor(seconds / 60);
+        hours             = absFloor(minutes / 60);
+        seconds %= 60;
+        minutes %= 60;
+
+        // 12 months -> 1 year
+        years  = absFloor(months / 12);
+        months %= 12;
+
+
+        // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+        var Y = years;
+        var M = months;
+        var D = days;
+        var h = hours;
+        var m = minutes;
+        var s = seconds;
+        var total = this.asSeconds();
+
+        if (!total) {
+            // this is the same as C#'s (Noda) and python (isodate)...
+            // but not other JS (goog.date)
+            return 'P0D';
+        }
+
+        return (total < 0 ? '-' : '') +
+            'P' +
+            (Y ? Y + 'Y' : '') +
+            (M ? M + 'M' : '') +
+            (D ? D + 'D' : '') +
+            ((h || m || s) ? 'T' : '') +
+            (h ? h + 'H' : '') +
+            (m ? m + 'M' : '') +
+            (s ? s + 'S' : '');
+    }
+
+    var duration_prototype__proto = Duration.prototype;
+
+    duration_prototype__proto.abs            = duration_abs__abs;
+    duration_prototype__proto.add            = duration_add_subtract__add;
+    duration_prototype__proto.subtract       = duration_add_subtract__subtract;
+    duration_prototype__proto.as             = as;
+    duration_prototype__proto.asMilliseconds = asMilliseconds;
+    duration_prototype__proto.asSeconds      = asSeconds;
+    duration_prototype__proto.asMinutes      = asMinutes;
+    duration_prototype__proto.asHours        = asHours;
+    duration_prototype__proto.asDays         = asDays;
+    duration_prototype__proto.asWeeks        = asWeeks;
+    duration_prototype__proto.asMonths       = asMonths;
+    duration_prototype__proto.asYears        = asYears;
+    duration_prototype__proto.valueOf        = duration_as__valueOf;
+    duration_prototype__proto._bubble        = bubble;
+    duration_prototype__proto.get            = duration_get__get;
+    duration_prototype__proto.milliseconds   = milliseconds;
+    duration_prototype__proto.seconds        = seconds;
+    duration_prototype__proto.minutes        = minutes;
+    duration_prototype__proto.hours          = hours;
+    duration_prototype__proto.days           = days;
+    duration_prototype__proto.weeks          = weeks;
+    duration_prototype__proto.months         = months;
+    duration_prototype__proto.years          = years;
+    duration_prototype__proto.humanize       = humanize;
+    duration_prototype__proto.toISOString    = iso_string__toISOString;
+    duration_prototype__proto.toString       = iso_string__toISOString;
+    duration_prototype__proto.toJSON         = iso_string__toISOString;
+    duration_prototype__proto.locale         = locale;
+    duration_prototype__proto.localeData     = localeData;
+
+    // Deprecations
+    duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString);
+    duration_prototype__proto.lang = lang;
+
+    // Side effect imports
+
+    // FORMATTING
+
+    addFormatToken('X', 0, 0, 'unix');
+    addFormatToken('x', 0, 0, 'valueOf');
+
+    // PARSING
+
+    addRegexToken('x', matchSigned);
+    addRegexToken('X', matchTimestamp);
+    addParseToken('X', function (input, array, config) {
+        config._d = new Date(parseFloat(input, 10) * 1000);
+    });
+    addParseToken('x', function (input, array, config) {
+        config._d = new Date(toInt(input));
+    });
+
+    // Side effect imports
+
+
+    utils_hooks__hooks.version = '2.15.1';
+
+    setHookCallback(local__createLocal);
+
+    utils_hooks__hooks.fn                    = momentPrototype;
+    utils_hooks__hooks.min                   = min;
+    utils_hooks__hooks.max                   = max;
+    utils_hooks__hooks.now                   = now;
+    utils_hooks__hooks.utc                   = create_utc__createUTC;
+    utils_hooks__hooks.unix                  = moment__createUnix;
+    utils_hooks__hooks.months                = lists__listMonths;
+    utils_hooks__hooks.isDate                = isDate;
+    utils_hooks__hooks.locale                = locale_locales__getSetGlobalLocale;
+    utils_hooks__hooks.invalid               = valid__createInvalid;
+    utils_hooks__hooks.duration              = create__createDuration;
+    utils_hooks__hooks.isMoment              = isMoment;
+    utils_hooks__hooks.weekdays              = lists__listWeekdays;
+    utils_hooks__hooks.parseZone             = moment__createInZone;
+    utils_hooks__hooks.localeData            = locale_locales__getLocale;
+    utils_hooks__hooks.isDuration            = isDuration;
+    utils_hooks__hooks.monthsShort           = lists__listMonthsShort;
+    utils_hooks__hooks.weekdaysMin           = lists__listWeekdaysMin;
+    utils_hooks__hooks.defineLocale          = defineLocale;
+    utils_hooks__hooks.updateLocale          = updateLocale;
+    utils_hooks__hooks.locales               = locale_locales__listLocales;
+    utils_hooks__hooks.weekdaysShort         = lists__listWeekdaysShort;
+    utils_hooks__hooks.normalizeUnits        = normalizeUnits;
+    utils_hooks__hooks.relativeTimeRounding = duration_humanize__getSetRelativeTimeRounding;
+    utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold;
+    utils_hooks__hooks.calendarFormat        = getCalendarFormat;
+    utils_hooks__hooks.prototype             = momentPrototype;
+
+    var _moment = utils_hooks__hooks;
+
+    return _moment;
+
+}));
Index: /tags/4.0.0-beta1/src/bp-core/js/webcam.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/webcam.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/webcam.js	(revision 12265)
@@ -0,0 +1,320 @@
+/* global bp, BP_Uploader, _, Backbone */
+
+window.bp = window.bp || {};
+
+( function() {
+
+	// Bail if not set
+	if ( typeof BP_Uploader === 'undefined' ) {
+		return;
+	}
+
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+
+	bp.WebCam = {
+		start: function() {
+			this.params = {
+				video:          null,
+				videoStream:    null,
+				capture_enable: false,
+				capture:        null,
+				canvas:         null,
+				warning:        null,
+				flipped:        false
+			};
+
+			bp.Avatar.nav.on( 'bp-avatar-view:changed', _.bind( this.setView, this ) );
+		},
+
+		setView: function( view ) {
+			if ( 'camera' !== view ) {
+				// Stop the camera if needed
+				if ( ! _.isNull( this.params.video ) ) {
+					this.stop();
+
+					// Remove all warnings as we're changing the view
+					this.removeWarning();
+				}
+
+				// Stop as this is not Camera area
+				return;
+			}
+
+			// Create the WebCam view
+			var cameraView = new bp.Views.WebCamAvatar( { model: new Backbone.Model( { user_media: false } ) } );
+
+			// Make sure the flipped param is reset
+			this.params.flipped = false;
+
+			// Add it to views
+			bp.Avatar.views.add( { id: 'camera', view: cameraView } );
+
+			// Display it
+	        cameraView.inject( '.bp-avatar' );
+		},
+
+		removeView: function() {
+			var camera;
+
+			if ( ! _.isUndefined( bp.Avatar.views.get( 'camera' ) ) ) {
+				camera = bp.Avatar.views.get( 'camera' );
+				camera.get( 'view' ).remove();
+				bp.Avatar.views.remove( { id: 'camera', view: camera } );
+			}
+		},
+
+		gotStream: function( stream ) {
+			var video = bp.WebCam.params.video;
+			bp.WebCam.params.videoStream = stream;
+
+			// User Feedback
+			bp.WebCam.displayWarning( 'loaded' );
+
+			video.onerror = function () {
+				// User Feedback
+				bp.WebCam.displayWarning( 'videoerror' );
+
+				if ( video ) {
+					bp.WebCam.stop();
+				}
+			};
+
+			stream.onended = bp.WebCam.noStream();
+
+			// Older browsers may not have srcObject
+			if ( 'srcObject' in video ) {
+				video.srcObject = stream;
+			} else {
+				// Avoid using this in new browsers, as it is going away.
+				video.src = window.URL.createObjectURL( stream );
+			}
+
+			video.onloadedmetadata = function() {
+				video.play();
+			};
+
+			bp.WebCam.params.capture_enable = true;
+		},
+
+		stop: function() {
+			bp.WebCam.params.capture_enable = false;
+			if ( bp.WebCam.params.videoStream ) {
+				if ( bp.WebCam.params.videoStream.stop ) {
+					bp.WebCam.params.videoStream.stop();
+				} else if ( bp.WebCam.params.videoStream.msStop ) {
+					bp.WebCam.params.videoStream.msStop();
+				}
+				bp.WebCam.params.videoStream.onended = null;
+				bp.WebCam.params.videoStream = null;
+			}
+			if ( bp.WebCam.params.video ) {
+				bp.WebCam.params.video.onerror = null;
+				bp.WebCam.params.video.pause();
+				if ( bp.WebCam.params.video.mozSrcObject ) {
+					bp.WebCam.params.video.mozSrcObject = null;
+				}
+				bp.WebCam.params.video.src = '';
+			}
+		},
+
+		noStream: function() {
+			if ( _.isNull( bp.WebCam.params.videoStream ) ) {
+				// User Feedback
+				bp.WebCam.displayWarning( 'noaccess' );
+
+				bp.WebCam.removeView();
+			}
+		},
+
+		setAvatar: function( avatar ) {
+			if ( ! avatar.get( 'url' ) ) {
+				bp.WebCam.displayWarning( 'nocapture' );
+			}
+
+			// Remove the view
+			bp.WebCam.removeView();
+
+			bp.Avatar.setAvatar( avatar );
+		},
+
+		removeWarning: function() {
+			if ( ! _.isNull( this.params.warning ) ) {
+				this.params.warning.remove();
+			}
+		},
+
+		displayWarning: function( code ) {
+			this.removeWarning();
+
+			this.params.warning = new bp.Views.uploaderWarning( {
+				value: BP_Uploader.strings.camera_warnings[code]
+			} );
+
+			this.params.warning.inject( '.bp-avatar-status' );
+		}
+	};
+
+	// BuddyPress WebCam view
+	bp.Views.WebCamAvatar = bp.View.extend( {
+		tagName: 'div',
+		id: 'bp-webcam-avatar',
+		template: bp.template( 'bp-avatar-webcam' ),
+
+		events: {
+			'click .avatar-webcam-capture': 'captureStream',
+			'click .avatar-webcam-save': 'saveCapture'
+		},
+
+		initialize: function() {
+			var params;
+
+			// Standardize getUserMedia browser call.
+			navigator.getUserMedia = (
+				navigator.getUserMedia ||
+				navigator.webkitGetUserMedia ||
+				navigator.mozGetUserMedia ||
+				navigator.msGetUserMedia ||
+				navigator.oGetUserMedia
+			);
+
+			if ( typeof navigator.getUserMedia !== 'undefined' ) {
+				// We need to add some cropping stuff to use bp.Avatar.setAvatar()
+				params = _.extend( _.pick( BP_Uploader.settings.defaults.multipart_params.bp_params,
+					'object',
+					'item_id',
+					'nonces'
+					), {
+						user_media:  true,
+						w: BP_Uploader.settings.crop.full_w,
+						h: BP_Uploader.settings.crop.full_h,
+						x: 0,
+						y: 0,
+						type: 'camera'
+					}
+				);
+
+				this.model.set( params );
+			}
+
+			this.on( 'ready', this.useStream, this );
+		},
+
+		useStream:function() {
+			// No support for user media... Stop!
+			if ( ! this.model.get( 'user_media' ) ) {
+				return;
+			}
+
+			this.options.video = new bp.Views.WebCamVideo();
+			this.options.canvas = new bp.Views.WebCamCanvas();
+
+			this.$el.find( '#avatar-to-crop' ).append( this.options.video.el );
+			this.$el.find( '#avatar-crop-pane' ).append( this.options.canvas.el );
+
+			bp.WebCam.params.video = this.options.video.el;
+			bp.WebCam.params.canvas = this.options.canvas.el;
+
+			// User Feedback
+			bp.WebCam.displayWarning( 'requesting' );
+
+			// Use deprecated getUserMedia call for browsers that require it.
+			if ( typeof navigator.mediaDevices.getUserMedia === 'undefined' ) {
+				navigator.getUserMedia({
+					audio: false,
+					video: true
+				}, bp.WebCam.gotStream, bp.WebCam.noStream);
+
+			// Teh new hotness!
+			} else {
+				navigator.mediaDevices.getUserMedia({
+					audio: false,
+					video: true
+				}).then(bp.WebCam.gotStream, bp.WebCam.noStream)
+				// ES3 compatibility.
+				['catch'](function() {
+					// User Feedback
+					bp.WebCam.displayWarning( 'errormsg' );
+				});
+			}
+		},
+
+		captureStream: function( event ) {
+			var sx, sc;
+			event.preventDefault();
+
+			if ( ! bp.WebCam.params.capture_enable ) {
+				// User Feedback
+				bp.WebCam.displayWarning( 'loading' );
+				return;
+			}
+
+			if ( this.model.get( 'h' ) > this.options.video.el.videoHeight || this.model.get( 'w' ) > this.options.video.el.videoWidth ) {
+				bp.WebCam.displayWarning( 'videoerror' );
+				return;
+			}
+
+			// Set the offset
+			sc = this.options.video.el.videoHeight;
+			sx = ( this.options.video.el.videoWidth - sc ) / 2;
+
+			// Flip only once.
+			if ( ! bp.WebCam.params.flipped ) {
+				this.options.canvas.el.getContext( '2d' ).translate( this.model.get( 'w' ), 0 );
+				this.options.canvas.el.getContext( '2d' ).scale( -1, 1 );
+				bp.WebCam.params.flipped = true;
+			}
+
+			this.options.canvas.el.getContext( '2d' ).drawImage( this.options.video.el, sx, 0, sc, sc, 0, 0, this.model.get( 'w' ), this.model.get( 'h' ) );
+			bp.WebCam.params.capture = this.options.canvas.el.toDataURL( 'image/png' );
+			this.model.set( 'url', bp.WebCam.params.capture );
+
+			// User Feedback
+			bp.WebCam.displayWarning( 'ready' );
+		},
+
+		saveCapture: function( event ) {
+			event.preventDefault();
+
+			if ( ! bp.WebCam.params.capture ) {
+				// User Feedback
+				bp.WebCam.displayWarning( 'nocapture' );
+				return;
+			}
+
+			bp.WebCam.stop();
+			bp.WebCam.setAvatar( this.model );
+		}
+	} );
+
+	// BuddyPress Video stream view
+	bp.Views.WebCamVideo = bp.View.extend( {
+		tagName: 'video',
+		id: 'bp-webcam-video',
+		attributes: {
+			autoplay: 'autoplay'
+		}
+	} );
+
+	// BuddyPress Canvas (capture) view
+	bp.Views.WebCamCanvas = bp.View.extend( {
+		tagName: 'canvas',
+		id: 'bp-webcam-canvas',
+		attributes: {
+			width:  150,
+			height: 150
+		},
+
+		initialize: function() {
+			// Make sure to take in account bp_core_avatar_full_height or bp_core_avatar_full_width php filters
+			if ( ! _.isUndefined( BP_Uploader.settings.crop.full_h ) && ! _.isUndefined( BP_Uploader.settings.crop.full_w ) ) {
+				this.el.attributes.width.value  = BP_Uploader.settings.crop.full_w;
+				this.el.attributes.height.value = BP_Uploader.settings.crop.full_h;
+			}
+		}
+	} );
+
+	bp.WebCam.start();
+
+})( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-core/js/widget-members.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-core/js/widget-members.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-core/js/widget-members.js	(revision 12265)
@@ -0,0 +1,60 @@
+jQuery(document).ready( function() {
+	member_widget_click_handler();
+
+	// WP 4.5 - Customizer selective refresh support.
+	if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) {
+		wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function() {
+			member_widget_click_handler();
+		} );
+	}
+});
+
+function member_widget_click_handler() {
+	jQuery('.widget div#members-list-options a').on('click',
+		function() {
+			var link = this;
+			jQuery(link).addClass('loading');
+
+			jQuery('.widget div#members-list-options a').removeClass('selected');
+			jQuery(this).addClass('selected');
+
+			jQuery.post( ajaxurl, {
+				action: 'widget_members',
+				'cookie': encodeURIComponent(document.cookie),
+				'_wpnonce': jQuery('input#_wpnonce-members').val(),
+				'max-members': jQuery('input#members_widget_max').val(),
+				'filter': jQuery(this).attr('id')
+			},
+			function(response)
+			{
+				jQuery(link).removeClass('loading');
+				member_widget_response(response);
+			});
+
+			return false;
+		}
+	);
+}
+
+function member_widget_response(response) {
+	response = response.substr(0, response.length-1);
+	response = response.split('[[SPLIT]]');
+
+	if ( response[0] !== '-1' ) {
+		jQuery('.widget ul#members-list').fadeOut(200,
+			function() {
+				jQuery('.widget ul#members-list').html(response[1]);
+				jQuery('.widget ul#members-list').fadeIn(200);
+			}
+		);
+
+	} else {
+		jQuery('.widget ul#members-list').fadeOut(200,
+			function() {
+				var message = '<p>' + response[1] + '</p>';
+				jQuery('.widget ul#members-list').html(message);
+				jQuery('.widget ul#members-list').fadeIn(200);
+			}
+		);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-friends/actions/add-friend.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/actions/add-friend.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/actions/add-friend.php	(revision 12265)
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Friends: Add action
+ *
+ * @package BuddyPress
+ * @subpackage FriendsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process friendship requests.
+ *
+ * @since 1.0.1
+ */
+function friends_action_add_friend() {
+	if ( !bp_is_friends_component() || !bp_is_current_action( 'add-friend' ) )
+		return false;
+
+	if ( !$potential_friend_id = (int)bp_action_variable( 0 ) )
+		return false;
+
+	if ( $potential_friend_id == bp_loggedin_user_id() )
+		return false;
+
+	$friendship_status = BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $potential_friend_id );
+
+	if ( 'not_friends' == $friendship_status ) {
+
+		if ( !check_admin_referer( 'friends_add_friend' ) )
+			return false;
+
+		if ( !friends_add_friend( bp_loggedin_user_id(), $potential_friend_id ) ) {
+			bp_core_add_message( __( 'Friendship could not be requested.', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'Friendship requested', 'buddypress' ) );
+		}
+
+	} elseif ( 'is_friend' == $friendship_status ) {
+		bp_core_add_message( __( 'You are already friends with this user', 'buddypress' ), 'error' );
+	} else {
+		bp_core_add_message( __( 'You already have a pending friendship request with this user', 'buddypress' ), 'error' );
+	}
+
+	bp_core_redirect( wp_get_referer() );
+
+	return false;
+}
+add_action( 'bp_actions', 'friends_action_add_friend' );
Index: /tags/4.0.0-beta1/src/bp-friends/actions/remove-friend.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/actions/remove-friend.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/actions/remove-friend.php	(revision 12265)
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Friends: Remove action
+ *
+ * @package BuddyPress
+ * @subpackage FriendsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process Remove Friendship requests.
+ *
+ * @since 1.0.1
+ */
+function friends_action_remove_friend() {
+	if ( !bp_is_friends_component() || !bp_is_current_action( 'remove-friend' ) )
+		return false;
+
+	if ( !$potential_friend_id = (int)bp_action_variable( 0 ) )
+		return false;
+
+	if ( $potential_friend_id == bp_loggedin_user_id() )
+		return false;
+
+	$friendship_status = BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $potential_friend_id );
+
+	if ( 'is_friend' == $friendship_status ) {
+
+		if ( !check_admin_referer( 'friends_remove_friend' ) )
+			return false;
+
+		if ( !friends_remove_friend( bp_loggedin_user_id(), $potential_friend_id ) ) {
+			bp_core_add_message( __( 'Friendship could not be canceled.', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'Friendship canceled', 'buddypress' ) );
+		}
+
+	} elseif ( 'not_friends' == $friendship_status ) {
+		bp_core_add_message( __( 'You are not yet friends with this user', 'buddypress' ), 'error' );
+	} else {
+		bp_core_add_message( __( 'You have a pending friendship request with this user', 'buddypress' ), 'error' );
+	}
+
+	bp_core_redirect( wp_get_referer() );
+
+	return false;
+}
+add_action( 'bp_actions', 'friends_action_remove_friend' );
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-activity.php	(revision 12265)
@@ -0,0 +1,423 @@
+<?php
+/**
+ * BuddyPress Friends Activity Functions.
+ *
+ * These functions handle the recording, deleting and formatting of activity
+ * for the user and for this specific component.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsActivity
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Record an activity item related to the Friends component.
+ *
+ * A wrapper for {@link bp_activity_add()} that provides some Friends-specific
+ * defaults.
+ *
+ * @since 1.0.0
+ *
+ * @see bp_activity_add() for more detailed description of parameters and
+ *      return values.
+ *
+ * @param array|string $args {
+ *     An array of arguments for the new activity item. Accepts all parameters
+ *     of {@link bp_activity_add()}. The one difference is the following
+ *     argument, which has a different default here:
+ *     @type string $component Default: the id of your Friends component
+ *                             (usually 'friends').
+ * }
+ * @return WP_Error|bool|int See {@link bp_activity_add()}.
+ */
+function friends_record_activity( $args = '' ) {
+
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	$r = wp_parse_args( $args, array(
+		'user_id'           => bp_loggedin_user_id(),
+		'action'            => '',
+		'content'           => '',
+		'primary_link'      => '',
+		'component'         => buddypress()->friends->id,
+		'type'              => false,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'recorded_time'     => bp_core_current_time(),
+		'hide_sitewide'     => false
+	) );
+
+	return bp_activity_add( $r );
+}
+
+/**
+ * Delete an activity item related to the Friends component.
+ *
+ * @since 1.0.0
+ *
+ * @param array $args {
+ *     An array of arguments for the item to delete.
+ *     @type int    $item_id ID of the 'item' associated with the activity item.
+ *                           For Friends activity items, this is usually the user ID of one
+ *                           of the friends.
+ *     @type string $type    The 'type' of the activity item (eg
+ *                           'friendship_accepted').
+ *     @type int    $user_id ID of the user associated with the activity item.
+ * }
+ * @return bool True on success, false on failure.
+ */
+function friends_delete_activity( $args ) {
+	if ( ! bp_is_active( 'activity' ) ) {
+		return;
+	}
+
+	bp_activity_delete_by_item_id( array(
+		'component' => buddypress()->friends->id,
+		'item_id'   => $args['item_id'],
+		'type'      => $args['type'],
+		'user_id'   => $args['user_id']
+	) );
+}
+
+/**
+ * Register the activity actions for bp-friends.
+ *
+ * @since 1.1.0
+ */
+function friends_register_activity_actions() {
+
+	if ( !bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	// These two added in BP 1.6.
+	bp_activity_set_action(
+		$bp->friends->id,
+		'friendship_accepted',
+		__( 'Friendships accepted', 'buddypress' ),
+		'bp_friends_format_activity_action_friendship_accepted',
+		__( 'Friendships', 'buddypress' ),
+		array( 'activity', 'member' )
+	);
+
+	bp_activity_set_action(
+		$bp->friends->id,
+		'friendship_created',
+		__( 'New friendships', 'buddypress' ),
+		'bp_friends_format_activity_action_friendship_created',
+		__( 'Friendships', 'buddypress' ),
+		array( 'activity', 'member' )
+	);
+
+	// < BP 1.6 backpat.
+	bp_activity_set_action( $bp->friends->id, 'friends_register_activity_action', __( 'New friendship created', 'buddypress' ) );
+
+	/**
+	 * Fires after all default bp-friends activity actions have been registered.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'friends_register_activity_actions' );
+}
+add_action( 'bp_register_activity_actions', 'friends_register_activity_actions' );
+
+/**
+ * Format 'friendship_accepted' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Activity action string.
+ * @param object $activity Activity data.
+ * @return string $action Formatted activity action.
+ */
+function bp_friends_format_activity_action_friendship_accepted( $action, $activity ) {
+	$initiator_link = bp_core_get_userlink( $activity->user_id );
+	$friend_link    = bp_core_get_userlink( $activity->secondary_item_id );
+
+	$action = sprintf( __( '%1$s and %2$s are now friends', 'buddypress' ), $initiator_link, $friend_link );
+
+	// Backward compatibility for legacy filter
+	// The old filter has the $friendship object passed to it. We want to
+	// avoid having to build this object if it's not necessary.
+	if ( has_filter( 'friends_activity_friendship_accepted_action' ) ) {
+		$friendship = new BP_Friends_Friendship( $activity->item_id );
+		$action     = apply_filters( 'friends_activity_friendsip_accepted_action', $action, $friendship );
+	}
+
+	/**
+	 * Filters the 'friendship_accepted' activity action format.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   String text for the 'friendship_accepted' action.
+	 * @param object $activity Activity data.
+	 */
+	return apply_filters( 'bp_friends_format_activity_action_friendship_accepted', $action, $activity );
+}
+
+/**
+ * Format 'friendship_created' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data.
+ * @return string $action Formatted activity action.
+ */
+function bp_friends_format_activity_action_friendship_created( $action, $activity ) {
+	$initiator_link = bp_core_get_userlink( $activity->user_id );
+	$friend_link    = bp_core_get_userlink( $activity->secondary_item_id );
+
+	$action = sprintf( __( '%1$s and %2$s are now friends', 'buddypress' ), $initiator_link, $friend_link );
+
+	// Backward compatibility for legacy filter
+	// The old filter has the $friendship object passed to it. We want to
+	// avoid having to build this object if it's not necessary.
+	if ( has_filter( 'friends_activity_friendship_accepted_action' ) ) {
+		$friendship = new BP_Friends_Friendship( $activity->item_id );
+		$action     = apply_filters( 'friends_activity_friendsip_accepted_action', $action, $friendship );
+	}
+
+	/**
+	 * Filters the 'friendship_created' activity action format.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   String text for the 'friendship_created' action.
+	 * @param object $activity Activity data.
+	 */
+	return apply_filters( 'bp_friends_format_activity_action_friendship_created', $action, $activity );
+}
+
+/**
+ * Fetch data related to friended users at the beginning of an activity loop.
+ *
+ * This reduces database overhead during the activity loop.
+ *
+ * @since 2.0.0
+ *
+ * @param array $activities Array of activity items.
+ * @return array
+ */
+function bp_friends_prefetch_activity_object_data( $activities ) {
+	if ( empty( $activities ) ) {
+		return $activities;
+	}
+
+	$friend_ids = array();
+
+	foreach ( $activities as $activity ) {
+		if ( buddypress()->friends->id !== $activity->component ) {
+			continue;
+		}
+
+		$friend_ids[] = $activity->secondary_item_id;
+	}
+
+	if ( ! empty( $friend_ids ) ) {
+		// Fire a user query to prime user caches.
+		new BP_User_Query( array(
+			'user_ids'          => $friend_ids,
+			'populate_extras'   => false,
+			'update_meta_cache' => false,
+		) );
+	}
+
+	return $activities;
+}
+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 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 = array(), $filter = array() ) {
+
+	// Determine the user_id.
+	if ( ! empty( $filter['user_id'] ) ) {
+		$user_id = $filter['user_id'];
+	} else {
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+	}
+
+	// Determine friends of user.
+	$friends = friends_get_friend_user_ids( $user_id );
+	if ( empty( $friends ) ) {
+		$friends = array( 0 );
+	}
+
+	$retval = array(
+		'relation' => 'AND',
+		array(
+			'column'  => 'user_id',
+			'compare' => 'IN',
+			'value'   => (array) $friends
+		),
+
+		// We should only be able to view sitewide activity content for friends.
+		array(
+			'column' => 'hide_sitewide',
+			'value'  => 0
+		),
+
+		// Overrides.
+		'override' => array(
+			'filter'      => array( 'user_id' => 0 ),
+			'show_hidden' => true
+		),
+	);
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_friends_scope_args', 'bp_friends_filter_activity_scope', 10, 2 );
+
+/**
+ * Set up activity arguments for use with the 'just-me' scope.
+ *
+ * For details on the syntax, see {@link BP_Activity_Query}.
+ *
+ * @since 2.2.0
+ *
+ * @param array $retval Empty array by default.
+ * @param array $filter Current activity arguments.
+ * @return array
+ */
+function bp_friends_filter_activity_just_me_scope( $retval = array(), $filter = array() ) {
+
+	// Determine the user_id.
+	if ( ! empty( $filter['user_id'] ) ) {
+		$user_id = $filter['user_id'];
+	} else {
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+	}
+
+	// Get the requested action.
+	$action = $filter['filter']['action'];
+
+	// Make sure actions are listed in an array.
+	if ( ! is_array( $action ) ) {
+		$action = explode( ',', $filter['filter']['action'] );
+	}
+
+	$action = array_flip( array_filter( $action ) );
+
+	/**
+	 * If filtering activities for something other than the friendship_created
+	 * action return without changing anything
+	 */
+	if ( ! empty( $action ) && ! isset( $action['friendship_created'] ) ) {
+		return $retval;
+	}
+
+	// Juggle existing override value.
+	$override = array();
+	if ( ! empty( $retval['override'] ) ) {
+		$override = $retval['override'];
+		unset( $retval['override'] );
+	}
+
+	/**
+	 * Else make sure to get the friendship_created action, the user is involved in
+	 * - user initiated the friendship
+	 * - user has been requested a friendship
+	 */
+	$retval = array(
+		'relation' => 'OR',
+		$retval,
+		array(
+			'relation' => 'AND',
+			array(
+				'column' => 'component',
+				'value'  => 'friends',
+			),
+			array(
+				'column' => 'secondary_item_id',
+				'value'  => $user_id,
+			),
+		)
+	);
+
+	// Juggle back override value.
+	if ( ! empty( $override ) ) {
+		$retval['override'] = $override;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_just-me_scope_args', 'bp_friends_filter_activity_just_me_scope', 20, 2 );
+
+/**
+ * Add activity stream items when one members accepts another members request
+ * for virtual friendship.
+ *
+ * @since 1.9.0
+ *
+ * @param int         $friendship_id       ID of the friendship.
+ * @param int         $initiator_user_id   ID of friendship initiator.
+ * @param int         $friend_user_id      ID of user whose friendship is requested.
+ * @param object|bool $friendship Optional Friendship object.
+ */
+function bp_friends_friendship_accepted_activity( $friendship_id, $initiator_user_id, $friend_user_id, $friendship = false ) {
+	if ( ! bp_is_active( 'activity' ) ) {
+		return;
+	}
+
+	// Record in activity streams for the initiator.
+	friends_record_activity( array(
+		'user_id'           => $initiator_user_id,
+		'type'              => 'friendship_created',
+		'item_id'           => $friendship_id,
+		'secondary_item_id' => $friend_user_id
+	) );
+}
+add_action( 'friends_friendship_accepted', 'bp_friends_friendship_accepted_activity', 10, 4 );
+
+/**
+ * Deletes friendship activity items when a user is deleted.
+ *
+ * @since 2.5.0
+ *
+ * @param int $user_id The ID of the user being deleted.
+ */
+function bp_friends_delete_activity_on_user_delete( $user_id = 0 ) {
+	if ( ! bp_is_active( 'activity' ) ) {
+		return;
+	}
+
+	bp_activity_delete( array(
+		'component'         => buddypress()->friends->id,
+		'type'              => 'friendship_created',
+		'secondary_item_id' => $user_id
+	) );
+}
+add_action( 'friends_remove_data', 'bp_friends_delete_activity_on_user_delete' );
+
+/**
+ * Remove friendship activity item when a friendship is deleted.
+ *
+ * @since 3.2.0
+ *
+ * @param int $friendship_id ID of the friendship.
+ */
+function bp_friends_delete_activity_on_friendship_delete( $friendship_id ) {
+	friends_delete_activity( array( 'item_id' => $friendship_id, 'type' => 'friendship_created', 'user_id' => 0 ) );
+}
+add_action( 'friends_friendship_deleted', 'bp_friends_delete_activity_on_friendship_delete' );
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-cache.php	(revision 12265)
@@ -0,0 +1,148 @@
+<?php
+/**
+ * BuddyPress Friends Caching.
+ *
+ * Caching functions handle the clearing of cached objects and pages on specific
+ * actions throughout BuddyPress.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsCaching
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Clear friends-related cache for members of a specific friendship.
+ *
+ * @since 1.0.0
+ *
+ * @param int $friendship_id ID of the friendship whose two members should
+ *                           have their friends cache busted.
+ * @return bool
+ */
+function friends_clear_friend_object_cache( $friendship_id ) {
+	if ( !$friendship = new BP_Friends_Friendship( $friendship_id ) )
+		return false;
+
+	wp_cache_delete( 'friends_friend_ids_' .    $friendship->initiator_user_id, 'bp' );
+	wp_cache_delete( 'friends_friend_ids_' .    $friendship->friend_user_id,    'bp' );
+}
+
+// List actions to clear object caches on.
+add_action( 'friends_friendship_accepted', 'friends_clear_friend_object_cache' );
+add_action( 'friends_friendship_deleted',  'friends_clear_friend_object_cache' );
+
+/**
+ * Clear friendship caches on friendship changes.
+ *
+ * @since 2.7.0
+ *
+ * @param int $friendship_id     ID of the friendship that has changed.
+ * @param int $initiator_user_id ID of the first user.
+ * @param int $friend_user_id    ID of the second user.
+ * @return bool
+ */
+function bp_friends_clear_bp_friends_friendships_cache( $friendship_id, $initiator_user_id, $friend_user_id ) {
+	// Clear friendship ID cache for each user.
+	wp_cache_delete( $initiator_user_id, 'bp_friends_friendships_for_user' );
+	wp_cache_delete( $friend_user_id,    'bp_friends_friendships_for_user' );
+
+	// Clear the friendship object cache.
+	wp_cache_delete( $friendship_id, 'bp_friends_friendships' );
+
+	// Clear incremented cache.
+	$friendship = new stdClass;
+	$friendship->initiator_user_id = $initiator_user_id;
+	$friendship->friend_user_id    = $friend_user_id;
+	bp_friends_delete_cached_friendships_on_friendship_save( $friendship );
+}
+add_action( 'friends_friendship_requested', 'bp_friends_clear_bp_friends_friendships_cache', 10, 3 );
+add_action( 'friends_friendship_accepted',  'bp_friends_clear_bp_friends_friendships_cache', 10, 3 );
+add_action( 'friends_friendship_deleted',   'bp_friends_clear_bp_friends_friendships_cache', 10, 3 );
+
+/**
+ * Clear friendship caches on friendship changes.
+ *
+ * @since 2.7.0
+ *
+ * @param int                   $friendship_id The friendship ID.
+ * @param BP_Friends_Friendship $friendship Friendship object.
+ */
+function bp_friends_clear_bp_friends_friendships_cache_remove( $friendship_id, BP_Friends_Friendship $friendship ) {
+	// Clear friendship ID cache for each user.
+	wp_cache_delete( $friendship->initiator_user_id, 'bp_friends_friendships_for_user' );
+	wp_cache_delete( $friendship->friend_user_id,    'bp_friends_friendships_for_user' );
+
+	// Clear the friendship object cache.
+	wp_cache_delete( $friendship_id, 'bp_friends_friendships' );
+
+	// Clear incremented cache.
+	bp_friends_delete_cached_friendships_on_friendship_save( $friendship );
+}
+add_action( 'friends_friendship_withdrawn', 'bp_friends_clear_bp_friends_friendships_cache_remove', 10, 2 );
+add_action( 'friends_friendship_rejected',  'bp_friends_clear_bp_friends_friendships_cache_remove', 10, 2 );
+
+/**
+ * Clear the friend request cache for the user not initiating the friendship.
+ *
+ * @since 2.0.0
+ *
+ * @param int $friend_user_id The user ID not initiating the friendship.
+ */
+function bp_friends_clear_request_cache( $friend_user_id ) {
+	wp_cache_delete( $friend_user_id, 'bp_friends_requests' );
+}
+
+/**
+ * Clear the friend request cache when a friendship is saved.
+ *
+ * A friendship is deemed saved when a friendship is requested or accepted.
+ *
+ * @since 2.0.0
+ *
+ * @param int $friendship_id     The friendship ID.
+ * @param int $initiator_user_id The user ID initiating the friendship.
+ * @param int $friend_user_id    The user ID not initiating the friendship.
+ */
+function bp_friends_clear_request_cache_on_save( $friendship_id, $initiator_user_id, $friend_user_id ) {
+	bp_friends_clear_request_cache( $friend_user_id );
+}
+add_action( 'friends_friendship_requested', 'bp_friends_clear_request_cache_on_save', 10, 3 );
+add_action( 'friends_friendship_accepted',  'bp_friends_clear_request_cache_on_save', 10, 3 );
+
+/**
+ * Clear the friend request cache when a friendship is removed.
+ *
+ * A friendship is deemed removed when a friendship is withdrawn or rejected.
+ *
+ * @since 2.0.0
+ *
+ * @param int                   $friendship_id The friendship ID.
+ * @param BP_Friends_Friendship $friendship Friendship object.
+ */
+function bp_friends_clear_request_cache_on_remove( $friendship_id, BP_Friends_Friendship $friendship ) {
+	bp_friends_clear_request_cache( $friendship->friend_user_id );
+}
+add_action( 'friends_friendship_withdrawn', 'bp_friends_clear_request_cache_on_remove', 10, 2 );
+add_action( 'friends_friendship_rejected',  'bp_friends_clear_request_cache_on_remove', 10, 2 );
+
+/**
+ * Delete individual friendships from the cache when they are changed.
+ *
+ * @since 3.0.0
+ *
+ * @param BP_Friends_Friendship $friendship Friendship object.
+ */
+function bp_friends_delete_cached_friendships_on_friendship_save( $friendship ) {
+	bp_core_delete_incremented_cache( $friendship->friend_user_id . ':' . $friendship->initiator_user_id, 'bp_friends' );
+	bp_core_delete_incremented_cache( $friendship->initiator_user_id . ':' . $friendship->friend_user_id, 'bp_friends' );
+}
+add_action( 'friends_friendship_after_save', 'bp_friends_delete_cached_friendships_on_friendship_save' );
+
+// List actions to clear super cached pages on, if super cache is installed.
+add_action( 'friends_friendship_rejected',  'bp_core_clear_cache' );
+add_action( 'friends_friendship_accepted',  'bp_core_clear_cache' );
+add_action( 'friends_friendship_deleted',   'bp_core_clear_cache' );
+add_action( 'friends_friendship_requested', 'bp_core_clear_cache' );
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-filters.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+/**
+ * BuddyPress Friend Filters.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsFilters
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Filter BP_User_Query::populate_extras to add confirmed friendship status.
+ *
+ * Each member in the user query is checked for confirmed friendship status
+ * against the logged-in user.
+ *
+ * @since 1.7.0
+ *
+ * @global WPDB $wpdb WordPress database access object.
+ *
+ * @param BP_User_Query $user_query   The BP_User_Query object.
+ * @param string        $user_ids_sql Comma-separated list of user IDs to fetch extra
+ *                                    data for, as determined by BP_User_Query.
+ */
+function bp_friends_filter_user_query_populate_extras( BP_User_Query $user_query, $user_ids_sql ) {
+	global $wpdb;
+
+	// Stop if user isn't logged in.
+	if ( ! $user_id = bp_loggedin_user_id() ) {
+		return;
+	}
+
+	$maybe_friend_ids = wp_parse_id_list( $user_ids_sql );
+
+	// Bulk prepare the friendship cache.
+	BP_Friends_Friendship::update_bp_friends_cache( $user_id, $maybe_friend_ids );
+
+	foreach ( $maybe_friend_ids as $friend_id ) {
+		$status = BP_Friends_Friendship::check_is_friend( $user_id, $friend_id );
+		$user_query->results[ $friend_id ]->friendship_status = $status;
+		if ( 'is_friend' == $status ) {
+			$user_query->results[ $friend_id ]->is_friend = 1;
+		}
+	}
+
+}
+add_filter( 'bp_user_query_populate_extras', 'bp_friends_filter_user_query_populate_extras', 4, 2 );
+
+/**
+ * Registers Friends personal data exporter.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_friends_register_personal_data_exporters( $exporters ) {
+	$exporters['buddypress-friends'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Friends', 'buddypress' ),
+		'callback'               => 'bp_friends_personal_data_exporter',
+	);
+
+	$exporters['buddypress-friends-pending-sent-requests'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Friend Requests (Sent)', 'buddypress' ),
+		'callback'               => 'bp_friends_pending_sent_requests_personal_data_exporter',
+	);
+
+	$exporters['buddypress-friends-pending-received-requests'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Friend Requests (Received)', 'buddypress' ),
+		'callback'               => 'bp_friends_pending_received_requests_personal_data_exporter',
+	);
+
+	return $exporters;
+}
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_friends_register_personal_data_exporters' );
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-functions.php	(revision 12265)
@@ -0,0 +1,1096 @@
+<?php
+/**
+ * BuddyPress Friends Functions.
+ *
+ * Functions are where all the magic happens in BuddyPress. They will
+ * handle the actual saving or manipulation of information. Usually they will
+ * hand off to a database class for data access, then return
+ * true or false on success or failure.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Create a new friendship.
+ *
+ * @since 1.0.0
+ *
+ * @param int  $initiator_userid ID of the "initiator" user (the user who is
+ *                               sending the friendship request).
+ * @param int  $friend_userid    ID of the "friend" user (the user whose friendship
+ *                               is being requested).
+ * @param bool $force_accept     Optional. Whether to force acceptance. When false,
+ *                               running friends_add_friend() will result in a friendship request.
+ *                               When true, running friends_add_friend() will result in an accepted
+ *                               friendship, with no notifications being sent. Default: false.
+ * @return bool True on success, false on failure.
+ */
+function friends_add_friend( $initiator_userid, $friend_userid, $force_accept = false ) {
+
+	// You cannot be friends with yourself!
+	if ( $initiator_userid == $friend_userid ) {
+		return false;
+	}
+
+	// Check if already friends, and bail if so.
+	if ( friends_check_friendship( $initiator_userid, $friend_userid ) ) {
+		return true;
+	}
+
+	// Setup the friendship data.
+	$friendship = new BP_Friends_Friendship;
+	$friendship->initiator_user_id = (int) $initiator_userid;
+	$friendship->friend_user_id    = (int) $friend_userid;
+	$friendship->is_confirmed      = 0;
+	$friendship->is_limited        = 0;
+	$friendship->date_created      = bp_core_current_time();
+
+	if ( ! empty( $force_accept ) ) {
+		$friendship->is_confirmed = 1;
+	}
+
+	// Bail if friendship could not be saved (how sad!).
+	if ( ! $friendship->save() ) {
+		return false;
+	}
+
+	// Send notifications.
+	if ( empty( $force_accept ) ) {
+		$action = 'requested';
+
+	// Update friend totals.
+	} else {
+		$action = 'accepted';
+		friends_update_friend_totals( $friendship->initiator_user_id, $friendship->friend_user_id, 'add' );
+	}
+
+	/**
+	 * Fires at the end of initiating a new friendship connection.
+	 *
+	 * This is a variable hook, depending on context.
+	 * The two potential hooks are: friends_friendship_requested, friends_friendship_accepted.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int    $id                ID of the pending friendship connection.
+	 * @param int    $initiator_user_id ID of the friendship initiator.
+	 * @param int    $friend_user_id    ID of the friend user.
+	 * @param object $friendship        BuddyPress Friendship Object.
+	 */
+	do_action( 'friends_friendship_' . $action, $friendship->id, $friendship->initiator_user_id, $friendship->friend_user_id, $friendship );
+
+	return true;
+}
+
+/**
+ * Remove a friendship.
+ *
+ * Will also delete the related "friendship_accepted" activity item.
+ *
+ * @since 1.0.0
+ *
+ * @param int $initiator_userid ID of the friendship initiator.
+ * @param int $friend_userid    ID of the friend user.
+ * @return bool True on success, false on failure.
+ */
+function friends_remove_friend( $initiator_userid, $friend_userid ) {
+
+	$friendship_id = BP_Friends_Friendship::get_friendship_id( $initiator_userid, $friend_userid );
+	$friendship    = new BP_Friends_Friendship( $friendship_id );
+
+	/**
+	 * Fires before the deletion of a friendship activity item
+	 * for the user who canceled the friendship.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $friendship_id    ID of the friendship object, if any, between a pair of users.
+	 * @param int $initiator_userid ID of the friendship initiator.
+	 * @param int $friend_userid    ID of the friend user.
+	 */
+	do_action( 'friends_before_friendship_delete', $friendship_id, $initiator_userid, $friend_userid );
+
+	/**
+	 * Fires before the friendship connection is removed.
+	 *
+	 * This hook is misleadingly named - the friendship is not yet deleted.
+	 * This is your last chance to do something while the friendship exists.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $friendship_id    ID of the friendship object, if any, between a pair of users.
+	 * @param int $initiator_userid ID of the friendship initiator.
+	 * @param int $friend_userid    ID of the friend user.
+	 */
+	do_action( 'friends_friendship_deleted', $friendship_id, $initiator_userid, $friend_userid );
+
+	if ( $friendship->delete() ) {
+		friends_update_friend_totals( $initiator_userid, $friend_userid, 'remove' );
+
+		/**
+		 * Fires after the friendship connection is removed.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param int $initiator_userid ID of the friendship initiator.
+		 * @param int $friend_userid    ID of the friend user.
+		 */
+		do_action( 'friends_friendship_post_delete', $initiator_userid, $friend_userid );
+
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Mark a friendship request as accepted.
+ *
+ * Also initiates a "friendship_accepted" activity item.
+ *
+ * @since 1.0.0
+ *
+ * @param int $friendship_id ID of the pending friendship object.
+ * @return bool True on success, false on failure.
+ */
+function friends_accept_friendship( $friendship_id ) {
+
+	// Get the friendship data.
+	$friendship = new BP_Friends_Friendship( $friendship_id, true, false );
+
+	// Accepting friendship.
+	if ( empty( $friendship->is_confirmed ) && BP_Friends_Friendship::accept( $friendship_id ) ) {
+
+		// Bump the friendship counts.
+		friends_update_friend_totals( $friendship->initiator_user_id, $friendship->friend_user_id );
+
+		/**
+		 * Fires after a friendship is accepted.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int    $id                ID of the pending friendship object.
+		 * @param int    $initiator_user_id ID of the friendship initiator.
+		 * @param int    $friend_user_id    ID of the user requested friendship with.
+		 * @param object $friendship        BuddyPress Friendship Object.
+		 */
+		do_action( 'friends_friendship_accepted', $friendship->id, $friendship->initiator_user_id, $friendship->friend_user_id, $friendship );
+
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Mark a friendship request as rejected.
+ *
+ * @since 1.0.0
+ *
+ * @param int $friendship_id ID of the pending friendship object.
+ * @return bool True on success, false on failure.
+ */
+function friends_reject_friendship( $friendship_id ) {
+	$friendship = new BP_Friends_Friendship( $friendship_id, true, false );
+
+	if ( empty( $friendship->is_confirmed ) && BP_Friends_Friendship::reject( $friendship_id ) ) {
+
+		/**
+		 * Fires after a friendship request is rejected.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int                   $friendship_id ID of the pending friendship.
+		 * @param BP_Friends_Friendship $friendships Friendship object. Passed by reference.
+		 */
+		do_action_ref_array( 'friends_friendship_rejected', array( $friendship_id, &$friendship ) );
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Withdraw a friendship request.
+ *
+ * @since 1.6.0
+ *
+ * @param int $initiator_userid ID of the friendship initiator - this is the
+ *                              user who requested the friendship, and is doing the withdrawing.
+ * @param int $friend_userid    ID of the requested friend.
+ * @return bool True on success, false on failure.
+ */
+function friends_withdraw_friendship( $initiator_userid, $friend_userid ) {
+	$friendship_id = BP_Friends_Friendship::get_friendship_id( $initiator_userid, $friend_userid );
+	$friendship    = new BP_Friends_Friendship( $friendship_id, true, false );
+
+	if ( empty( $friendship->is_confirmed ) && BP_Friends_Friendship::withdraw( $friendship_id ) ) {
+
+		// @deprecated Since 1.9
+		do_action_ref_array( 'friends_friendship_whithdrawn', array( $friendship_id, &$friendship ) );
+
+		/**
+		 * Fires after a friendship request has been withdrawn.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int                   $friendship_id ID of the friendship.
+		 * @param BP_Friends_Friendship $friendship    Friendship object. Passed by reference.
+		 */
+		do_action_ref_array( 'friends_friendship_withdrawn',  array( $friendship_id, &$friendship ) );
+
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Check whether two users are friends.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id            ID of the first user.
+ * @param int $possible_friend_id ID of the other user.
+ * @return bool Returns true if the two users are friends, otherwise false.
+ */
+function friends_check_friendship( $user_id, $possible_friend_id ) {
+
+	if ( 'is_friend' == BP_Friends_Friendship::check_is_friend( $user_id, $possible_friend_id ) )
+		return true;
+
+	return false;
+}
+
+/**
+ * Get the friendship status of two friends.
+ *
+ * Will return 'is_friends', 'not_friends', 'pending' or 'awaiting_response'.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id            ID of the first user.
+ * @param int $possible_friend_id ID of the other user.
+ * @return string Friend status of the two users.
+ */
+function friends_check_friendship_status( $user_id, $possible_friend_id ) {
+	global $members_template;
+
+	// Check the BP_User_Query first
+	// @see bp_friends_filter_user_query_populate_extras().
+	if ( ! empty( $members_template->in_the_loop ) ) {
+		if ( isset( $members_template->member->friendship_status ) ) {
+			return $members_template->member->friendship_status;
+		}
+	}
+
+	return BP_Friends_Friendship::check_is_friend( $user_id, $possible_friend_id );
+}
+
+/**
+ * Get the friend count of a given user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id ID of the user whose friends are being counted.
+ * @return int Friend count of the user.
+ */
+function friends_get_total_friend_count( $user_id = 0 ) {
+	if ( empty( $user_id ) )
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+
+	$count = bp_get_user_meta( $user_id, 'total_friend_count', true );
+	if ( empty( $count ) )
+		$count = 0;
+
+	/**
+	 * Filters the total friend count for a given user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $count Total friend count for a given user.
+	 */
+	return apply_filters( 'friends_get_total_friend_count', $count );
+}
+
+/**
+ * Check whether a given user has any friends.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user whose friends are being checked.
+ * @return bool True if the user has friends, otherwise false.
+ */
+function friends_check_user_has_friends( $user_id ) {
+	$friend_count = friends_get_total_friend_count( $user_id );
+
+	if ( empty( $friend_count ) )
+		return false;
+
+	if ( !(int) $friend_count )
+		return false;
+
+	return true;
+}
+
+/**
+ * Get the ID of two users' friendship, if it exists.
+ *
+ * @since 1.2.0
+ *
+ * @param int $initiator_user_id ID of the first user.
+ * @param int $friend_user_id    ID of the second user.
+ * @return int|null ID of the friendship if found, otherwise false.
+ */
+function friends_get_friendship_id( $initiator_user_id, $friend_user_id ) {
+	return BP_Friends_Friendship::get_friendship_id( $initiator_user_id, $friend_user_id );
+}
+
+/**
+ * Get the IDs of a given user's friends.
+ *
+ * @since 1.0.0
+ *
+ * @param int  $user_id              ID of the user whose friends are being retrieved.
+ * @param bool $friend_requests_only Optional. Whether to fetch unaccepted
+ *                                   requests only. Default: false.
+ * @param bool $assoc_arr            Optional. True to receive an array of arrays keyed as
+ *                                   'user_id' => $user_id; false to get a one-dimensional
+ *                                   array of user IDs. Default: false.
+ * @return array
+ */
+function friends_get_friend_user_ids( $user_id, $friend_requests_only = false, $assoc_arr = false ) {
+	return BP_Friends_Friendship::get_friend_user_ids( $user_id, $friend_requests_only, $assoc_arr );
+}
+
+/**
+ * Search the friends of a user by a search string.
+ *
+ * @since 1.0.0
+ *
+ * @param string $search_terms The search string, matched against xprofile fields (if
+ *                             available), or usermeta 'nickname' field.
+ * @param int    $user_id      ID of the user whose friends are being searched.
+ * @param int    $pag_num      Optional. Max number of friends to return.
+ * @param int    $pag_page     Optional. The page of results to return. Default: null (no
+ *                             pagination - return all results).
+ * @return array|bool On success, an array: {
+ *     @type array $friends IDs of friends returned by the query.
+ *     @type int   $count   Total number of friends (disregarding
+ *                          pagination) who match the search.
+ * }. Returns false on failure.
+ */
+function friends_search_friends( $search_terms, $user_id, $pag_num = 10, $pag_page = 1 ) {
+	return BP_Friends_Friendship::search_friends( $search_terms, $user_id, $pag_num, $pag_page );
+}
+
+/**
+ * Get a list of IDs of users who have requested friendship of a given user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id The ID of the user who has received the friendship requests.
+ * @return array|bool An array of user IDs, or false if none are found.
+ */
+function friends_get_friendship_request_user_ids( $user_id ) {
+	return BP_Friends_Friendship::get_friendship_request_user_ids( $user_id );
+}
+
+/**
+ * Get a user's most recently active friends.
+ *
+ * @since 1.0.0
+ *
+ * @see BP_Core_User::get_users() for a description of return value.
+ *
+ * @param int    $user_id  ID of the user whose friends are being retrieved.
+ * @param int    $per_page Optional. Number of results to return per page.
+ *                         Default: 0 (no pagination; show all results).
+ * @param int    $page     Optional. Number of the page of results to return.
+ *                         Default: 0 (no pagination; show all results).
+ * @param string $filter   Optional. Limit results to those matching a search
+ *                         string.
+ * @return array See {@link BP_Core_User::get_users()}.
+ */
+function friends_get_recently_active( $user_id, $per_page = 0, $page = 0, $filter = '' ) {
+	$friends = bp_core_get_users( array(
+		'type'         => 'active',
+		'per_page'     => $per_page,
+		'page'         => $page,
+		'user_id'      => $user_id,
+		'search_terms' => $filter,
+	) );
+
+	/**
+	 * Filters a user's most recently active friends.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $friends {
+	 *     @type int   $total_users Total number of users matched by query params.
+	 *     @type array $paged_users The current page of users matched by query params.
+	 * }
+	 */
+	return apply_filters( 'friends_get_recently_active', $friends );
+}
+
+/**
+ * Get a user's friends, in alphabetical order.
+ *
+ * @since 1.0.0
+ *
+ * @see BP_Core_User::get_users() for a description of return value.
+ *
+ * @param int    $user_id  ID of the user whose friends are being retrieved.
+ * @param int    $per_page Optional. Number of results to return per page.
+ *                         Default: 0 (no pagination; show all results).
+ * @param int    $page     Optional. Number of the page of results to return.
+ *                         Default: 0 (no pagination; show all results).
+ * @param string $filter   Optional. Limit results to those matching a search
+ *                         string.
+ * @return array See {@link BP_Core_User::get_users()}.
+ */
+function friends_get_alphabetically( $user_id, $per_page = 0, $page = 0, $filter = '' ) {
+	$friends = bp_core_get_users( array(
+		'type'         => 'alphabetical',
+		'per_page'     => $per_page,
+		'page'         => $page,
+		'user_id'      => $user_id,
+		'search_terms' => $filter,
+	) );
+
+	/**
+	 * Filters a user's friends listed in alphabetical order.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return array $friends {
+	 *     @type int   $total_users Total number of users matched by query params.
+	 *     @type array $paged_users The current page of users matched by query params.
+	 * }
+	 */
+	return apply_filters( 'friends_get_alphabetically', $friends );
+}
+
+/**
+ * Get a user's friends, in the order in which they joined the site.
+ *
+ * @since 1.0.0
+ *
+ * @see BP_Core_User::get_users() for a description of return value.
+ *
+ * @param int    $user_id  ID of the user whose friends are being retrieved.
+ * @param int    $per_page Optional. Number of results to return per page.
+ *                         Default: 0 (no pagination; show all results).
+ * @param int    $page     Optional. Number of the page of results to return.
+ *                         Default: 0 (no pagination; show all results).
+ * @param string $filter   Optional. Limit results to those matching a search
+ *                         string.
+ * @return array See {@link BP_Core_User::get_users()}.
+ */
+function friends_get_newest( $user_id, $per_page = 0, $page = 0, $filter = '' ) {
+	$friends = bp_core_get_users( array(
+		'type'         => 'newest',
+		'per_page'     => $per_page,
+		'page'         => $page,
+		'user_id'      => $user_id,
+		'search_terms' => $filter,
+	) );
+
+	/**
+	 * Filters a user's friends listed from newest to oldest.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $friends {
+	 *     @type int   $total_users Total number of users matched by query params.
+	 *     @type array $paged_users The current page of users matched by query params.
+	 * }
+	 */
+	return apply_filters( 'friends_get_newest', $friends );
+}
+
+/**
+ * Get the last active date of many users at once.
+ *
+ * @since 1.0.0
+ *
+ * @see BP_Friends_Friendship::get_bulk_last_active() for a description of
+ *      arguments and return value.
+ *
+ * @param array $friend_ids See BP_Friends_Friendship::get_bulk_last_active().
+ * @return array $user_ids See BP_Friends_Friendship::get_bulk_last_active().
+ */
+function friends_get_bulk_last_active( $friend_ids ) {
+	return BP_Friends_Friendship::get_bulk_last_active( $friend_ids );
+}
+
+/**
+ * Get a list of friends that a user can invite into this group.
+ *
+ * Excludes friends that are already in the group, and banned friends if the
+ * user is not a group admin.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  User ID whose friends to see can be invited. Default:
+ *                      ID of the logged-in user.
+ * @param int $group_id Group to check possible invitations against.
+ * @return mixed False if no friends, array of users if friends.
+ */
+function friends_get_friends_invite_list( $user_id = 0, $group_id = 0 ) {
+
+	// Default to logged in user id.
+	if ( empty( $user_id ) )
+		$user_id = bp_loggedin_user_id();
+
+	// Only group admins can invited previously banned users.
+	$user_is_admin = (bool) groups_is_user_admin( $user_id, $group_id );
+
+	// Assume no friends.
+	$friends = array();
+
+	/**
+	 * Filters default arguments for list of friends a user can invite into this group.
+	 *
+	 * @since 1.5.4
+	 *
+	 * @param array $value Array of default parameters for invite list.
+	 */
+	$args = apply_filters( 'bp_friends_pre_get_invite_list', array(
+		'user_id'  => $user_id,
+		'type'     => 'alphabetical',
+		'per_page' => 0
+	) );
+
+	// User has friends.
+	if ( bp_has_members( $args ) ) {
+
+		/**
+		 * Loop through all friends and try to add them to the invitation list.
+		 *
+		 * Exclude friends that:
+		 *     1. are already members of the group
+		 *     2. are banned from this group if the current user is also not a
+		 *        group admin.
+		 */
+		while ( bp_members() ) :
+
+			// Load the member.
+			bp_the_member();
+
+			// Get the user ID of the friend.
+			$friend_user_id = bp_get_member_user_id();
+
+			// Skip friend if already in the group.
+			if ( groups_is_user_member( $friend_user_id, $group_id ) )
+				continue;
+
+			// Skip friend if not group admin and user banned from group.
+			if ( ( false === $user_is_admin ) && groups_is_user_banned( $friend_user_id, $group_id ) )
+				continue;
+
+			// Friend is safe, so add it to the array of possible friends.
+			$friends[] = array(
+				'id'        => $friend_user_id,
+				'full_name' => bp_get_member_name()
+			);
+
+		endwhile;
+	}
+
+	// If no friends, explicitly set to false.
+	if ( empty( $friends ) )
+		$friends = false;
+
+	/**
+	 * Filters the list of potential friends that can be invited to this group.
+	 *
+	 * @since 1.5.4
+	 *
+	 * @param array|bool $friends  Array friends available to invite or false for no friends.
+	 * @param int        $user_id  ID of the user checked for who they can invite.
+	 * @param int        $group_id ID of the group being checked on.
+	 */
+	return apply_filters( 'bp_friends_get_invite_list', $friends, $user_id, $group_id );
+}
+
+/**
+ * Get a count of a user's friends who can be invited to a given group.
+ *
+ * Users can invite any of their friends except:
+ *
+ * - users who are already in the group
+ * - users who have a pending invite to the group
+ * - users who have been banned from the group
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user whose friends are being counted.
+ * @param int $group_id ID of the group friends are being invited to.
+ * @return int $invitable_count Eligible friend count.
+ */
+function friends_count_invitable_friends( $user_id, $group_id ) {
+	return BP_Friends_Friendship::get_invitable_friend_count( $user_id, $group_id );
+}
+
+/**
+ * Get a total friend count for a given user.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id Optional. ID of the user whose friendships you are
+ *                     counting. Default: displayed user (if any), otherwise logged-in user.
+ * @return int Friend count for the user.
+ */
+function friends_get_friend_count_for_user( $user_id ) {
+	return BP_Friends_Friendship::total_friend_count( $user_id );
+}
+
+/**
+ * Return a list of a user's friends, filtered by a search term.
+ *
+ * @since 1.0.0
+ *
+ * @param string $search_terms Search term to filter on.
+ * @param int    $user_id      ID of the user whose friends are being searched.
+ * @param int    $pag_num      Number of results to return per page. Default: 0 (no
+ *                             pagination - show all results).
+ * @param int    $pag_page     Number of the page being requested. Default: 0 (no
+ *                             pagination - show all results).
+ * @return array Array of BP_Core_User objects corresponding to friends.
+ */
+function friends_search_users( $search_terms, $user_id, $pag_num = 0, $pag_page = 0 ) {
+
+	$user_ids = BP_Friends_Friendship::search_users( $search_terms, $user_id, $pag_num, $pag_page );
+
+	if ( empty( $user_ids ) )
+		return false;
+
+	$users = array();
+	for ( $i = 0, $count = count( $user_ids ); $i < $count; ++$i )
+		$users[] = new BP_Core_User( $user_ids[$i] );
+
+	return array( 'users' => $users, 'count' => BP_Friends_Friendship::search_users_count( $search_terms ) );
+}
+
+/**
+ * Has a friendship been confirmed (accepted)?
+ *
+ * @since 1.0.0
+ *
+ * @param int $friendship_id The ID of the friendship being checked.
+ * @return bool True if the friendship is confirmed, otherwise false.
+ */
+function friends_is_friendship_confirmed( $friendship_id ) {
+	$friendship = new BP_Friends_Friendship( $friendship_id );
+	return $friendship->is_confirmed;
+}
+
+/**
+ * Update user friend counts.
+ *
+ * Friend counts are cached in usermeta for performance reasons. After a
+ * friendship event (acceptance, deletion), call this function to regenerate
+ * the cached values.
+ *
+ * @since 1.0.0
+ *
+ * @param int    $initiator_user_id ID of the first user.
+ * @param int    $friend_user_id    ID of the second user.
+ * @param string $status            Optional. The friendship event that's been triggered.
+ *                                  'add' will ++ each user's friend counts, while any other string
+ *                                  will --.
+ */
+function friends_update_friend_totals( $initiator_user_id, $friend_user_id, $status = 'add' ) {
+
+	if ( 'add' == $status ) {
+		bp_update_user_meta( $initiator_user_id, 'total_friend_count', (int)bp_get_user_meta( $initiator_user_id, 'total_friend_count', true ) + 1 );
+		bp_update_user_meta( $friend_user_id, 'total_friend_count', (int)bp_get_user_meta( $friend_user_id, 'total_friend_count', true ) + 1 );
+	} else {
+		bp_update_user_meta( $initiator_user_id, 'total_friend_count', (int)bp_get_user_meta( $initiator_user_id, 'total_friend_count', true ) - 1 );
+		bp_update_user_meta( $friend_user_id, 'total_friend_count', (int)bp_get_user_meta( $friend_user_id, 'total_friend_count', true ) - 1 );
+	}
+}
+
+/**
+ * Remove all friends-related data concerning a given user.
+ *
+ * Removes the following:
+ *
+ * - Friendships of which the user is a member.
+ * - Cached friend count for the user.
+ * - Notifications of friendship requests sent by the user.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user whose friend data is being removed.
+ */
+function friends_remove_data( $user_id ) {
+
+	/**
+	 * Fires before deletion of friend-related data for a given user.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $user_id ID for the user whose friend data is being removed.
+	 */
+	do_action( 'friends_before_remove_data', $user_id );
+
+	BP_Friends_Friendship::delete_all_for_user( $user_id );
+
+	// Remove usermeta.
+	bp_delete_user_meta( $user_id, 'total_friend_count' );
+
+	/**
+	 * Fires after deletion of friend-related data for a given user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id ID for the user whose friend data is being removed.
+	 */
+	do_action( 'friends_remove_data', $user_id );
+}
+add_action( 'wpmu_delete_user',  'friends_remove_data' );
+add_action( 'delete_user',       'friends_remove_data' );
+add_action( 'bp_make_spam_user', 'friends_remove_data' );
+
+/**
+ * Used by the Activity component's @mentions to print a JSON list of the current user's friends.
+ *
+ * This is intended to speed up @mentions lookups for a majority of use cases.
+ *
+ * @since 2.1.0
+ *
+ * @see bp_activity_mentions_script()
+ */
+function bp_friends_prime_mentions_results() {
+
+	// Stop here if user is not logged in.
+	if ( ! is_user_logged_in() ) {
+		return;
+	}
+
+	if ( ! bp_activity_maybe_load_mentions_scripts() ) {
+		return;
+	}
+
+	// Bail out if the site has a ton of users.
+	if ( is_multisite() && wp_is_large_network( 'users' ) ) {
+		return;
+	}
+
+	if ( friends_get_total_friend_count( get_current_user_id() ) > 30 ) {
+		return;
+	}
+
+	$friends_query = array(
+		'count_total'     => '',                    // Prevents total count.
+		'populate_extras' => false,
+
+		'type'            => 'alphabetical',
+		'user_id'         => get_current_user_id(),
+	);
+
+	$friends_query = new BP_User_Query( $friends_query );
+	$results       = array();
+
+	foreach ( $friends_query->results as $user ) {
+		$result        = new stdClass();
+		$result->ID    = $user->user_nicename;
+		$result->image = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
+
+		if ( ! empty( $user->display_name ) && ! bp_disable_profile_sync() ) {
+			$result->name = $user->display_name;
+		} else {
+			$result->name = bp_core_get_user_displayname( $user->ID );
+		}
+
+		$results[] = $result;
+	}
+
+	wp_localize_script( 'bp-mentions', 'BP_Suggestions', array(
+		'friends' => $results,
+	) );
+}
+add_action( 'bp_activity_mentions_prime_results', 'bp_friends_prime_mentions_results' );
+
+/** Emails ********************************************************************/
+
+/**
+ * Send notifications related to a new friendship request.
+ *
+ * When a friendship is requested, an email and a BP notification are sent to
+ * the user of whom friendship has been requested ($friend_id).
+ *
+ * @since 1.0.0
+ *
+ * @param int $friendship_id ID of the friendship object.
+ * @param int $initiator_id  ID of the user who initiated the request.
+ * @param int $friend_id     ID of the request recipient.
+ */
+function friends_notification_new_request( $friendship_id, $initiator_id, $friend_id ) {
+	if ( 'no' == bp_get_user_meta( (int) $friend_id, 'notification_friends_friendship_request', true ) ) {
+		return;
+	}
+
+	$unsubscribe_args = array(
+		'user_id'           => $friend_id,
+		'notification_type' => 'friends-request',
+	);
+
+	$args = array(
+		'tokens' => array(
+			'friend-requests.url' => esc_url( bp_core_get_user_domain( $friend_id ) . bp_get_friends_slug() . '/requests/' ),
+			'friend.id'           => $friend_id,
+			'friendship.id'       => $friendship_id,
+			'initiator.id'        => $initiator_id,
+			'initiator.url'       => esc_url( bp_core_get_user_domain( $initiator_id ) ),
+			'initiator.name'      => bp_core_get_user_displayname( $initiator_id ),
+			'unsubscribe'         => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+		),
+	);
+	bp_send_email( 'friends-request', $friend_id, $args );
+}
+add_action( 'friends_friendship_requested', 'friends_notification_new_request', 10, 3 );
+
+/**
+ * Send notifications related to the acceptance of a friendship request.
+ *
+ * When a friendship request is accepted, an email and a BP notification are
+ * sent to the user who requested the friendship ($initiator_id).
+ *
+ * @since 1.0.0
+ *
+ * @param int $friendship_id ID of the friendship object.
+ * @param int $initiator_id  ID of the user who initiated the request.
+ * @param int $friend_id     ID of the request recipient.
+ */
+function friends_notification_accepted_request( $friendship_id, $initiator_id, $friend_id ) {
+	if ( 'no' == bp_get_user_meta( (int) $initiator_id, 'notification_friends_friendship_accepted', true ) ) {
+		return;
+	}
+
+	$unsubscribe_args = array(
+		'user_id'           => $initiator_id,
+		'notification_type' => 'friends-request-accepted',
+	);
+
+	$args = array(
+		'tokens' => array(
+			'friend.id'      => $friend_id,
+			'friendship.url' => esc_url( bp_core_get_user_domain( $friend_id ) ),
+			'friend.name'    => bp_core_get_user_displayname( $friend_id ),
+			'friendship.id'  => $friendship_id,
+			'initiator.id'   => $initiator_id,
+			'unsubscribe'	   => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+		),
+	);
+	bp_send_email( 'friends-request-accepted', $initiator_id, $args );
+}
+add_action( 'friends_friendship_accepted', 'friends_notification_accepted_request', 10, 3 );
+
+/**
+ * Finds and exports friendship data associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_friends_personal_data_exporter( $email_address, $page ) {
+	$number = 50;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$friendships = BP_Friends_Friendship::get_friendships( $user->ID, array(
+		'is_confirmed' => true,
+		'page'         => $page,
+		'per_page'     => $number,
+	) );
+
+	$user_data_to_export = array();
+
+	foreach ( $friendships as $friendship ) {
+		if ( (int) $user->ID === (int) $friendship->initiator_user_id ) {
+			$friend_id         = $friendship->friend_user_id;
+			$user_is_initiator = true;
+		} else {
+			$friend_id         = $friendship->initiator_user_id;
+			$user_is_initiator = false;
+		}
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Friend', 'buddypress' ),
+				'value' => bp_core_get_userlink( $friend_id ),
+			),
+			array(
+				'name'  => __( 'Initiated By Me', 'buddypress' ),
+				'value' => $user_is_initiator ? __( 'Yes', 'buddypress' ) : __( 'No', 'buddypress' ),
+			),
+			array(
+				'name'  => __( 'Friendship Date', 'buddypress' ),
+				'value' => $friendship->date_created,
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_friends',
+			'group_label' => __( 'Friends', 'buddypress' ),
+			'item_id'     => "bp-friends-{$friend_id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $friendships ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
+
+/**
+ * Finds and exports pending sent friendship request data associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_friends_pending_sent_requests_personal_data_exporter( $email_address, $page ) {
+	$number = 50;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$friendships = BP_Friends_Friendship::get_friendships( $user->ID, array(
+		'is_confirmed'      => false,
+		'initiator_user_id' => $user->ID,
+		'page'              => $page,
+		'per_page'          => $number,
+	) );
+
+	$user_data_to_export = array();
+
+	foreach ( $friendships as $friendship ) {
+		$item_data = array(
+			array(
+				'name'  => __( 'Recipient', 'buddypress' ),
+				'value' => bp_core_get_userlink( $friendship->friend_user_id ),
+			),
+			array(
+				'name'  => __( 'Date Sent', 'buddypress' ),
+				'value' => $friendship->date_created,
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_friends_pending_sent_requests',
+			'group_label' => __( 'Pending Friend Requests (Sent)', 'buddypress' ),
+			'item_id'     => "bp-friends-pending-sent-request-{$friendship->friend_user_id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $friendships ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
+
+/**
+ * Finds and exports pending received friendship request data associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_friends_pending_received_requests_personal_data_exporter( $email_address, $page ) {
+	$number = 50;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$friendships = BP_Friends_Friendship::get_friendships( $user->ID, array(
+		'is_confirmed'   => false,
+		'friend_user_id' => $user->ID,
+		'page'           => $page,
+		'per_page'       => $number,
+	) );
+
+	$user_data_to_export = array();
+
+	foreach ( $friendships as $friendship ) {
+		$item_data = array(
+			array(
+				'name'  => __( 'Requester', 'buddypress' ),
+				'value' => bp_core_get_userlink( $friendship->initiator_user_id ),
+			),
+			array(
+				'name'  => __( 'Date Sent', 'buddypress' ),
+				'value' => $friendship->date_created,
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_friends_pending_received_requests',
+			'group_label' => __( 'Pending Friend Requests (Received)', 'buddypress' ),
+			'item_id'     => "bp-friends-pending-received-request-{$friendship->initiator_user_id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $friendships ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-loader.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BuddyPress Friends Streams Loader.
+ *
+ * The friends component is for users to create relationships with each other.
+ *
+ * @package BuddyPress
+ * @subpackage Friends
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-friends component.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_friends() {
+	buddypress()->friends = new BP_Friends_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_friends', 6 );
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-notifications.php	(revision 12265)
@@ -0,0 +1,297 @@
+<?php
+/**
+ * BuddyPress Friends Activity Functions.
+ *
+ * These functions handle the recording, deleting and formatting of activity
+ * for the user and for this specific component.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsNotifications
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Notification formatting callback for bp-friends notifications.
+ *
+ * @since 1.0.0
+ *
+ * @param string $action            The kind of notification being rendered.
+ * @param int    $item_id           The primary item ID.
+ * @param int    $secondary_item_id The secondary item ID.
+ * @param int    $total_items       The total number of messaging-related notifications
+ *                                  waiting for the user.
+ * @param string $format            'string' for BuddyBar-compatible notifications;
+ *                                  'array' for WP Toolbar. Default: 'string'.
+ * @return array|string
+ */
+function friends_format_notifications( $action, $item_id, $secondary_item_id, $total_items, $format = 'string' ) {
+
+	switch ( $action ) {
+		case 'friendship_accepted':
+			$link = trailingslashit( bp_loggedin_user_domain() . bp_get_friends_slug() . '/my-friends' );
+
+			// $action and $amount are used to generate dynamic filter names.
+			$action = 'accepted';
+
+			// Set up the string and the filter.
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( '%d friends accepted your friendship requests', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+			} else {
+				$text = sprintf( __( '%s accepted your friendship request', 'buddypress' ),  bp_core_get_user_displayname( $item_id ) );
+				$amount = 'single';
+			}
+
+			break;
+
+		case 'friendship_request':
+			$link = bp_loggedin_user_domain() . bp_get_friends_slug() . '/requests/?new';
+
+			$action = 'request';
+
+			// Set up the string and the filter.
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( 'You have %d pending friendship requests', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+			} else {
+				$text = sprintf( __( 'You have a friendship request from %s', 'buddypress' ),  bp_core_get_user_displayname( $item_id ) );
+				$amount = 'single';
+			}
+
+			break;
+	}
+
+	// Return either an HTML link or an array, depending on the requested format.
+	if ( 'string' == $format ) {
+
+		/**
+		 * Filters the format of friendship notifications based on type and amount * of notifications pending.
+		 *
+		 * This is a variable filter that has four possible versions.
+		 * The four possible versions are:
+		 *   - bp_friends_single_friendship_accepted_notification
+		 *   - bp_friends_multiple_friendship_accepted_notification
+		 *   - bp_friends_single_friendship_request_notification
+		 *   - bp_friends_multiple_friendship_request_notification
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string|array $value       Depending on format, an HTML link to new requests profile
+		 *                                  tab or array with link and text.
+		 * @param int          $total_items The total number of messaging-related notifications
+		 *                                  waiting for the user.
+		 * @param int          $item_id     The primary item ID.
+		 */
+		$return = apply_filters( 'bp_friends_' . $amount . '_friendship_' . $action . '_notification', '<a href="' . esc_url( $link ) . '">' . esc_html( $text ) . '</a>', (int) $total_items, $item_id );
+	} else {
+		/** This filter is documented in bp-friends/bp-friends-notifications.php */
+		$return = apply_filters( 'bp_friends_' . $amount . '_friendship_' . $action . '_notification', array(
+			'link' => $link,
+			'text' => $text
+		), (int) $total_items, $item_id );
+	}
+
+	/**
+	 * Fires at the end of the bp-friends notification format callback.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string       $action            The kind of notification being rendered.
+	 * @param int          $item_id           The primary item ID.
+	 * @param int          $secondary_item_id The secondary item ID.
+	 * @param int          $total_items       The total number of messaging-related notifications
+	 *                                        waiting for the user.
+	 * @param array|string $return            Notification text string or array of link and text.
+	 */
+	do_action( 'friends_format_notifications', $action, $item_id, $secondary_item_id, $total_items, $return );
+
+	return $return;
+}
+
+/**
+ * Clear friend-related notifications when ?new=1
+ *
+ * @since 1.2.0
+ */
+function friends_clear_friend_notifications() {
+	if ( isset( $_GET['new'] ) ) {
+		bp_notifications_mark_notifications_by_type( bp_loggedin_user_id(), buddypress()->friends->id, 'friendship_accepted' );
+	}
+}
+add_action( 'bp_activity_screen_my_activity', 'friends_clear_friend_notifications' );
+
+/**
+ * Delete any friendship request notifications for the logged in user.
+ *
+ * @since 1.9.0
+ */
+function bp_friends_mark_friendship_request_notifications_by_type() {
+	if ( isset( $_GET['new'] ) ) {
+		bp_notifications_mark_notifications_by_type( bp_loggedin_user_id(), buddypress()->friends->id, 'friendship_request' );
+	}
+}
+add_action( 'friends_screen_requests', 'bp_friends_mark_friendship_request_notifications_by_type' );
+
+/**
+ * Delete any friendship acceptance notifications for the logged in user.
+ *
+ * @since 1.9.0
+ */
+function bp_friends_mark_friendship_accepted_notifications_by_type() {
+	bp_notifications_mark_notifications_by_type( bp_loggedin_user_id(), buddypress()->friends->id, 'friendship_accepted' );
+}
+add_action( 'friends_screen_my_friends', 'bp_friends_mark_friendship_accepted_notifications_by_type' );
+
+/**
+ * Notify one use that another user has requested their virtual friendship.
+ *
+ * @since 1.9.0
+ *
+ * @param int $friendship_id     The unique ID of the friendship.
+ * @param int $initiator_user_id The friendship initiator user ID.
+ * @param int $friend_user_id    The friendship request receiver user ID.
+ */
+function bp_friends_friendship_requested_notification( $friendship_id, $initiator_user_id, $friend_user_id ) {
+	bp_notifications_add_notification( array(
+		'user_id'           => $friend_user_id,
+		'item_id'           => $initiator_user_id,
+		'secondary_item_id' => $friendship_id,
+		'component_name'    => buddypress()->friends->id,
+		'component_action'  => 'friendship_request',
+		'date_notified'     => bp_core_current_time(),
+		'is_new'            => 1,
+	) );
+}
+add_action( 'friends_friendship_requested', 'bp_friends_friendship_requested_notification', 10, 3 );
+
+/**
+ * Remove friend request notice when a member rejects another members
+ *
+ * @since 1.9.0
+ *
+ * @param int    $friendship_id Friendship ID (not used).
+ * @param object $friendship    Friendship object.
+ */
+function bp_friends_mark_friendship_rejected_notifications_by_item_id( $friendship_id, $friendship ) {
+	bp_notifications_mark_notifications_by_item_id( $friendship->friend_user_id, $friendship->initiator_user_id, buddypress()->friends->id, 'friendship_request' );
+}
+add_action( 'friends_friendship_rejected', 'bp_friends_mark_friendship_rejected_notifications_by_item_id', 10, 2 );
+
+/**
+ * Notify a member when another member accepts their virtual friendship request.
+ *
+ * @since 1.9.0
+ *
+ * @param int $friendship_id     The unique ID of the friendship.
+ * @param int $initiator_user_id The friendship initiator user ID.
+ * @param int $friend_user_id    The friendship request receiver user ID.
+ */
+function bp_friends_add_friendship_accepted_notification( $friendship_id, $initiator_user_id, $friend_user_id ) {
+	// Remove the friend request notice.
+	bp_notifications_mark_notifications_by_item_id( $friend_user_id, $initiator_user_id, buddypress()->friends->id, 'friendship_request' );
+
+	// Add a friend accepted notice for the initiating user.
+	bp_notifications_add_notification(  array(
+		'user_id'           => $initiator_user_id,
+		'item_id'           => $friend_user_id,
+		'secondary_item_id' => $friendship_id,
+		'component_name'    => buddypress()->friends->id,
+		'component_action'  => 'friendship_accepted',
+		'date_notified'     => bp_core_current_time(),
+		'is_new'            => 1,
+	) );
+}
+add_action( 'friends_friendship_accepted', 'bp_friends_add_friendship_accepted_notification', 10, 3 );
+
+/**
+ * Remove friend request notice when a member withdraws their friend request.
+ *
+ * @since 1.9.0
+ *
+ * @param int    $friendship_id Friendship ID (not used).
+ * @param object $friendship    Friendship Object.
+ */
+function bp_friends_mark_friendship_withdrawn_notifications_by_item_id( $friendship_id, $friendship ) {
+	bp_notifications_delete_notifications_by_item_id( $friendship->friend_user_id, $friendship->initiator_user_id, buddypress()->friends->id, 'friendship_request' );
+}
+add_action( 'friends_friendship_withdrawn', 'bp_friends_mark_friendship_withdrawn_notifications_by_item_id', 10, 2 );
+
+/**
+ * Remove friendship requests FROM user, used primarily when a user is deleted.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id ID of the user whose notifications are removed.
+ */
+function bp_friends_remove_notifications_data( $user_id = 0 ) {
+	bp_notifications_delete_notifications_from_user( $user_id, buddypress()->friends->id, 'friendship_request' );
+}
+add_action( 'friends_remove_data', 'bp_friends_remove_notifications_data', 10, 1 );
+
+/**
+ * Add Friends-related settings to the Settings > Notifications page.
+ *
+ * @since 1.0.0
+ */
+function friends_screen_notification_settings() {
+
+	if ( !$send_requests = bp_get_user_meta( bp_displayed_user_id(), 'notification_friends_friendship_request', true ) )
+		$send_requests   = 'yes';
+
+	if ( !$accept_requests = bp_get_user_meta( bp_displayed_user_id(), 'notification_friends_friendship_accepted', true ) )
+		$accept_requests = 'yes'; ?>
+
+	<table class="notification-settings" id="friends-notification-settings">
+		<thead>
+			<tr>
+				<th class="icon"></th>
+				<th class="title"><?php _ex( 'Friends', 'Friend settings on notification settings page', 'buddypress' ) ?></th>
+				<th class="yes"><?php _e( 'Yes', 'buddypress' ) ?></th>
+				<th class="no"><?php _e( 'No', 'buddypress' )?></th>
+			</tr>
+		</thead>
+
+		<tbody>
+			<tr id="friends-notification-settings-request">
+				<td></td>
+				<td><?php _ex( 'A member sends you a friendship request', 'Friend settings on notification settings page', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_friends_friendship_request]" id="notification-friends-friendship-request-yes" value="yes" <?php checked( $send_requests, 'yes', true ) ?>/><label for="notification-friends-friendship-request-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_friends_friendship_request]" id="notification-friends-friendship-request-no" value="no" <?php checked( $send_requests, 'no', true ) ?>/><label for="notification-friends-friendship-request-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+			<tr id="friends-notification-settings-accepted">
+				<td></td>
+				<td><?php _ex( 'A member accepts your friendship request', 'Friend settings on notification settings page', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_friends_friendship_accepted]" id="notification-friends-friendship-accepted-yes" value="yes" <?php checked( $accept_requests, 'yes', true ) ?>/><label for="notification-friends-friendship-accepted-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_friends_friendship_accepted]" id="notification-friends-friendship-accepted-no" value="no" <?php checked( $accept_requests, 'no', true ) ?>/><label for="notification-friends-friendship-accepted-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+
+			<?php
+
+			/**
+			 * Fires after the last table row on the friends notification screen.
+			 *
+			 * @since 1.0.0
+			 */
+			do_action( 'friends_screen_notification_settings' ); ?>
+
+		</tbody>
+	</table>
+
+<?php
+}
+add_action( 'bp_notification_settings', 'friends_screen_notification_settings' );
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-template.php	(revision 12265)
@@ -0,0 +1,732 @@
+<?php
+/**
+ * BuddyPress Friends Template Functions.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the friends component slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_friends_slug() {
+	echo bp_get_friends_slug();
+}
+	/**
+	 * Return the friends component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_friends_slug() {
+
+		/**
+		 * Filters the friends component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Friends component slug.
+		 */
+		return apply_filters( 'bp_get_friends_slug', buddypress()->friends->slug );
+	}
+
+/**
+ * Output the friends component root slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_friends_root_slug() {
+	echo bp_get_friends_root_slug();
+}
+	/**
+	 * Return the friends component root slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_friends_root_slug() {
+
+		/**
+		 * Filters the friends component root slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Friends component root slug.
+		 */
+		return apply_filters( 'bp_get_friends_root_slug', buddypress()->friends->root_slug );
+	}
+
+/**
+ * Output a block of random friends.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate
+ */
+function bp_friends_random_friends() {
+
+	if ( !$friend_ids = wp_cache_get( 'friends_friend_ids_' . bp_displayed_user_id(), 'bp' ) ) {
+		$friend_ids = BP_Friends_Friendship::get_random_friends( bp_displayed_user_id() );
+		wp_cache_set( 'friends_friend_ids_' . bp_displayed_user_id(), $friend_ids, 'bp' );
+	} ?>
+
+	<div class="info-group">
+		<h4><?php bp_word_or_name( __( "My Friends", 'buddypress' ), __( "%s's Friends", 'buddypress' ) ) ?>  (<?php echo BP_Friends_Friendship::total_friend_count( bp_displayed_user_id() ) ?>) <span><a href="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_friends_slug() ) ?>"><?php _e('See All', 'buddypress') ?></a></span></h4>
+
+		<?php if ( $friend_ids ) { ?>
+
+			<ul class="horiz-gallery">
+
+			<?php for ( $i = 0, $count = count( $friend_ids ); $i < $count; ++$i ) { ?>
+
+				<li>
+					<a href="<?php echo bp_core_get_user_domain( $friend_ids[$i] ) ?>"><?php echo bp_core_fetch_avatar( array( 'item_id' => $friend_ids[$i], 'type' => 'thumb' ) ) ?></a>
+					<h5><?php echo bp_core_get_userlink($friend_ids[$i]) ?></h5>
+				</li>
+
+			<?php } ?>
+
+			</ul>
+
+		<?php } else { ?>
+
+			<div id="message" class="info">
+				<p><?php bp_word_or_name( __( "You haven't added any friend connections yet.", 'buddypress' ), __( "%s hasn't created any friend connections yet.", 'buddypress' ) ) ?></p>
+			</div>
+
+		<?php } ?>
+
+		<div class="clear"></div>
+	</div>
+
+<?php
+}
+
+/**
+ * Pull up a group of random members, and display some profile data about them.
+ *
+ * This function is no longer used by BuddyPress core.
+ *
+ * @todo Deprecate
+ *
+ * @param int $total_members The number of members to retrieve.
+ */
+function bp_friends_random_members( $total_members = 5 ) {
+
+	if ( !$user_ids = wp_cache_get( 'friends_random_users', 'bp' ) ) {
+		$user_ids = BP_Core_User::get_users( 'random', $total_members );
+		wp_cache_set( 'friends_random_users', $user_ids, 'bp' );
+	}
+
+	?>
+
+	<?php if ( $user_ids['users'] ) { ?>
+
+		<ul class="item-list" id="random-members-list">
+
+		<?php for ( $i = 0, $count = count( $user_ids['users'] ); $i < $count; ++$i ) { ?>
+
+			<li>
+				<a href="<?php echo bp_core_get_user_domain( $user_ids['users'][$i]->id ) ?>"><?php echo bp_core_fetch_avatar( array( 'item_id' => $user_ids['users'][$i]->id, 'type' => 'thumb' ) ) ?></a>
+				<h5><?php echo bp_core_get_userlink( $user_ids['users'][$i]->id ) ?></h5>
+
+				<?php if ( bp_is_active( 'xprofile' ) ) { ?>
+
+					<?php $random_data = xprofile_get_random_profile_data( $user_ids['users'][$i]->id, true ); ?>
+
+					<div class="profile-data">
+						<p class="field-name"><?php echo $random_data[0]->name ?></p>
+
+						<?php echo $random_data[0]->value ?>
+
+					</div>
+
+				<?php } ?>
+
+				<div class="action">
+
+					<?php if ( bp_is_active( 'friends' ) ) { ?>
+
+						<?php bp_add_friend_button( $user_ids['users'][$i]->id ) ?>
+
+					<?php } ?>
+
+				</div>
+			</li>
+
+		<?php } ?>
+
+		</ul>
+
+	<?php } else { ?>
+
+		<div id="message" class="info">
+			<p><?php _e( "There aren't enough site members to show a random sample just yet.", 'buddypress' ) ?></p>
+		</div>
+
+	<?php } ?>
+<?php
+}
+
+/**
+ * Display a Friends search form.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate
+ */
+function bp_friend_search_form() {
+
+	$action = bp_displayed_user_domain() . bp_get_friends_slug() . '/my-friends/search/';
+	$label  = __( 'Filter Friends', 'buddypress' ); ?>
+
+		<form action="<?php echo $action ?>" id="friend-search-form" method="post">
+
+			<label for="friend-search-box" id="friend-search-label"><?php echo $label ?></label>
+			<input type="search" name="friend-search-box" id="friend-search-box" value="<?php echo $value ?>"<?php echo $disabled ?> />
+
+			<?php wp_nonce_field( 'friends_search', '_wpnonce_friend_search' ) ?>
+
+			<input type="hidden" name="initiator" id="initiator" value="<?php echo esc_attr( bp_displayed_user_id() ) ?>" />
+
+		</form>
+
+	<?php
+}
+
+/**
+ * Output the "Add Friend" button in the member loop.
+ *
+ * @since 1.2.6
+ */
+function bp_member_add_friend_button() {
+	bp_add_friend_button( bp_get_member_user_id() );
+}
+add_action( 'bp_directory_members_actions', 'bp_member_add_friend_button' );
+
+/**
+ * Output the friend count for the current member in the loop.
+ *
+ * @since 1.2.0
+ */
+function bp_member_total_friend_count() {
+	echo bp_get_member_total_friend_count();
+}
+	/**
+	 * Return the friend count for the current member in the loop.
+	 *
+	 * Return value is a string of the form "x friends".
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string A string of the form "x friends".
+	 */
+	function bp_get_member_total_friend_count() {
+		global $members_template;
+
+		if ( 1 == (int) $members_template->member->total_friend_count ) {
+
+			/**
+			 * Filters text used to denote total friend count.
+			 *
+			 * @since 1.2.0
+			 *
+			 * @param string $value String of the form "x friends".
+			 * @param int    $value Total friend count for current member in the loop.
+			 */
+			return apply_filters( 'bp_get_member_total_friend_count', sprintf( __( '%d friend', 'buddypress' ), (int) $members_template->member->total_friend_count ) );
+		} else {
+
+			/** This filter is documented in bp-friends/bp-friends-template.php */
+			return apply_filters( 'bp_get_member_total_friend_count', sprintf( __( '%d friends', 'buddypress' ), (int) $members_template->member->total_friend_count ) );
+		}
+	}
+
+/**
+ * Output the ID of the current user in the friend request loop.
+ *
+ * @since 1.2.6
+ *
+ * @see bp_get_potential_friend_id() for a description of arguments.
+ *
+ * @param int $user_id See {@link bp_get_potential_friend_id()}.
+ */
+function bp_potential_friend_id( $user_id = 0 ) {
+	echo bp_get_potential_friend_id( $user_id );
+}
+	/**
+	 * Return the ID of current user in the friend request loop.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @global object $friends_template
+	 *
+	 * @param int $user_id Optional. If provided, the function will simply
+	 *                     return this value.
+	 * @return int ID of potential friend.
+	 */
+	function bp_get_potential_friend_id( $user_id = 0 ) {
+		global $friends_template;
+
+		if ( empty( $user_id ) && isset( $friends_template->friendship->friend ) )
+			$user_id = $friends_template->friendship->friend->id;
+		elseif ( empty( $user_id ) && !isset( $friends_template->friendship->friend ) )
+			$user_id = bp_displayed_user_id();
+
+		/**
+		 * Filters the ID of current user in the friend request loop.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param int $user_id ID of current user in the friend request loop.
+		 */
+		return apply_filters( 'bp_get_potential_friend_id', (int) $user_id );
+	}
+
+/**
+ * Check whether a given user is a friend of the logged-in user.
+ *
+ * Returns - 'is_friend', 'not_friends', 'pending'.
+ *
+ * @since 1.2.6
+ *
+ * @param int $user_id ID of the potential friend. Default: the value of
+ *                     {@link bp_get_potential_friend_id()}.
+ * @return string 'is_friend', 'not_friends', or 'pending'.
+ */
+function bp_is_friend( $user_id = 0 ) {
+
+	if ( !is_user_logged_in() )
+		return false;
+
+	if ( empty( $user_id ) )
+		$user_id = bp_get_potential_friend_id( $user_id );
+
+	if ( bp_loggedin_user_id() == $user_id )
+		return false;
+
+	/**
+	 * Filters the status of friendship between logged in user and given user.
+	 *
+	 * @since 1.2.10
+	 *
+	 * @param string $value String status of friendship. Possible values are 'is_friend', 'not_friends', 'pending'.
+	 */
+	return apply_filters( 'bp_is_friend', friends_check_friendship_status( bp_loggedin_user_id(), $user_id ), $user_id );
+}
+
+/**
+ * Output the Add Friend button.
+ *
+ * @since 1.0.0
+ *
+ * @see bp_get_add_friend_button() for information on arguments.
+ *
+ * @param int      $potential_friend_id See {@link bp_get_add_friend_button()}.
+ * @param int|bool $friend_status       See {@link bp_get_add_friend_button()}.
+ */
+function bp_add_friend_button( $potential_friend_id = 0, $friend_status = false ) {
+	echo bp_get_add_friend_button( $potential_friend_id, $friend_status );
+}
+	/**
+	 * Create the Add Friend button.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param int  $potential_friend_id ID of the user to whom the button
+	 *                                  applies. Default: value of {@link bp_get_potential_friend_id()}.
+	 * @param bool $friend_status       Not currently used.
+	 * @return false|string HTML for the Add Friend button.
+	 */
+	function bp_get_add_friend_button( $potential_friend_id = 0, $friend_status = false ) {
+
+		if ( empty( $potential_friend_id ) )
+			$potential_friend_id = bp_get_potential_friend_id( $potential_friend_id );
+
+		$is_friend = bp_is_friend( $potential_friend_id );
+
+		if ( empty( $is_friend ) )
+			return false;
+
+		switch ( $is_friend ) {
+			case 'pending' :
+				$button = array(
+					'id'                => 'pending',
+					'component'         => 'friends',
+					'must_be_logged_in' => true,
+					'block_self'        => true,
+					'wrapper_class'     => 'friendship-button pending_friend',
+					'wrapper_id'        => 'friendship-button-' . $potential_friend_id,
+					'link_href'         => wp_nonce_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/requests/cancel/' . $potential_friend_id . '/', 'friends_withdraw_friendship' ),
+					'link_text'         => __( 'Cancel Friendship Request', 'buddypress' ),
+					'link_id'           => 'friend-' . $potential_friend_id,
+					'link_rel'          => 'remove',
+					'link_class'        => 'friendship-button pending_friend requested'
+				);
+				break;
+
+			case 'awaiting_response' :
+				$button = array(
+					'id'                => 'awaiting_response',
+					'component'         => 'friends',
+					'must_be_logged_in' => true,
+					'block_self'        => true,
+					'wrapper_class'     => 'friendship-button awaiting_response_friend',
+					'wrapper_id'        => 'friendship-button-' . $potential_friend_id,
+					'link_href'         => bp_loggedin_user_domain() . bp_get_friends_slug() . '/requests/',
+					'link_text'         => __( 'Friendship Requested', 'buddypress' ),
+					'link_id'           => 'friend-' . $potential_friend_id,
+					'link_rel'          => 'remove',
+					'link_class'        => 'friendship-button awaiting_response_friend requested'
+				);
+				break;
+
+			case 'is_friend' :
+				$button = array(
+					'id'                => 'is_friend',
+					'component'         => 'friends',
+					'must_be_logged_in' => true,
+					'block_self'        => false,
+					'wrapper_class'     => 'friendship-button is_friend',
+					'wrapper_id'        => 'friendship-button-' . $potential_friend_id,
+					'link_href'         => wp_nonce_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/remove-friend/' . $potential_friend_id . '/', 'friends_remove_friend' ),
+					'link_text'         => __( 'Cancel Friendship', 'buddypress' ),
+					'link_id'           => 'friend-' . $potential_friend_id,
+					'link_rel'          => 'remove',
+					'link_class'        => 'friendship-button is_friend remove'
+				);
+				break;
+
+			default:
+				$button = array(
+					'id'                => 'not_friends',
+					'component'         => 'friends',
+					'must_be_logged_in' => true,
+					'block_self'        => true,
+					'wrapper_class'     => 'friendship-button not_friends',
+					'wrapper_id'        => 'friendship-button-' . $potential_friend_id,
+					'link_href'         => wp_nonce_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/add-friend/' . $potential_friend_id . '/', 'friends_add_friend' ),
+					'link_text'         => __( 'Add Friend', 'buddypress' ),
+					'link_id'           => 'friend-' . $potential_friend_id,
+					'link_rel'          => 'add',
+					'link_class'        => 'friendship-button not_friends add'
+				);
+				break;
+		}
+
+		/**
+		 * Filters the HTML for the add friend button.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $button HTML markup for add friend button.
+		 */
+		return bp_get_button( apply_filters( 'bp_get_add_friend_button', $button ) );
+	}
+
+/**
+ * Get a comma-separated list of IDs of a user's friends.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id Optional. Default: the displayed user's ID, or the
+ *                     logged-in user's ID.
+ * @return false|string A comma-separated list of friend IDs if any are found,
+ *                      otherwise false.
+ */
+function bp_get_friend_ids( $user_id = 0 ) {
+
+	if ( empty( $user_id ) )
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+
+	$friend_ids = friends_get_friend_user_ids( $user_id );
+
+	if ( empty( $friend_ids ) )
+		return false;
+
+	return implode( ',', friends_get_friend_user_ids( $user_id ) );
+}
+
+/**
+ * Get a user's friendship requests.
+ *
+ * Note that we return a 0 if no pending requests are found. This is necessary
+ * because of the structure of the $include parameter in bp_has_members().
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id ID of the user whose requests are being retrieved.
+ *                     Defaults to displayed user.
+ * @return array|int An array of user IDs if found, or a 0 if none are found.
+ */
+function bp_get_friendship_requests( $user_id = 0 ) {
+	if ( !$user_id ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	if ( !$user_id ) {
+		return 0;
+	}
+
+	$requests = friends_get_friendship_request_user_ids( $user_id );
+
+	if ( !empty( $requests ) ) {
+		$requests = implode( ',', (array) $requests );
+	} else {
+		$requests = 0;
+	}
+
+	/**
+	 * Filters the total pending friendship requests for a user.
+	 *
+	 * @since 1.2.0
+	 * @since 2.6.0 Added the `$user_id` parameter.
+	 *
+	 * @param array|int $requests An array of user IDs if found, or a 0 if none are found.
+	 * @param int       $user_id  ID of the queried user.
+	 */
+	return apply_filters( 'bp_get_friendship_requests', $requests, $user_id );
+}
+
+/**
+ * Output the ID of the friendship between the logged-in user and the current user in the loop.
+ *
+ * @since 1.2.0
+ */
+function bp_friend_friendship_id() {
+	echo bp_get_friend_friendship_id();
+}
+	/**
+	 * Return the ID of the friendship between the logged-in user and the current user in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return int ID of the friendship.
+	 */
+	function bp_get_friend_friendship_id() {
+		global $members_template;
+
+		if ( !$friendship_id = wp_cache_get( 'friendship_id_' . $members_template->member->id . '_' . bp_loggedin_user_id() ) ) {
+			$friendship_id = friends_get_friendship_id( $members_template->member->id, bp_loggedin_user_id() );
+			wp_cache_set( 'friendship_id_' . $members_template->member->id . '_' . bp_loggedin_user_id(), $friendship_id, 'bp' );
+		}
+
+		/**
+		 * Filters the ID of the friendship between the logged in user and the current user in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $friendship_id ID of the friendship.
+		 */
+		return apply_filters( 'bp_get_friend_friendship_id', $friendship_id );
+	}
+
+/**
+ * Output the URL for accepting the current friendship request in the loop.
+ *
+ * @since 1.0.0
+ */
+function bp_friend_accept_request_link() {
+	echo bp_get_friend_accept_request_link();
+}
+	/**
+	 * Return the URL for accepting the current friendship request in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string accept-friendship URL.
+	 */
+	function bp_get_friend_accept_request_link() {
+		global $members_template;
+
+		if ( !$friendship_id = wp_cache_get( 'friendship_id_' . $members_template->member->id . '_' . bp_loggedin_user_id() ) ) {
+			$friendship_id = friends_get_friendship_id( $members_template->member->id, bp_loggedin_user_id() );
+			wp_cache_set( 'friendship_id_' . $members_template->member->id . '_' . bp_loggedin_user_id(), $friendship_id, 'bp' );
+		}
+
+		/**
+		 * Filters the URL for accepting the current friendship request in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.6.0 Added the `$friendship_id` parameter.
+		 *
+		 * @param string $value         Accept-friendship URL.
+		 * @param int    $friendship_id ID of the friendship.
+		 */
+		return apply_filters( 'bp_get_friend_accept_request_link', wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_friends_slug() . '/requests/accept/' . $friendship_id ), 'friends_accept_friendship' ), $friendship_id );
+	}
+
+/**
+ * Output the URL for rejecting the current friendship request in the loop.
+ *
+ * @since 1.0.0
+ */
+function bp_friend_reject_request_link() {
+	echo bp_get_friend_reject_request_link();
+}
+	/**
+	 * Return the URL for rejecting the current friendship request in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string reject-friendship URL.
+	 */
+	function bp_get_friend_reject_request_link() {
+		global $members_template;
+
+		if ( !$friendship_id = wp_cache_get( 'friendship_id_' . $members_template->member->id . '_' . bp_loggedin_user_id() ) ) {
+			$friendship_id = friends_get_friendship_id( $members_template->member->id, bp_loggedin_user_id() );
+			wp_cache_set( 'friendship_id_' . $members_template->member->id . '_' . bp_loggedin_user_id(), $friendship_id, 'bp' );
+		}
+
+		/**
+		 * Filters the URL for rejecting the current friendship request in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.6.0 Added the `$friendship_id` parameter.
+		 *
+		 * @param string $value         Reject-friendship URL.
+		 * @param int    $friendship_id ID of the friendship.
+		 */
+		return apply_filters( 'bp_get_friend_reject_request_link', wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_friends_slug() . '/requests/reject/' . $friendship_id ), 'friends_reject_friendship' ), $friendship_id );
+	}
+
+/**
+ * Output the total friend count for a given user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id See {@link friends_get_total_friend_count()}.
+ */
+function bp_total_friend_count( $user_id = 0 ) {
+	echo bp_get_total_friend_count( $user_id );
+}
+	/**
+	 * Return the total friend count for a given user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id See {@link friends_get_total_friend_count()}.
+	 * @return int Total friend count.
+	 */
+	function bp_get_total_friend_count( $user_id = 0 ) {
+
+		/**
+		 * Filters the total friend count for a given user.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param int $value   Total friend count.
+		 * @param int $user_id ID of the queried user.
+		 */
+		return apply_filters( 'bp_get_total_friend_count', friends_get_total_friend_count( $user_id ), $user_id );
+	}
+	add_filter( 'bp_get_total_friend_count', 'bp_core_number_format' );
+
+/**
+ * Output the total friendship request count for a given user.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_friend_get_total_requests_count() for description of arguments.
+ *
+ * @param int $user_id See {@link bp_friend_get_total_requests_count().
+ */
+function bp_friend_total_requests_count( $user_id = 0 ) {
+	echo bp_friend_get_total_requests_count( $user_id );
+}
+	/**
+	 * Return the total friendship request count for a given user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id ID of the user whose requests are being counted.
+	 *                     Default: ID of the logged-in user.
+	 * @return int Friend count.
+	 */
+	function bp_friend_get_total_requests_count( $user_id = 0 ) {
+		if ( empty( $user_id ) )
+			$user_id = bp_loggedin_user_id();
+
+		/**
+		 * Filters the total friendship request count for a given user.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param int $value   Friendship request count.
+		 * @param int $user_id ID of the queried user.
+		 */
+		return apply_filters( 'bp_friend_get_total_requests_count', count( BP_Friends_Friendship::get_friend_user_ids( $user_id, true ) ), $user_id );
+	}
+
+/** Stats **********************************************************************/
+
+/**
+ * Display the number of friends in user's profile.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args before|after|user_id.
+ */
+function bp_friends_profile_stats( $args = '' ) {
+	echo bp_friends_get_profile_stats( $args );
+}
+add_action( 'bp_members_admin_user_stats', 'bp_friends_profile_stats', 7, 1 );
+
+/**
+ * Return the number of friends in user's profile.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args before|after|user_id.
+ * @return string HTML for stats output.
+ */
+function bp_friends_get_profile_stats( $args = '' ) {
+
+	// Parse the args.
+	$r = bp_parse_args( $args, array(
+		'before'  => '<li class="bp-friends-profile-stats">',
+		'after'   => '</li>',
+		'user_id' => bp_displayed_user_id(),
+		'friends' => 0,
+		'output'  => ''
+	), 'friends_get_profile_stats' );
+
+	// Allow completely overloaded output.
+	if ( empty( $r['output'] ) ) {
+
+		// Only proceed if a user ID was passed.
+		if ( ! empty( $r['user_id'] ) ) {
+
+			// Get the user's friends.
+			if ( empty( $r['friends'] ) ) {
+				$r['friends'] = absint( friends_get_total_friend_count( $r['user_id'] ) );
+			}
+
+			// If friends exist, show some formatted output.
+			$r['output'] = $r['before'] . sprintf( _n( '%s friend', '%s friends', $r['friends'], 'buddypress' ), '<strong>' . $r['friends'] . '</strong>' ) . $r['after'];
+		}
+	}
+
+	/**
+	 * Filters the number of friends in user's profile.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $value Formatted string displaying total friends count.
+	 * @param array  $r     Array of arguments for string formatting and output.
+	 */
+	return apply_filters( 'bp_friends_get_profile_stats', $r['output'], $r );
+}
Index: /tags/4.0.0-beta1/src/bp-friends/bp-friends-widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/bp-friends-widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/bp-friends-widgets.php	(revision 12265)
@@ -0,0 +1,94 @@
+<?php
+/**
+ * BuddyPress Friends Widgets.
+ *
+ * @package BuddyPress
+ * @subpackage Friends
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register the friends widget.
+ *
+ * @since 1.9.0
+ */
+function bp_friends_register_widgets() {
+	if ( ! bp_is_active( 'friends' ) ) {
+		return;
+	}
+
+	// The Friends widget works only when looking an a displayed user,
+	// and the concept of "displayed user" doesn't exist on non-root blogs,
+	// so we don't register the widget there.
+	if ( ! bp_is_root_blog() ) {
+		return;
+	}
+
+	add_action( 'widgets_init', function() { register_widget( 'BP_Core_Friends_Widget' ); } );
+}
+add_action( 'bp_register_widgets', 'bp_friends_register_widgets' );
+
+/** Widget AJAX ***************************************************************/
+
+/**
+ * Process AJAX pagination or filtering for the Friends widget.
+ *
+ * @since 1.9.0
+ */
+function bp_core_ajax_widget_friends() {
+
+	check_ajax_referer( 'bp_core_widget_friends' );
+
+	switch ( $_POST['filter'] ) {
+		case 'newest-friends':
+			$type = 'newest';
+			break;
+
+		case 'recently-active-friends':
+			$type = 'active';
+			break;
+
+		case 'popular-friends':
+			$type = 'popular';
+			break;
+	}
+
+	$members_args = array(
+		'user_id'         => bp_displayed_user_id(),
+		'type'            => $type,
+		'max'             => absint( $_POST['max-friends'] ),
+		'populate_extras' => 1,
+	);
+
+	if ( bp_has_members( $members_args ) ) : ?>
+		<?php echo '0[[SPLIT]]'; // Return valid result. TODO: remove this. ?>
+		<?php while ( bp_members() ) : bp_the_member(); ?>
+			<li class="vcard">
+				<div class="item-avatar">
+					<a href="<?php bp_member_permalink(); ?>"><?php bp_member_avatar(); ?></a>
+				</div>
+
+				<div class="item">
+					<div class="item-title fn"><a href="<?php bp_member_permalink(); ?>"><?php bp_member_name(); ?></a></div>
+					<?php if ( 'active' == $type ) : ?>
+						<div class="item-meta"><span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_last_active( array( 'relative' => false ) ) ); ?>"><?php bp_member_last_active(); ?></span></div>
+					<?php elseif ( 'newest' == $type ) : ?>
+						<div class="item-meta"><span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_registered( array( 'relative' => false ) ) ); ?>"><?php bp_member_registered(); ?></span></div>
+					<?php elseif ( bp_is_active( 'friends' ) ) : ?>
+						<div class="item-meta"><span class="activity"><?php bp_member_total_friend_count(); ?></span></div>
+					<?php endif; ?>
+				</div>
+			</li>
+		<?php endwhile; ?>
+
+	<?php else: ?>
+		<?php echo "-1[[SPLIT]]<li>"; ?>
+		<?php _e( 'There were no members found, please try another filter.', 'buddypress' ); ?>
+		<?php echo "</li>"; ?>
+	<?php endif;
+}
+add_action( 'wp_ajax_widget_friends', 'bp_core_ajax_widget_friends' );
+add_action( 'wp_ajax_nopriv_widget_friends', 'bp_core_ajax_widget_friends' );
Index: /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-core-friends-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-core-friends-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-core-friends-widget.php	(revision 12265)
@@ -0,0 +1,205 @@
+<?php
+/**
+ * BuddyPress Friends Widget.
+ *
+ * @package BuddyPress
+ * @subpackage Friends
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The User Friends widget class.
+ *
+ * @since 1.9.0
+ */
+class BP_Core_Friends_Widget extends WP_Widget {
+
+	/**
+	 * Class constructor.
+	 *
+	 * @since 1.9.0
+	 */
+	function __construct() {
+		$widget_ops = array(
+			'description'                 => __( 'A dynamic list of recently active, popular, and newest Friends of the displayed member.  Widget is only shown when viewing a member profile.', 'buddypress' ),
+			'classname'                   => 'widget_bp_core_friends_widget buddypress widget',
+			'customize_selective_refresh' => true,
+		);
+		parent::__construct( false, $name = _x( '(BuddyPress) Friends', 'widget name', 'buddypress' ), $widget_ops );
+
+		if ( is_customize_preview() || is_active_widget( false, false, $this->id_base ) ) {
+			add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+		}
+	}
+
+	/**
+	 * Enqueue scripts.
+	 *
+	 * @since 2.6.0
+	 */
+	public function enqueue_scripts() {
+		$min = bp_core_get_minified_asset_suffix();
+		wp_enqueue_script( 'bp_core_widget_friends-js', buddypress()->plugin_url . "bp-friends/js/widget-friends{$min}.js", array( 'jquery' ), bp_get_version() );
+	}
+
+	/**
+	 * Display the widget.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args Widget arguments.
+	 * @param array $instance The widget settings, as saved by the user.
+	 */
+	function widget( $args, $instance ) {
+		global $members_template;
+
+		extract( $args );
+
+		if ( ! bp_displayed_user_id() ) {
+			return;
+		}
+
+		$user_id = bp_displayed_user_id();
+		$link = trailingslashit( bp_displayed_user_domain() . bp_get_friends_slug() );
+		$instance['title'] = sprintf( __( "%s's Friends", 'buddypress' ), bp_get_displayed_user_fullname() );
+
+		if ( empty( $instance['friend_default'] ) ) {
+			$instance['friend_default'] = 'active';
+		}
+
+		/**
+		 * Filters the Friends widget title.
+		 *
+		 * @since 1.8.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
+
+		echo $before_widget;
+
+		$title = $instance['link_title'] ? '<a href="' . esc_url( $link ) . '">' . esc_html( $title ) . '</a>' : esc_html( $title );
+
+		echo $before_title . $title . $after_title;
+
+		$members_args = array(
+			'user_id'         => absint( $user_id ),
+			'type'            => sanitize_text_field( $instance['friend_default'] ),
+			'max'             => absint( $instance['max_friends'] ),
+			'populate_extras' => 1,
+		);
+
+		// Back up the global.
+		$old_members_template = $members_template;
+
+		?>
+
+		<?php if ( bp_has_members( $members_args ) ) : ?>
+			<div class="item-options" id="friends-list-options">
+				<a href="<?php bp_members_directory_permalink(); ?>" id="newest-friends" <?php if ( $instance['friend_default'] == 'newest' ) : ?>class="selected"<?php endif; ?>><?php _e( 'Newest', 'buddypress' ); ?></a>
+				| <a href="<?php bp_members_directory_permalink(); ?>" id="recently-active-friends" <?php if ( $instance['friend_default'] == 'active' ) : ?>class="selected"<?php endif; ?>><?php _e( 'Active', 'buddypress' ); ?></a>
+				| <a href="<?php bp_members_directory_permalink(); ?>" id="popular-friends" <?php if ( $instance['friend_default'] == 'popular' ) : ?>class="selected"<?php endif; ?>><?php _e( 'Popular', 'buddypress' ); ?></a>
+			</div>
+
+			<ul id="friends-list" class="item-list">
+				<?php while ( bp_members() ) : bp_the_member(); ?>
+					<li class="vcard">
+						<div class="item-avatar">
+							<a href="<?php bp_member_permalink(); ?>" class="bp-tooltip" data-bp-tooltip="<?php bp_member_name(); ?>"><?php bp_member_avatar(); ?></a>
+						</div>
+
+						<div class="item">
+							<div class="item-title fn"><a href="<?php bp_member_permalink(); ?>"><?php bp_member_name(); ?></a></div>
+							<div class="item-meta">
+								<?php if ( 'newest' == $instance['friend_default'] ) : ?>
+									<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_registered( array( 'relative' => false ) ) ); ?>"><?php bp_member_registered(); ?></span>
+								<?php elseif ( 'active' == $instance['friend_default'] ) : ?>
+									<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_last_active( array( 'relative' => false ) ) ); ?>"><?php bp_member_last_active(); ?></span>
+								<?php else : ?>
+									<span class="activity"><?php bp_member_total_friend_count(); ?></span>
+								<?php endif; ?>
+							</div>
+						</div>
+					</li>
+
+				<?php endwhile; ?>
+			</ul>
+			<?php wp_nonce_field( 'bp_core_widget_friends', '_wpnonce-friends' ); ?>
+			<input type="hidden" name="friends_widget_max" id="friends_widget_max" value="<?php echo absint( $instance['max_friends'] ); ?>" />
+
+		<?php else: ?>
+
+			<div class="widget-error">
+				<?php _e( 'Sorry, no members were found.', 'buddypress' ); ?>
+			</div>
+
+		<?php endif; ?>
+
+		<?php echo $after_widget;
+
+		// Restore the global.
+		$members_template = $old_members_template;
+	}
+
+	/**
+	 * Process a widget save.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $new_instance The parameters saved by the user.
+	 * @param array $old_instance The parameters as previously saved to the database.
+	 * @return array $instance The processed settings to save.
+	 */
+	function update( $new_instance, $old_instance ) {
+		$instance = $old_instance;
+
+		$instance['max_friends']    = absint( $new_instance['max_friends'] );
+		$instance['friend_default'] = sanitize_text_field( $new_instance['friend_default'] );
+		$instance['link_title']	    = (bool) $new_instance['link_title'];
+
+		return $instance;
+	}
+
+	/**
+	 * Render the widget edit form.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $instance The saved widget settings.
+	 * @return void
+	 */
+	function form( $instance ) {
+		$defaults = array(
+			'max_friends' 	 => 5,
+			'friend_default' => 'active',
+			'link_title' 	 => false
+		);
+		$instance = wp_parse_args( (array) $instance, $defaults );
+
+		$max_friends 	= $instance['max_friends'];
+		$friend_default = $instance['friend_default'];
+		$link_title	= (bool) $instance['link_title'];
+		?>
+
+		<p><label for="<?php echo $this->get_field_id( 'link_title' ); ?>"><input type="checkbox" name="<?php echo $this->get_field_name('link_title'); ?>" id="<?php echo $this->get_field_id( 'link_title' ); ?>" value="1" <?php checked( $link_title ); ?> /> <?php _e( 'Link widget title to Members directory', 'buddypress' ); ?></label></p>
+
+		<p><label for="<?php echo $this->get_field_id( 'max_friends' ); ?>"><?php _e( 'Max friends to show:', 'buddypress' ); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'max_friends' ); ?>" name="<?php echo $this->get_field_name( 'max_friends' ); ?>" type="text" value="<?php echo absint( $max_friends ); ?>" style="width: 30%" /></label></p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'friend_default' ) ?>"><?php _e( 'Default friends to show:', 'buddypress' ); ?></label>
+			<select name="<?php echo $this->get_field_name( 'friend_default' ); ?>" id="<?php echo $this->get_field_id( 'friend_default' ); ?>">
+				<option value="newest" <?php selected( $friend_default, 'newest' ); ?>><?php _e( 'Newest', 'buddypress' ); ?></option>
+				<option value="active" <?php selected( $friend_default, 'active' );?>><?php _e( 'Active', 'buddypress' ); ?></option>
+				<option value="popular"  <?php selected( $friend_default, 'popular' ); ?>><?php _e( 'Popular', 'buddypress' ); ?></option>
+			</select>
+		</p>
+
+	<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-friends-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-friends-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-friends-component.php	(revision 12265)
@@ -0,0 +1,324 @@
+<?php
+/**
+ * BuddyPress Friends Streams Loader.
+ *
+ * The friends component is for users to create relationships with each other.
+ *
+ * @package BuddyPress
+ * @subpackage Friends
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Defines the BuddyPress Friends Component.
+ *
+ * @since 1.5.0
+ */
+class BP_Friends_Component extends BP_Component {
+
+	/**
+	 * Start the friends component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'friends',
+			_x( 'Friend Connections', 'Friends screen page <title>', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 60
+			)
+		);
+	}
+
+	/**
+	 * Include bp-friends files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::includes() for description of parameters.
+	 *
+	 * @param array $includes See {@link BP_Component::includes()}.
+	 */
+	public function includes( $includes = array() ) {
+		$includes = array(
+			'cache',
+			'filters',
+			'template',
+			'functions',
+			'widgets',
+		);
+
+		// Conditional includes.
+		if ( bp_is_active( 'activity' ) ) {
+			$includes[] = 'activity';
+		}
+		if ( bp_is_active( 'notifications' ) ) {
+			$includes[] = 'notifications';
+		}
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		// Friends.
+		if ( bp_is_user_friends() ) {
+			// Authenticated actions.
+			if ( is_user_logged_in() &&
+				in_array( bp_current_action(), array( 'add-friend', 'remove-friend' ), true )
+			) {
+				require $this->path . 'bp-friends/actions/' . bp_current_action() . '.php';
+			}
+
+			// User nav.
+			require $this->path . 'bp-friends/screens/my-friends.php';
+			if ( is_user_logged_in() && bp_is_user_friend_requests() ) {
+				require $this->path . 'bp-friends/screens/requests.php';
+			}
+		}
+	}
+
+	/**
+	 * Set up bp-friends global settings.
+	 *
+	 * The BP_FRIENDS_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_globals() for description of parameters.
+	 *
+	 * @param array $args See {@link BP_Component::setup_globals()}.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		// Deprecated. Do not use.
+		// Defined conditionally to support unit tests.
+		if ( ! defined( 'BP_FRIENDS_DB_VERSION' ) ) {
+			define( 'BP_FRIENDS_DB_VERSION', '1800' );
+		}
+
+		// Define a slug, if necessary.
+		if ( ! defined( 'BP_FRIENDS_SLUG' ) ) {
+			define( 'BP_FRIENDS_SLUG', $this->id );
+		}
+
+		// Global tables for the friends component.
+		$global_tables = array(
+			'table_name'      => $bp->table_prefix . 'bp_friends',
+			'table_name_meta' => $bp->table_prefix . 'bp_friends_meta',
+		);
+
+		// All globals for the friends component.
+		// Note that global_tables is included in this array.
+		$args = array(
+			'slug'                  => BP_FRIENDS_SLUG,
+			'has_directory'         => false,
+			'search_string'         => __( 'Search Friends...', 'buddypress' ),
+			'notification_callback' => 'friends_format_notifications',
+			'global_tables'         => $global_tables
+		);
+
+		parent::setup_globals( $args );
+	}
+
+	/**
+	 * Set up component navigation.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of arguments.
+	 *
+	 * @param array $main_nav Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 * @param array $sub_nav  Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$access       = bp_core_can_edit_settings();
+		$slug         = bp_get_friends_slug();
+		$friends_link = trailingslashit( $user_domain . $slug );
+
+		// Add 'Friends' to the main navigation.
+		$count = friends_get_total_friend_count();
+		$class = ( 0 === $count ) ? 'no-count' : 'count';
+
+		$main_nav_name = sprintf(
+			/* translators: %s: Friend count for the current user */
+			__( 'Friends %s', 'buddypress' ),
+			sprintf(
+				'<span class="%s">%s</span>',
+				esc_attr( $class ),
+				bp_core_number_format( $count )
+			)
+		);
+
+		$main_nav = array(
+			'name'                => $main_nav_name,
+			'slug'                => $slug,
+			'position'            => 60,
+			'screen_function'     => 'friends_screen_my_friends',
+			'default_subnav_slug' => 'my-friends',
+			'item_css_id'         => $this->id
+		);
+
+		// Add the subnav items to the friends nav item.
+		$sub_nav[] = array(
+			'name'            => _x( 'Friendships', 'Friends screen sub nav', 'buddypress' ),
+			'slug'            => 'my-friends',
+			'parent_url'      => $friends_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'friends_screen_my_friends',
+			'position'        => 10,
+			'item_css_id'     => 'friends-my-friends'
+		);
+
+		$sub_nav[] = array(
+			'name'            => _x( 'Requests', 'Friends screen sub nav', 'buddypress' ),
+			'slug'            => 'requests',
+			'parent_url'      => $friends_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'friends_screen_requests',
+			'position'        => 20,
+			'user_has_access' => $access
+		);
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up bp-friends integration with the WordPress admin bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_admin_bar() for a description of arguments.
+	 *
+	 * @param array $wp_admin_nav See BP_Component::setup_admin_bar()
+	 *                            for description.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$friends_link = trailingslashit( bp_loggedin_user_domain() . bp_get_friends_slug() );
+
+			// Pending friend requests.
+			$count = count( friends_get_friendship_request_user_ids( bp_loggedin_user_id() ) );
+			if ( !empty( $count ) ) {
+				$title = sprintf(
+					/* translators: %s: Pending friend request count for the current user */
+					_x( 'Friends %s', 'My Account Friends menu', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+				$pending = sprintf(
+					/* translators: %s: Pending friend request count for the current user */
+					_x( 'Pending Requests %s', 'My Account Friends menu sub nav', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+			} else {
+				$title   = _x( 'Friends',            'My Account Friends menu',         'buddypress' );
+				$pending = _x( 'No Pending Requests','My Account Friends menu sub nav', 'buddypress' );
+			}
+
+			// Add the "My Account" sub menus.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => $title,
+				'href'   => $friends_link
+			);
+
+			// My Friends.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-friendships',
+				'title'    => _x( 'Friendships', 'My Account Friends menu sub nav', 'buddypress' ),
+				'href'     => $friends_link,
+				'position' => 10
+			);
+
+			// Requests.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-requests',
+				'title'    => $pending,
+				'href'     => trailingslashit( $friends_link . 'requests' ),
+				'position' => 20
+			);
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 *
+	 * @since 1.5.0
+	 */
+	public function setup_title() {
+
+		// Adjust title.
+		if ( bp_is_friends_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() ) {
+				$bp->bp_options_title = __( 'Friendships', 'buddypress' );
+			} else {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title = bp_get_displayed_user_fullname();
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups.
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_friends_requests',
+			'bp_friends_friendships', // Individual friendship objects are cached here by ID.
+			'bp_friends_friendships_for_user' // All friendship IDs for a single user.
+		) );
+
+		parent::setup_cache_groups();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-friends-friendship.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-friends-friendship.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/classes/class-bp-friends-friendship.php	(revision 12265)
@@ -0,0 +1,1026 @@
+<?php
+/**
+ * BuddyPress Friends Classes.
+ *
+ * @package BuddyPress
+ * @subpackage FriendsClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Friendship object.
+ *
+ * @since 1.0.0
+ */
+class BP_Friends_Friendship {
+
+	/**
+	 * ID of the friendship.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * User ID of the friendship initiator.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $initiator_user_id;
+
+	/**
+	 * User ID of the 'friend' - the one invited to the friendship.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $friend_user_id;
+
+	/**
+	 * Has the friendship been confirmed/accepted?
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $is_confirmed;
+
+	/**
+	 * Is this a "limited" friendship?
+	 *
+	 * Not currently used by BuddyPress.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $is_limited;
+
+	/**
+	 * Date the friendship was created.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	public $date_created;
+
+	/**
+	 * Is this a request?
+	 *
+	 * Not currently used in BuddyPress.
+	 *
+	 * @since 1.0.0
+	 * @var bool
+	 */
+	public $is_request;
+
+	/**
+	 * Should additional friend details be queried?
+	 *
+	 * @since 1.0.0
+	 * @var bool
+	 */
+	public $populate_friend_details;
+
+	/**
+	 * Details about the friend.
+	 *
+	 * @since 1.0.0
+	 * @var BP_Core_User
+	 */
+	public $friend;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|null $id                      Optional. The ID of an existing friendship.
+	 * @param bool     $is_request              Deprecated.
+	 * @param bool     $populate_friend_details True if friend details should be queried.
+	 */
+	public function __construct( $id = null, $is_request = false, $populate_friend_details = true ) {
+		$this->is_request = $is_request;
+
+		if ( !empty( $id ) ) {
+			$this->id                      = (int) $id;
+			$this->populate_friend_details = $populate_friend_details;
+			$this->populate( $this->id );
+		}
+	}
+
+	/**
+	 * Set up data about the current friendship.
+	 *
+	 * @since 1.0.0
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// Check cache for friendship data.
+		$friendship = wp_cache_get( $this->id, 'bp_friends_friendships' );
+
+		// Cache missed, so query the DB.
+		if ( false === $friendship ) {
+			$friendship = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->friends->table_name} WHERE id = %d", $this->id ) );
+
+			wp_cache_set( $this->id, $friendship, 'bp_friends_friendships' );
+		}
+
+		// No friendship found so set the ID and bail.
+		if ( empty( $friendship ) || is_wp_error( $friendship ) ) {
+			$this->id = 0;
+			return;
+		}
+
+		$this->initiator_user_id = (int) $friendship->initiator_user_id;
+		$this->friend_user_id    = (int) $friendship->friend_user_id;
+		$this->is_confirmed      = (int) $friendship->is_confirmed;
+		$this->is_limited        = (int) $friendship->is_limited;
+		$this->date_created      = $friendship->date_created;
+
+		if ( ! empty( $this->populate_friend_details ) ) {
+			if ( $this->friend_user_id == bp_displayed_user_id() ) {
+				$this->friend = new BP_Core_User( $this->initiator_user_id );
+			} else {
+				$this->friend = new BP_Core_User( $this->friend_user_id );
+			}
+		}
+	}
+
+	/**
+	 * Save the current friendship to the database.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->initiator_user_id = apply_filters( 'friends_friendship_initiator_user_id_before_save', $this->initiator_user_id, $this->id );
+		$this->friend_user_id    = apply_filters( 'friends_friendship_friend_user_id_before_save',    $this->friend_user_id,    $this->id );
+		$this->is_confirmed      = apply_filters( 'friends_friendship_is_confirmed_before_save',      $this->is_confirmed,      $this->id );
+		$this->is_limited        = apply_filters( 'friends_friendship_is_limited_before_save',        $this->is_limited,        $this->id );
+		$this->date_created      = apply_filters( 'friends_friendship_date_created_before_save',      $this->date_created,      $this->id );
+
+		/**
+		 * Fires before processing and saving the current friendship request.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Friends_Friendship $value Current friendship request object.
+		 */
+		do_action_ref_array( 'friends_friendship_before_save', array( &$this ) );
+
+		// Update.
+		if (!empty( $this->id ) ) {
+			$result = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->friends->table_name} SET initiator_user_id = %d, friend_user_id = %d, is_confirmed = %d, is_limited = %d, date_created = %s WHERE id = %d", $this->initiator_user_id, $this->friend_user_id, $this->is_confirmed, $this->is_limited, $this->date_created, $this->id ) );
+
+		// Save.
+		} else {
+			$result = $wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->friends->table_name} ( initiator_user_id, friend_user_id, is_confirmed, is_limited, date_created ) VALUES ( %d, %d, %d, %d, %s )", $this->initiator_user_id, $this->friend_user_id, $this->is_confirmed, $this->is_limited, $this->date_created ) );
+			$this->id = $wpdb->insert_id;
+		}
+
+		/**
+		 * Fires after processing and saving the current friendship request.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Friends_Friendship $value Current friendship request object.
+		 */
+		do_action( 'friends_friendship_after_save', array( &$this ) );
+
+		return $result;
+	}
+
+	/**
+	 * Delete the current friendship from the database.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool|int
+	 */
+	public function delete() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE id = %d", $this->id ) );
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Get the friendships for a given user.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int   $user_id              ID of the user whose friends are being retrieved.
+	 * @param array $args {
+	 *        Optional. Filter parameters.
+	 *        @type int    $id                ID of specific friendship to retrieve.
+	 *        @type int    $initiator_user_id ID of friendship initiator.
+	 *        @type int    $friend_user_id    ID of specific friendship to retrieve.
+	 *        @type int    $is_confirmed      Whether the friendship has been accepted.
+	 *        @type int    $is_limited        Whether the friendship is limited.
+	 *        @type string $order_by          Column name to order by.
+	 *        @type string $sort_order        ASC or DESC. Default DESC.
+	 * }
+	 * @param string $operator            Optional. Operator to use in `wp_list_filter()`.
+	 *
+	 * @return array $friendships Array of friendship objects.
+	 */
+	public static function get_friendships( $user_id, $args = array(), $operator = 'AND' ) {
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$r = bp_parse_args( $args, array(
+			'id'                => null,
+			'initiator_user_id' => null,
+			'friend_user_id'    => null,
+			'is_confirmed'      => null,
+			'is_limited'        => null,
+			'order_by'          => 'date_created',
+			'sort_order'        => 'DESC',
+			'page'              => null,
+			'per_page'          => null
+		), 'bp_get_user_friendships' );
+
+		// First, we get all friendships that involve the user.
+		$friendship_ids = wp_cache_get( $user_id, 'bp_friends_friendships_for_user' );
+		if ( false === $friendship_ids ) {
+			$friendship_ids = self::get_friendship_ids_for_user( $user_id );
+			wp_cache_set( $user_id, $friendship_ids, 'bp_friends_friendships_for_user' );
+		}
+
+		// Prime the membership cache.
+		$uncached_friendship_ids = bp_get_non_cached_ids( $friendship_ids, 'bp_friends_friendships' );
+		if ( ! empty( $uncached_friendship_ids ) ) {
+			$uncached_friendships = self::get_friendships_by_id( $uncached_friendship_ids );
+
+			foreach ( $uncached_friendships as $uncached_friendship ) {
+				wp_cache_set( $uncached_friendship->id, $uncached_friendship, 'bp_friends_friendships' );
+			}
+		}
+
+		// Assemble filter array.
+		$filters = wp_array_slice_assoc( $r, array( 'id', 'initiator_user_id', 'friend_user_id', 'is_confirmed', 'is_limited' ) );
+		foreach ( $filters as $filter_name => $filter_value ) {
+			if ( is_null( $filter_value ) ) {
+				unset( $filters[ $filter_name ] );
+			}
+		}
+
+		// Populate friendship array from cache, and normalize.
+		$friendships = array();
+		$int_keys    = array( 'id', 'initiator_user_id', 'friend_user_id' );
+		$bool_keys   = array( 'is_confirmed', 'is_limited' );
+		foreach ( $friendship_ids as $friendship_id ) {
+			// Create a limited BP_Friends_Friendship object (don't fetch the user details).
+			$friendship = new BP_Friends_Friendship( $friendship_id, false, false );
+
+			// Sanity check.
+			if ( ! isset( $friendship->id ) ) {
+				continue;
+			}
+
+			// Integer values.
+			foreach ( $int_keys as $index ) {
+				$friendship->{$index} = intval( $friendship->{$index} );
+			}
+
+			// Boolean values.
+			foreach ( $bool_keys as $index ) {
+				$friendship->{$index} = (bool) $friendship->{$index};
+			}
+
+			// We need to support the same operators as wp_list_filter().
+			if ( 'OR' == $operator || 'NOT' == $operator ) {
+				$matched = 0;
+
+				foreach ( $filters as $filter_name => $filter_value ) {
+					if ( isset( $friendship->{$filter_name} ) && $filter_value == $friendship->{$filter_name} ) {
+						$matched++;
+					}
+				}
+
+				if ( ( 'OR' == $operator && $matched > 0 )
+				  || ( 'NOT' == $operator && 0 == $matched ) ) {
+					$friendships[ $friendship->id ] = $friendship;
+				}
+
+			} else {
+				/*
+				 * This is the more typical 'AND' style of filter.
+				 * If any of the filters miss, we move on.
+				 */
+				foreach ( $filters as $filter_name => $filter_value ) {
+					if ( ! isset( $friendship->{$filter_name} ) || $filter_value != $friendship->{$filter_name} ) {
+						continue 2;
+					}
+				}
+				$friendships[ $friendship->id ] = $friendship;
+			}
+
+		}
+
+		// Sort the results on a column name.
+		if ( in_array( $r['order_by'], array( 'id', 'initiator_user_id', 'friend_user_id' ) ) ) {
+			$friendships = bp_sort_by_key( $friendships, $r['order_by'], 'num', true );
+		}
+
+		// Adjust the sort direction of the results.
+		if ( 'ASC' === strtoupper( $r['sort_order'] ) ) {
+			// `true` to preserve keys.
+			$friendships = array_reverse( $friendships, true );
+		}
+
+		// Paginate the results.
+		if ( $r['per_page'] && $r['page'] ) {
+			$start       = ( $r['page'] - 1 ) * ( $r['per_page'] );
+			$friendships = array_slice( $friendships, $start, $r['per_page'] );
+		}
+
+		return $friendships;
+	}
+
+	/**
+	 * Get all friendship IDs for a user.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int $user_id ID of the user.
+	 * @return array
+	 */
+	public static function get_friendship_ids_for_user( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$friendship_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d OR friend_user_id = %d) ORDER BY date_created DESC", $user_id, $user_id ) );
+
+		return $friendship_ids;
+	}
+
+	/**
+	 * Get the IDs of a given user's friends.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int  $user_id              ID of the user whose friends are being retrieved.
+	 * @param bool $friend_requests_only Optional. Whether to fetch
+	 *                                   unaccepted requests only. Default: false.
+	 * @param bool $assoc_arr            Optional. True to receive an array of arrays
+	 *                                   keyed as 'user_id' => $user_id; false to get a one-dimensional
+	 *                                   array of user IDs. Default: false.
+	 * @return array $fids IDs of friends for provided user.
+	 */
+	public static function get_friend_user_ids( $user_id, $friend_requests_only = false, $assoc_arr = false ) {
+		global $wpdb;
+
+		if ( ! empty( $friend_requests_only ) ) {
+			$args = array(
+				'is_confirmed' => 0,
+				'friend_user_id' => $user_id
+			);
+		} else {
+			$args = array(
+				'is_confirmed' => 1,
+			);
+		}
+
+		$friendships = self::get_friendships( $user_id, $args );
+
+		$fids = array();
+		foreach ( $friendships as $friendship ) {
+			if ( ! empty( $assoc_arr ) ) {
+				$fids[] = array( 'user_id' => ( $friendship->friend_user_id == $user_id ) ? $friendship->initiator_user_id : $friendship->friend_user_id );
+			} else {
+				$fids[] = ( $friendship->friend_user_id == $user_id ) ? $friendship->initiator_user_id : $friendship->friend_user_id;
+			}
+		}
+
+		return array_map( 'intval', $fids );
+	}
+
+	/**
+	 * Get the ID of the friendship object, if any, between a pair of users.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id   The ID of the first user.
+	 * @param int $friend_id The ID of the second user.
+	 * @return int|null The ID of the friendship object if found, otherwise null.
+	 */
+	public static function get_friendship_id( $user_id, $friend_id ) {
+		$friendship_id = null;
+
+		// Can't friend yourself.
+		if ( $user_id == $friend_id ) {
+			return $friendship_id;
+		}
+
+		/*
+		 * Find friendships where the possible_friend_userid is the
+		 * initiator or friend.
+		 */
+		$args = array(
+			'initiator_user_id' => $friend_id,
+			'friend_user_id'    => $friend_id
+		);
+		$result = self::get_friendships( $user_id, $args, 'OR' );
+		if ( $result ) {
+			$friendship_id = current( $result )->id;
+		}
+		return $friendship_id;
+	}
+
+	/**
+	 * Get a list of IDs of users who have requested friendship of a given user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id The ID of the user who has received the
+	 *                     friendship requests.
+	 * @return array|bool An array of user IDs, or false if none are found.
+	 */
+	public static function get_friendship_request_user_ids( $user_id ) {
+		$friend_requests = wp_cache_get( $user_id, 'bp_friends_requests' );
+
+		if ( false === $friend_requests ) {
+			$friend_requests = self::get_friend_user_ids( $user_id, true );
+
+			wp_cache_set( $user_id, $friend_requests, 'bp_friends_requests' );
+		}
+
+		// Integer casting.
+		if ( ! empty( $friend_requests ) ) {
+			$friend_requests = array_map( 'intval', $friend_requests );
+		}
+
+		return $friend_requests;
+	}
+
+	/**
+	 * Get a total friend count for a given user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id Optional. ID of the user whose friendships you
+	 *                     are counting. Default: displayed user (if any), otherwise
+	 *                     logged-in user.
+	 * @return int Friend count for the user.
+	 */
+	public static function total_friend_count( $user_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) ) {
+			$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+		}
+
+		/*
+		 * This is stored in 'total_friend_count' usermeta.
+		 * This function will recalculate, update and return.
+		 */
+
+		$args = array(
+			'is_confirmed' => 1,
+		);
+		$friendships = self::get_friendships( $user_id, $args );
+		$count       = count( $friendships );
+
+		// Do not update meta if user has never had friends.
+		if ( ! $count && ! bp_get_user_meta( $user_id, 'total_friend_count', true ) ) {
+			return 0;
+		}
+
+		bp_update_user_meta( $user_id, 'total_friend_count', (int) $count );
+
+		return absint( $count );
+	}
+
+	/**
+	 * Search the friends of a user by a search string.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string   $filter  The search string, matched against xprofile
+	 *                        fields (if available), or usermeta 'nickname' field.
+	 * @param int      $user_id ID of the user whose friends are being searched.
+	 * @param int|null $limit   Optional. Max number of friends to return.
+	 * @param int|null $page    Optional. The page of results to return. Default:
+	 *                          null (no pagination - return all results).
+	 * @return array|bool On success, an array: {
+	 *     @type array $friends IDs of friends returned by the query.
+	 *     @type int   $count   Total number of friends (disregarding
+	 *                          pagination) who match the search.
+	 * }. Returns false on failure.
+	 */
+	public static function search_friends( $filter, $user_id, $limit = null, $page = null ) {
+		global $wpdb;
+
+		/*
+		 * TODO: Optimize this function.
+		 */
+
+		if ( empty( $user_id ) )
+			$user_id = bp_loggedin_user_id();
+
+		// Only search for matching strings at the beginning of the
+		// name (@todo - figure out why this restriction).
+		$search_terms_like = bp_esc_like( $filter ) . '%';
+
+		$pag_sql = '';
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		if ( !$friend_ids = BP_Friends_Friendship::get_friend_user_ids( $user_id ) )
+			return false;
+
+		// Get all the user ids for the current user's friends.
+		$fids = implode( ',', wp_parse_id_list( $friend_ids ) );
+
+		if ( empty( $fids ) )
+			return false;
+
+		$bp = buddypress();
+
+		// Filter the user_ids based on the search criteria.
+		if ( bp_is_active( 'xprofile' ) ) {
+			$sql       = $wpdb->prepare( "SELECT DISTINCT user_id FROM {$bp->profile->table_name_data} WHERE user_id IN ({$fids}) AND value LIKE %s {$pag_sql}", $search_terms_like );
+			$total_sql = $wpdb->prepare( "SELECT COUNT(DISTINCT user_id) FROM {$bp->profile->table_name_data} WHERE user_id IN ({$fids}) AND value LIKE %s", $search_terms_like );
+		} else {
+			$sql       = $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE user_id IN ({$fids}) AND meta_key = 'nickname' AND meta_value LIKE %s {$pag_sql}", $search_terms_like );
+			$total_sql = $wpdb->prepare( "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE user_id IN ({$fids}) AND meta_key = 'nickname' AND meta_value LIKE %s", $search_terms_like );
+		}
+
+		$filtered_friend_ids = $wpdb->get_col( $sql );
+		$total_friend_ids    = $wpdb->get_var( $total_sql );
+
+		if ( empty( $filtered_friend_ids ) )
+			return false;
+
+		return array( 'friends' => array_map( 'intval', $filtered_friend_ids ), 'total' => (int) $total_friend_ids );
+	}
+
+	/**
+	 * Check friendship status between two users.
+	 *
+	 * Note that 'pending' means that $initiator_userid has sent a friend
+	 * request to $possible_friend_userid that has not yet been approved,
+	 * while 'awaiting_response' is the other way around ($possible_friend_userid
+	 * sent the initial request).
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $initiator_userid       The ID of the user who is the initiator
+	 *                                    of the potential friendship/request.
+	 * @param int $possible_friend_userid The ID of the user who is the
+	 *                                    recipient of the potential friendship/request.
+	 * @return string|false $value The friendship status, from among 'not_friends',
+	 *                             'is_friend', 'pending', and 'awaiting_response'.
+	 */
+	public static function check_is_friend( $initiator_userid, $possible_friend_userid ) {
+		global $wpdb;
+
+		if ( empty( $initiator_userid ) || empty( $possible_friend_userid ) ) {
+			return false;
+		}
+
+		// Can't friend yourself.
+		if ( $initiator_userid == $possible_friend_userid ) {
+			return 'not_friends';
+		}
+
+		BP_Friends_Friendship::update_bp_friends_cache( $initiator_userid, $possible_friend_userid );
+
+		return bp_core_get_incremented_cache( $initiator_userid . ':' . $possible_friend_userid, 'bp_friends' );
+	}
+
+
+	/**
+	 * Find uncached friendships between a user and one or more other users and cache them.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param int $user_id                          The ID of the primary user for whom we want 
+	 *                                              to check friendships statuses.
+	 * @param int|array|string $possible_friend_ids The IDs of the one or more users 
+	 *                                              to check friendship status with primary user.
+	 * @return null
+	 */
+	public static function update_bp_friends_cache( $user_id, $possible_friend_ids ) {
+		global $wpdb;
+		$bp = buddypress();
+		$possible_friend_ids = wp_parse_id_list( $possible_friend_ids );
+
+		$fetch = array();
+		foreach ( $possible_friend_ids as $friend_id ) {
+			// Check for cached items in both friendship directions.
+			if ( false === bp_core_get_incremented_cache( $user_id . ':' . $friend_id, 'bp_friends' ) 
+				|| false === bp_core_get_incremented_cache( $friend_id . ':' . $user_id, 'bp_friends' ) ) {
+				$fetch[] = $friend_id;
+			}
+		}
+		if ( empty( $fetch ) ) {
+			return;
+		}
+
+		$friend_ids_sql = implode( ',', array_unique( $fetch ) );
+		$sql = $wpdb->prepare( "SELECT initiator_user_id, friend_user_id, is_confirmed FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d AND friend_user_id IN ({$friend_ids_sql}) ) OR (initiator_user_id IN ({$friend_ids_sql}) AND friend_user_id = %d )", $user_id, $user_id );
+		$friendships = $wpdb->get_results( $sql );
+
+		// Use $handled to keep track of all of the $possible_friend_ids we've matched.
+		$handled = array();
+		foreach ( $friendships as $friendship ) {
+			$initiator_user_id = (int) $friendship->initiator_user_id;
+			$friend_user_id    = (int) $friendship->friend_user_id;
+			if ( 1 === (int) $friendship->is_confirmed ) {
+				$status_initiator = $status_friend = 'is_friend';
+			} else {
+				$status_initiator = 'pending'; 
+				$status_friend    = 'awaiting_response';
+			}
+			bp_core_set_incremented_cache( $initiator_user_id . ':' . $friend_user_id, 'bp_friends', $status_initiator );
+			bp_core_set_incremented_cache( $friend_user_id . ':' . $initiator_user_id, 'bp_friends', $status_friend );
+
+			$handled[] = ( $initiator_user_id === $user_id ) ? $friend_user_id : $initiator_user_id;
+		}
+
+		// Set all those with no matching entry to "not friends" status.
+		$not_friends = array_diff( $fetch, $handled );
+
+		foreach ( $not_friends as $not_friend_id ) {
+			bp_core_set_incremented_cache( $user_id . ':' . $not_friend_id, 'bp_friends', 'not_friends' );
+			bp_core_set_incremented_cache( $not_friend_id . ':' . $user_id, 'bp_friends', 'not_friends' );
+		}
+	}
+
+	/**
+	 * Get the last active date of many users at once.
+	 *
+	 * @todo Why is this in the Friends component?
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $user_ids IDs of users whose last_active meta is
+	 *                        being queried.
+	 * @return array $retval Array of last_active values + user_ids.
+	 */
+	public static function get_bulk_last_active( $user_ids ) {
+		global $wpdb;
+
+		$last_activities = BP_Core_User::get_last_activity( $user_ids );
+
+		// Sort and structure as expected in legacy function.
+		usort( $last_activities, function( $a, $b ) {
+			if ( $a['date_recorded'] == $b['date_recorded'] ) {
+				return 0;
+			}
+
+			return ( strtotime( $a['date_recorded'] ) < strtotime( $b['date_recorded'] ) ) ? 1 : -1;
+		} );
+
+		$retval = array();
+		foreach ( $last_activities as $last_activity ) {
+			$u = new stdClass;
+			$u->last_activity = $last_activity['date_recorded'];
+			$u->user_id       = $last_activity['user_id'];
+
+			$retval[] = $u;
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Mark a friendship as accepted.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $friendship_id ID of the friendship to be accepted.
+	 * @return int Number of database rows updated.
+	 */
+	public static function accept($friendship_id) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "UPDATE {$bp->friends->table_name} SET is_confirmed = 1, date_created = %s WHERE id = %d AND friend_user_id = %d", bp_core_current_time(), $friendship_id, bp_loggedin_user_id() ) );
+	}
+
+	/**
+	 * Remove a friendship or a friendship request INITIATED BY the logged-in user.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $friendship_id ID of the friendship to be withdrawn.
+	 * @return int Number of database rows deleted.
+	 */
+	public static function withdraw($friendship_id) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE id = %d AND initiator_user_id = %d", $friendship_id, bp_loggedin_user_id() ) );
+	}
+
+	/**
+	 * Remove a friendship or a friendship request MADE OF the logged-in user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $friendship_id ID of the friendship to be rejected.
+	 * @return int Number of database rows deleted.
+	 */
+	public static function reject($friendship_id) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE id = %d AND friend_user_id = %d", $friendship_id, bp_loggedin_user_id() ) );
+	}
+
+	/**
+	 * Search users.
+	 *
+	 * @todo Why does this exist, and why is it in bp-friends?
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string   $filter  String to search by.
+	 * @param int      $user_id A user ID param that is unused.
+	 * @param int|null $limit   Optional. Max number of records to return.
+	 * @param int|null $page    Optional. Number of the page to return. Default:
+	 *                          false (no pagination - return all results).
+	 * @return array $filtered_ids IDs of users who match the query.
+	 */
+	public static function search_users( $filter, $user_id, $limit = null, $page = null ) {
+		global $wpdb;
+
+		// Only search for matching strings at the beginning of the
+		// name (@todo - figure out why this restriction).
+		$search_terms_like = bp_esc_like( $filter ) . '%';
+
+		$usermeta_table = $wpdb->base_prefix . 'usermeta';
+		$users_table    = $wpdb->base_prefix . 'users';
+
+		$pag_sql = '';
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * intval( $limit ) ), intval( $limit ) );
+
+		$bp = buddypress();
+
+		// Filter the user_ids based on the search criteria.
+		if ( bp_is_active( 'xprofile' ) ) {
+			$sql = $wpdb->prepare( "SELECT DISTINCT d.user_id as id FROM {$bp->profile->table_name_data} d, {$users_table} u WHERE d.user_id = u.id AND d.value LIKE %s ORDER BY d.value DESC {$pag_sql}", $search_terms_like );
+		} else {
+			$sql = $wpdb->prepare( "SELECT DISTINCT user_id as id FROM {$usermeta_table} WHERE meta_value LIKE %s ORDER BY d.value DESC {$pag_sql}", $search_terms_like );
+		}
+
+		$filtered_fids = $wpdb->get_col($sql);
+
+		if ( empty( $filtered_fids ) )
+			return false;
+
+		return $filtered_fids;
+	}
+
+	/**
+	 * Get a count of users who match a search term.
+	 *
+	 * @todo Why does this exist, and why is it in bp-friends?
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $filter Search term.
+	 * @return int Count of users matching the search term.
+	 */
+	public static function search_users_count( $filter ) {
+		global $wpdb;
+
+		// Only search for matching strings at the beginning of the
+		// name (@todo - figure out why this restriction).
+		$search_terms_like = bp_esc_like( $filter ) . '%';
+
+		$usermeta_table = $wpdb->prefix . 'usermeta';
+		$users_table    = $wpdb->base_prefix . 'users';
+
+		$bp = buddypress();
+
+		// Filter the user_ids based on the search criteria.
+		if ( bp_is_active( 'xprofile' ) ) {
+			$sql = $wpdb->prepare( "SELECT COUNT(DISTINCT d.user_id) FROM {$bp->profile->table_name_data} d, {$users_table} u WHERE d.user_id = u.id AND d.value LIKE %s", $search_terms_like );
+		} else {
+			$sql = $wpdb->prepare( "SELECT COUNT(DISTINCT user_id) FROM {$usermeta_table} WHERE meta_value LIKE %s", $search_terms_like );
+		}
+
+		$user_count = $wpdb->get_col($sql);
+
+		if ( empty( $user_count ) )
+			return false;
+
+		return $user_count[0];
+	}
+
+	/**
+	 * Sort a list of user IDs by their display names.
+	 *
+	 * @todo Why does this exist, and why is it in bp-friends?
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $user_ids Array of user IDs.
+	 * @return array User IDs, sorted by the associated display names.
+	 */
+	public static function sort_by_name( $user_ids ) {
+		global $wpdb;
+
+		if ( !bp_is_active( 'xprofile' ) )
+			return false;
+
+		$bp = buddypress();
+
+		$user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
+
+		return $wpdb->get_results( $wpdb->prepare( "SELECT user_id FROM {$bp->profile->table_name_data} pd, {$bp->profile->table_name_fields} pf WHERE pf.id = pd.field_id AND pf.name = %s AND pd.user_id IN ( {$user_ids} ) ORDER BY pd.value ASC", bp_xprofile_fullname_field_name() ) );
+	}
+
+	/**
+	 * Get a list of random friend IDs.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id       ID of the user whose friends are being retrieved.
+	 * @param int $total_friends Optional. Number of random friends to get.
+	 *                           Default: 5.
+	 * @return array|false An array of random friend user IDs on success;
+	 *                     false if none are found.
+	 */
+	public static function get_random_friends( $user_id, $total_friends = 5 ) {
+		global $wpdb;
+
+		$bp      = buddypress();
+		$fids    = array();
+		$sql     = $wpdb->prepare( "SELECT friend_user_id, initiator_user_id FROM {$bp->friends->table_name} WHERE (friend_user_id = %d || initiator_user_id = %d) && is_confirmed = 1 ORDER BY rand() LIMIT %d", $user_id, $user_id, $total_friends );
+		$results = $wpdb->get_results( $sql );
+
+		for ( $i = 0, $count = count( $results ); $i < $count; ++$i ) {
+			$fids[] = ( $results[$i]->friend_user_id == $user_id ) ? $results[$i]->initiator_user_id : $results[$i]->friend_user_id;
+		}
+
+		// Remove duplicates.
+		if ( count( $fids ) > 0 )
+			return array_flip( array_flip( $fids ) );
+		else
+			return false;
+	}
+
+	/**
+	 * Get a count of a user's friends who can be invited to a given group.
+	 *
+	 * Users can invite any of their friends except:
+	 *
+	 * - users who are already in the group
+	 * - users who have a pending invite to the group
+	 * - users who have been banned from the group
+	 *
+	 * @since 1.0.0
+	 * @todo Need to do a group component check before using group functions.
+	 *
+	 * @param int $user_id  ID of the user whose friends are being counted.
+	 * @param int $group_id ID of the group friends are being invited to.
+	 * @return int $invitable_count Eligible friend count.
+	 */
+	public static function get_invitable_friend_count( $user_id, $group_id ) {
+
+		// Setup some data we'll use below.
+		$is_group_admin  = groups_is_user_admin( $user_id, $group_id );
+		$friend_ids      = BP_Friends_Friendship::get_friend_user_ids( $user_id );
+		$invitable_count = 0;
+
+		for ( $i = 0, $count = count( $friend_ids ); $i < $count; ++$i ) {
+
+			// If already a member, they cannot be invited again.
+			if ( groups_is_user_member( (int) $friend_ids[$i], $group_id ) ) {
+				continue;
+			}
+
+			// If user already has invite, they cannot be added.
+			if ( groups_check_user_has_invite( (int) $friend_ids[$i], $group_id ) ) {
+				continue;
+			}
+
+			// If user is not group admin and friend is banned, they cannot be invited.
+			if ( ( false === $is_group_admin ) && groups_is_user_banned( (int) $friend_ids[$i], $group_id ) ) {
+				continue;
+			}
+
+			$invitable_count++;
+		}
+
+		return $invitable_count;
+	}
+
+	/**
+	 * Get friendship objects by ID (or an array of IDs).
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int|string|array $friendship_ids Single friendship ID or comma-separated/array list of friendship IDs.
+	 * @return array
+	 */
+	public static function get_friendships_by_id( $friendship_ids ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$friendship_ids = implode( ',', wp_parse_id_list( $friendship_ids ) );
+		return $wpdb->get_results( "SELECT * FROM {$bp->friends->table_name} WHERE id IN ({$friendship_ids})" );
+	}
+
+	/**
+	 * Get the friend user IDs for a given friendship.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $friendship_id ID of the friendship.
+	 * @return null|stdClass friend_user_id and initiator_user_id.
+	 */
+	public static function get_user_ids_for_friendship( $friendship_id ) {
+
+		$friendship = new BP_Friends_Friendship( $friendship_id, false, false );
+
+		if ( empty( $friendship->id ) ) {
+			return null;
+		}
+
+		$retval = new StdClass;
+		$retval->friend_user_id = $friendship->friend_user_id;
+		$retval->initiator_user_id = $friendship->initiator_user_id;
+
+		return $retval;
+	}
+
+	/**
+	 * Delete all friendships and friend notifications related to a user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id ID of the user being expunged.
+	 */
+	public static function delete_all_for_user( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// Get all friendships, of any status, for the user.
+		$friendships    = self::get_friendships( $user_id );
+		$friend_ids     = array();
+		$friendship_ids = array();
+		foreach ( $friendships as $friendship ) {
+			$friendship_ids[] = $friendship->id;
+			if ( $friendship->is_confirmed ) {
+				$friend_ids[] = ( $friendship->friend_user_id == $user_id ) ? $friendship->initiator_user_id : $friendship->friend_user_id;
+			}
+		}
+
+		// Delete the friendships from the database.
+		if ( $friendship_ids ) {
+			$friendship_ids_sql = implode( ',', wp_parse_id_list( $friendship_ids ) );
+			$wpdb->query( "DELETE FROM {$bp->friends->table_name} WHERE id IN ({$friendship_ids_sql})" );
+		}
+
+		// Delete friend request notifications for members who have a
+		// notification from this user.
+		if ( bp_is_active( 'notifications' ) ) {
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->notifications->table_name} WHERE component_name = 'friends' AND ( component_action = 'friendship_request' OR component_action = 'friendship_accepted' ) AND item_id = %d", $user_id ) );
+		}
+
+		// Clean up the friendships cache.
+		foreach ( $friendship_ids as $friendship_id ) {
+			wp_cache_delete( $friendship_id, 'bp_friends_friendships' );
+		}
+
+		// Loop through friend_ids to scrub user caches and update total count metas.
+		foreach ( (array) $friend_ids as $friend_id ) {
+			// Delete cached friendships.
+			wp_cache_delete( $friend_id, 'bp_friends_friendships_for_user' );
+			BP_Friends_Friendship::total_friend_count( $friend_id );
+		}
+
+		// Delete cached friendships.
+		wp_cache_delete( $user_id, 'bp_friends_friendships_for_user' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-friends/js/widget-friends.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/js/widget-friends.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/js/widget-friends.js	(revision 12265)
@@ -0,0 +1,60 @@
+jQuery(document).ready( function() {
+	friend_widget_click_handler();
+
+	// WP 4.5 - Customizer selective refresh support.
+	if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) {
+		wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function() {
+			friend_widget_click_handler();
+		} );
+	}
+});
+
+function friend_widget_click_handler() {
+	jQuery('.widget div#friends-list-options a').on('click',
+		function() {
+			var link = this;
+			jQuery(link).addClass('loading');
+
+			jQuery('.widget div#friends-list-options a').removeClass('selected');
+			jQuery(this).addClass('selected');
+
+			jQuery.post( ajaxurl, {
+				action: 'widget_friends',
+				'cookie': encodeURIComponent(document.cookie),
+				'_wpnonce': jQuery('input#_wpnonce-friends').val(),
+				'max-friends': jQuery('input#friends_widget_max').val(),
+				'filter': jQuery(this).attr('id')
+			},
+			function(response)
+			{
+				jQuery(link).removeClass('loading');
+				friend_widget_response(response);
+			});
+
+			return false;
+		}
+	);
+}
+
+function friend_widget_response(response) {
+	response = response.substr(0, response.length-1);
+	response = response.split('[[SPLIT]]');
+
+	if ( response[0] !== '-1' ) {
+		jQuery('.widget ul#friends-list').fadeOut(200,
+			function() {
+				jQuery('.widget ul#friends-list').html(response[1]);
+				jQuery('.widget ul#friends-list').fadeIn(200);
+			}
+		);
+
+	} else {
+		jQuery('.widget ul#friends-list').fadeOut(200,
+			function() {
+				var message = '<p>' + response[1] + '</p>';
+				jQuery('.widget ul#friends-list').html(message);
+				jQuery('.widget ul#friends-list').fadeIn(200);
+			}
+		);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-friends/screens/my-friends.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/screens/my-friends.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/screens/my-friends.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Friends: User's "Friends" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage FriendsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process the My Friends page.
+ *
+ * @since 1.0.0
+ */
+function friends_screen_my_friends() {
+
+	/**
+	 * Fires before the loading of template for the My Friends page.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'friends_screen_my_friends' );
+
+	/**
+	 * Filters the template used to display the My Friends page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the my friends template to load.
+	 */
+	bp_core_load_template( apply_filters( 'friends_template_my_friends', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-friends/screens/requests.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-friends/screens/requests.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-friends/screens/requests.php	(revision 12265)
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Friends: User's "Friends > Requests" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage FriendsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process the Requests page.
+ *
+ * @since 1.0.0
+ */
+function friends_screen_requests() {
+	if ( bp_is_action_variable( 'accept', 0 ) && is_numeric( bp_action_variable( 1 ) ) ) {
+		// Check the nonce.
+		check_admin_referer( 'friends_accept_friendship' );
+
+		if ( friends_accept_friendship( bp_action_variable( 1 ) ) )
+			bp_core_add_message( __( 'Friendship accepted', 'buddypress' ) );
+		else
+			bp_core_add_message( __( 'Friendship could not be accepted', 'buddypress' ), 'error' );
+
+		bp_core_redirect( trailingslashit( bp_loggedin_user_domain() . bp_current_component() . '/' . bp_current_action() ) );
+
+	} elseif ( bp_is_action_variable( 'reject', 0 ) && is_numeric( bp_action_variable( 1 ) ) ) {
+		// Check the nonce.
+		check_admin_referer( 'friends_reject_friendship' );
+
+		if ( friends_reject_friendship( bp_action_variable( 1 ) ) )
+			bp_core_add_message( __( 'Friendship rejected', 'buddypress' ) );
+		else
+			bp_core_add_message( __( 'Friendship could not be rejected', 'buddypress' ), 'error' );
+
+		bp_core_redirect( trailingslashit( bp_loggedin_user_domain() . bp_current_component() . '/' . bp_current_action() ) );
+
+	} elseif ( bp_is_action_variable( 'cancel', 0 ) && is_numeric( bp_action_variable( 1 ) ) ) {
+		// Check the nonce.
+		check_admin_referer( 'friends_withdraw_friendship' );
+
+		if ( friends_withdraw_friendship( bp_loggedin_user_id(), bp_action_variable( 1 ) ) )
+			bp_core_add_message( __( 'Friendship request withdrawn', 'buddypress' ) );
+		else
+			bp_core_add_message( __( 'Friendship request could not be withdrawn', 'buddypress' ), 'error' );
+
+		bp_core_redirect( trailingslashit( bp_loggedin_user_domain() . bp_current_component() . '/' . bp_current_action() ) );
+	}
+
+	/**
+	 * Fires before the loading of template for the friends requests page.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'friends_screen_requests' );
+
+	/**
+	 * Filters the template used to display the My Friends page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the friends request template to load.
+	 */
+	bp_core_load_template( apply_filters( 'friends_template_requests', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/actions/access.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/actions/access.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/actions/access.php	(revision 12265)
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Groups: Access protection action handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupActions
+ * @since 3.0.0
+ */
+
+/**
+ * Protect access to single groups.
+ *
+ * @since 2.1.0
+ */
+function bp_groups_group_access_protection() {
+	if ( ! bp_is_group() ) {
+		return;
+	}
+
+	$current_group   = groups_get_current_group();
+	$user_has_access = $current_group->user_has_access;
+	$is_visible      = $current_group->is_visible;
+	$no_access_args  = array();
+
+	// The user can know about the group but doesn't have full access.
+	if ( ! $user_has_access && $is_visible ) {
+		// Always allow access to home and request-membership.
+		if ( bp_is_current_action( 'home' ) || bp_is_current_action( 'request-membership' ) ) {
+			$user_has_access = true;
+
+		// User doesn't have access, so set up redirect args.
+		} elseif ( is_user_logged_in() ) {
+			$no_access_args = array(
+				'message'  => __( 'You do not have access to this group.', 'buddypress' ),
+				'root'     => bp_get_group_permalink( $current_group ) . 'home/',
+				'redirect' => false
+			);
+		}
+	}
+
+	// Protect the admin tab from non-admins.
+	if ( bp_is_current_action( 'admin' ) && ! bp_is_item_admin() ) {
+		$user_has_access = false;
+		$no_access_args  = array(
+			'message'  => __( 'You are not an admin of this group.', 'buddypress' ),
+			'root'     => bp_get_group_permalink( $current_group ),
+			'redirect' => false
+		);
+	}
+
+	/**
+	 * Allow plugins to filter whether the current user has access to this group content.
+	 *
+	 * Note that if a plugin sets $user_has_access to false, it may also
+	 * want to change the $no_access_args, to avoid problems such as
+	 * logged-in users being redirected to wp-login.php.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool  $user_has_access True if the user has access to the
+	 *                               content, otherwise false.
+	 * @param array $no_access_args  Arguments to be passed to bp_core_no_access() in case
+	 *                               of no access. Note that this value is passed by reference,
+	 *                               so it can be modified by the filter callback.
+	 */
+	$user_has_access = apply_filters_ref_array( 'bp_group_user_has_access', array( $user_has_access, &$no_access_args ) );
+
+	// If user has access, we return rather than redirect.
+	if ( $user_has_access ) {
+		return;
+	}
+
+	// Groups that the user cannot know about should return a 404 for non-members.
+	// Unset the current group so that you're not redirected
+	// to the default group tab.
+	if ( ! $is_visible ) {
+		buddypress()->groups->current_group = 0;
+		buddypress()->is_single_item        = false;
+		bp_do_404();
+		return;
+	} else {
+		bp_core_no_access( $no_access_args );
+	}
+
+}
+add_action( 'bp_actions', 'bp_groups_group_access_protection' );
Index: /tags/4.0.0-beta1/src/bp-groups/actions/create.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/actions/create.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/actions/create.php	(revision 12265)
@@ -0,0 +1,340 @@
+<?php
+/**
+ * Groups: Create action
+ *
+ * @package BuddyPress
+ * @subpackage GroupActions
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process group creation form submissions.
+ *
+ * @since 1.2.0
+ *
+ * @return bool
+ */
+function groups_action_create_group() {
+
+	// If we're not at domain.org/groups/create/ then return false.
+	if ( !bp_is_groups_component() || !bp_is_current_action( 'create' ) )
+		return false;
+
+	if ( !is_user_logged_in() )
+		return false;
+
+	if ( !bp_user_can_create_groups() ) {
+		bp_core_add_message( __( 'Sorry, you are not allowed to create groups.', 'buddypress' ), 'error' );
+		bp_core_redirect( bp_get_groups_directory_permalink() );
+	}
+
+	$bp = buddypress();
+
+	// Make sure creation steps are in the right order.
+	groups_action_sort_creation_steps();
+
+	// If no current step is set, reset everything so we can start a fresh group creation.
+	$bp->groups->current_create_step = bp_action_variable( 1 );
+	if ( !bp_get_groups_current_create_step() ) {
+		unset( $bp->groups->current_create_step );
+		unset( $bp->groups->completed_create_steps );
+
+		setcookie( 'bp_new_group_id', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+		setcookie( 'bp_completed_create_steps', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+
+		$reset_steps = true;
+		$keys        = array_keys( $bp->groups->group_creation_steps );
+		bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create/step/' . array_shift( $keys ) ) );
+	}
+
+	// If this is a creation step that is not recognized, just redirect them back to the first screen.
+	if ( bp_get_groups_current_create_step() && empty( $bp->groups->group_creation_steps[bp_get_groups_current_create_step()] ) ) {
+		bp_core_add_message( __('There was an error saving group details. Please try again.', 'buddypress'), 'error' );
+		bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create' ) );
+	}
+
+	// Fetch the currently completed steps variable.
+	if ( isset( $_COOKIE['bp_completed_create_steps'] ) && !isset( $reset_steps ) )
+		$bp->groups->completed_create_steps = json_decode( base64_decode( stripslashes( $_COOKIE['bp_completed_create_steps'] ) ) );
+
+	// Set the ID of the new group, if it has already been created in a previous step.
+	if ( bp_get_new_group_id() ) {
+		$bp->groups->current_group = groups_get_group( $bp->groups->new_group_id );
+
+		// Only allow the group creator to continue to edit the new group.
+		if ( ! bp_is_group_creator( $bp->groups->current_group, bp_loggedin_user_id() ) ) {
+			bp_core_add_message( __( 'Only the group creator may continue editing this group.', 'buddypress' ), 'error' );
+			bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create' ) );
+		}
+	}
+
+	// If the save, upload or skip button is hit, lets calculate what we need to save.
+	if ( isset( $_POST['save'] ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'groups_create_save_' . bp_get_groups_current_create_step() );
+
+		if ( 'group-details' == bp_get_groups_current_create_step() ) {
+			if ( empty( $_POST['group-name'] ) || empty( $_POST['group-desc'] ) || !strlen( trim( $_POST['group-name'] ) ) || !strlen( trim( $_POST['group-desc'] ) ) ) {
+				bp_core_add_message( __( 'Please fill in all of the required fields', 'buddypress' ), 'error' );
+				bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create/step/' . bp_get_groups_current_create_step() ) );
+			}
+
+			$new_group_id = isset( $bp->groups->new_group_id ) ? $bp->groups->new_group_id : 0;
+
+			if ( !$bp->groups->new_group_id = groups_create_group( array( 'group_id' => $new_group_id, 'name' => $_POST['group-name'], 'description' => $_POST['group-desc'], 'slug' => groups_check_slug( sanitize_title( esc_attr( $_POST['group-name'] ) ) ), 'date_created' => bp_core_current_time(), 'status' => 'public' ) ) ) {
+				bp_core_add_message( __( 'There was an error saving group details. Please try again.', 'buddypress' ), 'error' );
+				bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create/step/' . bp_get_groups_current_create_step() ) );
+			}
+		}
+
+		if ( 'group-settings' == bp_get_groups_current_create_step() ) {
+			$group_status = 'public';
+			$group_enable_forum = 1;
+
+			if ( !isset($_POST['group-show-forum']) ) {
+				$group_enable_forum = 0;
+			}
+
+			if ( 'private' == $_POST['group-status'] )
+				$group_status = 'private';
+			elseif ( 'hidden' == $_POST['group-status'] )
+				$group_status = 'hidden';
+
+			if ( !$bp->groups->new_group_id = groups_create_group( array( 'group_id' => $bp->groups->new_group_id, 'status' => $group_status, 'enable_forum' => $group_enable_forum ) ) ) {
+				bp_core_add_message( __( 'There was an error saving group details. Please try again.', 'buddypress' ), 'error' );
+				bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create/step/' . bp_get_groups_current_create_step() ) );
+			}
+
+			// Save group types.
+			if ( ! empty( $_POST['group-types'] ) ) {
+				bp_groups_set_group_type( $bp->groups->new_group_id, $_POST['group-types'] );
+			}
+
+			/**
+			 * Filters the allowed invite statuses.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param array $value Array of statuses allowed.
+			 *                     Possible values are 'members,
+			 *                     'mods', and 'admins'.
+			 */
+			$allowed_invite_status = apply_filters( 'groups_allowed_invite_status', array( 'members', 'mods', 'admins' ) );
+			$invite_status	       = !empty( $_POST['group-invite-status'] ) && in_array( $_POST['group-invite-status'], (array) $allowed_invite_status ) ? $_POST['group-invite-status'] : 'members';
+
+			groups_update_groupmeta( $bp->groups->new_group_id, 'invite_status', $invite_status );
+		}
+
+		if ( 'group-invites' === bp_get_groups_current_create_step() ) {
+			if ( ! empty( $_POST['friends'] ) ) {
+				foreach ( (array) $_POST['friends'] as $friend ) {
+					groups_invite_user( array(
+						'user_id'  => (int) $friend,
+						'group_id' => $bp->groups->new_group_id,
+					) );
+				}
+			}
+
+			groups_send_invites( bp_loggedin_user_id(), $bp->groups->new_group_id );
+		}
+
+		/**
+		 * Fires before finalization of group creation and cookies are set.
+		 *
+		 * This hook is a variable hook dependent on the current step
+		 * in the creation process.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'groups_create_group_step_save_' . bp_get_groups_current_create_step() );
+
+		/**
+		 * Fires after the group creation step is completed.
+		 *
+		 * Mostly for clearing cache on a generic action name.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'groups_create_group_step_complete' );
+
+		/**
+		 * Once we have successfully saved the details for this step of the creation process
+		 * we need to add the current step to the array of completed steps, then update the cookies
+		 * holding the information
+		 */
+		$completed_create_steps = isset( $bp->groups->completed_create_steps ) ? $bp->groups->completed_create_steps : array();
+		if ( !in_array( bp_get_groups_current_create_step(), $completed_create_steps ) )
+			$bp->groups->completed_create_steps[] = bp_get_groups_current_create_step();
+
+		// Reset cookie info.
+		setcookie( 'bp_new_group_id', $bp->groups->new_group_id, time()+60*60*24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+		setcookie( 'bp_completed_create_steps', base64_encode( json_encode( $bp->groups->completed_create_steps ) ), time()+60*60*24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+
+		// If we have completed all steps and hit done on the final step we
+		// can redirect to the completed group.
+		$keys = array_keys( $bp->groups->group_creation_steps );
+		if ( count( $bp->groups->completed_create_steps ) == count( $keys ) && bp_get_groups_current_create_step() == array_pop( $keys ) ) {
+			unset( $bp->groups->current_create_step );
+			unset( $bp->groups->completed_create_steps );
+
+			setcookie( 'bp_new_group_id', false, time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+			setcookie( 'bp_completed_create_steps', false, time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+
+			// Once we completed all steps, record the group creation in the activity stream.
+			if ( bp_is_active( 'activity' ) ) {
+				groups_record_activity( array(
+					'type' => 'created_group',
+					'item_id' => $bp->groups->new_group_id
+				) );
+			}
+
+			/**
+			 * Fires after the group has been successfully created.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param int $new_group_id ID of the newly created group.
+			 */
+			do_action( 'groups_group_create_complete', $bp->groups->new_group_id );
+
+			bp_core_redirect( bp_get_group_permalink( $bp->groups->current_group ) );
+		} else {
+			/**
+			 * Since we don't know what the next step is going to be (any plugin can insert steps)
+			 * we need to loop the step array and fetch the next step that way.
+			 */
+			foreach ( $keys as $key ) {
+				if ( $key == bp_get_groups_current_create_step() ) {
+					$next = 1;
+					continue;
+				}
+
+				if ( isset( $next ) ) {
+					$next_step = $key;
+					break;
+				}
+			}
+
+			bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create/step/' . $next_step ) );
+		}
+	}
+
+	// Remove invitations.
+	if ( 'group-invites' === bp_get_groups_current_create_step() && ! empty( $_REQUEST['user_id'] ) && is_numeric( $_REQUEST['user_id'] ) ) {
+		if ( ! check_admin_referer( 'groups_invite_uninvite_user' ) ) {
+			return false;
+		}
+
+		$message = __( 'Invite successfully removed', 'buddypress' );
+		$error   = false;
+
+		if( ! groups_uninvite_user( (int) $_REQUEST['user_id'], $bp->groups->new_group_id ) ) {
+			$message = __( 'There was an error removing the invite', 'buddypress' );
+			$error   = 'error';
+		}
+
+		bp_core_add_message( $message, $error );
+		bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . 'create/step/group-invites' ) );
+	}
+
+	// Group avatar is handled separately.
+	if ( 'group-avatar' == bp_get_groups_current_create_step() && isset( $_POST['upload'] ) ) {
+		if ( ! isset( $bp->avatar_admin ) ) {
+			$bp->avatar_admin = new stdClass();
+		}
+
+		if ( !empty( $_FILES ) && isset( $_POST['upload'] ) ) {
+			// Normally we would check a nonce here, but the group save nonce is used instead.
+			// Pass the file to the avatar upload handler.
+			if ( bp_core_avatar_handle_upload( $_FILES, 'groups_avatar_upload_dir' ) ) {
+				$bp->avatar_admin->step = 'crop-image';
+
+				// Make sure we include the jQuery jCrop file for image cropping.
+				add_action( 'wp_print_scripts', 'bp_core_add_jquery_cropper' );
+			}
+		}
+
+		// If the image cropping is done, crop the image and save a full/thumb version.
+		if ( isset( $_POST['avatar-crop-submit'] ) && isset( $_POST['upload'] ) ) {
+
+			// Normally we would check a nonce here, but the group save nonce is used instead.
+			$args = array(
+				'object'        => 'group',
+				'avatar_dir'    => 'group-avatars',
+				'item_id'       => $bp->groups->current_group->id,
+				'original_file' => $_POST['image_src'],
+				'crop_x'        => $_POST['x'],
+				'crop_y'        => $_POST['y'],
+				'crop_w'        => $_POST['w'],
+				'crop_h'        => $_POST['h']
+			);
+
+			if ( ! bp_core_avatar_handle_crop( $args ) ) {
+				bp_core_add_message( __( 'There was an error saving the group profile photo, please try uploading again.', 'buddypress' ), 'error' );
+			} else {
+				/**
+				 * Fires after a group avatar is uploaded.
+				 *
+				 * @since 2.8.0
+				 *
+				 * @param int    $group_id ID of the group.
+				 * @param string $type     Avatar type. 'crop' or 'full'.
+				 * @param array  $args     Array of parameters passed to the avatar handler.
+				 */
+				do_action( 'groups_avatar_uploaded', bp_get_current_group_id(), 'crop', $args );
+
+				bp_core_add_message( __( 'The group profile photo was uploaded successfully.', 'buddypress' ) );
+			}
+		}
+	}
+
+	/**
+	 * Filters the template to load for the group creation screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to the group creation template to load.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_create_group', 'groups/create' ) );
+}
+add_action( 'bp_actions', 'groups_action_create_group' );
+
+/**
+ * Sort the group creation steps.
+ *
+ * @since 1.1.0
+ *
+ * @return false|null False on failure.
+ */
+function groups_action_sort_creation_steps() {
+
+	if ( !bp_is_groups_component() || !bp_is_current_action( 'create' ) )
+		return false;
+
+	$bp = buddypress();
+
+	if ( !is_array( $bp->groups->group_creation_steps ) )
+		return false;
+
+	foreach ( (array) $bp->groups->group_creation_steps as $slug => $step ) {
+		while ( !empty( $temp[$step['position']] ) )
+			$step['position']++;
+
+		$temp[$step['position']] = array( 'name' => $step['name'], 'slug' => $slug );
+	}
+
+	// Sort the steps by their position key.
+	ksort($temp);
+	unset($bp->groups->group_creation_steps);
+
+	foreach( (array) $temp as $position => $step )
+		$bp->groups->group_creation_steps[$step['slug']] = array( 'name' => $step['name'], 'position' => $position );
+
+	/**
+	 * Fires after group creation sets have been sorted.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'groups_action_sort_creation_steps' );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/actions/feed.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/actions/feed.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/actions/feed.php	(revision 12265)
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Groups: RSS feed action
+ *
+ * @package BuddyPress
+ * @subpackage GroupActions
+ * @since 3.0.0
+ */
+
+/**
+ * Load the activity feed for the current group.
+ *
+ * @since 1.2.0
+ *
+ * @return false|null False on failure.
+ */
+function groups_action_group_feed() {
+
+	// Get current group.
+	$group = groups_get_current_group();
+
+	if ( ! bp_is_active( 'activity' ) || ! bp_is_groups_component() || ! $group || ! bp_is_current_action( 'feed' ) )
+		return false;
+
+	// If group isn't public or if logged-in user is not a member of the group, do
+	// not output the group activity feed.
+	if ( ! bp_group_is_visible( $group ) ) {
+		return false;
+	}
+
+	// Set up the feed.
+	buddypress()->activity->feed = new BP_Activity_Feed( array(
+		'id'            => 'group',
+
+		/* translators: Group activity RSS title - "[Site Name] | [Group Name] | Activity" */
+		'title'         => sprintf( __( '%1$s | %2$s | Activity', 'buddypress' ), bp_get_site_name(), bp_get_current_group_name() ),
+
+		'link'          => bp_get_group_permalink( $group ),
+		'description'   => sprintf( __( "Activity feed for the group, %s.", 'buddypress' ), bp_get_current_group_name() ),
+		'activity_args' => array(
+			'object'           => buddypress()->groups->id,
+			'primary_id'       => bp_get_current_group_id(),
+			'display_comments' => 'threaded'
+		)
+	) );
+}
+add_action( 'bp_actions', 'groups_action_group_feed' );
Index: /tags/4.0.0-beta1/src/bp-groups/actions/join.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/actions/join.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/actions/join.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Groups: Join action
+ *
+ * @package BuddyPress
+ * @subpackage GroupActions
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process "Join Group" button clicks.
+ *
+ * @since 1.0.0
+ *
+ * @return bool
+ */
+function groups_action_join_group() {
+
+	if ( !bp_is_single_item() || !bp_is_groups_component() || !bp_is_current_action( 'join' ) )
+		return false;
+
+	// Nonce check.
+	if ( !check_admin_referer( 'groups_join_group' ) )
+		return false;
+
+	$bp = buddypress();
+
+	// Skip if banned or already a member.
+	if ( !groups_is_user_member( bp_loggedin_user_id(), $bp->groups->current_group->id ) && !groups_is_user_banned( bp_loggedin_user_id(), $bp->groups->current_group->id ) ) {
+
+		// User wants to join a group that requires an invitation to join.
+		if ( ! bp_current_user_can( 'groups_join_group', array( 'group_id' => $bp->groups->current_group->id ) ) ) {
+			if ( !groups_check_user_has_invite( bp_loggedin_user_id(), $bp->groups->current_group->id ) ) {
+				bp_core_add_message( __( 'There was an error joining the group.', 'buddypress' ), 'error' );
+				bp_core_redirect( bp_get_group_permalink( $bp->groups->current_group ) );
+			}
+		}
+
+		// User wants to join any group.
+		if ( !groups_join_group( $bp->groups->current_group->id ) )
+			bp_core_add_message( __( 'There was an error joining the group.', 'buddypress' ), 'error' );
+		else
+			bp_core_add_message( __( 'You joined the group!', 'buddypress' ) );
+
+		bp_core_redirect( bp_get_group_permalink( $bp->groups->current_group ) );
+	}
+
+	/**
+	 * Filters the template to load for the single group screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to the single group template to load.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_home', 'groups/single/home' ) );
+}
+add_action( 'bp_actions', 'groups_action_join_group' );
Index: /tags/4.0.0-beta1/src/bp-groups/actions/leave-group.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/actions/leave-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/actions/leave-group.php	(revision 12265)
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Groups: Leave action
+ *
+ * @package BuddyPress
+ * @subpackage GroupActions
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and process "Leave Group" button clicks.
+ *
+ * When a group member clicks on the "Leave Group" button from a group's page,
+ * this function is run.
+ *
+ * Note: When leaving a group from the group directory, AJAX is used and
+ * another function handles this. See {@link bp_legacy_theme_ajax_joinleave_group()}.
+ *
+ * @since 1.2.4
+ *
+ * @return bool
+ */
+function groups_action_leave_group() {
+	if ( ! bp_is_single_item() || ! bp_is_groups_component() || ! bp_is_current_action( 'leave-group' ) ) {
+		return false;
+	}
+
+	// Nonce check.
+	if ( ! check_admin_referer( 'groups_leave_group' ) ) {
+		return false;
+	}
+
+	// User wants to leave any group.
+	if ( groups_is_user_member( bp_loggedin_user_id(), bp_get_current_group_id() ) ) {
+		$bp = buddypress();
+
+		// Stop sole admins from abandoning their group.
+		$group_admins = groups_get_group_admins( bp_get_current_group_id() );
+
+		if ( 1 == count( $group_admins ) && $group_admins[0]->user_id == bp_loggedin_user_id() ) {
+			bp_core_add_message( __( 'This group must have at least one admin', 'buddypress' ), 'error' );
+		} elseif ( ! groups_leave_group( $bp->groups->current_group->id ) ) {
+			bp_core_add_message( __( 'There was an error leaving the group.', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'You successfully left the group.', 'buddypress' ) );
+		}
+
+		$group = groups_get_current_group();
+		$redirect = bp_get_group_permalink( $group );
+
+		if ( ! $group->is_visible ) {
+			$redirect = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() );
+		}
+
+		bp_core_redirect( $redirect );
+	}
+
+	/** This filter is documented in bp-groups/bp-groups-actions.php */
+	bp_core_load_template( apply_filters( 'groups_template_group_home', 'groups/single/home' ) );
+}
+add_action( 'bp_actions', 'groups_action_leave_group' );
Index: /tags/4.0.0-beta1/src/bp-groups/actions/random.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/actions/random.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/actions/random.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Groups: Random group action handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupActions
+ * @since 3.0.0
+ */
+
+/**
+ * Catch requests for a random group page (example.com/groups/?random-group) and redirect.
+ *
+ * @since 1.2.0
+ */
+function groups_action_redirect_to_random_group() {
+
+	if ( bp_is_groups_component() && isset( $_GET['random-group'] ) ) {
+		$group = BP_Groups_Group::get_random( 1, 1 );
+
+		bp_core_redirect( trailingslashit( bp_get_groups_directory_permalink() . $group['groups'][0]->slug ) );
+	}
+}
+add_action( 'bp_actions', 'groups_action_redirect_to_random_group' );
Index: /tags/4.0.0-beta1/src/bp-groups/admin/css/admin-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/admin/css/admin-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/admin/css/admin-rtl.css	(revision 12265)
@@ -0,0 +1,148 @@
+body.toplevel_page_bp-groups table.groups th#status,
+body.toplevel_page_bp-groups table.groups th#members {
+	width: 10%;
+}
+
+body.toplevel_page_bp-groups table.groups th#last_active {
+	width: 15%;
+}
+
+#bp-groups-form .avatar {
+	float: right;
+	margin-left: 10px;
+	margin-top: 1px;
+}
+
+#bp-groups-edit-form input {
+	outline: medium none;
+}
+
+#bp-groups-edit-form input#bp-groups-name {
+	font-size: 1.7em;
+	width: 100%;
+	margin-bottom: 6px;
+}
+
+#bp-groups-edit-form input#bp-groups-new-members {
+	border: 1px solid #e5e5e5;
+	margin-top: 6px;
+	width: 100%;
+}
+
+#bp-groups-new-members-list {
+	margin: 0;
+}
+
+.bp-groups-settings-section {
+	margin-top: 10px;
+	line-height: 2;
+}
+
+.bp-groups-settings-section legend {
+	margin-top: 10px;
+	font-weight: 700;
+}
+
+.bp-groups-settings-section label {
+	clear: right;
+	display: block;
+	float: right;
+	vertical-align: middle;
+}
+
+#bp-groups-permalink-box {
+	line-height: 24px;
+	color: #666;
+}
+
+#bp-groups-permalink {
+	margin-left: 24px;
+}
+
+.bp-groups-member-type {
+	position: relative;
+}
+
+.bp-groups-member-type > h4 {
+	margin-bottom: 0.5em;
+}
+
+ul.bp-group-delete-list {
+	list-style-type: disc;
+	margin: 4px 26px;
+}
+
+/*
+ * Members Pagination
+ */
+.bp-group-admin-pagination {
+	position: absolute;
+	text-align: left;
+	width: 100%;
+}
+
+.bp-group-admin-pagination.table-top {
+	top: 0;
+}
+
+.bp-group-admin-pagination.table-bottom {
+	bottom: 0;
+}
+
+.bp-group-admin-pagination-viewing {
+	color: #777;
+	font-size: 12px;
+	font-style: italic;
+}
+
+.bp-group-admin-pagination-links {
+	white-space: nowrap;
+	padding-right: 15px;
+}
+
+.bp-group-admin-pagination-links .page-numbers {
+	display: inline-block;
+	min-width: 12px;
+	border: 1px solid #ccc;
+	padding: 2px 4px 4px;
+	background: #e5e5e5;
+	line-height: 1;
+	font-weight: 400;
+	text-align: center;
+	text-decoration: none;
+}
+
+.bp-group-admin-pagination-links a.page-numbers:hover {
+	text-decoration: underline;
+}
+
+.bp-group-admin-pagination-links .page-numbers.current,
+.bp-group-admin-pagination-links .page-numbers.dots {
+	border-color: #ddd;
+	background: #f7f7f7;
+	color: #a0a5aa;
+}
+
+/*
+ * Members List
+ */
+table.bp-group-members .uid-column {
+	padding-right: 20px;
+	padding-left: 20px;
+}
+
+table.bp-group-members .uname-column {
+	width: 70%;
+}
+
+table.bp-group-members .urole-column {
+	padding-right: 20px;
+	padding-left: 20px;
+}
+
+@media screen and (max-width: 782px) {
+
+	.bp-groups-settings-section label {
+		margin: 0.25em 0;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/admin/css/admin.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/admin/css/admin.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/admin/css/admin.css	(revision 12265)
@@ -0,0 +1,148 @@
+body.toplevel_page_bp-groups table.groups th#status,
+body.toplevel_page_bp-groups table.groups th#members {
+	width: 10%;
+}
+
+body.toplevel_page_bp-groups table.groups th#last_active {
+	width: 15%;
+}
+
+#bp-groups-form .avatar {
+	float: left;
+	margin-right: 10px;
+	margin-top: 1px;
+}
+
+#bp-groups-edit-form input {
+	outline: medium none;
+}
+
+#bp-groups-edit-form input#bp-groups-name {
+	font-size: 1.7em;
+	width: 100%;
+	margin-bottom: 6px;
+}
+
+#bp-groups-edit-form input#bp-groups-new-members {
+	border: 1px solid #e5e5e5;
+	margin-top: 6px;
+	width: 100%;
+}
+
+#bp-groups-new-members-list {
+	margin: 0;
+}
+
+.bp-groups-settings-section {
+	margin-top: 10px;
+	line-height: 2;
+}
+
+.bp-groups-settings-section legend {
+	margin-top: 10px;
+	font-weight: 700;
+}
+
+.bp-groups-settings-section label {
+	clear: left;
+	display: block;
+	float: left;
+	vertical-align: middle;
+}
+
+#bp-groups-permalink-box {
+	line-height: 24px;
+	color: #666;
+}
+
+#bp-groups-permalink {
+	margin-right: 24px;
+}
+
+.bp-groups-member-type {
+	position: relative;
+}
+
+.bp-groups-member-type > h4 {
+	margin-bottom: 0.5em;
+}
+
+ul.bp-group-delete-list {
+	list-style-type: disc;
+	margin: 4px 26px;
+}
+
+/*
+ * Members Pagination
+ */
+.bp-group-admin-pagination {
+	position: absolute;
+	text-align: right;
+	width: 100%;
+}
+
+.bp-group-admin-pagination.table-top {
+	top: 0;
+}
+
+.bp-group-admin-pagination.table-bottom {
+	bottom: 0;
+}
+
+.bp-group-admin-pagination-viewing {
+	color: #777;
+	font-size: 12px;
+	font-style: italic;
+}
+
+.bp-group-admin-pagination-links {
+	white-space: nowrap;
+	padding-left: 15px;
+}
+
+.bp-group-admin-pagination-links .page-numbers {
+	display: inline-block;
+	min-width: 12px;
+	border: 1px solid #ccc;
+	padding: 2px 4px 4px;
+	background: #e5e5e5;
+	line-height: 1;
+	font-weight: 400;
+	text-align: center;
+	text-decoration: none;
+}
+
+.bp-group-admin-pagination-links a.page-numbers:hover {
+	text-decoration: underline;
+}
+
+.bp-group-admin-pagination-links .page-numbers.current,
+.bp-group-admin-pagination-links .page-numbers.dots {
+	border-color: #ddd;
+	background: #f7f7f7;
+	color: #a0a5aa;
+}
+
+/*
+ * Members List
+ */
+table.bp-group-members .uid-column {
+	padding-left: 20px;
+	padding-right: 20px;
+}
+
+table.bp-group-members .uname-column {
+	width: 70%;
+}
+
+table.bp-group-members .urole-column {
+	padding-left: 20px;
+	padding-right: 20px;
+}
+
+@media screen and (max-width: 782px) {
+
+	.bp-groups-settings-section label {
+		margin: 0.25em 0;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/admin/js/admin.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/admin/js/admin.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/admin/js/admin.js	(revision 12265)
@@ -0,0 +1,59 @@
+/* global BP_Group_Admin, group_id, isRtl */
+
+(function($) {
+	function add_member_to_list( e, ui ) {
+		$('#bp-groups-new-members-list').append('<li data-login="' + ui.item.value + '"><a href="#" class="bp-groups-remove-new-member">x</a> ' + ui.item.label + '</li>');
+	}
+
+	var id = 'undefined' !== typeof group_id ? '&group_id=' + group_id : '';
+	$(document).ready( function() {
+		window.warn_on_leave = false;
+
+		/* Initialize autocomplete */
+		$( '.bp-suggest-user' ).autocomplete({
+			source:    ajaxurl + '?action=bp_group_admin_member_autocomplete' + id,
+			delay:     500,
+			minLength: 2,
+			position:  ( 'undefined' !== typeof isRtl && isRtl ) ? { my: 'right top', at: 'right bottom', offset: '0, -1' } : { offset: '0, -1' },
+			open:      function() { $(this).addClass('open'); },
+			close:     function() { $(this).removeClass('open'); $(this).val(''); },
+			select:    function( event, ui ) { add_member_to_list( event, ui ); }
+		});
+
+		/* Replace noscript placeholder */
+		$( '#bp-groups-new-members.bp-suggest-user' ).prop( 'placeholder', BP_Group_Admin.add_member_placeholder );
+
+		/* Remove a member on 'x' click */
+		$( '#bp_group_add_members' ).on( 'click', '.bp-groups-remove-new-member', function( e ) {
+			e.preventDefault();
+			$( e.target.parentNode ).remove();
+		} );
+
+		/* Warn before leaving unsaved changes */
+		$(document).on( 'change', 'input#bp-groups-name, input#bp-groups-description, select.bp-groups-role, #bp-groups-settings-section-status input[type="radio"]', function() {
+			window.warn_on_leave = true;
+		});
+
+		$( 'input#save' ).on( 'click', function() {
+			/* Check for users to add */
+			var users_to_add = [];
+
+			$( '#bp-groups-new-members-list li' ).each( function() {
+				users_to_add.push( $(this).data('login' ) );
+			} );
+
+			/* There are users to add, include a comma separated list of users login in the main field */
+			if ( users_to_add.length ) {
+				$( '#bp-groups-new-members' ).val( '' ).val( users_to_add.join( ', ' ) );
+			}
+
+			window.warn_on_leave = false;
+		});
+
+		window.onbeforeunload = function() {
+			if ( window.warn_on_leave ) {
+				return BP_Group_Admin.warn_on_leave;
+			}
+		};
+	});
+})(jQuery);
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-activity.php	(revision 12265)
@@ -0,0 +1,599 @@
+<?php
+/**
+ * BuddyPress Groups Activity Functions.
+ *
+ * These functions handle the recording, deleting and formatting of activity
+ * for the user and for this specific component.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsActivity
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register activity actions for the Groups component.
+ *
+ * @since 1.1.0
+ *
+ * @return false|null False on failure.
+ */
+function groups_register_activity_actions() {
+	$bp = buddypress();
+
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	bp_activity_set_action(
+		$bp->groups->id,
+		'created_group',
+		__( 'Created a group', 'buddypress' ),
+		'bp_groups_format_activity_action_created_group',
+		__( 'New Groups', 'buddypress' ),
+		array( 'activity', 'member', 'member_groups' )
+	);
+
+	bp_activity_set_action(
+		$bp->groups->id,
+		'joined_group',
+		__( 'Joined a group', 'buddypress' ),
+		'bp_groups_format_activity_action_joined_group',
+		__( 'Group Memberships', 'buddypress' ),
+		array( 'activity', 'group', 'member', 'member_groups' )
+	);
+
+	bp_activity_set_action(
+		$bp->groups->id,
+		'group_details_updated',
+		__( 'Group details edited', 'buddypress' ),
+		'bp_groups_format_activity_action_group_details_updated',
+		__( 'Group Updates', 'buddypress' ),
+		array( 'activity', 'group', 'member', 'member_groups' )
+	);
+
+	/**
+	 * Fires at end of registration of the default activity actions for the Groups component.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'groups_register_activity_actions' );
+}
+add_action( 'bp_register_activity_actions', 'groups_register_activity_actions' );
+
+/**
+ * Format 'created_group' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string
+ */
+function bp_groups_format_activity_action_created_group( $action, $activity ) {
+	$user_link = bp_core_get_userlink( $activity->user_id );
+
+	$group      = groups_get_group( $activity->item_id );
+	$group_link = '<a href="' . esc_url( bp_get_group_permalink( $group ) ) . '">' . esc_html( $group->name ) . '</a>';
+
+	$action = sprintf( __( '%1$s created the group %2$s', 'buddypress'), $user_link, $group_link );
+
+	/**
+	 * Filters the 'created_group' activity actions.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $action   The 'created_group' activity action.
+	 * @param object $activity Activity data object.
+	 */
+	return apply_filters( 'groups_activity_created_group_action', $action, $activity );
+}
+
+/**
+ * Format 'joined_group' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity data object.
+ * @return string
+ */
+function bp_groups_format_activity_action_joined_group( $action, $activity ) {
+	$user_link = bp_core_get_userlink( $activity->user_id );
+
+	$group      = groups_get_group( $activity->item_id );
+	$group_link = '<a href="' . esc_url( bp_get_group_permalink( $group ) ) . '">' . esc_html( $group->name ) . '</a>';
+
+	$action = sprintf( __( '%1$s joined the group %2$s', 'buddypress' ), $user_link, $group_link );
+
+	// Legacy filters (do not follow parameter patterns of other activity
+	// action filters, and requires apply_filters_ref_array()).
+	if ( has_filter( 'groups_activity_membership_accepted_action' ) ) {
+		$action = apply_filters_ref_array( 'groups_activity_membership_accepted_action', array( $action, $user_link, &$group ) );
+	}
+
+	// Another legacy filter.
+	if ( has_filter( 'groups_activity_accepted_invite_action' ) ) {
+		$action = apply_filters_ref_array( 'groups_activity_accepted_invite_action', array( $action, $activity->user_id, &$group ) );
+	}
+
+	/**
+	 * Filters the 'joined_group' activity actions.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   The 'joined_group' activity actions.
+	 * @param object $activity Activity data object.
+	 */
+	return apply_filters( 'bp_groups_format_activity_action_joined_group', $action, $activity );
+}
+
+/**
+ * Format 'group_details_updated' activity actions.
+ *
+ * @since 2.2.0
+ *
+ * @param  string $action   Static activity action.
+ * @param  object $activity Activity data object.
+ * @return string
+ */
+function bp_groups_format_activity_action_group_details_updated( $action, $activity ) {
+	$user_link = bp_core_get_userlink( $activity->user_id );
+
+	$group      = groups_get_group( $activity->item_id );
+	$group_link = '<a href="' . esc_url( bp_get_group_permalink( $group ) ) . '">' . esc_html( $group->name ) . '</a>';
+
+	/*
+	 * Changed group details are stored in groupmeta, keyed by the activity
+	 * timestamp. See {@link bp_groups_group_details_updated_add_activity()}.
+	 */
+	$changed = groups_get_groupmeta( $activity->item_id, 'updated_details_' . $activity->date_recorded );
+
+	// No changed details were found, so use a generic message.
+	if ( empty( $changed ) ) {
+		$action = sprintf( __( '%1$s updated details for the group %2$s', 'buddypress' ), $user_link, $group_link );
+
+	// Name and description changed - to keep things short, don't describe changes in detail.
+	} elseif ( isset( $changed['name'] ) && isset( $changed['description'] ) ) {
+		$action = sprintf( __( '%1$s changed the name and description of the group %2$s', 'buddypress' ), $user_link, $group_link );
+
+	// Name only.
+	} elseif ( ! empty( $changed['name']['old'] ) && ! empty( $changed['name']['new'] ) ) {
+		$action = sprintf( __( '%1$s changed the name of the group %2$s from "%3$s" to "%4$s"', 'buddypress' ), $user_link, $group_link, esc_html( $changed['name']['old'] ), esc_html( $changed['name']['new'] ) );
+
+	// Description only.
+	} elseif ( ! empty( $changed['description']['old'] ) && ! empty( $changed['description']['new'] ) ) {
+		$action = sprintf( __( '%1$s changed the description of the group %2$s from "%3$s" to "%4$s"', 'buddypress' ), $user_link, $group_link, esc_html( $changed['description']['old'] ), esc_html( $changed['description']['new'] ) );
+
+	} elseif ( ! empty( $changed['slug']['old'] ) && ! empty( $changed['slug']['new'] ) ) {
+		$action = sprintf( __( '%1$s changed the permalink of the group %2$s.', 'buddypress' ), $user_link, $group_link );
+
+	}
+
+	/**
+	 * Filters the 'group_details_updated' activity actions.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   The 'group_details_updated' activity actions.
+	 * @param object $activity Activity data object.
+	 */
+	return apply_filters( 'bp_groups_format_activity_action_joined_group', $action, $activity );
+}
+
+/**
+ * Fetch data related to groups at the beginning of an activity loop.
+ *
+ * This reduces database overhead during the activity loop.
+ *
+ * @since 2.0.0
+ *
+ * @param array $activities Array of activity items.
+ * @return array
+ */
+function bp_groups_prefetch_activity_object_data( $activities ) {
+	$group_ids = array();
+
+	if ( empty( $activities ) ) {
+		return $activities;
+	}
+
+	foreach ( $activities as $activity ) {
+		if ( buddypress()->groups->id !== $activity->component ) {
+			continue;
+		}
+
+		$group_ids[] = $activity->item_id;
+	}
+
+	if ( ! empty( $group_ids ) ) {
+
+		// TEMPORARY - Once the 'populate_extras' issue is solved
+		// in the groups component, we can do this with groups_get_groups()
+		// rather than manually.
+		$uncached_ids = array();
+		foreach ( $group_ids as $group_id ) {
+			if ( false === wp_cache_get( $group_id, 'bp_groups' ) ) {
+				$uncached_ids[] = $group_id;
+			}
+		}
+
+		if ( ! empty( $uncached_ids ) ) {
+			global $wpdb;
+			$bp = buddypress();
+			$uncached_ids_sql = implode( ',', wp_parse_id_list( $uncached_ids ) );
+			$groups = $wpdb->get_results( "SELECT * FROM {$bp->groups->table_name} WHERE id IN ({$uncached_ids_sql})" );
+			foreach ( $groups as $group ) {
+				wp_cache_set( $group->id, $group, 'bp_groups' );
+			}
+		}
+	}
+
+	return $activities;
+}
+add_filter( 'bp_activity_prefetch_object_data', 'bp_groups_prefetch_activity_object_data' );
+
+/**
+ * Set up activity arguments for use with the 'groups' scope.
+ *
+ * @since 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 = array(), $filter = array() ) {
+
+	// Determine the user_id.
+	if ( ! empty( $filter['user_id'] ) ) {
+		$user_id = $filter['user_id'];
+	} else {
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+	}
+
+	// Determine groups of user.
+	$groups = groups_get_user_groups( $user_id );
+	if ( empty( $groups['groups'] ) ) {
+		$groups = array( 'groups' => 0 );
+	}
+
+	// Should we show all items regardless of sitewide visibility?
+	$show_hidden = array();
+	if ( ! empty( $user_id ) && ( $user_id !== bp_loggedin_user_id() ) ) {
+		$show_hidden = array(
+			'column' => 'hide_sitewide',
+			'value'  => 0
+		);
+	}
+
+	$retval = array(
+		'relation' => 'AND',
+		array(
+			'relation' => 'AND',
+			array(
+				'column' => 'component',
+				'value'  => buddypress()->groups->id
+			),
+			array(
+				'column'  => 'item_id',
+				'compare' => 'IN',
+				'value'   => (array) $groups['groups']
+			),
+		),
+		$show_hidden,
+
+		// Overrides.
+		'override' => array(
+			'filter'      => array( 'user_id' => 0 ),
+			'show_hidden' => true
+		),
+	);
+
+	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
+ * defaults.
+ *
+ * @since 1.0.0
+ *
+ * @see bp_activity_add() for more detailed description of parameters and
+ *      return values.
+ *
+ * @param array|string $args {
+ *     An array of arguments for the new activity item. Accepts all parameters
+ *     of {@link bp_activity_add()}. However, this wrapper provides some
+ *     additional defaults, as described below:
+ *     @type string $component     Default: the id of your Groups component
+ *                                 (usually 'groups').
+ *     @type bool   $hide_sitewide Default: True if the current group is not
+ *                                 public, otherwise false.
+ * }
+ * @return WP_Error|bool|int See {@link bp_activity_add()}.
+ */
+function groups_record_activity( $args = '' ) {
+
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	// Set the default for hide_sitewide by checking the status of the group.
+	$hide_sitewide = false;
+	if ( !empty( $args['item_id'] ) ) {
+		if ( bp_get_current_group_id() == $args['item_id'] ) {
+			$group = groups_get_current_group();
+		} else {
+			$group = groups_get_group( $args['item_id'] );
+		}
+
+		if ( isset( $group->status ) && 'public' != $group->status ) {
+			$hide_sitewide = true;
+		}
+	}
+
+	$r = bp_parse_args( $args, array(
+		'id'                => false,
+		'user_id'           => bp_loggedin_user_id(),
+		'action'            => '',
+		'content'           => '',
+		'primary_link'      => '',
+		'component'         => buddypress()->groups->id,
+		'type'              => false,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'recorded_time'     => bp_core_current_time(),
+		'hide_sitewide'     => $hide_sitewide,
+		'error_type'        => 'bool'
+	), 'groups_record_activity' );
+
+	return bp_activity_add( $r );
+}
+
+/**
+ * Function used to determine if a user can comment on a group activity item.
+ *
+ * Used as a filter callback to 'bp_activity_can_comment'.
+ *
+ * @since 3.0.0
+ *
+ * @param  bool                      $retval   True if item can receive comments.
+ * @param  null|BP_Activity_Activity $activity Null by default. Pass an activity object to check against that instead.
+ * @return bool
+ */
+function bp_groups_filter_activity_can_comment( $retval, $activity = null ) {
+	// Bail if item cannot receive comments or if no current user.
+	if ( empty( $retval ) || ! is_user_logged_in() ) {
+		return $retval;
+	}
+
+	// Use passed activity object, if available.
+	if ( is_a( $activity, 'BP_Activity_Activity' ) ) {
+		$component = $activity->component;
+		$group_id  = $activity->item_id;
+
+	// Use activity info from current activity item in the loop.
+	} else {
+		$component = bp_get_activity_object_name();
+		$group_id  = bp_get_activity_item_id();
+	}
+
+	// If not a group activity item, bail.
+	if ( 'groups' !== $component ) {
+		return $retval;
+	}
+
+	// If current user is not a group member or is banned, user cannot comment.
+	if ( ! bp_current_user_can( 'bp_moderate' ) &&
+		( ! groups_is_user_member( bp_loggedin_user_id(), $group_id ) || groups_is_user_banned( bp_loggedin_user_id(), $group_id ) )
+	) {
+		$retval = false;
+	}
+
+	return $retval;
+}
+add_filter( 'bp_activity_can_comment', 'bp_groups_filter_activity_can_comment', 99, 1 );
+
+/**
+ * Function used to determine if a user can reply on a group activity comment.
+ *
+ * Used as a filter callback to 'bp_activity_can_comment_reply'.
+ *
+ * @since 3.0.0
+ *
+ * @param  bool        $retval  True if activity comment can be replied to.
+ * @param  object|bool $comment Current activity comment object. If empty, parameter is boolean false.
+ * @return bool
+ */
+function bp_groups_filter_activity_can_comment_reply( $retval, $comment ) {
+	// Bail if no current user, if comment is empty or if retval is already empty.
+	if ( ! is_user_logged_in() || empty( $comment ) || empty( $retval ) ) {
+		return $retval;
+	}
+
+	// Grab parent activity item.
+	$parent = new BP_Activity_Activity( $comment->item_id );
+
+	// Check to see if user can reply to parent group activity item.
+	return bp_groups_filter_activity_can_comment( $retval, $parent );
+}
+add_filter( 'bp_activity_can_comment_reply', 'bp_groups_filter_activity_can_comment_reply', 99, 2 );
+
+/**
+ * Add an activity stream item when a member joins a group.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id  ID of the user joining the group.
+ * @param int $group_id ID of the group.
+ * @return false|null False on failure.
+ */
+function bp_groups_membership_accepted_add_activity( $user_id, $group_id ) {
+
+	// Bail if Activity is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	// Get the group so we can get it's name.
+	$group = groups_get_group( $group_id );
+
+	/**
+	 * Filters the 'membership_accepted' activity actions.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $value    The 'membership_accepted' activity action.
+	 * @param int    $user_id  ID of the user joining the group.
+	 * @param int    $group_id ID of the group. Passed by reference.
+	 */
+	$action = apply_filters_ref_array( 'groups_activity_membership_accepted_action', array( sprintf( __( '%1$s joined the group %2$s', 'buddypress' ), bp_core_get_userlink( $user_id ), '<a href="' . bp_get_group_permalink( $group ) . '">' . esc_attr( $group->name ) . '</a>' ), $user_id, &$group ) );
+
+	// Record in activity streams.
+	groups_record_activity( array(
+		'action'  => $action,
+		'type'    => 'joined_group',
+		'item_id' => $group_id,
+		'user_id' => $user_id
+	) );
+}
+add_action( 'groups_membership_accepted', 'bp_groups_membership_accepted_add_activity', 10, 2 );
+
+/**
+ * Add an activity item when a group's details are updated.
+ *
+ * @since 2.2.0
+ *
+ * @param  int             $group_id       ID of the group.
+ * @param  BP_Groups_Group $old_group      Group object before the details had been changed.
+ * @param  bool            $notify_members True if the admin has opted to notify group members, otherwise false.
+ * @return null|WP_Error|bool|int The ID of the activity on success. False on error.
+ */
+function bp_groups_group_details_updated_add_activity( $group_id, $old_group, $notify_members ) {
+
+	// Bail if Activity is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	if ( ! isset( $old_group->name ) || ! isset( $old_group->slug ) || ! isset( $old_group->description ) ) {
+		return false;
+	}
+
+	// If the admin has opted not to notify members, don't post an activity item either.
+	if ( empty( $notify_members ) ) {
+		return;
+	}
+
+	$group = groups_get_group( array(
+		'group_id' => $group_id,
+	) );
+
+	/*
+	 * Store the changed data, which will be used to generate the activity
+	 * action. Since we haven't yet created the activity item, we store the
+	 * old group data in groupmeta, keyed by the timestamp that we'll put
+	 * on the activity item.
+	 */
+	$changed = array();
+
+	if ( $group->name !== $old_group->name ) {
+		$changed['name'] = array(
+			'old' => $old_group->name,
+			'new' => $group->name,
+		);
+	}
+
+	if ( $group->slug !== $old_group->slug ) {
+		$changed['slug'] = array(
+			'old' => $old_group->slug,
+			'new' => $group->slug,
+		);
+	}
+
+	if ( $group->description !== $old_group->description ) {
+		$changed['description'] = array(
+			'old' => $old_group->description,
+			'new' => $group->description,
+		);
+	}
+
+	// If there are no changes, don't post an activity item.
+	if ( empty( $changed ) ) {
+		return;
+	}
+
+	$time = bp_core_current_time();
+	groups_update_groupmeta( $group_id, 'updated_details_' . $time, $changed );
+
+	// Record in activity streams.
+	return groups_record_activity( array(
+		'type'          => 'group_details_updated',
+		'item_id'       => $group_id,
+		'user_id'       => bp_loggedin_user_id(),
+		'recorded_time' => $time,
+
+	) );
+
+}
+add_action( 'groups_details_updated', 'bp_groups_group_details_updated_add_activity', 10, 3 );
+
+/**
+ * Delete all activity items related to a specific group.
+ *
+ * @since 1.9.0
+ *
+ * @param int $group_id ID of the group.
+ */
+function bp_groups_delete_group_delete_all_activity( $group_id ) {
+	if ( bp_is_active( 'activity' ) ) {
+		bp_activity_delete_by_item_id( array(
+			'item_id'   => $group_id,
+			'component' => buddypress()->groups->id
+		) );
+	}
+}
+add_action( 'groups_delete_group', 'bp_groups_delete_group_delete_all_activity', 10 );
+
+/**
+ * Delete group member activity if they leave or are removed within 5 minutes of membership modification.
+ *
+ * If the user joined this group less than five minutes ago, remove the
+ * joined_group activity so users cannot flood the activity stream by
+ * joining/leaving the group in quick succession.
+ *
+ * @since 1.9.0
+ *
+ * @param int $group_id ID of the group.
+ * @param int $user_id  ID of the user leaving the group.
+ */
+function bp_groups_leave_group_delete_recent_activity( $group_id, $user_id ) {
+
+	// Bail if Activity component is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return;
+	}
+
+	// Get the member's group membership information.
+	$membership = new BP_Groups_Member( $user_id, $group_id );
+
+	// Check the time period, and maybe delete their recent group activity.
+	if ( time() <= strtotime( '+5 minutes', (int) strtotime( $membership->date_modified ) ) ) {
+		bp_activity_delete( array(
+			'component' => buddypress()->groups->id,
+			'type'      => 'joined_group',
+			'user_id'   => $user_id,
+			'item_id'   => $group_id
+		) );
+	}
+}
+add_action( 'groups_leave_group',   'bp_groups_leave_group_delete_recent_activity', 10, 2 );
+add_action( 'groups_remove_member', 'bp_groups_leave_group_delete_recent_activity', 10, 2 );
+add_action( 'groups_ban_member',    'bp_groups_leave_group_delete_recent_activity', 10, 2 );
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-admin.php	(revision 12265)
@@ -0,0 +1,1386 @@
+<?php
+/**
+ * BuddyPress Groups component admin screen.
+ *
+ * Props to WordPress core for the Comments admin screen, and its contextual
+ * help text, on which this implementation is heavily based.
+ *
+ * @package BuddyPress
+ * @subpackage Groups
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+// Include WP's list table class.
+if ( !class_exists( 'WP_List_Table' ) ) require( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
+
+// The per_page screen option. Has to be hooked in extremely early.
+if ( is_admin() && ! empty( $_REQUEST['page'] ) && 'bp-groups' == $_REQUEST['page'] )
+	add_filter( 'set-screen-option', 'bp_groups_admin_screen_options', 10, 3 );
+
+/**
+ * Register the Groups component admin screen.
+ *
+ * @since 1.7.0
+ */
+function bp_groups_add_admin_menu() {
+
+	// Add our screen.
+	$hook = add_menu_page(
+		_x( 'Groups', 'Admin Groups page title', 'buddypress' ),
+		_x( 'Groups', 'Admin Groups menu', 'buddypress' ),
+		'bp_moderate',
+		'bp-groups',
+		'bp_groups_admin',
+		'div'
+	);
+
+	// Hook into early actions to load custom CSS and our init handler.
+	add_action( "load-$hook", 'bp_groups_admin_load' );
+}
+add_action( bp_core_admin_hook(), 'bp_groups_add_admin_menu' );
+
+/**
+ * Add groups component to custom menus array.
+ *
+ * This ensures that the Groups menu item appears in the proper order on the
+ * main Dashboard menu.
+ *
+ * @since 1.7.0
+ *
+ * @param array $custom_menus Array of BP top-level menu items.
+ * @return array Menu item array, with Groups added.
+ */
+function bp_groups_admin_menu_order( $custom_menus = array() ) {
+	array_push( $custom_menus, 'bp-groups' );
+	return $custom_menus;
+}
+add_filter( 'bp_admin_menu_order', 'bp_groups_admin_menu_order' );
+
+/**
+ * Set up the Groups admin page.
+ *
+ * Loaded before the page is rendered, this function does all initial setup,
+ * including: processing form requests, registering contextual help, and
+ * setting up screen options.
+ *
+ * @since 1.7.0
+ *
+ * @global BP_Groups_List_Table $bp_groups_list_table Groups screen list table.
+ */
+function bp_groups_admin_load() {
+	global $bp_groups_list_table;
+
+	// Build redirection URL.
+	$redirect_to = remove_query_arg( array( 'action', 'action2', 'gid', 'deleted', 'error', 'updated', 'success_new', 'error_new', 'success_modified', 'error_modified' ), $_SERVER['REQUEST_URI'] );
+
+	$doaction   = bp_admin_list_table_current_bulk_action();
+	$min        = bp_core_get_minified_asset_suffix();
+
+	/**
+	 * Fires at top of groups admin page.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $doaction Current $_GET action being performed in admin screen.
+	 */
+	do_action( 'bp_groups_admin_load', $doaction );
+
+	// Edit screen.
+	if ( 'do_delete' == $doaction && ! empty( $_GET['gid'] ) ) {
+
+		check_admin_referer( 'bp-groups-delete' );
+
+		$group_ids = wp_parse_id_list( $_GET['gid'] );
+
+		$count = 0;
+		foreach ( $group_ids as $group_id ) {
+			if ( groups_delete_group( $group_id ) ) {
+				$count++;
+			}
+		}
+
+		$redirect_to = add_query_arg( 'deleted', $count, $redirect_to );
+
+		bp_core_redirect( $redirect_to );
+
+	} elseif ( 'edit' == $doaction && ! empty( $_GET['gid'] ) ) {
+		// Columns screen option.
+		add_screen_option( 'layout_columns', array( 'default' => 2, 'max' => 2, ) );
+
+		get_current_screen()->add_help_tab( array(
+			'id'      => 'bp-group-edit-overview',
+			'title'   => __( 'Overview', 'buddypress' ),
+			'content' =>
+				'<p>' . __( 'This page is a convenient way to edit the details associated with one of your groups.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'The Name and Description box is fixed in place, but you can reposition all the other boxes using drag and drop, and can minimize or expand them by clicking the title bar of each box. Use the Screen Options tab to hide or unhide, or to choose a 1- or 2-column layout for this screen.', 'buddypress' ) . '</p>'
+		) );
+
+		// Help panel - sidebar links.
+		get_current_screen()->set_help_sidebar(
+			'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+			'<p><a href="https://buddypress.org/support">' . __( 'Support Forums', 'buddypress' ) . '</a></p>'
+		);
+
+		// Register metaboxes for the edit screen.
+		add_meta_box( 'submitdiv', _x( 'Save', 'group admin edit screen', 'buddypress' ), 'bp_groups_admin_edit_metabox_status', get_current_screen()->id, 'side', 'high' );
+		add_meta_box( 'bp_group_settings', _x( 'Settings', 'group admin edit screen', 'buddypress' ), 'bp_groups_admin_edit_metabox_settings', get_current_screen()->id, 'side', 'core' );
+		add_meta_box( 'bp_group_add_members', _x( 'Add New Members', 'group admin edit screen', 'buddypress' ), 'bp_groups_admin_edit_metabox_add_new_members', get_current_screen()->id, 'normal', 'core' );
+		add_meta_box( 'bp_group_members', _x( 'Manage Members', 'group admin edit screen', 'buddypress' ), 'bp_groups_admin_edit_metabox_members', get_current_screen()->id, 'normal', 'core' );
+
+		// Group Type metabox. Only added if group types have been registered.
+		$group_types = bp_groups_get_group_types();
+		if ( ! empty( $group_types ) ) {
+			add_meta_box(
+				'bp_groups_admin_group_type',
+				_x( 'Group Type', 'groups admin edit screen', 'buddypress' ),
+				'bp_groups_admin_edit_metabox_group_type',
+				get_current_screen()->id,
+				'side',
+				'core'
+			);
+		}
+
+		/**
+		 * Fires after the registration of all of the default group meta boxes.
+		 *
+		 * @since 1.7.0
+		 */
+		do_action( 'bp_groups_admin_meta_boxes' );
+
+		// Enqueue JavaScript files.
+		wp_enqueue_script( 'postbox' );
+		wp_enqueue_script( 'dashboard' );
+
+	// Index screen.
+	} else {
+		// Create the Groups screen list table.
+		$bp_groups_list_table = new BP_Groups_List_Table();
+
+		// The per_page screen option.
+		add_screen_option( 'per_page', array( 'label' => _x( 'Groups', 'Groups per page (screen options)', 'buddypress' )) );
+
+		// Help panel - overview text.
+		get_current_screen()->add_help_tab( array(
+			'id'      => 'bp-groups-overview',
+			'title'   => __( 'Overview', 'buddypress' ),
+			'content' =>
+				'<p>' . __( 'You can manage groups much like you can manage comments and other content. This screen is customizable in the same ways as other management screens, and you can act on groups by using the on-hover action links or the Bulk Actions.', 'buddypress' ) . '</p>',
+		) );
+
+		get_current_screen()->add_help_tab( array(
+			'id'      => 'bp-groups-overview-actions',
+			'title'   => __( 'Group Actions', 'buddypress' ),
+			'content' =>
+				'<p>' . __( 'Clicking "Visit" will take you to the group&#8217;s public page. Use this link to see what the group looks like on the front end of your site.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'Clicking "Edit" will take you to a Dashboard panel where you can manage various details about the group, such as its name and description, its members, and other settings.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'If you click "Delete" under a specific group, or select a number of groups and then choose Delete from the Bulk Actions menu, you will be led to a page where you&#8217;ll be asked to confirm the permanent deletion of the group(s).', 'buddypress' ) . '</p>',
+		) );
+
+		// Help panel - sidebar links.
+		get_current_screen()->set_help_sidebar(
+			'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+			'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+		);
+
+		// Add accessible hidden heading and text for Groups screen pagination.
+		get_current_screen()->set_screen_reader_content( array(
+			/* translators: accessibility text */
+			'heading_pagination' => __( 'Groups list navigation', 'buddypress' ),
+		) );
+	}
+
+	$bp = buddypress();
+
+	// Enqueue CSS and JavaScript.
+	wp_enqueue_script( 'bp_groups_admin_js', $bp->plugin_url . "bp-groups/admin/js/admin{$min}.js", array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), bp_get_version(), true );
+	wp_localize_script( 'bp_groups_admin_js', 'BP_Group_Admin', array(
+		'add_member_placeholder' => __( 'Start typing a username to add a new member.', 'buddypress' ),
+		'warn_on_leave'          => __( 'If you leave this page, you will lose any unsaved changes you have made to the group.', 'buddypress' ),
+	) );
+	wp_enqueue_style( 'bp_groups_admin_css', $bp->plugin_url . "bp-groups/admin/css/admin{$min}.css", array(), bp_get_version() );
+
+	wp_style_add_data( 'bp_groups_admin_css', 'rtl', true );
+	if ( $min ) {
+		wp_style_add_data( 'bp_groups_admin_css', 'suffix', $min );
+	}
+
+
+	if ( $doaction && 'save' == $doaction ) {
+		// Get group ID.
+		$group_id = isset( $_REQUEST['gid'] ) ? (int) $_REQUEST['gid'] : '';
+
+		$redirect_to = add_query_arg( array(
+			'gid'    => (int) $group_id,
+			'action' => 'edit'
+		), $redirect_to );
+
+		// Check this is a valid form submission.
+		check_admin_referer( 'edit-group_' . $group_id );
+
+		// Get the group from the database.
+		$group = groups_get_group( $group_id );
+
+		// If the group doesn't exist, just redirect back to the index.
+		if ( empty( $group->slug ) ) {
+			wp_redirect( $redirect_to );
+			exit;
+		}
+
+		// Check the form for the updated properties.
+		// Store errors.
+		$error = 0;
+		$success_new = $error_new = $success_modified = $error_modified = array();
+
+		// Name, description and slug must not be empty.
+		if ( empty( $_POST['bp-groups-name'] ) ) {
+			$error = $error - 1;
+		}
+		if ( empty( $_POST['bp-groups-description'] ) ) {
+			$error = $error - 2;
+		}
+		if ( empty( $_POST['bp-groups-slug'] ) ) {
+			$error = $error - 4;
+		}
+
+		/*
+		 * Group name, slug, and description are handled with
+		 * groups_edit_base_group_details().
+		 */
+		if ( ! $error && ! groups_edit_base_group_details( array(
+				'group_id'       => $group_id,
+				'name'           => $_POST['bp-groups-name'],
+				'slug'           => $_POST['bp-groups-slug'],
+				'description'    => $_POST['bp-groups-description'],
+				'notify_members' => false,
+			) ) ) {
+			$error = $group_id;
+		}
+
+		// Enable discussion forum.
+		$enable_forum   = ( isset( $_POST['group-show-forum'] ) ) ? 1 : 0;
+
+		/**
+		 * Filters the allowed status values for the group.
+		 *
+		 * @since 1.0.2
+		 *
+		 * @param array $value Array of allowed group statuses.
+		 */
+		$allowed_status = apply_filters( 'groups_allowed_status', array( 'public', 'private', 'hidden' ) );
+		$status         = ( in_array( $_POST['group-status'], (array) $allowed_status ) ) ? $_POST['group-status'] : 'public';
+
+		/**
+		 * Filters the allowed invite status values for the group.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param array $value Array of allowed invite statuses.
+		 */
+		$allowed_invite_status = apply_filters( 'groups_allowed_invite_status', array( 'members', 'mods', 'admins' ) );
+		$invite_status	       = in_array( $_POST['group-invite-status'], (array) $allowed_invite_status ) ? $_POST['group-invite-status'] : 'members';
+
+		if ( !groups_edit_group_settings( $group_id, $enable_forum, $status, $invite_status ) ) {
+			$error = $group_id;
+		}
+
+		// Process new members.
+		$user_names = array();
+
+		if ( ! empty( $_POST['bp-groups-new-members'] ) ) {
+			$user_names = array_merge( $user_names, explode( ',', $_POST['bp-groups-new-members'] ) );
+		}
+
+		if ( ! empty( $user_names ) ) {
+
+			foreach( array_values( $user_names ) as $user_name ) {
+				$un = trim( $user_name );
+
+				// Make sure the user exists before attempting
+				// to add to the group.
+				$user = get_user_by( 'slug', $un );
+
+				if ( empty( $user ) ) {
+					$error_new[] = $un;
+				} else {
+					if ( ! groups_join_group( $group_id, $user->ID ) ) {
+						$error_new[]   = $un;
+					} else {
+						$success_new[] = $un;
+					}
+				}
+			}
+		}
+
+		// Process member role changes.
+		if ( ! empty( $_POST['bp-groups-role'] ) && ! empty( $_POST['bp-groups-existing-role'] ) ) {
+
+			// Before processing anything, make sure you're not
+			// attempting to remove the all user admins.
+			$admin_count = 0;
+			foreach ( (array) $_POST['bp-groups-role'] as $new_role ) {
+				if ( 'admin' == $new_role ) {
+					$admin_count++;
+					break;
+				}
+			}
+
+			if ( ! $admin_count ) {
+
+				$redirect_to = add_query_arg( 'no_admins', 1, $redirect_to );
+				$error = $group_id;
+
+			} else {
+
+				// Process only those users who have had their roles changed.
+				foreach ( (array) $_POST['bp-groups-role'] as $user_id => $new_role ) {
+					$user_id = (int) $user_id;
+
+					$existing_role = isset( $_POST['bp-groups-existing-role'][$user_id] ) ? $_POST['bp-groups-existing-role'][$user_id] : '';
+
+					if ( $existing_role != $new_role ) {
+						$result = false;
+
+						switch ( $new_role ) {
+							case 'mod' :
+								// Admin to mod is a demotion. Demote to
+								// member, then fall through.
+								if ( 'admin' == $existing_role ) {
+									groups_demote_member( $user_id, $group_id );
+								}
+
+							case 'admin' :
+								// If the user was banned, we must
+								// unban first.
+								if ( 'banned' == $existing_role ) {
+									groups_unban_member( $user_id, $group_id );
+								}
+
+								// At this point, each existing_role
+								// is a member, so promote.
+								$result = groups_promote_member( $user_id, $group_id, $new_role );
+
+								break;
+
+							case 'member' :
+
+								if ( 'admin' == $existing_role || 'mod' == $existing_role ) {
+									$result = groups_demote_member( $user_id, $group_id );
+								} elseif ( 'banned' == $existing_role ) {
+									$result = groups_unban_member( $user_id, $group_id );
+								}
+
+								break;
+
+							case 'banned' :
+
+								$result = groups_ban_member( $user_id, $group_id );
+
+								break;
+
+							case 'remove' :
+
+								$result = groups_remove_member( $user_id, $group_id );
+
+								break;
+						}
+
+						// Store the success or failure.
+						if ( $result ) {
+							$success_modified[] = $user_id;
+						} else {
+							$error_modified[]   = $user_id;
+						}
+					}
+				}
+			}
+		}
+
+		/**
+		 * Fires before redirect so plugins can do something first on save action.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param int $group_id ID of the group being edited.
+		 */
+		do_action( 'bp_group_admin_edit_after', $group_id );
+
+		// Create the redirect URL.
+		if ( $error ) {
+			// This means there was an error updating group details.
+			$redirect_to = add_query_arg( 'error', (int) $error, $redirect_to );
+		} else {
+			// Group details were update successfully.
+			$redirect_to = add_query_arg( 'updated', 1, $redirect_to );
+		}
+
+		if ( !empty( $success_new ) ) {
+			$success_new = implode( ',', array_filter( $success_new, 'urlencode' ) );
+			$redirect_to = add_query_arg( 'success_new', $success_new, $redirect_to );
+		}
+
+		if ( !empty( $error_new ) ) {
+			$error_new = implode( ',', array_filter( $error_new, 'urlencode' ) );
+			$redirect_to = add_query_arg( 'error_new', $error_new, $redirect_to );
+		}
+
+		if ( !empty( $success_modified ) ) {
+			$success_modified = implode( ',', array_filter( $success_modified, 'urlencode' ) );
+			$redirect_to = add_query_arg( 'success_modified', $success_modified, $redirect_to );
+		}
+
+		if ( !empty( $error_modified ) ) {
+			$error_modified = implode( ',', array_filter( $error_modified, 'urlencode' ) );
+			$redirect_to = add_query_arg( 'error_modified', $error_modified, $redirect_to );
+		}
+
+		/**
+		 * Filters the URL to redirect to after successfully editing a group.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $redirect_to URL to redirect user to.
+		 */
+		wp_redirect( apply_filters( 'bp_group_admin_edit_redirect', $redirect_to ) );
+		exit;
+
+
+	// If a referrer and a nonce is supplied, but no action, redirect back.
+	} elseif ( ! empty( $_GET['_wp_http_referer'] ) ) {
+		wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), stripslashes( $_SERVER['REQUEST_URI'] ) ) );
+		exit;
+	}
+}
+
+/**
+ * Handle save/update of screen options for the Groups component admin screen.
+ *
+ * @since 1.7.0
+ *
+ * @param string $value     Will always be false unless another plugin filters it first.
+ * @param string $option    Screen option name.
+ * @param string $new_value Screen option form value.
+ * @return string|int Option value. False to abandon update.
+ */
+function bp_groups_admin_screen_options( $value, $option, $new_value ) {
+	if ( 'toplevel_page_bp_groups_per_page' != $option && 'toplevel_page_bp_groups_network_per_page' != $option )
+		return $value;
+
+	// Per page.
+	$new_value = (int) $new_value;
+	if ( $new_value < 1 || $new_value > 999 )
+		return $value;
+
+	return $new_value;
+}
+
+/**
+ * Select the appropriate Groups admin screen, and output it.
+ *
+ * @since 1.7.0
+ */
+function bp_groups_admin() {
+	// Decide whether to load the index or edit screen.
+	$doaction = bp_admin_list_table_current_bulk_action();
+
+	// Display the single group edit screen.
+	if ( 'edit' == $doaction && ! empty( $_GET['gid'] ) ) {
+		bp_groups_admin_edit();
+
+	// Display the group deletion confirmation screen.
+	} elseif ( 'delete' == $doaction && ! empty( $_GET['gid'] ) ) {
+		bp_groups_admin_delete();
+
+	// Otherwise, display the groups index screen.
+	} else {
+		bp_groups_admin_index();
+	}
+}
+
+/**
+ * Display the single groups edit screen.
+ *
+ * @since 1.7.0
+ */
+function bp_groups_admin_edit() {
+
+	if ( ! bp_current_user_can( 'bp_moderate' ) )
+		die( '-1' );
+
+	$messages = array();
+
+	// If the user has just made a change to a group, build status messages.
+	if ( !empty( $_REQUEST['no_admins'] ) || ! empty( $_REQUEST['error'] ) || ! empty( $_REQUEST['updated'] ) || ! empty( $_REQUEST['error_new'] ) || ! empty( $_REQUEST['success_new'] ) || ! empty( $_REQUEST['error_modified'] ) || ! empty( $_REQUEST['success_modified'] ) ) {
+		$no_admins        = ! empty( $_REQUEST['no_admins']        ) ? 1                                             : 0;
+		$errors           = ! empty( $_REQUEST['error']            ) ? $_REQUEST['error']                            : '';
+		$updated          = ! empty( $_REQUEST['updated']          ) ? $_REQUEST['updated']                          : '';
+		$error_new        = ! empty( $_REQUEST['error_new']        ) ? explode( ',', $_REQUEST['error_new'] )        : array();
+		$success_new      = ! empty( $_REQUEST['success_new']      ) ? explode( ',', $_REQUEST['success_new'] )      : array();
+		$error_modified   = ! empty( $_REQUEST['error_modified']   ) ? explode( ',', $_REQUEST['error_modified'] )   : array();
+		$success_modified = ! empty( $_REQUEST['success_modified'] ) ? explode( ',', $_REQUEST['success_modified'] ) : array();
+
+		if ( ! empty( $no_admins ) ) {
+			$messages[] = __( 'You cannot remove all administrators from a group.', 'buddypress' );
+		}
+
+		if ( ! empty( $errors ) ) {
+			if ( $errors < 0 ) {
+				$messages[] = __( 'Group name, slug, and description are all required fields.', 'buddypress' );
+			} else {
+				$messages[] = __( 'An error occurred when trying to update your group details.', 'buddypress' );
+			}
+
+		} elseif ( ! empty( $updated ) ) {
+			$messages[] = __( 'The group has been updated successfully.', 'buddypress' );
+		}
+
+		if ( ! empty( $error_new ) ) {
+			$messages[] = sprintf( __( 'The following users could not be added to the group: %s', 'buddypress' ), '<em>' . esc_html( implode( ', ', $error_new ) ) . '</em>' );
+		}
+
+		if ( ! empty( $success_new ) ) {
+			$messages[] = sprintf( __( 'The following users were successfully added to the group: %s', 'buddypress' ), '<em>' . esc_html( implode( ', ', $success_new ) ) . '</em>' );
+		}
+
+		if ( ! empty( $error_modified ) ) {
+			$error_modified = bp_groups_admin_get_usernames_from_ids( $error_modified );
+			$messages[] = sprintf( __( 'An error occurred when trying to modify the following members: %s', 'buddypress' ), '<em>' . esc_html( implode( ', ', $error_modified ) ) . '</em>' );
+		}
+
+		if ( ! empty( $success_modified ) ) {
+			$success_modified = bp_groups_admin_get_usernames_from_ids( $success_modified );
+			$messages[] = sprintf( __( 'The following members were successfully modified: %s', 'buddypress' ), '<em>' . esc_html( implode( ', ', $success_modified ) ) . '</em>' );
+		}
+	}
+
+	$is_error = ! empty( $no_admins ) || ! empty( $errors ) || ! empty( $error_new ) || ! empty( $error_modified );
+
+	// Get the group from the database.
+	$group      = groups_get_group( (int) $_GET['gid'] );
+
+	$group_name = isset( $group->name ) ? bp_get_group_name( $group ) : '';
+
+	// Construct URL for form.
+	$form_url = remove_query_arg( array( 'action', 'deleted', 'no_admins', 'error', 'error_new', 'success_new', 'error_modified', 'success_modified' ), $_SERVER['REQUEST_URI'] );
+	$form_url = add_query_arg( 'action', 'save', $form_url );
+
+	/**
+	 * Fires before the display of the edit form.
+	 *
+	 * Useful for plugins to modify the group before display.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param BP_Groups_Group $this Instance of the current group being edited. Passed by reference.
+	 */
+	do_action_ref_array( 'bp_groups_admin_edit', array( &$group ) ); ?>
+
+	<div class="wrap">
+		<?php if ( version_compare( $GLOBALS['wp_version'], '4.8', '>=' ) ) : ?>
+
+			<h1 class="wp-heading-inline"><?php _e( 'Edit Group', 'buddypress' ); ?></h1>
+
+			<?php if ( is_user_logged_in() && bp_user_can_create_groups() ) : ?>
+				<a class="page-title-action" href="<?php echo trailingslashit( bp_get_groups_directory_permalink() . 'create' ); ?>"><?php _e( 'Add New', 'buddypress' ); ?></a>
+			<?php endif; ?>
+
+			<hr class="wp-header-end">
+
+		<?php else : ?>
+
+			<h1><?php _e( 'Edit Group', 'buddypress' ); ?>
+
+				<?php if ( is_user_logged_in() && bp_user_can_create_groups() ) : ?>
+					<a class="add-new-h2" href="<?php echo trailingslashit( bp_get_groups_directory_permalink() . 'create' ); ?>"><?php _e( 'Add New', 'buddypress' ); ?></a>
+				<?php endif; ?>
+
+			</h1>
+
+		<?php endif; ?>
+
+		<?php // If the user has just made a change to an group, display the status messages. ?>
+		<?php if ( !empty( $messages ) ) : ?>
+			<div id="moderated" class="<?php echo ( $is_error ) ? 'error' : 'updated'; ?>"><p><?php echo implode( "</p><p>", $messages ); ?></p></div>
+		<?php endif; ?>
+
+		<?php if ( $group->id ) : ?>
+
+			<form action="<?php echo esc_url( $form_url ); ?>" id="bp-groups-edit-form" method="post">
+				<div id="poststuff">
+
+					<div id="post-body" class="metabox-holder columns-<?php echo 1 == get_current_screen()->get_columns() ? '1' : '2'; ?>">
+						<div id="post-body-content">
+							<div id="postdiv">
+								<div id="bp_groups_name" class="postbox">
+									<h2><?php _e( 'Name and Description', 'buddypress' ); ?></h2>
+									<div class="inside">
+										<label for="bp-groups-name" class="screen-reader-text"><?php
+											/* translators: accessibility text */
+											_e( 'Group Name', 'buddypress' );
+										?></label>
+										<input type="text" name="bp-groups-name" id="bp-groups-name" value="<?php echo esc_attr( stripslashes( $group_name ) ) ?>" />
+										<div id="bp-groups-permalink-box">
+											<strong><?php esc_html_e( 'Permalink:', 'buddypress' ) ?></strong>
+											<span id="bp-groups-permalink">
+												<?php bp_groups_directory_permalink(); ?> <input type="text" id="bp-groups-slug" name="bp-groups-slug" value="<?php bp_group_slug( $group ); ?>" autocomplete="off"> /
+											</span>
+											<a href="<?php echo bp_group_permalink( $group ) ?>" class="button button-small" id="bp-groups-visit-group"><?php esc_html_e( 'Visit Group', 'buddypress' ) ?></a>
+										</div>
+
+										<label for="bp-groups-description" class="screen-reader-text"><?php
+											/* translators: accessibility text */
+											_e( 'Group Description', 'buddypress' );
+										?></label>
+										<?php wp_editor( stripslashes( $group->description ), 'bp-groups-description', array( 'media_buttons' => false, 'teeny' => true, 'textarea_rows' => 5, 'quicktags' => array( 'buttons' => 'strong,em,link,block,del,ins,img,code,spell,close' ) ) ); ?>
+									</div>
+								</div>
+							</div>
+						</div><!-- #post-body-content -->
+
+						<div id="postbox-container-1" class="postbox-container">
+							<?php do_meta_boxes( get_current_screen()->id, 'side', $group ); ?>
+						</div>
+
+						<div id="postbox-container-2" class="postbox-container">
+							<?php do_meta_boxes( get_current_screen()->id, 'normal', $group ); ?>
+							<?php do_meta_boxes( get_current_screen()->id, 'advanced', $group ); ?>
+						</div>
+					</div><!-- #post-body -->
+
+				</div><!-- #poststuff -->
+				<?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
+				<?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>
+				<?php wp_nonce_field( 'edit-group_' . $group->id ); ?>
+			</form>
+
+		<?php else : ?>
+
+			<p><?php
+				printf(
+					'%1$s <a href="%2$s">%3$s</a>',
+					__( 'No group found with this ID.', 'buddypress' ),
+					esc_url( bp_get_admin_url( 'admin.php?page=bp-groups' ) ),
+					__( 'Go back and try again.', 'buddypress' )
+				);
+			?></p>
+
+		<?php endif; ?>
+
+	</div><!-- .wrap -->
+
+<?php
+}
+
+/**
+ * Display the Group delete confirmation screen.
+ *
+ * We include a separate confirmation because group deletion is truly
+ * irreversible.
+ *
+ * @since 1.7.0
+ */
+function bp_groups_admin_delete() {
+
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		die( '-1' );
+	}
+
+	$group_ids = isset( $_REQUEST['gid'] ) ? $_REQUEST['gid'] : 0;
+	if ( ! is_array( $group_ids ) ) {
+		$group_ids = explode( ',', $group_ids );
+	}
+	$group_ids = wp_parse_id_list( $group_ids );
+	$groups    = groups_get_groups( array(
+		'include'     => $group_ids,
+		'show_hidden' => true,
+		'per_page'    => null, // Return all results.
+	) );
+
+	// Create a new list of group ids, based on those that actually exist.
+	$gids = array();
+	foreach ( $groups['groups'] as $group ) {
+		$gids[] = $group->id;
+	}
+
+	$base_url  = remove_query_arg( array( 'action', 'action2', 'paged', 's', '_wpnonce', 'gid' ), $_SERVER['REQUEST_URI'] ); ?>
+
+	<div class="wrap">
+		<h1><?php _e( 'Delete Groups', 'buddypress' ) ?></h1>
+		<p><?php _e( 'You are about to delete the following groups:', 'buddypress' ) ?></p>
+
+		<ul class="bp-group-delete-list">
+		<?php foreach ( $groups['groups'] as $group ) : ?>
+			<li><?php echo esc_html( bp_get_group_name( $group ) ); ?></li>
+		<?php endforeach; ?>
+		</ul>
+
+		<p><strong><?php _e( 'This action cannot be undone.', 'buddypress' ) ?></strong></p>
+
+		<a class="button-primary" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'do_delete', 'gid' => implode( ',', $gids ) ), $base_url ), 'bp-groups-delete' ) ); ?>"><?php _e( 'Delete Permanently', 'buddypress' ) ?></a>
+		<a class="button" href="<?php echo esc_attr( $base_url ); ?>"><?php _e( 'Cancel', 'buddypress' ) ?></a>
+	</div>
+
+	<?php
+}
+
+/**
+ * Display the Groups admin index screen.
+ *
+ * This screen contains a list of all BuddyPress groups.
+ *
+ * @since 1.7.0
+ *
+ * @global BP_Groups_List_Table $bp_groups_list_table Group screen list table.
+ * @global string $plugin_page Currently viewed plugin page.
+ */
+function bp_groups_admin_index() {
+	global $bp_groups_list_table, $plugin_page;
+
+	$messages = array();
+
+	// If the user has just made a change to a group, build status messages.
+	if ( ! empty( $_REQUEST['deleted'] ) ) {
+		$deleted  = ! empty( $_REQUEST['deleted'] ) ? (int) $_REQUEST['deleted'] : 0;
+
+		if ( $deleted > 0 ) {
+			$messages[] = sprintf( _n( '%s group has been permanently deleted.', '%s groups have been permanently deleted.', $deleted, 'buddypress' ), number_format_i18n( $deleted ) );
+		}
+	}
+
+	// Prepare the group items for display.
+	$bp_groups_list_table->prepare_items();
+
+	/**
+	 * Fires before the display of messages for the edit form.
+	 *
+	 * Useful for plugins to modify the messages before display.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $messages Array of messages to be displayed.
+	 */
+	do_action( 'bp_groups_admin_index', $messages ); ?>
+
+	<div class="wrap">
+		<?php if ( version_compare( $GLOBALS['wp_version'], '4.8', '>=' ) ) : ?>
+
+			<h1 class="wp-heading-inline"><?php _e( 'Groups', 'buddypress' ); ?></h1>
+
+			<?php if ( is_user_logged_in() && bp_user_can_create_groups() ) : ?>
+				<a class="page-title-action" href="<?php echo trailingslashit( bp_get_groups_directory_permalink() . 'create' ); ?>"><?php _e( 'Add New', 'buddypress' ); ?></a>
+			<?php endif; ?>
+
+			<?php if ( !empty( $_REQUEST['s'] ) ) : ?>
+				<span class="subtitle"><?php printf( __( 'Search results for &#8220;%s&#8221;', 'buddypress' ), wp_html_excerpt( esc_html( stripslashes( $_REQUEST['s'] ) ), 50 ) ); ?></span>
+			<?php endif; ?>
+
+			<hr class="wp-header-end">
+
+		<?php else : ?>
+
+		<h1>
+			<?php _e( 'Groups', 'buddypress' ); ?>
+
+			<?php if ( is_user_logged_in() && bp_user_can_create_groups() ) : ?>
+				<a class="add-new-h2" href="<?php echo trailingslashit( bp_get_groups_directory_permalink() . 'create' ); ?>"><?php _e( 'Add New', 'buddypress' ); ?></a>
+			<?php endif; ?>
+
+			<?php if ( !empty( $_REQUEST['s'] ) ) : ?>
+				<span class="subtitle"><?php printf( __( 'Search results for &#8220;%s&#8221;', 'buddypress' ), wp_html_excerpt( esc_html( stripslashes( $_REQUEST['s'] ) ), 50 ) ); ?></span>
+			<?php endif; ?>
+		</h1>
+
+		<?php endif; ?>
+
+		<?php // If the user has just made a change to an group, display the status messages. ?>
+		<?php if ( !empty( $messages ) ) : ?>
+			<div id="moderated" class="<?php echo ( ! empty( $_REQUEST['error'] ) ) ? 'error' : 'updated'; ?>"><p><?php echo implode( "<br/>\n", $messages ); ?></p></div>
+		<?php endif; ?>
+
+		<?php // Display each group on its own row. ?>
+		<?php $bp_groups_list_table->views(); ?>
+
+		<form id="bp-groups-form" action="" method="get">
+			<?php $bp_groups_list_table->search_box( __( 'Search all Groups', 'buddypress' ), 'bp-groups' ); ?>
+			<input type="hidden" name="page" value="<?php echo esc_attr( $plugin_page ); ?>" />
+			<?php $bp_groups_list_table->display(); ?>
+		</form>
+
+	</div>
+
+<?php
+}
+
+/**
+ * Markup for the single group's Settings metabox.
+ *
+ * @since 1.7.0
+ *
+ * @param object $item Information about the current group.
+ */
+function bp_groups_admin_edit_metabox_settings( $item ) {
+
+	$invite_status = bp_group_get_invite_status( $item->id ); ?>
+
+	<?php if ( bp_is_active( 'forums' ) ) : ?>
+		<div class="bp-groups-settings-section" id="bp-groups-settings-section-forum">
+			<label for="group-show-forum"><input type="checkbox" name="group-show-forum" id="group-show-forum" <?php checked( $item->enable_forum ) ?> /> <?php _e( 'Enable discussion forum', 'buddypress' ) ?></label>
+		</div>
+	<?php endif; ?>
+
+	<div class="bp-groups-settings-section" id="bp-groups-settings-section-status">
+		<fieldset>
+			<legend><?php _e( 'Privacy', 'buddypress' ); ?></legend>
+
+			<label for="bp-group-status-public"><input type="radio" name="group-status" id="bp-group-status-public" value="public" <?php checked( $item->status, 'public' ) ?> /><?php _e( 'Public', 'buddypress' ) ?></label>
+			<label for="bp-group-status-private"><input type="radio" name="group-status" id="bp-group-status-private" value="private" <?php checked( $item->status, 'private' ) ?> /><?php _e( 'Private', 'buddypress' ) ?></label>
+			<label for="bp-group-status-hidden"><input type="radio" name="group-status" id="bp-group-status-hidden" value="hidden" <?php checked( $item->status, 'hidden' ) ?> /><?php _e( 'Hidden', 'buddypress' ) ?></label>
+		</fieldset>
+	</div>
+
+	<div class="bp-groups-settings-section" id="bp-groups-settings-section-invite-status">
+		<fieldset>
+			<legend><?php _e( 'Who can invite others to this group?', 'buddypress' ); ?></legend>
+
+			<label for="bp-group-invite-status-members"><input type="radio" name="group-invite-status" id="bp-group-invite-status-members" value="members" <?php checked( $invite_status, 'members' ) ?> /><?php _e( 'All group members', 'buddypress' ) ?></label>
+			<label for="bp-group-invite-status-mods"><input type="radio" name="group-invite-status" id="bp-group-invite-status-mods" value="mods" <?php checked( $invite_status, 'mods' ) ?> /><?php _e( 'Group admins and mods only', 'buddypress' ) ?></label>
+			<label for="bp-group-invite-status-admins"><input type="radio" name="group-invite-status" id="bp-group-invite-status-admins" value="admins" <?php checked( $invite_status, 'admins' ) ?> /><?php _e( 'Group admins only', 'buddypress' ) ?></label>
+		</fieldset>
+	</div>
+
+<?php
+}
+
+/**
+ * Output the markup for a single group's Add New Members metabox.
+ *
+ * @since 1.7.0
+ *
+ * @param BP_Groups_Group $item The BP_Groups_Group object for the current group.
+ */
+function bp_groups_admin_edit_metabox_add_new_members( $item ) {
+	if ( wp_is_large_network( 'users' ) ) {
+		$class  = '';
+		$notice = __( 'Enter a comma-separated list of user logins.', 'buddypress' );
+	} else {
+		$class  = 'bp-suggest-user';
+		$notice = '';
+	}
+
+	?>
+
+	<label for="bp-groups-new-members" class="screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Add new members', 'buddypress' );
+	?></label>
+	<input name="bp-groups-new-members" type="text" id="bp-groups-new-members" class="<?php echo esc_attr( $class ); ?>" placeholder="" />
+	<?php if ( $notice ) : ?>
+		<p class="description"><?php echo esc_html( $notice ); ?></p>
+	<?php endif; ?>
+	<ul id="bp-groups-new-members-list"></ul>
+	<?php
+}
+
+/**
+ * Renders the Members metabox on single group pages.
+ *
+ * @since 1.7.0
+ *
+ * @param BP_Groups_Group $item The BP_Groups_Group object for the current group.
+ */
+function bp_groups_admin_edit_metabox_members( $item ) {
+
+	// Pull up a list of group members, so we can separate out the types
+	// We'll also keep track of group members here to place them into a
+	// JavaScript variable, which will help with group member autocomplete.
+	$members = array(
+		'admin'  => array(),
+		'mod'    => array(),
+		'member' => array(),
+		'banned' => array(),
+	);
+
+	$pagination = array(
+		'admin'  => array(),
+		'mod'    => array(),
+		'member' => array(),
+		'banned' => array(),
+	);
+
+	foreach ( $members as $type => &$member_type_users ) {
+		$page_qs_key       = $type . '_page';
+		$current_type_page = isset( $_GET[ $page_qs_key ] ) ? absint( $_GET[ $page_qs_key ] ) : 1;
+		$member_type_query = new BP_Group_Member_Query( array(
+			'group_id'   => $item->id,
+			'group_role' => array( $type ),
+			'type'       => 'alphabetical',
+			/**
+			 * Filters the admin members type per page value.
+			 *
+			 * @since 2.8.0
+			 *
+			 * @param int    $value Member types per page. Default 10.
+			 * @param string $type  Member type.
+			 */
+			'per_page'   => apply_filters( 'bp_groups_admin_members_type_per_page', 10, $type ),
+			'page'       => $current_type_page,
+		) );
+
+		$member_type_users   = $member_type_query->results;
+		$pagination[ $type ] = bp_groups_admin_create_pagination_links( $member_type_query, $type );
+	}
+
+	// Echo out the JavaScript variable.
+	echo '<script type="text/javascript">var group_id = "' . esc_js( $item->id ) . '";</script>';
+
+	// Loop through each member type.
+	foreach ( $members as $member_type => $type_users ) : ?>
+
+		<div class="bp-groups-member-type" id="bp-groups-member-type-<?php echo esc_attr( $member_type ) ?>">
+
+			<h3><?php switch ( $member_type ) :
+					case 'admin'  : esc_html_e( 'Administrators', 'buddypress' ); break;
+					case 'mod'    : esc_html_e( 'Moderators',     'buddypress' ); break;
+					case 'member' : esc_html_e( 'Members',        'buddypress' ); break;
+					case 'banned' : esc_html_e( 'Banned Members', 'buddypress' ); break;
+			endswitch; ?></h3>
+
+			<div class="bp-group-admin-pagination table-top">
+				<?php echo $pagination[ $member_type ] ?>
+			</div>
+
+		<?php if ( !empty( $type_users ) ) : ?>
+
+			<table class="widefat bp-group-members">
+				<thead>
+					<tr>
+						<th scope="col" class="uid-column"><?php _ex( 'ID', 'Group member user_id in group admin', 'buddypress' ); ?></th>
+						<th scope="col" class="uname-column"><?php _ex( 'Name', 'Group member name in group admin', 'buddypress' ); ?></th>
+						<th scope="col" class="urole-column"><?php _ex( 'Group Role', 'Group member role in group admin', 'buddypress' ); ?></th>
+					</tr>
+				</thead>
+
+				<tbody>
+
+				<?php foreach ( $type_users as $type_user ) : ?>
+					<tr>
+						<th scope="row" class="uid-column"><?php echo esc_html( $type_user->ID ); ?></th>
+
+						<td class="uname-column">
+							<a style="float: left;" href="<?php echo bp_core_get_user_domain( $type_user->ID ); ?>"><?php echo bp_core_fetch_avatar( array(
+								'item_id' => $type_user->ID,
+								'width'   => '32',
+								'height'  => '32'
+							) ); ?></a>
+
+							<span style="margin: 8px; float: left;"><?php echo bp_core_get_userlink( $type_user->ID ); ?></span>
+						</td>
+
+						<td class="urole-column">
+							<label for="bp-groups-role-<?php echo esc_attr( $type_user->ID ); ?>" class="screen-reader-text"><?php
+								/* translators: accessibility text */
+								_e( 'Select group role for member', 'buddypress' );
+							?></label>
+							<select class="bp-groups-role" id="bp-groups-role-<?php echo esc_attr( $type_user->ID ); ?>" name="bp-groups-role[<?php echo esc_attr( $type_user->ID ); ?>]">
+								<optgroup label="<?php esc_attr_e( 'Roles', 'buddypress' ); ?>">
+									<option class="admin"  value="admin"  <?php selected( 'admin',  $member_type ); ?>><?php esc_html_e( 'Administrator', 'buddypress' ); ?></option>
+									<option class="mod"    value="mod"    <?php selected( 'mod',    $member_type ); ?>><?php esc_html_e( 'Moderator',     'buddypress' ); ?></option>
+									<option class="member" value="member" <?php selected( 'member', $member_type ); ?>><?php esc_html_e( 'Member',        'buddypress' ); ?></option>
+									<?php if ( 'banned' === $member_type ) : ?>
+									<option class="banned" value="banned" <?php selected( 'banned', $member_type ); ?>><?php esc_html_e( 'Banned',        'buddypress' ); ?></option>
+									<?php endif; ?>
+								</optgroup>
+								<optgroup label="<?php esc_attr_e( 'Actions', 'buddypress' ); ?>">
+									<option class="remove" value="remove"><?php esc_html_e( 'Remove', 'buddypress' ); ?></option>
+									<?php if ( 'banned' !== $member_type ) : ?>
+										<option class="banned" value="banned"><?php esc_html_e( 'Ban', 'buddypress' ); ?></option>
+									<?php endif; ?>
+								</optgroup>
+							</select>
+
+							<?php
+							/**
+							 * Store the current role for this user,
+							 * so we can easily detect changes.
+							 *
+							 * @todo remove this, and do database detection on save
+							 */
+							?>
+							<input type="hidden" name="bp-groups-existing-role[<?php echo esc_attr( $type_user->ID ); ?>]" value="<?php echo esc_attr( $member_type ); ?>" />
+						</td>
+					</tr>
+
+					<?php if ( has_filter( 'bp_groups_admin_manage_member_row' ) ) : ?>
+						<tr>
+							<td colspan="3">
+								<?php
+
+								/**
+								 * Fires after the listing of a single row for members in a group on the group edit screen.
+								 *
+								 * @since 1.8.0
+								 *
+								 * @param int             $ID   ID of the user being rendered.
+								 * @param BP_Groups_Group $item Object for the current group.
+								 */
+								do_action( 'bp_groups_admin_manage_member_row', $type_user->ID, $item ); ?>
+							</td>
+						</tr>
+					<?php endif; ?>
+
+				<?php endforeach; ?>
+
+				</tbody>
+			</table>
+
+		<?php else : ?>
+
+			<p class="bp-groups-no-members description"><?php esc_html_e( 'No members of this type', 'buddypress' ); ?></p>
+
+		<?php endif; ?>
+
+		</div><!-- .bp-groups-member-type -->
+
+	<?php endforeach;
+}
+
+/**
+ * Renders the Status metabox for the Groups admin edit screen.
+ *
+ * @since 1.7.0
+ *
+ * @param object $item Information about the currently displayed group.
+ */
+function bp_groups_admin_edit_metabox_status( $item ) {
+	$base_url = add_query_arg( array(
+		'page' => 'bp-groups',
+		'gid'  => $item->id
+	), bp_get_admin_url( 'admin.php' ) ); ?>
+
+	<div id="submitcomment" class="submitbox">
+		<div id="major-publishing-actions">
+			<div id="delete-action">
+				<a class="submitdelete deletion" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'action', 'delete', $base_url ), 'bp-groups-delete' ) ); ?>"><?php _e( 'Delete Group', 'buddypress' ) ?></a>
+			</div>
+
+			<div id="publishing-action">
+				<?php submit_button( __( 'Save Changes', 'buddypress' ), 'primary', 'save', false ); ?>
+			</div>
+			<div class="clear"></div>
+		</div><!-- #major-publishing-actions -->
+	</div><!-- #submitcomment -->
+
+<?php
+}
+
+/**
+ * Render the Group Type metabox.
+ *
+ * @since 2.6.0
+ *
+ * @param BP_Groups_Group|null $group The BP_Groups_Group object corresponding to the group being edited.
+ */
+function bp_groups_admin_edit_metabox_group_type( BP_Groups_Group $group = null ) {
+
+	// Bail if no group ID.
+	if ( empty( $group->id ) ) {
+		return;
+	}
+
+	$types         = bp_groups_get_group_types( array(), 'objects' );
+	$current_types = (array) bp_groups_get_group_type( $group->id, false );
+	$backend_only  = bp_groups_get_group_types( array( 'show_in_create_screen' => false ) );
+	?>
+
+	<label for="bp-groups-group-type" class="screen-reader-text"><?php
+		/* translators: accessibility text */
+		esc_html_e( 'Select group type', 'buddypress' );
+	?></label>
+
+	<ul class="categorychecklist form-no-clear">
+		<?php foreach ( $types as $type ) : ?>
+			<li>
+				<label class="selectit"><input value="<?php echo esc_attr( $type->name ) ?>" name="bp-groups-group-type[]" type="checkbox" <?php checked( true, in_array( $type->name, $current_types ) ); ?>>
+					<?php
+						echo esc_html( $type->labels['singular_name'] );
+						if ( in_array( $type->name, $backend_only ) ) {
+							printf( ' <span class="description">%s</span>', esc_html__( '(Not available on the front end)', 'buddypress' ) );
+						}
+					?>
+
+				</label>
+			</li>
+
+		<?php endforeach; ?>
+
+	</ul>
+
+	<?php
+
+	wp_nonce_field( 'bp-group-type-change-' . $group->id, 'bp-group-type-nonce' );
+}
+
+/**
+ * Process changes from the Group Type metabox.
+ *
+ * @since 2.6.0
+ *
+ * @param int $group_id Group ID.
+ */
+function bp_groups_process_group_type_update( $group_id ) {
+	if ( ! isset( $_POST['bp-group-type-nonce'] ) ) {
+		return;
+	}
+
+	check_admin_referer( 'bp-group-type-change-' . $group_id, 'bp-group-type-nonce' );
+
+	// Permission check.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return;
+	}
+
+	$group_types = ! empty( $_POST['bp-groups-group-type'] ) ? wp_unslash( $_POST['bp-groups-group-type'] ) : array();
+
+	/*
+	 * If an invalid group type is passed, someone's doing something
+	 * fishy with the POST request, so we can fail silently.
+	 */
+	if ( bp_groups_set_group_type( $group_id, $group_types ) ) {
+		// @todo Success messages can't be posted because other stuff happens on the page load.
+	}
+}
+add_action( 'bp_group_admin_edit_after', 'bp_groups_process_group_type_update' );
+
+/**
+ * Create pagination links out of a BP_Group_Member_Query.
+ *
+ * This function is intended to create pagination links for use under the
+ * Manage Members section of the Groups Admin Dashboard pages. It is a stopgap
+ * measure until a more general pagination solution is in place for BuddyPress.
+ * Plugin authors should not use this function, as it is likely to be
+ * deprecated soon.
+ *
+ * @since 1.8.0
+ *
+ * @param BP_Group_Member_Query $query       A BP_Group_Member_Query object.
+ * @param string                $member_type member|mod|admin|banned.
+ * @return string Pagination links HTML.
+ */
+function bp_groups_admin_create_pagination_links( BP_Group_Member_Query $query, $member_type ) {
+	$pagination = '';
+
+	if ( ! in_array( $member_type, array( 'admin', 'mod', 'member', 'banned' ) ) ) {
+		return $pagination;
+	}
+
+	// The key used to paginate this member type in the $_GET global.
+	$qs_key   = $member_type . '_page';
+	$url_base = remove_query_arg( array( $qs_key, 'updated', 'success_modified' ), $_SERVER['REQUEST_URI'] );
+
+	$page = isset( $_GET[ $qs_key ] ) ? absint( $_GET[ $qs_key ] ) : 1;
+
+	/**
+	 * Filters the number of members per member type that is displayed in group editing admin area.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param string $member_type Member type, which is a group role (admin, mod etc).
+	 */
+	$per_page = apply_filters( 'bp_groups_admin_members_type_per_page', 10, $member_type );
+
+	// Don't show anything if there's no pagination.
+	if ( 1 === $page && $query->total_users <= $per_page ) {
+		return $pagination;
+	}
+
+	$current_page_start = ( ( $page - 1 ) * $per_page ) + 1;
+	$current_page_end   = $page * $per_page > intval( $query->total_users ) ? $query->total_users : $page * $per_page;
+
+	$pag_links = paginate_links( array(
+		'base'      => add_query_arg( $qs_key, '%#%', $url_base ),
+		'format'    => '',
+		'prev_text' => __( '&laquo;', 'buddypress' ),
+		'next_text' => __( '&raquo;', 'buddypress' ),
+		'total'     => ceil( $query->total_users / $per_page ),
+		'current'   => $page,
+	) );
+
+	if ( 1 == $query->total_users ) {
+		$viewing_text = __( 'Viewing 1 member', 'buddypress' );
+	} else {
+		$viewing_text = sprintf(
+			_nx( 'Viewing %1$s - %2$s of %3$s member', 'Viewing %1$s - %2$s of %3$s members', $query->total_users, 'Group members pagination in group admin', 'buddypress' ),
+			bp_core_number_format( $current_page_start ),
+			bp_core_number_format( $current_page_end ),
+			bp_core_number_format( $query->total_users )
+		);
+	}
+
+	$pagination .= '<span class="bp-group-admin-pagination-viewing">' . $viewing_text . '</span>';
+	$pagination .= '<span class="bp-group-admin-pagination-links">' . $pag_links . '</span>';
+
+	return $pagination;
+}
+
+/**
+ * Get a set of usernames corresponding to a set of user IDs.
+ *
+ * @since 1.7.0
+ *
+ * @param array $user_ids Array of user IDs.
+ * @return array Array of user_logins corresponding to $user_ids.
+ */
+function bp_groups_admin_get_usernames_from_ids( $user_ids = array() ) {
+
+	$usernames = array();
+	$users     = new WP_User_Query( array( 'blog_id' => 0, 'include' => $user_ids ) );
+
+	foreach ( (array) $users->results as $user ) {
+		$usernames[] = $user->user_login;
+	}
+
+	return $usernames;
+}
+
+/**
+ * AJAX handler for group member autocomplete requests.
+ *
+ * @since 1.7.0
+ */
+function bp_groups_admin_autocomplete_handler() {
+
+	// Bail if user user shouldn't be here, or is a large network.
+	if ( ! bp_current_user_can( 'bp_moderate' ) || ( is_multisite() && wp_is_large_network( 'users' ) ) ) {
+		wp_die( -1 );
+	}
+
+	$term     = isset( $_GET['term'] )     ? sanitize_text_field( $_GET['term'] ) : '';
+	$group_id = isset( $_GET['group_id'] ) ? absint( $_GET['group_id'] )          : 0;
+
+	if ( ! $term || ! $group_id ) {
+		wp_die( -1 );
+	}
+
+	$suggestions = bp_core_get_suggestions( array(
+		'group_id' => -$group_id,  // A negative value will exclude this group's members from the suggestions.
+		'limit'    => 10,
+		'term'     => $term,
+		'type'     => 'members',
+	) );
+
+	$matches = array();
+
+	if ( $suggestions && ! is_wp_error( $suggestions ) ) {
+		foreach ( $suggestions as $user ) {
+
+			$matches[] = array(
+				// Translators: 1: user_login, 2: user_email.
+				'label' => sprintf( __( '%1$s (%2$s)', 'buddypress' ), $user->name, $user->ID ),
+				'value' => $user->ID,
+			);
+		}
+	}
+
+	wp_die( json_encode( $matches ) );
+}
+add_action( 'wp_ajax_bp_group_admin_member_autocomplete', 'bp_groups_admin_autocomplete_handler' );
+
+/**
+ * Process input from the Group Type bulk change select.
+ *
+ * @since 2.7.0
+ *
+ * @param string $doaction Current $_GET action being performed in admin screen.
+ */
+function bp_groups_admin_process_group_type_bulk_changes( $doaction ) {
+	// Bail if no groups are specified or if this isn't a relevant action.
+	if ( empty( $_REQUEST['gid'] )
+		|| ( empty( $_REQUEST['bp_change_type'] ) && empty( $_REQUEST['bp_change_type2'] ) )
+		|| empty( $_REQUEST['bp_change_group_type'] )
+	) {
+		return;
+	}
+
+	// Bail if nonce check fails.
+	check_admin_referer( 'bp-bulk-groups-change-type-' . bp_loggedin_user_id(), 'bp-bulk-groups-change-type-nonce' );
+
+	if ( ! bp_current_user_can( 'bp_moderate' )  ) {
+		return;
+	}
+
+	$new_type = '';
+	if ( ! empty( $_REQUEST['bp_change_type2'] ) ) {
+		$new_type = sanitize_text_field( $_REQUEST['bp_change_type2'] );
+	} elseif ( ! empty( $_REQUEST['bp_change_type'] ) ) {
+		$new_type = sanitize_text_field( $_REQUEST['bp_change_type'] );
+	}
+
+	// Check that the selected type actually exists.
+	if ( 'remove_group_type' !== $new_type && null === bp_groups_get_group_type_object( $new_type ) ) {
+		$error = true;
+	} else {
+		// Run through group ids.
+		$error = false;
+		foreach ( (array) $_REQUEST['gid'] as $group_id ) {
+			$group_id = (int) $group_id;
+
+			// Get the old group type to check against.
+			$group_type = bp_groups_get_group_type( $group_id );
+
+			if ( 'remove_group_type' === $new_type ) {
+				// Remove the current group type, if there's one to remove.
+				if ( $group_type ) {
+					$removed = bp_groups_remove_group_type( $group_id, $group_type );
+					if ( false === $removed || is_wp_error( $removed ) ) {
+						$error = true;
+					}
+				}
+			} else {
+				// Set the new group type.
+				if ( $new_type !== $group_type ) {
+					$set = bp_groups_set_group_type( $group_id, $new_type );
+					if ( false === $set || is_wp_error( $set ) ) {
+						$error = true;
+					}
+				}
+			}
+		}
+	}
+
+	// If there were any errors, show the error message.
+	if ( $error ) {
+		$redirect = add_query_arg( array( 'updated' => 'group-type-change-error' ), wp_get_referer() );
+	} else {
+		$redirect = add_query_arg( array( 'updated' => 'group-type-change-success' ), wp_get_referer() );
+	}
+
+	wp_redirect( $redirect );
+	exit();
+}
+add_action( 'bp_groups_admin_load', 'bp_groups_admin_process_group_type_bulk_changes' );
+
+/**
+ * Display an admin notice upon group type bulk update.
+ *
+ * @since 2.7.0
+ */
+function bp_groups_admin_groups_type_change_notice() {
+	$updated = isset( $_REQUEST['updated'] ) ? $_REQUEST['updated'] : false;
+
+	// Display feedback.
+	if ( $updated && in_array( $updated, array( 'group-type-change-error', 'group-type-change-success' ), true ) ) {
+
+		if ( 'group-type-change-error' === $updated ) {
+			$notice = __( 'There was an error while changing group type. Please try again.', 'buddypress' );
+			$type   = 'error';
+		} else {
+			$notice = __( 'Group type was changed successfully.', 'buddypress' );
+			$type   = 'updated';
+		}
+
+		bp_core_add_admin_notice( $notice, $type );
+	}
+}
+add_action( bp_core_admin_hook(), 'bp_groups_admin_groups_type_change_notice' );
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-adminbar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-adminbar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-adminbar.php	(revision 12265)
@@ -0,0 +1,95 @@
+<?php
+/**
+ * BuddyPress Groups Toolbar.
+ *
+ * Handles the groups functions related to the WordPress Toolbar.
+ *
+ * @package BuddyPress
+ * @subpackage Groups
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add the Group Admin top-level menu when viewing group pages.
+ *
+ * @since 1.5.0
+ *
+ * @todo Add dynamic menu items for group extensions.
+ *
+ * @return false|null False if not on a group page, or if user does not have
+ *                    access to group admin options.
+ */
+function bp_groups_group_admin_menu() {
+	global $wp_admin_bar;
+	$bp = buddypress();
+
+	// Only show if viewing a group.
+	if ( ! bp_is_group() || bp_is_group_create() ) {
+		return false;
+	}
+
+	// Only show this menu to group admins and super admins.
+	if ( ! bp_current_user_can( 'bp_moderate' ) && ! bp_group_is_admin() ) {
+		return false;
+	}
+
+	// Unique ID for the 'Edit Group' menu.
+	$bp->group_admin_menu_id = 'group-admin';
+
+	// Add the top-level Group Admin button.
+	$wp_admin_bar->add_menu( array(
+		'id'    => $bp->group_admin_menu_id,
+		'title' => __( 'Edit Group', 'buddypress' ),
+		'href'  => bp_get_group_permalink( $bp->groups->current_group )
+	) );
+
+	// Index of the Manage tabs parent slug.
+	$secondary_nav_items = $bp->groups->nav->get_secondary( array( 'parent_slug' => $bp->groups->current_group->slug . '_manage' ) );
+
+	// Check if current group has Manage tabs.
+	if ( ! $secondary_nav_items ) {
+		return;
+	}
+
+	// Build the Group Admin menus.
+	foreach ( $secondary_nav_items as $menu ) {
+		/**
+		 * Should we add the current manage link in the Group's "Edit" Admin Bar menu ?
+		 *
+		 * All core items will be added, plugins can use a new parameter in the BP Group Extension API
+		 * to also add the link to the "edit screen" of their group component. To do so, set the
+		 * the 'show_in_admin_bar' argument of your edit screen to true
+		 */
+		if ( $menu->show_in_admin_bar ) {
+			$title = sprintf( _x( 'Edit Group %s', 'Group WP Admin Bar manage links', 'buddypress' ), $menu->name );
+
+			// Title is specific for delete.
+			if ( 'delete-group' == $menu->slug ) {
+				$title = sprintf( _x( '%s Group', 'Group WP Admin Bar delete link', 'buddypress' ), $menu->name );
+			}
+
+			$wp_admin_bar->add_menu( array(
+				'parent' => $bp->group_admin_menu_id,
+				'id'     => $menu->slug,
+				'title'  => $title,
+				'href'   => bp_get_groups_action_link( 'admin/' . $menu->slug )
+			) );
+		}
+	}
+}
+add_action( 'admin_bar_menu', 'bp_groups_group_admin_menu', 99 );
+
+/**
+ * Remove rogue WP core Edit menu when viewing a single group.
+ *
+ * @since 1.6.0
+ */
+function bp_groups_remove_edit_page_menu() {
+	if ( bp_is_group() ) {
+		remove_action( 'admin_bar_menu', 'wp_admin_bar_edit_menu', 80 );
+	}
+}
+add_action( 'add_admin_bar_menus', 'bp_groups_remove_edit_page_menu' );
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-cache.php	(revision 12265)
@@ -0,0 +1,349 @@
+<?php
+/**
+ * BuddyPress Groups Caching.
+ *
+ * Caching functions handle the clearing of cached objects and pages on specific
+ * actions throughout BuddyPress.
+ *
+ * @package BuddyPress
+ * @subpackage Groups
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Slurp up metadata for a set of groups.
+ *
+ * This function is called in two places in the BP_Groups_Group class:
+ *   - in the populate() method, when single group objects are populated
+ *   - in the get() method, when multiple groups are queried
+ *
+ * It grabs all groupmeta associated with all of the groups passed in
+ * $group_ids and adds it to WP cache. This improves efficiency when using
+ * groupmeta within a loop context.
+ *
+ * @since 1.6.0
+ *
+ * @param int|string|array|bool $group_ids Accepts a single group_id, or a
+ *                                         comma-separated list or array of
+ *                                         group ids.
+ */
+function bp_groups_update_meta_cache( $group_ids = false ) {
+	$bp = buddypress();
+
+	$cache_args = array(
+		'object_ids' 	   => $group_ids,
+		'object_type' 	   => $bp->groups->id,
+		'cache_group'      => 'group_meta',
+		'object_column'    => 'group_id',
+		'meta_table' 	   => $bp->groups->table_name_groupmeta,
+		'cache_key_prefix' => 'bp_groups_groupmeta'
+	);
+
+	bp_update_meta_cache( $cache_args );
+}
+
+/**
+ * Clear the cached group count.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id Not used.
+ */
+function groups_clear_group_object_cache( $group_id ) {
+	wp_cache_delete( 'bp_total_group_count', 'bp' );
+}
+add_action( 'groups_group_deleted',              'groups_clear_group_object_cache' );
+add_action( 'groups_settings_updated',           'groups_clear_group_object_cache' );
+add_action( 'groups_details_updated',            'groups_clear_group_object_cache' );
+add_action( 'groups_group_avatar_updated',       'groups_clear_group_object_cache' );
+add_action( 'groups_create_group_step_complete', 'groups_clear_group_object_cache' );
+
+/**
+ * Bust group caches when editing or deleting.
+ *
+ * @since 1.7.0
+ *
+ * @param int $group_id The group being edited.
+ */
+function bp_groups_delete_group_cache( $group_id = 0 ) {
+	wp_cache_delete( $group_id, 'bp_groups' );
+}
+add_action( 'groups_delete_group',     'bp_groups_delete_group_cache' );
+add_action( 'groups_update_group',     'bp_groups_delete_group_cache' );
+add_action( 'groups_details_updated',  'bp_groups_delete_group_cache' );
+add_action( 'groups_settings_updated', 'bp_groups_delete_group_cache' );
+
+/**
+ * Bust group cache when modifying metadata.
+ *
+ * @since 2.0.0
+ *
+ * @param int $meta_id Meta ID.
+ * @param int $group_id Group ID.
+ */
+function bp_groups_delete_group_cache_on_metadata_change( $meta_id, $group_id ) {
+	wp_cache_delete( $group_id, 'bp_groups' );
+}
+add_action( 'updated_group_meta', 'bp_groups_delete_group_cache_on_metadata_change', 10, 2 );
+add_action( 'added_group_meta', 'bp_groups_delete_group_cache_on_metadata_change', 10, 2 );
+
+/**
+ * Clear caches for the group creator when a group is created.
+ *
+ * @since 1.6.0
+ *
+ * @param int             $group_id  ID of the group.
+ * @param BP_Groups_Group $group_obj Group object.
+ */
+function bp_groups_clear_group_creator_cache( $group_id, $group_obj ) {
+	// Clears the 'total groups' for this user.
+	groups_clear_group_user_object_cache( $group_obj->id, $group_obj->creator_id );
+}
+add_action( 'groups_created_group', 'bp_groups_clear_group_creator_cache', 10, 2 );
+
+/**
+ * Clears caches for all members in a group when a group is deleted.
+ *
+ * @since 1.6.0
+ *
+ * @param BP_Groups_Group $group_obj Group object.
+ * @param array           $user_ids  User IDs who were in this group.
+ */
+function bp_groups_clear_group_members_caches( $group_obj, $user_ids ) {
+	// Clears the 'total groups' cache for each member in a group.
+	foreach ( (array) $user_ids as $user_id )
+		groups_clear_group_user_object_cache( $group_obj->id, $user_id );
+}
+add_action( 'bp_groups_delete_group', 'bp_groups_clear_group_members_caches', 10, 2 );
+
+/**
+ * Clear a user's cached total group invite count.
+ *
+ * Count is cleared when an invite is accepted, rejected or deleted.
+ *
+ * @since 2.0.0
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_groups_clear_invite_count_for_user( $user_id ) {
+	wp_cache_delete( $user_id, 'bp_group_invite_count' );
+}
+add_action( 'groups_accept_invite', 'bp_groups_clear_invite_count_for_user' );
+add_action( 'groups_reject_invite', 'bp_groups_clear_invite_count_for_user' );
+add_action( 'groups_delete_invite', 'bp_groups_clear_invite_count_for_user' );
+
+/**
+ * Clear a user's cached total group invite count when a user is uninvited.
+ *
+ * Groan. Our API functions are not consistent.
+ *
+ * @since 2.0.0
+ *
+ * @param int $group_id The group ID. Not used in this function.
+ * @param int $user_id  The user ID.
+ */
+function bp_groups_clear_invite_count_on_uninvite( $group_id, $user_id ) {
+	bp_groups_clear_invite_count_for_user( $user_id );
+}
+add_action( 'groups_uninvite_user', 'bp_groups_clear_invite_count_on_uninvite', 10, 2 );
+
+/**
+ * Clear a user's cached total group invite count when a new invite is sent.
+ *
+ * @since 2.0.0
+ *
+ * @param int   $group_id      The group ID. Not used in this function.
+ * @param array $invited_users Array of invited user IDs.
+ */
+function bp_groups_clear_invite_count_on_send( $group_id, $invited_users ) {
+	foreach ( $invited_users as $user_id ) {
+		bp_groups_clear_invite_count_for_user( $user_id );
+	}
+}
+add_action( 'groups_send_invites', 'bp_groups_clear_invite_count_on_send', 10, 2 );
+
+/**
+ * Clear a user's cached group count.
+ *
+ * @since 1.2.0
+ *
+ * @param int $group_id The group ID. Not used in this function.
+ * @param int $user_id  The user ID.
+ */
+function groups_clear_group_user_object_cache( $group_id, $user_id ) {
+	wp_cache_delete( 'bp_total_groups_for_user_' . $user_id, 'bp' );
+}
+add_action( 'groups_join_group',    'groups_clear_group_user_object_cache', 10, 2 );
+add_action( 'groups_leave_group',   'groups_clear_group_user_object_cache', 10, 2 );
+add_action( 'groups_ban_member',    'groups_clear_group_user_object_cache', 10, 2 );
+add_action( 'groups_unban_member',  'groups_clear_group_user_object_cache', 10, 2 );
+add_action( 'groups_uninvite_user', 'groups_clear_group_user_object_cache', 10, 2 );
+add_action( 'groups_remove_member', 'groups_clear_group_user_object_cache', 10, 2 );
+
+/**
+ * Clear group administrator and moderator cache.
+ *
+ * @since 2.1.0
+ *
+ * @param int $group_id The group ID.
+ */
+function groups_clear_group_administrator_cache( $group_id ) {
+	wp_cache_delete( $group_id, 'bp_group_admins' );
+	wp_cache_delete( $group_id, 'bp_group_mods' );
+}
+add_action( 'groups_promote_member', 'groups_clear_group_administrator_cache' );
+add_action( 'groups_demote_member',  'groups_clear_group_administrator_cache' );
+add_action( 'groups_delete_group',   'groups_clear_group_administrator_cache' );
+
+/**
+ * Clear group administrator and moderator cache when a group member is saved.
+ *
+ * This accounts for situations where group admins or mods are added manually
+ * using {@link BP_Groups_Member::save()}.  Usually via a plugin.
+ *
+ * @since 2.1.0
+ *
+ * @param BP_Groups_Member $member Member object.
+ */
+function groups_clear_group_administrator_cache_on_member_save( BP_Groups_Member $member ) {
+	groups_clear_group_administrator_cache( $member->group_id );
+}
+add_action( 'groups_member_after_save', 'groups_clear_group_administrator_cache_on_member_save' );
+
+/**
+ * Clear the group type cache for a group.
+ *
+ * Called when group is deleted.
+ *
+ * @since 2.6.0
+ *
+ * @param int $group_id The group ID.
+ */
+function groups_clear_group_type_cache( $group_id = 0 ) {
+	wp_cache_delete( $group_id, 'bp_groups_group_type' );
+}
+add_action( 'groups_delete_group', 'groups_clear_group_type_cache' );
+
+/**
+ * Clear caches on membership save.
+ *
+ * @since 2.6.0
+ *
+ * @param BP_Groups_Member $member BP Groups Member instance.
+ */
+function bp_groups_clear_user_group_cache_on_membership_save( BP_Groups_Member $member ) {
+	wp_cache_delete( $member->user_id, 'bp_groups_memberships_for_user' );
+	wp_cache_delete( $member->id, 'bp_groups_memberships' );
+}
+add_action( 'groups_member_before_save', 'bp_groups_clear_user_group_cache_on_membership_save' );
+add_action( 'groups_member_before_remove', 'bp_groups_clear_user_group_cache_on_membership_save' );
+
+/**
+ * Clear group memberships cache on miscellaneous actions not covered by the 'after_save' hook.
+ *
+ * @since 2.6.0
+ *
+ * @param int $user_id  Current user ID.
+ * @param int $group_id Current group ID.
+ */
+function bp_groups_clear_user_group_cache_on_other_events( $user_id, $group_id ) {
+	wp_cache_delete( $user_id, 'bp_groups_memberships_for_user' );
+
+	$membership = new BP_Groups_Member( $user_id, $group_id );
+	wp_cache_delete( $membership->id, 'bp_groups_memberships' );
+}
+add_action( 'bp_groups_member_before_delete', 'bp_groups_clear_user_group_cache_on_other_events', 10, 2 );
+add_action( 'bp_groups_member_before_delete_invite', 'bp_groups_clear_user_group_cache_on_other_events', 10, 2 );
+add_action( 'groups_accept_invite', 'bp_groups_clear_user_group_cache_on_other_events', 10, 2 );
+
+/**
+ * Reset cache incrementor for the Groups component.
+ *
+ * This function invalidates all cached results of group queries,
+ * whenever one of the following events takes place:
+ *   - A group is created or updated.
+ *   - A group is deleted.
+ *   - A group's metadata is modified.
+ *
+ * @since 2.7.0
+ *
+ * @return bool True on success, false on failure.
+ */
+function bp_groups_reset_cache_incrementor() {
+	return bp_core_reset_incrementor( 'bp_groups' );
+}
+add_action( 'groups_group_after_save', 'bp_groups_reset_cache_incrementor' );
+add_action( 'bp_groups_delete_group',  'bp_groups_reset_cache_incrementor' );
+add_action( 'updated_group_meta',      'bp_groups_reset_cache_incrementor' );
+add_action( 'deleted_group_meta',      'bp_groups_reset_cache_incrementor' );
+add_action( 'added_group_meta',        'bp_groups_reset_cache_incrementor' );
+
+/**
+ * Reset cache incrementor for Groups component when a group's taxonomy terms change.
+ *
+ * We infer that a group is being affected by looking at the objects belonging
+ * to the taxonomy being affected.
+ *
+ * @since 2.7.0
+ *
+ * @param int    $object_id ID of the item whose terms are being modified.
+ * @param array  $terms     Array of object terms.
+ * @param array  $tt_ids    Array of term taxonomy IDs.
+ * @param string $taxonomy  Taxonomy slug.
+ * @return bool True on success, false on failure.
+ */
+function bp_groups_reset_cache_incrementor_on_group_term_change( $object_id, $terms, $tt_ids, $taxonomy ) {
+	$tax_object = get_taxonomy( $taxonomy );
+	if ( $tax_object && in_array( 'bp_group', $tax_object->object_type, true ) ) {
+		return bp_groups_reset_cache_incrementor();
+	}
+
+	return false;
+}
+add_action( 'bp_set_object_terms', 'bp_groups_reset_cache_incrementor_on_group_term_change', 10, 4 );
+
+/**
+ * Reset cache incrementor for Groups component when a group's taxonomy terms are removed.
+ *
+ * We infer that a group is being affected by looking at the objects belonging
+ * to the taxonomy being affected.
+ *
+ * @since 2.7.0
+ *
+ * @param int    $object_id ID of the item whose terms are being modified.
+ * @param array  $terms     Array of object terms.
+ * @param string $taxonomy  Taxonomy slug.
+ * @return bool True on success, false on failure.
+ */
+function bp_groups_reset_cache_incrementor_on_group_term_remove( $object_id, $terms, $taxonomy ) {
+	$tax_object = get_taxonomy( $taxonomy );
+	if ( $tax_object && in_array( 'bp_group', $tax_object->object_type, true ) ) {
+		return bp_groups_reset_cache_incrementor();
+	}
+
+	return false;
+}
+add_action( 'bp_remove_object_terms', 'bp_groups_reset_cache_incrementor_on_group_term_remove', 10, 3 );
+
+/* List actions to clear super cached pages on, if super cache is installed */
+add_action( 'groups_join_group',                 'bp_core_clear_cache' );
+add_action( 'groups_leave_group',                'bp_core_clear_cache' );
+add_action( 'groups_accept_invite',              'bp_core_clear_cache' );
+add_action( 'groups_reject_invite',              'bp_core_clear_cache' );
+add_action( 'groups_invite_user',                'bp_core_clear_cache' );
+add_action( 'groups_uninvite_user',              'bp_core_clear_cache' );
+add_action( 'groups_details_updated',            'bp_core_clear_cache' );
+add_action( 'groups_settings_updated',           'bp_core_clear_cache' );
+add_action( 'groups_unban_member',               'bp_core_clear_cache' );
+add_action( 'groups_ban_member',                 'bp_core_clear_cache' );
+add_action( 'groups_demote_member',              'bp_core_clear_cache' );
+add_action( 'groups_promote_member',             'bp_core_clear_cache' );
+add_action( 'groups_membership_rejected',        'bp_core_clear_cache' );
+add_action( 'groups_membership_accepted',        'bp_core_clear_cache' );
+add_action( 'groups_membership_requested',       'bp_core_clear_cache' );
+add_action( 'groups_create_group_step_complete', 'bp_core_clear_cache' );
+add_action( 'groups_created_group',              'bp_core_clear_cache' );
+add_action( 'groups_group_avatar_updated',       'bp_core_clear_cache' );
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-filters.php	(revision 12265)
@@ -0,0 +1,399 @@
+<?php
+/**
+ * BuddyPress Groups Filters.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsFilters
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+// Filter BuddyPress template locations.
+add_filter( 'bp_groups_get_directory_template', 'bp_add_template_locations' );
+add_filter( 'bp_get_single_group_template',    'bp_add_template_locations' );
+
+/* Apply WordPress defined filters */
+add_filter( 'bp_get_group_description',         'wptexturize' );
+add_filter( 'bp_get_group_description_excerpt', 'wptexturize' );
+add_filter( 'bp_get_group_name',                'wptexturize' );
+
+add_filter( 'bp_get_group_description',         'convert_smilies' );
+add_filter( 'bp_get_group_description_excerpt', 'convert_smilies' );
+
+add_filter( 'bp_get_group_description',         'convert_chars' );
+add_filter( 'bp_get_group_description_excerpt', 'convert_chars' );
+add_filter( 'bp_get_group_name',                'convert_chars' );
+
+add_filter( 'bp_get_group_description',         'wpautop' );
+add_filter( 'bp_get_group_description_excerpt', 'wpautop' );
+
+add_filter( 'bp_get_group_description',         'make_clickable', 9 );
+add_filter( 'bp_get_group_description_excerpt', 'make_clickable', 9 );
+
+add_filter( 'bp_get_group_name',                    'wp_filter_kses',        1 );
+add_filter( 'bp_get_group_permalink',               'wp_filter_kses',        1 );
+add_filter( 'bp_get_group_description',             'bp_groups_filter_kses', 1 );
+add_filter( 'bp_get_group_description_excerpt',     'wp_filter_kses',        1 );
+add_filter( 'groups_group_name_before_save',        'wp_filter_kses',        1 );
+add_filter( 'groups_group_description_before_save', 'wp_filter_kses',        1 );
+
+add_filter( 'bp_get_group_description',         'stripslashes' );
+add_filter( 'bp_get_group_description_excerpt', 'stripslashes' );
+add_filter( 'bp_get_group_name',                'stripslashes' );
+add_filter( 'bp_get_group_member_name',         'stripslashes' );
+add_filter( 'bp_get_group_member_link',         'stripslashes' );
+
+add_filter( 'groups_group_name_before_save',        'force_balance_tags' );
+add_filter( 'groups_group_description_before_save', 'force_balance_tags' );
+
+// Trim trailing spaces from name and description when saving.
+add_filter( 'groups_group_name_before_save',        'trim' );
+add_filter( 'groups_group_description_before_save', 'trim' );
+
+// Support emoji.
+if ( function_exists( 'wp_encode_emoji' ) ) {
+	add_filter( 'groups_group_description_before_save', 'wp_encode_emoji' );
+}
+
+// Escape output of new group creation details.
+add_filter( 'bp_get_new_group_name',        'esc_attr'     );
+add_filter( 'bp_get_new_group_description', 'esc_textarea' );
+
+// Format numerical output.
+add_filter( 'bp_get_total_group_count',          'bp_core_number_format' );
+add_filter( 'bp_get_group_total_for_member',     'bp_core_number_format' );
+add_filter( 'bp_get_group_total_members',        'bp_core_number_format' );
+add_filter( 'bp_get_total_group_count_for_user', 'bp_core_number_format' );
+
+// Activity component integration.
+add_filter( 'bp_activity_at_name_do_notifications', 'bp_groups_disable_at_mention_notification_for_non_public_groups', 10, 4 );
+
+// Default group avatar.
+add_filter( 'bp_core_avatar_default',       'bp_groups_default_avatar', 10, 3 );
+add_filter( 'bp_core_avatar_default_thumb', 'bp_groups_default_avatar', 10, 3 );
+
+// Personal data export.
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_groups_register_personal_data_exporters' );
+
+/**
+ * Filter output of Group Description through WordPress's KSES API.
+ *
+ * @since 1.1.0
+ *
+ * @param string $content Content to filter.
+ * @return string
+ */
+function bp_groups_filter_kses( $content = '' ) {
+
+	/**
+	 * Note that we don't immediately bail if $content is empty. This is because
+	 * WordPress's KSES API calls several other filters that might be relevant
+	 * to someone's workflow (like `pre_kses`)
+	 */
+
+	// Get allowed tags using core WordPress API allowing third party plugins
+	// to target the specific `buddypress-groups` context.
+	$allowed_tags = wp_kses_allowed_html( 'buddypress-groups' );
+
+	// Add our own tags allowed in group descriptions.
+	$allowed_tags['a']['class']    = array();
+	$allowed_tags['img']           = array();
+	$allowed_tags['img']['src']    = array();
+	$allowed_tags['img']['alt']    = array();
+	$allowed_tags['img']['width']  = array();
+	$allowed_tags['img']['height'] = array();
+	$allowed_tags['img']['class']  = array();
+	$allowed_tags['img']['id']     = array();
+	$allowed_tags['code']          = array();
+
+	/**
+	 * Filters the HTML elements allowed for a given context.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $allowed_tags Allowed tags, attributes, and/or entities.
+	 */
+	$tags = apply_filters( 'bp_groups_filter_kses', $allowed_tags );
+
+	// Return KSES'ed content, allowing the above tags.
+	return wp_kses( $content, $tags );
+}
+
+/**
+ * Should BuddyPress load the mentions scripts and related assets, including results to prime the
+ * mentions suggestions?
+ *
+ * @since 2.2.0
+ *
+ * @param bool $load_mentions    True to load mentions assets, false otherwise.
+ * @param bool $mentions_enabled True if mentions are enabled.
+ * @return bool True if mentions scripts should be loaded.
+ */
+function bp_groups_maybe_load_mentions_scripts( $load_mentions, $mentions_enabled ) {
+	if ( ! $mentions_enabled ) {
+		return $load_mentions;
+	}
+
+	if ( $load_mentions || bp_is_group_activity() ) {
+		return true;
+	}
+
+	return $load_mentions;
+}
+add_filter( 'bp_activity_maybe_load_mentions_scripts', 'bp_groups_maybe_load_mentions_scripts', 10, 2 );
+
+/**
+ * Disable at-mention notifications for users who are not a member of the non-public group where the activity appears.
+ *
+ * @since 2.5.0
+ *
+ * @param bool                 $send      Whether to send the notification.
+ * @param array                $usernames Array of all usernames being notified.
+ * @param int                  $user_id   ID of the user to be notified.
+ * @param BP_Activity_Activity $activity  Activity object.
+ * @return bool
+ */
+function bp_groups_disable_at_mention_notification_for_non_public_groups( $send, $usernames, $user_id, BP_Activity_Activity $activity ) {
+	// Skip the check for administrators, who can get notifications from non-public groups.
+	if ( bp_user_can( $user_id, 'bp_moderate' ) ) {
+		return $send;
+	}
+
+	if ( 'groups' === $activity->component && ! bp_user_can( $user_id, 'groups_access_group', array( 'group_id' => $activity->item_id ) ) ) {
+		$send = false;
+	}
+
+	return $send;
+}
+
+/**
+ * Use the mystery group avatar for groups.
+ *
+ * @since 2.6.0
+ *
+ * @param string $avatar Current avatar src.
+ * @param array  $params Avatar params.
+ * @return string
+ */
+function bp_groups_default_avatar( $avatar, $params ) {
+	if ( isset( $params['object'] ) && 'group' === $params['object'] ) {
+		if ( isset( $params['type'] ) && 'thumb' === $params['type'] ) {
+			$file = 'mystery-group-50.png';
+		} else {
+			$file = 'mystery-group.png';
+		}
+
+		$avatar = buddypress()->plugin_url . "bp-core/images/$file";
+	}
+
+	return $avatar;
+}
+
+/**
+ * Filter the bp_user_can value to determine what the user can do
+ * with regards to a specific group.
+ *
+ * @since 3.0.0
+ *
+ * @param bool   $retval     Whether or not the current user has the capability.
+ * @param int    $user_id
+ * @param string $capability The capability being checked for.
+ * @param int    $site_id    Site ID. Defaults to the BP root blog.
+ * @param array  $args       Array of extra arguments passed.
+ *
+ * @return bool
+ */
+function bp_groups_user_can_filter( $retval, $user_id, $capability, $site_id, $args ) {
+	if ( empty( $args['group_id'] ) ) {
+		$group_id = bp_get_current_group_id();
+	} else {
+		$group_id = (int) $args['group_id'];
+	}
+
+	switch ( $capability ) {
+		case 'groups_join_group':
+			// Return early if the user isn't logged in or the group ID is unknown.
+			if ( ! $user_id || ! $group_id ) {
+				break;
+			}
+
+			// Set to false to begin with.
+			$retval = false;
+
+			// The group must allow joining, and the user should not currently be a member.
+			$group = groups_get_group( $group_id );
+			if ( ( 'public' === bp_get_group_status( $group )
+				&& ! groups_is_user_member( $user_id, $group->id )
+				&& ! groups_is_user_banned( $user_id, $group->id ) )
+				// Site admins can join any group they are not a member of.
+				|| ( bp_user_can( $user_id, 'bp_moderate' )
+				&& ! groups_is_user_member( $user_id, $group->id ) )
+			) {
+				$retval = true;
+			}
+			break;
+
+		case 'groups_request_membership':
+			// Return early if the user isn't logged in or the group ID is unknown.
+			if ( ! $user_id || ! $group_id ) {
+				break;
+			}
+
+			// Set to false to begin with.
+			$retval = false;
+
+			/*
+			* The group must accept membership requests, and the user should not
+			* currently be a member, have an active request, or be banned.
+			*/
+			$group = groups_get_group( $group_id );
+			if ( 'private' === bp_get_group_status( $group )
+				&& ! groups_is_user_member( $user_id, $group->id )
+				&& ! groups_check_for_membership_request( $user_id, $group->id )
+				&& ! groups_is_user_banned( $user_id, $group->id )
+			) {
+				$retval = true;
+			}
+			break;
+
+		case 'groups_send_invitation':
+			// Return early if the user isn't logged in or the group ID is unknown.
+			if ( ! $user_id || ! $group_id ) {
+				break;
+			}
+
+			/*
+			* The group must allow invitations, and the user should not
+			* currently be a member or be banned from the group.
+			*/
+			// Users with the 'bp_moderate' cap can always send invitations.
+			if ( bp_user_can( $user_id, 'bp_moderate' ) ) {
+				$retval = true;
+			} else {
+				$invite_status = bp_group_get_invite_status( $group_id );
+
+				switch ( $invite_status ) {
+					case 'admins' :
+						if ( groups_is_user_admin( $user_id, $group_id ) ) {
+							$retval = true;
+						}
+						break;
+
+					case 'mods' :
+						if ( groups_is_user_mod( $user_id, $group_id ) || groups_is_user_admin( $user_id, $group_id ) ) {
+							$retval = true;
+						}
+						break;
+
+					case 'members' :
+						if ( groups_is_user_member( $user_id, $group_id ) ) {
+							$retval = true;
+						}
+						break;
+				}
+			}
+			break;
+
+		case 'groups_receive_invitation':
+			// Return early if the user isn't logged in or the group ID is unknown.
+			if ( ! $user_id || ! $group_id ) {
+				break;
+			}
+
+			// Set to false to begin with.
+			$retval = false;
+
+			/*
+			* The group must allow invitations, and the user should not
+			* currently be a member or be banned from the group.
+			*/
+			$group = groups_get_group( $group_id );
+			if ( in_array( bp_get_group_status( $group ), array( 'private', 'hidden' ), true )
+				&& ! groups_is_user_member( $user_id, $group->id )
+				&& ! groups_is_user_banned( $user_id, $group->id )
+			) {
+				$retval = true;
+			}
+			break;
+
+		case 'groups_access_group':
+			// Return early if the group ID is unknown.
+			if ( ! $group_id ) {
+				break;
+			}
+
+			$group = groups_get_group( $group_id );
+
+			// If the check is for the logged-in user, use the BP_Groups_Group property.
+			if ( $user_id === bp_loggedin_user_id() ) {
+				$retval = $group->user_has_access;
+
+			/*
+			 * If the check is for a specified user who is not the logged-in user
+			 * run the check manually.
+			 */
+			} elseif ( 'public' === bp_get_group_status( $group ) || groups_is_user_member( $user_id, $group->id ) ) {
+				$retval = true;
+			}
+			break;
+
+		case 'groups_see_group':
+			// Return early if the group ID is unknown.
+			if ( ! $group_id ) {
+				break;
+			}
+
+			$group = groups_get_group( $group_id );
+
+			// If the check is for the logged-in user, use the BP_Groups_Group property.
+			if ( $user_id === bp_loggedin_user_id() ) {
+				$retval = $group->is_visible;
+
+			/*
+			 * If the check is for a specified user who is not the logged-in user
+			 * run the check manually.
+			 */
+			} elseif ( 'hidden' !== bp_get_group_status( $group ) || groups_is_user_member( $user_id, $group->id ) ) {
+				$retval = true;
+			}
+			break;
+	}
+
+	return $retval;
+
+}
+add_filter( 'bp_user_can', 'bp_groups_user_can_filter', 10, 5 );
+
+/**
+ * Registers Groups personal data exporters.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_groups_register_personal_data_exporters( $exporters ) {
+	$exporters['buddypress-groups-memberships'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Group Memberships', 'buddypress' ),
+		'callback'               => 'bp_groups_memberships_personal_data_exporter',
+	);
+
+	$exporters['buddypress-groups-pending-requests'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Pending Group Membership Requests', 'buddypress' ),
+		'callback'               => 'bp_groups_pending_requests_personal_data_exporter',
+	);
+
+	$exporters['buddypress-groups-pending-received-invitations'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Pending Group Invitations (Received)', 'buddypress' ),
+		'callback'               => 'bp_groups_pending_received_invitations_personal_data_exporter',
+	);
+
+	$exporters['buddypress-groups-pending-sent-invitations'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Pending Group Invitations (Sent)', 'buddypress' ),
+		'callback'               => 'bp_groups_pending_sent_invitations_personal_data_exporter',
+	);
+
+	return $exporters;
+}
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-functions.php	(revision 12265)
@@ -0,0 +1,2938 @@
+<?php
+/**
+ * BuddyPress Groups Functions.
+ *
+ * Functions are where all the magic happens in BuddyPress. They will
+ * handle the actual saving or manipulation of information. Usually they will
+ * hand off to a database class for data access, then return
+ * true or false on success or failure.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Check whether there is a Groups directory page in the $bp global.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if set, False if empty.
+ */
+function bp_groups_has_directory() {
+	$bp = buddypress();
+
+	return (bool) !empty( $bp->pages->groups->id );
+}
+
+/**
+ * Fetch a single group object.
+ *
+ * When calling up a group object, you should always use this function instead
+ * of instantiating BP_Groups_Group directly, so that you will inherit cache
+ * support and pass through the groups_get_group filter.
+ *
+ * @since 1.2.0
+ * @since 2.7.0 The function signature was changed to accept a group ID only,
+ *              instead of an array containing the group ID.
+ *
+ * @param int $group_id ID of the group.
+ * @return BP_Groups_Group $group The group object.
+ */
+function groups_get_group( $group_id ) {
+	/*
+	 * Backward compatibilty.
+	 * Old-style arguments take the form of an array or a query string.
+	 */
+	if ( ! is_numeric( $group_id ) ) {
+		$r = bp_parse_args( $group_id, array(
+			'group_id'        => false,
+			'load_users'      => false,
+			'populate_extras' => false,
+		), 'groups_get_group' );
+
+		$group_id = $r['group_id'];
+	}
+
+	$group = new BP_Groups_Group( $group_id );
+
+	/**
+	 * Filters a single group object.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param BP_Groups_Group $group Single group object.
+	 */
+	return apply_filters( 'groups_get_group', $group );
+}
+
+/** Group Creation, Editing & Deletion ****************************************/
+
+/**
+ * Create a group.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {
+ *     An array of arguments.
+ *     @type int|bool $group_id     Pass a group ID to update an existing item, or
+ *                                  0 / false to create a new group. Default: 0.
+ *     @type int      $creator_id   The user ID that creates the group.
+ *     @type string   $name         The group name.
+ *     @type string   $description  Optional. The group's description.
+ *     @type string   $slug         The group slug.
+ *     @type string   $status       The group's status. Accepts 'public', 'private' or
+ *                                  'hidden'. Defaults to 'public'.
+ *     @type int      $parent_id    The ID of the parent group. Default: 0.
+ *     @type int      $enable_forum Optional. Whether the group has a forum enabled.
+ *                                  If a bbPress forum is enabled for the group,
+ *                                  set this to 1. Default: 0.
+ *     @type string   $date_created The GMT time, in Y-m-d h:i:s format, when the group
+ *                                  was created. Defaults to the current time.
+ * }
+ * @return int|bool The ID of the group on success. False on error.
+ */
+function groups_create_group( $args = '' ) {
+
+	$args = bp_parse_args( $args, array(
+		'group_id'     => 0,
+		'creator_id'   => 0,
+		'name'         => '',
+		'description'  => '',
+		'slug'         => '',
+		'status'       => null,
+		'parent_id'    => null,
+		'enable_forum' => null,
+		'date_created' => null
+	), 'groups_create_group' );
+
+	extract( $args, EXTR_SKIP );
+
+	// Pass an existing group ID.
+	if ( ! empty( $group_id ) ) {
+		$group = groups_get_group( $group_id );
+		$name  = ! empty( $name ) ? $name : $group->name;
+		$slug  = ! empty( $slug ) ? $slug : $group->slug;
+		$creator_id  = ! empty( $creator_id ) ? $creator_id : $group->creator_id;
+		$description = ! empty( $description ) ? $description : $group->description;
+		$status = ! is_null( $status ) ? $status : $group->status;
+		$parent_id = ! is_null( $parent_id ) ? $parent_id : $group->parent_id;
+		$enable_forum = ! is_null( $enable_forum ) ? $enable_forum : $group->enable_forum;
+		$date_created = ! is_null( $date_created ) ? $date_created : $group->date_created;
+
+		// Groups need at least a name.
+		if ( empty( $name ) ) {
+			return false;
+		}
+
+	// Create a new group.
+	} else {
+		// Instantiate new group object.
+		$group = new BP_Groups_Group;
+
+		// Check for null values, reset to sensible defaults.
+		$status = ! is_null( $status ) ? $status : 'public';
+		$parent_id = ! is_null( $parent_id ) ? $parent_id : 0;
+		$enable_forum = ! is_null( $enable_forum ) ? $enable_forum : 0;
+		$date_created = ! is_null( $date_created ) ? $date_created : bp_core_current_time();
+	}
+
+	// Set creator ID.
+	if ( $creator_id ) {
+		$group->creator_id = (int) $creator_id;
+	} elseif ( is_user_logged_in() ) {
+		$group->creator_id = bp_loggedin_user_id();
+	}
+
+	if ( ! $group->creator_id ) {
+		return false;
+	}
+
+	// Validate status.
+	if ( ! groups_is_valid_status( $status ) ) {
+		return false;
+	}
+
+	// Set group name.
+	$group->name         = $name;
+	$group->description  = $description;
+	$group->slug         = $slug;
+	$group->status       = $status;
+	$group->parent_id    = $parent_id;
+	$group->enable_forum = (int) $enable_forum;
+	$group->date_created = $date_created;
+
+	// Save group.
+	if ( ! $group->save() ) {
+		return false;
+	}
+
+	// If this is a new group, set up the creator as the first member and admin.
+	if ( empty( $group_id ) ) {
+		$member                = new BP_Groups_Member;
+		$member->group_id      = $group->id;
+		$member->user_id       = $group->creator_id;
+		$member->is_admin      = 1;
+		$member->user_title    = __( 'Group Admin', 'buddypress' );
+		$member->is_confirmed  = 1;
+		$member->date_modified = bp_core_current_time();
+		$member->save();
+
+		/**
+		 * Fires after the creation of a new group and a group creator needs to be made.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int              $id     ID of the newly created group.
+		 * @param BP_Groups_Member $member Instance of the member who is assigned
+		 *                                 as group creator.
+		 * @param BP_Groups_Group  $group  Instance of the group being created.
+		 */
+		do_action( 'groups_create_group', $group->id, $member, $group );
+
+	} else {
+
+		/**
+		 * Fires after the update of a group.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int             $id    ID of the updated group.
+		 * @param BP_Groups_Group $group Instance of the group being updated.
+		 */
+		do_action( 'groups_update_group', $group->id, $group );
+	}
+
+	/**
+	 * Fires after the creation or update of a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int             $id    ID of the newly created group.
+	 * @param BP_Groups_Group $group Instance of the group being updated.
+	 */
+	do_action( 'groups_created_group', $group->id, $group );
+
+	return $group->id;
+}
+
+/**
+ * Edit the base details for a group.
+ *
+ * These are the settings that appear on the first page of the group's Admin
+ * section (Name, Description, and "Notify members...").
+ *
+ * @since 1.0.0
+ *
+ * @param array $args {
+ *     An array of optional arguments.
+ *     @type int    $group_id       ID of the group.
+ *     @type string $name           Name of the group.
+ *     @type string $slug           Slug of the group.
+ *     @type string $description    Description of the group.
+ *     @type bool   $notify_members Whether to send an email notification to group
+ *                                  members about changes in these details.
+ * }
+ * @return bool True on success, false on failure.
+ */
+function groups_edit_base_group_details( $args = array() ) {
+
+	// Backward compatibility with old method of passing arguments.
+	if ( ! is_array( $args ) || func_num_args() > 1 ) {
+		_deprecated_argument( __METHOD__, '2.9.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+		$old_args_keys = array(
+			0 => 'group_id',
+			1 => 'name',
+			2 => 'description',
+			3 => 'notify_members',
+		);
+
+		$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+	}
+
+	$r = bp_parse_args( $args, array(
+		'group_id'       => bp_get_current_group_id(),
+		'name'           => null,
+		'slug'           => null,
+		'description'    => null,
+		'notify_members' => false,
+	), 'groups_edit_base_group_details' );
+
+	if ( ! $r['group_id'] ) {
+		return false;
+	}
+
+	$group     = groups_get_group( $r['group_id'] );
+	$old_group = clone $group;
+
+	// Group name, slug and description can never be empty. Update only if provided.
+	if ( $r['name'] ) {
+		$group->name = $r['name'];
+	}
+	if ( $r['slug'] && $r['slug'] != $group->slug ) {
+		$group->slug = groups_check_slug( $r['slug'] );
+	}
+	if ( $r['description'] ) {
+		$group->description = $r['description'];
+	}
+
+	if ( ! $group->save() ) {
+		return false;
+	}
+
+	// Maybe update the "previous_slug" groupmeta.
+	if ( $group->slug != $old_group->slug ) {
+		/*
+		 * If the old slug exists in this group's past, delete that entry.
+		 * Recent previous_slugs are preferred when selecting the current group
+		 * from an old group slug, so we want the previous slug to be
+		 * saved "now" in the groupmeta table and don't need the old record.
+		 */
+		groups_delete_groupmeta( $group->id, 'previous_slug', $old_group->slug );
+		groups_add_groupmeta( $group->id, 'previous_slug', $old_group->slug );
+	}
+
+	if ( $r['notify_members'] ) {
+		groups_notification_group_updated( $group->id, $old_group );
+	}
+
+	/**
+	 * Fired after a group's details are updated.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param int             $value          ID of the group.
+	 * @param BP_Groups_Group $old_group      Group object, before being modified.
+	 * @param bool            $notify_members Whether to send an email notification to members about the change.
+	 */
+	do_action( 'groups_details_updated', $group->id, $old_group, $r['notify_members'] );
+
+	return true;
+}
+
+/**
+ * Edit the base details for a group.
+ *
+ * These are the settings that appear on the Settings page of the group's Admin
+ * section (privacy settings, "enable forum", invitation status).
+ *
+ * @since 1.0.0
+ *
+ * @param int         $group_id      ID of the group.
+ * @param bool        $enable_forum  Whether to enable a forum for the group.
+ * @param string      $status        Group status. 'public', 'private', 'hidden'.
+ * @param string|bool $invite_status Optional. Who is allowed to send invitations
+ *                                   to the group. 'members', 'mods', or 'admins'.
+ * @param int|bool    $parent_id     Parent group ID.
+ * @return bool True on success, false on failure.
+ */
+function groups_edit_group_settings( $group_id, $enable_forum, $status, $invite_status = false, $parent_id = false ) {
+
+	$group = groups_get_group( $group_id );
+	$group->enable_forum = $enable_forum;
+
+	/**
+	 * Before we potentially switch the group status, if it has been changed to public
+	 * from private and there are outstanding membership requests, auto-accept those requests.
+	 */
+	if ( 'private' == $group->status && 'public' == $status )
+		groups_accept_all_pending_membership_requests( $group->id );
+
+	// Now update the status.
+	$group->status = $status;
+
+	// Update the parent ID if necessary.
+	if ( false !== $parent_id ) {
+		$group->parent_id = $parent_id;
+	}
+
+	if ( !$group->save() )
+		return false;
+
+	// Set the invite status.
+	if ( $invite_status )
+		groups_update_groupmeta( $group->id, 'invite_status', $invite_status );
+
+	groups_update_groupmeta( $group->id, 'last_activity', bp_core_current_time() );
+
+	/**
+	 * Fires after the update of a groups settings.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group that was updated.
+	 */
+	do_action( 'groups_settings_updated', $group->id );
+
+	return true;
+}
+
+/**
+ * Delete a group and all of its associated metadata.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group to delete.
+ * @return bool True on success, false on failure.
+ */
+function groups_delete_group( $group_id ) {
+
+	/**
+	 * Fires before the deletion of a group.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $group_id ID of the group to be deleted.
+	 */
+	do_action( 'groups_before_delete_group', $group_id );
+
+	// Get the group object.
+	$group = groups_get_group( $group_id );
+
+	// Bail if group cannot be deleted.
+	if ( ! $group->delete() ) {
+		return false;
+	}
+
+	// Remove all outstanding invites for this group.
+	groups_delete_all_group_invites( $group_id );
+
+	/**
+	 * Fires after the deletion of a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group that was deleted.
+	 */
+	do_action( 'groups_delete_group', $group_id );
+
+	return true;
+}
+
+/**
+ * Check a group status (eg 'private') against the whitelist of registered statuses.
+ *
+ * @since 1.1.0
+ *
+ * @param string $status Status to check.
+ * @return bool True if status is allowed, otherwise false.
+ */
+function groups_is_valid_status( $status ) {
+	$bp = buddypress();
+
+	return in_array( $status, (array) $bp->groups->valid_status );
+}
+
+/**
+ * Provide a unique, sanitized version of a group slug.
+ *
+ * @since 1.0.0
+ *
+ * @param string $slug Group slug to check.
+ * @return string $slug A unique and sanitized slug.
+ */
+function groups_check_slug( $slug ) {
+	$bp = buddypress();
+
+	// First, make the proposed slug work in a URL.
+	$slug = sanitize_title( $slug );
+
+	if ( 'wp' == substr( $slug, 0, 2 ) )
+		$slug = substr( $slug, 2, strlen( $slug ) - 2 );
+
+	if ( in_array( $slug, (array) $bp->groups->forbidden_names ) )
+		$slug = $slug . '-' . rand();
+
+	if ( BP_Groups_Group::check_slug( $slug ) ) {
+		do {
+			$slug = $slug . '-' . rand();
+		}
+		while ( BP_Groups_Group::check_slug( $slug ) );
+	}
+
+	return $slug;
+}
+
+/**
+ * Get a group slug by its ID.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id The numeric ID of the group.
+ * @return string The group's slug.
+ */
+function groups_get_slug( $group_id ) {
+	$group = groups_get_group( $group_id );
+	return !empty( $group->slug ) ? $group->slug : '';
+}
+
+/**
+ * Get a group ID by its slug.
+ *
+ * @since 1.6.0
+ *
+ * @param string $group_slug The group's slug.
+ * @return int|null The group ID on success; null on failure.
+ */
+function groups_get_id( $group_slug ) {
+	return BP_Groups_Group::group_exists( $group_slug );
+}
+
+/**
+ * Get a group ID by checking against old (not currently active) slugs.
+ *
+ * @since 2.9.0
+ *
+ * @param string $group_slug The group's slug.
+ * @return int|null The group ID on success; null on failure.
+ */
+function groups_get_id_by_previous_slug( $group_slug ) {
+	return BP_Groups_Group::get_id_by_previous_slug( $group_slug );
+}
+
+/** User Actions **************************************************************/
+
+/**
+ * Remove a user from a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group.
+ * @param int $user_id  Optional. ID of the user. Defaults to the currently
+ *                      logged-in user.
+ * @return bool True on success, false on failure.
+ */
+function groups_leave_group( $group_id, $user_id = 0 ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_loggedin_user_id();
+
+	// Don't let single admins leave the group.
+	if ( count( groups_get_group_admins( $group_id ) ) < 2 ) {
+		if ( groups_is_user_admin( $user_id, $group_id ) ) {
+			bp_core_add_message( __( 'As the only admin, you cannot leave the group.', 'buddypress' ), 'error' );
+			return false;
+		}
+	}
+
+	if ( ! BP_Groups_Member::delete( $user_id, $group_id ) ) {
+		return false;
+	}
+
+	bp_core_add_message( __( 'You successfully left the group.', 'buddypress' ) );
+
+	/**
+	 * Fires after a user leaves a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @param int $user_id  ID of the user leaving the group.
+	 */
+	do_action( 'groups_leave_group', $group_id, $user_id );
+
+	return true;
+}
+
+/**
+ * Add a user to a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group.
+ * @param int $user_id  Optional. ID of the user. Defaults to the currently
+ *                      logged-in user.
+ * @return bool True on success, false on failure.
+ */
+function groups_join_group( $group_id, $user_id = 0 ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_loggedin_user_id();
+
+	// Check if the user has an outstanding invite. If so, delete it.
+	if ( groups_check_user_has_invite( $user_id, $group_id ) )
+		groups_delete_invite( $user_id, $group_id );
+
+	// Check if the user has an outstanding request. If so, delete it.
+	if ( groups_check_for_membership_request( $user_id, $group_id ) )
+		groups_delete_membership_request( null, $user_id, $group_id );
+
+	// User is already a member, just return true.
+	if ( groups_is_user_member( $user_id, $group_id ) )
+		return true;
+
+	$new_member                = new BP_Groups_Member;
+	$new_member->group_id      = $group_id;
+	$new_member->user_id       = $user_id;
+	$new_member->inviter_id    = 0;
+	$new_member->is_admin      = 0;
+	$new_member->user_title    = '';
+	$new_member->date_modified = bp_core_current_time();
+	$new_member->is_confirmed  = 1;
+
+	if ( !$new_member->save() )
+		return false;
+
+	$bp = buddypress();
+
+	if ( !isset( $bp->groups->current_group ) || !$bp->groups->current_group || $group_id != $bp->groups->current_group->id )
+		$group = groups_get_group( $group_id );
+	else
+		$group = $bp->groups->current_group;
+
+	// Record this in activity streams.
+	if ( bp_is_active( 'activity' ) ) {
+		groups_record_activity( array(
+			'type'    => 'joined_group',
+			'item_id' => $group_id,
+			'user_id' => $user_id,
+		) );
+	}
+
+	/**
+	 * Fires after a user joins a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @param int $user_id  ID of the user joining the group.
+	 */
+	do_action( 'groups_join_group', $group_id, $user_id );
+
+	return true;
+}
+
+/**
+ * Update the last_activity meta value for a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id Optional. The ID of the group whose last_activity is
+ *                      being updated. Default: the current group's ID.
+ * @return false|null False on failure.
+ */
+function groups_update_last_activity( $group_id = 0 ) {
+
+	if ( empty( $group_id ) ) {
+		$group_id = buddypress()->groups->current_group->id;
+	}
+
+	if ( empty( $group_id ) ) {
+		return false;
+	}
+
+	groups_update_groupmeta( $group_id, 'last_activity', bp_core_current_time() );
+}
+add_action( 'groups_join_group',           'groups_update_last_activity' );
+add_action( 'groups_leave_group',          'groups_update_last_activity' );
+add_action( 'groups_created_group',        'groups_update_last_activity' );
+
+/** General Group Functions ***************************************************/
+
+/**
+ * Get a list of group administrators.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group.
+ * @return array Info about group admins (user_id + date_modified).
+ */
+function groups_get_group_admins( $group_id ) {
+	return BP_Groups_Member::get_group_administrator_ids( $group_id );
+}
+
+/**
+ * Get a list of group moderators.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group.
+ * @return array Info about group admins (user_id + date_modified).
+ */
+function groups_get_group_mods( $group_id ) {
+	return BP_Groups_Member::get_group_moderator_ids( $group_id );
+}
+
+/**
+ * Fetch the members of a group.
+ *
+ * Since BuddyPress 1.8, a procedural wrapper for BP_Group_Member_Query.
+ * Previously called BP_Groups_Member::get_all_for_group().
+ *
+ * To use the legacy query, filter 'bp_use_legacy_group_member_query',
+ * returning true.
+ *
+ * @since 1.0.0
+ * @since 3.0.0 $group_id now supports multiple values. Only works if legacy query is not
+ *              in use.
+ *
+ * @param array $args {
+ *     An array of optional arguments.
+ *     @type int|array|string $group_id            ID of the group to limit results to. Also accepts multiple values
+ *                                                 either as an array or as a comma-delimited string.
+ *     @type int              $page                Page of results to be queried. Default: 1.
+ *     @type int              $per_page            Number of items to return per page of results. Default: 20.
+ *     @type int              $max                 Optional. Max number of items to return.
+ *     @type array            $exclude             Optional. Array of user IDs to exclude.
+ *     @type bool|int         $exclude_admins_mods True (or 1) to exclude admins and mods from results. Default: 1.
+ *     @type bool|int         $exclude_banned      True (or 1) to exclude banned users from results. Default: 1.
+ *     @type array            $group_role          Optional. Array of group roles to include.
+ *     @type string           $search_terms        Optional. Filter results by a search string.
+ *     @type string           $type                Optional. Sort the order of results. 'last_joined', 'first_joined', or
+ *                                                 any of the $type params available in {@link BP_User_Query}. Default:
+ *                                                 'last_joined'.
+ * }
+ * @return false|array Multi-d array of 'members' list and 'count'.
+ */
+function groups_get_group_members( $args = array() ) {
+
+	// Backward compatibility with old method of passing arguments.
+	if ( ! is_array( $args ) || func_num_args() > 1 ) {
+		_deprecated_argument( __METHOD__, '2.0.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+		$old_args_keys = array(
+			0 => 'group_id',
+			1 => 'per_page',
+			2 => 'page',
+			3 => 'exclude_admins_mods',
+			4 => 'exclude_banned',
+			5 => 'exclude',
+			6 => 'group_role',
+		);
+
+		$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+	}
+
+	$r = bp_parse_args( $args, array(
+		'group_id'            => bp_get_current_group_id(),
+		'per_page'            => false,
+		'page'                => false,
+		'exclude_admins_mods' => true,
+		'exclude_banned'      => true,
+		'exclude'             => false,
+		'group_role'          => array(),
+		'search_terms'        => false,
+		'type'                => 'last_joined',
+	), 'groups_get_group_members' );
+
+	// For legacy users. Use of BP_Groups_Member::get_all_for_group() is deprecated.
+	if ( apply_filters( 'bp_use_legacy_group_member_query', false, __FUNCTION__, func_get_args() ) ) {
+		$retval = BP_Groups_Member::get_all_for_group( $r['group_id'], $r['per_page'], $r['page'], $r['exclude_admins_mods'], $r['exclude_banned'], $r['exclude'] );
+	} else {
+
+		// Both exclude_admins_mods and exclude_banned are legacy arguments.
+		// Convert to group_role.
+		if ( empty( $r['group_role'] ) ) {
+			$r['group_role'] = array( 'member' );
+
+			if ( ! $r['exclude_admins_mods'] ) {
+				$r['group_role'][] = 'mod';
+				$r['group_role'][] = 'admin';
+			}
+
+			if ( ! $r['exclude_banned'] ) {
+				$r['group_role'][] = 'banned';
+			}
+		}
+
+		// Perform the group member query (extends BP_User_Query).
+		$members = new BP_Group_Member_Query( array(
+			'group_id'       => $r['group_id'],
+			'per_page'       => $r['per_page'],
+			'page'           => $r['page'],
+			'group_role'     => $r['group_role'],
+			'exclude'        => $r['exclude'],
+			'search_terms'   => $r['search_terms'],
+			'type'           => $r['type'],
+		) );
+
+		// Structure the return value as expected by the template functions.
+		$retval = array(
+			'members' => array_values( $members->results ),
+			'count'   => $members->total_users,
+		);
+	}
+
+	return $retval;
+}
+
+/**
+ * Get the member count for a group.
+ *
+ * @since 1.2.3
+ *
+ * @param int $group_id Group ID.
+ * @return int Count of confirmed members for the group.
+ */
+function groups_get_total_member_count( $group_id ) {
+	return BP_Groups_Group::get_total_member_count( $group_id );
+}
+
+/** Group Fetching, Filtering & Searching  ************************************/
+
+/**
+ * Get a collection of groups, based on the parameters passed.
+ *
+ * @since 1.2.0
+ * @since 2.6.0 Added `$group_type`, `$group_type__in`, and `$group_type__not_in` parameters.
+ * @since 2.7.0 Added `$update_admin_cache` and `$parent_id` parameters.
+ *
+ * @param array|string $args {
+ *     Array of arguments. Supports all arguments of
+ *     {@link BP_Groups_Group::get()}. Where the default values differ, they
+ *     have been described here.
+ *     @type int $per_page Default: 20.
+ *     @type int $page Default: 1.
+ * }
+ * @return array See {@link BP_Groups_Group::get()}.
+ */
+function groups_get_groups( $args = '' ) {
+
+	$defaults = array(
+		'type'               => false,          // Active, newest, alphabetical, random, popular.
+		'order'              => 'DESC',         // 'ASC' or 'DESC'
+		'orderby'            => 'date_created', // date_created, last_activity, total_member_count, name, random, meta_id.
+		'user_id'            => false,          // Pass a user_id to limit to only groups that this user is a member of.
+		'include'            => false,          // Only include these specific groups (group_ids).
+		'exclude'            => false,          // Do not include these specific groups (group_ids).
+		'parent_id'          => null,           // Get groups that are children of the specified group(s).
+		'slug'               => array(),        // Find a group or groups by slug.
+		'search_terms'       => false,          // Limit to groups that match these search terms.
+		'search_columns'     => array(),        // Select which columns to search.
+		'group_type'         => '',             // Array or comma-separated list of group types to limit results to.
+		'group_type__in'     => '',             // Array or comma-separated list of group types to limit results to.
+		'group_type__not_in' => '',             // Array or comma-separated list of group types that will be excluded from results.
+		'meta_query'         => false,          // Filter by groupmeta. See WP_Meta_Query for syntax.
+		'show_hidden'        => false,          // Show hidden groups to non-admins.
+		'status'             => array(),        // Array or comma-separated list of group statuses to limit results to.
+		'per_page'           => 20,             // The number of results to return per page.
+		'page'               => 1,              // The page to return if limiting per page.
+		'update_meta_cache'  => true,           // Pre-fetch groupmeta for queried groups.
+		'update_admin_cache' => false,
+		'fields'             => 'all',          // Return BP_Groups_Group objects or a list of ids.
+	);
+
+	$r = bp_parse_args( $args, $defaults, 'groups_get_groups' );
+
+	$groups = BP_Groups_Group::get( array(
+		'type'               => $r['type'],
+		'user_id'            => $r['user_id'],
+		'include'            => $r['include'],
+		'exclude'            => $r['exclude'],
+		'slug'               => $r['slug'],
+		'parent_id'          => $r['parent_id'],
+		'search_terms'       => $r['search_terms'],
+		'search_columns'     => $r['search_columns'],
+		'group_type'         => $r['group_type'],
+		'group_type__in'     => $r['group_type__in'],
+		'group_type__not_in' => $r['group_type__not_in'],
+		'meta_query'         => $r['meta_query'],
+		'show_hidden'        => $r['show_hidden'],
+		'status'             => $r['status'],
+		'per_page'           => $r['per_page'],
+		'page'               => $r['page'],
+		'update_meta_cache'  => $r['update_meta_cache'],
+		'update_admin_cache' => $r['update_admin_cache'],
+		'order'              => $r['order'],
+		'orderby'            => $r['orderby'],
+		'fields'             => $r['fields'],
+	) );
+
+	/**
+	 * Filters the collection of groups based on parsed parameters.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param BP_Groups_Group $groups Object of found groups based on parameters.
+	 *                                Passed by reference.
+	 * @param array           $r      Array of parsed arguments used for group query.
+	 *                                Passed by reference.
+	 */
+	return apply_filters_ref_array( 'groups_get_groups', array( &$groups, &$r ) );
+}
+
+/**
+ * Get the total group count for the site.
+ *
+ * @since 1.2.0
+ *
+ * @return int
+ */
+function groups_get_total_group_count() {
+	$count = wp_cache_get( 'bp_total_group_count', 'bp' );
+
+	if ( false === $count ) {
+		$count = BP_Groups_Group::get_total_group_count();
+		wp_cache_set( 'bp_total_group_count', $count, 'bp' );
+	}
+
+	return $count;
+}
+
+/**
+ * Get the IDs of the groups of which a specified user is a member.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $pag_num  Optional. Max number of results to return.
+ *                      Default: false (no limit).
+ * @param int $pag_page Optional. Page offset of results to return.
+ *                      Default: false (no limit).
+ * @return array {
+ *     @type array $groups Array of groups returned by paginated query.
+ *     @type int   $total Count of groups matching query.
+ * }
+ */
+function groups_get_user_groups( $user_id = 0, $pag_num = 0, $pag_page = 0 ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_displayed_user_id();
+
+	return BP_Groups_Member::get_group_ids( $user_id, $pag_num, $pag_page );
+}
+
+/**
+ * Get a list of groups of which the specified user is a member.
+ *
+ * Get a list of the groups to which this member belongs,
+ * filtered by group membership status and role.
+ * Usage examples: Used with no arguments specified,
+ *
+ *    bp_get_user_groups( bp_loggedin_user_id() );
+ *
+ * returns an array of the groups in which the logged-in user
+ * is an unpromoted member. To fetch an array of all groups that
+ * the current user belongs to, in any membership role,
+ * member, moderator or administrator, use
+ *
+ *    bp_get_user_groups( $user_id, array(
+ *        'is_admin' => null,
+ *        'is_mod' => null,
+ *    ) );
+ *
+ * @since 2.6.0
+ *
+ * @param int $user_id ID of the user.
+ * @param array $args {
+ *     Array of optional args.
+ *     @param bool|null   $is_confirmed Whether to return only confirmed memberships. Pass `null` to disable this
+ *                                      filter. Default: true.
+ *     @param bool|null   $is_banned    Whether to return only banned memberships. Pass `null` to disable this filter.
+ *                                      Default: false.
+ *     @param bool|null   $is_admin     Whether to return only admin memberships. Pass `null` to disable this filter.
+ *                                      Default: false.
+ *     @param bool|null   $is_mod       Whether to return only mod memberships. Pass `null` to disable this filter.
+ *                                      Default: false.
+ *     @param bool|null   $invite_sent  Whether to return only memberships with 'invite_sent'. Pass `null` to disable
+ *                                      this filter. Default: false.
+ *     @param string      $orderby      Field to order by. Accepts 'id' (membership ID), 'group_id', 'date_modified'.
+ *                                      Default: 'group_id'.
+ *     @param string      $order        Sort order. Accepts 'ASC' or 'DESC'. Default: 'ASC'.
+ * }
+ * @return array Array of matching group memberships, keyed by group ID.
+ */
+function bp_get_user_groups( $user_id, $args = array() ) {
+	$r = bp_parse_args( $args, array(
+		'is_confirmed' => true,
+		'is_banned'    => false,
+		'is_admin'     => false,
+		'is_mod'       => false,
+		'invite_sent'  => null,
+		'orderby'      => 'group_id',
+		'order'        => 'ASC',
+	), 'get_user_groups' );
+
+	$user_id = intval( $user_id );
+
+	$membership_ids = wp_cache_get( $user_id, 'bp_groups_memberships_for_user' );
+	if ( false === $membership_ids ) {
+		$membership_ids = BP_Groups_Member::get_membership_ids_for_user( $user_id );
+		wp_cache_set( $user_id, $membership_ids, 'bp_groups_memberships_for_user' );
+	}
+
+	// Prime the membership cache.
+	$uncached_membership_ids = bp_get_non_cached_ids( $membership_ids, 'bp_groups_memberships' );
+	if ( ! empty( $uncached_membership_ids ) ) {
+		$uncached_memberships = BP_Groups_Member::get_memberships_by_id( $uncached_membership_ids );
+
+		foreach ( $uncached_memberships as $uncached_membership ) {
+			wp_cache_set( $uncached_membership->id, $uncached_membership, 'bp_groups_memberships' );
+		}
+	}
+
+	// Assemble filter array for use in `wp_list_filter()`.
+	$filters = wp_array_slice_assoc( $r, array( 'is_confirmed', 'is_banned', 'is_admin', 'is_mod', 'invite_sent' ) );
+	foreach ( $filters as $filter_name => $filter_value ) {
+		if ( is_null( $filter_value ) ) {
+			unset( $filters[ $filter_name ] );
+		}
+	}
+
+	// Populate group membership array from cache, and normalize.
+	$groups    = array();
+	$int_keys  = array( 'id', 'group_id', 'user_id', 'inviter_id' );
+	$bool_keys = array( 'is_admin', 'is_mod', 'is_confirmed', 'is_banned', 'invite_sent' );
+	foreach ( $membership_ids as $membership_id ) {
+		$membership = wp_cache_get( $membership_id, 'bp_groups_memberships' );
+
+		// Sanity check.
+		if ( ! isset( $membership->group_id ) ) {
+			continue;
+		}
+
+		// Integer values.
+		foreach ( $int_keys as $index ) {
+			$membership->{$index} = intval( $membership->{$index} );
+		}
+
+		// Boolean values.
+		foreach ( $bool_keys as $index ) {
+			$membership->{$index} = (bool) $membership->{$index};
+		}
+
+		foreach ( $filters as $filter_name => $filter_value ) {
+			if ( ! isset( $membership->{$filter_name} ) || $filter_value != $membership->{$filter_name} ) {
+				continue 2;
+			}
+		}
+
+		$group_id = (int) $membership->group_id;
+
+		$groups[ $group_id ] = $membership;
+	}
+
+	// By default, results are ordered by membership id.
+	if ( 'group_id' === $r['orderby'] ) {
+		ksort( $groups );
+	} elseif ( in_array( $r['orderby'], array( 'id', 'date_modified' ) ) ) {
+		$groups = bp_sort_by_key( $groups, $r['orderby'] );
+	}
+
+	// By default, results are ordered ASC.
+	if ( 'DESC' === strtoupper( $r['order'] ) ) {
+		// `true` to preserve keys.
+		$groups = array_reverse( $groups, true );
+	}
+
+	return $groups;
+}
+
+/**
+ * Get the count of groups of which the specified user is a member.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id Optional. Default: ID of the displayed user.
+ * @return int Group count.
+ */
+function groups_total_groups_for_user( $user_id = 0 ) {
+
+	if ( empty( $user_id ) )
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+
+	$count = wp_cache_get( 'bp_total_groups_for_user_' . $user_id, 'bp' );
+
+	if ( false === $count ) {
+		$count = BP_Groups_Member::total_group_count( $user_id );
+		wp_cache_set( 'bp_total_groups_for_user_' . $user_id, $count, 'bp' );
+	}
+
+	return (int) $count;
+}
+
+/**
+ * Get the BP_Groups_Group object corresponding to the current group.
+ *
+ * @since 1.5.0
+ *
+ * @return BP_Groups_Group The current group object.
+ */
+function groups_get_current_group() {
+	$bp = buddypress();
+
+	$current_group = isset( $bp->groups->current_group )
+		? $bp->groups->current_group
+		: false;
+
+	/**
+	 * Filters the BP_Groups_Group object corresponding to the current group.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param BP_Groups_Group $current_group Current BP_Groups_Group object.
+	 */
+	return apply_filters( 'groups_get_current_group', $current_group );
+}
+
+/** Group Avatars *************************************************************/
+
+/**
+ * Generate the avatar upload directory path for a given group.
+ *
+ * @since 1.1.0
+ *
+ * @param int $group_id Optional. ID of the group. Default: ID of the current group.
+ * @return string
+ */
+function groups_avatar_upload_dir( $group_id = 0 ) {
+
+	if ( empty( $group_id ) ) {
+		$group_id = bp_get_current_group_id();
+	}
+
+	$directory = 'group-avatars';
+	$path      = bp_core_avatar_upload_path() . '/' . $directory . '/' . $group_id;
+	$newbdir   = $path;
+	$newurl    = bp_core_avatar_url() . '/' . $directory . '/' . $group_id;
+	$newburl   = $newurl;
+	$newsubdir = '/' . $directory . '/' . $group_id;
+
+	/**
+	 * Filters the avatar upload directory path for a given group.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array $value Array of parts related to the groups avatar upload directory.
+	 */
+	return apply_filters( 'groups_avatar_upload_dir', array(
+		'path'    => $path,
+		'url'     => $newurl,
+		'subdir'  => $newsubdir,
+		'basedir' => $newbdir,
+		'baseurl' => $newburl,
+		'error'   => false
+	) );
+}
+
+/** Group Member Status Checks ************************************************/
+
+/**
+ * Check whether a user is an admin of a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if the user is admin, otherwise false.
+ */
+function groups_is_user_admin( $user_id, $group_id ) {
+	$is_admin = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'is_admin' => true,
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) ) {
+		$is_admin = $user_groups[ $group_id ]->id;
+	}
+
+	return $is_admin;
+}
+
+/**
+ * Check whether a user is a mod of a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if the user is mod, otherwise false.
+ */
+function groups_is_user_mod( $user_id, $group_id ) {
+	$is_mod = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'is_mod' => true,
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) ) {
+		$is_mod = $user_groups[ $group_id ]->id;
+	}
+
+	return $is_mod;
+}
+
+/**
+ * Check whether a user is a member of a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if the user is member, otherwise false.
+ */
+function groups_is_user_member( $user_id, $group_id ) {
+	$is_member = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'is_admin' => null,
+		'is_mod' => null,
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) ) {
+		$is_member = $user_groups[ $group_id ]->id;
+	}
+
+	return $is_member;
+}
+
+/**
+ * Check whether a user is banned from a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if the user is banned, otherwise false.
+ */
+function groups_is_user_banned( $user_id, $group_id ) {
+	$is_banned = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'is_confirmed' => null,
+		'is_banned' => true,
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) ) {
+		$is_banned = $user_groups[ $group_id ]->id;
+	}
+
+	return $is_banned;
+}
+
+/**
+ * Check whether a user has an outstanding invitation to a group.
+ *
+ * @since 2.6.0
+ *
+ * @param int $user_id ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if the user is invited, otherwise false.
+ */
+function groups_is_user_invited( $user_id, $group_id ) {
+	$is_invited = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'invite_sent' => true,
+		'is_confirmed' => false,
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) ) {
+		$is_invited = $user_groups[ $group_id ]->id;
+	}
+
+	return $is_invited;
+}
+
+/**
+ * Check whether a user has a pending membership request for a group.
+ *
+ * @since 2.6.0
+ *
+ * @param int $user_id ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if the user is pending, otherwise false.
+ */
+function groups_is_user_pending( $user_id, $group_id ) {
+	$is_pending = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'invite_sent' => false,
+		'is_confirmed' => false,
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) ) {
+		$is_pending = $user_groups[ $group_id ]->id;
+	}
+
+	return $is_pending;
+}
+
+/**
+ * Is the specified user the creator of the group?
+ *
+ * @since 1.2.6
+ *
+ * @param int $user_id ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|null
+ */
+function groups_is_user_creator( $user_id, $group_id ) {
+	return BP_Groups_Member::check_is_creator( $user_id, $group_id );
+}
+
+/** Group Activity Posting ****************************************************/
+
+/**
+ * Post an Activity status update affiliated with a group.
+ *
+ * @since 1.2.0
+ * @since 2.6.0 Added 'error_type' parameter to $args.
+ *
+ * @param array|string $args {
+ *     Array of arguments.
+ *     @type string $content  The content of the update.
+ *     @type int    $user_id  Optional. ID of the user posting the update. Default:
+ *                            ID of the logged-in user.
+ *     @type int    $group_id Optional. ID of the group to be affiliated with the
+ *                            update. Default: ID of the current group.
+ * }
+ * @return WP_Error|bool|int Returns the ID of the new activity item on success, or false on failure.
+ */
+function groups_post_update( $args = '' ) {
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	$r = bp_parse_args( $args, array(
+		'content'    => false,
+		'user_id'    => bp_loggedin_user_id(),
+		'group_id'   => 0,
+		'error_type' => 'bool'
+	), 'groups_post_update' );
+	extract( $r, EXTR_SKIP );
+
+	if ( empty( $group_id ) && !empty( $bp->groups->current_group->id ) )
+		$group_id = $bp->groups->current_group->id;
+
+	if ( empty( $content ) || !strlen( trim( $content ) ) || empty( $user_id ) || empty( $group_id ) )
+		return false;
+
+	$bp->groups->current_group = groups_get_group( $group_id );
+
+	// Be sure the user is a member of the group before posting.
+	if ( !bp_current_user_can( 'bp_moderate' ) && !groups_is_user_member( $user_id, $group_id ) )
+		return false;
+
+	// Record this in activity streams.
+	$activity_action  = sprintf( __( '%1$s posted an update in the group %2$s', 'buddypress'), bp_core_get_userlink( $user_id ), '<a href="' . bp_get_group_permalink( $bp->groups->current_group ) . '">' . esc_attr( $bp->groups->current_group->name ) . '</a>' );
+	$activity_content = $content;
+
+	/**
+	 * Filters the action for the new group activity update.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $activity_action The new group activity update.
+	 */
+	$action = apply_filters( 'groups_activity_new_update_action',  $activity_action  );
+
+	/**
+	 * Filters the content for the new group activity update.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $activity_content The content of the update.
+	 */
+	$content_filtered = apply_filters( 'groups_activity_new_update_content', $activity_content );
+
+	$activity_id = groups_record_activity( array(
+		'user_id'    => $user_id,
+		'action'     => $action,
+		'content'    => $content_filtered,
+		'type'       => 'activity_update',
+		'item_id'    => $group_id,
+		'error_type' => $error_type
+	) );
+
+	groups_update_groupmeta( $group_id, 'last_activity', bp_core_current_time() );
+
+	/**
+	 * Fires after posting of an Activity status update affiliated with a group.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $content     The content of the update.
+	 * @param int    $user_id     ID of the user posting the update.
+	 * @param int    $group_id    ID of the group being posted to.
+	 * @param bool   $activity_id Whether or not the activity recording succeeded.
+	 */
+	do_action( 'bp_groups_posted_update', $content, $user_id, $group_id, $activity_id );
+
+	return $activity_id;
+}
+
+/** Group Invitations *********************************************************/
+
+/**
+ * Get IDs of users with outstanding invites to a given group from a specified user.
+ *
+ * @since 1.0.0
+ *
+ * @param int               $user_id ID of the inviting user.
+ * @param int|bool          $limit   Limit to restrict to.
+ * @param int|bool          $page    Optional. Page offset of results to return.
+ * @param string|array|bool $exclude Array of comma-separated list of group IDs
+ *                                   to exclude from results.
+ * @return array $value IDs of users who have been invited to the group by the
+ *                      user but have not yet accepted.
+ */
+function groups_get_invites_for_user( $user_id = 0, $limit = false, $page = false, $exclude = false ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_loggedin_user_id();
+
+	return BP_Groups_Member::get_invites( $user_id, $limit, $page, $exclude );
+}
+
+/**
+ * Get the total group invite count for a user.
+ *
+ * @since 2.0.0
+ *
+ * @param int $user_id The user ID.
+ * @return int
+ */
+function groups_get_invite_count_for_user( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	return BP_Groups_Member::get_invite_count_for_user( $user_id );
+}
+
+/**
+ * Invite a user to a group.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {
+ *     Array of arguments.
+ *     @type int    $user_id       ID of the user being invited.
+ *     @type int    $group_id      ID of the group to which the user is being invited.
+ *     @type int    $inviter_id    Optional. ID of the inviting user. Default:
+ *                                 ID of the logged-in user.
+ *     @type string $date_modified Optional. Modified date for the invitation.
+ *                                 Default: current date/time.
+ *     @type bool   $is_confirmed  Optional. Whether the invitation should be
+ *                                 marked confirmed. Default: false.
+ * }
+ * @return bool True on success, false on failure.
+ */
+function groups_invite_user( $args = '' ) {
+
+	$args = bp_parse_args( $args, array(
+		'user_id'       => false,
+		'group_id'      => false,
+		'inviter_id'    => bp_loggedin_user_id(),
+		'date_modified' => bp_core_current_time(),
+		'is_confirmed'  => 0
+	), 'groups_invite_user' );
+	extract( $args, EXTR_SKIP );
+
+	if ( ! $user_id || ! $group_id || ! $inviter_id ) {
+		return false;
+	}
+
+	// If the user has already requested membership, accept the request.
+	if ( $membership_id = groups_check_for_membership_request( $user_id, $group_id ) ) {
+		groups_accept_membership_request( $membership_id, $user_id, $group_id );
+
+	// Otherwise, create a new invitation.
+	} elseif ( ! groups_is_user_member( $user_id, $group_id ) && ! groups_check_user_has_invite( $user_id, $group_id, 'all' ) ) {
+		$invite                = new BP_Groups_Member;
+		$invite->group_id      = $group_id;
+		$invite->user_id       = $user_id;
+		$invite->date_modified = $date_modified;
+		$invite->inviter_id    = $inviter_id;
+		$invite->is_confirmed  = $is_confirmed;
+
+		if ( !$invite->save() )
+			return false;
+
+		/**
+		 * Fires after the creation of a new group invite.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param array $args Array of parsed arguments for the group invite.
+		 */
+		do_action( 'groups_invite_user', $args );
+	}
+
+	return true;
+}
+
+/**
+ * Uninvite a user from a group.
+ *
+ * Functionally, this is equivalent to removing a user from a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_uninvite_user( $user_id, $group_id ) {
+
+	if ( ! BP_Groups_Member::delete_invite( $user_id, $group_id ) )
+		return false;
+
+	/**
+	 * Fires after uninviting a user from a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group being uninvited from.
+	 * @param int $user_id  ID of the user being uninvited.
+	 */
+	do_action( 'groups_uninvite_user', $group_id, $user_id );
+
+	return true;
+}
+
+/**
+ * Process the acceptance of a group invitation.
+ *
+ * Returns true if a user is already a member of the group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True when the user is a member of the group, otherwise false.
+ */
+function groups_accept_invite( $user_id, $group_id ) {
+
+	// If the user is already a member (because BP at one point allowed two invitations to
+	// slip through), delete all existing invitations/requests and return true.
+	if ( groups_is_user_member( $user_id, $group_id ) ) {
+		if ( groups_check_user_has_invite( $user_id, $group_id ) ) {
+			groups_delete_invite( $user_id, $group_id );
+		}
+
+		if ( groups_check_for_membership_request( $user_id, $group_id ) ) {
+			groups_delete_membership_request( null, $user_id, $group_id );
+		}
+
+		return true;
+	}
+
+	$member = new BP_Groups_Member( $user_id, $group_id );
+
+	// Save the inviter ID so that we can pass it to the action below.
+	$inviter_id = $member->inviter_id;
+
+	$member->accept_invite();
+
+	if ( !$member->save() ) {
+		return false;
+	}
+
+	// Remove request to join.
+	if ( $member->check_for_membership_request( $user_id, $group_id ) ) {
+		$member->delete_request( $user_id, $group_id );
+	}
+
+	// Modify group meta.
+	groups_update_groupmeta( $group_id, 'last_activity', bp_core_current_time() );
+
+	/**
+	 * Fires after a user has accepted a group invite.
+	 *
+	 * @since 1.0.0
+	 * @since 2.8.0 The $inviter_id arg was added.
+	 *
+	 * @param int $user_id    ID of the user who accepted the group invite.
+	 * @param int $group_id   ID of the group being accepted to.
+	 * @param int $inviter_id ID of the user who invited this user to the group.
+	 */
+	do_action( 'groups_accept_invite', $user_id, $group_id, $inviter_id );
+
+	return true;
+}
+
+/**
+ * Reject a group invitation.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_reject_invite( $user_id, $group_id ) {
+	if ( ! BP_Groups_Member::delete_invite( $user_id, $group_id ) )
+		return false;
+
+	/**
+	 * Fires after a user rejects a group invitation.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id  ID of the user rejecting the invite.
+	 * @param int $group_id ID of the group being rejected.
+	 */
+	do_action( 'groups_reject_invite', $user_id, $group_id );
+
+	return true;
+}
+
+/**
+ * Delete a group invitation.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the invited user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_delete_invite( $user_id, $group_id ) {
+	if ( ! BP_Groups_Member::delete_invite( $user_id, $group_id ) )
+		return false;
+
+	/**
+	 * Fires after the deletion of a group invitation.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int $user_id  ID of the user whose invitation is being deleted.
+	 * @param int $group_id ID of the group whose invitation is being deleted.
+	 */
+	do_action( 'groups_delete_invite', $user_id, $group_id );
+
+	return true;
+}
+
+/**
+ * Send all pending invites by a single user to a specific group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the inviting user.
+ * @param int $group_id ID of the group.
+ */
+function groups_send_invites( $user_id, $group_id ) {
+
+	if ( empty( $user_id ) )
+		$user_id = bp_loggedin_user_id();
+
+	// Send friend invites.
+	$invited_users = groups_get_invites_for_group( $user_id, $group_id );
+	$group = groups_get_group( $group_id );
+
+	for ( $i = 0, $count = count( $invited_users ); $i < $count; ++$i ) {
+		$member = new BP_Groups_Member( $invited_users[$i], $group_id );
+
+		// Skip if we've already sent an invite to this user.
+		if ( $member->invite_sent ) {
+			continue;
+		}
+
+		// Send the actual invite.
+		groups_notification_group_invites( $group, $member, $user_id );
+
+		$member->invite_sent = 1;
+		$member->save();
+	}
+
+	/**
+	 * Fires after the sending of invites for a group.
+	 *
+	 * @since 1.0.0
+	 * @since 2.5.0 Added $user_id to passed parameters.
+	 *
+	 * @param int   $group_id      ID of the group who's being invited to.
+	 * @param array $invited_users Array of users being invited to the group.
+	 * @param int   $user_id       ID of the inviting user.
+	 */
+	do_action( 'groups_send_invites', $group_id, $invited_users, $user_id );
+}
+
+/**
+ * Get IDs of users with outstanding invites to a given group from a specified user.
+ *
+ * @since 1.0.0
+ * @since 2.9.0 Added $sent as a parameter.
+ *
+ * @param  int      $user_id  ID of the inviting user.
+ * @param  int      $group_id ID of the group.
+ * @param  int|null $sent     Query for a specific invite sent status. If 0, this will query for users
+ *                            that haven't had an invite sent to them yet. If 1, this will query for
+ *                            users that have had an invite sent to them. If null, no invite status will
+ *                            queried. Default: null.
+ * @return array    IDs of users who have been invited to the group by the user but have not
+ *                  yet accepted.
+ */
+function groups_get_invites_for_group( $user_id, $group_id, $sent = null ) {
+	return BP_Groups_Group::get_invites( $user_id, $group_id, $sent );
+}
+
+/**
+ * Check to see whether a user has already been invited to a group.
+ *
+ * By default, the function checks for invitations that have been sent.
+ * Entering 'all' as the $type parameter will return unsent invitations as
+ * well (useful to make sure AJAX requests are not duplicated).
+ *
+ * @since 1.0.0
+ *
+ * @param int    $user_id  ID of potential group member.
+ * @param int    $group_id ID of potential group.
+ * @param string $type     Optional. Use 'sent' to check for sent invites,
+ *                         'all' to check for all. Default: 'sent'.
+ * @return int|bool ID of the membership if found, otherwise false.
+ */
+function groups_check_user_has_invite( $user_id, $group_id, $type = 'sent' ) {
+	$invite = false;
+
+	$args = array(
+		'is_confirmed' => false,
+		'is_banned'    => null,
+		'is_admin'     => null,
+		'is_mod'       => null,
+	);
+
+	if ( 'sent' === $type ) {
+		$args['invite_sent'] = true;
+	}
+
+	$user_groups = bp_get_user_groups( $user_id, $args );
+
+	if ( isset( $user_groups[ $group_id ] ) && 0 !== $user_groups[ $group_id ]->inviter_id ) {
+		$invite = $user_groups[ $group_id ]->id;
+	}
+
+	return $invite;
+}
+
+/**
+ * Delete all invitations to a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group whose invitations are being deleted.
+ * @return int|null Number of rows records deleted on success, null on failure.
+ */
+function groups_delete_all_group_invites( $group_id ) {
+	return BP_Groups_Group::delete_all_invites( $group_id );
+}
+
+/** Group Promotion & Banning *************************************************/
+
+/**
+ * Promote a member to a new status within a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int    $user_id  ID of the user.
+ * @param int    $group_id ID of the group.
+ * @param string $status   The new status. 'mod' or 'admin'.
+ * @return bool True on success, false on failure.
+ */
+function groups_promote_member( $user_id, $group_id, $status ) {
+
+	if ( ! bp_is_item_admin() )
+		return false;
+
+	$member = new BP_Groups_Member( $user_id, $group_id );
+
+	// Don't use this action. It's deprecated as of BuddyPress 1.6.
+	do_action( 'groups_premote_member', $group_id, $user_id, $status );
+
+	/**
+	 * Fires before the promotion of a user to a new status.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int    $group_id ID of the group being promoted in.
+	 * @param int    $user_id  ID of the user being promoted.
+	 * @param string $status   New status being promoted to.
+	 */
+	do_action( 'groups_promote_member', $group_id, $user_id, $status );
+
+	return $member->promote( $status );
+}
+
+/**
+ * Demote a user to 'member' status within a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_demote_member( $user_id, $group_id ) {
+
+	if ( ! bp_is_item_admin() )
+		return false;
+
+	$member = new BP_Groups_Member( $user_id, $group_id );
+
+	/**
+	 * Fires before the demotion of a user to 'member'.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group being demoted in.
+	 * @param int $user_id  ID of the user being demoted.
+	 */
+	do_action( 'groups_demote_member', $group_id, $user_id );
+
+	return $member->demote();
+}
+
+/**
+ * Ban a member from a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_ban_member( $user_id, $group_id ) {
+
+	if ( ! bp_is_item_admin() )
+		return false;
+
+	$member = new BP_Groups_Member( $user_id, $group_id );
+
+	/**
+	 * Fires before the banning of a member from a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group being banned from.
+	 * @param int $user_id  ID of the user being banned.
+	 */
+	do_action( 'groups_ban_member', $group_id, $user_id );
+
+	return $member->ban();
+}
+
+/**
+ * Unban a member from a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_unban_member( $user_id, $group_id ) {
+
+	if ( ! bp_is_item_admin() )
+		return false;
+
+	$member = new BP_Groups_Member( $user_id, $group_id );
+
+	/**
+	 * Fires before the unbanning of a member from a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group being unbanned from.
+	 * @param int $user_id  ID of the user being unbanned.
+	 */
+	do_action( 'groups_unban_member', $group_id, $user_id );
+
+	return $member->unban();
+}
+
+/** Group Removal *************************************************************/
+
+/**
+ * Remove a member from a group.
+ *
+ * @since 1.2.6
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_remove_member( $user_id, $group_id ) {
+
+	if ( ! bp_is_item_admin() ) {
+		return false;
+	}
+
+	$member = new BP_Groups_Member( $user_id, $group_id );
+
+	/**
+	 * Fires before the removal of a member from a group.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param int $group_id ID of the group being removed from.
+	 * @param int $user_id  ID of the user being removed.
+	 */
+	do_action( 'groups_remove_member', $group_id, $user_id );
+
+	return $member->remove();
+}
+
+/** Group Membership **********************************************************/
+
+/**
+ * Create a group membership request.
+ *
+ * @since 1.0.0
+ *
+ * @param int $requesting_user_id ID of the user requesting membership.
+ * @param int $group_id           ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_send_membership_request( $requesting_user_id, $group_id ) {
+
+	// Prevent duplicate requests.
+	if ( groups_check_for_membership_request( $requesting_user_id, $group_id ) )
+		return false;
+
+	// Check if the user is already a member or is banned.
+	if ( groups_is_user_member( $requesting_user_id, $group_id ) || groups_is_user_banned( $requesting_user_id, $group_id ) )
+		return false;
+
+	// Check if the user is already invited - if so, simply accept invite.
+	if ( groups_check_user_has_invite( $requesting_user_id, $group_id ) ) {
+		groups_accept_invite( $requesting_user_id, $group_id );
+		return true;
+	}
+
+	$requesting_user                = new BP_Groups_Member;
+	$requesting_user->group_id      = $group_id;
+	$requesting_user->user_id       = $requesting_user_id;
+	$requesting_user->inviter_id    = 0;
+	$requesting_user->is_admin      = 0;
+	$requesting_user->user_title    = '';
+	$requesting_user->date_modified = bp_core_current_time();
+	$requesting_user->is_confirmed  = 0;
+	$requesting_user->comments      = isset( $_POST['group-request-membership-comments'] ) ? $_POST['group-request-membership-comments'] : '';
+
+	if ( $requesting_user->save() ) {
+		$admins = groups_get_group_admins( $group_id );
+
+		// Saved okay, now send the email notification.
+		for ( $i = 0, $count = count( $admins ); $i < $count; ++$i )
+			groups_notification_new_membership_request( $requesting_user_id, $admins[$i]->user_id, $group_id, $requesting_user->id );
+
+		/**
+		 * Fires after the creation of a new membership request.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int   $requesting_user_id  ID of the user requesting membership.
+		 * @param array $admins              Array of group admins.
+		 * @param int   $group_id            ID of the group being requested to.
+		 * @param int   $requesting_user->id ID of the membership.
+		 */
+		do_action( 'groups_membership_requested', $requesting_user_id, $admins, $group_id, $requesting_user->id );
+
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Accept a pending group membership request.
+ *
+ * @since 1.0.0
+ *
+ * @param int $membership_id ID of the membership object.
+ * @param int $user_id       Optional. ID of the user who requested membership.
+ *                           Provide this value along with $group_id to override
+ *                           $membership_id.
+ * @param int $group_id      Optional. ID of the group to which membership is being
+ *                           requested. Provide this value along with $user_id to
+ *                           override $membership_id.
+ * @return bool True on success, false on failure.
+ */
+function groups_accept_membership_request( $membership_id, $user_id = 0, $group_id = 0 ) {
+
+	if ( !empty( $user_id ) && !empty( $group_id ) ) {
+		$membership = new BP_Groups_Member( $user_id, $group_id );
+	} else {
+		$membership = new BP_Groups_Member( false, false, $membership_id );
+	}
+
+	$membership->accept_request();
+
+	if ( !$membership->save() ) {
+		return false;
+	}
+
+	// Check if the user has an outstanding invite, if so delete it.
+	if ( groups_check_user_has_invite( $membership->user_id, $membership->group_id ) ) {
+		groups_delete_invite( $membership->user_id, $membership->group_id );
+	}
+
+	/**
+	 * Fires after a group membership request has been accepted.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int  $user_id  ID of the user who accepted membership.
+	 * @param int  $group_id ID of the group that was accepted membership to.
+	 * @param bool $value    If membership was accepted.
+	 */
+	do_action( 'groups_membership_accepted', $membership->user_id, $membership->group_id, true );
+
+	return true;
+}
+
+/**
+ * Reject a pending group membership request.
+ *
+ * @since 1.0.0
+ *
+ * @param int $membership_id ID of the membership object.
+ * @param int $user_id       Optional. ID of the user who requested membership.
+ *                           Provide this value along with $group_id to override
+ *                           $membership_id.
+ * @param int $group_id      Optional. ID of the group to which membership is being
+ *                           requested. Provide this value along with $user_id to
+ *                           override $membership_id.
+ * @return bool True on success, false on failure.
+ */
+function groups_reject_membership_request( $membership_id, $user_id = 0, $group_id = 0 ) {
+	if ( !$membership = groups_delete_membership_request( $membership_id, $user_id, $group_id ) ) {
+		return false;
+	}
+
+	/**
+	 * Fires after a group membership request has been rejected.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int  $user_id  ID of the user who rejected membership.
+	 * @param int  $group_id ID of the group that was rejected membership to.
+	 * @param bool $value    If membership was accepted.
+	 */
+	do_action( 'groups_membership_rejected', $membership->user_id, $membership->group_id, false );
+
+	return true;
+}
+
+/**
+ * Delete a pending group membership request.
+ *
+ * @since 1.2.0
+ *
+ * @param int $membership_id ID of the membership object.
+ * @param int $user_id       Optional. ID of the user who requested membership.
+ *                           Provide this value along with $group_id to override
+ *                           $membership_id.
+ * @param int $group_id      Optional. ID of the group to which membership is being
+ *                           requested. Provide this value along with $user_id to
+ *                           override $membership_id.
+ * @return false|BP_Groups_Member True on success, false on failure.
+ */
+function groups_delete_membership_request( $membership_id, $user_id = 0, $group_id = 0 ) {
+	if ( !empty( $user_id ) && !empty( $group_id ) )
+		$membership = new BP_Groups_Member( $user_id, $group_id );
+	else
+		$membership = new BP_Groups_Member( false, false, $membership_id );
+
+	if ( ! BP_Groups_Member::delete_request( $membership->user_id, $membership->group_id ) )
+		return false;
+
+	return $membership;
+}
+
+/**
+ * Check whether a user has an outstanding membership request for a given group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ * @return int|bool ID of the membership if found, otherwise false.
+ */
+function groups_check_for_membership_request( $user_id, $group_id ) {
+	$request = false;
+
+	$user_groups = bp_get_user_groups( $user_id, array(
+		'is_confirmed' => false,
+		'is_banned'    => false,
+		'is_admin'     => null,
+		'is_mod'       => null
+	) );
+
+	if ( isset( $user_groups[ $group_id ] ) && 0 === $user_groups[ $group_id ]->inviter_id ) {
+		$request = $user_groups[ $group_id ]->id;
+	}
+
+	return $request;
+}
+
+/**
+ * Accept all pending membership requests to a group.
+ *
+ * @since 1.0.2
+ *
+ * @param int $group_id ID of the group.
+ * @return bool True on success, false on failure.
+ */
+function groups_accept_all_pending_membership_requests( $group_id ) {
+	$user_ids = BP_Groups_Member::get_all_membership_request_user_ids( $group_id );
+
+	if ( !$user_ids )
+		return false;
+
+	foreach ( (array) $user_ids as $user_id )
+		groups_accept_membership_request( false, $user_id, $group_id );
+
+	/**
+	 * Fires after the acceptance of all pending membership requests to a group.
+	 *
+	 * @since 1.0.2
+	 *
+	 * @param int $group_id ID of the group whose pending memberships were accepted.
+	 */
+	do_action( 'groups_accept_all_pending_membership_requests', $group_id );
+
+	return true;
+}
+
+/** Group Meta ****************************************************************/
+
+/**
+ * Delete metadata for a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int         $group_id   ID of the group.
+ * @param string|bool $meta_key   The key of the row to delete.
+ * @param string|bool $meta_value Optional. Metadata value. If specified, only delete
+ *                                metadata entries with this value.
+ * @param bool        $delete_all Optional. If true, delete matching metadata entries
+ *                                for all groups. Otherwise, only delete matching
+ *                                metadata entries for the specified group.
+ *                                Default: false.
+ * @return bool True on success, false on failure.
+ */
+function groups_delete_groupmeta( $group_id, $meta_key = false, $meta_value = false, $delete_all = false ) {
+	global $wpdb;
+
+	// Legacy - if no meta_key is passed, delete all for the item.
+	if ( empty( $meta_key ) ) {
+		$table_name = buddypress()->groups->table_name_groupmeta;
+		$sql        = "SELECT meta_key FROM {$table_name} WHERE group_id = %d";
+		$query      = $wpdb->prepare( $sql, $group_id );
+		$keys       = $wpdb->get_col( $query );
+
+		// With no meta_key, ignore $delete_all.
+		$delete_all = false;
+	} else {
+		$keys = array( $meta_key );
+	}
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	$retval = true;
+	foreach ( $keys as $key ) {
+		$retval = delete_metadata( 'group', $group_id, $key, $meta_value, $delete_all );
+	}
+
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Get a piece of group metadata.
+ *
+ * @since 1.0.0
+ *
+ * @param int    $group_id ID of the group.
+ * @param string $meta_key Metadata key.
+ * @param bool   $single   Optional. If true, return only the first value of the
+ *                         specified meta_key. This parameter has no effect if
+ *                         meta_key is empty.
+ * @return mixed Metadata value.
+ */
+function groups_get_groupmeta( $group_id, $meta_key = '', $single = true ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = get_metadata( 'group', $group_id, $meta_key, $single );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Update a piece of group metadata.
+ *
+ * @since 1.0.0
+ *
+ * @param int    $group_id   ID of the group.
+ * @param string $meta_key   Metadata key.
+ * @param mixed  $meta_value Value to store.
+ * @param mixed  $prev_value Optional. If specified, only update existing
+ *                           metadata entries with the specified value.
+ *                           Otherwise, update all entries.
+ * @return bool|int $retval Returns false on failure. On successful update of existing
+ *                          metadata, returns true. On successful creation of new metadata,
+ *                          returns the integer ID of the new metadata row.
+ */
+function groups_update_groupmeta( $group_id, $meta_key, $meta_value, $prev_value = '' ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = update_metadata( 'group', $group_id, $meta_key, $meta_value, $prev_value );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Add a piece of group metadata.
+ *
+ * @since 2.0.0
+ *
+ * @param int    $group_id   ID of the group.
+ * @param string $meta_key   Metadata key.
+ * @param mixed  $meta_value Metadata value.
+ * @param bool   $unique     Optional. Whether to enforce a single metadata value
+ *                           for the given key. If true, and the object already
+ *                           has a value for the key, no change will be made.
+ *                           Default: false.
+ * @return int|bool The meta ID on successful update, false on failure.
+ */
+function groups_add_groupmeta( $group_id, $meta_key, $meta_value, $unique = false ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = add_metadata( 'group', $group_id, $meta_key, $meta_value, $unique );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/** Group Cleanup Functions ***************************************************/
+
+/**
+ * Delete all group membership information for the specified user.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user.
+ */
+function groups_remove_data_for_user( $user_id ) {
+	BP_Groups_Member::delete_all_for_user( $user_id );
+
+	/**
+	 * Fires after the deletion of all data for a user.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param int $user_id ID of the user whose data is being deleted.
+	 */
+	do_action( 'groups_remove_data_for_user', $user_id );
+}
+add_action( 'wpmu_delete_user',  'groups_remove_data_for_user' );
+add_action( 'delete_user',       'groups_remove_data_for_user' );
+add_action( 'bp_make_spam_user', 'groups_remove_data_for_user' );
+
+/**
+ * Update orphaned child groups when the parent is deleted.
+ *
+ * @since 2.7.0
+ *
+ * @param BP_Groups_Group $group Instance of the group item being deleted.
+ */
+function bp_groups_update_orphaned_groups_on_group_delete( $group ) {
+	// Get child groups and set the parent to the deleted parent's parent.
+	$grandparent_group_id = $group->parent_id;
+	$child_args = array(
+		'parent_id'         => $group->id,
+		'show_hidden'       => true,
+		'per_page'          => false,
+		'update_meta_cache' => false,
+	);
+	$children = groups_get_groups( $child_args );
+	$children = $children['groups'];
+
+	foreach ( $children as $cgroup ) {
+		$cgroup->parent_id = $grandparent_group_id;
+		$cgroup->save();
+	}
+}
+add_action( 'bp_groups_delete_group', 'bp_groups_update_orphaned_groups_on_group_delete', 10, 2 );
+
+/** Group Types ***************************************************************/
+
+/**
+ * Fire the 'bp_groups_register_group_types' action.
+ *
+ * @since 2.6.0
+ */
+function bp_groups_register_group_types() {
+	/**
+	 * Fires when it's appropriate to register group types.
+	 *
+	 * @since 2.6.0
+	 */
+	do_action( 'bp_groups_register_group_types' );
+}
+add_action( 'bp_register_taxonomies', 'bp_groups_register_group_types' );
+
+/**
+ * Register a group type.
+ *
+ * @since 2.6.0
+ * @since 2.7.0 Introduce $has_directory, $show_in_create_screen, $show_in_list, and
+ *              $description, $create_screen_checked as $args parameters.
+ *
+ * @param string $group_type Unique string identifier for the group type.
+ * @param array  $args {
+ *     Array of arguments describing the group type.
+ *
+ *     @type string|bool $has_directory         Set the slug to be used for custom group directory page. eg.
+ *                                              example.com/groups/type/MY_SLUG. Default: false.
+ *     @type bool        $show_in_create_screen Whether this group type is allowed to be selected on the group creation
+ *                                              page. Default: false.
+ *     @type bool|null   $show_in_list          Whether this group type should be shown in lists rendered by
+ *                                              bp_group_type_list(). Default: null. If $show_in_create_screen is true,
+ *                                              this will default to true, unless this is set explicitly to false.
+ *     @type string      $description           A short descriptive summary of what the group type is. Currently shown
+ *                                              on a group's "Manage > Settings" page when selecting group types.
+ *     @type bool        $create_screen_checked If $show_in_create_screen is true, whether we should have our group type
+ *                                              checkbox checked by default. Handy if you want to imply that the group
+ *                                              type should be enforced, but decision lies with the group creator.
+ *                                              Default: false.
+ *     @type array       $labels {
+ *         Array of labels to use in various parts of the interface.
+ *
+ *         @type string $name          Default name. Should typically be plural.
+ *         @type string $singular_name Singular name.
+ *     }
+ * }
+ * @return object|WP_Error Group type object on success, WP_Error object on failure.
+ */
+function bp_groups_register_group_type( $group_type, $args = array() ) {
+	$bp = buddypress();
+
+	if ( isset( $bp->groups->types[ $group_type ] ) ) {
+		return new WP_Error( 'bp_group_type_exists', __( 'Group type already exists.', 'buddypress' ), $group_type );
+	}
+
+	$r = bp_parse_args( $args, array(
+		'has_directory'         => false,
+		'show_in_create_screen' => false,
+		'show_in_list'          => null,
+		'description'           => '',
+		'create_screen_checked' => false,
+		'labels'                => array(),
+	), 'register_group_type' );
+
+	$group_type = sanitize_key( $group_type );
+
+	/**
+	 * Filters the list of illegal group type names.
+	 *
+	 * - 'any' is a special pseudo-type, representing items unassociated with any group type.
+	 * - 'null' is a special pseudo-type, representing users without any type.
+	 * - '_none' is used internally to denote an item that should not apply to any group types.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array $illegal_names Array of illegal names.
+	 */
+	$illegal_names = apply_filters( 'bp_group_type_illegal_names', array( 'any', 'null', '_none' ) );
+	if ( in_array( $group_type, $illegal_names, true ) ) {
+		return new WP_Error( 'bp_group_type_illegal_name', __( 'You may not register a group type with this name.', 'buddypress' ), $group_type );
+	}
+
+	// Store the group type name as data in the object (not just as the array key).
+	$r['name'] = $group_type;
+
+	// Make sure the relevant labels have been filled in.
+	$default_name = isset( $r['labels']['name'] ) ? $r['labels']['name'] : ucfirst( $r['name'] );
+	$r['labels'] = array_merge( array(
+		'name'          => $default_name,
+		'singular_name' => $default_name,
+	), $r['labels'] );
+
+	// Directory slug.
+	if ( ! empty( $r['has_directory'] ) ) {
+		// A string value is intepreted as the directory slug.
+		if ( is_string( $r['has_directory'] ) ) {
+			$directory_slug = $r['has_directory'];
+
+		// Otherwise fall back on group type.
+		} else {
+			$directory_slug = $group_type;
+		}
+
+		// Sanitize for use in URLs.
+		$r['directory_slug'] = sanitize_title( $directory_slug );
+		$r['has_directory']  = true;
+	} else {
+		$r['directory_slug'] = '';
+		$r['has_directory']  = false;
+	}
+
+	// Type lists.
+	if ( true === $r['show_in_create_screen'] && is_null( $r['show_in_list'] ) ) {
+		$r['show_in_list'] = true;
+	} else {
+		$r['show_in_list'] = (bool) $r['show_in_list'];
+	}
+
+	$bp->groups->types[ $group_type ] = $type = (object) $r;
+
+	/**
+	 * Fires after a group type is registered.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $group_type Group type identifier.
+	 * @param object $type       Group type object.
+	 */
+	do_action( 'bp_groups_register_group_type', $group_type, $type );
+
+	return $type;
+}
+
+/**
+ * Get a list of all registered group type objects.
+ *
+ * @since 2.6.0
+ *
+ * @see bp_groups_register_group_type() for accepted arguments.
+ *
+ * @param array|string $args     Optional. An array of key => value arguments to match against
+ *                               the group type objects. Default empty array.
+ * @param string       $output   Optional. The type of output to return. Accepts 'names'
+ *                               or 'objects'. Default 'names'.
+ * @param string       $operator Optional. The logical operation to perform. 'or' means only one
+ *                               element from the array needs to match; 'and' means all elements
+ *                               must match. Accepts 'or' or 'and'. Default 'and'.
+ * @return array       $types    A list of groups type names or objects.
+ */
+function bp_groups_get_group_types( $args = array(), $output = 'names', $operator = 'and' ) {
+	$types = buddypress()->groups->types;
+
+	$types = wp_filter_object_list( $types, $args, $operator );
+
+	/**
+	 * Filters the array of group type objects.
+	 *
+	 * This filter is run before the $output filter has been applied, so that
+	 * filtering functions have access to the entire group type objects.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array  $types     group type objects, keyed by name.
+	 * @param array  $args      Array of key=>value arguments for filtering.
+	 * @param string $operator  'or' to match any of $args, 'and' to require all.
+	 */
+	$types = apply_filters( 'bp_groups_get_group_types', $types, $args, $operator );
+
+	if ( 'names' === $output ) {
+		$types = wp_list_pluck( $types, 'name' );
+	}
+
+	return $types;
+}
+
+/**
+ * Retrieve a group type object by name.
+ *
+ * @since 2.6.0
+ *
+ * @param string $group_type The name of the group type.
+ * @return object A group type object.
+ */
+function bp_groups_get_group_type_object( $group_type ) {
+	$types = bp_groups_get_group_types( array(), 'objects' );
+
+	if ( empty( $types[ $group_type ] ) ) {
+		return null;
+	}
+
+	return $types[ $group_type ];
+}
+
+/**
+ * Set type for a group.
+ *
+ * @since 2.6.0
+ * @since 2.7.0 $group_type parameter also accepts an array of group types now.
+ *
+ * @param int          $group_id   ID of the group.
+ * @param string|array $group_type Group type or array of group types to set.
+ * @param bool         $append     Optional. True to append this to existing types for group,
+ *                                 false to replace. Default: false.
+ * @return false|array $retval See bp_set_object_terms().
+ */
+function bp_groups_set_group_type( $group_id, $group_type, $append = false ) {
+	// Pass an empty group type to remove group's type.
+	if ( ! empty( $group_type ) && is_string( $group_type ) && ! bp_groups_get_group_type_object( $group_type ) ) {
+		return false;
+	}
+
+	// Cast as array.
+	$group_type = (array) $group_type;
+
+	// Validate group types.
+	foreach ( $group_type as $type ) {
+		// Remove any invalid group types.
+		if ( is_null( bp_groups_get_group_type_object( $type ) ) ) {
+			unset( $group_type[ $type ] );
+		}
+	}
+
+	$retval = bp_set_object_terms( $group_id, $group_type, 'bp_group_type', $append );
+
+	// Bust the cache if the type has been updated.
+	if ( ! is_wp_error( $retval ) ) {
+		wp_cache_delete( $group_id, 'bp_groups_group_type' );
+
+		/**
+		 * Fires just after a group type has been changed.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param int          $group_id   ID of the group whose group type has been updated.
+		 * @param string|array $group_type Group type or array of group types.
+		 * @param bool         $append     Whether the type is being appended to existing types.
+		 */
+		do_action( 'bp_groups_set_group_type', $group_id, $group_type, $append );
+	}
+
+	return $retval;
+}
+
+/**
+ * Get type for a group.
+ *
+ * @since 2.6.0
+ *
+ * @param int  $group_id ID of the group.
+ * @param bool $single   Optional. Whether to return a single type string. If multiple types are found
+ *                       for the group, the oldest one will be returned. Default: true.
+ * @return string|array|bool On success, returns a single group type (if `$single` is true) or an array of group
+ *                           types (if `$single` is false). Returns false on failure.
+ */
+function bp_groups_get_group_type( $group_id, $single = true ) {
+	$types = wp_cache_get( $group_id, 'bp_groups_group_type' );
+
+	if ( false === $types ) {
+		$raw_types = bp_get_object_terms( $group_id, 'bp_group_type' );
+
+		if ( ! is_wp_error( $raw_types ) ) {
+			$types = array();
+
+			// Only include currently registered group types.
+			foreach ( $raw_types as $gtype ) {
+				if ( bp_groups_get_group_type_object( $gtype->name ) ) {
+					$types[] = $gtype->name;
+				}
+			}
+
+			wp_cache_set( $group_id, $types, 'bp_groups_group_type' );
+		}
+	}
+
+	$type = false;
+	if ( ! empty( $types ) ) {
+		if ( $single ) {
+			$type = end( $types );
+		} else {
+			$type = $types;
+		}
+	}
+
+	/**
+	 * Filters a groups's group type(s).
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string|array $type     Group type.
+	 * @param int          $group_id ID of the group.
+	 * @param bool         $single   Whether to return a single type string, or an array.
+	 */
+	return apply_filters( 'bp_groups_get_group_type', $type, $group_id, $single );
+}
+
+/**
+ * Remove type for a group.
+ *
+ * @since 2.6.0
+ *
+ * @param int            $group_id   ID of the user.
+ * @param string         $group_type Group type.
+ * @return bool|WP_Error $deleted    True on success. False or WP_Error on failure.
+ */
+function bp_groups_remove_group_type( $group_id, $group_type ) {
+	if ( empty( $group_type ) || ! bp_groups_get_group_type_object( $group_type ) ) {
+		return false;
+	}
+
+	$deleted = bp_remove_object_terms( $group_id, $group_type, 'bp_group_type' );
+
+	// Bust the case, if the type has been removed.
+	if ( ! is_wp_error( $deleted ) ) {
+		wp_cache_delete( $group_id, 'bp_groups_group_type' );
+
+		/**
+		 * Fires just after a group's group type has been removed.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param int    $group      ID of the group whose group type has been removed.
+		 * @param string $group_type Group type.
+		 */
+		do_action( 'bp_groups_remove_group_type', $group_id, $group_type );
+	}
+
+	return $deleted;
+}
+
+/**
+ * Check whether the given group has a certain group type.
+ *
+ * @since 2.6.0
+ *
+ * @param  int    $group_id   ID of the group.
+ * @param  string $group_type Group type.
+ * @return bool   Whether the group has the give group type.
+ */
+function bp_groups_has_group_type( $group_id, $group_type ) {
+	if ( empty( $group_type ) || ! bp_groups_get_group_type_object( $group_type ) ) {
+		return false;
+	}
+
+	// Get all group's group types.
+	$types = bp_groups_get_group_type( $group_id, false );
+
+	if ( ! is_array( $types ) ) {
+		return false;
+	}
+
+	return in_array( $group_type, $types );
+}
+
+/**
+ * Get the "current" group type, if one is provided, in group directories.
+ *
+ * @since 2.7.0
+ *
+ * @return string
+ */
+function bp_get_current_group_directory_type() {
+
+	/**
+	 * Filters the "current" group type, if one is provided, in group directories.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $value "Current" group type.
+	 */
+	return apply_filters( 'bp_get_current_group_directory_type', buddypress()->groups->current_directory_type );
+}
+
+/**
+ * Delete a group's type when the group is deleted.
+ *
+ * @since 2.6.0
+ *
+ * @param  int   $group_id ID of the group.
+ * @return array|null $value    See {@see bp_groups_set_group_type()}.
+ */
+function bp_remove_group_type_on_group_delete( $group_id = 0 ) {
+	bp_groups_set_group_type( $group_id, '' );
+}
+add_action( 'groups_delete_group', 'bp_remove_group_type_on_group_delete' );
+
+/**
+ * Finds and exports group membership data associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_groups_memberships_personal_data_exporter( $email_address, $page ) {
+	$number = 20;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$memberships = BP_Groups_Member::get_user_memberships( $user->ID, array(
+		'type'     => 'membership',
+		'page'     => $page,
+		'per_page' => $number,
+	) );
+
+	foreach ( $memberships as $membership ) {
+		$group = groups_get_group( $membership->group_id );
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Group Name', 'buddypress' ),
+				'value' => bp_get_group_name( $group ),
+			),
+			array(
+				'name'  => __( 'Group URL', 'buddypress' ),
+				'value' => bp_get_group_permalink( $group ),
+			),
+		);
+
+		if ( $membership->inviter_id ) {
+			$item_data[] = array(
+				'name'  => __( 'Invited By', 'buddypress' ),
+				'value' => bp_core_get_userlink( $membership->inviter_id ),
+			);
+		}
+
+		if ( $group->creator_id === $user->ID ) {
+			$group_role = __( 'Creator', 'buddypress' );
+		} elseif ( $membership->is_admin ) {
+			$group_role = __( 'Admin', 'buddypress' );
+		} elseif ( $membership->is_mod ) {
+			$group_role = __( 'Moderator', 'buddypress' );
+		} else {
+			$group_role = __( 'Member', 'buddypress' );
+		}
+
+		$item_data[] = array(
+			'name'  => __( 'Group Role', 'buddypress' ),
+			'value' => $group_role,
+		);
+
+		$item_data[] = array(
+			'name'  => __( 'Date Joined', 'buddypress' ),
+			'value' => $membership->date_modified,
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_groups_memberships',
+			'group_label' => __( 'Group Memberships', 'buddypress' ),
+			'item_id'     => "bp-group-membership-{$group->id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $memberships ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
+
+/**
+ * Finds and exports data on pending group membership requests associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_groups_pending_requests_personal_data_exporter( $email_address, $page ) {
+	$number = 20;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$requests = BP_Groups_Member::get_user_memberships( $user->ID, array(
+		'type'     => 'pending_request',
+		'page'     => $page,
+		'per_page' => $number,
+	) );
+
+	foreach ( $requests as $request ) {
+		$group = groups_get_group( $request->group_id );
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Group Name', 'buddypress' ),
+				'value' => bp_get_group_name( $group ),
+			),
+			array(
+				'name'  => __( 'Group URL', 'buddypress' ),
+				'value' => bp_get_group_permalink( $group ),
+			),
+			array(
+				'name'  => __( 'Date Sent', 'buddypress' ),
+				'value' => $request->date_modified,
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_groups_pending_requests',
+			'group_label' => __( 'Pending Group Membership Requests', 'buddypress' ),
+			'item_id'     => "bp-group-pending-request-{$group->id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $requests ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
+
+/**
+ * Finds and exports data on pending group invitations sent by a user associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_groups_pending_sent_invitations_personal_data_exporter( $email_address, $page ) {
+	$number = 20;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$invitations = BP_Groups_Member::get_user_memberships( $user->ID, array(
+		'type'     => 'pending_sent_invitation',
+		'page'     => $page,
+		'per_page' => $number,
+	) );
+
+	foreach ( $invitations as $invitation ) {
+		$group = groups_get_group( $invitation->group_id );
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Group Name', 'buddypress' ),
+				'value' => bp_get_group_name( $group ),
+			),
+			array(
+				'name'  => __( 'Group URL', 'buddypress' ),
+				'value' => bp_get_group_permalink( $group ),
+			),
+			array(
+				'name'  => __( 'Sent To', 'buddypress' ),
+				'value' => bp_core_get_userlink( $invitation->user_id ),
+			),
+			array(
+				'name'  => __( 'Date Sent', 'buddypress' ),
+				'value' => $invitation->date_modified,
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_groups_pending_sent_invitations',
+			'group_label' => __( 'Pending Group Invitations (Sent)', 'buddypress' ),
+			'item_id'     => "bp-group-pending-sent-invitation-{$group->id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $invitations ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
+
+/**
+ * Finds and exports data on pending group invitations received by a user associated with an email address.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_groups_pending_received_invitations_personal_data_exporter( $email_address, $page ) {
+	$number = 20;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$invitations = BP_Groups_Member::get_user_memberships( $user->ID, array(
+		'type'     => 'pending_received_invitation',
+		'page'     => $page,
+		'per_page' => $number,
+	) );
+
+	foreach ( $invitations as $invitation ) {
+		$group = groups_get_group( $invitation->group_id );
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Group Name', 'buddypress' ),
+				'value' => bp_get_group_name( $group ),
+			),
+			array(
+				'name'  => __( 'Group URL', 'buddypress' ),
+				'value' => bp_get_group_permalink( $group ),
+			),
+			array(
+				'name'  => __( 'Invited By', 'buddypress' ),
+				'value' => bp_core_get_userlink( $invitation->inviter_id ),
+			),
+			array(
+				'name'  => __( 'Date Sent', 'buddypress' ),
+				'value' => $invitation->date_modified,
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_groups_pending_received_invitations',
+			'group_label' => __( 'Pending Group Invitations (Received)', 'buddypress' ),
+			'item_id'     => "bp-group-pending-received-invitation-{$group->id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $invitations ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-loader.php	(revision 12265)
@@ -0,0 +1,25 @@
+<?php
+/**
+ * BuddyPress Groups Loader.
+ *
+ * A groups component, for users to group themselves together. Includes a
+ * robust sub-component API that allows Groups to be extended.
+ * Comes preconfigured with an activity stream, discussion forums, and settings.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-groups component.
+ *
+ * @since 1.5.0
+ */
+function bp_setup_groups() {
+	buddypress()->groups = new BP_Groups_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_groups', 6 );
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-notifications.php	(revision 12265)
@@ -0,0 +1,1166 @@
+<?php
+/**
+ * BuddyPress Groups Notification Functions.
+ *
+ * These functions handle the recording, deleting and formatting of notifications
+ * for the user and for this specific component.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsActivity
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Emails ********************************************************************/
+
+/**
+ * Notify all group members when a group is updated.
+ *
+ * @since 1.0.0
+ *
+ * @param int                  $group_id  ID of the group.
+ * @param BP_Groups_Group|null $old_group Group before new details were saved.
+ */
+function groups_notification_group_updated( $group_id = 0, $old_group = null ) {
+	$group = groups_get_group( $group_id );
+
+	if ( $old_group instanceof BP_Groups_Group ) {
+		$changed = array();
+
+		if ( $group->name !== $old_group->name ) {
+			$changed[] = sprintf(
+				_x( '* Name changed from "%s" to "%s".', 'Group update email text', 'buddypress' ),
+				esc_html( $old_group->name ),
+				esc_html( $group->name )
+			);
+		}
+
+		if ( $group->description !== $old_group->description ) {
+			$changed[] = sprintf(
+				_x( '* Description changed from "%s" to "%s".', 'Group update email text', 'buddypress' ),
+				esc_html( $old_group->description ),
+				esc_html( $group->description )
+			);
+		}
+
+		if ( $group->slug !== $old_group->slug ) {
+			$changed[] = sprintf(
+				_x( '* Permalink changed from "%s" to "%s".', 'Group update email text', 'buddypress' ),
+				esc_url( bp_get_group_permalink( $old_group ) ),
+				esc_url( bp_get_group_permalink( $group ) )
+			);
+		}
+	}
+
+	/**
+	 * Filters the bullet points listing updated items in the email notification after a group is updated.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $changed Array of bullet points.
+	 */
+	$changed = apply_filters( 'groups_notification_group_update_updated_items', $changed );
+
+	$changed_text = '';
+	if ( ! empty( $changed ) ) {
+		$changed_text = implode( "\n", $changed );
+	}
+
+	$user_ids = BP_Groups_Member::get_group_member_ids( $group->id );
+	foreach ( (array) $user_ids as $user_id ) {
+
+		// Continue if member opted out of receiving this email.
+		if ( 'no' === bp_get_user_meta( $user_id, 'notification_groups_group_updated', true ) ) {
+			continue;
+		}
+
+		$unsubscribe_args = array(
+			'user_id'           => $user_id,
+			'notification_type' => 'groups-details-updated',
+		);
+
+		$args = array(
+			'tokens' => array(
+				'changed_text' => $changed_text,
+				'group'        => $group,
+				'group.id'     => $group_id,
+				'group.url'    => esc_url( bp_get_group_permalink( $group ) ),
+				'group.name'   => $group->name,
+				'unsubscribe'  => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+			),
+		);
+		bp_send_email( 'groups-details-updated', (int) $user_id, $args );
+	}
+
+	/**
+	 * Fires after the notification is sent that a group has been updated.
+	 *
+	 * See https://buddypress.trac.wordpress.org/ticket/3644 for blank message parameter.
+	 *
+	 * @since 1.5.0
+	 * @since 2.5.0 $subject has been unset and is deprecated.
+	 *
+	 * @param array  $user_ids Array of user IDs to notify about the update.
+	 * @param string $subject  Deprecated in 2.5; now an empty string.
+	 * @param string $value    Empty string preventing PHP error.
+	 * @param int    $group_id ID of the group that was updated.
+	 */
+	do_action( 'bp_groups_sent_updated_email', $user_ids, '', '', $group_id );
+}
+
+/**
+ * Notify group admin about new membership request.
+ *
+ * @since 1.0.0
+ *
+ * @param int $requesting_user_id ID of the user requesting group membership.
+ * @param int $admin_id           ID of the group admin.
+ * @param int $group_id           ID of the group.
+ * @param int $membership_id      ID of the group membership object.
+ */
+function groups_notification_new_membership_request( $requesting_user_id = 0, $admin_id = 0, $group_id = 0, $membership_id = 0 ) {
+
+	// Trigger a BuddyPress Notification.
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_add_notification( array(
+			'user_id'           => $admin_id,
+			'item_id'           => $group_id,
+			'secondary_item_id' => $requesting_user_id,
+			'component_name'    => buddypress()->groups->id,
+			'component_action'  => 'new_membership_request',
+		) );
+	}
+
+	// Bail if member opted out of receiving this email.
+	if ( 'no' === bp_get_user_meta( $admin_id, 'notification_groups_membership_request', true ) ) {
+		return;
+	}
+
+	$unsubscribe_args = array(
+		'user_id'           => $admin_id,
+		'notification_type' => 'groups-membership-request',
+	);
+
+	$group = groups_get_group( $group_id );
+	$args  = array(
+		'tokens' => array(
+			'admin.id'             => $admin_id,
+			'group'                => $group,
+			'group.name'           => $group->name,
+			'group.id'             => $group_id,
+			'group-requests.url'   => esc_url( bp_get_group_permalink( $group ) . 'admin/membership-requests' ),
+			'membership.id'        => $membership_id,
+			'profile.url'          => esc_url( bp_core_get_user_domain( $requesting_user_id ) ),
+			'requesting-user.id'   => $requesting_user_id,
+			'requesting-user.name' => bp_core_get_user_displayname( $requesting_user_id ),
+			'unsubscribe'          => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+		),
+	);
+	bp_send_email( 'groups-membership-request', (int) $admin_id, $args );
+}
+
+/**
+ * Notify member about their group membership request.
+ *
+ * @since 1.0.0
+ *
+ * @param int  $requesting_user_id ID of the user requesting group membership.
+ * @param int  $group_id           ID of the group.
+ * @param bool $accepted           Optional. Whether the membership request was accepted.
+ *                                 Default: true.
+ */
+function groups_notification_membership_request_completed( $requesting_user_id = 0, $group_id = 0, $accepted = true ) {
+
+	// Trigger a BuddyPress Notification.
+	if ( bp_is_active( 'notifications' ) ) {
+
+		// What type of acknowledgement.
+		$type = ! empty( $accepted ) ? 'membership_request_accepted' : 'membership_request_rejected';
+
+		bp_notifications_add_notification( array(
+			'user_id'           => $requesting_user_id,
+			'item_id'           => $group_id,
+			'component_name'    => buddypress()->groups->id,
+			'component_action'  => $type,
+		) );
+	}
+
+	// Bail if member opted out of receiving this email.
+	if ( 'no' === bp_get_user_meta( $requesting_user_id, 'notification_membership_request_completed', true ) ) {
+		return;
+	}
+
+	$group = groups_get_group( $group_id );
+	$args  = array(
+		'tokens' => array(
+			'group'              => $group,
+			'group.id'           => $group_id,
+			'group.name'         => $group->name,
+			'group.url'          => esc_url( bp_get_group_permalink( $group ) ),
+			'requesting-user.id' => $requesting_user_id,
+		),
+	);
+
+	if ( ! empty( $accepted ) ) {
+
+		$unsubscribe_args = array(
+			'user_id'           => $requesting_user_id,
+			'notification_type' => 'groups-membership-request-accepted',
+		);
+
+		$args['tokens']['unsubscribe'] = esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) );
+
+		bp_send_email( 'groups-membership-request-accepted', (int) $requesting_user_id, $args );
+
+	} else {
+
+		$unsubscribe_args = array(
+			'user_id'           => $requesting_user_id,
+			'notification_type' => 'groups-membership-request-rejected',
+		);
+
+		$args['tokens']['unsubscribe'] = esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) );
+
+		bp_send_email( 'groups-membership-request-rejected', (int) $requesting_user_id, $args );
+	}
+}
+add_action( 'groups_membership_accepted', 'groups_notification_membership_request_completed', 10, 3 );
+add_action( 'groups_membership_rejected', 'groups_notification_membership_request_completed', 10, 3 );
+
+/**
+ * Notify group member they have been promoted.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ */
+function groups_notification_promoted_member( $user_id = 0, $group_id = 0 ) {
+
+	// What type of promotion is this?
+	if ( groups_is_user_admin( $user_id, $group_id ) ) {
+		$promoted_to = __( 'an administrator', 'buddypress' );
+		$type        = 'member_promoted_to_admin';
+	} else {
+		$promoted_to = __( 'a moderator', 'buddypress' );
+		$type        = 'member_promoted_to_mod';
+	}
+
+	// Trigger a BuddyPress Notification.
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_add_notification( array(
+			'user_id'           => $user_id,
+			'item_id'           => $group_id,
+			'component_name'    => buddypress()->groups->id,
+			'component_action'  => $type,
+		) );
+	}
+
+	// Bail if admin opted out of receiving this email.
+	if ( 'no' === bp_get_user_meta( $user_id, 'notification_groups_admin_promotion', true ) ) {
+		return;
+	}
+
+	$unsubscribe_args = array(
+		'user_id'           => $user_id,
+		'notification_type' => 'groups-member-promoted',
+	);
+
+	$group = groups_get_group( $group_id );
+	$args  = array(
+		'tokens' => array(
+			'group'       => $group,
+			'group.id'    => $group_id,
+			'group.url'   => esc_url( bp_get_group_permalink( $group ) ),
+			'group.name'  => $group->name,
+			'promoted_to' => $promoted_to,
+			'user.id'     => $user_id,
+			'unsubscribe' => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+		),
+	);
+	bp_send_email( 'groups-member-promoted', (int) $user_id, $args );
+}
+add_action( 'groups_promoted_member', 'groups_notification_promoted_member', 10, 2 );
+
+/**
+ * Notify a member they have been invited to a group.
+ *
+ * @since 1.0.0
+ *
+ * @param BP_Groups_Group  $group           Group object.
+ * @param BP_Groups_Member $member          Member object.
+ * @param int              $inviter_user_id ID of the user who sent the invite.
+ */
+function groups_notification_group_invites( &$group, &$member, $inviter_user_id ) {
+
+	// Bail if member has already been invited.
+	if ( ! empty( $member->invite_sent ) ) {
+		return;
+	}
+
+	// @todo $inviter_ud may be used for caching, test without it
+	$inviter_ud      = bp_core_get_core_userdata( $inviter_user_id );
+	$invited_user_id = $member->user_id;
+
+	// Trigger a BuddyPress Notification.
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_add_notification( array(
+			'user_id'          => $invited_user_id,
+			'item_id'          => $group->id,
+			'component_name'   => buddypress()->groups->id,
+			'component_action' => 'group_invite',
+		) );
+	}
+
+	// Bail if member opted out of receiving this email.
+	if ( 'no' === bp_get_user_meta( $invited_user_id, 'notification_groups_invite', true ) ) {
+		return;
+	}
+
+	$invited_link = bp_core_get_user_domain( $invited_user_id ) . bp_get_groups_slug();
+
+	$unsubscribe_args = array(
+		'user_id'           => $invited_user_id,
+		'notification_type' => 'groups-invitation',
+	);
+
+	$args         = array(
+		'tokens' => array(
+			'group'        => $group,
+			'group.url'    => bp_get_group_permalink( $group ),
+			'group.name'   => $group->name,
+			'inviter.name' => bp_core_get_userlink( $inviter_user_id, true, false, true ),
+			'inviter.url'  => bp_core_get_user_domain( $inviter_user_id ),
+			'inviter.id'   => $inviter_user_id,
+			'invites.url'  => esc_url( $invited_link . '/invites/' ),
+			'unsubscribe'  => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+		),
+	);
+	bp_send_email( 'groups-invitation', (int) $invited_user_id, $args );
+}
+
+/** Notifications *************************************************************/
+
+/**
+ * Format notifications for the Groups component.
+ *
+ * @since 1.0.0
+ *
+ * @param string $action            The kind of notification being rendered.
+ * @param int    $item_id           The primary item ID.
+ * @param int    $secondary_item_id The secondary item ID.
+ * @param int    $total_items       The total number of messaging-related notifications
+ *                                  waiting for the user.
+ * @param string $format            'string' for BuddyBar-compatible notifications; 'array'
+ *                                  for WP Toolbar. Default: 'string'.
+ * @return string
+ */
+function groups_format_notifications( $action, $item_id, $secondary_item_id, $total_items, $format = 'string' ) {
+
+	switch ( $action ) {
+		case 'new_membership_request':
+			$group_id = $item_id;
+			$requesting_user_id = $secondary_item_id;
+
+			$group = groups_get_group( $group_id );
+			$group_link = bp_get_group_permalink( $group );
+			$amount = 'single';
+
+			// Set up the string and the filter
+			// because different values are passed to the filters,
+			// we'll return values inline.
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( '%1$d new membership requests for the group "%2$s"', 'buddypress' ), (int) $total_items, $group->name );
+				$amount = 'multiple';
+				$notification_link = $group_link . 'admin/membership-requests/?n=1';
+
+				if ( 'string' == $format ) {
+
+					/**
+					 * Filters groups multiple new membership request notification for string format.
+					 *
+					 * This is a dynamic filter that is dependent on item count and action.
+					 * Complete filter - bp_groups_multiple_new_membership_requests_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for request.
+					 * @param string $group_link        The permalink for the group.
+					 * @param int    $total_items       Total number of membership requests.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . 's_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $total_items, $group->name, $text, $notification_link );
+				} else {
+
+					/**
+					 * Filters groups multiple new membership request notification for any non-string format.
+					 *
+					 * This is a dynamic filter that is dependent on item count and action.
+					 * Complete filter - bp_groups_multiple_new_membership_requests_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param string $group_link        The permalink for the group.
+					 * @param int    $total_items       Total number of membership requests.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . 's_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $total_items, $group->name, $text, $notification_link );
+				}
+			} else {
+				$user_fullname = bp_core_get_user_displayname( $requesting_user_id );
+				$text = sprintf( __( '%s requests group membership', 'buddypress' ), $user_fullname );
+				$notification_link = $group_link . 'admin/membership-requests/?n=1';
+
+				if ( 'string' == $format ) {
+
+					/**
+					 * Filters groups single new membership request notification for string format.
+					 *
+					 * This is a dynamic filter that is dependent on item count and action.
+					 * Complete filter - bp_groups_single_new_membership_request_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for request.
+					 * @param string $group_link        The permalink for the group.
+					 * @param string $user_fullname     Full name of requesting user.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $user_fullname, $group->name, $text, $notification_link );
+				} else {
+
+					/**
+					 * Filters groups single new membership request notification for any non-string format.
+					 *
+					 * This is a dynamic filter that is dependent on item count and action.
+					 * Complete filter - bp_groups_single_new_membership_request_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param string $group_link        The permalink for the group.
+					 * @param string $user_fullname     Full name of requesting user.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $user_fullname, $group->name, $text, $notification_link );
+				}
+			}
+
+			break;
+
+		case 'membership_request_accepted':
+			$group_id = $item_id;
+
+			$group = groups_get_group( $group_id );
+			$group_link = bp_get_group_permalink( $group );
+			$amount = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( '%d accepted group membership requests', 'buddypress' ), (int) $total_items, $group->name );
+				$amount = 'multiple';
+				$notification_link = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() ) . '?n=1';
+
+				if ( 'string' == $format ) {
+
+					/**
+					 * Filters multiple accepted group membership requests notification for string format.
+					 * Complete filter - bp_groups_multiple_membership_request_accepted_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $total_items       Total number of accepted requests.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $total_items, $group->name, $text, $notification_link );
+				} else {
+
+					/**
+					 * Filters multiple accepted group membership requests notification for non-string format.
+					 * Complete filter - bp_groups_multiple_membership_request_accepted_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification
+					 * @param int    $total_items       Total number of accepted requests.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $total_items, $group->name, $text, $notification_link );
+				}
+			} else {
+				$text = sprintf( __( 'Membership for group "%s" accepted', 'buddypress' ), $group->name );
+				$filter = 'bp_groups_single_membership_request_accepted_notification';
+				$notification_link = $group_link . '?n=1';
+
+				if ( 'string' == $format ) {
+
+					/**
+					 * Filters single accepted group membership request notification for string format.
+					 * Complete filter - bp_groups_single_membership_request_accepted_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param string $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $group->name, $text, $notification_link );
+				} else {
+
+					/**
+					 * Filters single accepted group membership request notification for non-string format.
+					 * Complete filter - bp_groups_single_membership_request_accepted_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param string $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( $filter, array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $group->name, $text, $notification_link );
+				}
+			}
+
+			break;
+
+		case 'membership_request_rejected':
+			$group_id = $item_id;
+
+			$group = groups_get_group( $group_id );
+			$group_link = bp_get_group_permalink( $group );
+			$amount = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( '%d rejected group membership requests', 'buddypress' ), (int) $total_items, $group->name );
+				$amount = 'multiple';
+				$notification_link = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() ) . '?n=1';
+
+				if ( 'string' == $format ) {
+
+					/**
+					 * Filters multiple rejected group membership requests notification for string format.
+					 * Complete filter - bp_groups_multiple_membership_request_rejected_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $total_items, $group->name );
+				} else {
+
+					/**
+					 * Filters multiple rejected group membership requests notification for non-string format.
+					 * Complete filter - bp_groups_multiple_membership_request_rejected_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $total_items, $group->name, $text, $notification_link );
+				}
+			} else {
+				$text = sprintf( __( 'Membership for group "%s" rejected', 'buddypress' ), $group->name );
+				$notification_link = $group_link . '?n=1';
+
+				if ( 'string' == $format ) {
+
+					/**
+					 * Filters single rejected group membership requests notification for string format.
+					 * Complete filter - bp_groups_single_membership_request_rejected_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $group->name, $text, $notification_link );
+				} else {
+
+					/**
+					 * Filters single rejected group membership requests notification for non-string format.
+					 * Complete filter - bp_groups_single_membership_request_rejected_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $group->name, $text, $notification_link );
+				}
+			}
+
+			break;
+
+		case 'member_promoted_to_admin':
+			$group_id = $item_id;
+
+			$group = groups_get_group( $group_id );
+			$group_link = bp_get_group_permalink( $group );
+			$amount = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( 'You were promoted to an admin in %d groups', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+				$notification_link = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() ) . '?n=1';
+
+				if ( 'string' == $format ) {
+					/**
+					 * Filters multiple promoted to group admin notification for string format.
+					 * Complete filter - bp_groups_multiple_member_promoted_to_admin_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $total_items, $text, $notification_link );
+				} else {
+					/**
+					 * Filters multiple promoted to group admin notification for non-string format.
+					 * Complete filter - bp_groups_multiple_member_promoted_to_admin_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $total_items, $text, $notification_link );
+				}
+			} else {
+				$text = sprintf( __( 'You were promoted to an admin in the group "%s"', 'buddypress' ), $group->name );
+				$notification_link = $group_link . '?n=1';
+
+				if ( 'string' == $format ) {
+					/**
+					 * Filters single promoted to group admin notification for non-string format.
+					 * Complete filter - bp_groups_single_member_promoted_to_admin_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $group->name, $text, $notification_link );
+				} else {
+					/**
+					 * Filters single promoted to group admin notification for non-string format.
+					 * Complete filter - bp_groups_single_member_promoted_to_admin_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $group->name, $text, $notification_link );
+				}
+			}
+
+			break;
+
+		case 'member_promoted_to_mod':
+			$group_id = $item_id;
+
+			$group = groups_get_group( $group_id );
+			$group_link = bp_get_group_permalink( $group );
+			$amount = 'single';
+
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( 'You were promoted to a mod in %d groups', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+				$notification_link = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() ) . '?n=1';
+
+				if ( 'string' == $format ) {
+					/**
+					 * Filters multiple promoted to group mod notification for string format.
+					 * Complete filter - bp_groups_multiple_member_promoted_to_mod_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $total_items, $text, $notification_link );
+				} else {
+					/**
+					 * Filters multiple promoted to group mod notification for non-string format.
+					 * Complete filter - bp_groups_multiple_member_promoted_to_mod_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $total_items, $text, $notification_link );
+				}
+			} else {
+				$text = sprintf( __( 'You were promoted to a mod in the group "%s"', 'buddypress' ), $group->name );
+				$notification_link = $group_link . '?n=1';
+
+				if ( 'string' == $format ) {
+					/**
+					 * Filters single promoted to group mod notification for string format.
+					 * Complete filter - bp_groups_single_member_promoted_to_mod_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $group->name, $text, $notification_link );
+				} else {
+					/**
+					 * Filters single promoted to group admin notification for non-string format.
+					 * Complete filter - bp_groups_single_member_promoted_to_mod_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $group->name, $text, $notification_link );
+				}
+			}
+
+			break;
+
+		case 'group_invite':
+			$group_id = $item_id;
+			$group = groups_get_group( $group_id );
+			$group_link = bp_get_group_permalink( $group );
+			$amount = 'single';
+
+			$notification_link = bp_loggedin_user_domain() . bp_get_groups_slug() . '/invites/?n=1';
+
+			if ( (int) $total_items > 1 ) {
+				$text = sprintf( __( 'You have %d new group invitations', 'buddypress' ), (int) $total_items );
+				$amount = 'multiple';
+
+				if ( 'string' == $format ) {
+					/**
+					 * Filters multiple group invitation notification for string format.
+					 * Complete filter - bp_groups_multiple_group_invite_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $total_items, $text, $notification_link );
+				} else {
+					/**
+					 * Filters multiple group invitation notification for non-string format.
+					 * Complete filter - bp_groups_multiple_group_invite_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $total_items       Total number of rejected requests.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $total_items, $text, $notification_link );
+				}
+			} else {
+				$text = sprintf( __( 'You have an invitation to the group: %s', 'buddypress' ), $group->name );
+				$filter = 'bp_groups_single_group_invite_notification';
+
+				if ( 'string' == $format ) {
+					/**
+					 * Filters single group invitation notification for string format.
+					 * Complete filter - bp_groups_single_group_invite_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param string $string            HTML anchor tag for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', '<a href="' . $notification_link . '">' . $text . '</a>', $group_link, $group->name, $text, $notification_link );
+				} else {
+					/**
+					 * Filters single group invitation notification for non-string format.
+					 * Complete filter - bp_groups_single_group_invite_notification.
+					 *
+					 * @since 1.0.0
+					 *
+					 * @param array  $array             Array holding permalink and content for notification.
+					 * @param int    $group_link        The permalink for the group.
+					 * @param string $group->name       Name of the group.
+					 * @param string $text              Notification content.
+					 * @param string $notification_link The permalink for notification.
+					 */
+					return apply_filters( 'bp_groups_' . $amount . '_' . $action . '_notification', array(
+						'link' => $notification_link,
+						'text' => $text
+					), $group_link, $group->name, $text, $notification_link );
+				}
+			}
+
+			break;
+
+		default:
+
+			/**
+			 * Filters plugin-added group-related custom component_actions.
+			 *
+			 * @since 2.4.0
+			 *
+			 * @param string $notification      Null value.
+			 * @param int    $item_id           The primary item ID.
+			 * @param int    $secondary_item_id The secondary item ID.
+			 * @param int    $total_items       The total number of messaging-related notifications
+			 *                                  waiting for the user.
+			 * @param string $format            'string' for BuddyBar-compatible notifications;
+			 *                                  'array' for WP Toolbar.
+			 */
+			$custom_action_notification = apply_filters( 'bp_groups_' . $action . '_notification', null, $item_id, $secondary_item_id, $total_items, $format );
+
+			if ( ! is_null( $custom_action_notification ) ) {
+				return $custom_action_notification;
+			}
+
+			break;
+	}
+
+	/**
+	 * Fires right before returning the formatted group notifications.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $action            The type of notification being rendered.
+	 * @param int    $item_id           The primary item ID.
+	 * @param int    $secondary_item_id The secondary item ID.
+	 * @param int    $total_items       Total amount of items to format.
+	 */
+	do_action( 'groups_format_notifications', $action, $item_id, $secondary_item_id, $total_items );
+
+	return false;
+}
+
+/**
+ * Remove all notifications for any member belonging to a specific group.
+ *
+ * @since 1.9.0
+ *
+ * @param int $group_id ID of the group.
+ */
+function bp_groups_delete_group_delete_all_notifications( $group_id ) {
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_delete_all_notifications_by_type( $group_id, buddypress()->groups->id );
+	}
+}
+add_action( 'groups_delete_group', 'bp_groups_delete_group_delete_all_notifications', 10 );
+
+/**
+ * When a demotion takes place, delete any corresponding promotion notifications.
+ *
+ * @since 2.0.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ */
+function bp_groups_delete_promotion_notifications( $user_id = 0, $group_id = 0 ) {
+	if ( bp_is_active( 'notifications' ) && ! empty( $group_id ) && ! empty( $user_id ) ) {
+		bp_notifications_delete_notifications_by_item_id( $user_id, $group_id, buddypress()->groups->id, 'member_promoted_to_admin' );
+		bp_notifications_delete_notifications_by_item_id( $user_id, $group_id, buddypress()->groups->id, 'member_promoted_to_mod' );
+	}
+}
+add_action( 'groups_demoted_member', 'bp_groups_delete_promotion_notifications', 10, 2 );
+
+/**
+ * Mark notifications read when a member accepts a group invitation.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ */
+function bp_groups_accept_invite_mark_notifications( $user_id, $group_id ) {
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_mark_notifications_by_item_id( $user_id, $group_id, buddypress()->groups->id, 'group_invite' );
+	}
+}
+add_action( 'groups_accept_invite', 'bp_groups_accept_invite_mark_notifications', 10, 2 );
+add_action( 'groups_reject_invite', 'bp_groups_accept_invite_mark_notifications', 10, 2 );
+add_action( 'groups_delete_invite', 'bp_groups_accept_invite_mark_notifications', 10, 2 );
+
+/**
+ * Mark notifications read when a member's group membership request is granted.
+ *
+ * @since 2.8.0
+ *
+ * @param int $user_id  ID of the user.
+ * @param int $group_id ID of the group.
+ */
+function bp_groups_accept_request_mark_notifications( $user_id, $group_id ) {
+	if ( bp_is_active( 'notifications' ) ) {
+		// First null parameter marks read for all admins.
+		bp_notifications_mark_notifications_by_item_id( null, $group_id, buddypress()->groups->id, 'new_membership_request', $user_id );
+	}
+}
+add_action( 'groups_membership_accepted', 'bp_groups_accept_request_mark_notifications', 10, 2 );
+add_action( 'groups_membership_rejected', 'bp_groups_accept_request_mark_notifications', 10, 2 );
+
+/**
+ * Mark notifications read when a member views their group memberships.
+ *
+ * @since 1.9.0
+ */
+function bp_groups_screen_my_groups_mark_notifications() {
+
+	// Delete group request notifications for the user.
+	if ( isset( $_GET['n'] ) && bp_is_active( 'notifications' ) ) {
+
+		// Get the necessary ID's.
+		$group_id = buddypress()->groups->id;
+		$user_id  = bp_loggedin_user_id();
+
+		// Mark notifications read.
+		bp_notifications_mark_notifications_by_type( $user_id, $group_id, 'membership_request_accepted' );
+		bp_notifications_mark_notifications_by_type( $user_id, $group_id, 'membership_request_rejected' );
+		bp_notifications_mark_notifications_by_type( $user_id, $group_id, 'member_promoted_to_mod'      );
+		bp_notifications_mark_notifications_by_type( $user_id, $group_id, 'member_promoted_to_admin'    );
+	}
+}
+add_action( 'groups_screen_my_groups',  'bp_groups_screen_my_groups_mark_notifications', 10 );
+add_action( 'groups_screen_group_home', 'bp_groups_screen_my_groups_mark_notifications', 10 );
+
+/**
+ * Mark group invitation notifications read when a member views their invitations.
+ *
+ * @since 1.9.0
+ */
+function bp_groups_screen_invites_mark_notifications() {
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_mark_notifications_by_type( bp_loggedin_user_id(), buddypress()->groups->id, 'group_invite' );
+	}
+}
+add_action( 'groups_screen_group_invites', 'bp_groups_screen_invites_mark_notifications', 10 );
+
+/**
+ * Mark group join requests read when an admin or moderator visits the group administration area.
+ *
+ * @since 1.9.0
+ */
+function bp_groups_screen_group_admin_requests_mark_notifications() {
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_mark_notifications_by_type( bp_loggedin_user_id(), buddypress()->groups->id, 'new_membership_request' );
+	}
+}
+add_action( 'groups_screen_group_admin_requests', 'bp_groups_screen_group_admin_requests_mark_notifications', 10 );
+
+/**
+ * Delete new group membership notifications when a user is being deleted.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id ID of the user.
+ */
+function bp_groups_remove_data_for_user_notifications( $user_id ) {
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_delete_notifications_from_user( $user_id, buddypress()->groups->id, 'new_membership_request' );
+	}
+}
+add_action( 'groups_remove_data_for_user', 'bp_groups_remove_data_for_user_notifications', 10 );
+
+/**
+ * Render the group settings fields on the Notification Settings page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_notification_settings() {
+
+	if ( !$group_invite = bp_get_user_meta( bp_displayed_user_id(), 'notification_groups_invite', true ) )
+		$group_invite  = 'yes';
+
+	if ( !$group_update = bp_get_user_meta( bp_displayed_user_id(), 'notification_groups_group_updated', true ) )
+		$group_update  = 'yes';
+
+	if ( !$group_promo = bp_get_user_meta( bp_displayed_user_id(), 'notification_groups_admin_promotion', true ) )
+		$group_promo   = 'yes';
+
+	if ( !$group_request = bp_get_user_meta( bp_displayed_user_id(), 'notification_groups_membership_request', true ) )
+		$group_request = 'yes';
+
+	if ( ! $group_request_completed = bp_get_user_meta( bp_displayed_user_id(), 'notification_membership_request_completed', true ) ) {
+		$group_request_completed = 'yes';
+	}
+	?>
+
+	<table class="notification-settings" id="groups-notification-settings">
+		<thead>
+			<tr>
+				<th class="icon"></th>
+				<th class="title"><?php _ex( 'Groups', 'Group settings on notification settings page', 'buddypress' ) ?></th>
+				<th class="yes"><?php _e( 'Yes', 'buddypress' ) ?></th>
+				<th class="no"><?php _e( 'No', 'buddypress' )?></th>
+			</tr>
+		</thead>
+
+		<tbody>
+			<tr id="groups-notification-settings-invitation">
+				<td></td>
+				<td><?php _ex( 'A member invites you to join a group', 'group settings on notification settings page','buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_groups_invite]" id="notification-groups-invite-yes" value="yes" <?php checked( $group_invite, 'yes', true ) ?>/><label for="notification-groups-invite-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_groups_invite]" id="notification-groups-invite-no" value="no" <?php checked( $group_invite, 'no', true ) ?>/><label for="notification-groups-invite-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+			<tr id="groups-notification-settings-info-updated">
+				<td></td>
+				<td><?php _ex( 'Group information is updated', 'group settings on notification settings page', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_groups_group_updated]" id="notification-groups-group-updated-yes" value="yes" <?php checked( $group_update, 'yes', true ) ?>/><label for="notification-groups-group-updated-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_groups_group_updated]" id="notification-groups-group-updated-no" value="no" <?php checked( $group_update, 'no', true ) ?>/><label for="notification-groups-group-updated-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+			<tr id="groups-notification-settings-promoted">
+				<td></td>
+				<td><?php _ex( 'You are promoted to a group administrator or moderator', 'group settings on notification settings page', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_groups_admin_promotion]" id="notification-groups-admin-promotion-yes" value="yes" <?php checked( $group_promo, 'yes', true ) ?>/><label for="notification-groups-admin-promotion-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_groups_admin_promotion]" id="notification-groups-admin-promotion-no" value="no" <?php checked( $group_promo, 'no', true ) ?>/><label for="notification-groups-admin-promotion-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+			<tr id="groups-notification-settings-request">
+				<td></td>
+				<td><?php _ex( 'A member requests to join a private group for which you are an admin', 'group settings on notification settings page', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_groups_membership_request]" id="notification-groups-membership-request-yes" value="yes" <?php checked( $group_request, 'yes', true ) ?>/><label for="notification-groups-membership-request-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_groups_membership_request]" id="notification-groups-membership-request-no" value="no" <?php checked( $group_request, 'no', true ) ?>/><label for="notification-groups-membership-request-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+			<tr id="groups-notification-settings-request-completed">
+				<td></td>
+				<td><?php _ex( 'Your request to join a group has been approved or denied', 'group settings on notification settings page', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_membership_request_completed]" id="notification-groups-membership-request-completed-yes" value="yes" <?php checked( $group_request_completed, 'yes', true ) ?>/><label for="notification-groups-membership-request-completed-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_membership_request_completed]" id="notification-groups-membership-request-completed-no" value="no" <?php checked( $group_request_completed, 'no', true ) ?>/><label for="notification-groups-membership-request-completed-no" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'No, do not send email', 'buddypress' );
+				?></label></td>
+			</tr>
+
+			<?php
+
+			/**
+			 * Fires at the end of the available group settings fields on Notification Settings page.
+			 *
+			 * @since 1.0.0
+			 */
+			do_action( 'groups_screen_notification_settings' ); ?>
+
+		</tbody>
+	</table>
+
+<?php
+}
+add_action( 'bp_notification_settings', 'groups_screen_notification_settings' );
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-template.php	(revision 12265)
@@ -0,0 +1,6163 @@
+<?php
+/**
+ * BuddyPress Groups Template Functions.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsTemplates
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the groups component slug.
+ *
+ * @since 1.5.0
+ */
+function bp_groups_slug() {
+	echo bp_get_groups_slug();
+}
+	/**
+	 * Return the groups component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_groups_slug() {
+
+		/**
+		 * Filters the groups component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Groups component slug.
+		 */
+		return apply_filters( 'bp_get_groups_slug', buddypress()->groups->slug );
+	}
+
+/**
+ * Output the groups component root slug.
+ *
+ * @since 1.5.0
+ */
+function bp_groups_root_slug() {
+	echo bp_get_groups_root_slug();
+}
+	/**
+	 * Return the groups component root slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_groups_root_slug() {
+
+		/**
+		 * Filters the groups component root slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $root_slug Groups component root slug.
+		 */
+		return apply_filters( 'bp_get_groups_root_slug', buddypress()->groups->root_slug );
+	}
+
+/**
+ * Output the group type base slug.
+ *
+ * @since 2.7.0
+ */
+function bp_groups_group_type_base() {
+	echo esc_url( bp_get_groups_group_type_base() );
+}
+	/**
+	 * Get the group type base slug.
+	 *
+	 * The base slug is the string used as the base prefix when generating group
+	 * type directory URLs. For example, in example.com/groups/type/foo/, 'foo' is
+	 * the group type and 'type' is the base slug.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return string
+	 */
+	function bp_get_groups_group_type_base() {
+		/**
+		 * Filters the group type URL base.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string $base
+		 */
+		return apply_filters( 'bp_groups_group_type_base', _x( 'type', 'group type URL base', 'buddypress' ) );
+	}
+
+/**
+ * Output group directory permalink.
+ *
+ * @since 1.5.0
+ */
+function bp_groups_directory_permalink() {
+	echo esc_url( bp_get_groups_directory_permalink() );
+}
+	/**
+	 * Return group directory permalink.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_groups_directory_permalink() {
+
+		/**
+		 * Filters the group directory permalink.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Permalink for the group directory.
+		 */
+		return apply_filters( 'bp_get_groups_directory_permalink', trailingslashit( bp_get_root_domain() . '/' . bp_get_groups_root_slug() ) );
+	}
+
+/**
+ * Output group type directory permalink.
+ *
+ * @since 2.7.0
+ *
+ * @param string $group_type Optional. Group type.
+ */
+function bp_group_type_directory_permalink( $group_type = '' ) {
+	echo esc_url( bp_get_group_type_directory_permalink( $group_type ) );
+}
+	/**
+	 * Return group type directory permalink.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $group_type Optional. Group type. Defaults to current group type.
+	 * @return string Group type directory URL on success, an empty string on failure.
+	 */
+	function bp_get_group_type_directory_permalink( $group_type = '' ) {
+
+		if ( $group_type ) {
+			$_group_type = $group_type;
+		} else {
+			// Fall back on the current group type.
+			$_group_type = bp_get_current_group_directory_type();
+		}
+
+		$type = bp_groups_get_group_type_object( $_group_type );
+
+		// Bail when member type is not found or has no directory.
+		if ( ! $type || ! $type->has_directory ) {
+			return '';
+		}
+
+		/**
+		 * Filters the group type directory permalink.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string $value       Group type directory permalink.
+		 * @param object $type        Group type object.
+		 * @param string $member_type Group type name, as passed to the function.
+		 */
+		return apply_filters( 'bp_get_group_type_directory_permalink', trailingslashit( bp_get_groups_directory_permalink() . bp_get_groups_group_type_base() . '/' . $type->directory_slug ), $type, $group_type );
+	}
+
+/**
+ * Output group type directory link.
+ *
+ * @since 2.7.0
+ *
+ * @param string $group_type Unique group type identifier as used in bp_groups_register_group_type().
+ */
+function bp_group_type_directory_link( $group_type = '' ) {
+	echo bp_get_group_type_directory_link( $group_type );
+}
+	/**
+	 * Return group type directory link.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $group_type Unique group type identifier as used in bp_groups_register_group_type().
+	 * @return string
+	 */
+	function bp_get_group_type_directory_link( $group_type = '' ) {
+		if ( empty( $group_type ) ) {
+			return '';
+		}
+
+		return sprintf( '<a href="%s">%s</a>', esc_url( bp_get_group_type_directory_permalink( $group_type ) ), bp_groups_get_group_type_object( $group_type )->labels['name'] );
+	}
+
+/**
+ * Output a comma-delimited list of group types.
+ *
+ * @since 2.7.0
+ * @see   bp_get_group_type_list() for parameter documentation.
+ */
+function bp_group_type_list( $group_id = 0, $r = array() ) {
+	echo bp_get_group_type_list( $group_id, $r );
+}
+	/**
+	 * Return a comma-delimited list of group types.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int $group_id Group ID. Defaults to current group ID if on a group page.
+	 * @param array|string $r {
+	 *     Array of parameters. All items are optional.
+	 *     @type string $parent_element Element to wrap around the list. Defaults to 'p'.
+	 *     @type array  $parent_attr    Element attributes for parent element. Defaults to
+	 *                                  array( 'class' => 'bp-group-type-list' ).
+	 *     @type string $label          Label to add before the list. Defaults to 'Group Types:'.
+	 *     @type string $label_element  Element to wrap around the label. Defaults to 'strong'.
+	 *     @type array  $label_attr     Element attributes for label element. Defaults to array().
+	 *     @type bool   $show_all       Whether to show all registered group types. Defaults to 'false'. If
+	 *                                 'false', only shows group types with the 'show_in_list' parameter set to
+	 *                                  true. See bp_groups_register_group_type() for more info.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_type_list( $group_id = 0, $r = array() ) {
+		if ( empty( $group_id ) ) {
+			$group_id = bp_get_current_group_id();
+		}
+
+		$r = bp_parse_args( $r, array(
+			'parent_element' => 'p',
+			'parent_attr'    => array(
+				 'class' => 'bp-group-type-list',
+			),
+			'label'          => __( 'Group Types:', 'buddypress' ),
+			'label_element'  => 'strong',
+			'label_attr'     => array(),
+			'show_all'       => false,
+		), 'group_type_list' );
+
+		$retval = '';
+
+		if ( $types = bp_groups_get_group_type( $group_id, false ) ) {
+			// Make sure we can show the type in the list.
+			if ( false === $r['show_all'] ) {
+				$types = array_intersect( bp_groups_get_group_types( array( 'show_in_list' => true ) ), $types );
+				if ( empty( $types ) ) {
+					return $retval;
+				}
+			}
+
+			$before = $after = $label = '';
+
+			// Render parent element.
+			if ( ! empty( $r['parent_element'] ) ) {
+				$parent_elem = new BP_Core_HTML_Element( array(
+					'element' => $r['parent_element'],
+					'attr'    => $r['parent_attr']
+				) );
+
+				// Set before and after.
+				$before = $parent_elem->get( 'open_tag' );
+				$after  = $parent_elem->get( 'close_tag' );
+			}
+
+			// Render label element.
+			if ( ! empty( $r['label_element'] ) ) {
+				$label = new BP_Core_HTML_Element( array(
+					'element'    => $r['label_element'],
+					'attr'       => $r['label_attr'],
+					'inner_html' => esc_html( $r['label'] )
+				) );
+				$label = $label->contents() . ' ';
+
+			// No element, just the label.
+			} else {
+				$label = esc_html( $r['label'] );
+			}
+
+			// Comma-delimit each type into the group type directory link.
+			$label .= implode( ', ', array_map( 'bp_get_group_type_directory_link', $types ) );
+
+			// Retval time!
+			$retval = $before . $label . $after;
+		}
+
+		return $retval;
+	}
+
+/**
+ * Start the Groups Template Loop.
+ *
+ * @since 1.0.0
+ * @since 2.6.0 Added `$group_type`, `$group_type__in`, and `$group_type__not_in` parameters.
+ * @since 2.7.0 Added `$update_admin_cache` parameter.
+ *
+ * @param array|string $args {
+ *     Array of parameters. All items are optional.
+ *     @type string       $type               Shorthand for certain orderby/order combinations. 'newest', 'active',
+ *                                            'popular', 'alphabetical', 'random'. When present, will override
+ *                                            orderby and order params. Default: null.
+ *     @type string       $order              Sort order. 'ASC' or 'DESC'. Default: 'DESC'.
+ *     @type string       $orderby            Property to sort by. 'date_created', 'last_activity',
+ *                                            'total_member_count', 'name', 'random'. Default: 'last_activity'.
+ *     @type int          $page               Page offset of results to return. Default: 1 (first page of results).
+ *     @type int          $per_page           Number of items to return per page of results. Default: 20.
+ *     @type int          $max                Does NOT affect query. May change the reported number of total groups
+ *                                            found, but not the actual number of found groups. Default: false.
+ *     @type bool         $show_hidden        Whether to include hidden groups in results. Default: false.
+ *     @type string       $page_arg           Query argument used for pagination. Default: 'grpage'.
+ *     @type int          $user_id            If provided, results will be limited to groups of which the specified
+ *                                            user is a member. Default: value of bp_displayed_user_id().
+ *     @type string       $slug               If provided, only the group with the matching slug will be returned.
+ *                                            Default: false.
+ *     @type string       $search_terms       If provided, only groups whose names or descriptions match the search
+ *                                            terms will be returned. Default: value of `$_REQUEST['groups_search']` or
+ *                                            `$_REQUEST['s']`, if present. Otherwise false.
+ *     @type array|string $group_type         Array or comma-separated list of group types to limit results to.
+ *     @type array|string $group_type__in     Array or comma-separated list of group types to limit results to.
+ *     @type array|string $group_type__not_in Array or comma-separated list of group types that will be
+ *                                            excluded from results.
+ *     @type array        $meta_query         An array of meta_query conditions.
+ *                                            See {@link WP_Meta_Query::queries} for description.
+ *     @type array|string $include            Array or comma-separated list of group IDs. Results will be limited
+ *                                            to groups within the list. Default: false.
+ *     @type array|string $exclude            Array or comma-separated list of group IDs. Results will exclude
+ *                                            the listed groups. Default: false.
+ *     @type array|string $parent_id          Array or comma-separated list of group IDs. Results will include only
+ *                                            child groups of the listed groups. Default: null.
+ *     @type bool         $update_meta_cache  Whether to fetch groupmeta for queried groups. Default: true.
+ *     @type bool         $update_admin_cache Whether to pre-fetch group admins for queried groups.
+ *                                            Defaults to true when on a group directory, where this
+ *                                            information is needed in the loop. Otherwise false.
+ * }
+ * @return bool True if there are groups to display that match the params
+ */
+function bp_has_groups( $args = '' ) {
+	global $groups_template;
+
+	/*
+	 * Defaults based on the current page & overridden by parsed $args
+	 */
+	$slug         = false;
+	$type         = '';
+	$search_terms = false;
+
+	// When looking your own groups, check for two action variables.
+	if ( bp_is_current_action( 'my-groups' ) ) {
+		if ( bp_is_action_variable( 'most-popular', 0 ) ) {
+			$type = 'popular';
+		} elseif ( bp_is_action_variable( 'alphabetically', 0 ) ) {
+			$type = 'alphabetical';
+		}
+
+	// When looking at invites, set type to invites.
+	} elseif ( bp_is_current_action( 'invites' ) ) {
+		$type = 'invites';
+
+	// When looking at a single group, set the type and slug.
+	} elseif ( bp_get_current_group_slug() ) {
+		$type = 'single-group';
+		$slug = bp_get_current_group_slug();
+	}
+
+	$group_type = bp_get_current_group_directory_type();
+	if ( ! $group_type && ! empty( $_GET['group_type'] ) ) {
+		if ( is_array( $_GET['group_type'] ) ) {
+			$group_type = $_GET['group_type'];
+		} else {
+			// Can be a comma-separated list.
+			$group_type = explode( ',', $_GET['group_type'] );
+		}
+	}
+
+	// Default search string (too soon to escape here).
+	$search_query_arg = bp_core_get_component_search_query_arg( 'groups' );
+	if ( ! empty( $_REQUEST[ $search_query_arg ] ) ) {
+		$search_terms = stripslashes( $_REQUEST[ $search_query_arg ] );
+	} elseif ( ! empty( $_REQUEST['group-filter-box'] ) ) {
+		$search_terms = $_REQUEST['group-filter-box'];
+	} elseif ( !empty( $_REQUEST['s'] ) ) {
+		$search_terms = $_REQUEST['s'];
+	}
+
+	// Parse defaults and requested arguments.
+	$r = bp_parse_args( $args, array(
+		'type'               => $type,
+		'order'              => 'DESC',
+		'orderby'            => 'last_activity',
+		'page'               => 1,
+		'per_page'           => 20,
+		'max'                => false,
+		'show_hidden'        => false,
+		'page_arg'           => 'grpage',
+		'user_id'            => bp_displayed_user_id(),
+		'slug'               => $slug,
+		'search_terms'       => $search_terms,
+		'group_type'         => $group_type,
+		'group_type__in'     => '',
+		'group_type__not_in' => '',
+		'meta_query'         => false,
+		'include'            => false,
+		'exclude'            => false,
+		'parent_id'          => null,
+		'update_meta_cache'  => true,
+		'update_admin_cache' => bp_is_groups_directory() || bp_is_user_groups(),
+	), 'has_groups' );
+
+	// Setup the Groups template global.
+	$groups_template = new BP_Groups_Template( array(
+		'type'               => $r['type'],
+		'order'              => $r['order'],
+		'orderby'            => $r['orderby'],
+		'page'               => (int) $r['page'],
+		'per_page'           => (int) $r['per_page'],
+		'max'                => (int) $r['max'],
+		'show_hidden'        => $r['show_hidden'],
+		'page_arg'           => $r['page_arg'],
+		'user_id'            => (int) $r['user_id'],
+		'slug'               => $r['slug'],
+		'search_terms'       => $r['search_terms'],
+		'group_type'         => $r['group_type'],
+		'group_type__in'     => $r['group_type__in'],
+		'group_type__not_in' => $r['group_type__not_in'],
+		'meta_query'         => $r['meta_query'],
+		'include'            => $r['include'],
+		'exclude'            => $r['exclude'],
+		'parent_id'          => $r['parent_id'],
+		'update_meta_cache'  => (bool) $r['update_meta_cache'],
+		'update_admin_cache' => (bool) $r['update_admin_cache'],
+	) );
+
+	/**
+	 * Filters whether or not there are groups to iterate over for the groups loop.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool               $value           Whether or not there are groups to iterate over.
+	 * @param BP_Groups_Template $groups_template BP_Groups_Template object based on parsed arguments.
+	 * @param array              $r               Array of parsed arguments for the query.
+	 */
+	return apply_filters( 'bp_has_groups', $groups_template->has_groups(), $groups_template, $r );
+}
+
+/**
+ * Check whether there are more groups to iterate over.
+ *
+ * @since 1.0.0
+ *
+ * @return bool
+ */
+function bp_groups() {
+	global $groups_template;
+	return $groups_template->groups();
+}
+
+/**
+ * Set up the current group inside the loop.
+ *
+ * @since 1.0.0
+ *
+ * @return object
+ */
+function bp_the_group() {
+	global $groups_template;
+	return $groups_template->the_group();
+}
+
+/**
+ * Is the group accessible to the currently logged-in user?
+ * Despite the name of the function, it has historically checked
+ * whether a user has access to a group.
+ * In BP 2.9, a property was added to the BP_Groups_Group class,
+ * `is_visible`, that describes whether a user can know the group exists.
+ * If you wish to check that property, use the check:
+ * bp_current_user_can( 'groups_see_group' ).
+ *
+ * @since 1.0.0
+ *
+ * @param BP_Groups_Group|null $group Optional. Group object. Default: current group in loop.
+ * @return bool
+ */
+function bp_group_is_visible( $group = null ) {
+	global $groups_template;
+
+	if ( bp_current_user_can( 'bp_moderate' ) ) {
+		return true;
+	}
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	return bp_current_user_can( 'groups_access_group', array( 'group_id' => $group->id ) );
+}
+
+/**
+ * Output the ID of the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object. Default: current group in loop.
+ */
+function bp_group_id( $group = false ) {
+	echo bp_get_group_id( $group );
+}
+	/**
+	 * Get the ID of the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return int
+	 */
+	function bp_get_group_id( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the ID of the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param int    $id    ID of the current group in the loop.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_id', $group->id, $group );
+	}
+
+/**
+ * Output the row class of the current group in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param array $classes Array of custom classes.
+ */
+function bp_group_class( $classes = array() ) {
+	echo bp_get_group_class( $classes );
+}
+	/**
+	 * Get the row class of the current group in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $classes Array of custom classes.
+	 * @return string Row class of the group.
+	 */
+	function bp_get_group_class( $classes = array() ) {
+		global $groups_template;
+
+		// Add even/odd classes, but only if there's more than 1 group.
+		if ( $groups_template->group_count > 1 ) {
+			$pos_in_loop = (int) $groups_template->current_group;
+			$classes[]   = ( $pos_in_loop % 2 ) ? 'even' : 'odd';
+
+		// If we've only one group in the loop, don't bother with odd and even.
+		} else {
+			$classes[] = 'bp-single-group';
+		}
+
+		// Group type - public, private, hidden.
+		$classes[] = sanitize_key( $groups_template->group->status );
+
+		// Add current group types.
+		if ( $group_types = bp_groups_get_group_type( bp_get_group_id(), false ) ) {
+			foreach ( $group_types as $group_type ) {
+				$classes[] = sprintf( 'group-type-%s', esc_attr( $group_type ) );
+			}
+		}
+
+		// User's group role.
+		if ( bp_is_user_active() ) {
+
+			// Admin.
+			if ( bp_group_is_admin() ) {
+				$classes[] = 'is-admin';
+			}
+
+			// Moderator.
+			if ( bp_group_is_mod() ) {
+				$classes[] = 'is-mod';
+			}
+
+			// Member.
+			if ( bp_group_is_member() ) {
+				$classes[] = 'is-member';
+			}
+		}
+
+		// Whether a group avatar will appear.
+		if ( bp_disable_group_avatar_uploads() || ! buddypress()->avatar->show_avatars ) {
+			$classes[] = 'group-no-avatar';
+		} else {
+			$classes[] = 'group-has-avatar';
+		}
+
+		/**
+		 * Filters classes that will be applied to row class of the current group in the loop.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array $classes Array of determined classes for the row.
+		 */
+		$classes = apply_filters( 'bp_get_group_class', $classes );
+		$classes = array_merge( $classes, array() );
+		$retval = 'class="' . join( ' ', $classes ) . '"';
+
+		return $retval;
+	}
+
+/**
+ * Output the name of the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_name( $group = false ) {
+	echo bp_get_group_name( $group );
+}
+	/**
+	 * Get the name of the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_name( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the name of the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $name  Name of the current group in the loop.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_name', $group->name, $group );
+	}
+
+/**
+ * Output the type of the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_type( $group = false ) {
+	echo bp_get_group_type( $group );
+}
+
+/**
+ * Get the type of the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ * @return string
+ */
+function bp_get_group_type( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( 'public' == $group->status ) {
+		$type = __( "Public Group", 'buddypress' );
+	} elseif ( 'hidden' == $group->status ) {
+		$type = __( "Hidden Group", 'buddypress' );
+	} elseif ( 'private' == $group->status ) {
+		$type = __( "Private Group", 'buddypress' );
+	} else {
+		$type = ucwords( $group->status ) . ' ' . __( 'Group', 'buddypress' );
+	}
+
+	/**
+	 * Filters the type for the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param string $type  Type for the current group in the loop.
+	 * @param object $group Group object.
+	 */
+	return apply_filters( 'bp_get_group_type', $type, $group );
+}
+/**
+ * Output the status of the current group in the loop.
+ *
+ * @since 1.1.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_status( $group = false ) {
+	echo bp_get_group_status( $group );
+}
+	/**
+	 * Get the status of the current group in the loop.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_status( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the status of the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $status Status of the current group in the loop.
+		 * @param object $group  Group object.
+		 */
+		return apply_filters( 'bp_get_group_status', $group->status, $group );
+	}
+
+/**
+ * Output the group avatar while in the groups loop.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {
+ *      See {@link bp_get_group_avatar()} for description of arguments.
+ * }
+ */
+function bp_group_avatar( $args = '' ) {
+	echo bp_get_group_avatar( $args );
+}
+	/**
+	 * Get a group's avatar.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and return values.
+	 *
+	 * @param array|string $args {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see {@link bp_core_fetch_avatar()}.
+	 *
+	 *     @type string   $alt     Default: 'Group logo of [group name]'.
+	 *     @type string   $class   Default: 'avatar'.
+	 *     @type string   $type    Default: 'full'.
+	 *     @type int|bool $width   Default: false.
+	 *     @type int|bool $height  Default: false.
+	 *     @type bool     $id      Passed to `$css_id` parameter.
+	 * }
+	 * @return string Group avatar string.
+	 */
+	function bp_get_group_avatar( $args = '' ) {
+		global $groups_template;
+
+		// Bail if avatars are turned off.
+		if ( bp_disable_group_avatar_uploads() || ! buddypress()->avatar->show_avatars ) {
+			return false;
+		}
+
+		// Parse the arguments.
+		$r = bp_parse_args( $args, array(
+			'type'   => 'full',
+			'width'  => false,
+			'height' => false,
+			'class'  => 'avatar',
+			'id'     => false,
+			'alt'    => sprintf( __( 'Group logo of %s', 'buddypress' ), $groups_template->group->name )
+		) );
+
+		// Fetch the avatar from the folder.
+		$avatar = bp_core_fetch_avatar( array(
+			'item_id'    => $groups_template->group->id,
+			'avatar_dir' => 'group-avatars',
+			'object'     => 'group',
+			'type'       => $r['type'],
+			'alt'        => $r['alt'],
+			'css_id'     => $r['id'],
+			'class'      => $r['class'],
+			'width'      => $r['width'],
+			'height'     => $r['height'],
+		) );
+
+		// If No avatar found, provide some backwards compatibility.
+		if ( empty( $avatar ) ) {
+			$avatar = '<img src="' . esc_url( $groups_template->group->avatar_thumb ) . '" class="avatar" alt="' . esc_attr( $groups_template->group->name ) . '" />';
+		}
+
+		/**
+		 * Filters the group avatar while in the groups loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $avatar HTML image element holding the group avatar.
+		 * @param array  $r      Array of parsed arguments for the group avatar.
+		 */
+		return apply_filters( 'bp_get_group_avatar', $avatar, $r );
+	}
+
+/**
+ * Output the group avatar thumbnail while in the groups loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_avatar_thumb( $group = false ) {
+	echo bp_get_group_avatar_thumb( $group );
+}
+	/**
+	 * Return the group avatar thumbnail while in the groups loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_avatar_thumb( $group = false ) {
+		return bp_get_group_avatar( array(
+			'type' => 'thumb',
+			'id'   => ! empty( $group->id ) ? $group->id : false
+		) );
+	}
+
+/**
+ * Output the miniature group avatar thumbnail while in the groups loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_avatar_mini( $group = false ) {
+	echo bp_get_group_avatar_mini( $group );
+}
+	/**
+	 * Return the miniature group avatar thumbnail while in the groups loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_avatar_mini( $group = false ) {
+		return bp_get_group_avatar( array(
+			'type'   => 'thumb',
+			'width'  => 30,
+			'height' => 30,
+			'id'     => ! empty( $group->id ) ? $group->id : false
+		) );
+	}
+
+/** Group cover image *********************************************************/
+
+/**
+ * Should we use the group's cover image header.
+ *
+ * @since 2.4.0
+ *
+ * @return bool True if the displayed user has a cover image,
+ *              False otherwise
+ */
+function bp_group_use_cover_image_header() {
+	return (bool) bp_is_active( 'groups', 'cover_image' ) && ! bp_disable_group_cover_image_uploads() && bp_attachments_is_wp_version_supported();
+}
+
+/**
+ * Output the 'last active' string for the current group in the loop.
+ *
+ * @since 1.0.0
+ * @since 2.7.0 Added $args as a parameter.
+ *
+ * @param object|bool  $group Optional. Group object. Default: current group in loop.
+ * @param array|string $args Optional. {@see bp_get_group_last_active()}.
+ */
+function bp_group_last_active( $group = false, $args = array() ) {
+	echo bp_get_group_last_active( $group, $args );
+}
+	/**
+	 * Return the 'last active' string for the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 * @since 2.7.0 Added $args as a parameter.
+	 *
+	 * @param object|bool  $group Optional. Group object. Default: current group in loop.
+	 * @param array|string $args {
+	 *     Array of optional parameters.
+	 *
+	 *     @type bool $relative Optional. If true, returns relative activity date. eg. active 5 months ago.
+	 *                          If false, returns active date value from database. Default: true.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_last_active( $group = false, $args = array() ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		$r = bp_parse_args( $args, array(
+			'relative' => true,
+		), 'group_last_active' );
+
+		$last_active = $group->last_activity;
+		if ( ! $last_active ) {
+			$last_active = groups_get_groupmeta( $group->id, 'last_activity' );
+		}
+
+		// We do not want relative time, so return now.
+		// @todo Should the 'bp_get_group_last_active' filter be applied here?
+		if ( ! $r['relative'] ) {
+			return esc_attr( $last_active );
+		}
+
+		if ( empty( $last_active ) ) {
+			return __( 'not yet active', 'buddypress' );
+		} else {
+
+			/**
+			 * Filters the 'last active' string for the current group in the loop.
+			 *
+			 * @since 1.0.0
+			 * @since 2.5.0 Added the `$group` parameter.
+			 *
+			 * @param string $value Determined last active value for the current group.
+			 * @param object $group Group object.
+			 */
+			return apply_filters( 'bp_get_group_last_active', bp_core_time_since( $last_active ), $group );
+		}
+	}
+
+/**
+ * Output the permalink for the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param BP_Groups_Group|null $group Optional. Group object. Default: current group in loop.
+ */
+function bp_group_permalink( $group = null ) {
+	echo bp_get_group_permalink( $group );
+}
+	/**
+	 * Return the permalink for the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param BP_Groups_Group|null $group Optional. Group object. Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_permalink( $group = null ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the permalink for the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Permalink for the current group in the loop.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_permalink', trailingslashit( bp_get_groups_directory_permalink() . bp_get_group_slug( $group ) . '/' ), $group );
+	}
+
+/**
+ * Output an HTML-formatted link for the current group in the loop.
+ *
+ * @since 2.9.0
+ *
+ * @param BP_Groups_Group|null $group Optional. Group object.
+ *                                    Default: current group in loop.
+ */
+function bp_group_link( $group = null ) {
+	echo bp_get_group_link( $group );
+}
+	/**
+	 * Return an HTML-formatted link for the current group in the loop.
+	 *
+	 * @since 2.9.0
+	 *
+	 * @param BP_Groups_Group|null $group Optional. Group object.
+	 *                                    Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_link( $group = null ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		$link = sprintf(
+			'<a href="%s" class="bp-group-home-link %s-home-link">%s</a>',
+			esc_url( bp_get_group_permalink( $group ) ),
+			esc_attr( bp_get_group_slug( $group ) ),
+			esc_html( bp_get_group_name( $group ) )
+		);
+
+		/**
+		 * Filters the HTML-formatted link for the current group in the loop.
+		 *
+		 * @since 2.9.0
+		 *
+		 * @param string          $value HTML-formatted link for the
+		 *                               current group in the loop.
+		 * @param BP_Groups_Group $group The current group object.
+		 */
+		return apply_filters( 'bp_get_group_link', $link, $group );
+	}
+
+/**
+ * Output the permalink for the admin section of the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_admin_permalink( $group = false ) {
+	echo bp_get_group_admin_permalink( $group );
+}
+	/**
+	 * Return the permalink for the admin section of the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_admin_permalink( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the permalink for the admin section of the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Permalink for the admin section of the current group in the loop.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_admin_permalink', trailingslashit( bp_get_group_permalink( $group ) . 'admin' ), $group );
+	}
+
+/**
+ * Return the slug for the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_slug( $group = false ) {
+	echo bp_get_group_slug( $group );
+}
+	/**
+	 * Return the slug for the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_slug( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the slug for the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $slug  Slug for the current group in the loop.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_slug', $group->slug, $group );
+	}
+
+/**
+ * Output the description for the current group in the loop.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_description( $group = false ) {
+	echo bp_get_group_description( $group );
+}
+	/**
+	 * Return the description for the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_description( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the description for the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Description for the current group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_description', stripslashes( $group->description ), $group );
+	}
+
+/**
+ * Output the description for the current group in the loop, for use in a textarea.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_description_editable( $group = false ) {
+	echo bp_get_group_description_editable( $group );
+}
+	/**
+	 * Return the permalink for the current group in the loop, for use in a textarea.
+	 *
+	 * 'bp_get_group_description_editable' does not have the formatting
+	 * filters that 'bp_get_group_description' has, which makes it
+	 * appropriate for "raw" editing.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_description_editable( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the permalink for the current group in the loop, for use in a textarea.
+		 *
+		 * 'bp_get_group_description_editable' does not have the formatting filters that
+		 * 'bp_get_group_description' has, which makes it appropriate for "raw" editing.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $description Description for the current group in the loop.
+		 * @param object $group       Group object.
+		 */
+		return apply_filters( 'bp_get_group_description_editable', $group->description, $group );
+	}
+
+/**
+ * Output an excerpt of the group description.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group  Optional. The group being referenced.
+ *                            Defaults to the group currently being
+ *                            iterated on in the groups loop.
+ * @param int         $length Optional. Length of returned string, including ellipsis.
+ *                            Default: 225.
+ */
+function bp_group_description_excerpt( $group = false, $length = 225 ) {
+	echo bp_get_group_description_excerpt( $group, $length );
+}
+	/**
+	 * Get an excerpt of a group description.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group  Optional. The group being referenced.
+	 *                            Defaults to the group currently being
+	 *                            iterated on in the groups loop.
+	 * @param int         $length Optional. Length of returned string, including ellipsis.
+	 *                            Default: 225.
+	 * @return string Excerpt.
+	 */
+	function bp_get_group_description_excerpt( $group = false, $length = 225 ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the excerpt of a group description.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Excerpt of a group description.
+		 * @param object $group Object for group whose description is made into an excerpt.
+		 */
+		return apply_filters( 'bp_get_group_description_excerpt', bp_create_excerpt( $group->description, $length ), $group );
+	}
+
+/**
+ * Output the status of the current group in the loop.
+ *
+ * Either 'Public' or 'Private'.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_public_status( $group = false ) {
+	echo bp_get_group_public_status( $group );
+}
+	/**
+	 * Return the status of the current group in the loop.
+	 *
+	 * Either 'Public' or 'Private'.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_public_status( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		if ( $group->is_public ) {
+			return __( 'Public', 'buddypress' );
+		} else {
+			return __( 'Private', 'buddypress' );
+		}
+	}
+
+/**
+ * Output whether the current group in the loop is public.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_is_public( $group = false ) {
+	echo bp_get_group_is_public( $group );
+}
+	/**
+	 * Return whether the current group in the loop is public.
+	 *
+	 * No longer used in BuddyPress.
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return mixed
+	 */
+	function bp_get_group_is_public( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters whether the current group in the loop is public.
+		 *
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param bool   $public True if the group is public.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_is_public', $group->is_public, $group );
+	}
+
+/**
+ * Output the created date of the current group in the loop.
+ *
+ * @since 1.0.0
+ * @since 2.7.0 Added $args as a parameter.
+ *
+ * @param object|bool  $group Optional. Group object. Default: current group in loop.
+ * @param array|string $args  {@see bp_get_group_date_created()}.
+ */
+function bp_group_date_created( $group = false, $args = array() ) {
+	echo bp_get_group_date_created( $group, $args );
+}
+	/**
+	 * Return the created date of the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 * @since 2.7.0 Added $args as a parameter.
+	 *
+	 * @param object|bool  $group Optional. Group object. Default: current group in loop.
+	 * @param array|string $args {
+	 *     Array of optional parameters.
+	 *
+	 *     @type bool $relative Optional. If true, returns relative created date. eg. active 5 months ago.
+	 *                          If false, returns created date value from database. Default: true.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_date_created( $group = false, $args = array() ) {
+		global $groups_template;
+
+		$r = bp_parse_args( $args, array(
+			'relative' => true,
+		), 'group_date_created' );
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		// We do not want relative time, so return now.
+		// @todo Should the 'bp_get_group_date_created' filter be applied here?
+		if ( ! $r['relative'] ) {
+			return esc_attr( $group->date_created );
+		}
+
+		/**
+		 * Filters the created date of the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Created date for the current group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_date_created', bp_core_time_since( $group->date_created ), $group );
+	}
+
+/**
+ * Output the username of the creator of the current group in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_creator_username( $group = false ) {
+	echo bp_get_group_creator_username( $group );
+}
+	/**
+	 * Return the username of the creator of the current group in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_creator_username( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the username of the creator of the current group in the loop.
+		 *
+		 * @since 1.7.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Username of the group creator.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_creator_username', bp_core_get_user_displayname( $group->creator_id ), $group );
+	}
+
+/**
+ * Output the user ID of the creator of the current group in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_creator_id( $group = false ) {
+	echo bp_get_group_creator_id( $group );
+}
+	/**
+	 * Return the user ID of the creator of the current group in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return int
+	 */
+	function bp_get_group_creator_id( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the user ID of the creator of the current group in the loop.
+		 *
+		 * @since 1.7.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param int $creator_id User ID of the group creator.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_creator_id', $group->creator_id, $group );
+	}
+
+/**
+ * Output the permalink of the creator of the current group in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_creator_permalink( $group = false ) {
+	echo bp_get_group_creator_permalink( $group );
+}
+	/**
+	 * Return the permalink of the creator of the current group in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_creator_permalink( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the permalink of the creator of the current group in the loop.
+		 *
+		 * @since 1.7.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Permalink of the group creator.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_creator_permalink', bp_core_get_user_domain( $group->creator_id ), $group );
+	}
+
+/**
+ * Determine whether a user is the creator of the current group in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param BP_Groups_Group|null $group   Optional. Group object. Default: current group in loop.
+ * @param int                  $user_id ID of the user.
+ * @return bool
+ */
+function bp_is_group_creator( $group = null, $user_id = 0 ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	return (bool) ( $group->creator_id == $user_id );
+}
+
+/**
+ * Output the avatar of the creator of the current group in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ * @param array       $args {
+ *     Array of optional arguments. See {@link bp_get_group_creator_avatar()}
+ *     for description.
+ * }
+ */
+function bp_group_creator_avatar( $group = false, $args = array() ) {
+	echo bp_get_group_creator_avatar( $group, $args );
+}
+	/**
+	 * Return the avatar of the creator of the current group in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @param array       $args {
+	 *     Array of optional arguments. See {@link bp_core_fetch_avatar()}
+	 *     for detailed description of arguments.
+	 *     @type string $type   Default: 'full'.
+	 *     @type int    $width  Default: false.
+	 *     @type int    $height Default: false.
+	 *     @type int    $class  Default: 'avatar'.
+	 *     @type string $id     Passed to 'css_id'. Default: false.
+	 *     @type string $alt    Alt text. Default: 'Group creator profile
+	 *                          photo of [user display name]'.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_creator_avatar( $group = false, $args = array() ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		$r = bp_parse_args( $args, array(
+			'type'   => 'full',
+			'width'  => false,
+			'height' => false,
+			'class'  => 'avatar',
+			'id'     => false,
+			'alt'    => sprintf( __( 'Group creator profile photo of %s', 'buddypress' ),  bp_core_get_user_displayname( $group->creator_id ) )
+		), 'group_creator_avatar' );
+		extract( $r, EXTR_SKIP );
+
+		$avatar = bp_core_fetch_avatar( array( 'item_id' => $group->creator_id, 'type' => $type, 'css_id' => $id, 'class' => $class, 'width' => $width, 'height' => $height, 'alt' => $alt ) );
+
+		/**
+		 * Filters the avatar of the creator of the current group in the loop.
+		 *
+		 * @since 1.7.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $avatar Avatar of the group creator.
+		 * @param object $group  Group object.
+		 */
+		return apply_filters( 'bp_get_group_creator_avatar', $avatar, $group );
+	}
+
+/**
+ * Determine whether the current user is the admin of the current group.
+ *
+ * Alias of {@link bp_is_item_admin()}.
+ *
+ * @since 1.1.0
+ *
+ * @return bool
+ */
+function bp_group_is_admin() {
+	return bp_is_item_admin();
+}
+
+/**
+ * Determine whether the current user is a mod of the current group.
+ *
+ * Alias of {@link bp_is_item_mod()}.
+ *
+ * @since 1.1.0
+ *
+ * @return bool
+ */
+function bp_group_is_mod() {
+	return bp_is_item_mod();
+}
+
+/**
+ * Output markup listing group admins.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_list_admins( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( ! empty( $group->admins ) ) { ?>
+		<ul id="group-admins">
+			<?php foreach( (array) $group->admins as $admin ) { ?>
+				<li>
+					<a href="<?php echo bp_core_get_user_domain( $admin->user_id, $admin->user_nicename, $admin->user_login ) ?>" class="bp-tooltip" data-bp-tooltip="<?php printf( ('%s'),  bp_core_get_user_displayname( $admin->user_id ) ); ?>"><?php echo bp_core_fetch_avatar( array( 'item_id' => $admin->user_id, 'email' => $admin->user_email, 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $admin->user_id ) ) ) ) ?></a>
+				</li>
+			<?php } ?>
+		</ul>
+	<?php } else { ?>
+		<span class="activity"><?php _e( 'No Admins', 'buddypress' ) ?></span>
+	<?php } ?>
+<?php
+}
+
+/**
+ * Output markup listing group mod.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in loop.
+ */
+function bp_group_list_mods( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( ! empty( $group->mods ) ) : ?>
+
+		<ul id="group-mods">
+
+			<?php foreach( (array) $group->mods as $mod ) { ?>
+
+				<li>
+					<a href="<?php echo bp_core_get_user_domain( $mod->user_id, $mod->user_nicename, $mod->user_login ) ?>" class="bp-tooltip" data-bp-tooltip="<?php printf( ('%s'),  bp_core_get_user_displayname( $mod->user_id ) ); ?>"><?php echo bp_core_fetch_avatar( array( 'item_id' => $mod->user_id, 'email' => $mod->user_email, 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $mod->user_id ) ) ) ) ?></a>
+				</li>
+
+			<?php } ?>
+
+		</ul>
+
+<?php else : ?>
+
+		<span class="activity"><?php _e( 'No Mods', 'buddypress' ) ?></span>
+
+<?php endif;
+
+}
+
+/**
+ * Return a list of user IDs for a group's admins.
+ *
+ * @since 1.5.0
+ *
+ * @param BP_Groups_Group|bool $group     Optional. The group being queried. Defaults
+ *                                        to the current group in the loop.
+ * @param string               $format    Optional. 'string' to get a comma-separated string,
+ *                                        'array' to get an array.
+ * @return mixed               $admin_ids A string or array of user IDs.
+ */
+function bp_group_admin_ids( $group = false, $format = 'string' ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	$admin_ids = array();
+
+	if ( $group->admins ) {
+		foreach( $group->admins as $admin ) {
+			$admin_ids[] = $admin->user_id;
+		}
+	}
+
+	if ( 'string' == $format ) {
+		$admin_ids = implode( ',', $admin_ids );
+	}
+
+	/**
+	 * Filters a list of user IDs for a group's admins.
+	 *
+	 * This filter may return either an array or a comma separated string.
+	 *
+	 * @since 1.5.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param array|string $admin_ids List of user IDs for a group's admins.
+	 * @param object       $group     Group object.
+	 */
+	return apply_filters( 'bp_group_admin_ids', $admin_ids, $group );
+}
+
+/**
+ * Return a list of user IDs for a group's moderators.
+ *
+ * @since 1.5.0
+ *
+ * @param BP_Groups_Group|bool $group   Optional. The group being queried.
+ *                                      Defaults to the current group in the loop.
+ * @param string               $format  Optional. 'string' to get a comma-separated string,
+ *                                      'array' to get an array.
+ * @return mixed               $mod_ids A string or array of user IDs.
+ */
+function bp_group_mod_ids( $group = false, $format = 'string' ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	$mod_ids = array();
+
+	if ( $group->mods ) {
+		foreach( $group->mods as $mod ) {
+			$mod_ids[] = $mod->user_id;
+		}
+	}
+
+	if ( 'string' == $format ) {
+		$mod_ids = implode( ',', $mod_ids );
+	}
+
+	/**
+	 * Filters a list of user IDs for a group's moderators.
+	 *
+	 * This filter may return either an array or a comma separated string.
+	 *
+	 * @since 1.5.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param array|string $admin_ids List of user IDs for a group's moderators.
+	 * @param object       $group     Group object.
+	 */
+	return apply_filters( 'bp_group_mod_ids', $mod_ids, $group );
+}
+
+/**
+ * Output the permalink of the current group's Members page.
+ *
+ * @since 1.0.0
+ */
+function bp_group_all_members_permalink() {
+	echo bp_get_group_all_members_permalink();
+}
+	/**
+	 * Return the permalink of the Members page of the current group in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_all_members_permalink( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the permalink of the Members page for the current group in the loop.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Permalink of the Members page for the current group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_all_members_permalink', trailingslashit( bp_get_group_permalink( $group ) . 'members' ), $group );
+	}
+
+/**
+ * Display a Groups search form.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ */
+function bp_group_search_form() {
+
+	$action = bp_displayed_user_domain() . bp_get_groups_slug() . '/my-groups/search/';
+	$label = __('Filter Groups', 'buddypress');
+	$name = 'group-filter-box';
+
+	$search_form_html = '<form action="' . $action . '" id="group-search-form" method="post">
+		<label for="'. $name .'" id="'. $name .'-label">'. $label .'</label>
+		<input type="search" name="'. $name . '" id="'. $name .'" value="'. $value .'"'.  $disabled .' />
+
+		'. wp_nonce_field( 'group-filter-box', '_wpnonce_group_filter', true, false ) .'
+		</form>';
+
+	echo apply_filters( 'bp_group_search_form', $search_form_html );
+}
+
+/**
+ * Determine whether the displayed user has no groups.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ *
+ * @return bool True if the displayed user has no groups, otherwise false.
+ */
+function bp_group_show_no_groups_message() {
+	if ( !groups_total_groups_for_user( bp_displayed_user_id() ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Determine whether the current page is a group activity permalink.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ *
+ * @return bool True if this is a group activity permalink, otherwise false.
+ */
+function bp_group_is_activity_permalink() {
+
+	if ( !bp_is_single_item() || !bp_is_groups_component() || !bp_is_current_action( bp_get_activity_slug() ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Output the pagination HTML for a group loop.
+ *
+ * @since 1.2.0
+ */
+function bp_groups_pagination_links() {
+	echo bp_get_groups_pagination_links();
+}
+	/**
+	 * Get the pagination HTML for a group loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_groups_pagination_links() {
+		global $groups_template;
+
+		/**
+		 * Filters the pagination HTML for a group loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $pag_links HTML markup for the pagination links.
+		 */
+		return apply_filters( 'bp_get_groups_pagination_links', $groups_template->pag_links );
+	}
+
+/**
+ * Output the "Viewing x-y of z groups" pagination message.
+ *
+ * @since 1.2.0
+ */
+function bp_groups_pagination_count() {
+	echo bp_get_groups_pagination_count();
+}
+	/**
+	 * Generate the "Viewing x-y of z groups" pagination message.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_groups_pagination_count() {
+		global $groups_template;
+
+		$start_num = intval( ( $groups_template->pag_page - 1 ) * $groups_template->pag_num ) + 1;
+		$from_num  = bp_core_number_format( $start_num );
+		$to_num    = bp_core_number_format( ( $start_num + ( $groups_template->pag_num - 1 ) > $groups_template->total_group_count ) ? $groups_template->total_group_count : $start_num + ( $groups_template->pag_num - 1 ) );
+		$total     = bp_core_number_format( $groups_template->total_group_count );
+
+		if ( 1 == $groups_template->total_group_count ) {
+			$message = __( 'Viewing 1 group', 'buddypress' );
+		} else {
+			$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s group', 'Viewing %1$s - %2$s of %3$s groups', $groups_template->total_group_count, 'buddypress' ), $from_num, $to_num, $total );
+		}
+
+		/**
+		 * Filters the "Viewing x-y of z groups" pagination message.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $message  "Viewing x-y of z groups" text.
+		 * @param string $from_num Total amount for the low value in the range.
+		 * @param string $to_num   Total amount for the high value in the range.
+		 * @param string $total    Total amount of groups found.
+		 */
+		return apply_filters( 'bp_get_groups_pagination_count', $message, $from_num, $to_num, $total );
+	}
+
+/**
+ * Determine whether groups auto-join is enabled.
+ *
+ * "Auto-join" is the toggle that determines whether users are joined to a
+ * public group automatically when creating content in that group.
+ *
+ * @since 1.2.6
+ *
+ * @return bool
+ */
+function bp_groups_auto_join() {
+
+	/**
+	 * Filters whether groups auto-join is enabled.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param bool $value Enabled status.
+	 */
+	return apply_filters( 'bp_groups_auto_join', (bool) buddypress()->groups->auto_join );
+}
+
+/**
+ * Output the total member count for a group.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object. Default: current group in loop.
+ */
+function bp_group_total_members( $group = false ) {
+	echo bp_get_group_total_members( $group );
+}
+	/**
+	 * Get the total member count for a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return int
+	 */
+	function bp_get_group_total_members( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the total member count for a group.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param int    $total_member_count Total member count for a group.
+		 * @param object $group              Group object.
+		 */
+		return apply_filters( 'bp_get_group_total_members', $group->total_member_count, $group );
+	}
+
+/**
+ * Output the "x members" count string for a group.
+ *
+ * @since 1.2.0
+ */
+function bp_group_member_count() {
+	echo bp_get_group_member_count();
+}
+	/**
+	 * Generate the "x members" count string for a group.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_member_count() {
+		global $groups_template;
+
+		if ( isset( $groups_template->group->total_member_count ) ) {
+			$count = (int) $groups_template->group->total_member_count;
+		} else {
+			$count = 0;
+		}
+
+		$count_string = sprintf( _n( '%s member', '%s members', $count, 'buddypress' ), bp_core_number_format( $count ) );
+
+		/**
+		 * Filters the "x members" count string for a group.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $count_string The "x members" count string for a group.
+		 */
+		return apply_filters( 'bp_get_group_member_count', $count_string );
+	}
+
+/**
+ * Output the URL of the Forum page of the current group in the loop.
+ *
+ * @since 1.0.0
+ */
+function bp_group_forum_permalink() {
+	echo bp_get_group_forum_permalink();
+}
+	/**
+	 * Generate the URL of the Forum page of a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in loop.
+	 * @return string
+	 */
+	function bp_get_group_forum_permalink( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the URL of the Forum page of a group.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL permalink for the Forum Page.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_forum_permalink', trailingslashit( bp_get_group_permalink( $group ) . 'forum' ), $group );
+	}
+
+/**
+ * Determine whether forums are enabled for a group.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object. Default: current group in loop.
+ * @return bool
+ */
+function bp_group_is_forum_enabled( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( ! empty( $group->enable_forum ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Output the 'checked' attribute for the group forums settings UI.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object. Default: current group in loop.
+ */
+function bp_group_show_forum_setting( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( $group->enable_forum ) {
+		echo ' checked="checked"';
+	}
+}
+
+/**
+ * Output the 'checked' attribute for a given status in the settings UI.
+ *
+ * @since 1.0.0
+ *
+ * @param string      $setting Group status. 'public', 'private', 'hidden'.
+ * @param object|bool $group   Optional. Group object. Default: current group in loop.
+ */
+function bp_group_show_status_setting( $setting, $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( $setting == $group->status ) {
+		echo ' checked="checked"';
+	}
+}
+
+/**
+ * Output the 'checked' value, if needed, for a given invite_status on the group create/admin screens
+ *
+ * @since 1.5.0
+ *
+ * @param string      $setting The setting you want to check against ('members',
+ *                             'mods', or 'admins').
+ * @param object|bool $group   Optional. Group object. Default: current group in loop.
+ */
+function bp_group_show_invite_status_setting( $setting, $group = false ) {
+	$group_id = isset( $group->id ) ? $group->id : false;
+
+	$invite_status = bp_group_get_invite_status( $group_id );
+
+	if ( $setting == $invite_status ) {
+		echo ' checked="checked"';
+	}
+}
+
+/**
+ * Get the invite status of a group.
+ *
+ * 'invite_status' became part of BuddyPress in BP 1.5. In order to provide
+ * backward compatibility with earlier installations, groups without a status
+ * set will default to 'members', ie all members in a group can send
+ * invitations. Filter 'bp_group_invite_status_fallback' to change this
+ * fallback behavior.
+ *
+ * This function can be used either in or out of the loop.
+ *
+ * @since 1.5.0
+ *
+ * @param int|bool $group_id Optional. The ID of the group whose status you want to
+ *                           check. Default: the displayed group, or the current group
+ *                           in the loop.
+ * @return bool|string Returns false when no group can be found. Otherwise
+ *                     returns the group invite status, from among 'members',
+ *                     'mods', and 'admins'.
+ */
+function bp_group_get_invite_status( $group_id = false ) {
+	global $groups_template;
+
+	if ( !$group_id ) {
+		$bp = buddypress();
+
+		if ( isset( $bp->groups->current_group->id ) ) {
+			// Default to the current group first.
+			$group_id = $bp->groups->current_group->id;
+		} elseif ( isset( $groups_template->group->id ) ) {
+			// Then see if we're in the loop.
+			$group_id = $groups_template->group->id;
+		} else {
+			return false;
+		}
+	}
+
+	$invite_status = groups_get_groupmeta( $group_id, 'invite_status' );
+
+	// Backward compatibility. When 'invite_status' is not set, fall back to a default value.
+	if ( !$invite_status ) {
+		$invite_status = apply_filters( 'bp_group_invite_status_fallback', 'members' );
+	}
+
+	/**
+	 * Filters the invite status of a group.
+	 *
+	 * Invite status in this case means who from the group can send invites.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $invite_status Membership level needed to send an invite.
+	 * @param int    $group_id      ID of the group whose status is being checked.
+	 */
+	return apply_filters( 'bp_group_get_invite_status', $invite_status, $group_id );
+}
+
+/**
+ * Can a user send invitations in the specified group?
+ *
+ * @since 1.5.0
+ * @since 2.2.0 Added the $user_id parameter.
+ *
+ * @param int $group_id The group ID to check.
+ * @param int $user_id  The user ID to check.
+ * @return bool
+ */
+function bp_groups_user_can_send_invites( $group_id = 0, $user_id = 0 ) {
+	$can_send_invites = false;
+	$invite_status    = false;
+
+	// If $user_id isn't specified, we check against the logged-in user.
+	if ( ! $user_id ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// If $group_id isn't specified, use existing one if available.
+	if ( ! $group_id ) {
+		$group_id = bp_get_current_group_id();
+	}
+
+	if ( $user_id ) {
+		$can_send_invites = bp_user_can( $user_id, 'groups_send_invitation', array( 'group_id' => $group_id ) );
+	}
+
+	/**
+	 * Filters whether a user can send invites in a group.
+	 *
+	 * @since 1.5.0
+	 * @since 2.2.0 Added the $user_id parameter.
+	 *
+	 * @param bool $can_send_invites Whether the user can send invites
+	 * @param int  $group_id         The group ID being checked
+	 * @param bool $invite_status    The group's current invite status
+	 * @param int  $user_id          The user ID being checked
+	 */
+	return apply_filters( 'bp_groups_user_can_send_invites', $can_send_invites, $group_id, $invite_status, $user_id );
+}
+
+/**
+ * Since BuddyPress 1.0, this generated the group settings admin/member screen.
+ * As of BuddyPress 1.5 (r4489), and because this function outputs HTML, it was moved into /bp-default/groups/single/admin.php.
+ *
+ * @deprecated 1.5
+ * @deprecated No longer used.
+ * @since 1.0.0
+ * @todo Remove in 1.4
+ *
+ * @param bool $admin_list
+ * @param bool $group
+ */
+function bp_group_admin_memberlist( $admin_list = false, $group = false ) {
+	global $groups_template;
+
+	_deprecated_function( __FUNCTION__, '1.5', 'No longer used. See /bp-default/groups/single/admin.php' );
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+
+	if ( $admins = groups_get_group_admins( $group->id ) ) : ?>
+
+		<ul id="admins-list" class="item-list<?php if ( !empty( $admin_list ) ) : ?> single-line<?php endif; ?>">
+
+		<?php foreach ( (array) $admins as $admin ) { ?>
+
+			<?php if ( !empty( $admin_list ) ) : ?>
+
+			<li>
+
+				<?php echo bp_core_fetch_avatar( array( 'item_id' => $admin->user_id, 'type' => 'thumb', 'width' => 30, 'height' => 30, 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $admin->user_id ) ) ) ) ?>
+
+				<h5>
+
+					<?php echo bp_core_get_userlink( $admin->user_id ); ?>
+
+					<span class="small">
+						<a class="button confirm admin-demote-to-member" href="<?php bp_group_member_demote_link($admin->user_id) ?>"><?php _e( 'Demote to Member', 'buddypress' ) ?></a>
+					</span>
+				</h5>
+			</li>
+
+			<?php else : ?>
+
+			<li>
+
+				<?php echo bp_core_fetch_avatar( array( 'item_id' => $admin->user_id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $admin->user_id ) ) ) ) ?>
+
+				<h5><?php echo bp_core_get_userlink( $admin->user_id ) ?></h5>
+				<span class="activity">
+					<?php echo bp_core_get_last_activity( strtotime( $admin->date_modified ), __( 'joined %s', 'buddypress') ); ?>
+				</span>
+
+				<?php if ( bp_is_active( 'friends' ) ) : ?>
+
+					<div class="action">
+
+						<?php bp_add_friend_button( $admin->user_id ); ?>
+
+					</div>
+
+				<?php endif; ?>
+
+			</li>
+
+			<?php endif;
+		} ?>
+
+		</ul>
+
+	<?php else : ?>
+
+		<div id="message" class="info">
+			<p><?php _e( 'This group has no administrators', 'buddypress' ); ?></p>
+		</div>
+
+	<?php endif;
+}
+
+/**
+ * Generate the HTML for a list of group moderators.
+ *
+ * No longer used.
+ *
+ * @todo Deprecate.
+ *
+ * @param bool $admin_list
+ * @param bool $group
+ */
+function bp_group_mod_memberlist( $admin_list = false, $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( $group_mods = groups_get_group_mods( $group->id ) ) { ?>
+
+		<ul id="mods-list" class="item-list<?php if ( $admin_list ) { ?> single-line<?php } ?>">
+
+		<?php foreach ( (array) $group_mods as $mod ) { ?>
+
+			<?php if ( !empty( $admin_list ) ) { ?>
+
+			<li>
+
+				<?php echo bp_core_fetch_avatar( array( 'item_id' => $mod->user_id, 'type' => 'thumb', 'width' => 30, 'height' => 30, 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $mod->user_id ) ) ) ) ?>
+
+				<h5>
+					<?php echo bp_core_get_userlink( $mod->user_id ); ?>
+
+					<span class="small">
+						<a href="<?php bp_group_member_promote_admin_link( array( 'user_id' => $mod->user_id ) ) ?>" class="button confirm mod-promote-to-admin"><?php _e( 'Promote to Admin', 'buddypress' ); ?></a>
+						<a class="button confirm mod-demote-to-member" href="<?php bp_group_member_demote_link($mod->user_id) ?>"><?php _e( 'Demote to Member', 'buddypress' ) ?></a>
+					</span>
+				</h5>
+			</li>
+
+			<?php } else { ?>
+
+			<li>
+
+				<?php echo bp_core_fetch_avatar( array( 'item_id' => $mod->user_id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $mod->user_id ) ) ) ) ?>
+
+				<h5><?php echo bp_core_get_userlink( $mod->user_id ) ?></h5>
+
+				<span class="activity"><?php echo bp_core_get_last_activity( strtotime( $mod->date_modified ), __( 'joined %s', 'buddypress') ); ?></span>
+
+				<?php if ( bp_is_active( 'friends' ) ) : ?>
+
+					<div class="action">
+						<?php bp_add_friend_button( $mod->user_id ) ?>
+					</div>
+
+				<?php endif; ?>
+
+			</li>
+
+			<?php } ?>
+		<?php } ?>
+
+		</ul>
+
+	<?php } else { ?>
+
+		<div id="message" class="info">
+			<p><?php _e( 'This group has no moderators', 'buddypress' ); ?></p>
+		</div>
+
+	<?php }
+}
+
+/**
+ * Determine whether a group has moderators.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object. Default: current group in loop.
+ * @return array Info about group admins (user_id + date_modified).
+ */
+function bp_group_has_moderators( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	/**
+	 * Filters whether a group has moderators.
+	 *
+	 * @since 1.0.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param array  $value Array of user IDs who are a moderator of the provided group.
+	 * @param object $group Group object.
+	 */
+	return apply_filters( 'bp_group_has_moderators', groups_get_group_mods( $group->id ), $group );
+}
+
+/**
+ * Output a URL for promoting a user to moderator.
+ *
+ * @since 1.1.0
+ *
+ * @param array|string $args See {@link bp_get_group_member_promote_mod_link()}.
+ */
+function bp_group_member_promote_mod_link( $args = '' ) {
+	echo bp_get_group_member_promote_mod_link( $args );
+}
+	/**
+	 * Generate a URL for promoting a user to moderator.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array|string $args {
+	 *     @type int    $user_id ID of the member to promote. Default:
+	 *                           current member in a group member loop.
+	 *     @type object $group   Group object. Default: current group.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_member_promote_mod_link( $args = '' ) {
+		global $members_template, $groups_template;
+
+		$r = bp_parse_args( $args, array(
+			'user_id' => $members_template->member->user_id,
+			'group'   => &$groups_template->group
+		), 'group_member_promote_mod_link' );
+		extract( $r, EXTR_SKIP );
+
+		/**
+		 * Filters a URL for promoting a user to moderator.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value URL to use for promoting a user to moderator.
+		 */
+		return apply_filters( 'bp_get_group_member_promote_mod_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'admin/manage-members/promote/mod/' . $user_id ), 'groups_promote_member' ) );
+	}
+
+/**
+ * Output a URL for promoting a user to admin.
+ *
+ * @since 1.1.0
+ *
+ * @param array|string $args See {@link bp_get_group_member_promote_admin_link()}.
+ */
+function bp_group_member_promote_admin_link( $args = '' ) {
+	echo bp_get_group_member_promote_admin_link( $args );
+}
+	/**
+	 * Generate a URL for promoting a user to admin.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array|string $args {
+	 *     @type int    $user_id ID of the member to promote. Default:
+	 *                           current member in a group member loop.
+	 *     @type object $group   Group object. Default: current group.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_member_promote_admin_link( $args = '' ) {
+		global $members_template, $groups_template;
+
+		$r = bp_parse_args( $args, array(
+			'user_id' => !empty( $members_template->member->user_id ) ? $members_template->member->user_id : false,
+			'group'   => &$groups_template->group
+		), 'group_member_promote_admin_link' );
+		extract( $r, EXTR_SKIP );
+
+		/**
+		 * Filters a URL for promoting a user to admin.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value URL to use for promoting a user to admin.
+		 */
+		return apply_filters( 'bp_get_group_member_promote_admin_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'admin/manage-members/promote/admin/' . $user_id ), 'groups_promote_member' ) );
+	}
+
+/**
+ * Output a URL for demoting a user to member.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the member to demote. Default: current member in
+ *                     a member loop.
+ */
+function bp_group_member_demote_link( $user_id = 0 ) {
+	global $members_template;
+
+	if ( !$user_id ) {
+		$user_id = $members_template->member->user_id;
+	}
+
+	echo bp_get_group_member_demote_link( $user_id );
+}
+	/**
+	 * Generate a URL for demoting a user to member.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int         $user_id ID of the member to demote. Default: current
+	 *                             member in a member loop.
+	 * @param object|bool $group   Optional. Group object. Default: current group.
+	 * @return string
+	 */
+	function bp_get_group_member_demote_link( $user_id = 0, $group = false ) {
+		global $members_template, $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		if ( !$user_id ) {
+			$user_id = $members_template->member->user_id;
+		}
+
+		/**
+		 * Filters a URL for demoting a user to member.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL to use for demoting a user to member.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_member_demote_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'admin/manage-members/demote/' . $user_id ), 'groups_demote_member' ), $group );
+	}
+
+/**
+ * Output a URL for banning a member from a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the member to ban.
+ *                     Default: current member in a member loop.
+ */
+function bp_group_member_ban_link( $user_id = 0 ) {
+	global $members_template;
+
+	if ( !$user_id ) {
+		$user_id = $members_template->member->user_id;
+	}
+
+	echo bp_get_group_member_ban_link( $user_id );
+}
+	/**
+	 * Generate a URL for banning a member from a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int         $user_id ID of the member to ban.
+	 *                             Default: current member in a member loop.
+	 * @param object|bool $group   Optional. Group object. Default: current group.
+	 * @return string
+	 */
+	function bp_get_group_member_ban_link( $user_id = 0, $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters a URL for banning a member from a group.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value URL to use for banning a member.
+		 */
+		return apply_filters( 'bp_get_group_member_ban_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'admin/manage-members/ban/' . $user_id ), 'groups_ban_member' ) );
+	}
+
+/**
+ * Output a URL for unbanning a member from a group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the member to unban.
+ *                     Default: current member in a member loop.
+ */
+function bp_group_member_unban_link( $user_id = 0 ) {
+	global $members_template;
+
+	if ( !$user_id ) {
+		$user_id = $members_template->member->user_id;
+	}
+
+	echo bp_get_group_member_unban_link( $user_id );
+}
+	/**
+	 * Generate a URL for unbanning a member from a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int         $user_id ID of the member to unban.
+	 *                             Default: current member in a member loop.
+	 * @param object|bool $group   Optional. Group object. Default: current group.
+	 * @return string
+	 */
+	function bp_get_group_member_unban_link( $user_id = 0, $group = false ) {
+		global $members_template, $groups_template;
+
+		if ( !$user_id ) {
+			$user_id = $members_template->member->user_id;
+		}
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters a URL for unbanning a member from a group.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value URL to use for unbanning a member.
+		 */
+		return apply_filters( 'bp_get_group_member_unban_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'admin/manage-members/unban/' . $user_id ), 'groups_unban_member' ) );
+	}
+
+/**
+ * Output a URL for removing a member from a group.
+ *
+ * @since 1.2.6
+ *
+ * @param int $user_id ID of the member to remove.
+ *                     Default: current member in a member loop.
+ */
+function bp_group_member_remove_link( $user_id = 0 ) {
+	global $members_template;
+
+	if ( !$user_id ) {
+		$user_id = $members_template->member->user_id;
+	}
+
+	echo bp_get_group_member_remove_link( $user_id );
+}
+	/**
+	 * Generate a URL for removing a member from a group.
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param int         $user_id ID of the member to remove.
+	 *                             Default: current member in a member loop.
+	 * @param object|bool $group   Optional. Group object. Default: current group.
+	 * @return string
+	 */
+	function bp_get_group_member_remove_link( $user_id = 0, $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters a URL for removing a member from a group.
+		 *
+		 * @since 1.2.6
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL to use for removing a member.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_member_remove_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'admin/manage-members/remove/' . $user_id ), 'groups_remove_member' ), $group );
+	}
+
+/**
+ * HTML admin subnav items for group pages.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in the loop.
+ */
+function bp_group_admin_tabs( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group = ( $groups_template->group ) ? $groups_template->group : groups_get_current_group();
+	}
+
+	$css_id = 'manage-members';
+
+	if ( 'private' == $group->status ) {
+		$css_id = 'membership-requests';
+	}
+
+	add_filter( "bp_get_options_nav_{$css_id}", 'bp_group_admin_tabs_backcompat', 10, 3 );
+
+	bp_get_options_nav( $group->slug . '_manage' );
+
+	remove_filter( "bp_get_options_nav_{$css_id}", 'bp_group_admin_tabs_backcompat', 10 );
+}
+
+/**
+ * BackCompat for plugins/themes directly hooking groups_admin_tabs
+ * without using the Groups Extension API.
+ *
+ * @since 2.2.0
+ *
+ * @param  string $subnav_output Subnav item output.
+ * @param  string $subnav_item   subnav item params.
+ * @param  string $selected_item Surrent selected tab.
+ * @return string HTML output
+ */
+function bp_group_admin_tabs_backcompat( $subnav_output = '', $subnav_item = '', $selected_item = '' ) {
+	if ( ! has_action( 'groups_admin_tabs' ) ) {
+		return $subnav_output;
+	}
+
+	$group = groups_get_current_group();
+
+	ob_start();
+
+	do_action( 'groups_admin_tabs', $selected_item, $group->slug );
+
+	$admin_tabs_backcompat = trim( ob_get_contents() );
+	ob_end_clean();
+
+	if ( ! empty( $admin_tabs_backcompat ) ) {
+		_doing_it_wrong( "do_action( 'groups_admin_tabs' )", __( 'This action should not be used directly. Please use the BuddyPress Group Extension API to generate Manage tabs.', 'buddypress' ), '2.2.0' );
+		$subnav_output .= $admin_tabs_backcompat;
+	}
+
+	return $subnav_output;
+}
+
+/**
+ * Output the group count for the displayed user.
+ *
+ * @since 1.1.0
+ */
+function bp_group_total_for_member() {
+	echo bp_get_group_total_for_member();
+}
+	/**
+	 * Get the group count for the displayed user.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_total_for_member() {
+
+		/**
+		 * FIlters the group count for a displayed user.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $value Total group count for a displayed user.
+		 */
+		return apply_filters( 'bp_get_group_total_for_member', BP_Groups_Member::total_group_count() );
+	}
+
+/**
+ * Output the 'action' attribute for a group form.
+ *
+ * @since 1.0.0
+ *
+ * @param string $page Page slug.
+ */
+function bp_group_form_action( $page ) {
+	echo bp_get_group_form_action( $page );
+}
+	/**
+	 * Generate the 'action' attribute for a group form.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string      $page  Page slug.
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_form_action( $page, $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the 'action' attribute for a group form.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Action attribute for a group form.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_group_form_action', trailingslashit( bp_get_group_permalink( $group ) . $page ), $group );
+	}
+
+/**
+ * Output the 'action' attribute for a group admin form.
+ *
+ * @since 1.0.0
+ *
+ * @param string|bool $page Optional. Page slug.
+ */
+function bp_group_admin_form_action( $page = false ) {
+	echo bp_get_group_admin_form_action( $page );
+}
+	/**
+	 * Generate the 'action' attribute for a group admin form.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string|bool $page  Optional. Page slug.
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_admin_form_action( $page = false, $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		if ( empty( $page ) ) {
+			$page = bp_action_variable( 0 );
+		}
+
+		/**
+		 * Filters the 'action' attribute for a group admin form.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Action attribute for a group admin form.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_group_admin_form_action', trailingslashit( bp_get_group_permalink( $group ) . 'admin/' . $page ), $group );
+	}
+
+/**
+ * Determine whether the logged-in user has requested membership to a group.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in the loop.
+ * @return bool
+ */
+function bp_group_has_requested_membership( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( groups_check_for_membership_request( bp_loggedin_user_id(), $group->id ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Check if current user is member of a group.
+ *
+ * @since 1.0.0
+ *
+ * @global object $groups_template
+ *
+ * @param object|bool $group Optional. Group to check is_member.
+ *                           Default: current group in the loop.
+ * @return bool If user is member of group or not.
+ */
+function bp_group_is_member( $group = false ) {
+	global $groups_template;
+
+	// Site admins always have access.
+	if ( bp_current_user_can( 'bp_moderate' ) ) {
+		return true;
+	}
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	/**
+	 * Filters whether current user is member of a group.
+	 *
+	 * @since 1.2.4
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param bool   $is_member If user is a member of group or not.
+	 * @param object $group     Group object.
+	 */
+	return apply_filters( 'bp_group_is_member', ! empty( $group->is_member ), $group );
+}
+
+/**
+ * Check whether the current user has an outstanding invite to the current group in the loop.
+ *
+ * @since 2.1.0
+ *
+ * @param object|bool $group Optional. Group data object.
+ *                           Default: the current group in the groups loop.
+ * @return bool True if the user has an outstanding invite, otherwise false.
+ */
+function bp_group_is_invited( $group = false ) {
+	global $groups_template;
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	/**
+	 * Filters whether current user has an outstanding invite to current group in loop.
+	 *
+	 * @since 2.1.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param bool   $is_invited If user has an outstanding group invite.
+	 * @param object $group      Group object.
+	 */
+	return apply_filters( 'bp_group_is_invited', ! empty( $group->is_invited ), $group );
+}
+
+/**
+ * Check if a user is banned from a group.
+ *
+ * If this function is invoked inside the groups template loop, then we check
+ * $groups_template->group->is_banned instead of using {@link groups_is_user_banned()}
+ * and making another SQL query.
+ *
+ * In BuddyPress 2.1, to standardize this function, we are defaulting the
+ * return value to a boolean.  In previous versions, using this function would
+ * return either a string of the integer (0 or 1) or null if a result couldn't
+ * be found from the database.  If the logged-in user had the 'bp_moderate'
+ * capability, the return value would be boolean false.
+ *
+ * @since 1.5.0
+ *
+ * @global BP_Groups_Template $groups_template Group template loop object.
+ *
+ * @param BP_Groups_Group|bool $group   Group to check if user is banned.
+ * @param int                  $user_id The user ID to check.
+ * @return bool True if user is banned.  False if user isn't banned.
+ */
+function bp_group_is_user_banned( $group = false, $user_id = 0 ) {
+	global $groups_template;
+
+	// Site admins always have access.
+	if ( bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	// Check groups loop first
+	// @see BP_Groups_Group::get_group_extras().
+	if ( ! empty( $groups_template->in_the_loop ) && isset( $groups_template->group->is_banned ) ) {
+		$retval = $groups_template->group->is_banned;
+
+	// Not in loop.
+	} else {
+		// Default to not banned.
+		$retval = false;
+
+		if ( empty( $group ) ) {
+			$group = $groups_template->group;
+		}
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		if ( ! empty( $user_id ) && ! empty( $group->id ) ) {
+			$retval = groups_is_user_banned( $user_id, $group->id );
+		}
+	}
+
+	/**
+	 * Filters whether current user has been banned from current group in loop.
+	 *
+	 * @since 1.5.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param bool   $is_invited If user has been from current group.
+	 * @param object $group      Group object.
+	 */
+	return (bool) apply_filters( 'bp_group_is_user_banned', $retval, $group );
+}
+
+/**
+ * Output the URL for accepting an invitation to the current group in the loop.
+ *
+ * @since 1.0.0
+ */
+function bp_group_accept_invite_link() {
+	echo bp_get_group_accept_invite_link();
+}
+	/**
+	 * Generate the URL for accepting an invitation to a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: Current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_accept_invite_link( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		$bp = buddypress();
+
+		/**
+		 * Filters the URL for accepting an invitation to a group.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL for accepting an invitation to a group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_accept_invite_link', wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() . '/invites/accept/' . $group->id ), 'groups_accept_invite' ), $group );
+	}
+
+/**
+ * Output the URL for accepting an invitation to the current group in the loop.
+ *
+ * @since 1.0.0
+ */
+function bp_group_reject_invite_link() {
+	echo bp_get_group_reject_invite_link();
+}
+	/**
+	 * Generate the URL for rejecting an invitation to a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: Current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_reject_invite_link( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		$bp = buddypress();
+
+		/**
+		 * Filters the URL for rejecting an invitation to a group.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL for rejecting an invitation to a group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_reject_invite_link', wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() . '/invites/reject/' . $group->id ), 'groups_reject_invite' ), $group );
+	}
+
+/**
+ * Output the URL for confirming a request to leave a group.
+ *
+ * @since 1.0.0
+ */
+function bp_group_leave_confirm_link() {
+	echo bp_get_group_leave_confirm_link();
+}
+	/**
+	 * Generate the URL for confirming a request to leave a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: Current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_leave_confirm_link( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the URL for confirming a request to leave a group.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL for confirming a request to leave a group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_group_leave_confirm_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'leave-group/yes' ), 'groups_leave_group' ), $group );
+	}
+
+/**
+ * Output the URL for rejecting a request to leave a group.
+ *
+ * @since 1.0.0
+ */
+function bp_group_leave_reject_link() {
+	echo bp_get_group_leave_reject_link();
+}
+	/**
+	 * Generate the URL for rejecting a request to leave a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: Current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_leave_reject_link( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the URL for rejecting a request to leave a group.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value URL for rejecting a request to leave a group.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_get_group_leave_reject_link', bp_get_group_permalink( $group ), $group );
+	}
+
+/**
+ * Output the 'action' attribute for a group send invite form.
+ *
+ * @since 1.0.0
+ */
+function bp_group_send_invite_form_action() {
+	echo bp_get_group_send_invite_form_action();
+}
+	/**
+	 * Output the 'action' attribute for a group send invite form.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Optional. Group object.
+	 *                           Default: current group in the loop.
+	 * @return string
+	 */
+	function bp_get_group_send_invite_form_action( $group = false ) {
+		global $groups_template;
+
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		/**
+		 * Filters the 'action' attribute for a group send invite form.
+		 *
+		 * @since 1.0.0
+		 * @since 2.5.0 Added the `$group` parameter.
+		 *
+		 * @param string $value Action attribute for a group send invite form.
+		 * @param object $group Group object.
+		 */
+		return apply_filters( 'bp_group_send_invite_form_action', trailingslashit( bp_get_group_permalink( $group ) . 'send-invites/send' ), $group );
+	}
+
+/**
+ * Determine whether the current user has friends to invite to a group.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Optional. Group object.
+ *                           Default: current group in the loop.
+ * @return bool
+ */
+function bp_has_friends_to_invite( $group = false ) {
+	global $groups_template;
+
+	if ( !bp_is_active( 'friends' ) ) {
+		return false;
+	}
+
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	if ( !friends_check_user_has_friends( bp_loggedin_user_id() ) || !friends_count_invitable_friends( bp_loggedin_user_id(), $group->id ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Output button to join a group.
+ *
+ * @since 1.0.0
+ *
+ * @param object|bool $group Single group object.
+ */
+function bp_group_join_button( $group = false ) {
+	echo bp_get_group_join_button( $group );
+}
+	/**
+	 * Return button to join a group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param object|bool $group Single group object.
+	 * @return false|string
+	 */
+	function bp_get_group_join_button( $group = false ) {
+		global $groups_template;
+
+		// Set group to current loop group if none passed.
+		if ( empty( $group ) ) {
+			$group =& $groups_template->group;
+		}
+
+		// Don't show button if not logged in or previously banned.
+		if ( ! is_user_logged_in() || bp_group_is_user_banned( $group ) ) {
+			return false;
+		}
+
+		// Group creation was not completed or status is unknown.
+		if ( empty( $group->status ) ) {
+			return false;
+		}
+
+		// Already a member.
+		if ( ! empty( $group->is_member ) ) {
+
+			// Stop sole admins from abandoning their group.
+			$group_admins = groups_get_group_admins( $group->id );
+			if ( ( 1 == count( $group_admins ) ) && ( bp_loggedin_user_id() === (int) $group_admins[0]->user_id ) ) {
+				return false;
+			}
+
+			// Setup button attributes.
+			$button = array(
+				'id'                => 'leave_group',
+				'component'         => 'groups',
+				'must_be_logged_in' => true,
+				'block_self'        => false,
+				'wrapper_class'     => 'group-button ' . $group->status,
+				'wrapper_id'        => 'groupbutton-' . $group->id,
+				'link_href'         => wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'leave-group' ), 'groups_leave_group' ),
+				'link_text'         => __( 'Leave Group', 'buddypress' ),
+				'link_class'        => 'group-button leave-group',
+			);
+
+		// Not a member.
+		} else {
+
+			// Show different buttons based on group status.
+			switch ( $group->status ) {
+				case 'hidden' :
+					return false;
+
+				case 'public':
+					$button = array(
+						'id'                => 'join_group',
+						'component'         => 'groups',
+						'must_be_logged_in' => true,
+						'block_self'        => false,
+						'wrapper_class'     => 'group-button ' . $group->status,
+						'wrapper_id'        => 'groupbutton-' . $group->id,
+						'link_href'         => wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'join' ), 'groups_join_group' ),
+						'link_text'         => __( 'Join Group', 'buddypress' ),
+						'link_class'        => 'group-button join-group',
+					);
+					break;
+
+				case 'private' :
+
+					// Member has outstanding invitation -
+					// show an "Accept Invitation" button.
+					if ( $group->is_invited ) {
+						$button = array(
+							'id'                => 'accept_invite',
+							'component'         => 'groups',
+							'must_be_logged_in' => true,
+							'block_self'        => false,
+							'wrapper_class'     => 'group-button ' . $group->status,
+							'wrapper_id'        => 'groupbutton-' . $group->id,
+							'link_href'         => add_query_arg( 'redirect_to', bp_get_group_permalink( $group ), bp_get_group_accept_invite_link( $group ) ),
+							'link_text'         => __( 'Accept Invitation', 'buddypress' ),
+							'link_class'        => 'group-button accept-invite',
+						);
+
+					// Member has requested membership but request is pending -
+					// show a "Request Sent" button.
+					} elseif ( $group->is_pending ) {
+						$button = array(
+							'id'                => 'membership_requested',
+							'component'         => 'groups',
+							'must_be_logged_in' => true,
+							'block_self'        => false,
+							'wrapper_class'     => 'group-button pending ' . $group->status,
+							'wrapper_id'        => 'groupbutton-' . $group->id,
+							'link_href'         => bp_get_group_permalink( $group ),
+							'link_text'         => __( 'Request Sent', 'buddypress' ),
+							'link_class'        => 'group-button pending membership-requested',
+						);
+
+					// Member has not requested membership yet -
+					// show a "Request Membership" button.
+					} else {
+						$button = array(
+							'id'                => 'request_membership',
+							'component'         => 'groups',
+							'must_be_logged_in' => true,
+							'block_self'        => false,
+							'wrapper_class'     => 'group-button ' . $group->status,
+							'wrapper_id'        => 'groupbutton-' . $group->id,
+							'link_href'         => wp_nonce_url( trailingslashit( bp_get_group_permalink( $group ) . 'request-membership' ), 'groups_request_membership' ),
+							'link_text'         => __( 'Request Membership', 'buddypress' ),
+							'link_class'        => 'group-button request-membership',
+						);
+					}
+
+					break;
+			}
+		}
+
+		/**
+		 * Filters the HTML button for joining a group.
+		 *
+		 * @since 1.2.6
+		 * @since 2.4.0 Added $group parameter to filter args.
+		 *
+		 * @param string $button HTML button for joining a group.
+		 * @param object $group BuddyPress group object
+		 */
+		return bp_get_button( apply_filters( 'bp_get_group_join_button', $button, $group ) );
+	}
+
+/**
+ * Output the Create a Group button.
+ *
+ * @since 2.0.0
+ */
+function bp_group_create_button() {
+	echo bp_get_group_create_button();
+}
+	/**
+	 * Get the Create a Group button.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return false|string
+	 */
+	function bp_get_group_create_button() {
+		if ( ! is_user_logged_in() ) {
+			return false;
+		}
+
+		if ( ! bp_user_can_create_groups() ) {
+			return false;
+		}
+
+		$button_args = array(
+			'id'         => 'create_group',
+			'component'  => 'groups',
+			'link_text'  => __( 'Create a Group', 'buddypress' ),
+			'link_class' => 'group-create no-ajax',
+			'link_href'  => trailingslashit( bp_get_groups_directory_permalink() . 'create' ),
+			'wrapper'    => false,
+			'block_self' => false,
+		);
+
+		/**
+		 * Filters the HTML button for creating a group.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $button HTML button for creating a group.
+		 */
+		return bp_get_button( apply_filters( 'bp_get_group_create_button', $button_args ) );
+	}
+
+/**
+ * Output the Create a Group nav item.
+ *
+ * @since 2.2.0
+ */
+function bp_group_create_nav_item() {
+	echo bp_get_group_create_nav_item();
+}
+
+	/**
+	 * Get the Create a Group nav item.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_create_nav_item() {
+		// Get the create a group button.
+		$create_group_button = bp_get_group_create_button();
+
+		// Make sure the button is available.
+		if ( empty( $create_group_button ) ) {
+			return;
+		}
+
+		$output = '<li id="group-create-nav">' . $create_group_button . '</li>';
+
+		/**
+		 * Filters the Create a Group nav item.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param string $output HTML output for nav item.
+		 */
+		return apply_filters( 'bp_get_group_create_nav_item', $output );
+	}
+
+/**
+ * Checks if a specific theme is still filtering the Groups directory title
+ * if so, transform the title button into a Groups directory nav item.
+ *
+ * @since 2.2.0
+ *
+ * @return string|null HTML Output
+ */
+function bp_group_backcompat_create_nav_item() {
+	// Bail if the Groups nav item is already used by bp-legacy.
+	if ( has_action( 'bp_groups_directory_group_filter', 'bp_legacy_theme_group_create_nav', 999 ) ) {
+		return;
+	}
+
+	// Bail if the theme is not filtering the Groups directory title.
+	if ( ! has_filter( 'bp_groups_directory_header' ) ) {
+		return;
+	}
+
+	bp_group_create_nav_item();
+}
+add_action( 'bp_groups_directory_group_filter', 'bp_group_backcompat_create_nav_item', 1000 );
+
+/**
+ * Prints a message if the group is not visible to the current user (it is a
+ * hidden or private group, and the user does not have access).
+ *
+ * @since 1.0.0
+ *
+ * @global BP_Groups_Template $groups_template Groups template object.
+ *
+ * @param object|null $group Group to get status message for. Optional; defaults to current group.
+ */
+function bp_group_status_message( $group = null ) {
+	global $groups_template;
+
+	// Group not passed so look for loop.
+	if ( empty( $group ) ) {
+		$group =& $groups_template->group;
+	}
+
+	// Group status is not set (maybe outside of group loop?).
+	if ( empty( $group->status ) ) {
+		$message = __( 'This group is not currently accessible.', 'buddypress' );
+
+	// Group has a status.
+	} else {
+		switch( $group->status ) {
+
+			// Private group.
+			case 'private' :
+				if ( ! bp_group_has_requested_membership( $group ) ) {
+					if ( is_user_logged_in() ) {
+						if ( bp_group_is_invited( $group ) ) {
+							$message = __( 'You must accept your pending invitation before you can access this private group.', 'buddypress' );
+						} else {
+							$message = __( 'This is a private group and you must request group membership in order to join.', 'buddypress' );
+						}
+					} else {
+						$message = __( 'This is a private group. To join you must be a registered site member and request group membership.', 'buddypress' );
+					}
+				} else {
+					$message = __( 'This is a private group. Your membership request is awaiting approval from the group administrator.', 'buddypress' );
+				}
+
+				break;
+
+			// Hidden group.
+			case 'hidden' :
+			default :
+				$message = __( 'This is a hidden group and only invited members can join.', 'buddypress' );
+				break;
+		}
+	}
+
+	/**
+	 * Filters a message if the group is not visible to the current user.
+	 *
+	 * This will be true if it is a hidden or private group, and the user does not have access.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $message Message to display to the current user.
+	 * @param object $group   Group to get status message for.
+	 */
+	echo apply_filters( 'bp_group_status_message', $message, $group );
+}
+
+/**
+ * Output hidden form fields for group.
+ *
+ * This function is no longer used, but may still be used by older themes.
+ *
+ * @since 1.0.0
+ */
+function bp_group_hidden_fields() {
+	$query_arg = bp_core_get_component_search_query_arg( 'groups' );
+
+	if ( isset( $_REQUEST[ $query_arg ] ) ) {
+		echo '<input type="hidden" id="search_terms" value="' . esc_attr( $_REQUEST[ $query_arg ] ) . '" name="search_terms" />';
+	}
+
+	if ( isset( $_REQUEST['letter'] ) ) {
+		echo '<input type="hidden" id="selected_letter" value="' . esc_attr( $_REQUEST['letter'] ) . '" name="selected_letter" />';
+	}
+
+	if ( isset( $_REQUEST['groups_search'] ) ) {
+		echo '<input type="hidden" id="search_terms" value="' . esc_attr( $_REQUEST['groups_search'] ) . '" name="search_terms" />';
+	}
+}
+
+/**
+ * Output the total number of groups.
+ *
+ * @since 1.0.0
+ */
+function bp_total_group_count() {
+	echo bp_get_total_group_count();
+}
+	/**
+	 * Return the total number of groups.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return int
+	 */
+	function bp_get_total_group_count() {
+
+		/**
+		 * Filters the total number of groups.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $value Total number of groups found.
+		 */
+		return apply_filters( 'bp_get_total_group_count', groups_get_total_group_count() );
+	}
+
+/**
+ * Output the total number of groups a user belongs to.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id User ID to get group membership count.
+ */
+function bp_total_group_count_for_user( $user_id = 0 ) {
+	echo bp_get_total_group_count_for_user( $user_id );
+}
+	/**
+	 * Return the total number of groups a user belongs to.
+	 *
+	 * Filtered by `bp_core_number_format()` by default
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id User ID to get group membership count.
+	 * @return string
+	 */
+	function bp_get_total_group_count_for_user( $user_id = 0 ) {
+		$count = groups_total_groups_for_user( $user_id );
+
+		/**
+		 * Filters the total number of groups a user belongs to.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $count   Total number of groups for the user.
+		 * @param int $user_id ID of the user being checked.
+		 */
+		return apply_filters( 'bp_get_total_group_count_for_user', $count, $user_id );
+	}
+
+/* Group Members *************************************************************/
+
+/**
+ * Initialize a group member query loop.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {
+ *     An array of optional arguments.
+ *     @type int      $group_id           ID of the group whose members are being queried.
+ *                                        Default: current group ID.
+ *     @type int      $page               Page of results to be queried. Default: 1.
+ *     @type int      $per_page           Number of items to return per page of results.
+ *                                        Default: 20.
+ *     @type int      $max                Optional. Max number of items to return.
+ *     @type array    $exclude            Optional. Array of user IDs to exclude.
+ *     @type bool|int $exclude_admin_mods True (or 1) to exclude admins and mods from results.
+ *                                        Default: 1.
+ *     @type bool|int $exclude_banned     True (or 1) to exclude banned users from results.
+ *                                        Default: 1.
+ *     @type array    $group_role         Optional. Array of group roles to include.
+ *     @type string   $type               Optional. Sort order of results. 'last_joined',
+ *                                        'first_joined', or any of the $type params available in
+ *                                        {@link BP_User_Query}. Default: 'last_joined'.
+ *     @type string   $search_terms       Optional. Search terms to match. Pass an
+ *                                        empty string to force-disable search, even in
+ *                                        the presence of $_REQUEST['s']. Default: false.
+ * }
+ *
+ * @return bool
+ */
+function bp_group_has_members( $args = '' ) {
+	global $members_template;
+
+	$exclude_admins_mods = 1;
+
+	if ( bp_is_group_members() ) {
+		$exclude_admins_mods = 0;
+	}
+
+	/*
+	 * Use false as the search_terms default so that BP_User_Query
+	 * doesn't add a search clause.
+	 */
+	$search_terms_default = false;
+	$search_query_arg = bp_core_get_component_search_query_arg( 'members' );
+	if ( ! empty( $_REQUEST[ $search_query_arg ] ) ) {
+		$search_terms_default = stripslashes( $_REQUEST[ $search_query_arg ] );
+	}
+
+	$r = bp_parse_args( $args, array(
+		'group_id'            => bp_get_current_group_id(),
+		'page'                => 1,
+		'per_page'            => 20,
+		'max'                 => false,
+		'exclude'             => false,
+		'exclude_admins_mods' => $exclude_admins_mods,
+		'exclude_banned'      => 1,
+		'group_role'          => false,
+		'search_terms'        => $search_terms_default,
+		'type'                => 'last_joined',
+	), 'group_has_members' );
+
+	/*
+	 * If an empty search_terms string has been passed,
+	 * the developer is force-disabling search.
+	 */
+	if ( '' === $r['search_terms'] ) {
+		// Set the search_terms to false for BP_User_Query efficiency.
+		$r['search_terms'] = false;
+	} elseif ( ! empty( $_REQUEST['s'] ) ) {
+		$r['search_terms'] = $_REQUEST['s'];
+	}
+
+	$members_template = new BP_Groups_Group_Members_Template( $r );
+
+	/**
+	 * Filters whether or not a group member query has members to display.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool                             $value            Whether there are members to display.
+	 * @param BP_Groups_Group_Members_Template $members_template Object holding the member query results.
+	 */
+	return apply_filters( 'bp_group_has_members', $members_template->has_members(), $members_template );
+}
+
+/**
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_group_members() {
+	global $members_template;
+
+	return $members_template->members();
+}
+
+/**
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_group_the_member() {
+	global $members_template;
+
+	return $members_template->the_member();
+}
+
+/**
+ * Output the group member avatar while in the groups members loop.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {@see bp_core_fetch_avatar()}.
+ */
+function bp_group_member_avatar( $args = '' ) {
+	echo bp_get_group_member_avatar( $args );
+}
+	/**
+	 * Return the group member avatar while in the groups members loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array|string $args {@see bp_core_fetch_avatar()}.
+	 * @return string
+	 */
+	function bp_get_group_member_avatar( $args = '' ) {
+		global $members_template;
+
+		$r = bp_parse_args( $args, array(
+			'item_id' => $members_template->member->user_id,
+			'type'    => 'full',
+			'email'   => $members_template->member->user_email,
+			'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), $members_template->member->display_name )
+		) );
+
+		/**
+		 * Filters the group member avatar while in the groups members loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value HTML markup for group member avatar.
+		 * @param array  $r     Parsed args used for the avatar query.
+		 */
+		return apply_filters( 'bp_get_group_member_avatar', bp_core_fetch_avatar( $r ), $r );
+	}
+
+/**
+ * Output the group member avatar while in the groups members loop.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {@see bp_core_fetch_avatar()}.
+ */
+function bp_group_member_avatar_thumb( $args = '' ) {
+	echo bp_get_group_member_avatar_thumb( $args );
+}
+	/**
+	 * Return the group member avatar while in the groups members loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array|string $args {@see bp_core_fetch_avatar()}.
+	 * @return string
+	 */
+	function bp_get_group_member_avatar_thumb( $args = '' ) {
+		global $members_template;
+
+		$r = bp_parse_args( $args, array(
+			'item_id' => $members_template->member->user_id,
+			'type'    => 'thumb',
+			'email'   => $members_template->member->user_email,
+			'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), $members_template->member->display_name )
+		) );
+
+		/**
+		 * Filters the group member avatar thumb while in the groups members loop.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value HTML markup for group member avatar thumb.
+		 * @param array  $r     Parsed args used for the avatar query.
+		 */
+		return apply_filters( 'bp_get_group_member_avatar_thumb', bp_core_fetch_avatar( $r ), $r );
+	}
+
+/**
+ * Output the group member avatar while in the groups members loop.
+ *
+ * @since 1.0.0
+ *
+ * @param int $width  Width of avatar to fetch.
+ * @param int $height Height of avatar to fetch.
+ */
+function bp_group_member_avatar_mini( $width = 30, $height = 30 ) {
+	echo bp_get_group_member_avatar_mini( $width, $height );
+}
+	/**
+	 * Output the group member avatar while in the groups members loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $width  Width of avatar to fetch.
+	 * @param int $height Height of avatar to fetch.
+	 * @return string
+	 */
+	function bp_get_group_member_avatar_mini( $width = 30, $height = 30 ) {
+		global $members_template;
+
+		$r = bp_parse_args( array(), array(
+			'item_id' => $members_template->member->user_id,
+			'type'    => 'thumb',
+			'email'   => $members_template->member->user_email,
+			'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), $members_template->member->display_name ),
+			'width'   => absint( $width ),
+			'height'  => absint( $height )
+		) );
+
+		/**
+		 * Filters the group member avatar mini while in the groups members loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value HTML markup for group member avatar mini.
+		 * @param array  $r     Parsed args used for the avatar query.
+		 */
+		return apply_filters( 'bp_get_group_member_avatar_mini', bp_core_fetch_avatar( $r ), $r );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_name() {
+	echo bp_get_group_member_name();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_name() {
+		global $members_template;
+
+		/**
+		 * Filters the group member display name of the current user in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $display_name Display name of the current user.
+		 */
+		return apply_filters( 'bp_get_group_member_name', $members_template->member->display_name );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_url() {
+	echo bp_get_group_member_url();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_url() {
+		global $members_template;
+
+		/**
+		 * Filters the group member url for the current user in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value URL for the current user.
+		 */
+		return apply_filters( 'bp_get_group_member_url', bp_core_get_user_domain( $members_template->member->user_id, $members_template->member->user_nicename, $members_template->member->user_login ) );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_link() {
+	echo bp_get_group_member_link();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_link() {
+		global $members_template;
+
+		/**
+		 * Filters the group member HTML link for the current user in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value HTML link for the current user.
+		 */
+		return apply_filters( 'bp_get_group_member_link', '<a href="' . bp_core_get_user_domain( $members_template->member->user_id, $members_template->member->user_nicename, $members_template->member->user_login ) . '">' . $members_template->member->display_name . '</a>' );
+	}
+
+/**
+ * @since 1.2.0
+ */
+function bp_group_member_domain() {
+	echo bp_get_group_member_domain();
+}
+
+	/**
+	 * @since 1.2.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_domain() {
+		global $members_template;
+
+		/**
+		 * Filters the group member domain for the current user in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Domain for the current user.
+		 */
+		return apply_filters( 'bp_get_group_member_domain', bp_core_get_user_domain( $members_template->member->user_id, $members_template->member->user_nicename, $members_template->member->user_login ) );
+	}
+
+/**
+ * @since 1.2.0
+ */
+function bp_group_member_is_friend() {
+	echo bp_get_group_member_is_friend();
+}
+
+	/**
+	 * @since 1.2.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_is_friend() {
+		global $members_template;
+
+		if ( !isset( $members_template->member->is_friend ) ) {
+			$friend_status = 'not_friends';
+		} else {
+			$friend_status = ( 0 == $members_template->member->is_friend )
+				? 'pending'
+				: 'is_friend';
+		}
+
+		/**
+		 * Filters the friendship status between current user and displayed user in group member loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $friend_status Current status of the friendship.
+		 */
+		return apply_filters( 'bp_get_group_member_is_friend', $friend_status );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_is_banned() {
+	echo bp_get_group_member_is_banned();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_is_banned() {
+		global $members_template;
+
+		/**
+		 * Filters whether the member is banned from the current group.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param bool $is_banned Whether or not the member is banned.
+		 */
+		return apply_filters( 'bp_get_group_member_is_banned', $members_template->member->is_banned );
+	}
+
+/**
+ * @since 1.2.6
+ */
+function bp_group_member_css_class() {
+	global $members_template;
+
+	if ( $members_template->member->is_banned ) {
+
+		/**
+		 * Filters the class to add to the HTML if member is banned.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string $value HTML class to add.
+		 */
+		echo apply_filters( 'bp_group_member_css_class', 'banned-user' );
+	}
+}
+
+/**
+ * Output the joined date for the current member in the group member loop.
+ *
+ * @since 1.0.0
+ * @since 2.7.0 Added $args as a parameter.
+ *
+ * @param array|string $args {@see bp_get_group_member_joined_since()}
+ * @return string|null
+ */
+function bp_group_member_joined_since( $args = array() ) {
+	echo bp_get_group_member_joined_since( $args );
+}
+	/**
+	 * Return the joined date for the current member in the group member loop.
+	 *
+	 * @since 1.0.0
+	 * @since 2.7.0 Added $args as a parameter.
+	 *
+	 * @param array|string $args {
+	 *     Array of optional parameters.
+	 *
+	 *     @type bool $relative Optional. If true, returns relative joined date. eg. joined 5 months ago.
+	 *                          If false, returns joined date value from database. Default: true.
+	 * }
+	 * @return string
+	 */
+	function bp_get_group_member_joined_since( $args = array() ) {
+		global $members_template;
+
+		$r = bp_parse_args( $args, array(
+			'relative' => true,
+		), 'group_member_joined_since' );
+
+		// We do not want relative time, so return now.
+		// @todo Should the 'bp_get_group_member_joined_since' filter be applied here?
+		if ( ! $r['relative'] ) {
+			return esc_attr( $members_template->member->date_modified );
+		}
+
+		/**
+		 * Filters the joined since time for the current member in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Joined since time.
+		 */
+		return apply_filters( 'bp_get_group_member_joined_since', bp_core_get_last_activity( $members_template->member->date_modified, __( 'joined %s', 'buddypress') ) );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_id() {
+	echo bp_get_group_member_id();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_id() {
+		global $members_template;
+
+		/**
+		 * Filters the member's user ID for group members loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $user_id User ID of the member.
+		 */
+		return apply_filters( 'bp_get_group_member_id', $members_template->member->user_id );
+	}
+
+/**
+ * @since 1.0.0
+ *
+ * @return bool
+ */
+function bp_group_member_needs_pagination() {
+	global $members_template;
+
+	if ( $members_template->total_member_count > $members_template->pag_num ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_pag_id() {
+	echo bp_get_group_pag_id();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_pag_id() {
+
+		/**
+		 * Filters the string to be used as the group pag id.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Value to use for the pag id.
+		 */
+		return apply_filters( 'bp_get_group_pag_id', 'pag' );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_pagination() {
+	echo bp_get_group_member_pagination();
+	wp_nonce_field( 'bp_groups_member_list', '_member_pag_nonce' );
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_pagination() {
+		global $members_template;
+
+		/**
+		 * Filters the HTML markup to be used for group member listing pagination.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $pag_links HTML markup for the pagination.
+		 */
+		return apply_filters( 'bp_get_group_member_pagination', $members_template->pag_links );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_pagination_count() {
+	echo bp_get_group_member_pagination_count();
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_member_pagination_count() {
+		global $members_template;
+
+		$start_num = intval( ( $members_template->pag_page - 1 ) * $members_template->pag_num ) + 1;
+		$from_num  = bp_core_number_format( $start_num );
+		$to_num    = bp_core_number_format( ( $start_num + ( $members_template->pag_num - 1 ) > $members_template->total_member_count ) ? $members_template->total_member_count : $start_num + ( $members_template->pag_num - 1 ) );
+		$total     = bp_core_number_format( $members_template->total_member_count );
+
+		if ( 1 == $members_template->total_member_count ) {
+			$message = __( 'Viewing 1 member', 'buddypress' );
+		} else {
+			$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s member', 'Viewing %1$s - %2$s of %3$s members', $members_template->total_member_count, 'buddypress' ), $from_num, $to_num, $total );
+		}
+
+		/**
+		 * Filters the "Viewing x-y of z members" pagination message.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value    "Viewing x-y of z members" text.
+		 * @param string $from_num Total amount for the low value in the range.
+		 * @param string $to_num   Total amount for the high value in the range.
+		 * @param string $total    Total amount of members found.
+		 */
+		return apply_filters( 'bp_get_group_member_pagination_count', $message, $from_num, $to_num, $total );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_member_admin_pagination() {
+	echo bp_get_group_member_admin_pagination();
+	wp_nonce_field( 'bp_groups_member_admin_list', '_member_admin_pag_nonce' );
+}
+
+	/**
+	 * @since 1.0.0
+	 *
+	 * @return mixed
+	 */
+	function bp_get_group_member_admin_pagination() {
+		global $members_template;
+
+		return $members_template->pag_links;
+	}
+
+/**
+ * Output the contents of the current group's home page.
+ *
+ * You should only use this when on a single group page.
+ *
+ * @since 2.4.0
+ */
+function bp_groups_front_template_part() {
+	$located = bp_groups_get_front_template();
+
+	if ( false !== $located ) {
+		$slug = str_replace( '.php', '', $located );
+
+		/**
+		 * Let plugins adding an action to bp_get_template_part get it from here
+		 *
+		 * @param string $slug Template part slug requested.
+		 * @param string $name Template part name requested.
+		 */
+		do_action( 'get_template_part_' . $slug, $slug, false );
+
+		load_template( $located, true );
+
+	} else if ( bp_is_active( 'activity' ) ) {
+		bp_get_template_part( 'groups/single/activity' );
+
+	} else if ( bp_is_active( 'members'  ) ) {
+		bp_groups_members_template_part();
+	}
+
+	return $located;
+}
+
+/**
+ * Locate a custom group front template if it exists.
+ *
+ * @since 2.4.0
+ * @since 2.6.0 Adds the Group Type to the front template hierarchy.
+ *
+ * @param  BP_Groups_Group|null $group Optional. Falls back to current group if not passed.
+ * @return string|bool                 Path to front template on success; boolean false on failure.
+ */
+function bp_groups_get_front_template( $group = null ) {
+	if ( ! is_a( $group, 'BP_Groups_Group' ) ) {
+		$group = groups_get_current_group();
+	}
+
+	if ( ! isset( $group->id ) ) {
+		return false;
+	}
+
+	if ( isset( $group->front_template ) ) {
+		return $group->front_template;
+	}
+
+	$template_names = array(
+		'groups/single/front-id-'     . sanitize_file_name( $group->id )     . '.php',
+		'groups/single/front-slug-'   . sanitize_file_name( $group->slug )   . '.php',
+	);
+
+	if ( bp_groups_get_group_types() ) {
+		$group_type = bp_groups_get_group_type( $group->id );
+		if ( ! $group_type ) {
+			$group_type = 'none';
+		}
+
+		$template_names[] = 'groups/single/front-group-type-' . sanitize_file_name( $group_type )   . '.php';
+	}
+
+	$template_names = array_merge( $template_names, array(
+		'groups/single/front-status-' . sanitize_file_name( $group->status ) . '.php',
+		'groups/single/front.php'
+	) );
+
+	/**
+	 * Filters the hierarchy of group front templates corresponding to a specific group.
+	 *
+	 * @since 2.4.0
+	 * @since 2.5.0 Added the `$group` parameter.
+	 *
+	 * @param array  $template_names Array of template paths.
+	 * @param object $group          Group object.
+	 */
+	return bp_locate_template( apply_filters( 'bp_groups_get_front_template', $template_names, $group ), false, true );
+}
+
+/**
+ * Output the Group members template
+ *
+ * @since 2.0.0
+ */
+function bp_groups_members_template_part() {
+	?>
+	<div class="item-list-tabs" id="subnav" aria-label="<?php esc_attr_e( 'Group secondary navigation', 'buddypress' ); ?>" role="navigation">
+		<ul>
+			<li class="groups-members-search" role="search">
+				<?php bp_directory_members_search_form(); ?>
+			</li>
+
+			<?php bp_groups_members_filter(); ?>
+			<?php
+
+			/**
+			 * Fires at the end of the group members search unordered list.
+			 *
+			 * Part of bp_groups_members_template_part().
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_members_directory_member_sub_types' ); ?>
+
+		</ul>
+	</div>
+
+	<h2 class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Members', 'buddypress' );
+	?></h2>
+
+	<div id="members-group-list" class="group_members dir-list">
+
+		<?php bp_get_template_part( 'groups/single/members' ); ?>
+
+	</div>
+	<?php
+}
+
+/**
+ * Output the Group members filters
+ *
+ * @since 2.0.0
+ */
+function bp_groups_members_filter() {
+	?>
+	<li id="group_members-order-select" class="last filter">
+		<label for="group_members-order-by"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+		<select id="group_members-order-by">
+			<option value="last_joined"><?php _e( 'Newest', 'buddypress' ); ?></option>
+			<option value="first_joined"><?php _e( 'Oldest', 'buddypress' ); ?></option>
+
+			<?php if ( bp_is_active( 'activity' ) ) : ?>
+				<option value="group_activity"><?php _e( 'Group Activity', 'buddypress' ); ?></option>
+			<?php endif; ?>
+
+			<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+
+			<?php
+
+			/**
+			 * Fires at the end of the Group members filters select input.
+			 *
+			 * Useful for plugins to add more filter options.
+			 *
+			 * @since 2.0.0
+			 */
+			do_action( 'bp_groups_members_order_options' ); ?>
+
+		</select>
+	</li>
+	<?php
+}
+
+/*
+ * Group Creation Process Template Tags
+ */
+
+/**
+ * Determine if the current logged in user can create groups.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if user can create groups. False otherwise.
+ */
+function bp_user_can_create_groups() {
+
+	// Super admin can always create groups.
+	if ( bp_current_user_can( 'bp_moderate' ) ) {
+		return true;
+	}
+
+	// Get group creation option, default to 0 (allowed).
+	$restricted = (int) bp_get_option( 'bp_restrict_group_creation', 0 );
+
+	// Allow by default.
+	$can_create = true;
+
+	// Are regular users restricted?
+	if ( $restricted ) {
+		$can_create = false;
+	}
+
+	/**
+	 * Filters if the current logged in user can create groups.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $can_create Whether the person can create groups.
+	 * @param int  $restricted Whether or not group creation is restricted.
+	 */
+	return apply_filters( 'bp_user_can_create_groups', $can_create, $restricted );
+}
+
+/**
+ * @since 1.0.0
+ *
+ * @return bool
+ */
+function bp_group_creation_tabs() {
+	$bp = buddypress();
+
+	if ( !is_array( $bp->groups->group_creation_steps ) ) {
+		return false;
+	}
+
+	if ( !bp_get_groups_current_create_step() ) {
+		$keys = array_keys( $bp->groups->group_creation_steps );
+		$bp->groups->current_create_step = array_shift( $keys );
+	}
+
+	$counter = 1;
+
+	foreach ( (array) $bp->groups->group_creation_steps as $slug => $step ) {
+		$is_enabled = bp_are_previous_group_creation_steps_complete( $slug ); ?>
+
+		<li<?php if ( bp_get_groups_current_create_step() == $slug ) : ?> class="current"<?php endif; ?>><?php if ( $is_enabled ) : ?><a href="<?php bp_groups_directory_permalink(); ?>create/step/<?php echo $slug ?>/"><?php else: ?><span><?php endif; ?><?php echo $counter ?>. <?php echo $step['name'] ?><?php if ( $is_enabled ) : ?></a><?php else: ?></span><?php endif ?></li><?php
+		$counter++;
+	}
+
+	unset( $is_enabled );
+
+	/**
+	 * Fires at the end of the creation of the group tabs.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'groups_creation_tabs' );
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_creation_stage_title() {
+	$bp = buddypress();
+
+	/**
+	 * Filters the group creation stage title.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string $value HTML markup for the group creation stage title.
+	 */
+	echo apply_filters( 'bp_group_creation_stage_title', '<span>&mdash; ' . $bp->groups->group_creation_steps[bp_get_groups_current_create_step()]['name'] . '</span>' );
+}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_creation_form_action() {
+	echo bp_get_group_creation_form_action();
+}
+
+/**
+ * @since 1.1.0
+ *
+ * @return mixed|void
+ */
+	function bp_get_group_creation_form_action() {
+		$bp = buddypress();
+
+		if ( !bp_action_variable( 1 ) ) {
+			$keys = array_keys( $bp->groups->group_creation_steps );
+			$bp->action_variables[1] = array_shift( $keys );
+		}
+
+		/**
+		 * Filters the group creation form action.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Action to be used with group creation form.
+		 */
+		return apply_filters( 'bp_get_group_creation_form_action', trailingslashit( bp_get_groups_directory_permalink() . 'create/step/' . bp_action_variable( 1 ) ) );
+	}
+
+/**
+ * @since 1.1.0
+ *
+ * @param string $step_slug
+ *
+ * @return bool
+ */
+function bp_is_group_creation_step( $step_slug ) {
+
+	// Make sure we are in the groups component.
+	if ( ! bp_is_groups_component() || ! bp_is_current_action( 'create' ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	// If this the first step, we can just accept and return true.
+	$keys = array_keys( $bp->groups->group_creation_steps );
+	if ( !bp_action_variable( 1 ) && array_shift( $keys ) == $step_slug ) {
+		return true;
+	}
+
+	// Before allowing a user to see a group creation step we must make sure
+	// previous steps are completed.
+	if ( !bp_is_first_group_creation_step() ) {
+		if ( !bp_are_previous_group_creation_steps_complete( $step_slug ) ) {
+			return false;
+		}
+	}
+
+	// Check the current step against the step parameter.
+	if ( bp_is_action_variable( $step_slug ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * @since 1.1.0
+ *
+ * @param array $step_slugs
+ *
+ * @return bool
+ */
+function bp_is_group_creation_step_complete( $step_slugs ) {
+	$bp = buddypress();
+
+	if ( !isset( $bp->groups->completed_create_steps ) ) {
+		return false;
+	}
+
+	if ( is_array( $step_slugs ) ) {
+		$found = true;
+
+		foreach ( (array) $step_slugs as $step_slug ) {
+			if ( !in_array( $step_slug, $bp->groups->completed_create_steps ) ) {
+				$found = false;
+			}
+		}
+
+		return $found;
+	} else {
+		return in_array( $step_slugs, $bp->groups->completed_create_steps );
+	}
+
+	return true;
+}
+
+/**
+ * @since 1.1.0
+ *
+ * @param string $step_slug
+ *
+ * @return bool
+ */
+function bp_are_previous_group_creation_steps_complete( $step_slug ) {
+	$bp = buddypress();
+
+	// If this is the first group creation step, return true.
+	$keys = array_keys( $bp->groups->group_creation_steps );
+	if ( array_shift( $keys ) == $step_slug ) {
+		return true;
+	}
+
+	reset( $bp->groups->group_creation_steps );
+
+	$previous_steps = array();
+
+	// Get previous steps.
+	foreach ( (array) $bp->groups->group_creation_steps as $slug => $name ) {
+		if ( $slug === $step_slug ) {
+			break;
+		}
+
+		$previous_steps[] = $slug;
+	}
+
+	return bp_is_group_creation_step_complete( $previous_steps );
+}
+
+/**
+ * @since 1.1.0
+ */
+function bp_new_group_id() {
+	echo bp_get_new_group_id();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return int
+	 */
+	function bp_get_new_group_id() {
+		$bp           = buddypress();
+		$new_group_id = isset( $bp->groups->new_group_id )
+			? $bp->groups->new_group_id
+			: 0;
+
+		/**
+		 * Filters the new group ID.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $new_group_id ID of the new group.
+		 */
+		return (int) apply_filters( 'bp_get_new_group_id', $new_group_id );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_new_group_name() {
+	echo bp_get_new_group_name();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_new_group_name() {
+		$bp   = buddypress();
+		$name = isset( $bp->groups->current_group->name )
+			? $bp->groups->current_group->name
+			: '';
+
+		/**
+		 * Filters the new group name.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $name Name of the new group.
+		 */
+		return apply_filters( 'bp_get_new_group_name', $name );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_new_group_description() {
+	echo bp_get_new_group_description();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_new_group_description() {
+		$bp          = buddypress();
+		$description = isset( $bp->groups->current_group->description )
+			? $bp->groups->current_group->description
+			: '';
+
+		/**
+		 * Filters the new group description.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $name Description of the new group.
+		 */
+		return apply_filters( 'bp_get_new_group_description', $description );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_new_group_enable_forum() {
+	echo bp_get_new_group_enable_forum();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return int
+	 */
+	function bp_get_new_group_enable_forum() {
+		$bp    = buddypress();
+		$forum = isset( $bp->groups->current_group->enable_forum )
+			? $bp->groups->current_group->enable_forum
+			: false;
+
+		/**
+		 * Filters whether or not to enable forums for the new group.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $forum Whether or not to enable forums.
+		 */
+		return (int) apply_filters( 'bp_get_new_group_enable_forum', $forum );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_new_group_status() {
+	echo bp_get_new_group_status();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_new_group_status() {
+		$bp     = buddypress();
+		$status = isset( $bp->groups->current_group->status )
+			? $bp->groups->current_group->status
+			: 'public';
+
+		/**
+		 * Filters the new group status.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $status Status for the new group.
+		 */
+		return apply_filters( 'bp_get_new_group_status', $status );
+	}
+
+/**
+ * Output the avatar for the group currently being created
+ *
+ * @since 1.1.0
+ *
+ * @see bp_core_fetch_avatar() For more information on accepted arguments
+ *
+ * @param array|string $args See bp_core_fetch_avatar().
+ */
+function bp_new_group_avatar( $args = '' ) {
+	echo bp_get_new_group_avatar( $args );
+}
+	/**
+	 * Return the avatar for the group currently being created
+	 *
+	 * @since 1.1.0
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and return values.
+	 *
+	 * @param array|string $args {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see {@link bp_core_fetch_avatar()}.
+	 *
+	 *     @type string   $alt     Default: 'Group photo'.
+	 *     @type string   $class   Default: 'avatar'.
+	 *     @type string   $type    Default: 'full'.
+	 *     @type int|bool $width   Default: false.
+	 *     @type int|bool $height  Default: false.
+	 *     @type string   $id      Passed to $css_id parameter. Default: 'avatar-crop-preview'.
+	 * }
+	 * @return string       The avatar for the group being created
+	 */
+	function bp_get_new_group_avatar( $args = '' ) {
+
+		// Parse arguments.
+		$r = bp_parse_args( $args, array(
+			'type'    => 'full',
+			'width'   => false,
+			'height'  => false,
+			'class'   => 'avatar',
+			'id'      => 'avatar-crop-preview',
+			'alt'     => __( 'Group photo', 'buddypress' ),
+		), 'get_new_group_avatar' );
+
+		// Merge parsed arguments with object specific data.
+		$r = array_merge( $r, array(
+			'item_id'    => bp_get_current_group_id(),
+			'object'     => 'group',
+			'avatar_dir' => 'group-avatars',
+		) );
+
+		// Get the avatar.
+		$avatar = bp_core_fetch_avatar( $r );
+
+		/**
+		 * Filters the new group avatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $avatar HTML markup for the new group avatar.
+		 * @param array  $r      Array of parsed arguments for the group avatar.
+		 * @param array  $args   Array of original arguments passed to the function.
+		 */
+		return apply_filters( 'bp_get_new_group_avatar', $avatar, $r, $args );
+	}
+
+/**
+ * Escape & output the URL to the previous group creation step
+ *
+ * @since 1.1.0
+ */
+function bp_group_creation_previous_link() {
+	echo esc_url( bp_get_group_creation_previous_link() );
+}
+	/**
+	 * Return the URL to the previous group creation step
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_creation_previous_link() {
+		$bp    = buddypress();
+		$steps = array_keys( $bp->groups->group_creation_steps );
+
+		// Loop through steps.
+		foreach ( $steps as $slug ) {
+
+			// Break when the current step is found.
+			if ( bp_is_action_variable( $slug ) ) {
+				break;
+			}
+
+			// Add slug to previous steps.
+			$previous_steps[] = $slug;
+		}
+
+		// Generate the URL for the previous step.
+		$group_directory = bp_get_groups_directory_permalink();
+		$create_step     = 'create/step/';
+		$previous_step   = array_pop( $previous_steps );
+		$url             = trailingslashit( $group_directory . $create_step . $previous_step );
+
+		/**
+		 * Filters the permalink for the previous step with the group creation process.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $url Permalink for the previous step.
+		 */
+		return apply_filters( 'bp_get_group_creation_previous_link', $url );
+	}
+
+/**
+ * Echoes the current group creation step.
+ *
+ * @since 1.6.0
+ */
+function bp_groups_current_create_step() {
+	echo bp_get_groups_current_create_step();
+}
+	/**
+	 * Returns the current group creation step. If none is found, returns an empty string.
+	 *
+	 * @since 1.6.0
+	 *
+	 *
+	 * @return string $current_create_step
+	 */
+	function bp_get_groups_current_create_step() {
+		$bp = buddypress();
+
+		if ( !empty( $bp->groups->current_create_step ) ) {
+			$current_create_step = $bp->groups->current_create_step;
+		} else {
+			$current_create_step = '';
+		}
+
+		/**
+		 * Filters the current group creation step.
+		 *
+		 * If none is found, returns an empty string.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $current_create_step Current step in the group creation process.
+		 */
+		return apply_filters( 'bp_get_groups_current_create_step', $current_create_step );
+	}
+
+/**
+ * Is the user looking at the last step in the group creation process.
+ *
+ * @since 1.1.0
+ *
+ * @param string $step Step to compare.
+ * @return bool True if yes, False if no
+ */
+function bp_is_last_group_creation_step( $step = '' ) {
+
+	// Use current step, if no step passed.
+	if ( empty( $step ) ) {
+		$step = bp_get_groups_current_create_step();
+	}
+
+	// Get the last step.
+	$bp     = buddypress();
+	$steps  = array_keys( $bp->groups->group_creation_steps );
+	$l_step = array_pop( $steps );
+
+	// Compare last step to step.
+	$retval = ( $l_step === $step );
+
+	/**
+	 * Filters whether or not user is looking at last step in group creation process.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param bool   $retval Whether or not we are looking at last step.
+	 * @param array  $steps  Array of steps from the group creation process.
+	 * @param string $step   Step to compare.
+	 */
+	return (bool) apply_filters( 'bp_is_last_group_creation_step', $retval, $steps, $step );
+}
+
+/**
+ * Is the user looking at the first step in the group creation process
+ *
+ * @since 1.1.0
+ *
+ * @param string $step Step to compare.
+ * @return bool True if yes, False if no
+ */
+function bp_is_first_group_creation_step( $step = '' ) {
+
+	// Use current step, if no step passed.
+	if ( empty( $step ) ) {
+		$step = bp_get_groups_current_create_step();
+	}
+
+	// Get the first step.
+	$bp     = buddypress();
+	$steps  = array_keys( $bp->groups->group_creation_steps );
+	$f_step = array_shift( $steps );
+
+	// Compare first step to step.
+	$retval = ( $f_step === $step );
+
+	/**
+	 * Filters whether or not user is looking at first step in group creation process.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param bool   $retval Whether or not we are looking at first step.
+	 * @param array  $steps  Array of steps from the group creation process.
+	 * @param string $step   Step to compare.
+	 */
+	return (bool) apply_filters( 'bp_is_first_group_creation_step', $retval, $steps, $step );
+}
+
+/**
+ * Output a list of friends who can be invited to a group
+ *
+ * @since 1.0.0
+ *
+ * @param array $args Array of arguments for friends list output.
+ */
+function bp_new_group_invite_friend_list( $args = array() ) {
+	echo bp_get_new_group_invite_friend_list( $args );
+}
+	/**
+	 * Return a list of friends who can be invited to a group
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $args Array of arguments for friends list output.
+	 * @return false|string HTML list of checkboxes, or false
+	 */
+	function bp_get_new_group_invite_friend_list( $args = array() ) {
+
+		// Bail if no friends component.
+		if ( ! bp_is_active( 'friends' ) ) {
+			return false;
+		}
+
+		// Parse arguments.
+		$r = bp_parse_args( $args, array(
+			'user_id'   => bp_loggedin_user_id(),
+			'group_id'  => false,
+			'before'    => '',
+			'separator' => 'li',
+			'after'     => '',
+		), 'group_invite_friend_list' );
+
+		// No group passed, so look for new or current group ID's.
+		if ( empty( $r['group_id'] ) ) {
+			$bp            = buddypress();
+			$r['group_id'] = ! empty( $bp->groups->new_group_id )
+				? $bp->groups->new_group_id
+				: $bp->groups->current_group->id;
+		}
+
+		// Setup empty items array.
+		$items = array();
+
+		// Build list markup parent elements.
+		$before = '';
+		if ( ! empty( $r['before'] ) ) {
+			$before = $r['before'];
+		}
+
+		$after = '';
+		if ( ! empty( $r['after'] ) ) {
+			$after = $r['after'];
+		}
+
+		// Get user's friends who are not in this group already.
+		$friends = friends_get_friends_invite_list( $r['user_id'], $r['group_id'] );
+
+		if ( ! empty( $friends ) ) {
+
+			// Get already invited users.
+			$invites = groups_get_invites_for_group( $r['user_id'], $r['group_id'] );
+
+			for ( $i = 0, $count = count( $friends ); $i < $count; ++$i ) {
+				$checked = in_array( (int) $friends[ $i ]['id'], (array) $invites );
+				$items[] = '<' . $r['separator'] . '><label for="f-' . esc_attr( $friends[ $i ]['id'] ) . '"><input' . checked( $checked, true, false ) . ' type="checkbox" name="friends[]" id="f-' . esc_attr( $friends[ $i ]['id'] ) . '" value="' . esc_attr( $friends[ $i ]['id'] ) . '" /> ' . esc_html( $friends[ $i ]['full_name'] ) . '</label></' . $r['separator'] . '>';
+			}
+		}
+
+		/**
+		 * Filters the array of friends who can be invited to a group.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param array $items Array of friends.
+		 * @param array $r     Parsed arguments from bp_get_new_group_invite_friend_list()
+		 * @param array $args  Unparsed arguments from bp_get_new_group_invite_friend_list()
+		 */
+		$invitable_friends = apply_filters( 'bp_get_new_group_invite_friend_list', $items, $r, $args );
+
+		if ( ! empty( $invitable_friends ) && is_array( $invitable_friends ) ) {
+			$retval = $before . implode( "\n", $invitable_friends ) . $after;
+		} else {
+			$retval = false;
+		}
+
+		return $retval;
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_directory_groups_search_form() {
+
+	$query_arg = bp_core_get_component_search_query_arg( 'groups' );
+
+	if ( ! empty( $_REQUEST[ $query_arg ] ) ) {
+		$search_value = stripslashes( $_REQUEST[ $query_arg ] );
+	} else {
+		$search_value = bp_get_search_default_text( 'groups' );
+	}
+
+	$search_form_html = '<form action="" method="get" id="search-groups-form">
+		<label for="groups_search"><input type="text" name="' . esc_attr( $query_arg ) . '" id="groups_search" placeholder="'. esc_attr( $search_value ) .'" /></label>
+		<input type="submit" id="groups_search_submit" name="groups_search_submit" value="'. __( 'Search', 'buddypress' ) .'" />
+	</form>';
+
+	/**
+	 * Filters the HTML markup for the groups search form.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $search_form_html HTML markup for the search form.
+	 */
+	echo apply_filters( 'bp_directory_groups_search_form', $search_form_html );
+
+}
+
+/**
+ * Displays group header tabs.
+ *
+ * @since 1.0.0
+ *
+ * @todo Deprecate?
+ */
+function bp_groups_header_tabs() {
+	$user_groups = bp_displayed_user_domain() . bp_get_groups_slug(); ?>
+
+	<li<?php if ( !bp_action_variable( 0 ) || bp_is_action_variable( 'recently-active', 0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( $user_groups . '/my-groups/recently-active' ); ?>"><?php _e( 'Recently Active', 'buddypress' ); ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'recently-joined', 0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( $user_groups . '/my-groups/recently-joined' ); ?>"><?php _e( 'Recently Joined',  'buddypress' ); ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'most-popular',    0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( $user_groups . '/my-groups/most-popular'    ); ?>"><?php _e( 'Most Popular',     'buddypress' ); ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'admin-of',        0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( $user_groups . '/my-groups/admin-of'        ); ?>"><?php _e( 'Administrator Of', 'buddypress' ); ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'mod-of',          0 ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( $user_groups . '/my-groups/mod-of'          ); ?>"><?php _e( 'Moderator Of',     'buddypress' ); ?></a></li>
+	<li<?php if ( bp_is_action_variable( 'alphabetically'     ) ) : ?> class="current"<?php endif; ?>><a href="<?php echo trailingslashit( $user_groups . '/my-groups/alphabetically'  ); ?>"><?php _e( 'Alphabetically',   'buddypress' ); ?></a></li>
+
+<?php
+	do_action( 'groups_header_tabs' );
+}
+
+/**
+ * Displays group filter titles.
+ *
+ * @since 1.0.0
+ *
+ * @todo Deprecate?
+ */
+function bp_groups_filter_title() {
+	$current_filter = bp_action_variable( 0 );
+
+	switch ( $current_filter ) {
+		case 'recently-active': default:
+			_e( 'Recently Active', 'buddypress' );
+			break;
+		case 'recently-joined':
+			_e( 'Recently Joined', 'buddypress' );
+			break;
+		case 'most-popular':
+			_e( 'Most Popular', 'buddypress' );
+			break;
+		case 'admin-of':
+			_e( 'Administrator Of', 'buddypress' );
+			break;
+		case 'mod-of':
+			_e( 'Moderator Of', 'buddypress' );
+			break;
+		case 'alphabetically':
+			_e( 'Alphabetically', 'buddypress' );
+		break;
+	}
+	do_action( 'bp_groups_filter_title' );
+}
+
+/**
+ * Echo the current group type message.
+ *
+ * @since 2.7.0
+ */
+function bp_current_group_directory_type_message() {
+	echo bp_get_current_group_directory_type_message();
+}
+	/**
+	 * Generate the current group type message.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return string
+	 */
+	function bp_get_current_group_directory_type_message() {
+		$type_object = bp_groups_get_group_type_object( bp_get_current_group_directory_type() );
+
+		$message = sprintf( __( 'Viewing groups of the type: %s', 'buddypress' ), '<strong>' . $type_object->labels['singular_name'] . '</strong>' );
+
+		/**
+		 * Filters the current group type message.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string $message Message to filter.
+		 */
+		return apply_filters( 'bp_get_current_group_type_message', $message );
+	}
+
+/**
+ * Is the current page a specific group admin screen?
+ *
+ * @since 1.1.0
+ *
+ * @param string $slug Admin screen slug.
+ * @return bool
+ */
+function bp_is_group_admin_screen( $slug = '' ) {
+	return (bool) ( bp_is_group_admin_page() && bp_is_action_variable( $slug ) );
+}
+
+/**
+ * Echoes the current group admin tab slug.
+ *
+ * @since 1.6.0
+ */
+function bp_group_current_admin_tab() {
+	echo bp_get_group_current_admin_tab();
+}
+	/**
+	 * Returns the current group admin tab slug.
+	 *
+	 * @since 1.6.0
+	 *
+	 *
+	 * @return string $tab The current tab's slug.
+	 */
+	function bp_get_group_current_admin_tab() {
+		if ( bp_is_groups_component() && bp_is_current_action( 'admin' ) ) {
+			$tab = bp_action_variable( 0 );
+		} else {
+			$tab = '';
+		}
+
+		/**
+		 * Filters the current group admin tab slug.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $tab Current group admin tab slug.
+		 */
+		return apply_filters( 'bp_get_current_group_admin_tab', $tab );
+	}
+
+/** Group Avatar Template Tags ************************************************/
+
+/**
+ * Outputs the current group avatar.
+ *
+ * @since 1.0.0
+ *
+ * @param string $type Thumb or full.
+ */
+function bp_group_current_avatar( $type = 'thumb' ) {
+	echo bp_get_group_current_avatar( $type );
+}
+	/**
+	 * Returns the current group avatar.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $type Thumb or full.
+	 * @return string $tab The current tab's slug.
+	 */
+	function bp_get_group_current_avatar( $type = 'thumb' ) {
+
+		$group_avatar = bp_core_fetch_avatar( array(
+			'item_id'    => bp_get_current_group_id(),
+			'object'     => 'group',
+			'type'       => $type,
+			'avatar_dir' => 'group-avatars',
+			'alt'        => __( 'Group avatar', 'buddypress' ),
+			'class'      => 'avatar'
+		) );
+
+		/**
+		 * Filters the current group avatar.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $group_avatar HTML markup for current group avatar.
+		 */
+		return apply_filters( 'bp_get_group_current_avatar', $group_avatar );
+	}
+
+/**
+ * Return whether a group has an avatar.
+ *
+ * @since 1.1.0
+ *
+ * @param int|bool $group_id Group ID to check.
+ * @return boolean
+ */
+function bp_get_group_has_avatar( $group_id = false ) {
+
+	if ( false === $group_id ) {
+		$group_id = bp_get_current_group_id();
+	}
+
+	$avatar_args = array(
+		'item_id' => $group_id,
+		'object'  => 'group',
+		'no_grav' => true,
+		'html'    => false,
+		'type'    => 'thumb',
+	);
+
+	$group_avatar = bp_core_fetch_avatar( $avatar_args );
+
+	if ( bp_core_avatar_default( 'local', $avatar_args ) === $group_avatar ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_avatar_delete_link() {
+	echo bp_get_group_avatar_delete_link();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_avatar_delete_link() {
+		$bp = buddypress();
+
+		/**
+		 * Filters the URL to delete the group avatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value URL to delete the group avatar.
+		 */
+		return apply_filters( 'bp_get_group_avatar_delete_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( $bp->groups->current_group ) . 'admin/group-avatar/delete' ), 'bp_group_avatar_delete' ) );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_custom_group_boxes() {
+	do_action( 'groups_custom_group_boxes' );
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_custom_group_admin_tabs() {
+	do_action( 'groups_custom_group_admin_tabs' );
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_custom_group_fields_editable() {
+	do_action( 'groups_custom_group_fields_editable' );
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_custom_group_fields() {
+	do_action( 'groups_custom_group_fields' );
+}
+
+/* Group Membership Requests *************************************************/
+
+/**
+ * Initialize a group membership request template loop.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {
+ *     @type int $group_id ID of the group. Defaults to current group.
+ *     @type int $per_page Number of records to return per page. Default: 10.
+ *     @type int $page     Page of results to return. Default: 1.
+ *     @type int $max      Max number of items to return. Default: false.
+ * }
+ * @return bool True if there are requests, otherwise false.
+ */
+function bp_group_has_membership_requests( $args = '' ) {
+	global $requests_template;
+
+	$r = bp_parse_args( $args, array(
+		'group_id' => bp_get_current_group_id(),
+		'per_page' => 10,
+		'page'     => 1,
+		'max'      => false
+	), 'group_has_membership_requests' );
+
+	$requests_template = new BP_Groups_Membership_Requests_Template( $r );
+
+	/**
+	 * Filters whether or not a group membership query has requests to display.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool                                   $value             Whether there are requests to display.
+	 * @param BP_Groups_Membership_Requests_Template $requests_template Object holding the requests query results.
+	 */
+	return apply_filters( 'bp_group_has_membership_requests', $requests_template->has_requests(), $requests_template );
+}
+
+/**
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_group_membership_requests() {
+	global $requests_template;
+
+	return $requests_template->requests();
+}
+
+/**
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_group_the_membership_request() {
+	global $requests_template;
+
+	return $requests_template->the_request();
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_request_user_avatar_thumb() {
+	global $requests_template;
+
+	/**
+	 * Filters the requesting user's avatar thumbnail.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value HTML markup for the user's avatar thumbnail.
+	 */
+	echo apply_filters( 'bp_group_request_user_avatar_thumb', bp_core_fetch_avatar( array( 'item_id' => $requests_template->request->user_id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_core_get_user_displayname( $requests_template->request->user_id ) ) ) ) );
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_request_reject_link() {
+	echo bp_get_group_request_reject_link();
+}
+
+	/**
+	 * @since 1.2.6
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_request_reject_link() {
+		global $requests_template;
+
+		/**
+		 * Filters the URL to use to reject a membership request.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string $value URL to use to reject a membership request.
+		 */
+		return apply_filters( 'bp_get_group_request_reject_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( groups_get_current_group() ) . 'admin/membership-requests/reject/' . $requests_template->request->membership_id ), 'groups_reject_membership_request' ) );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_request_accept_link() {
+	echo bp_get_group_request_accept_link();
+}
+
+	/**
+	 * @since 1.2.6
+	 * @return mixed|void
+	 */
+	function bp_get_group_request_accept_link() {
+		global $requests_template;
+
+		/**
+		 * Filters the URL to use to accept a membership request.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string $value URL to use to accept a membership request.
+		 */
+		return apply_filters( 'bp_get_group_request_accept_link', wp_nonce_url( trailingslashit( bp_get_group_permalink( groups_get_current_group() ) . 'admin/membership-requests/accept/' . $requests_template->request->membership_id ), 'groups_accept_membership_request' ) );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_request_user_link() {
+	echo bp_get_group_request_user_link();
+}
+
+	/**
+	 * @since 1.2.6
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_request_user_link() {
+		global $requests_template;
+
+		/**
+		 * Filters the URL for the user requesting membership.
+		 *
+		 * @since 1.2.6
+		 *
+		 * @param string $value URL for the user requestion membership.
+		 */
+		return apply_filters( 'bp_get_group_request_user_link', bp_core_get_userlink( $requests_template->request->user_id ) );
+	}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_request_time_since_requested() {
+	global $requests_template;
+
+	/**
+	 * Filters the formatted time since membership was requested.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Formatted time since membership was requested.
+	 */
+	echo apply_filters( 'bp_group_request_time_since_requested', sprintf( __( 'requested %s', 'buddypress' ), bp_core_time_since( $requests_template->request->date_modified ) ) );
+}
+
+/**
+ * @since 1.0.0
+ */
+function bp_group_request_comment() {
+	global $requests_template;
+
+	/**
+	 * Filters the membership request comment left by user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Membership request comment left by user.
+	 */
+	echo apply_filters( 'bp_group_request_comment', strip_tags( stripslashes( $requests_template->request->comments ) ) );
+}
+
+/**
+ * Output pagination links for group membership requests.
+ *
+ * @since 2.0.0
+ */
+function bp_group_requests_pagination_links() {
+	echo bp_get_group_requests_pagination_links();
+}
+	/**
+	 * Get pagination links for group membership requests.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_requests_pagination_links() {
+		global $requests_template;
+
+		/**
+		 * Filters pagination links for group membership requests.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $value Pagination links for group membership requests.
+		 */
+		return apply_filters( 'bp_get_group_requests_pagination_links', $requests_template->pag_links );
+	}
+
+/**
+ * Output pagination count text for group membership requests.
+ *
+ * @since 2.0.0
+ */
+function bp_group_requests_pagination_count() {
+	echo bp_get_group_requests_pagination_count();
+}
+	/**
+	 * Get pagination count text for group membership requests.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_requests_pagination_count() {
+		global $requests_template;
+
+		$start_num = intval( ( $requests_template->pag_page - 1 ) * $requests_template->pag_num ) + 1;
+		$from_num  = bp_core_number_format( $start_num );
+		$to_num    = bp_core_number_format( ( $start_num + ( $requests_template->pag_num - 1 ) > $requests_template->total_request_count ) ? $requests_template->total_request_count : $start_num + ( $requests_template->pag_num - 1 ) );
+		$total     = bp_core_number_format( $requests_template->total_request_count );
+
+		if ( 1 == $requests_template->total_request_count ) {
+			$message = __( 'Viewing 1 request', 'buddypress' );
+		} else {
+			$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s request', 'Viewing %1$s - %2$s of %3$s requests', $requests_template->total_request_count, 'buddypress' ), $from_num, $to_num, $total );
+		}
+
+		/**
+		 * Filters pagination count text for group membership requests.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $message  Pagination count text for group membership requests.
+		 * @param string $from_num Total amount for the low value in the range.
+		 * @param string $to_num   Total amount for the high value in the range.
+		 * @param string $total    Total amount of members found.
+		 */
+		return apply_filters( 'bp_get_group_requests_pagination_count', $message, $from_num, $to_num, $total );
+	}
+
+/** Group Invitations *********************************************************/
+
+/**
+ * Whether or not there are invites.
+ *
+ * @since 1.1.0
+ *
+ * @param string $args
+ * @return bool|mixed|void
+ */
+function bp_group_has_invites( $args = '' ) {
+	global $invites_template, $group_id;
+
+	$r = bp_parse_args( $args, array(
+		'group_id' => false,
+		'user_id'  => bp_loggedin_user_id(),
+		'per_page' => false,
+		'page'     => 1,
+	), 'group_has_invites' );
+
+	if ( empty( $r['group_id'] ) ) {
+		if ( groups_get_current_group() ) {
+			$r['group_id'] = bp_get_current_group_id();
+		} elseif ( ! empty( buddypress()->groups->new_group_id ) ) {
+			$r['group_id'] = buddypress()->groups->new_group_id;
+		}
+	}
+
+	// Set the global (for use in BP_Groups_Invite_Template::the_invite()).
+	if ( empty( $group_id ) ) {
+		$group_id = $r['group_id'];
+	}
+
+	if ( ! $group_id ) {
+		return false;
+	}
+
+	$invites_template = new BP_Groups_Invite_Template( $r );
+
+	/**
+	 * Filters whether or not a group invites query has invites to display.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool                      $value            Whether there are requests to display.
+	 * @param BP_Groups_Invite_Template $invites_template Object holding the invites query results.
+	 */
+	return apply_filters( 'bp_group_has_invites', $invites_template->has_invites(), $invites_template );
+}
+
+/**
+ * @since 1.1.0
+ *
+ * @return mixed
+ */
+function bp_group_invites() {
+	global $invites_template;
+
+	return $invites_template->invites();
+}
+
+/**
+ * @since 1.1.0
+ *
+ * @return mixed
+ */
+function bp_group_the_invite() {
+	global $invites_template;
+
+	return $invites_template->the_invite();
+}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_invite_item_id() {
+	echo bp_get_group_invite_item_id();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_invite_item_id() {
+		global $invites_template;
+
+		/**
+		 * Filters the group invite item ID.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Group invite item ID.
+		 */
+		return apply_filters( 'bp_get_group_invite_item_id', 'uid-' . $invites_template->invite->user->id );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_invite_user_avatar() {
+	echo bp_get_group_invite_user_avatar();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_invite_user_avatar() {
+		global $invites_template;
+
+		/**
+		 * Filters the group invite user avatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Group invite user avatar.
+		 */
+		return apply_filters( 'bp_get_group_invite_user_avatar', $invites_template->invite->user->avatar_thumb );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_invite_user_link() {
+	echo bp_get_group_invite_user_link();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_invite_user_link() {
+		global $invites_template;
+
+		/**
+		 * Filters the group invite user link.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Group invite user link.
+		 */
+		return apply_filters( 'bp_get_group_invite_user_link', bp_core_get_userlink( $invites_template->invite->user->id ) );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_invite_user_last_active() {
+	echo bp_get_group_invite_user_last_active();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return mixed|void
+	 */
+	function bp_get_group_invite_user_last_active() {
+		global $invites_template;
+
+		/**
+		 * Filters the group invite user's last active time.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Group invite user's last active time.
+		 */
+		return apply_filters( 'bp_get_group_invite_user_last_active', $invites_template->invite->user->last_active );
+	}
+
+/**
+ * @since 1.1.0
+ */
+function bp_group_invite_user_remove_invite_url() {
+	echo bp_get_group_invite_user_remove_invite_url();
+}
+
+	/**
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_invite_user_remove_invite_url() {
+		global $invites_template;
+
+		$user_id = intval( $invites_template->invite->user->id );
+
+		if ( bp_is_current_action( 'create' ) ) {
+			$uninvite_url = bp_get_groups_directory_permalink() . 'create/step/group-invites/?user_id=' . $user_id;
+		} else {
+			$uninvite_url = trailingslashit( bp_get_group_permalink( groups_get_current_group() ) . 'send-invites/remove/' . $user_id );
+		}
+
+		return wp_nonce_url( $uninvite_url, 'groups_invite_uninvite_user' );
+	}
+
+/**
+ * Output pagination links for group invitations.
+ *
+ * @since 2.0.0
+ */
+function bp_group_invite_pagination_links() {
+	echo bp_get_group_invite_pagination_links();
+}
+
+	/**
+	 * Get pagination links for group invitations.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_invite_pagination_links() {
+		global $invites_template;
+
+		/**
+		 * Filters the pagination links for group invitations.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $value Pagination links for group invitations.
+		 */
+		return apply_filters( 'bp_get_group_invite_pagination_links', $invites_template->pag_links );
+	}
+
+/**
+ * Output pagination count text for group invitations.
+ *
+ * @since 2.0.0
+ */
+function bp_group_invite_pagination_count() {
+	echo bp_get_group_invite_pagination_count();
+}
+	/**
+	 * Get pagination count text for group invitations.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_invite_pagination_count() {
+		global $invites_template;
+
+		$start_num = intval( ( $invites_template->pag_page - 1 ) * $invites_template->pag_num ) + 1;
+		$from_num  = bp_core_number_format( $start_num );
+		$to_num    = bp_core_number_format( ( $start_num + ( $invites_template->pag_num - 1 ) > $invites_template->total_invite_count ) ? $invites_template->total_invite_count : $start_num + ( $invites_template->pag_num - 1 ) );
+		$total     = bp_core_number_format( $invites_template->total_invite_count );
+
+		if ( 1 == $invites_template->total_invite_count ) {
+			$message = __( 'Viewing 1 invitation', 'buddypress' );
+		} else {
+			$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s invitation', 'Viewing %1$s - %2$s of %3$s invitations', $invites_template->total_invite_count, 'buddypress' ), $from_num, $to_num, $total );
+		}
+
+		/** This filter is documented in bp-groups/bp-groups-template.php */
+		return apply_filters( 'bp_get_groups_pagination_count', $message, $from_num, $to_num, $total );
+	}
+
+/** Group RSS *****************************************************************/
+
+/**
+ * Hook group activity feed to <head>.
+ *
+ * @since 1.5.0
+ */
+function bp_groups_activity_feed() {
+
+	// Bail if not viewing a single group or activity is not active.
+	if ( ! bp_is_active( 'groups' ) || ! bp_is_active( 'activity' ) || ! bp_is_group() ) {
+		return;
+	} ?>
+
+	<link rel="alternate" type="application/rss+xml" title="<?php bloginfo( 'name' ) ?> | <?php bp_current_group_name() ?> | <?php _e( 'Group Activity RSS Feed', 'buddypress' ) ?>" href="<?php bp_group_activity_feed_link() ?>" />
+
+<?php
+}
+add_action( 'bp_head', 'bp_groups_activity_feed' );
+
+/**
+ * Output the current group activity-stream RSS URL.
+ *
+ * @since 1.5.0
+ */
+function bp_group_activity_feed_link() {
+	echo bp_get_group_activity_feed_link();
+}
+	/**
+	 * Return the current group activity-stream RSS URL.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_group_activity_feed_link() {
+		$current_group = groups_get_current_group();
+		$group_link    = bp_get_group_permalink( $current_group ) . 'feed';
+		$feed_link     = trailingslashit( $group_link );
+
+		/**
+		 * Filters the current group activity-stream RSS URL.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $feed_link Current group activity-stream RSS URL.
+		 */
+		return apply_filters( 'bp_get_group_activity_feed_link', $feed_link );
+	}
+
+/** Current Group *************************************************************/
+
+/**
+ * Echoes the output of bp_get_current_group_id().
+ *
+ * @since 1.5.0
+ */
+function bp_current_group_id() {
+	echo bp_get_current_group_id();
+}
+	/**
+	 * Returns the ID of the current group.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return int $current_group_id The id of the current group, if there is one.
+	 */
+	function bp_get_current_group_id() {
+		$current_group    = groups_get_current_group();
+		$current_group_id = isset( $current_group->id ) ? (int) $current_group->id : 0;
+
+		/**
+		 * Filters the ID of the current group.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int    $current_group_id ID of the current group.
+		 * @param object $current_group    Instance holding the current group.
+		 */
+		return apply_filters( 'bp_get_current_group_id', $current_group_id, $current_group );
+	}
+
+/**
+ * Echoes the output of bp_get_current_group_slug().
+ *
+ * @since 1.5.0
+ */
+function bp_current_group_slug() {
+	echo bp_get_current_group_slug();
+}
+	/**
+	 * Returns the slug of the current group.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string $current_group_slug The slug of the current group, if there is one.
+	 */
+	function bp_get_current_group_slug() {
+		$current_group      = groups_get_current_group();
+		$current_group_slug = isset( $current_group->slug ) ? $current_group->slug : '';
+
+		/**
+		 * Filters the slug of the current group.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $current_group_slug Slug of the current group.
+		 * @param object $current_group      Instance holding the current group.
+		 */
+		return apply_filters( 'bp_get_current_group_slug', $current_group_slug, $current_group );
+	}
+
+/**
+ * Echoes the output of bp_get_current_group_name().
+ *
+ * @since 1.5.0
+ */
+function bp_current_group_name() {
+	echo bp_get_current_group_name();
+}
+	/**
+	 * Returns the name of the current group.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string The name of the current group, if there is one.
+	 */
+	function bp_get_current_group_name() {
+		$current_group = groups_get_current_group();
+		$current_name  = bp_get_group_name( $current_group );
+
+		/**
+		 * Filters the name of the current group.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $current_name  Name of the current group.
+		 * @param object $current_group Instance holding the current group.
+		 */
+		return apply_filters( 'bp_get_current_group_name', $current_name, $current_group );
+	}
+
+/**
+ * Echoes the output of bp_get_current_group_description().
+ *
+ * @since 2.1.0
+ */
+function bp_current_group_description() {
+	echo bp_get_current_group_description();
+}
+	/**
+	 * Returns the description of the current group.
+	 *
+	 * @since 2.1.0
+	 *                       this output.
+	 *
+	 * @return string The description of the current group, if there is one.
+	 */
+	function bp_get_current_group_description() {
+		$current_group      = groups_get_current_group();
+		$current_group_desc = isset( $current_group->description ) ? $current_group->description : '';
+
+		/**
+		 * Filters the description of the current group.
+		 *
+		 * This filter is used to apply extra filters related to formatting.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $current_group_desc Description of the current group.
+		 */
+		$desc               = apply_filters( 'bp_get_group_description', $current_group_desc );
+
+		/**
+		 * Filters the description of the current group.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $desc Description of the current group.
+		 */
+		return apply_filters( 'bp_get_current_group_description', $desc );
+	}
+
+/**
+ * Output a URL for a group component action.
+ *
+ * @since 1.2.0
+ *
+ * @param string $action
+ * @param string $query_args
+ * @param bool $nonce
+ * @return string|null
+ */
+function bp_groups_action_link( $action = '', $query_args = '', $nonce = false ) {
+	echo bp_get_groups_action_link( $action, $query_args, $nonce );
+}
+	/**
+	 * Get a URL for a group component action.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $action
+	 * @param string $query_args
+	 * @param bool $nonce
+	 * @return string
+	 */
+	function bp_get_groups_action_link( $action = '', $query_args = '', $nonce = false ) {
+
+		$current_group = groups_get_current_group();
+		$url           = '';
+
+		// Must be a group.
+		if ( ! empty( $current_group->id ) ) {
+
+			// Append $action to $url if provided
+			if ( !empty( $action ) ) {
+				$url = bp_get_group_permalink( $current_group ) . $action;
+			} else {
+				$url = bp_get_group_permalink( $current_group );
+			}
+
+			// Add a slash at the end of our user url.
+			$url = trailingslashit( $url );
+
+			// Add possible query args.
+			if ( !empty( $query_args ) && is_array( $query_args ) ) {
+				$url = add_query_arg( $query_args, $url );
+			}
+
+			// To nonce, or not to nonce...
+			if ( true === $nonce ) {
+				$url = wp_nonce_url( $url );
+			} elseif ( is_string( $nonce ) ) {
+				$url = wp_nonce_url( $url, $nonce );
+			}
+		}
+
+		/**
+		 * Filters a URL for a group component action.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $url        URL for a group component action.
+		 * @param string $action     Action being taken for the group.
+		 * @param string $query_args Query arguments being passed.
+		 * @param bool   $nonce      Whether or not to add a nonce.
+		 */
+		return apply_filters( 'bp_get_groups_action_link', $url, $action, $query_args, $nonce );
+	}
+
+/** Stats **********************************************************************/
+
+/**
+ * Display the number of groups in user's profile.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args before|after|user_id
+ *
+ */
+function bp_groups_profile_stats( $args = '' ) {
+	echo bp_groups_get_profile_stats( $args );
+}
+add_action( 'bp_members_admin_user_stats', 'bp_groups_profile_stats', 8, 1 );
+
+/**
+ * Return the number of groups in user's profile.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args before|after|user_id
+ * @return string HTML for stats output.
+ */
+function bp_groups_get_profile_stats( $args = '' ) {
+
+	// Parse the args
+	$r = bp_parse_args( $args, array(
+		'before'  => '<li class="bp-groups-profile-stats">',
+		'after'   => '</li>',
+		'user_id' => bp_displayed_user_id(),
+		'groups'  => 0,
+		'output'  => ''
+	), 'groups_get_profile_stats' );
+
+	// Allow completely overloaded output
+	if ( empty( $r['output'] ) ) {
+
+		// Only proceed if a user ID was passed
+		if ( ! empty( $r['user_id'] ) ) {
+
+			// Get the user groups
+			if ( empty( $r['groups'] ) ) {
+				$r['groups'] = absint( bp_get_total_group_count_for_user( $r['user_id'] ) );
+			}
+
+			// If groups exist, show some formatted output
+			$r['output'] = $r['before'] . sprintf( _n( '%s group', '%s groups', $r['groups'], 'buddypress' ), '<strong>' . $r['groups'] . '</strong>' ) . $r['after'];
+		}
+	}
+
+	/**
+	 * Filters the number of groups in user's profile.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $value HTML for stats output.
+	 * @param array  $r     Array of parsed arguments for query.
+	 */
+	return apply_filters( 'bp_groups_get_profile_stats', $r['output'], $r );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/bp-groups-widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/bp-groups-widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/bp-groups-widgets.php	(revision 12265)
@@ -0,0 +1,90 @@
+<?php
+/**
+ * BuddyPress Groups Widgets
+ *
+ * @package BuddyPress
+ * @subpackage GroupsWidgets
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register widgets for groups component.
+ *
+ * @since 1.0.0
+ */
+function groups_register_widgets() {
+	add_action( 'widgets_init', function() { register_widget( 'BP_Groups_Widget' ); } );
+}
+add_action( 'bp_register_widgets', 'groups_register_widgets' );
+
+/**
+ * AJAX callback for the Groups List widget.
+ *
+ * @since 1.0.0
+ */
+function groups_ajax_widget_groups_list() {
+
+	check_ajax_referer( 'groups_widget_groups_list' );
+
+	switch ( $_POST['filter'] ) {
+		case 'newest-groups':
+			$type = 'newest';
+		break;
+		case 'recently-active-groups':
+			$type = 'active';
+		break;
+		case 'popular-groups':
+			$type = 'popular';
+		break;
+		case 'alphabetical-groups':
+			$type = 'alphabetical';
+		break;
+	}
+
+	$per_page = isset( $_POST['max_groups'] ) ? intval( $_POST['max_groups'] ) : 5;
+
+	$groups_args = array(
+		'user_id'  => 0,
+		'type'     => $type,
+		'per_page' => $per_page,
+		'max'      => $per_page,
+	);
+
+	if ( bp_has_groups( $groups_args ) ) : ?>
+		<?php echo "0[[SPLIT]]"; ?>
+		<?php while ( bp_groups() ) : bp_the_group(); ?>
+			<li <?php bp_group_class(); ?>>
+				<div class="item-avatar">
+					<a href="<?php bp_group_permalink() ?>"><?php bp_group_avatar_thumb() ?></a>
+				</div>
+
+				<div class="item">
+					<div class="item-title"><?php bp_group_link(); ?></div>
+					<div class="item-meta">
+						<?php if ( 'newest-groups' === $_POST['filter'] ) : ?>
+							<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_date_created( 0, array( 'relative' => false ) ) ); ?>"><?php printf( __( 'created %s', 'buddypress' ), bp_get_group_date_created() ); ?></span>
+						<?php elseif ( 'popular-groups' === $_POST['filter'] ) : ?>
+							<span class="activity"><?php bp_group_member_count(); ?></span>
+						<?php else : ?>
+							<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_last_active( 0, array( 'relative' => false ) ) ); ?>"><?php printf( __( 'active %s', 'buddypress' ), bp_get_group_last_active() ); ?></span>
+						<?php endif; ?>
+					</div>
+				</div>
+			</li>
+		<?php endwhile; ?>
+
+		<?php wp_nonce_field( 'groups_widget_groups_list', '_wpnonce-groups' ); ?>
+		<input type="hidden" name="groups_widget_max" id="groups_widget_max" value="<?php echo esc_attr( $_POST['max_groups'] ); ?>" />
+
+	<?php else: ?>
+
+		<?php echo "-1[[SPLIT]]<li>" . __( "No groups matched the current filter.", 'buddypress' ); ?>
+
+	<?php endif;
+
+}
+add_action( 'wp_ajax_widget_groups_list',        'groups_ajax_widget_groups_list' );
+add_action( 'wp_ajax_nopriv_widget_groups_list', 'groups_ajax_widget_groups_list' );
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-group-extension.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-group-extension.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-group-extension.php	(revision 12265)
@@ -0,0 +1,1720 @@
+<?php
+/**
+ * BuddyPress Groups Classes.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsClasses
+ * @since 1.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * API for creating group extensions without having to hardcode the content into
+ * the theme.
+ *
+ * To implement, extend this class. In your constructor, pass an optional array
+ * of arguments to parent::init() to configure your widget. The config array
+ * supports the following values:
+ *   - 'slug' A unique identifier for your extension. This value will be used
+ *     to build URLs, so make it URL-safe.
+ *   - 'name' A translatable name for your extension. This value is used to
+ *     populate the navigation tab, as well as the default titles for admin/
+ *     edit/create tabs.
+ *   - 'visibility' Set to 'public' (default) for your extension (the main tab
+ *     as well as the widget) to be available to anyone who can access the
+ *     group, 'private' otherwise.
+ *   - 'nav_item_position' An integer explaining where the nav item should
+ *     appear in the tab list.
+ *   - 'enable_nav_item' Set to true for your extension's main tab to be
+ *     available to anyone who can access the group.
+ *   - 'nav_item_name' The translatable text you want to appear in the nav tab.
+ *     Defaults to the value of 'name'.
+ *   - 'display_hook' The WordPress action that the widget_display() method is
+ *     hooked to.
+ *   - 'template_file' The template file that will be used to load the content
+ *     of your main extension tab. Defaults to 'groups/single/plugins.php'.
+ *   - 'screens' A multi-dimensional array, described below.
+ *   - 'access' Which users can visit the plugin's tab.
+ *   - 'show_tab' Which users can see the plugin's navigation tab.
+ *
+ * BP_Group_Extension uses the concept of "settings screens". There are three
+ * contexts for settings screens:
+ *   - 'create', which inserts a new step into the group creation process
+ *   - 'edit', which adds a tab for your extension into the Admin section of
+ *     a group
+ *   - 'admin', which adds a metabox to the Groups administration panel in the
+ *     WordPress Dashboard
+ * Each of these settings screens is populated by a pair of methods: one that
+ * creates the markup for the screen, and one that processes form data
+ * submitted from the screen. If your plugin needs screens in all three
+ * contexts, and if the markup and form processing logic will be the same in
+ * each case, you can define two methods to handle all of the screens:
+ *   function settings_screen() {}
+ *   function settings_screen_save() {}
+ * If one or more of your settings screen needs separate logic, you may define
+ * context-specific methods, for example:
+ *   function edit_screen() {}
+ *   function edit_screen_save() {}
+ * BP_Group_Extension will use the more specific methods if they are available.
+ *
+ * You can further customize the settings screens (tab names, etc) by passing
+ * an optional 'screens' parameter to the init array. The format is as follows:
+ *   'screens' => array(
+ *       'create' => array(
+ *       'slug' => 'foo',
+ *       'name' => 'Foo',
+ *       'position' => 55,
+ *       'screen_callback' => 'my_create_screen_callback',
+ *       'screen_save_callback' => 'my_create_screen_save_callback',
+ *   ),
+ *   'edit' => array( // ...
+ *   ),
+ * Only provide those arguments that you actually want to change from the
+ * default configuration. BP_Group_Extension will do the rest.
+ *
+ * Note that the 'edit' screen accepts an additional parameter: 'submit_text',
+ * which defines the text of the Submit button automatically added to the Edit
+ * screen of the extension (defaults to 'Save Changes'). Also, the 'admin'
+ * screen accepts two additional parameters: 'metabox_priority' and
+ * 'metabox_context'. See the docs for add_meta_box() for more details on these
+ * arguments.
+ *
+ * Prior to BuddyPress 1.7, group extension configurations were set slightly
+ * differently. The legacy method is still supported, though deprecated.
+ *
+ * @since 1.1.0
+ */
+class BP_Group_Extension {
+
+	/** Public ************************************************************/
+
+	/**
+	 * Information about this extension's screens.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	public $screens = array();
+
+	/**
+	 * The name of the extending class.
+	 *
+	 * @since 1.8.0
+	 * @var string
+	 */
+	public $class_name = '';
+
+	/**
+	 * A ReflectionClass object of the current extension.
+	 *
+	 * @since 1.8.0
+	 * @var ReflectionClass
+	 */
+	public $class_reflection = null;
+
+	/**
+	 * Parsed configuration parameters for the extension.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	public $params = array();
+
+	/**
+	 * Raw config params, as passed by the extending class.
+	 *
+	 * @since 2.1.0
+	 * @var array
+	 */
+	public $params_raw = array();
+
+	/**
+	 * The ID of the current group.
+	 *
+	 * @since 1.8.0
+	 * @var int
+	 */
+	public $group_id = 0;
+
+	/**
+	 * The slug of the current extension.
+	 *
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $slug = '';
+
+	/**
+	 * The translatable name of the current extension.
+	 *
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $name = '';
+
+	/**
+	 * The visibility of the extension tab. 'public' or 'private'.
+	 *
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $visibility = 'public';
+
+	/**
+	 * The numeric position of the main nav item.
+	 *
+	 * @since 1.1.0
+	 * @var int
+	 */
+	public $nav_item_position = 81;
+
+	/**
+	 * Whether to show the nav item.
+	 *
+	 * @since 1.1.0
+	 * @var bool
+	 */
+	public $enable_nav_item = true;
+
+	/**
+	 * Whether the current user should see the navigation item.
+	 *
+	 * @since 2.1.0
+	 * @var bool
+	 */
+	public $user_can_see_nav_item;
+
+	/**
+	 * Whether the current user can visit the tab.
+	 *
+	 * @since 2.1.0
+	 * @var bool
+	 */
+	public $user_can_visit;
+
+	/**
+	 * The text of the nav item. Defaults to self::name.
+	 *
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $nav_item_name = '';
+
+	/**
+	 * The WP action that self::widget_display() is attached to.
+	 *
+	 * Default: 'groups_custom_group_boxes'.
+	 *
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $display_hook = 'groups_custom_group_boxes';
+
+	/**
+	 * The template file used to load the plugin content.
+	 *
+	 * Default: 'groups/single/plugins'.
+	 *
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $template_file = 'groups/single/plugins';
+
+	/** Protected *********************************************************/
+
+	/**
+	 * Has the extension been initialized?
+	 *
+	 * @since 1.8.0
+	 * @var bool
+	 */
+	protected $initialized = false;
+
+	/**
+	 * Extension properties as set by legacy extensions.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	protected $legacy_properties = array();
+
+	/**
+	 * Converted legacy parameters.
+	 *
+	 * These are the extension properties as set by legacy extensions, but
+	 * then converted to match the new format for params.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	protected $legacy_properties_converted = array();
+
+	/**
+	 * Redirect location as defined by post-edit save callback.
+	 *
+	 * @since 2.1.0
+	 * @var string
+	 */
+	protected $post_save_redirect;
+
+	/**
+	 * Miscellaneous data as set by the __set() magic method.
+	 *
+	 * @since 1.8.0
+	 * @var array
+	 */
+	protected $data = array();
+
+	/** Screen Overrides **************************************************/
+
+	/*
+	 * Screen override methods are how your extension will display content
+	 * and handle form submits. Your extension should only override those
+	 * methods that it needs for its purposes.
+	 */
+
+	/**
+	 * The content of the group tab.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param int|null $group_id ID of the group to display.
+	 */
+	public function display( $group_id = null ) {}
+
+	/**
+	 * Content displayed in a widget sidebar, if applicable.
+	 *
+	 * @since 1.1.0
+	 */
+	public function widget_display() {}
+
+	/*
+	 * *_screen() displays the settings form for the given context
+	 * *_screen_save() processes data submitted via the settings form
+	 * The settings_* methods are generic fallbacks, which can optionally
+	 * be overridden by the more specific edit_*, create_*, and admin_*
+	 * versions.
+	 */
+	public function settings_screen( $group_id = null ) {}
+	public function settings_screen_save( $group_id = null ) {}
+	public function edit_screen( $group_id = null ) {}
+	public function edit_screen_save( $group_id = null ) {}
+	public function create_screen( $group_id = null ) {}
+	public function create_screen_save( $group_id = null ) {}
+	public function admin_screen( $group_id = null ) {}
+	public function admin_screen_save( $group_id = null ) {}
+
+	/** Setup *************************************************************/
+
+	/**
+	 * Initialize the extension, using your config settings.
+	 *
+	 * Your plugin should call this method at the very end of its
+	 * constructor, like so:
+	 *
+	 *   public function __construct() {
+	 *       $args = array(
+	 *           'slug' => 'my-group-extension',
+	 *           'name' => 'My Group Extension',
+	 *           // ...
+	 *       );
+	 *
+	 *       parent::init( $args );
+	 *   }
+	 *
+	 * @since 1.8.0
+	 * @since 2.1.0 Added 'access' and 'show_tab' arguments to `$args`.
+	 *
+	 * @param array $args {
+	 *     Array of initialization arguments.
+	 *     @type string       $slug              Unique, URL-safe identifier for your extension.
+	 *     @type string       $name              Translatable name for your extension. Used to populate
+	 *                                           navigation items.
+	 *     @type string       $visibility        Optional. Set to 'public' for your extension (the main tab as well
+	 *                                           as the widget) to be available to anyone who can access the group;
+	 *                                           set to 'private' otherwise. Default: 'public'.
+	 *     @type int          $nav_item_position Optional. Location of the nav item in the tab list.
+	 *                                           Default: 81.
+	 *     @type bool         $enable_nav_item   Optional. Whether the extension's tab should be accessible to
+	 *                                           anyone who can view the group. Default: true.
+	 *     @type string       $nav_item_name     Optional. The translatable text you want to appear in the nav tab.
+	 *                                           Default: the value of `$name`.
+	 *     @type string       $display_hook      Optional. The WordPress action that the widget_display() method is
+	 *                                           hooked to. Default: 'groups_custom_group_boxes'.
+	 *     @type string       $template_file     Optional. Theme-relative path to the template file BP should use
+	 *                                           to load the content of your main extension tab.
+	 *                                           Default: 'groups/single/plugins.php'.
+	 *     @type array        $screens           A multi-dimensional array of configuration information for the
+	 *                                           extension screens. See docblock of {@link BP_Group_Extension}
+	 *                                           for more details.
+	 *     @type string|array $access            Which users can visit the plugin's tab. Possible values: 'anyone',
+	 *                                           'loggedin', 'member', 'mod', 'admin' or 'noone'. ('member', 'mod',
+	 *                                           'admin' refer to user's role in group.) Note that 'mod' targets
+	 *                                           only group moderators. If you want to allow access to group moderators
+	 *                                           and admins, specify `array( 'mod', 'admin' )`. Defaults to 'anyone'
+	 *                                           for public groups and 'member' for private groups.
+	 *     @type string|array $show_tab          Which users can see the plugin's navigation tab. Possible values:
+	 *                                           'anyone', 'loggedin', 'member', 'mod', 'admin' or 'noone'.
+	 *                                           ('member', 'mod', 'admin' refer to user's role in group.) Note
+	 *                                           that 'mod' targets only group moderators. If you want to show the
+	 *                                           tab to group moderators and admins, specify
+	 *                                           `array( 'mod', 'admin' )`. Defaults to 'anyone' for public groups
+	 *                                           and 'member' for private groups.
+	 * }
+	 */
+	public function init( $args = array() ) {
+		// Store the raw arguments.
+		$this->params_raw = $args;
+
+		// Before this init() method was introduced, plugins were
+		// encouraged to set their config directly. For backward
+		// compatibility with these plugins, we detect whether this is
+		// one of those legacy plugins, and parse any legacy arguments
+		// with those passed to init().
+		$this->parse_legacy_properties();
+		$args = $this->parse_args_r( $args, $this->legacy_properties_converted );
+
+		// Parse with defaults.
+		$this->params = $this->parse_args_r( $args, array(
+			'slug'              => $this->slug,
+			'name'              => $this->name,
+			'visibility'        => $this->visibility,
+			'nav_item_position' => $this->nav_item_position,
+			'enable_nav_item'   => (bool) $this->enable_nav_item,
+			'nav_item_name'     => $this->nav_item_name,
+			'display_hook'      => $this->display_hook,
+			'template_file'     => $this->template_file,
+			'screens'           => $this->get_default_screens(),
+			'access'            => null,
+			'show_tab'          => null,
+		) );
+
+		$this->initialized = true;
+	}
+
+	/**
+	 * The main setup routine for the extension.
+	 *
+	 * This method contains the primary logic for setting up an extension's
+	 * configuration, setting up backward compatibility for legacy plugins,
+	 * and hooking the extension's screen functions into WP and BP.
+	 *
+	 * Marked 'public' because it must be accessible to add_action().
+	 * However, you should never need to invoke this method yourself - it
+	 * is called automatically at the right point in the load order by
+	 * bp_register_group_extension().
+	 *
+	 * @since 1.1.0
+	 */
+	public function _register() {
+
+		// Detect and parse properties set by legacy extensions.
+		$this->parse_legacy_properties();
+
+		// Initialize, if necessary. This should only happen for
+		// legacy extensions that don't call parent::init() themselves.
+		if ( true !== $this->initialized ) {
+			$this->init();
+		}
+
+		// Set some config values, based on the parsed params.
+		$this->group_id          = $this->get_group_id();
+		$this->slug              = $this->params['slug'];
+		$this->name              = $this->params['name'];
+		$this->visibility        = $this->params['visibility'];
+		$this->nav_item_position = $this->params['nav_item_position'];
+		$this->nav_item_name     = $this->params['nav_item_name'];
+		$this->display_hook      = $this->params['display_hook'];
+		$this->template_file     = $this->params['template_file'];
+
+		// Configure 'screens': create, admin, and edit contexts.
+		$this->setup_screens();
+
+		// Configure access-related settings.
+		$this->setup_access_settings();
+
+		// Mirror configuration data so it's accessible to plugins
+		// that look for it in its old locations.
+		$this->setup_legacy_properties();
+
+		// Hook the extension into BuddyPress.
+		$this->setup_display_hooks();
+		$this->setup_create_hooks();
+		$this->setup_edit_hooks();
+		$this->setup_admin_hooks();
+	}
+
+	/**
+	 * Set up some basic info about the Extension.
+	 *
+	 * Here we collect the name of the extending class, as well as a
+	 * ReflectionClass that is used in get_screen_callback() to determine
+	 * whether your extension overrides certain callback methods.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_class_info() {
+		if ( empty( $this->class_name ) ) {
+			$this->class_name = get_class( $this );
+		}
+
+		if ( is_null( $this->class_reflection ) ) {
+			$this->class_reflection = new ReflectionClass( $this->class_name );
+		}
+	}
+
+	/**
+	 * Get the current group ID.
+	 *
+	 * Check for:
+	 *   - current group
+	 *   - new group
+	 *   - group admin
+	 *
+	 * @since 1.8.0
+	 *
+	 * @return int
+	 */
+	public static function get_group_id() {
+
+		// Usually this will work.
+		$group_id = bp_get_current_group_id();
+
+		// On the admin, get the group id out of the $_GET params.
+		if ( empty( $group_id ) && is_admin() && ( isset( $_GET['page'] ) && ( 'bp-groups' === $_GET['page'] ) ) && ! empty( $_GET['gid'] ) ) {
+			$group_id = (int) $_GET['gid'];
+		}
+
+		// This fallback will only be hit when the create step is very
+		// early.
+		if ( empty( $group_id ) && bp_get_new_group_id() ) {
+			$group_id = bp_get_new_group_id();
+		}
+
+		// On some setups, the group id has to be fetched out of the
+		// $_POST array
+		// @todo Figure out why this is happening during group creation.
+		if ( empty( $group_id ) && isset( $_POST['group_id'] ) ) {
+			$group_id = (int) $_POST['group_id'];
+		}
+
+		return $group_id;
+	}
+
+	/**
+	 * Gather configuration data about your screens.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @return array
+	 */
+	protected function get_default_screens() {
+		$this->setup_class_info();
+
+		$screens = array(
+			'create' => array(
+				'position' => 81,
+			),
+			'edit'   => array(
+				'submit_text' => __( 'Save Changes', 'buddypress' ),
+			),
+			'admin'  => array(
+				'metabox_context'  => 'normal',
+				'metabox_priority' => 'core',
+			),
+		);
+
+		foreach ( $screens as $context => &$screen ) {
+			$screen['enabled']     = true;
+			$screen['name']        = $this->name;
+			$screen['slug']        = $this->slug;
+
+			$screen['screen_callback']      = $this->get_screen_callback( $context, 'screen'      );
+			$screen['screen_save_callback'] = $this->get_screen_callback( $context, 'screen_save' );
+		}
+
+		return $screens;
+	}
+
+	/**
+	 * Set up screens array based on params.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_screens() {
+		foreach ( (array) $this->params['screens'] as $context => $screen ) {
+			if ( empty( $screen['slug'] ) ) {
+				$screen['slug'] = $this->slug;
+			}
+
+			if ( empty( $screen['name'] ) ) {
+				$screen['name'] = $this->name;
+			}
+
+			$this->screens[ $context ] = $screen;
+		}
+	}
+
+	/**
+	 * Set up access-related settings for this extension.
+	 *
+	 * @since 2.1.0
+	 */
+	protected function setup_access_settings() {
+
+		// Bail if no group ID is available.
+		if ( empty( $this->group_id ) ) {
+			return;
+		}
+
+		// Backward compatibility.
+		if ( isset( $this->params['enable_nav_item'] ) ) {
+			$this->enable_nav_item = (bool) $this->params['enable_nav_item'];
+		}
+
+		// Tab Access.
+		$this->user_can_visit = false;
+
+		// Backward compatibility for components that do not provide
+		// explicit 'access' parameter.
+		if ( empty( $this->params['access'] ) ) {
+			if ( false === $this->params['enable_nav_item'] ) {
+				$this->params['access'] = 'noone';
+			} else {
+				$group = groups_get_group( $this->group_id );
+
+				if ( ! empty( $group->status ) && 'public' === $group->status ) {
+					// Tabs in public groups are accessible to anyone by default.
+					$this->params['access'] = 'anyone';
+				} else {
+					// All other groups have members-only as the default.
+					$this->params['access'] = 'member';
+				}
+			}
+		}
+
+		// Parse multiple access conditions into an array.
+		$access_conditions = $this->params['access'];
+		if ( ! is_array( $access_conditions ) ) {
+			$access_conditions = explode( ',', $access_conditions );
+		}
+
+		// If the current user meets at least one condition, the
+		// get access.
+		foreach ( $access_conditions as $access_condition ) {
+			if ( $this->user_meets_access_condition( $access_condition ) ) {
+				$this->user_can_visit = true;
+				break;
+			}
+		}
+
+		// Tab Visibility.
+		$this->user_can_see_nav_item = false;
+
+		// Backward compatibility for components that do not provide
+		// explicit 'show_tab' parameter.
+		if ( empty( $this->params['show_tab'] ) ) {
+			if ( false === $this->params['enable_nav_item'] ) {
+				// The enable_nav_item index is only false if it's been
+				// defined explicitly as such in the
+				// constructor. So we always trust this value.
+				$this->params['show_tab'] = 'noone';
+
+			} elseif ( isset( $this->params_raw['enable_nav_item'] ) || isset( $this->params_raw['visibility'] ) ) {
+				// If enable_nav_item or visibility is passed,
+				// we assume this  is a legacy extension.
+				// Legacy behavior is that enable_nav_item=true +
+				// visibility=private implies members-only.
+				if ( 'public' !== $this->visibility ) {
+					$this->params['show_tab'] = 'member';
+				} else {
+					$this->params['show_tab'] = 'anyone';
+				}
+
+			} else {
+				// No show_tab or enable_nav_item value is
+				// available, so match the value of 'access'.
+				$this->params['show_tab'] = $this->params['access'];
+			}
+		}
+
+		// Parse multiple access conditions into an array.
+		$access_conditions = $this->params['show_tab'];
+		if ( ! is_array( $access_conditions ) ) {
+			$access_conditions = explode( ',', $access_conditions );
+		}
+
+		// If the current user meets at least one condition, the
+		// get access.
+		foreach ( $access_conditions as $access_condition ) {
+			if ( $this->user_meets_access_condition( $access_condition ) ) {
+				$this->user_can_see_nav_item = true;
+				break;
+			}
+		}
+	}
+
+	/**
+	 * Check whether the current user meets an access condition.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param string $access_condition 'anyone', 'loggedin', 'member',
+	 *                                 'mod', 'admin' or 'noone'.
+	 * @return bool
+	 */
+	protected function user_meets_access_condition( $access_condition ) {
+
+		switch ( $access_condition ) {
+			case 'admin' :
+				$meets_condition = groups_is_user_admin( bp_loggedin_user_id(), $this->group_id );
+				break;
+
+			case 'mod' :
+				$meets_condition = groups_is_user_mod( bp_loggedin_user_id(), $this->group_id );
+				break;
+
+			case 'member' :
+				$meets_condition = groups_is_user_member( bp_loggedin_user_id(), $this->group_id );
+				break;
+
+			case 'loggedin' :
+				$meets_condition = is_user_logged_in();
+				break;
+
+			case 'noone' :
+				$meets_condition = false;
+				break;
+
+			case 'anyone' :
+			default :
+				$meets_condition = true;
+				break;
+		}
+
+		return $meets_condition;
+	}
+
+	/** Display ***********************************************************/
+
+	/**
+	 * Hook this extension's group tab into BuddyPress, if necessary.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_display_hooks() {
+
+		// Bail if not a group.
+		if ( ! bp_is_group() ) {
+			return;
+		}
+
+		// Backward compatibility only.
+		if ( ( 'public' !== $this->visibility ) && ! buddypress()->groups->current_group->user_has_access ) {
+			return;
+		}
+
+		// If the user can see the nav item, we create it.
+		$user_can_see_nav_item = $this->user_can_see_nav_item();
+
+		if ( $user_can_see_nav_item ) {
+			$group_permalink = bp_get_group_permalink( groups_get_current_group() );
+
+			bp_core_create_subnav_link( array(
+				'name'            => ! $this->nav_item_name ? $this->name : $this->nav_item_name,
+				'slug'            => $this->slug,
+				'parent_slug'     => bp_get_current_group_slug(),
+				'parent_url'      => $group_permalink,
+				'position'        => $this->nav_item_position,
+				'item_css_id'     => 'nav-' . $this->slug,
+				'screen_function' => array( &$this, '_display_hook' ),
+				'user_has_access' => $user_can_see_nav_item,
+				'no_access_url'   => $group_permalink,
+			), 'groups' );
+		}
+
+		// If the user can visit the screen, we register it.
+		$user_can_visit = $this->user_can_visit();
+
+		if ( $user_can_visit ) {
+			$group_permalink = bp_get_group_permalink( groups_get_current_group() );
+
+			bp_core_register_subnav_screen_function( array(
+				'slug'            => $this->slug,
+				'parent_slug'     => bp_get_current_group_slug(),
+				'screen_function' => array( &$this, '_display_hook' ),
+				'user_has_access' => $user_can_visit,
+				'no_access_url'   => $group_permalink,
+			), 'groups' );
+
+			// When we are viewing the extension display page, set the title and options title.
+			if ( bp_is_current_action( $this->slug ) ) {
+				add_filter( 'bp_group_user_has_access',   array( $this, 'group_access_protection' ), 10, 2 );
+
+				$extension_name = $this->name;
+				add_action( 'bp_template_content_header', function() use ( $extension_name ) {
+					echo esc_attr( $extension_name );
+				} );
+				add_action( 'bp_template_title', function() use ( $extension_name ) {
+					echo esc_attr( $extension_name );
+				} );
+			}
+		}
+
+		// Hook the group home widget.
+		if ( bp_is_group_home() ) {
+			add_action( $this->display_hook, array( &$this, 'widget_display' ) );
+		}
+	}
+
+	/**
+	 * Hook the main display method, and loads the template file.
+	 *
+	 * @since 1.1.0
+	 */
+	public function _display_hook() {
+		add_action( 'bp_template_content', array( &$this, 'call_display' ) );
+
+		/**
+		 * Filters the template to load for the main display method.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $template_file Path to the template to load.
+		 */
+		bp_core_load_template( apply_filters( 'bp_core_template_plugin', $this->template_file ) );
+	}
+
+	/**
+	 * Call the display() method.
+	 *
+	 * We use this wrapper so that we can pass the group_id to the
+	 * display() callback.
+	 *
+	 * @since 2.1.1
+	 */
+	public function call_display() {
+		$this->display( $this->group_id );
+	}
+
+	/**
+	 * Determine whether the current user should see this nav tab.
+	 *
+	 * Note that this controls only the display of the navigation item.
+	 * Access to the tab is controlled by the user_can_visit() check.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $user_can_see_nav_item Whether or not the user can see the nav item.
+	 * @return bool
+	 */
+	public function user_can_see_nav_item( $user_can_see_nav_item = false ) {
+
+		// Always allow moderators to see nav items, even if explicitly 'noone'
+		if ( ( 'noone' !== $this->params['show_tab'] ) && bp_current_user_can( 'bp_moderate' ) ) {
+			return true;
+		}
+
+		return $this->user_can_see_nav_item;
+	}
+
+	/**
+	 * Determine whether the current user has access to visit this tab.
+	 *
+	 * Note that this controls the ability of a user to access a tab.
+	 * Display of the navigation item is controlled by user_can_see_nav_item().
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $user_can_visit Whether or not the user can visit the tab.
+	 * @return bool
+	 */
+	public function user_can_visit( $user_can_visit = false ) {
+
+		// Always allow moderators to visit a tab, even if explicitly 'noone'
+		if ( ( 'noone' !== $this->params['access'] ) && bp_current_user_can( 'bp_moderate' ) ) {
+			return true;
+		}
+
+		return $this->user_can_visit;
+	}
+
+	/**
+	 * Filter the access check in bp_groups_group_access_protection() for this extension.
+	 *
+	 * Note that $no_access_args is passed by reference, as there are some
+	 * circumstances where the bp_core_no_access() arguments need to be
+	 * modified before the redirect takes place.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool  $user_can_visit Whether or not the user can visit the tab.
+	 * @param array $no_access_args Array of args to help determine access.
+	 * @return bool
+	 */
+	public function group_access_protection( $user_can_visit, &$no_access_args ) {
+		$user_can_visit = $this->user_can_visit();
+
+		if ( ! $user_can_visit && is_user_logged_in() ) {
+			$current_group = groups_get_group( $this->group_id );
+
+			$no_access_args['message'] = __( 'You do not have access to this content.', 'buddypress' );
+			$no_access_args['root'] = bp_get_group_permalink( $current_group ) . 'home/';
+			$no_access_args['redirect'] = false;
+		}
+
+		return $user_can_visit;
+	}
+
+
+	/** Create ************************************************************/
+
+	/**
+	 * Hook this extension's Create step into BuddyPress, if necessary.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_create_hooks() {
+		if ( ! $this->is_screen_enabled( 'create' ) ) {
+			return;
+		}
+
+		$screen = $this->screens['create'];
+
+		// Insert the group creation step for the new group extension.
+		buddypress()->groups->group_creation_steps[ $screen['slug'] ] = array(
+			'name'     => $screen['name'],
+			'slug'     => $screen['slug'],
+			'position' => $screen['position'],
+		);
+
+		// The maybe_ methods check to see whether the create_*
+		// callbacks should be invoked (ie, are we on the
+		// correct group creation step). Hooked in separate
+		// methods because current creation step info not yet
+		// available at this point.
+		add_action( 'groups_custom_create_steps', array( $this, 'maybe_create_screen' ) );
+		add_action( 'groups_create_group_step_save_' . $screen['slug'], array( $this, 'maybe_create_screen_save' ) );
+	}
+
+	/**
+	 * Call the create_screen() method, if we're on the right page.
+	 *
+	 * @since 1.8.0
+	 */
+	public function maybe_create_screen() {
+		if ( ! bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
+			return;
+		}
+
+		call_user_func( $this->screens['create']['screen_callback'], $this->group_id );
+		$this->nonce_field( 'create' );
+
+		// The create screen requires an additional nonce field
+		// due to a quirk in the way the templates are built.
+		wp_nonce_field( 'groups_create_save_' . bp_get_groups_current_create_step(), '_wpnonce', false );
+	}
+
+	/**
+	 * Call the create_screen_save() method, if we're on the right page.
+	 *
+	 * @since 1.8.0
+	 */
+	public function maybe_create_screen_save() {
+		if ( ! bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
+			return;
+		}
+
+		$this->check_nonce( 'create' );
+		call_user_func( $this->screens['create']['screen_save_callback'], $this->group_id );
+	}
+
+	/** Edit **************************************************************/
+
+	/**
+	 * Hook this extension's Edit panel into BuddyPress, if necessary.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_edit_hooks() {
+		// Bail if not in a group.
+		if ( ! bp_is_group() ) {
+			return;
+		}
+
+		// Bail if not an edit screen.
+		if ( ! $this->is_screen_enabled( 'edit' ) || ! bp_is_item_admin() ) {
+			return;
+		}
+
+		$screen = $this->screens['edit'];
+
+		$position = isset( $screen['position'] ) ? (int) $screen['position'] : 10;
+		$position += 40;
+
+		$current_group = groups_get_current_group();
+		$admin_link    = trailingslashit( bp_get_group_permalink( $current_group ) . 'admin' );
+
+		$subnav_args = array(
+			'name'            => $screen['name'],
+			'slug'            => $screen['slug'],
+			'parent_slug'     => $current_group->slug . '_manage',
+			'parent_url'      => $admin_link,
+			'user_has_access' => bp_is_item_admin(),
+			'position'        => $position,
+			'screen_function' => 'groups_screen_group_admin',
+		);
+
+		// Should we add a menu to the Group's WP Admin Bar.
+		if ( ! empty( $screen['show_in_admin_bar'] ) ) {
+			$subnav_args['show_in_admin_bar'] = true;
+		}
+
+		// Add the tab to the manage navigation.
+		bp_core_new_subnav_item( $subnav_args, 'groups' );
+
+		// Catch the edit screen and forward it to the plugin template.
+		if ( bp_is_groups_component() && bp_is_current_action( 'admin' ) && bp_is_action_variable( $screen['slug'], 0 ) ) {
+			$this->call_edit_screen_save( $this->group_id );
+
+			add_action( 'groups_custom_edit_steps', array( &$this, 'call_edit_screen' ) );
+
+			// Determine the proper template and save for later
+			// loading.
+			if ( '' !== bp_locate_template( array( 'groups/single/home.php' ), false ) ) {
+				$this->edit_screen_template = '/groups/single/home';
+			} else {
+				add_action( 'bp_template_content_header', function() {
+					echo '<ul class="content-header-nav">';
+					bp_group_admin_tabs();
+					echo '</ul>';
+				} );
+				add_action( 'bp_template_content', array( &$this, 'call_edit_screen' ) );
+				$this->edit_screen_template = '/groups/single/plugins';
+			}
+
+			// We load the template at bp_screens, to give all
+			// extensions a chance to load.
+			add_action( 'bp_screens', array( $this, 'call_edit_screen_template_loader' ) );
+		}
+	}
+
+	/**
+	 * Call the edit_screen() method.
+	 *
+	 * Previous versions of BP_Group_Extension required plugins to provide
+	 * their own Submit button and nonce fields when building markup. In
+	 * BP 1.8, this requirement was lifted - BP_Group_Extension now handles
+	 * all required submit buttons and nonces.
+	 *
+	 * We put the edit screen markup into an output buffer before echoing.
+	 * This is so that we can check for the presence of a hardcoded submit
+	 * button, as would be present in legacy plugins; if one is found, we
+	 * do not auto-add our own button.
+	 *
+	 * @since 1.8.0
+	 */
+	public function call_edit_screen() {
+		ob_start();
+		call_user_func( $this->screens['edit']['screen_callback'], $this->group_id );
+		$screen = ob_get_contents();
+		ob_end_clean();
+
+		echo $this->maybe_add_submit_button( $screen );
+
+		$this->nonce_field( 'edit' );
+	}
+
+	/**
+	 * Check the nonce, and call the edit_screen_save() method.
+	 *
+	 * @since 1.8.0
+	 */
+	public function call_edit_screen_save() {
+		if ( empty( $_POST ) ) {
+			return;
+		}
+
+		// When DOING_AJAX, the POST global will be populated, but we
+		// should assume it's a save.
+		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+			return;
+		}
+
+		$this->check_nonce( 'edit' );
+
+		// Detect whether the screen_save_callback is performing a
+		// redirect, so that we don't do one of our own.
+		add_filter( 'wp_redirect', array( $this, 'detect_post_save_redirect' ) );
+
+		// Call the extension's save routine.
+		call_user_func( $this->screens['edit']['screen_save_callback'], $this->group_id );
+
+		// Clean up detection filters.
+		remove_filter( 'wp_redirect', array( $this, 'detect_post_save_redirect' ) );
+
+		// Perform a redirect only if one has not already taken place.
+		if ( empty( $this->post_save_redirect ) ) {
+
+			/**
+			 * Filters the URL to redirect to after group edit screen save.
+			 *
+			 * Only runs if a redirect has not already occurred.
+			 *
+			 * @since 2.1.0
+			 *
+			 * @param string $value URL to redirect to.
+			 */
+			$redirect_to = apply_filters( 'bp_group_extension_edit_screen_save_redirect', bp_get_requested_url( ) );
+
+			bp_core_redirect( $redirect_to );
+			die();
+		}
+	}
+
+	/**
+	 * Load the template that houses the Edit screen.
+	 *
+	 * Separated out into a callback so that it can run after all other
+	 * Group Extensions have had a chance to register their navigation, to
+	 * avoid missing tabs.
+	 *
+	 * Hooked to 'bp_screens'.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @see BP_Group_Extension::setup_edit_hooks()
+	 */
+	public function call_edit_screen_template_loader() {
+		bp_core_load_template( $this->edit_screen_template );
+	}
+
+	/**
+	 * Add a submit button to the edit form, if it needs one.
+	 *
+	 * There's an inconsistency in the way that the group Edit and Create
+	 * screens are rendered: the Create screen has a submit button built
+	 * in, but the Edit screen does not. This function allows plugin
+	 * authors to write markup that does not contain the submit button for
+	 * use on both the Create and Edit screens - BP will provide the button
+	 * if one is not found.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $screen The screen markup, captured in the output
+	 *                       buffer.
+	 * @return string $screen The same markup, with a submit button added.
+	 */
+	protected function maybe_add_submit_button( $screen = '' ) {
+		if ( $this->has_submit_button( $screen ) ) {
+			return $screen;
+		}
+
+		return $screen . sprintf(
+			'<div id="%s"><input type="submit" name="save" value="%s" id="%s"></div>',
+			'bp-group-edit-' . $this->slug . '-submit-wrapper',
+			$this->screens['edit']['submit_text'],
+			'bp-group-edit-' . $this->slug . '-submit'
+		);
+	}
+
+	/**
+	 * Does the given markup have a submit button?
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $screen The markup to check.
+	 * @return bool True if a Submit button is found, otherwise false.
+	 */
+	public static function has_submit_button( $screen = '' ) {
+		$pattern = "/<input[^>]+type=[\'\"]submit[\'\"]/";
+		preg_match( $pattern, $screen, $matches );
+		return ! empty( $matches[0] );
+	}
+
+	/**
+	 * Detect redirects hardcoded into edit_screen_save() callbacks.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param string $redirect Redirect string.
+	 * @return string
+	 */
+	public function detect_post_save_redirect( $redirect = '' ) {
+		if ( ! empty( $redirect ) ) {
+			$this->post_save_redirect = $redirect;
+		}
+
+		return $redirect;
+	}
+
+	/** Admin *************************************************************/
+
+	/**
+	 * Hook this extension's Admin metabox into BuddyPress, if necessary.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function setup_admin_hooks() {
+		if ( ! $this->is_screen_enabled( 'admin' ) || ! is_admin() ) {
+			return;
+		}
+
+		// Hook the admin screen markup function to the content hook.
+		add_action( 'bp_groups_admin_meta_box_content_' . $this->slug, array( $this, 'call_admin_screen' ) );
+
+		// Initialize the metabox.
+		add_action( 'bp_groups_admin_meta_boxes', array( $this, '_meta_box_display_callback' ) );
+
+		// Catch the metabox save.
+		add_action( 'bp_group_admin_edit_after', array( $this, 'call_admin_screen_save' ), 10 );
+	}
+
+	/**
+	 * Call the admin_screen() method, and add a nonce field.
+	 *
+	 * @since 1.8.0
+	 */
+	public function call_admin_screen() {
+		call_user_func( $this->screens['admin']['screen_callback'], $this->group_id );
+		$this->nonce_field( 'admin' );
+	}
+
+	/**
+	 * Check the nonce, and call the admin_screen_save() method.
+	 *
+	 * @since 1.8.0
+	 */
+	public function call_admin_screen_save() {
+		$this->check_nonce( 'admin' );
+		call_user_func( $this->screens['admin']['screen_save_callback'], $this->group_id );
+	}
+
+	/**
+	 * Create the Dashboard meta box for this extension.
+	 *
+	 * @since 1.7.0
+	 */
+	public function _meta_box_display_callback() {
+		$group_id = isset( $_GET['gid'] ) ? (int) $_GET['gid'] : 0;
+		$screen   = $this->screens['admin'];
+
+		$extension_slug = $this->slug;
+		$callback = function() use ( $extension_slug, $group_id ) {
+			do_action( 'bp_groups_admin_meta_box_content_' . $extension_slug, $group_id );
+		};
+
+		add_meta_box(
+			$screen['slug'],
+			$screen['name'],
+			$callback,
+			get_current_screen()->id,
+			$screen['metabox_context'],
+			$screen['metabox_priority']
+		);
+	}
+
+
+	/** Utilities *********************************************************/
+
+	/**
+	 * Generate the nonce fields for a settings form.
+	 *
+	 * The nonce field name (the second param passed to wp_nonce_field)
+	 * contains this extension's slug and is thus unique to this extension.
+	 * This is necessary because in some cases (namely, the Dashboard),
+	 * more than one extension may generate nonces on the same page, and we
+	 * must avoid name clashes.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
+	 */
+	public function nonce_field( $context = '' ) {
+		wp_nonce_field( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
+	}
+
+	/**
+	 * Check the nonce on a submitted settings form.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
+	 */
+	public function check_nonce( $context = '' ) {
+		check_admin_referer( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
+	}
+
+	/**
+	 * Is the specified screen enabled?
+	 *
+	 * To be enabled, a screen must both have the 'enabled' key set to true
+	 * (legacy: $this->enable_create_step, etc), and its screen_callback
+	 * must also exist and be callable.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
+	 * @return bool True if the screen is enabled, otherwise false.
+	 */
+	public function is_screen_enabled( $context = '' ) {
+		$enabled = false;
+
+		if ( isset( $this->screens[ $context ] ) ) {
+			$enabled = $this->screens[ $context ]['enabled'] && is_callable( $this->screens[ $context ]['screen_callback'] );
+		}
+
+		return (bool) $enabled;
+	}
+
+	/**
+	 * Get the appropriate screen callback for the specified context/type.
+	 *
+	 * BP Group Extensions have three special "screen contexts": create,
+	 * admin, and edit. Each of these contexts has a corresponding
+	 * _screen() and _screen_save() method, which allow group extension
+	 * plugins to define different markup and logic for each context.
+	 *
+	 * BP also supports fallback settings_screen() and
+	 * settings_screen_save() methods, which can be used to define markup
+	 * and logic that is shared between context. For each context, you may
+	 * either provide context-specific methods, or you can let BP fall back
+	 * on the shared settings_* callbacks.
+	 *
+	 * For example, consider a BP_Group_Extension implementation that looks
+	 * like this:
+	 *
+	 *   // ...
+	 *   function create_screen( $group_id ) { ... }
+	 *   function create_screen_save( $group_id ) { ... }
+	 *   function settings_screen( $group_id ) { ... }
+	 *   function settings_screen_save( $group_id ) { ... }
+	 *   // ...
+	 *
+	 * BP_Group_Extension will use your create_* methods for the Create
+	 * steps, and will use your generic settings_* methods for the Edit
+	 * and Admin contexts. This schema allows plugin authors maximum
+	 * flexibility without having to repeat themselves.
+	 *
+	 * The get_screen_callback() method uses a ReflectionClass object to
+	 * determine whether your extension has provided a given callback.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
+	 * @param string $type    Screen type. 'screen' or 'screen_save'. Default:
+	 *                        'screen'.
+	 * @return callable A callable function handle.
+	 */
+	public function get_screen_callback( $context = '', $type = 'screen' ) {
+		$callback = '';
+
+		// Try the context-specific callback first.
+		$method  = $context . '_' . $type;
+		$rmethod = $this->class_reflection->getMethod( $method );
+		if ( isset( $rmethod->class ) && $this->class_name === $rmethod->class ) {
+			$callback = array( $this, $method );
+		}
+
+		if ( empty( $callback ) ) {
+			$fallback_method  = 'settings_' . $type;
+			$rfallback_method = $this->class_reflection->getMethod( $fallback_method );
+			if ( isset( $rfallback_method->class ) && $this->class_name === $rfallback_method->class ) {
+				$callback = array( $this, $fallback_method );
+			}
+		}
+
+		return $callback;
+	}
+
+	/**
+	 * Recursive argument parsing.
+	 *
+	 * This acts like a multi-dimensional version of wp_parse_args() (minus
+	 * the querystring parsing - you must pass arrays).
+	 *
+	 * Values from $a override those from $b; keys in $b that don't exist
+	 * in $a are passed through.
+	 *
+	 * This is different from array_merge_recursive(), both because of the
+	 * order of preference ($a overrides $b) and because of the fact that
+	 * array_merge_recursive() combines arrays deep in the tree, rather
+	 * than overwriting the b array with the a array.
+	 *
+	 * The implementation of this function is specific to the needs of
+	 * BP_Group_Extension, where we know that arrays will always be
+	 * associative, and that an argument under a given key in one array
+	 * will be matched by a value of identical depth in the other one. The
+	 * function is NOT designed for general use, and will probably result
+	 * in unexpected results when used with data in the wild. See, eg,
+	 * https://core.trac.wordpress.org/ticket/19888
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $a First set of arguments.
+	 * @param array $b Second set of arguments.
+	 * @return array Parsed arguments.
+	 */
+	public static function parse_args_r( &$a, $b ) {
+		$a = (array) $a;
+		$b = (array) $b;
+		$r = $b;
+
+		foreach ( $a as $k => &$v ) {
+			if ( is_array( $v ) && isset( $r[ $k ] ) ) {
+				$r[ $k ] = self::parse_args_r( $v, $r[ $k ] );
+			} else {
+				$r[ $k ] = $v;
+			}
+		}
+
+		return $r;
+	}
+
+	/** Legacy Support ********************************************************/
+
+	/*
+	 * In BuddyPress 1.8, the recommended technique for configuring
+	 * extensions changed from directly setting various object properties
+	 * in the class constructor, to passing a configuration array to
+	 * parent::init(). The following methods ensure that extensions created
+	 * in the old way continue to work, by converting legacy configuration
+	 * data to the new format.
+	 */
+
+	/**
+	 * Provide access to otherwise unavailable object properties.
+	 *
+	 * This magic method is here for backward compatibility with plugins
+	 * that refer to config properties that have moved to a different
+	 * location (such as enable_create_step, which is now at
+	 * $this->screens['create']['enabled']
+	 *
+	 * The legacy_properties array is set up in
+	 * self::setup_legacy_properties().
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $key Property name.
+	 * @return mixed The value if found, otherwise null.
+	 */
+	public function __get( $key ) {
+		if ( isset( $this->legacy_properties[ $key ] ) ) {
+			return $this->legacy_properties[ $key ];
+		} elseif ( isset( $this->data[ $key ] ) ) {
+			return $this->data[ $key ];
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Provide a fallback for isset( $this->foo ) when foo is unavailable.
+	 *
+	 * This magic method is here for backward compatibility with plugins
+	 * that have set their class config options directly in the class
+	 * constructor. The parse_legacy_properties() method of the current
+	 * class needs to check whether any legacy keys have been put into the
+	 * $this->data array.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $key Property name.
+	 * @return bool True if the value is set, otherwise false.
+	 */
+	public function __isset( $key ) {
+		if ( isset( $this->legacy_properties[ $key ] ) ) {
+			return true;
+		} elseif ( isset( $this->data[ $key ] ) ) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	/**
+	 * Allow plugins to set otherwise unavailable object properties.
+	 *
+	 * This magic method is here for backward compatibility with plugins
+	 * that may attempt to modify the group extension by manually assigning
+	 * a value to an object property that no longer exists, such as
+	 * $this->enable_create_step.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $key Property name.
+	 * @param mixed  $value Property value.
+	 */
+	public function __set( $key, $value ) {
+
+		if ( empty( $this->initialized ) ) {
+			$this->data[ $key ] = $value;
+		}
+
+		switch ( $key ) {
+			case 'enable_create_step' :
+				$this->screens['create']['enabled'] = $value;
+				break;
+
+			case 'enable_edit_item' :
+				$this->screens['edit']['enabled'] = $value;
+				break;
+
+			case 'enable_admin_item' :
+				$this->screens['admin']['enabled'] = $value;
+				break;
+
+			case 'create_step_position' :
+				$this->screens['create']['position'] = $value;
+				break;
+
+			// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'.
+			case 'admin_name' :
+				$this->screens['edit']['name'] = $value;
+				break;
+
+			case 'admin_slug' :
+				$this->screens['edit']['slug'] = $value;
+				break;
+
+			case 'create_name' :
+				$this->screens['create']['name'] = $value;
+				break;
+
+			case 'create_slug' :
+				$this->screens['create']['slug'] = $value;
+				break;
+
+			case 'admin_metabox_context' :
+				$this->screens['admin']['metabox_context'] = $value;
+				break;
+
+			case 'admin_metabox_priority' :
+				$this->screens['admin']['metabox_priority'] = $value;
+				break;
+
+			default :
+				$this->data[ $key ] = $value;
+				break;
+		}
+	}
+
+	/**
+	 * Return a list of legacy properties.
+	 *
+	 * The legacy implementation of BP_Group_Extension used all of these
+	 * object properties for configuration. Some have been moved.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @return array List of legacy property keys.
+	 */
+	protected function get_legacy_property_list() {
+		return array(
+			'name',
+			'slug',
+			'admin_name',
+			'admin_slug',
+			'create_name',
+			'create_slug',
+			'visibility',
+			'create_step_position',
+			'nav_item_position',
+			'admin_metabox_context',
+			'admin_metabox_priority',
+			'enable_create_step',
+			'enable_nav_item',
+			'enable_edit_item',
+			'enable_admin_item',
+			'nav_item_name',
+			'display_hook',
+			'template_file',
+		);
+	}
+
+	/**
+	 * Parse legacy properties.
+	 *
+	 * The old standard for BP_Group_Extension was for plugins to register
+	 * their settings as properties in their constructor. The new method is
+	 * to pass a config array to the init() method. In order to support
+	 * legacy plugins, we slurp up legacy properties, and later on we'll
+	 * parse them into the new init() array.
+	 *
+	 * @since 1.8.0
+	 */
+	protected function parse_legacy_properties() {
+
+		// Only run this one time.
+		if ( ! empty( $this->legacy_properties_converted ) ) {
+			return;
+		}
+
+		$properties = $this->get_legacy_property_list();
+
+		// By-reference variable for convenience.
+		$lpc =& $this->legacy_properties_converted;
+
+		foreach ( $properties as $property ) {
+
+			// No legacy config exists for this key.
+			if ( ! isset( $this->{$property} ) ) {
+				continue;
+			}
+
+			// Grab the value and record it as appropriate.
+			$value = $this->{$property};
+
+			switch ( $property ) {
+				case 'enable_create_step' :
+					$lpc['screens']['create']['enabled'] = (bool) $value;
+					break;
+
+				case 'enable_edit_item' :
+					$lpc['screens']['edit']['enabled'] = (bool) $value;
+					break;
+
+				case 'enable_admin_item' :
+					$lpc['screens']['admin']['enabled'] = (bool) $value;
+					break;
+
+				case 'create_step_position' :
+					$lpc['screens']['create']['position'] = $value;
+					break;
+
+				// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'.
+				case 'admin_name' :
+					$lpc['screens']['edit']['name'] = $value;
+					break;
+
+				case 'admin_slug' :
+					$lpc['screens']['edit']['slug'] = $value;
+					break;
+
+				case 'create_name' :
+					$lpc['screens']['create']['name'] = $value;
+					break;
+
+				case 'create_slug' :
+					$lpc['screens']['create']['slug'] = $value;
+					break;
+
+				case 'admin_metabox_context' :
+					$lpc['screens']['admin']['metabox_context'] = $value;
+					break;
+
+				case 'admin_metabox_priority' :
+					$lpc['screens']['admin']['metabox_priority'] = $value;
+					break;
+
+				default :
+					$lpc[ $property ] = $value;
+					break;
+			}
+		}
+	}
+
+	/**
+	 * Set up legacy properties.
+	 *
+	 * This method is responsible for ensuring that all legacy config
+	 * properties are stored in an array $this->legacy_properties, so that
+	 * they remain available to plugins that reference the variables at
+	 * their old locations.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @see BP_Group_Extension::__get()
+	 */
+	protected function setup_legacy_properties() {
+
+		// Only run this one time.
+		if ( ! empty( $this->legacy_properties ) ) {
+			return;
+		}
+
+		$properties = $this->get_legacy_property_list();
+		$params     = $this->params;
+		$lp         =& $this->legacy_properties;
+
+		foreach ( $properties as $property ) {
+			switch ( $property ) {
+				case 'enable_create_step' :
+					$lp['enable_create_step'] = $params['screens']['create']['enabled'];
+					break;
+
+				case 'enable_edit_item' :
+					$lp['enable_edit_item'] = $params['screens']['edit']['enabled'];
+					break;
+
+				case 'enable_admin_item' :
+					$lp['enable_admin_item'] = $params['screens']['admin']['enabled'];
+					break;
+
+				case 'create_step_position' :
+					$lp['create_step_position'] = $params['screens']['create']['position'];
+					break;
+
+				// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'.
+				case 'admin_name' :
+					$lp['admin_name'] = $params['screens']['edit']['name'];
+					break;
+
+				case 'admin_slug' :
+					$lp['admin_slug'] = $params['screens']['edit']['slug'];
+					break;
+
+				case 'create_name' :
+					$lp['create_name'] = $params['screens']['create']['name'];
+					break;
+
+				case 'create_slug' :
+					$lp['create_slug'] = $params['screens']['create']['slug'];
+					break;
+
+				case 'admin_metabox_context' :
+					$lp['admin_metabox_context'] = $params['screens']['admin']['metabox_context'];
+					break;
+
+				case 'admin_metabox_priority' :
+					$lp['admin_metabox_priority'] = $params['screens']['admin']['metabox_priority'];
+					break;
+
+				default :
+					// All other items get moved over.
+					$lp[ $property ] = $params[ $property ];
+
+					// Also reapply to the object, for backpat.
+					$this->{$property} = $params[ $property ];
+
+					break;
+			}
+		}
+	}
+}
+
+/**
+ * Register a new Group Extension.
+ *
+ * @since 1.1.0
+ *
+ * @param string $group_extension_class Name of the Extension class.
+ * @return false|null Returns false on failure, otherwise null.
+ */
+function bp_register_group_extension( $group_extension_class = '' ) {
+
+	if ( ! class_exists( $group_extension_class ) ) {
+		return false;
+	}
+
+	// Register the group extension on the bp_init action so we have access
+	// to all plugins.
+	add_action( 'bp_init', function() use ( $group_extension_class ) {
+		$extension = new $group_extension_class;
+		add_action( 'bp_actions', array( &$extension, '_register' ), 8 );
+		add_action( 'admin_init', array( &$extension, '_register' ) );
+	}, 11 );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-group-member-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-group-member-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-group-member-query.php	(revision 12265)
@@ -0,0 +1,379 @@
+<?php
+/**
+ * BuddyPress Groups Classes.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsClasses
+ * @since 1.8.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Query for the members of a group.
+ *
+ * Special notes about the group members data schema:
+ * - *Members* are entries with is_confirmed = 1.
+ * - *Pending requests* are entries with is_confirmed = 0 and inviter_id = 0.
+ * - *Pending and sent invitations* are entries with is_confirmed = 0 and
+ *   inviter_id != 0 and invite_sent = 1.
+ * - *Pending and unsent invitations* are entries with is_confirmed = 0 and
+ *   inviter_id != 0 and invite_sent = 0.
+ * - *Membership requests* are entries with is_confirmed = 0 and
+ *   inviter_id = 0 (and invite_sent = 0).
+ *
+ * @since 1.8.0
+ * @since 3.0.0 $group_id now supports multiple values.
+ *
+ * @param array $args  {
+ *     Array of arguments. Accepts all arguments from
+ *     {@link BP_User_Query}, with the following additions:
+ *
+ *     @type int|array|string $group_id     ID of the group to limit results to. Also accepts multiple values
+ *                                          either as an array or as a comma-delimited string.
+ *     @type array            $group_role   Array of group roles to match ('member', 'mod', 'admin', 'banned').
+ *                                          Default: array( 'member' ).
+ *     @type bool             $is_confirmed Whether to limit to confirmed members. Default: true.
+ *     @type string           $type         Sort order. Accepts any value supported by {@link BP_User_Query}, in
+ *                                          addition to 'last_joined' and 'first_joined'. Default: 'last_joined'.
+ * }
+ */
+class BP_Group_Member_Query extends BP_User_Query {
+
+	/**
+	 * Array of group member ids, cached to prevent redundant lookups.
+	 *
+	 * @since 1.8.1
+	 * @var null|array Null if not yet defined, otherwise an array of ints.
+	 */
+	protected $group_member_ids;
+
+	/**
+	 * Set up action hooks.
+	 *
+	 * @since 1.8.0
+	 */
+	public function setup_hooks() {
+		// Take this early opportunity to set the default 'type' param
+		// to 'last_joined', which will ensure that BP_User_Query
+		// trusts our order and does not try to apply its own.
+		if ( empty( $this->query_vars_raw['type'] ) ) {
+			$this->query_vars_raw['type'] = 'last_joined';
+		}
+
+		// Set the sort order.
+		add_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
+
+		// Set up our populate_extras method.
+		add_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
+	}
+
+	/**
+	 * Get a list of user_ids to include in the IN clause of the main query.
+	 *
+	 * Overrides BP_User_Query::get_include_ids(), adding our additional
+	 * group-member logic.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $include Existing group IDs in the $include parameter,
+	 *                       as calculated in BP_User_Query.
+	 * @return array
+	 */
+	public function get_include_ids( $include = array() ) {
+		// The following args are specific to group member queries, and
+		// are not present in the query_vars of a normal BP_User_Query.
+		// We loop through to make sure that defaults are set (though
+		// values passed to the constructor will, as usual, override
+		// these defaults).
+		$this->query_vars = bp_parse_args( $this->query_vars, array(
+			'group_id'     => 0,
+			'group_role'   => array( 'member' ),
+			'is_confirmed' => true,
+			'invite_sent'  => null,
+			'inviter_id'   => null,
+			'type'         => 'last_joined',
+		), 'bp_group_member_query_get_include_ids' );
+
+		$group_member_ids = $this->get_group_member_ids();
+
+		// If the group member query returned no users, bail with an
+		// array that will guarantee no matches for BP_User_Query.
+		if ( empty( $group_member_ids ) ) {
+			return array( 0 );
+		}
+
+		if ( ! empty( $include ) ) {
+			$group_member_ids = array_intersect( $include, $group_member_ids );
+		}
+
+		return $group_member_ids;
+	}
+
+	/**
+	 * Get the members of the queried group.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @return array $ids User IDs of relevant group member ids.
+	 */
+	protected function get_group_member_ids() {
+		global $wpdb;
+
+		if ( is_array( $this->group_member_ids ) ) {
+			return $this->group_member_ids;
+		}
+
+		$bp  = buddypress();
+		$sql = array(
+			'select'  => "SELECT user_id FROM {$bp->groups->table_name_members}",
+			'where'   => array(),
+			'orderby' => '',
+			'order'   => '',
+		);
+
+		/* WHERE clauses *****************************************************/
+
+		// Group id.
+		$group_ids = wp_parse_id_list( $this->query_vars['group_id'] );
+		$group_ids = implode( ',', $group_ids );
+		$sql['where'][] = "group_id IN ({$group_ids})";
+
+		// If is_confirmed.
+		$is_confirmed = ! empty( $this->query_vars['is_confirmed'] ) ? 1 : 0;
+		$sql['where'][] = $wpdb->prepare( "is_confirmed = %d", $is_confirmed );
+
+		// If invite_sent.
+		if ( ! is_null( $this->query_vars['invite_sent'] ) ) {
+			$invite_sent = ! empty( $this->query_vars['invite_sent'] ) ? 1 : 0;
+			$sql['where'][] = $wpdb->prepare( "invite_sent = %d", $invite_sent );
+		}
+
+		// If inviter_id.
+		if ( ! is_null( $this->query_vars['inviter_id'] ) ) {
+			$inviter_id = $this->query_vars['inviter_id'];
+
+			// Empty: inviter_id = 0. (pass false, 0, or empty array).
+			if ( empty( $inviter_id ) ) {
+				$sql['where'][] = "inviter_id = 0";
+
+			// The string 'any' matches any non-zero value (inviter_id != 0).
+			} elseif ( 'any' === $inviter_id ) {
+				$sql['where'][] = "inviter_id != 0";
+
+			// Assume that a list of inviter IDs has been passed.
+			} else {
+				// Parse and sanitize.
+				$inviter_ids = wp_parse_id_list( $inviter_id );
+				if ( ! empty( $inviter_ids ) ) {
+					$inviter_ids_sql = implode( ',', $inviter_ids );
+					$sql['where'][] = "inviter_id IN ({$inviter_ids_sql})";
+				}
+			}
+		}
+
+		// Role information is stored as follows: admins have
+		// is_admin = 1, mods have is_mod = 1, banned have is_banned =
+		// 1, and members have all three set to 0.
+		$roles = !empty( $this->query_vars['group_role'] ) ? $this->query_vars['group_role'] : array();
+		if ( is_string( $roles ) ) {
+			$roles = explode( ',', $roles );
+		}
+
+		// Sanitize: Only 'admin', 'mod', 'member', and 'banned' are valid.
+		$allowed_roles = array( 'admin', 'mod', 'member', 'banned' );
+		foreach ( $roles as $role_key => $role_value ) {
+			if ( ! in_array( $role_value, $allowed_roles ) ) {
+				unset( $roles[ $role_key ] );
+			}
+		}
+
+		$roles = array_unique( $roles );
+
+		// When querying for a set of roles containing 'member' (for
+		// which there is no dedicated is_ column), figure out a list
+		// of columns *not* to match.
+		$roles_sql = '';
+		if ( in_array( 'member', $roles ) ) {
+			$role_columns = array();
+			foreach ( array_diff( $allowed_roles, $roles ) as $excluded_role ) {
+				$role_columns[] = 'is_' . $excluded_role . ' = 0';
+			}
+
+			if ( ! empty( $role_columns ) ) {
+				$roles_sql = '(' . implode( ' AND ', $role_columns ) . ')';
+			}
+
+		// When querying for a set of roles *not* containing 'member',
+		// simply construct a list of is_* = 1 clauses.
+		} else {
+			$role_columns = array();
+			foreach ( $roles as $role ) {
+				$role_columns[] = 'is_' . $role . ' = 1';
+			}
+
+			if ( ! empty( $role_columns ) ) {
+				$roles_sql = '(' . implode( ' OR ', $role_columns ) . ')';
+			}
+		}
+
+		if ( ! empty( $roles_sql ) ) {
+			$sql['where'][] = $roles_sql;
+		}
+
+		$sql['where'] = ! empty( $sql['where'] ) ? 'WHERE ' . implode( ' AND ', $sql['where'] ) : '';
+
+		// We fetch group members in order of last_joined, regardless
+		// of 'type'. If the 'type' value is not 'last_joined' or
+		// 'first_joined', the order will be overridden in
+		// BP_Group_Member_Query::set_orderby().
+		$sql['orderby'] = "ORDER BY date_modified";
+		$sql['order']   = 'first_joined' === $this->query_vars['type'] ? 'ASC' : 'DESC';
+
+		$this->group_member_ids = $wpdb->get_col( "{$sql['select']} {$sql['where']} {$sql['orderby']} {$sql['order']}" );
+
+		/**
+		 * Filters the member IDs for the current group member query.
+		 *
+		 * Use this filter to build a custom query (such as when you've
+		 * defined a custom 'type').
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array                 $group_member_ids Array of associated member IDs.
+		 * @param BP_Group_Member_Query $this             Current BP_Group_Member_Query instance.
+		 */
+		$this->group_member_ids = apply_filters( 'bp_group_member_query_group_member_ids', $this->group_member_ids, $this );
+
+		return $this->group_member_ids;
+	}
+
+	/**
+	 * Tell BP_User_Query to order by the order of our query results.
+	 *
+	 * We only override BP_User_Query's native ordering in case of the
+	 * 'last_joined' and 'first_joined' $type parameters.
+	 *
+	 * @since 1.8.1
+	 *
+	 * @param BP_User_Query $query BP_User_Query object.
+	 */
+	public function set_orderby( $query ) {
+		$gm_ids = $this->get_group_member_ids();
+		if ( empty( $gm_ids ) ) {
+			$gm_ids = array( 0 );
+		}
+
+		// For 'last_joined', 'first_joined', and 'group_activity'
+		// types, we override the default orderby clause of
+		// BP_User_Query. In the case of 'group_activity', we perform
+		// a separate query to get the necessary order. In the case of
+		// 'last_joined' and 'first_joined', we can trust the order of
+		// results from  BP_Group_Member_Query::get_group_members().
+		// In all other cases, we fall through and let BP_User_Query
+		// do its own (non-group-specific) ordering.
+		if ( in_array( $query->query_vars['type'], array( 'last_joined', 'first_joined', 'group_activity' ) ) ) {
+
+			// Group Activity DESC.
+			if ( 'group_activity' == $query->query_vars['type'] ) {
+				$gm_ids = $this->get_gm_ids_ordered_by_activity( $query, $gm_ids );
+			}
+
+			// The first param in the FIELD() clause is the sort column id.
+			$gm_ids = array_merge( array( 'u.id' ), wp_parse_id_list( $gm_ids ) );
+			$gm_ids_sql = implode( ',', $gm_ids );
+
+			$query->uid_clauses['orderby'] = "ORDER BY FIELD(" . $gm_ids_sql . ")";
+		}
+
+		// Prevent this filter from running on future BP_User_Query
+		// instances on the same page.
+		remove_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
+	}
+
+	/**
+	 * Fetch additional data required in bp_group_has_members() loops.
+	 *
+	 * Additional data fetched:
+	 *      - is_banned
+	 *      - date_modified
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param BP_User_Query $query        BP_User_Query object. Because we're
+	 *                                    filtering the current object, we use
+	 *                                    $this inside of the method instead.
+	 * @param string        $user_ids_sql Sanitized, comma-separated string of
+	 *                                    the user ids returned by the main query.
+	 */
+	public function populate_group_member_extras( $query, $user_ids_sql ) {
+		global $wpdb;
+
+		$bp     = buddypress();
+		$extras = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, date_modified, is_admin, is_mod, comments, user_title, invite_sent, is_confirmed, inviter_id, is_banned FROM {$bp->groups->table_name_members} WHERE user_id IN ({$user_ids_sql}) AND group_id = %d", $this->query_vars['group_id'] ) );
+
+		foreach ( (array) $extras as $extra ) {
+			if ( isset( $this->results[ $extra->user_id ] ) ) {
+				// The user_id is provided for backward compatibility.
+				$this->results[ $extra->user_id ]->user_id       = (int) $extra->user_id;
+				$this->results[ $extra->user_id ]->is_admin      = (int) $extra->is_admin;
+				$this->results[ $extra->user_id ]->is_mod        = (int) $extra->is_mod;
+				$this->results[ $extra->user_id ]->is_banned     = (int) $extra->is_banned;
+				$this->results[ $extra->user_id ]->date_modified = $extra->date_modified;
+				$this->results[ $extra->user_id ]->user_title    = $extra->user_title;
+				$this->results[ $extra->user_id ]->comments      = $extra->comments;
+				$this->results[ $extra->user_id ]->invite_sent   = (int) $extra->invite_sent;
+				$this->results[ $extra->user_id ]->inviter_id    = (int) $extra->inviter_id;
+				$this->results[ $extra->user_id ]->is_confirmed  = (int) $extra->is_confirmed;
+				$this->results[ $extra->user_id ]->membership_id = (int) $extra->id;
+			}
+		}
+
+		// Don't filter other BP_User_Query objects on the same page.
+		remove_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10 );
+	}
+
+	/**
+	 * Sort user IDs by how recently they have generated activity within a given group.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param BP_User_Query $query  BP_User_Query object.
+	 * @param array         $gm_ids array of group member ids.
+	 * @return array
+	 */
+	public function get_gm_ids_ordered_by_activity( $query, $gm_ids = array() ) {
+		global $wpdb;
+
+		if ( empty( $gm_ids ) ) {
+			return $gm_ids;
+		}
+
+		if ( ! bp_is_active( 'activity' ) ) {
+			return $gm_ids;
+		}
+
+		$activity_table = buddypress()->activity->table_name;
+
+		$sql = array(
+			'select'  => "SELECT user_id, max( date_recorded ) as date_recorded FROM {$activity_table}",
+			'where'   => array(),
+			'groupby' => 'GROUP BY user_id',
+			'orderby' => 'ORDER BY date_recorded',
+			'order'   => 'DESC',
+		);
+
+		$sql['where'] = array(
+			'user_id IN (' . implode( ',', wp_parse_id_list( $gm_ids ) ) . ')',
+			'item_id = ' . absint( $query->query_vars['group_id'] ),
+			$wpdb->prepare( "component = %s", buddypress()->groups->id ),
+		);
+
+		$sql['where'] = 'WHERE ' . implode( ' AND ', $sql['where'] );
+
+		$group_user_ids = $wpdb->get_results( "{$sql['select']} {$sql['where']} {$sql['groupby']} {$sql['orderby']} {$sql['order']}" );
+
+		return wp_list_pluck( $group_user_ids, 'user_id' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-component.php	(revision 12265)
@@ -0,0 +1,924 @@
+<?php
+/**
+ * BuddyPress Groups Component Class.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Creates our Groups component.
+ *
+ * @since 1.5.0
+ */
+class BP_Groups_Component extends BP_Component {
+
+	/**
+	 * Auto-join group when non group member performs group activity.
+	 *
+	 * @since 1.5.0
+	 * @var bool
+	 */
+	public $auto_join;
+
+	/**
+	 * The group being currently accessed.
+	 *
+	 * @since 1.5.0
+	 * @var BP_Groups_Group
+	 */
+	public $current_group;
+
+	/**
+	 * Default group extension.
+	 *
+	 * @since 1.6.0
+	 * @todo Is this used anywhere? Is this a duplicate of $default_extension?
+	 * @var string
+	 */
+	var $default_component;
+
+	/**
+	 * Default group extension.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $default_extension;
+
+	/**
+	 * Illegal group names/slugs.
+	 *
+	 * @since 1.5.0
+	 * @var array
+	 */
+	public $forbidden_names;
+
+	/**
+	 * Group creation/edit steps (e.g. Details, Settings, Avatar, Invites).
+	 *
+	 * @since 1.5.0
+	 * @var array
+	 */
+	public $group_creation_steps;
+
+	/**
+	 * Types of group statuses (Public, Private, Hidden).
+	 *
+	 * @since 1.5.0
+	 * @var array
+	 */
+	public $valid_status;
+
+	/**
+	 * Group types.
+	 *
+	 * @see bp_groups_register_group_type()
+	 *
+	 * @since 2.6.0
+	 * @var array
+	 */
+	public $types = array();
+
+	/**
+	 * Current directory group type.
+	 *
+	 * @see groups_directory_groups_setup()
+	 *
+	 * @since 2.7.0
+	 * @var string
+	 */
+	public $current_directory_type = '';
+
+	/**
+	 * Start the groups component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'groups',
+			_x( 'User Groups', 'Group screen page <title>', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 70,
+				'search_query_arg' => 'groups_search',
+			)
+		);
+	}
+
+	/**
+	 * Include Groups component files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::includes() for a description of arguments.
+	 *
+	 * @param array $includes See BP_Component::includes() for a description.
+	 */
+	public function includes( $includes = array() ) {
+		$includes = array(
+			'cache',
+			'filters',
+			'widgets',
+			'template',
+			'adminbar',
+			'functions',
+			'notifications'
+		);
+
+		// Conditional includes.
+		if ( bp_is_active( 'activity' ) ) {
+			$includes[] = 'activity';
+		}
+		if ( is_admin() ) {
+			$includes[] = 'admin';
+		}
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		if ( bp_is_groups_component() ) {
+			// Authenticated actions.
+			if ( is_user_logged_in() &&
+				in_array( bp_current_action(), array( 'create', 'join', 'leave-group' ), true )
+			) {
+				require $this->path . 'bp-groups/actions/' . bp_current_action() . '.php';
+			}
+
+			// Actions - RSS feed handler.
+			if ( bp_is_active( 'activity' ) && bp_is_current_action( 'feed' ) ) {
+				require $this->path . 'bp-groups/actions/feed.php';
+			}
+
+			// Actions - Random group handler.
+			if ( isset( $_GET['random-group'] ) ) {
+				require $this->path . 'bp-groups/actions/random.php';
+			}
+
+			// Screens - Directory.
+			if ( bp_is_groups_directory() ) {
+				require $this->path . 'bp-groups/screens/directory.php';
+			}
+
+			// Screens - User profile integration.
+			if ( bp_is_user() ) {
+				require $this->path . 'bp-groups/screens/user/my-groups.php';
+
+				if ( bp_is_current_action( 'invites' ) ) {
+					require $this->path . 'bp-groups/screens/user/invites.php';
+				}
+			}
+
+			// Single group.
+			if ( bp_is_group() ) {
+				// Actions - Access protection.
+				require $this->path . 'bp-groups/actions/access.php';
+
+				// Public nav items.
+				if ( in_array( bp_current_action(), array( 'home', 'request-membership', 'activity', 'members', 'send-invites' ), true ) ) {
+					require $this->path . 'bp-groups/screens/single/' . bp_current_action() . '.php';
+				}
+
+				// Admin nav items.
+				if ( bp_is_item_admin() && is_user_logged_in() ) {
+					require $this->path . 'bp-groups/screens/single/admin.php';
+
+					if ( in_array( bp_get_group_current_admin_tab(), array( 'edit-details', 'group-settings', 'group-avatar', 'group-cover-image', 'manage-members', 'membership-requests', 'delete-group' ), true ) ) {
+						require $this->path . 'bp-groups/screens/single/admin/' . bp_get_group_current_admin_tab() . '.php';
+					}
+				}
+			}
+
+			// Theme compatibility.
+			new BP_Groups_Theme_Compat();
+		}
+	}
+
+	/**
+	 * Set up component global data.
+	 *
+	 * The BP_GROUPS_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_globals() for a description of arguments.
+	 *
+	 * @param array $args See BP_Component::setup_globals() for a description.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		// Define a slug, if necessary.
+		if ( ! defined( 'BP_GROUPS_SLUG' ) ) {
+			define( 'BP_GROUPS_SLUG', $this->id );
+		}
+
+		// Global tables for groups component.
+		$global_tables = array(
+			'table_name'           => $bp->table_prefix . 'bp_groups',
+			'table_name_members'   => $bp->table_prefix . 'bp_groups_members',
+			'table_name_groupmeta' => $bp->table_prefix . 'bp_groups_groupmeta'
+		);
+
+		// Metadata tables for groups component.
+		$meta_tables = array(
+			'group' => $bp->table_prefix . 'bp_groups_groupmeta',
+		);
+
+		// Fetch the default directory title.
+		$default_directory_titles = bp_core_get_directory_page_default_titles();
+		$default_directory_title  = $default_directory_titles[$this->id];
+
+		// All globals for groups component.
+		// Note that global_tables is included in this array.
+		$args = array(
+			'slug'                  => BP_GROUPS_SLUG,
+			'root_slug'             => isset( $bp->pages->groups->slug ) ? $bp->pages->groups->slug : BP_GROUPS_SLUG,
+			'has_directory'         => true,
+			'directory_title'       => isset( $bp->pages->groups->title ) ? $bp->pages->groups->title : $default_directory_title,
+			'notification_callback' => 'groups_format_notifications',
+			'search_string'         => _x( 'Search Groups...', 'Component directory search', 'buddypress' ),
+			'global_tables'         => $global_tables,
+			'meta_tables'           => $meta_tables,
+		);
+
+		parent::setup_globals( $args );
+
+		/* Single Group Globals **********************************************/
+
+		// Are we viewing a single group?
+		if ( bp_is_groups_component()
+			&& ( ( $group_id = BP_Groups_Group::group_exists( bp_current_action() ) )
+				|| ( $group_id = BP_Groups_Group::get_id_by_previous_slug( bp_current_action() ) ) )
+			) {
+			$bp->is_single_item  = true;
+
+			/**
+			 * Filters the current PHP Class being used.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $value Name of the class being used.
+			 */
+			$current_group_class = apply_filters( 'bp_groups_current_group_class', 'BP_Groups_Group' );
+
+			if ( $current_group_class == 'BP_Groups_Group' ) {
+				$this->current_group = groups_get_group( $group_id );
+
+			} else {
+
+				/**
+				 * Filters the current group object being instantiated from previous filter.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param object $value Newly instantiated object for the group.
+				 */
+				$this->current_group = apply_filters( 'bp_groups_current_group_object', new $current_group_class( $group_id ) );
+			}
+
+			// When in a single group, the first action is bumped down one because of the
+			// group name, so we need to adjust this and set the group name to current_item.
+			$bp->current_item   = bp_current_action();
+			$bp->current_action = bp_action_variable( 0 );
+			array_shift( $bp->action_variables );
+
+			// Using "item" not "group" for generic support in other components.
+			if ( bp_current_user_can( 'bp_moderate' ) ) {
+				bp_update_is_item_admin( true, 'groups' );
+			} else {
+				bp_update_is_item_admin( groups_is_user_admin( bp_loggedin_user_id(), $this->current_group->id ), 'groups' );
+			}
+
+			// If the user is not an admin, check if they are a moderator.
+			if ( ! bp_is_item_admin() ) {
+				bp_update_is_item_mod  ( groups_is_user_mod  ( bp_loggedin_user_id(), $this->current_group->id ), 'groups' );
+			}
+
+			// Check once if the current group has a custom front template.
+			$this->current_group->front_template = bp_groups_get_front_template( $this->current_group );
+
+			// Initialize the nav for the groups component.
+			$this->nav = new BP_Core_Nav( $this->current_group->id );
+
+		// Set current_group to 0 to prevent debug errors.
+		} else {
+			$this->current_group = 0;
+		}
+
+		// Set group type if available.
+		if ( bp_is_groups_directory() && bp_is_current_action( bp_get_groups_group_type_base() ) && bp_action_variable() ) {
+			$matched_types = bp_groups_get_group_types( array(
+				'has_directory'  => true,
+				'directory_slug' => bp_action_variable(),
+			) );
+
+			// Set 404 if we do not have a valid group type.
+			if ( empty( $matched_types ) ) {
+				bp_do_404();
+				return;
+			}
+
+			// Set our directory type marker.
+			$this->current_directory_type = reset( $matched_types );
+		}
+
+		// Set up variables specific to the group creation process.
+		if ( bp_is_groups_component() && bp_is_current_action( 'create' ) && bp_user_can_create_groups() && isset( $_COOKIE['bp_new_group_id'] ) ) {
+			$bp->groups->new_group_id = (int) $_COOKIE['bp_new_group_id'];
+		}
+
+		/**
+		 * Filters the list of illegal groups names/slugs.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param array $value Array of illegal group names/slugs.
+		 */
+		$this->forbidden_names = apply_filters( 'groups_forbidden_names', array(
+			'my-groups',
+			'create',
+			'invites',
+			'send-invites',
+			'forum',
+			'delete',
+			'add',
+			'admin',
+			'request-membership',
+			'members',
+			'settings',
+			'avatar',
+			$this->slug,
+			$this->root_slug,
+		) );
+
+		// If the user was attempting to access a group, but no group by that name was found, 404.
+		if ( bp_is_groups_component() && empty( $this->current_group ) && empty( $this->current_directory_type ) && bp_current_action() && ! in_array( bp_current_action(), $this->forbidden_names ) ) {
+			bp_do_404();
+			return;
+		}
+
+		/**
+		 * Filters the preconfigured groups creation steps.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param array $value Array of preconfigured group creation steps.
+		 */
+		$this->group_creation_steps = apply_filters( 'groups_create_group_steps', array(
+			'group-details'  => array(
+				'name'       => _x( 'Details', 'Group screen nav', 'buddypress' ),
+				'position'   => 0
+			),
+			'group-settings' => array(
+				'name'       => _x( 'Settings', 'Group screen nav', 'buddypress' ),
+				'position'   => 10
+			)
+		) );
+
+		// If avatar uploads are not disabled, add avatar option.
+		$disabled_avatar_uploads = (int) bp_disable_group_avatar_uploads();
+		if ( ! $disabled_avatar_uploads && $bp->avatar->show_avatars ) {
+			$this->group_creation_steps['group-avatar'] = array(
+				'name'     => _x( 'Photo', 'Group screen nav', 'buddypress' ),
+				'position' => 20
+			);
+		}
+
+		if ( bp_group_use_cover_image_header() ) {
+			$this->group_creation_steps['group-cover-image'] = array(
+				'name'     => _x( 'Cover Image', 'Group screen nav', 'buddypress' ),
+				'position' => 25
+			);
+		}
+
+		// If friends component is active, add invitations.
+		if ( bp_is_active( 'friends' ) ) {
+			$this->group_creation_steps['group-invites'] = array(
+				'name'     => _x( 'Invites',  'Group screen nav', 'buddypress' ),
+				'position' => 30
+			);
+		}
+
+		/**
+		 * Filters the list of valid groups statuses.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param array $value Array of valid group statuses.
+		 */
+		$this->valid_status = apply_filters( 'groups_valid_status', array(
+			'public',
+			'private',
+			'hidden'
+		) );
+
+		// Auto join group when non group member performs group activity.
+		$this->auto_join = defined( 'BP_DISABLE_AUTO_GROUP_JOIN' ) && BP_DISABLE_AUTO_GROUP_JOIN ? false : true;
+	}
+
+	/**
+	 * Set up canonical stack for this component.
+	 *
+	 * @since 2.1.0
+	 */
+	public function setup_canonical_stack() {
+		if ( ! bp_is_groups_component() ) {
+			return;
+		}
+
+		if ( empty( $this->current_group ) ) {
+			return;
+		}
+
+		/**
+		 * Filters the default groups extension.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $value BP_GROUPS_DEFAULT_EXTENSION constant if defined,
+		 *                      else 'home'.
+		 */
+		$this->default_extension = apply_filters( 'bp_groups_default_extension', defined( 'BP_GROUPS_DEFAULT_EXTENSION' ) ? BP_GROUPS_DEFAULT_EXTENSION : 'home' );
+
+		$bp = buddypress();
+
+		// If the activity component is not active and the current group has no custom front, members are displayed in the home nav.
+		if ( 'members' === $this->default_extension && ! bp_is_active( 'activity' ) && ! $this->current_group->front_template ) {
+			$this->default_extension = 'home';
+		}
+
+		if ( ! bp_current_action() ) {
+			$bp->current_action = $this->default_extension;
+		}
+
+		// Prepare for a redirect to the canonical URL.
+		$bp->canonical_stack['base_url'] = bp_get_group_permalink( $this->current_group );
+
+		if ( bp_current_action() ) {
+			$bp->canonical_stack['action'] = bp_current_action();
+		}
+
+		/**
+		 * If there's no custom front.php template for the group, we need to make sure the canonical stack action
+		 * is set to 'home' in these 2 cases:
+		 *
+		 * - the current action is 'activity' (eg: site.url/groups/single/activity) and the Activity component is active
+		 * - the current action is 'members' (eg: site.url/groups/single/members) and the Activity component is *not* active.
+		 */
+		if ( ! $this->current_group->front_template && ( bp_is_current_action( 'activity' ) || ( ! bp_is_active( 'activity' ) && bp_is_current_action( 'members' ) ) ) ) {
+			$bp->canonical_stack['action'] = 'home';
+		}
+
+		if ( ! empty( $bp->action_variables ) ) {
+			$bp->canonical_stack['action_variables'] = bp_action_variables();
+		}
+
+		// When viewing the default extension, the canonical URL should not have
+		// that extension's slug, unless more has been tacked onto the URL via
+		// action variables.
+		if ( bp_is_current_action( $this->default_extension ) && empty( $bp->action_variables ) )  {
+			unset( $bp->canonical_stack['action'] );
+		}
+	}
+
+	/**
+	 * Set up component navigation.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of arguments.
+	 *
+	 * @param array $main_nav Optional. See BP_Component::setup_nav() for description.
+	 * @param array $sub_nav  Optional. See BP_Component::setup_nav() for description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			$user_domain = false;
+		}
+
+		// Only grab count if we're on a user page.
+		if ( bp_is_user() ) {
+			$class = ( 0 === groups_total_groups_for_user( bp_displayed_user_id() ) ) ? 'no-count' : 'count';
+
+			$nav_name = sprintf(
+				/* translators: %s: Group count for the current user */
+				_x( 'Groups %s', 'Group screen nav with counter', 'buddypress' ),
+				sprintf(
+					'<span class="%s">%s</span>',
+					esc_attr( $class ),
+					bp_get_total_group_count_for_user()
+				)
+			);
+		} else {
+			$nav_name = _x( 'Groups', 'Group screen nav without counter', 'buddypress' );
+		}
+
+		$slug = bp_get_groups_slug();
+
+		// Add 'Groups' to the main navigation.
+		$main_nav = array(
+			'name'                => $nav_name,
+			'slug'                => $slug,
+			'position'            => 70,
+			'screen_function'     => 'groups_screen_my_groups',
+			'default_subnav_slug' => 'my-groups',
+			'item_css_id'         => $this->id
+		);
+
+		if ( ! empty( $user_domain ) ) {
+			$access      = bp_core_can_edit_settings();
+			$groups_link = trailingslashit( $user_domain . $slug );
+
+			// Add the My Groups nav item.
+			$sub_nav[] = array(
+				'name'            => __( 'Memberships', 'buddypress' ),
+				'slug'            => 'my-groups',
+				'parent_url'      => $groups_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'groups_screen_my_groups',
+				'position'        => 10,
+				'item_css_id'     => 'groups-my-groups'
+			);
+
+			// Add the Group Invites nav item.
+			$sub_nav[] = array(
+				'name'            => __( 'Invitations', 'buddypress' ),
+				'slug'            => 'invites',
+				'parent_url'      => $groups_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'groups_screen_group_invites',
+				'user_has_access' => $access,
+				'position'        => 30
+			);
+
+			parent::setup_nav( $main_nav, $sub_nav );
+		}
+
+		if ( bp_is_groups_component() && bp_is_single_item() ) {
+
+			// Reset sub nav.
+			$sub_nav = array();
+
+			/*
+			 * The top-level Groups item is called 'Memberships' for legacy reasons.
+			 * It does not appear in the interface.
+			 */
+			bp_core_new_nav_item( array(
+				'name'                => __( 'Memberships', 'buddypress' ),
+				'slug'                => $this->current_group->slug,
+				'position'            => -1, // Do not show in BuddyBar.
+				'screen_function'     => 'groups_screen_group_home',
+				'default_subnav_slug' => $this->default_extension,
+				'item_css_id'         => $this->id
+			), 'groups' );
+
+			$group_link = bp_get_group_permalink( $this->current_group );
+
+			// Add the "Home" subnav item, as this will always be present.
+			$sub_nav[] = array(
+				'name'            =>  _x( 'Home', 'Group screen navigation title', 'buddypress' ),
+				'slug'            => 'home',
+				'parent_url'      => $group_link,
+				'parent_slug'     => $this->current_group->slug,
+				'screen_function' => 'groups_screen_group_home',
+				'position'        => 10,
+				'item_css_id'     => 'home'
+			);
+
+			// If this is a private group, and the user is not a
+			// member and does not have an outstanding invitation,
+			// show a "Request Membership" nav item.
+			if ( bp_current_user_can( 'groups_request_membership', array( 'group_id' => $this->current_group->id ) ) ) {
+
+				$sub_nav[] = array(
+					'name'            => _x( 'Request Membership','Group screen nav', 'buddypress' ),
+					'slug'            => 'request-membership',
+					'parent_url'      => $group_link,
+					'parent_slug'     => $this->current_group->slug,
+					'screen_function' => 'groups_screen_group_request_membership',
+					'position'        => 30
+				);
+			}
+
+			if ( $this->current_group->front_template || bp_is_active( 'activity' ) ) {
+				/**
+				 * If the theme is using a custom front, create activity subnav.
+				 */
+				if ( $this->current_group->front_template && bp_is_active( 'activity' ) ) {
+					$sub_nav[] = array(
+						'name'            => _x( 'Activity', 'My Group screen nav', 'buddypress' ),
+						'slug'            => 'activity',
+						'parent_url'      => $group_link,
+						'parent_slug'     => $this->current_group->slug,
+						'screen_function' => 'groups_screen_group_activity',
+						'position'        => 11,
+						'user_has_access' => $this->current_group->user_has_access,
+						'item_css_id'     => 'activity',
+						'no_access_url'   => $group_link,
+					);
+				}
+
+				/**
+				 * Only add the members subnav if it's not the home's nav.
+				 */
+				$sub_nav[] = array(
+					'name'            => sprintf( _x( 'Members %s', 'My Group screen nav', 'buddypress' ), '<span>' . number_format( $this->current_group->total_member_count ) . '</span>' ),
+					'slug'            => 'members',
+					'parent_url'      => $group_link,
+					'parent_slug'     => $this->current_group->slug,
+					'screen_function' => 'groups_screen_group_members',
+					'position'        => 60,
+					'user_has_access' => $this->current_group->user_has_access,
+					'item_css_id'     => 'members',
+					'no_access_url'   => $group_link,
+				);
+			}
+
+			if ( bp_is_active( 'friends' ) && bp_groups_user_can_send_invites() ) {
+				$sub_nav[] = array(
+					'name'            => _x( 'Send Invites', 'My Group screen nav', 'buddypress' ),
+					'slug'            => 'send-invites',
+					'parent_url'      => $group_link,
+					'parent_slug'     => $this->current_group->slug,
+					'screen_function' => 'groups_screen_group_invite',
+					'item_css_id'     => 'invite',
+					'position'        => 70,
+					'user_has_access' => $this->current_group->user_has_access,
+					'no_access_url'   => $group_link,
+				);
+			}
+
+			// If the user is a group admin, then show the group admin nav item.
+			if ( bp_is_item_admin() ) {
+				$sub_nav[] = array(
+					'name'            => _x( 'Manage', 'My Group screen nav', 'buddypress' ),
+					'slug'            => 'admin',
+					'parent_url'      => $group_link,
+					'parent_slug'     => $this->current_group->slug,
+					'screen_function' => 'groups_screen_group_admin',
+					'position'        => 1000,
+					'user_has_access' => true,
+					'item_css_id'     => 'admin',
+					'no_access_url'   => $group_link,
+				);
+
+				$admin_link = trailingslashit( $group_link . 'admin' );
+
+				// Common params to all nav items.
+				$default_params = array(
+					'parent_url'        => $admin_link,
+					'parent_slug'       => $this->current_group->slug . '_manage',
+					'screen_function'   => 'groups_screen_group_admin',
+					'user_has_access'   => bp_is_item_admin(),
+					'show_in_admin_bar' => true,
+				);
+
+				$sub_nav[] = array_merge( array(
+					'name'     => __( 'Details', 'buddypress' ),
+					'slug'     => 'edit-details',
+					'position' => 0,
+				), $default_params );
+
+				$sub_nav[] = array_merge( array(
+					'name'     => __( 'Settings', 'buddypress' ),
+					'slug'     => 'group-settings',
+					'position' => 10,
+				), $default_params );
+
+				if ( ! bp_disable_group_avatar_uploads() && buddypress()->avatar->show_avatars ) {
+					$sub_nav[] = array_merge( array(
+						'name'     => __( 'Photo', 'buddypress' ),
+						'slug'     => 'group-avatar',
+						'position' => 20,
+					), $default_params );
+				}
+
+				if ( bp_group_use_cover_image_header() ) {
+					$sub_nav[] = array_merge( array(
+						'name'     => __( 'Cover Image', 'buddypress' ),
+						'slug'     => 'group-cover-image',
+						'position' => 25,
+					), $default_params );
+				}
+
+				$sub_nav[] = array_merge( array(
+					'name'     => __( 'Members', 'buddypress' ),
+					'slug'     => 'manage-members',
+					'position' => 30,
+				), $default_params );
+
+				if ( 'private' == $this->current_group->status ) {
+					$sub_nav[] = array_merge( array(
+						'name'     => __( 'Requests', 'buddypress' ),
+						'slug'     => 'membership-requests',
+						'position' => 40,
+					), $default_params );
+				}
+
+				$sub_nav[] = array_merge( array(
+					'name'     => __( 'Delete', 'buddypress' ),
+					'slug'     => 'delete-group',
+					'position' => 1000,
+				), $default_params );
+			}
+
+			foreach ( $sub_nav as $nav ) {
+				bp_core_new_subnav_item( $nav, 'groups' );
+			}
+		}
+
+		if ( isset( $this->current_group->user_has_access ) ) {
+
+			/**
+			 * Fires at the end of the groups navigation setup if user has access.
+			 *
+			 * @since 1.0.2
+			 *
+			 * @param bool $user_has_access Whether or not user has access.
+			 */
+			do_action( 'groups_setup_nav', $this->current_group->user_has_access );
+		} else {
+
+			/** This action is documented in bp-groups/bp-groups-loader.php */
+			do_action( 'groups_setup_nav');
+		}
+	}
+
+	/**
+	 * Set up the component entries in the WordPress Admin Bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of the $wp_admin_nav
+	 *      parameter array.
+	 *
+	 * @param array $wp_admin_nav See BP_Component::setup_admin_bar() for a description.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$groups_link = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() );
+
+			// Pending group invites.
+			$count   = groups_get_invite_count_for_user();
+			$title   = _x( 'Groups', 'My Account Groups', 'buddypress' );
+			$pending = _x( 'No Pending Invites', 'My Account Groups sub nav', 'buddypress' );
+
+			if ( ! empty( $count['total'] ) ) {
+				$title = sprintf(
+					/* translators: %s: Group invitation count for the current user */
+					_x( 'Groups %s', 'My Account Groups nav', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+
+				$pending = sprintf(
+					/* translators: %s: Group invitation count for the current user */
+					_x( 'Pending Invites %s', 'My Account Groups sub nav', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+			}
+
+			// Add the "My Account" sub menus.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => $title,
+				'href'   => $groups_link
+			);
+
+			// My Groups.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-memberships',
+				'title'    => _x( 'Memberships', 'My Account Groups sub nav', 'buddypress' ),
+				'href'     => $groups_link,
+				'position' => 10
+			);
+
+			// Invitations.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-invites',
+				'title'    => $pending,
+				'href'     => trailingslashit( $groups_link . 'invites' ),
+				'position' => 30
+			);
+
+			// Create a Group.
+			if ( bp_user_can_create_groups() ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-create',
+					'title'    => _x( 'Create a Group', 'My Account Groups sub nav', 'buddypress' ),
+					'href'     => trailingslashit( bp_get_groups_directory_permalink() . 'create' ),
+					'position' => 90
+				);
+			}
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 *
+	 * @since 1.5.0
+	 */
+	public function setup_title() {
+
+		if ( bp_is_groups_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() && !bp_is_single_item() ) {
+				$bp->bp_options_title = _x( 'Memberships', 'My Groups page <title>', 'buddypress' );
+
+			} elseif ( !bp_is_my_profile() && !bp_is_single_item() ) {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title = bp_get_displayed_user_fullname();
+
+			// We are viewing a single group, so set up the
+			// group navigation menu using the $this->current_group global.
+			} elseif ( bp_is_single_item() ) {
+				$bp->bp_options_title  = $this->current_group->name;
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id'    => $this->current_group->id,
+					'object'     => 'group',
+					'type'       => 'thumb',
+					'avatar_dir' => 'group-avatars',
+					'alt'        => __( 'Group Profile Photo', 'buddypress' )
+				) );
+
+				if ( empty( $bp->bp_options_avatar ) ) {
+					$bp->bp_options_avatar = '<img src="' . esc_url( bp_core_avatar_default_thumb() ) . '" alt="' . esc_attr__( 'No Group Profile Photo', 'buddypress' ) . '" class="avatar" />';
+				}
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_groups',
+			'bp_group_admins',
+			'bp_group_invite_count',
+			'group_meta',
+			'bp_groups_memberships',
+			'bp_groups_memberships_for_user',
+		) );
+
+		parent::setup_cache_groups();
+	}
+
+	/**
+	 * Set up taxonomies.
+	 *
+	 * @since 2.6.0
+	 */
+	public function register_taxonomies() {
+		// Group Type.
+		register_taxonomy( 'bp_group_type', 'bp_group', array(
+			'public' => false,
+		) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-group-members-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-group-members-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-group-members-template.php	(revision 12265)
@@ -0,0 +1,279 @@
+<?php
+/**
+ * BuddyPress Groups group members loop template class.
+ *
+ * @package BuddyPress
+ * @since 1.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Group Members Loop template class.
+ *
+ * @since 1.0.0
+ */
+class BP_Groups_Group_Members_Template {
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $current_member = -1;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $member_count;
+
+	/**
+	 * @since 1.0.0
+	 * @var array
+	 */
+	public $members;
+
+	/**
+	 * @since 1.0.0
+	 * @var object
+	 */
+	public $member;
+
+	/**
+	 * @since 1.0.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $pag_page;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $pag_num;
+
+	/**
+	 * @since 1.0.0
+	 * @var array|string|void
+	 */
+	public $pag_links;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $total_group_count;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args {
+	 *     An array of optional arguments.
+	 *     @type int      $group_id           ID of the group whose members are being
+	 *                                        queried. Default: current group ID.
+	 *     @type int      $page               Page of results to be queried. Default: 1.
+	 *     @type int      $per_page           Number of items to return per page of
+	 *                                        results. Default: 20.
+	 *     @type int      $max                Optional. Max number of items to return.
+	 *     @type array    $exclude            Optional. Array of user IDs to exclude.
+	 *     @type bool|int $exclude_admin_mods True (or 1) to exclude admins and mods from
+	 *                                        results. Default: 1.
+	 *     @type bool|int $exclude_banned     True (or 1) to exclude banned users from results.
+	 *                                        Default: 1.
+	 *     @type array    $group_role         Optional. Array of group roles to include.
+	 *     @type string   $search_terms       Optional. Search terms to match.
+	 * }
+	 */
+	public function __construct( $args = array() ) {
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '2.0.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'group_id',
+				1 => 'per_page',
+				2 => 'max',
+				3 => 'exclude_admins_mods',
+				4 => 'exclude_banned',
+				5 => 'exclude',
+				6 => 'group_role',
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$r = bp_parse_args( $args, array(
+			'group_id'            => bp_get_current_group_id(),
+			'page'                => 1,
+			'per_page'            => 20,
+			'page_arg'            => 'mlpage',
+			'max'                 => false,
+			'exclude'             => false,
+			'exclude_admins_mods' => 1,
+			'exclude_banned'      => 1,
+			'group_role'          => false,
+			'search_terms'        => false,
+			'type'                => 'last_joined',
+		), 'group_members_template' );
+
+		$this->pag_arg  = sanitize_key( $r['page_arg'] );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+
+		/**
+		 * Check the current group is the same as the supplied group ID.
+		 * It can differ when using {@link bp_group_has_members()} outside the Groups screens.
+		 */
+		$current_group = groups_get_current_group();
+		if ( empty( $current_group ) || ( $current_group && $current_group->id !== bp_get_current_group_id() ) ) {
+			$current_group = groups_get_group( $r['group_id'] );
+		}
+
+		// Assemble the base URL for pagination.
+		$base_url = trailingslashit( bp_get_group_permalink( $current_group ) . bp_current_action() );
+		if ( bp_action_variable() ) {
+			$base_url = trailingslashit( $base_url . bp_action_variable() );
+		}
+
+		$members_args = $r;
+
+		$members_args['page']     = $this->pag_page;
+		$members_args['per_page'] = $this->pag_num;
+
+		// Get group members for this loop.
+		$this->members = groups_get_group_members( $members_args );
+
+		if ( empty( $r['max'] ) || ( $r['max'] >= (int) $this->members['count'] ) ) {
+			$this->total_member_count = (int) $this->members['count'];
+		} else {
+			$this->total_member_count = (int) $r['max'];
+		}
+
+		// Reset members array for subsequent looping.
+		$this->members = $this->members['members'];
+
+		if ( empty( $r['max'] ) || ( $r['max'] >= count( $this->members ) ) ) {
+			$this->member_count = (int) count( $this->members );
+		} else {
+			$this->member_count = (int) $r['max'];
+		}
+
+		$this->pag_links = paginate_links( array(
+			'base'      => add_query_arg( array( $this->pag_arg => '%#%' ), $base_url ),
+			'format'    => '',
+			'total'     => ! empty( $this->pag_num ) ? ceil( $this->total_member_count / $this->pag_num ) : $this->total_member_count,
+			'current'   => $this->pag_page,
+			'prev_text' => '&larr;',
+			'next_text' => '&rarr;',
+			'mid_size'  => 1,
+			'add_args'  => array(),
+		) );
+	}
+
+	/**
+	 * Whether or not there are members to display.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function has_members() {
+		if ( ! empty( $this->member_count ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Increments to the next member to display.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return object
+	 */
+	public function next_member() {
+		$this->current_member++;
+		$this->member = $this->members[ $this->current_member ];
+
+		return $this->member;
+	}
+
+	/**
+	 * Rewinds to the first member to display.
+	 *
+	 * @since 1.0.0
+	 */
+	public function rewind_members() {
+		$this->current_member = -1;
+		if ( $this->member_count > 0 ) {
+			$this->member = $this->members[0];
+		}
+	}
+
+	/**
+	 * Finishes up the members for display.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function members() {
+		$tick = intval( $this->current_member + 1 );
+		if ( $tick < $this->member_count ) {
+			return true;
+		} elseif ( $tick == $this->member_count ) {
+
+			/**
+			 * Fires right before the rewinding of members list.
+			 *
+			 * @since 1.0.0
+			 * @since 2.3.0 `$this` parameter added.
+			 * @since 2.7.0 Action renamed from `loop_end`.
+			 *
+			 * @param BP_Groups_Group_Members_Template $this Instance of the current Members template.
+			 */
+			do_action( 'group_members_loop_end', $this );
+
+			// Do some cleaning up after the loop.
+			$this->rewind_members();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Sets up the member to display.
+	 *
+	 * @since 1.0.0
+	 */
+	public function the_member() {
+		$this->in_the_loop = true;
+		$this->member      = $this->next_member();
+
+		// Loop has just started.
+		if ( 0 == $this->current_member ) {
+
+			/**
+			 * Fires if the current member item is the first in the members list.
+			 *
+			 * @since 1.0.0
+			 * @since 2.3.0 `$this` parameter added.
+			 * @since 2.7.0 Action renamed from `loop_start`.
+			 *
+			 * @param BP_Groups_Group_Members_Template $this Instance of the current Members template.
+			 */
+			do_action( 'group_members_loop_start', $this );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-group.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-group.php	(revision 12265)
@@ -0,0 +1,1785 @@
+<?php
+/**
+ * BuddyPress Groups Classes.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Group object.
+ *
+ * @since 1.6.0
+ */
+class BP_Groups_Group {
+
+	/**
+	 * ID of the group.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * User ID of the group's creator.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	public $creator_id;
+
+	/**
+	 * Name of the group.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $name;
+
+	/**
+	 * Group slug.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $slug;
+
+	/**
+	 * Group description.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $description;
+
+	/**
+	 * Group status.
+	 *
+	 * Core statuses are 'public', 'private', and 'hidden'.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $status;
+
+	/**
+	 * Parent ID.
+	 *
+	 * ID of parent group, if applicable.
+	 *
+	 * @since 2.7.0
+	 * @var int
+	 */
+	public $parent_id;
+
+	/**
+	 * Controls whether the group has a forum enabled.
+	 *
+	 * @since 1.6.0
+	 * @since 3.0.0 Previously, this referred to Legacy Forums. It's still used by bbPress 2 for integration.
+	 *
+	 * @var int
+	 */
+	public $enable_forum;
+
+	/**
+	 * Date the group was created.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	public $date_created;
+
+	/**
+	 * Data about the group's admins.
+	 *
+	 * @since 1.6.0
+	 * @var array
+	 */
+	protected $admins;
+
+	/**
+	 * Data about the group's moderators.
+	 *
+	 * @since 1.6.0
+	 * @var array
+	 */
+	protected $mods;
+
+	/**
+	 * Total count of group members.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	protected $total_member_count;
+
+	/**
+	 * Is the current user a member of this group?
+	 *
+	 * @since 1.2.0
+	 * @var bool
+	 */
+	protected $is_member;
+
+	/**
+	 * Is the current user a member of this group?
+	 * Alias of $is_member for backward compatibility.
+	 *
+	 * @since 2.9.0
+	 * @var bool
+	 */
+	protected $is_user_member;
+
+	/**
+	 * Does the current user have an outstanding invitation to this group?
+	 *
+	 * @since 1.9.0
+	 * @var bool
+	 */
+	protected $is_invited;
+
+	/**
+	 * Does the current user have a pending membership request to this group?
+	 *
+	 * @since 1.9.0
+	 * @var bool
+	 */
+	protected $is_pending;
+
+	/**
+	 * Timestamp of the last activity that happened in this group.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	protected $last_activity;
+
+	/**
+	 * If this is a private or hidden group, does the current user have access?
+	 *
+	 * @since 1.6.0
+	 * @var bool
+	 */
+	protected $user_has_access;
+
+	/**
+	 * Can the current user know that this group exists?
+	 *
+	 * @since 2.9.0
+	 * @var bool
+	 */
+	protected $is_visible;
+
+	/**
+	 * Raw arguments passed to the constructor.
+	 *
+	 * @since 2.0.0
+	 * @var array
+	 */
+	public $args;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int|null $id   Optional. If the ID of an existing group is provided,
+	 *                       the object will be pre-populated with info about that group.
+	 * @param array    $args {
+	 *     Array of optional arguments.
+	 *     @type bool $populate_extras Deprecated.
+	 * }
+	 */
+	public function __construct( $id = null, $args = array() ) {
+		if ( !empty( $id ) ) {
+			$this->id = (int) $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Set up data about the current group.
+	 *
+	 * @since 1.6.0
+	 */
+	public function populate() {
+		global $wpdb;
+
+		// Get BuddyPress.
+		$bp    = buddypress();
+
+		// Check cache for group data.
+		$group = wp_cache_get( $this->id, 'bp_groups' );
+
+		// Cache missed, so query the DB.
+		if ( false === $group ) {
+			$group = $wpdb->get_row( $wpdb->prepare( "SELECT g.* FROM {$bp->groups->table_name} g WHERE g.id = %d", $this->id ) );
+
+			wp_cache_set( $this->id, $group, 'bp_groups' );
+		}
+
+		// No group found so set the ID and bail.
+		if ( empty( $group ) || is_wp_error( $group ) ) {
+			$this->id = 0;
+			return;
+		}
+
+		// Group found so setup the object variables.
+		$this->id           = (int) $group->id;
+		$this->creator_id   = (int) $group->creator_id;
+		$this->name         = stripslashes( $group->name );
+		$this->slug         = $group->slug;
+		$this->description  = stripslashes( $group->description );
+		$this->status       = $group->status;
+		$this->parent_id    = (int) $group->parent_id;
+		$this->enable_forum = (int) $group->enable_forum;
+		$this->date_created = $group->date_created;
+	}
+
+	/**
+	 * Save the current group to the database.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->creator_id   = apply_filters( 'groups_group_creator_id_before_save',   $this->creator_id,   $this->id );
+		$this->name         = apply_filters( 'groups_group_name_before_save',         $this->name,         $this->id );
+		$this->slug         = apply_filters( 'groups_group_slug_before_save',         $this->slug,         $this->id );
+		$this->description  = apply_filters( 'groups_group_description_before_save',  $this->description,  $this->id );
+		$this->status       = apply_filters( 'groups_group_status_before_save',       $this->status,       $this->id );
+		$this->parent_id    = apply_filters( 'groups_group_parent_id_before_save',    $this->parent_id,    $this->id );
+		$this->enable_forum = apply_filters( 'groups_group_enable_forum_before_save', $this->enable_forum, $this->id );
+		$this->date_created = apply_filters( 'groups_group_date_created_before_save', $this->date_created, $this->id );
+
+		/**
+		 * Fires before the current group item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Groups_Group $this Current instance of the group item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'groups_group_before_save', array( &$this ) );
+
+		// Groups need at least a name.
+		if ( empty( $this->name ) ) {
+			return false;
+		}
+
+		// Set slug with group title if not passed.
+		if ( empty( $this->slug ) ) {
+			$this->slug = sanitize_title( $this->name );
+		}
+
+		// Sanity check.
+		if ( empty( $this->slug ) ) {
+			return false;
+		}
+
+		// Check for slug conflicts if creating new group.
+		if ( empty( $this->id ) ) {
+			$this->slug = groups_check_slug( $this->slug );
+		}
+
+		if ( !empty( $this->id ) ) {
+			$sql = $wpdb->prepare(
+				"UPDATE {$bp->groups->table_name} SET
+					creator_id = %d,
+					name = %s,
+					slug = %s,
+					description = %s,
+					status = %s,
+					parent_id = %d,
+					enable_forum = %d,
+					date_created = %s
+				WHERE
+					id = %d
+				",
+					$this->creator_id,
+					$this->name,
+					$this->slug,
+					$this->description,
+					$this->status,
+					$this->parent_id,
+					$this->enable_forum,
+					$this->date_created,
+					$this->id
+			);
+		} else {
+			$sql = $wpdb->prepare(
+				"INSERT INTO {$bp->groups->table_name} (
+					creator_id,
+					name,
+					slug,
+					description,
+					status,
+					parent_id,
+					enable_forum,
+					date_created
+				) VALUES (
+					%d, %s, %s, %s, %s, %d, %d, %s
+				)",
+					$this->creator_id,
+					$this->name,
+					$this->slug,
+					$this->description,
+					$this->status,
+					$this->parent_id,
+					$this->enable_forum,
+					$this->date_created
+			);
+		}
+
+		if ( false === $wpdb->query($sql) )
+			return false;
+
+		if ( empty( $this->id ) )
+			$this->id = $wpdb->insert_id;
+
+		/**
+		 * Fires after the current group item has been saved.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Groups_Group $this Current instance of the group item that was saved. Passed by reference.
+		 */
+		do_action_ref_array( 'groups_group_after_save', array( &$this ) );
+
+		wp_cache_delete( $this->id, 'bp_groups' );
+
+		return true;
+	}
+
+	/**
+	 * Delete the current group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function delete() {
+		global $wpdb;
+
+		// Delete groupmeta for the group.
+		groups_delete_groupmeta( $this->id );
+
+		// Fetch the user IDs of all the members of the group.
+		$user_ids    = BP_Groups_Member::get_group_member_ids( $this->id );
+		$user_id_str = esc_sql( implode( ',', wp_parse_id_list( $user_ids ) ) );
+
+		// Modify group count usermeta for members.
+		$wpdb->query( "UPDATE {$wpdb->usermeta} SET meta_value = meta_value - 1 WHERE meta_key = 'total_group_count' AND user_id IN ( {$user_id_str} )" );
+
+		// Now delete all group member entries.
+		BP_Groups_Member::delete_all( $this->id );
+
+		/**
+		 * Fires before the deletion of a group.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param BP_Groups_Group $this     Current instance of the group item being deleted. Passed by reference.
+		 * @param array           $user_ids Array of user IDs that were members of the group.
+		 */
+		do_action_ref_array( 'bp_groups_delete_group', array( &$this, $user_ids ) );
+
+		wp_cache_delete( $this->id, 'bp_groups' );
+
+		$bp = buddypress();
+
+		// Finally remove the group entry from the DB.
+		if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name} WHERE id = %d", $this->id ) ) )
+			return false;
+
+		return true;
+	}
+
+	/**
+	 * Magic getter.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $key Property name.
+	 * @return mixed
+	 */
+	public function __get( $key ) {
+		switch ( $key ) {
+			case 'last_activity' :
+			case 'total_member_count' :
+			case 'forum_id' :
+				$retval = groups_get_groupmeta( $this->id, $key );
+
+				if ( 'last_activity' !== $key ) {
+					$retval = (int) $retval;
+				}
+
+				return $retval;
+
+			case 'admins' :
+				return $this->get_admins();
+
+			case 'mods' :
+				return $this->get_mods();
+
+			case 'is_member' :
+			case 'is_user_member' :
+				return $this->get_is_member();
+
+			case 'is_invited' :
+				return groups_check_user_has_invite( bp_loggedin_user_id(), $this->id );
+
+			case 'is_pending' :
+				return groups_check_for_membership_request( bp_loggedin_user_id(), $this->id );
+
+			case 'user_has_access' :
+				return $this->get_user_has_access();
+
+			case 'is_visible' :
+				return $this->is_visible();
+
+			default :
+				return isset( $this->{$key} ) ? $this->{$key} : null;
+		}
+	}
+
+	/**
+	 * Magic issetter.
+	 *
+	 * Used to maintain backward compatibility for properties that are now
+	 * accessible only via magic method.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $key Property name.
+	 * @return bool
+	 */
+	public function __isset( $key ) {
+		switch ( $key ) {
+			case 'admins' :
+			case 'is_invited' :
+			case 'is_member' :
+			case 'is_user_member' :
+			case 'is_pending' :
+			case 'last_activity' :
+			case 'mods' :
+			case 'total_member_count' :
+			case 'user_has_access' :
+			case 'is_visible' :
+			case 'forum_id' :
+				return true;
+
+			default :
+				return isset( $this->{$key} );
+		}
+	}
+
+	/**
+	 * Magic setter.
+	 *
+	 * Used to maintain backward compatibility for properties that are now
+	 * accessible only via magic method.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $key   Property name.
+	 * @param mixed  $value Property value.
+	 * @return bool
+	 */
+	public function __set( $key, $value ) {
+		switch ( $key ) {
+			case 'user_has_access' :
+				return $this->user_has_access = (bool) $value;
+
+			default :
+				$this->{$key} = $value;
+		}
+	}
+
+	/**
+	 * Get a list of the group's admins.
+	 *
+	 * Used to provide cache-friendly access to the 'admins' property of
+	 * the group object.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return array|null
+	 */
+	protected function get_admins() {
+		if ( isset( $this->admins ) ) {
+			return $this->admins;
+		}
+
+		$this->set_up_admins_and_mods();
+		return $this->admins;
+	}
+
+	/**
+	 * Get a list of the group's mods.
+	 *
+	 * Used to provide cache-friendly access to the 'mods' property of
+	 * the group object.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return array|null
+	 */
+	protected function get_mods() {
+		if ( isset( $this->mods ) ) {
+			return $this->mods;
+		}
+
+		$this->set_up_admins_and_mods();
+		return $this->mods;
+	}
+
+	/**
+	 * Set up admins and mods for the current group object.
+	 *
+	 * Called only when the 'admins' or 'mods' property is accessed.
+	 *
+	 * @since 2.7.0
+	 */
+	protected function set_up_admins_and_mods() {
+		$admin_ids = BP_Groups_Member::get_group_administrator_ids( $this->id );
+		$admin_ids_plucked = wp_list_pluck( $admin_ids, 'user_id' );
+
+		$mod_ids = BP_Groups_Member::get_group_moderator_ids( $this->id );
+		$mod_ids_plucked = wp_list_pluck( $mod_ids, 'user_id' );
+
+		$admin_mod_ids = array_merge( $admin_ids_plucked, $mod_ids_plucked );
+		$admin_mod_users = array();
+
+		if ( ! empty( $admin_mod_ids ) ) {
+			$admin_mod_users = get_users( array(
+				'include' => $admin_mod_ids,
+				'blog_id' => null,
+			) );
+		}
+
+		$admin_objects = $mod_objects = array();
+		foreach ( $admin_mod_users as $admin_mod_user ) {
+			$obj = new stdClass();
+			$obj->user_id = $admin_mod_user->ID;
+			$obj->user_login = $admin_mod_user->user_login;
+			$obj->user_email = $admin_mod_user->user_email;
+			$obj->user_nicename = $admin_mod_user->user_nicename;
+
+			if ( in_array( $admin_mod_user->ID, $admin_ids_plucked, true ) ) {
+				$obj->is_admin = 1;
+				$obj->is_mod = 0;
+				$admin_objects[] = $obj;
+			} else {
+				$obj->is_admin = 0;
+				$obj->is_mod = 1;
+				$mod_objects[] = $obj;
+			}
+		}
+
+		$this->admins = $admin_objects;
+		$this->mods   = $mod_objects;
+	}
+
+	/**
+	 * Checks whether the logged-in user is a member of the group.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return bool|int
+	 */
+	protected function get_is_member() {
+		if ( isset( $this->is_member ) ) {
+			return $this->is_member;
+		}
+
+		$this->is_member = groups_is_user_member( bp_loggedin_user_id(), $this->id );
+		return $this->is_member;
+	}
+
+	/**
+	 * Checks whether the logged-in user has access to the group.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return bool
+	 */
+	protected function get_user_has_access() {
+		if ( isset( $this->user_has_access ) ) {
+			return $this->user_has_access;
+		}
+
+		if ( ( 'private' === $this->status ) || ( 'hidden' === $this->status ) ) {
+
+			// Assume user does not have access to hidden/private groups.
+			$this->user_has_access = false;
+
+			// Group members or community moderators have access.
+			if ( ( is_user_logged_in() && $this->get_is_member() ) || bp_current_user_can( 'bp_moderate' ) ) {
+				$this->user_has_access = true;
+			}
+		} else {
+			$this->user_has_access = true;
+		}
+
+		return $this->user_has_access;
+	}
+
+	/**
+	 * Checks whether the current user can know the group exists.
+	 *
+	 * @since 2.9.0
+	 *
+	 * @return bool
+	 */
+	protected function is_visible() {
+		if ( isset( $this->is_visible ) ) {
+			return $this->is_visible;
+		}
+
+		if ( 'hidden' === $this->status ) {
+
+			// Assume user can not know about hidden groups.
+			$this->is_visible = false;
+
+			// Group members or community moderators have access.
+			if ( ( is_user_logged_in() && $this->get_is_member() ) || bp_current_user_can( 'bp_moderate' ) ) {
+				$this->is_visible = true;
+			}
+		} else {
+			$this->is_visible = true;
+		}
+
+		return $this->is_visible;
+	}
+
+	/** Static Methods ****************************************************/
+
+	/**
+	 * Get whether a group exists for a given slug.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string      $slug       Slug to check.
+	 * @param string|bool $table_name Deprecated.
+	 * @return int|null Group ID if found; null if not.
+	 */
+	public static function group_exists( $slug, $table_name = false ) {
+		global $wpdb;
+
+		if ( empty( $slug ) ) {
+			return false;
+		}
+
+		$args = array(
+			'slug'               => $slug,
+			'per_page'           => 1,
+			'page'               => 1,
+			'update_meta_cache'  => false,
+			'show_hidden'        => true,
+		);
+
+		$groups = BP_Groups_Group::get( $args );
+
+		$group_id = null;
+		if ( $groups['groups'] ) {
+			$group_id = current( $groups['groups'] )->id;
+		}
+
+		return $group_id;
+	}
+
+	/**
+	 * Get the ID of a group by the group's slug.
+	 *
+	 * Alias of {@link BP_Groups_Group::group_exists()}.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $slug See {@link BP_Groups_Group::group_exists()}.
+	 * @return int|null See {@link BP_Groups_Group::group_exists()}.
+	 */
+	public static function get_id_from_slug( $slug ) {
+		return BP_Groups_Group::group_exists( $slug );
+	}
+
+	/**
+	 * Get whether a group exists for an old slug.
+	 *
+	 * @since 2.9.0
+	 *
+	 * @param string      $slug       Slug to check.
+	 *
+	 * @return int|null|false Group ID if found; null if not; false if missing parameters.
+	 */
+	public static function get_id_by_previous_slug( $slug ) {
+		global $wpdb;
+
+		if ( empty( $slug ) ) {
+			return false;
+		}
+
+		$args = array(
+			'meta_query'         => array(
+				array(
+					'key'   => 'previous_slug',
+					'value' => $slug
+				),
+			),
+			'orderby'            => 'meta_id',
+			'order'              => 'DESC',
+			'per_page'           => 1,
+			'page'               => 1,
+			'update_meta_cache'  => false,
+			'show_hidden'        => true,
+		);
+		$groups = BP_Groups_Group::get( $args );
+
+		$group_id = null;
+		if ( $groups['groups'] ) {
+			$group_id = current( $groups['groups'] )->id;
+		}
+
+		return $group_id;
+	}
+
+	/**
+	 * Get IDs of users with outstanding invites to a given group from a specified user.
+	 *
+	 * @since 1.6.0
+	 * @since 2.9.0 Added $sent as a parameter.
+	 *
+	 * @param  int      $user_id  ID of the inviting user.
+	 * @param  int      $group_id ID of the group.
+	 * @param  int|null $sent     Query for a specific invite sent status. If 0, this will query for users
+	 *                            that haven't had an invite sent to them yet. If 1, this will query for
+	 *                            users that have had an invite sent to them. If null, no invite status will
+	 *                            queried. Default: null.
+	 * @return array    IDs of users who have been invited to the group by the user but have not
+	 *                  yet accepted.
+	 */
+	public static function get_invites( $user_id, $group_id, $sent = null ) {
+		global $wpdb;
+
+		$bp  = buddypress();
+		$sql = $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE group_id = %d and is_confirmed = 0 AND inviter_id = %d", $group_id, $user_id );
+
+		// Query for a specific invite sent status.
+		if ( ! is_null( $sent ) ) {
+			$sql .= $wpdb->prepare( ' AND invite_sent = %d', $sent );
+		}
+
+		return $wpdb->get_col( $sql );
+	}
+
+	/**
+	 * Get a list of a user's groups, filtered by a search string.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string   $filter  Search term. Matches against 'name' and
+	 *                          'description' fields.
+	 * @param int      $user_id ID of the user whose groups are being searched.
+	 *                          Default: the displayed user.
+	 * @param mixed    $order   Not used.
+	 * @param int|null $limit   Optional. The max number of results to return.
+	 *                          Default: null (no limit).
+	 * @param int|null $page    Optional. The page offset of results to return.
+	 *                          Default: null (no limit).
+	 * @return false|array {
+	 *     @type array $groups Array of matched and paginated group IDs.
+	 *     @type int   $total  Total count of groups matching the query.
+	 * }
+	 */
+	public static function filter_user_groups( $filter, $user_id = 0, $order = false, $limit = null, $page = null ) {
+		if ( empty( $user_id ) ) {
+			$user_id = bp_displayed_user_id();
+		}
+
+		$args = array(
+			'search_terms' => $filter,
+			'user_id'      => $user_id,
+			'per_page'     => $limit,
+			'page'         => $page,
+			'order'        => $order,
+		);
+
+		$groups = BP_Groups_Group::get( $args );
+
+		// Modify the results to match the old format.
+		$paged_groups = array();
+		$i = 0;
+		foreach ( $groups['groups'] as $group ) {
+			$paged_groups[ $i ] = new stdClass;
+			$paged_groups[ $i ]->group_id = $group->id;
+			$i++;
+		}
+
+		return array( 'groups' => $paged_groups, 'total' => $groups['total'] );
+	}
+
+	/**
+	 * Get a list of groups, filtered by a search string.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string      $filter  Search term. Matches against 'name' and
+	 *                             'description' fields.
+	 * @param int|null    $limit   Optional. The max number of results to return.
+	 *                             Default: null (no limit).
+	 * @param int|null    $page    Optional. The page offset of results to return.
+	 *                             Default: null (no limit).
+	 * @param string|bool $sort_by Column to sort by. Default: false (default
+	 *        sort).
+	 * @param string|bool $order   ASC or DESC. Default: false (default sort).
+	 * @return array {
+	 *     @type array $groups Array of matched and paginated group IDs.
+	 *     @type int   $total  Total count of groups matching the query.
+	 * }
+	 */
+	public static function search_groups( $filter, $limit = null, $page = null, $sort_by = false, $order = false ) {
+		$args = array(
+			'search_terms' => $filter,
+			'per_page'     => $limit,
+			'page'         => $page,
+			'orderby'      => $sort_by,
+			'order'        => $order,
+		);
+
+		$groups = BP_Groups_Group::get( $args );
+
+		// Modify the results to match the old format.
+		$paged_groups = array();
+		$i = 0;
+		foreach ( $groups['groups'] as $group ) {
+			$paged_groups[ $i ] = new stdClass;
+			$paged_groups[ $i ]->group_id = $group->id;
+			$i++;
+		}
+
+		return array( 'groups' => $paged_groups, 'total' => $groups['total'] );
+	}
+
+	/**
+	 * Check for the existence of a slug.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $slug Slug to check.
+	 * @return string|null The slug, if found. Otherwise null.
+	 */
+	public static function check_slug( $slug ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM {$bp->groups->table_name} WHERE slug = %s", $slug ) );
+	}
+
+	/**
+	 * Get the slug for a given group ID.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return string|null The slug, if found. Otherwise null.
+	 */
+	public static function get_slug( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM {$bp->groups->table_name} WHERE id = %d", $group_id ) );
+	}
+
+	/**
+	 * Check whether a given group has any members.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return bool True if the group has members, otherwise false.
+	 */
+	public static function has_members( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$members = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d", $group_id ) );
+
+		if ( empty( $members ) )
+			return false;
+
+		return true;
+	}
+
+	/**
+	 * Check whether a group has outstanding membership requests.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return int|null The number of outstanding requests, or null if
+	 *                  none are found.
+	 */
+	public static function has_membership_requests( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0", $group_id ) );
+	}
+
+	/**
+	 * Get outstanding membership requests for a group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int      $group_id ID of the group.
+	 * @param int|null $limit    Optional. Max number of results to return.
+	 *                           Default: null (no limit).
+	 * @param int|null $page     Optional. Page offset of results returned. Default:
+	 *                           null (no limit).
+	 * @return array {
+	 *     @type array $requests The requested page of located requests.
+	 *     @type int   $total    Total number of requests outstanding for the
+	 *                           group.
+	 * }
+	 */
+	public static function get_membership_requests( $group_id, $limit = null, $page = null ) {
+		global $wpdb;
+
+		if ( !empty( $limit ) && !empty( $page ) ) {
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+		}
+
+		$bp = buddypress();
+
+		$paged_requests = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0 AND inviter_id = 0{$pag_sql}", $group_id ) );
+		$total_requests = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0 AND inviter_id = 0", $group_id ) );
+
+		return array( 'requests' => $paged_requests, 'total' => $total_requests );
+	}
+
+	/**
+	 * Query for groups.
+	 *
+	 * @see WP_Meta_Query::queries for a description of the 'meta_query'
+	 *      parameter format.
+	 *
+	 * @since 1.6.0
+	 * @since 2.6.0 Added `$group_type`, `$group_type__in`, and `$group_type__not_in` parameters.
+	 * @since 2.7.0 Added `$update_admin_cache` and `$parent_id` parameters.
+	 * @since 2.8.0 Changed `$search_terms` parameter handling and added `$search_columns` parameter.
+	 * @since 2.9.0 Added `$slug` parameter.
+	 *
+	 * @param array $args {
+	 *     Array of parameters. All items are optional.
+	 *     @type string       $type               Optional. Shorthand for certain orderby/order combinations.
+	 *                                            'newest', 'active', 'popular', 'alphabetical', 'random'.
+	 *                                            When present, will override orderby and order params.
+	 *                                            Default: null.
+	 *     @type string       $orderby            Optional. Property to sort by. 'date_created', 'last_activity',
+	 *                                            'total_member_count', 'name', 'random', 'meta_id'.
+	 *                                            Default: 'date_created'.
+	 *     @type string       $order              Optional. Sort order. 'ASC' or 'DESC'. Default: 'DESC'.
+	 *     @type int          $per_page           Optional. Number of items to return per page of results.
+	 *                                            Default: null (no limit).
+	 *     @type int          $page               Optional. Page offset of results to return.
+	 *                                            Default: null (no limit).
+	 *     @type int          $user_id            Optional. If provided, results will be limited to groups
+	 *                                            of which the specified user is a member. Default: null.
+ 	 *     @type array|string $slug               Optional. Array or comma-separated list of group slugs to limit
+ 	 *                                            results to.
+	 *                                            Default: false.
+	 *     @type string       $search_terms       Optional. If provided, only groups whose names or descriptions
+	 *                                            match the search terms will be returned. Allows specifying the
+	 *                                            wildcard position using a '*' character before or after the
+	 *                                            string or both. Works in concert with $search_columns.
+	 *                                            Default: false.
+  	 *     @type string       $search_columns     Optional. If provided, only apply the search terms to the
+  	 *                                            specified columns. Works in concert with $search_terms.
+  	 *                                            Default: empty array.
+	 *     @type array|string $group_type         Array or comma-separated list of group types to limit results to.
+	 *     @type array|string $group_type__in     Array or comma-separated list of group types to limit results to.
+	 *     @type array|string $group_type__not_in Array or comma-separated list of group types that will be
+	 *                                            excluded from results.
+	 *     @type array        $meta_query         Optional. An array of meta_query conditions.
+	 *                                            See {@link WP_Meta_Query::queries} for description.
+	 *     @type array|string $value              Optional. Array or comma-separated list of group IDs. Results
+	 *                                            will be limited to groups within the list. Default: false.
+	 *     @type array|string $parent_id          Optional. Array or comma-separated list of group IDs. Results
+	 *                                            will be limited to children of the specified groups. Default: null.
+	 *     @type array|string $exclude            Optional. Array or comma-separated list of group IDs.
+	 *                                            Results will exclude the listed groups. Default: false.
+	 *     @type bool         $update_meta_cache  Whether to pre-fetch groupmeta for the returned groups.
+	 *                                            Default: true.
+	 *     @type bool         $update_admin_cache Whether to pre-fetch administrator IDs for the returned
+	 *                                            groups. Default: false.
+	 *     @type bool         $show_hidden        Whether to include hidden groups in results. Default: false.
+ 	 *     @type array|string $status             Optional. Array or comma-separated list of group statuses to limit
+ 	 *                                            results to. If specified, $show_hidden is ignored.
+	 *                                            Default: empty array.
+ 	 *     @type string       $fields             Which fields to return. Specify 'ids' to fetch a list of IDs.
+ 	 *                                            Default: 'all' (return BP_Groups_Group objects).
+ 	 *                                            If set, meta and admin caches will not be prefetched.
+	 * }
+	 * @return array {
+	 *     @type array $groups Array of group objects returned by the
+	 *                         paginated query. (IDs only if `fields` is set to `ids`.)
+	 *     @type int   $total  Total count of all groups matching non-
+	 *                         paginated query params.
+	 * }
+	 */
+	public static function get( $args = array() ) {
+		global $wpdb;
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '1.7', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'type',
+				1 => 'per_page',
+				2 => 'page',
+				3 => 'user_id',
+				4 => 'search_terms',
+				5 => 'include',
+				6 => 'populate_extras',
+				7 => 'exclude',
+				8 => 'show_hidden',
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$defaults = array(
+			'type'               => null,
+			'orderby'            => 'date_created',
+			'order'              => 'DESC',
+			'per_page'           => null,
+			'page'               => null,
+			'user_id'            => 0,
+			'slug'               => array(),
+			'search_terms'       => false,
+			'search_columns'     => array(),
+			'group_type'         => '',
+			'group_type__in'     => '',
+			'group_type__not_in' => '',
+			'meta_query'         => false,
+			'include'            => false,
+			'parent_id'          => null,
+			'update_meta_cache'  => true,
+			'update_admin_cache' => false,
+			'exclude'            => false,
+			'show_hidden'        => false,
+			'status'             => array(),
+			'fields'             => 'all',
+		);
+
+		$r = bp_parse_args( $args, $defaults, 'bp_groups_group_get' );
+
+		$bp = buddypress();
+
+		$sql = array(
+			'select'     => "SELECT DISTINCT g.id",
+			'from'       => "{$bp->groups->table_name} g",
+			'where'      => '',
+			'orderby'    => '',
+			'pagination' => '',
+		);
+
+		if ( ! empty( $r['user_id'] ) ) {
+			$sql['from'] .= " JOIN {$bp->groups->table_name_members} m ON ( g.id = m.group_id )";
+		}
+
+		$where_conditions = array();
+
+		if ( ! empty( $r['status'] ) ) {
+			if ( ! is_array( $r['status'] ) ) {
+				$r['status'] = preg_split( '/[\s,]+/', $r['status'] );
+			}
+			$r['status'] = array_map( 'sanitize_title', $r['status'] );
+			$status_in = "'" . implode( "','", $r['status'] ) . "'";
+			$where_conditions['status'] = "g.status IN ({$status_in})";
+		} elseif ( empty( $r['show_hidden'] ) ) {
+			$where_conditions['hidden'] = "g.status != 'hidden'";
+		}
+
+		if ( ! empty( $r['slug'] ) ) {
+			if ( ! is_array( $r['slug'] ) ) {
+				$r['slug'] = preg_split( '/[\s,]+/', $r['slug'] );
+			}
+			$r['slug'] = array_map( 'sanitize_title', $r['slug'] );
+			$slug_in = "'" . implode( "','", $r['slug'] ) . "'";
+			$where_conditions['slug'] = "g.slug IN ({$slug_in})";
+		}
+
+		$search = '';
+		if ( isset( $r['search_terms'] ) ) {
+			$search = trim( $r['search_terms'] );
+		}
+
+		if ( $search ) {
+			$leading_wild = ( ltrim( $search, '*' ) != $search );
+			$trailing_wild = ( rtrim( $search, '*' ) != $search );
+			if ( $leading_wild && $trailing_wild ) {
+				$wild = 'both';
+			} elseif ( $leading_wild ) {
+				$wild = 'leading';
+			} elseif ( $trailing_wild ) {
+				$wild = 'trailing';
+			} else {
+				// Default is to wrap in wildcard characters.
+				$wild = 'both';
+			}
+			$search = trim( $search, '*' );
+
+			$searches = array();
+			$leading_wild = ( 'leading' == $wild || 'both' == $wild ) ? '%' : '';
+			$trailing_wild = ( 'trailing' == $wild || 'both' == $wild ) ? '%' : '';
+			$wildcarded = $leading_wild . bp_esc_like( $search ) . $trailing_wild;
+
+			$search_columns = array( 'name', 'description' );
+			if ( $r['search_columns'] ) {
+				$search_columns = array_intersect( $r['search_columns'], $search_columns );
+			}
+
+			foreach ( $search_columns as $search_column ) {
+				$searches[] = $wpdb->prepare( "$search_column LIKE %s", $wildcarded );
+			}
+
+			$where_conditions['search'] = '(' . implode(' OR ', $searches) . ')';
+		}
+
+		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
+
+		if ( ! empty( $meta_query_sql['join'] ) ) {
+			$sql['from'] .= $meta_query_sql['join'];
+		}
+
+		if ( ! empty( $meta_query_sql['where'] ) ) {
+			$where_conditions['meta'] = $meta_query_sql['where'];
+		}
+
+		// Only use 'group_type__in', if 'group_type' is not set.
+		if ( empty( $r['group_type'] ) && ! empty( $r['group_type__in']) ) {
+			$r['group_type'] = $r['group_type__in'];
+		}
+
+		// Group types to exclude. This has priority over inclusions.
+		if ( ! empty( $r['group_type__not_in'] ) ) {
+			$group_type_clause = self::get_sql_clause_for_group_types( $r['group_type__not_in'], 'NOT IN' );
+
+		// Group types to include.
+		} elseif ( ! empty( $r['group_type'] ) ) {
+			$group_type_clause = self::get_sql_clause_for_group_types( $r['group_type'], 'IN' );
+		}
+
+		if ( ! empty( $group_type_clause ) ) {
+			$where_conditions['group_type'] = $group_type_clause;
+		}
+
+		if ( ! empty( $r['user_id'] ) ) {
+			$where_conditions['user'] = $wpdb->prepare( "m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $r['user_id'] );
+		}
+
+		if ( ! empty( $r['include'] ) ) {
+			$include        = implode( ',', wp_parse_id_list( $r['include'] ) );
+			$where_conditions['include'] = "g.id IN ({$include})";
+		}
+
+		if ( ! is_null( $r['parent_id'] ) ) {
+			// Note that `wp_parse_id_list()` converts `false` to 0.
+			$parent_id        = implode( ',', wp_parse_id_list( $r['parent_id'] ) );
+			$where_conditions['parent_id'] = "g.parent_id IN ({$parent_id})";
+		}
+
+		if ( ! empty( $r['exclude'] ) ) {
+			$exclude        = implode( ',', wp_parse_id_list( $r['exclude'] ) );
+			$where_conditions['exclude'] = "g.id NOT IN ({$exclude})";
+		}
+
+		/* Order/orderby ********************************************/
+
+		$order   = $r['order'];
+		$orderby = $r['orderby'];
+
+		// If a 'type' parameter was passed, parse it and overwrite
+		// 'order' and 'orderby' params passed to the function.
+		if (  ! empty( $r['type'] ) ) {
+
+			/**
+			 * Filters the 'type' parameter used to overwrite 'order' and 'orderby' values.
+			 *
+			 * @since 2.1.0
+			 *
+			 * @param array  $value Converted 'type' value for order and orderby.
+			 * @param string $value Parsed 'type' value for the get method.
+			 */
+			$order_orderby = apply_filters( 'bp_groups_get_orderby', self::convert_type_to_order_orderby( $r['type'] ), $r['type'] );
+
+			// If an invalid type is passed, $order_orderby will be
+			// an array with empty values. In this case, we stick
+			// with the default values of $order and $orderby.
+			if ( ! empty( $order_orderby['order'] ) ) {
+				$order = $order_orderby['order'];
+			}
+
+			if ( ! empty( $order_orderby['orderby'] ) ) {
+				$orderby = $order_orderby['orderby'];
+			}
+		}
+
+		// 'total_member_count' and 'last_activity' sorts require additional table joins.
+		if ( 'total_member_count' === $orderby ) {
+			$sql['from'] .= " JOIN {$bp->groups->table_name_groupmeta} gm_total_member_count ON ( g.id = gm_total_member_count.group_id )";
+			$where_conditions['total_member_count'] = "gm_total_member_count.meta_key = 'total_member_count'";
+		} elseif ( 'last_activity' === $orderby ) {
+
+			$sql['from'] .= " JOIN {$bp->groups->table_name_groupmeta} gm_last_activity on ( g.id = gm_last_activity.group_id )";
+			$where_conditions['last_activity'] = "gm_last_activity.meta_key = 'last_activity'";
+		}
+
+		// If 'meta_id' is the requested order, and there's no meta query, fall back to the default.
+		if ( 'meta_id' === $orderby && empty( $meta_query_sql['join'] ) ) {
+			$orderby = 'date_created';
+		}
+
+		// Sanitize 'order'.
+		$order = bp_esc_sql_order( $order );
+
+		/**
+		 * Filters the converted 'orderby' term.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $value   Converted 'orderby' term.
+		 * @param string $orderby Original orderby value.
+		 * @param string $value   Parsed 'type' value for the get method.
+		 */
+		$orderby = apply_filters( 'bp_groups_get_orderby_converted_by_term', self::convert_orderby_to_order_by_term( $orderby ), $orderby, $r['type'] );
+
+		// Random order is a special case.
+		if ( 'rand()' === $orderby ) {
+			$sql['orderby'] = "ORDER BY rand()";
+		} else {
+			$sql['orderby'] = "ORDER BY {$orderby} {$order}";
+		}
+
+		if ( ! empty( $r['per_page'] ) && ! empty( $r['page'] ) && $r['per_page'] != -1 ) {
+			$sql['pagination'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $r['page'] - 1 ) * $r['per_page']), intval( $r['per_page'] ) );
+		}
+
+		$where = '';
+		if ( ! empty( $where_conditions ) ) {
+			$sql['where'] = implode( ' AND ', $where_conditions );
+			$where = "WHERE {$sql['where']}";
+		}
+
+		$paged_groups_sql = "{$sql['select']} FROM {$sql['from']} {$where} {$sql['orderby']} {$sql['pagination']}";
+
+		/**
+		 * Filters the pagination SQL statement.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Concatenated SQL statement.
+		 * @param array  $sql   Array of SQL parts before concatenation.
+		 * @param array  $r     Array of parsed arguments for the get method.
+		 */
+		$paged_groups_sql = apply_filters( 'bp_groups_get_paged_groups_sql', $paged_groups_sql, $sql, $r );
+
+		$cached = bp_core_get_incremented_cache( $paged_groups_sql, 'bp_groups' );
+		if ( false === $cached ) {
+			$paged_group_ids = $wpdb->get_col( $paged_groups_sql );
+			bp_core_set_incremented_cache( $paged_groups_sql, 'bp_groups', $paged_group_ids );
+		} else {
+			$paged_group_ids = $cached;
+		}
+
+		if ( 'ids' === $r['fields'] ) {
+			// We only want the IDs.
+			$paged_groups = array_map( 'intval', $paged_group_ids );
+		} else {
+			$uncached_group_ids = bp_get_non_cached_ids( $paged_group_ids, 'bp_groups' );
+			if ( $uncached_group_ids ) {
+				$group_ids_sql = implode( ',', array_map( 'intval', $uncached_group_ids ) );
+				$group_data_objects = $wpdb->get_results( "SELECT g.* FROM {$bp->groups->table_name} g WHERE g.id IN ({$group_ids_sql})" );
+				foreach ( $group_data_objects as $group_data_object ) {
+					wp_cache_set( $group_data_object->id, $group_data_object, 'bp_groups' );
+				}
+			}
+
+			$paged_groups = array();
+			foreach ( $paged_group_ids as $paged_group_id ) {
+				$paged_groups[] = new BP_Groups_Group( $paged_group_id );
+			}
+
+			$group_ids = array();
+			foreach ( (array) $paged_groups as $group ) {
+				$group_ids[] = $group->id;
+			}
+
+			// Grab all groupmeta.
+			if ( ! empty( $r['update_meta_cache'] ) ) {
+				bp_groups_update_meta_cache( $group_ids );
+			}
+
+			// Prefetch all administrator IDs, if requested.
+			if ( $r['update_admin_cache'] ) {
+				BP_Groups_Member::prime_group_admins_mods_cache( $group_ids );
+			}
+
+			// Set up integer properties needing casting.
+			$int_props = array(
+				'id', 'creator_id', 'enable_forum'
+			);
+
+			// Integer casting.
+			foreach ( $paged_groups as $key => $g ) {
+				foreach ( $int_props as $int_prop ) {
+					$paged_groups[ $key ]->{$int_prop} = (int) $paged_groups[ $key ]->{$int_prop};
+				}
+			}
+
+		}
+
+		// Find the total number of groups in the results set.
+		$total_groups_sql = "SELECT COUNT(DISTINCT g.id) FROM {$sql['from']} $where";
+
+		/**
+		 * Filters the SQL used to retrieve total group results.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $t_sql     Concatenated SQL statement used for retrieving total group results.
+		 * @param array  $total_sql Array of SQL parts for the query.
+		 * @param array  $r         Array of parsed arguments for the get method.
+		 */
+		$total_groups_sql = apply_filters( 'bp_groups_get_total_groups_sql', $total_groups_sql, $sql, $r );
+
+		$cached = bp_core_get_incremented_cache( $total_groups_sql, 'bp_groups' );
+		if ( false === $cached ) {
+			$total_groups = (int) $wpdb->get_var( $total_groups_sql );
+			bp_core_set_incremented_cache( $total_groups_sql, 'bp_groups', $total_groups );
+		} else {
+			$total_groups = (int) $cached;
+		}
+
+		return array( 'groups' => $paged_groups, 'total' => $total_groups );
+	}
+
+	/**
+	 * Get the SQL for the 'meta_query' param in BP_Activity_Activity::get()
+	 *
+	 * We use WP_Meta_Query to do the heavy lifting of parsing the
+	 * meta_query array and creating the necessary SQL clauses.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $meta_query An array of meta_query filters. See the
+	 *                          documentation for {@link WP_Meta_Query} for details.
+	 * @return array $sql_array 'join' and 'where' clauses.
+	 */
+	protected static function get_meta_query_sql( $meta_query = array() ) {
+		global $wpdb;
+
+		$sql_array = array(
+			'join'  => '',
+			'where' => '',
+		);
+
+		if ( ! empty( $meta_query ) ) {
+			$groups_meta_query = new WP_Meta_Query( $meta_query );
+
+			// WP_Meta_Query expects the table name at
+			// $wpdb->group.
+			$wpdb->groupmeta = buddypress()->groups->table_name_groupmeta;
+
+			$meta_sql = $groups_meta_query->get_sql( 'group', 'g', 'id' );
+			$sql_array['join']  = $meta_sql['join'];
+			$sql_array['where'] = self::strip_leading_and( $meta_sql['where'] );
+		}
+
+		return $sql_array;
+	}
+
+	/**
+	 * Convert the 'type' parameter to 'order' and 'orderby'.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $type The 'type' shorthand param.
+	 *
+	 * @return array {
+	 *     @type string $order   SQL-friendly order string.
+	 *     @type string $orderby SQL-friendly orderby column name.
+	 * }
+	 */
+	protected static function convert_type_to_order_orderby( $type = '' ) {
+		$order = $orderby = '';
+
+		switch ( $type ) {
+			case 'newest' :
+				$order   = 'DESC';
+				$orderby = 'date_created';
+				break;
+
+			case 'active' :
+				$order   = 'DESC';
+				$orderby = 'last_activity';
+				break;
+
+			case 'popular' :
+				$order   = 'DESC';
+				$orderby = 'total_member_count';
+				break;
+
+			case 'alphabetical' :
+				$order   = 'ASC';
+				$orderby = 'name';
+				break;
+
+			case 'random' :
+				$order   = '';
+				$orderby = 'random';
+				break;
+		}
+
+		return array( 'order' => $order, 'orderby' => $orderby );
+	}
+
+	/**
+	 * Convert the 'orderby' param into a proper SQL term/column.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $orderby Orderby term as passed to get().
+	 * @return string $order_by_term SQL-friendly orderby term.
+	 */
+	protected static function convert_orderby_to_order_by_term( $orderby ) {
+		$order_by_term = '';
+
+		switch ( $orderby ) {
+			case 'date_created' :
+			default :
+				$order_by_term = 'g.date_created';
+				break;
+
+			case 'last_activity' :
+				$order_by_term = 'gm_last_activity.meta_value';
+				break;
+
+			case 'total_member_count' :
+				$order_by_term = 'CONVERT(gm_total_member_count.meta_value, SIGNED)';
+				break;
+
+			case 'name' :
+				$order_by_term = 'g.name';
+				break;
+
+			case 'random' :
+				$order_by_term = 'rand()';
+				break;
+
+			case 'meta_id' :
+				$order_by_term = buddypress()->groups->table_name_groupmeta . '.id';
+				break;
+		}
+
+		return $order_by_term;
+	}
+
+	/**
+	 * Get a list of groups whose names start with a given letter.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string            $letter          The letter.
+	 * @param int|null          $limit           Optional. The max number of results to return.
+	 *                                           Default: null (no limit).
+	 * @param int|null          $page            Optional. The page offset of results to return.
+	 *                                           Default: null (no limit).
+	 * @param bool              $populate_extras Deprecated.
+	 * @param string|array|bool $exclude         Optional. Array or comma-separated list of group
+	 *                                           IDs to exclude from results.
+	 * @return false|array {
+	 *     @type array $groups Array of group objects returned by the
+	 *                         paginated query.
+	 *     @type int   $total  Total count of all groups matching non-
+	 *                         paginated query params.
+	 * }
+	 */
+	public static function get_by_letter( $letter, $limit = null, $page = null, $populate_extras = true, $exclude = false ) {
+		global $wpdb;
+
+		$pag_sql = $hidden_sql = $exclude_sql = '';
+
+		// Multibyte compliance.
+		if ( function_exists( 'mb_strlen' ) ) {
+			if ( mb_strlen( $letter, 'UTF-8' ) > 1 || is_numeric( $letter ) || !$letter ) {
+				return false;
+			}
+		} else {
+			if ( strlen( $letter ) > 1 || is_numeric( $letter ) || !$letter ) {
+				return false;
+			}
+		}
+
+		$args = array(
+			'per_page'       => $limit,
+			'page'           => $page,
+			'search_terms'   => $letter . '*',
+			'search_columns' => array( 'name' ),
+			'exclude'        => $exclude,
+		);
+
+		return BP_Groups_Group::get( $args );
+	}
+
+	/**
+	 * Get a list of random groups.
+	 *
+	 * Use BP_Groups_Group::get() with 'type' = 'random' instead.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int|null          $limit           Optional. The max number of results to return.
+	 *                                           Default: null (no limit).
+	 * @param int|null          $page            Optional. The page offset of results to return.
+	 *                                           Default: null (no limit).
+	 * @param int               $user_id         Optional. If present, groups will be limited to
+	 *                                           those of which the specified user is a member.
+	 * @param string|bool       $search_terms    Optional. Limit groups to those whose name
+	 *                                           or description field contain the search string.
+	 * @param bool              $populate_extras Optional. Whether to fetch extra
+	 *                                           information about the groups. Default: true.
+	 * @param string|array|bool $exclude         Optional. Array or comma-separated list of group
+	 *                                           IDs to exclude from results.
+	 * @return array {
+	 *     @type array $groups Array of group objects returned by the
+	 *                         paginated query.
+	 *     @type int   $total  Total count of all groups matching non-
+	 *                         paginated query params.
+	 * }
+	 */
+	public static function get_random( $limit = null, $page = null, $user_id = 0, $search_terms = false, $populate_extras = true, $exclude = false ) {
+		$args = array(
+			'type'               => 'random',
+			'per_page'           => $limit,
+			'page'               => $page,
+			'user_id'            => $user_id,
+			'search_terms'       => $search_terms,
+			'exclude'            => $exclude,
+		);
+
+		return BP_Groups_Group::get( $args );
+	}
+
+	/**
+	 * Fetch extra data for a list of groups.
+	 *
+	 * This method is used throughout the class, by methods that take a
+	 * $populate_extras parameter.
+	 *
+	 * Data fetched:
+	 *     - Logged-in user's status within each group (is_member,
+	 *       is_confirmed, is_pending, is_banned)
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array        $paged_groups Array of groups.
+	 * @param string|array $group_ids    Array or comma-separated list of IDs matching
+	 *                                   $paged_groups.
+	 * @param string|bool  $type         Not used.
+	 * @return array $paged_groups
+	 */
+	public static function get_group_extras( &$paged_groups, &$group_ids, $type = false ) {
+		$user_id = bp_loggedin_user_id();
+
+		foreach ( $paged_groups as &$group ) {
+			$group->is_member  = groups_is_user_member( $user_id, $group->id )  ? 1 : 0;
+			$group->is_invited = groups_is_user_invited( $user_id, $group->id ) ? 1 : 0;
+			$group->is_pending = groups_is_user_pending( $user_id, $group->id ) ? 1 : 0;
+			$group->is_banned  = (bool) groups_is_user_banned( $user_id, $group->id );
+		}
+
+		return $paged_groups;
+	}
+
+	/**
+	 * Delete all invitations to a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group whose invitations are being deleted.
+	 * @return int|null Number of rows records deleted on success, null on
+	 *                  failure.
+	 */
+	public static function delete_all_invites( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE group_id = %d AND invite_sent = 1", $group_id ) );
+	}
+
+	/**
+	 * Get a total group count for the site.
+	 *
+	 * Will include hidden groups in the count only if
+	 * bp_current_user_can( 'bp_moderate' ).
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return int Group count.
+	 */
+	public static function get_total_group_count() {
+		global $wpdb;
+
+		$hidden_sql = '';
+		if ( !bp_current_user_can( 'bp_moderate' ) )
+			$hidden_sql = "WHERE status != 'hidden'";
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->groups->table_name} {$hidden_sql}" );
+	}
+
+	/**
+	 * Get the member count for a group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id Group ID.
+	 * @return int Count of confirmed members for the group.
+	 */
+	public static function get_total_member_count( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 1 AND is_banned = 0", $group_id ) );
+	}
+
+	/**
+	 * Get an array containing ids for each group type.
+	 *
+	 * A bit of a kludge workaround for some issues
+	 * with bp_has_groups().
+	 *
+	 * @since 1.7.0
+	 *
+	 * @return array
+	 */
+	public static function get_group_type_ids() {
+		global $wpdb;
+
+		$bp  = buddypress();
+		$ids = array();
+
+		$ids['all']     = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name}" );
+		$ids['public']  = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'public'" );
+		$ids['private'] = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'private'" );
+		$ids['hidden']  = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'hidden'" );
+
+		return $ids;
+	}
+
+	/**
+	 * Get SQL clause for group type(s).
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string|array $group_types Group type(s).
+	 * @param  string       $operator    'IN' or 'NOT IN'.
+	 * @return string       $clause      SQL clause.
+	 */
+	protected static function get_sql_clause_for_group_types( $group_types, $operator ) {
+		global $wpdb;
+
+		// Sanitize operator.
+		if ( 'NOT IN' !== $operator ) {
+			$operator = 'IN';
+		}
+
+		// Parse and sanitize types.
+		if ( ! is_array( $group_types ) ) {
+			$group_types = preg_split( '/[,\s+]/', $group_types );
+		}
+
+		$types = array();
+		foreach ( $group_types as $gt ) {
+			if ( bp_groups_get_group_type_object( $gt ) ) {
+				$types[] = $gt;
+			}
+		}
+
+		$tax_query = new WP_Tax_Query( array(
+			array(
+				'taxonomy' => 'bp_group_type',
+				'field'    => 'name',
+				'operator' => $operator,
+				'terms'    => $types,
+			),
+		) );
+
+		$site_id  = bp_get_taxonomy_term_site_id( 'bp_group_type' );
+		$switched = false;
+		if ( $site_id !== get_current_blog_id() ) {
+			switch_to_blog( $site_id );
+			$switched = true;
+		}
+
+		$sql_clauses = $tax_query->get_sql( 'g', 'id' );
+
+		$clause = '';
+
+		// The no_results clauses are the same between IN and NOT IN.
+		if ( false !== strpos( $sql_clauses['where'], '0 = 1' ) ) {
+			$clause = self::strip_leading_and( $sql_clauses['where'] );
+
+		// The tax_query clause generated for NOT IN can be used almost as-is.
+		} elseif ( 'NOT IN' === $operator ) {
+			$clause = self::strip_leading_and( $sql_clauses['where'] );
+
+		// IN clauses must be converted to a subquery.
+		} elseif ( preg_match( '/' . $wpdb->term_relationships . '\.term_taxonomy_id IN \([0-9, ]+\)/', $sql_clauses['where'], $matches ) ) {
+			$clause = " g.id IN ( SELECT object_id FROM $wpdb->term_relationships WHERE {$matches[0]} )";
+		}
+
+		if ( $switched ) {
+			restore_current_blog();
+		}
+
+		return $clause;
+	}
+
+	/**
+	 * Strips the leading AND and any surrounding whitespace from a string.
+	 *
+	 * Used here to normalize SQL fragments generated by `WP_Meta_Query` and
+	 * other utility classes.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $s String.
+	 * @return string
+	 */
+	protected static function strip_leading_and( $s ) {
+		return preg_replace( '/^\s*AND\s*/', '', $s );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-invite-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-invite-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-invite-template.php	(revision 12265)
@@ -0,0 +1,271 @@
+<?php
+/**
+ * BuddyPress Groups Invitation template loop class.
+ *
+ * @package BuddyPress
+ * @since 1.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Group invitation template loop class.
+ *
+ * @since 1.1.0
+ */
+class BP_Groups_Invite_Template {
+
+	/**
+	 * @since 1.1.0
+	 * @var int
+	 */
+	public $current_invite = -1;
+
+	/**
+	 * @since 1.1.0
+	 * @var int
+	 */
+	public $invite_count;
+
+	/**
+	 * @since 1.1.0
+	 * @var array
+	 */
+	public $invites;
+
+	/**
+	 * @since 1.1.0
+	 * @var object
+	 */
+	public $invite;
+
+	/**
+	 * @since 1.1.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * @since 1.1.0
+	 * @var int
+	 */
+	public $pag_page;
+
+	/**
+	 * @since 1.1.0
+	 * @var int
+	 */
+	public $pag_num;
+
+	/**
+	 * @since 1.1.0
+	 * @var string
+	 */
+	public $pag_links;
+
+	/**
+	 * @since 1.1.0
+	 * @var int
+	 */
+	public $total_invite_count;
+
+	/**
+	 * BP_Groups_Invite_Template constructor.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args
+	 */
+	public function __construct( $args = array() ) {
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '2.0.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0  => 'user_id',
+				1  => 'group_id',
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$r = bp_parse_args( $args, array(
+			'page'     => 1,
+			'per_page' => 10,
+			'page_arg' => 'invitepage',
+			'user_id'  => bp_loggedin_user_id(),
+			'group_id' => bp_get_current_group_id(),
+		), 'groups_invite_template' );
+
+		$this->pag_arg  = sanitize_key( $r['page_arg'] );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+
+		$iquery = new BP_Group_Member_Query( array(
+			'group_id' => $r['group_id'],
+			'type'     => 'first_joined',
+			'per_page' => $this->pag_num,
+			'page'     => $this->pag_page,
+
+			// These filters ensure we get only pending invites.
+			'is_confirmed' => false,
+			'inviter_id'   => $r['user_id'],
+		) );
+
+		$this->invite_data        = $iquery->results;
+		$this->total_invite_count = $iquery->total_users;
+		$this->invites            = array_values( wp_list_pluck( $this->invite_data, 'ID' ) );
+		$this->invite_count       = count( $this->invites );
+
+		// If per_page is set to 0 (show all results), don't generate
+		// pag_links.
+		if ( ! empty( $this->pag_num ) ) {
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $this->pag_arg, '%#%' ),
+				'format'    => '',
+				'total'     => ceil( $this->total_invite_count / $this->pag_num ),
+				'current'   => $this->pag_page,
+				'prev_text' => '&larr;',
+				'next_text' => '&rarr;',
+				'mid_size'  => 1,
+				'add_args'  => array(),
+			) );
+		} else {
+			$this->pag_links = '';
+		}
+	}
+
+	/**
+	 * Whether or not there are invites to show.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return bool
+	 */
+	public function has_invites() {
+		if ( ! empty( $this->invite_count ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Increments up to the next invite to show.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return object
+	 */
+	public function next_invite() {
+		$this->current_invite++;
+		$this->invite = $this->invites[ $this->current_invite ];
+
+		return $this->invite;
+	}
+
+	/**
+	 * Rewinds to the first invite to show.
+	 *
+	 * @since 1.1.0
+	 */
+	public function rewind_invites() {
+		$this->current_invite = -1;
+		if ( $this->invite_count > 0 ) {
+			$this->invite = $this->invites[0];
+		}
+	}
+
+	/**
+	 * Finishes up the invites to show.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return bool
+	 */
+	public function invites() {
+		$tick = intval( $this->current_invite + 1 );
+		if ( $tick < $this->invite_count ) {
+			return true;
+		} elseif ( $tick == $this->invite_count ) {
+
+			/**
+			 * Fires right before the rewinding of invites list.
+			 *
+			 * @since 1.1.0
+			 * @since 2.3.0 `$this` parameter added.
+			 * @since 2.7.0 Action renamed from `loop_start`.
+			 *
+			 * @param BP_Groups_Invite_Template $this Instance of the current Invites template.
+			 */
+			do_action( 'group_invitation_loop_end', $this );
+
+			// Do some cleaning up after the loop
+			$this->rewind_invites();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Sets up the invite to show.
+	 *
+	 * @since 1.1.0
+	 */
+	public function the_invite() {
+		global $group_id;
+
+		$this->in_the_loop  = true;
+		$user_id            = $this->next_invite();
+
+		$this->invite       = new stdClass;
+		$this->invite->user = $this->invite_data[ $user_id ];
+
+		// This method previously populated the user object with
+		// BP_Core_User. We manually configure BP_Core_User data for
+		// backward compatibility.
+		if ( bp_is_active( 'xprofile' ) ) {
+			$this->invite->user->profile_data = BP_XProfile_ProfileData::get_all_for_user( $user_id );
+		}
+
+		$this->invite->user->avatar       = bp_core_fetch_avatar( array( 'item_id' => $user_id, 'type' => 'full',  'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->invite->user->fullname ) ) );
+		$this->invite->user->avatar_thumb = bp_core_fetch_avatar( array( 'item_id' => $user_id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->invite->user->fullname ) ) );
+		$this->invite->user->avatar_mini  = bp_core_fetch_avatar( array( 'item_id' => $user_id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->invite->user->fullname ), 'width' => 30, 'height' => 30 ) );
+		$this->invite->user->email        = $this->invite->user->user_email;
+		$this->invite->user->user_url     = bp_core_get_user_domain( $user_id, $this->invite->user->user_nicename, $this->invite->user->user_login );
+		$this->invite->user->user_link    = "<a href='{$this->invite->user->user_url}'>{$this->invite->user->fullname}</a>";
+		$this->invite->user->last_active  = bp_core_get_last_activity( $this->invite->user->last_activity, __( 'active %s', 'buddypress' ) );
+
+		if ( bp_is_active( 'groups' ) ) {
+			$total_groups = BP_Groups_Member::total_group_count( $user_id );
+			$this->invite->user->total_groups = sprintf( _n( '%d group', '%d groups', $total_groups, 'buddypress' ), $total_groups );
+		}
+
+		if ( bp_is_active( 'friends' ) ) {
+			$this->invite->user->total_friends = BP_Friends_Friendship::total_friend_count( $user_id );
+		}
+
+		$this->invite->user->total_blogs = null;
+
+		// Global'ed in bp_group_has_invites()
+		$this->invite->group_id = $group_id;
+
+		// loop has just started
+		if ( 0 == $this->current_invite ) {
+
+			/**
+			 * Fires if the current invite item is the first in the loop.
+			 *
+			 * @since 1.1.0
+			 * @since 2.3.0 `$this` parameter added.
+			 * @since 2.7.0 Action renamed from `loop_start`.
+			 *
+			 * @param BP_Groups_Invite_Template $this Instance of the current Invites template.
+			 */
+			do_action( 'group_invitation_loop_start', $this );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-list-table.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-list-table.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-list-table.php	(revision 12265)
@@ -0,0 +1,793 @@
+<?php
+/**
+ * BuddyPress Groups admin list table class.
+ *
+ * Props to WordPress core for the Comments admin screen, and its contextual
+ * help text, on which this implementation is heavily based.
+ *
+ * @package BuddyPress
+ * @subpackage Groups
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * List table class for the Groups component admin page.
+ *
+ * @since 1.7.0
+ */
+class BP_Groups_List_Table extends WP_List_Table {
+
+	/**
+	 * The type of view currently being displayed.
+	 *
+	 * E.g. "All", "Pending", "Approved", "Spam"...
+	 *
+	 * @since 1.7.0
+	 * @var string
+	 */
+	public $view = 'all';
+
+	/**
+	 * Group counts for each group type.
+	 *
+	 * @since 1.7.0
+	 * @var int
+	 */
+	public $group_counts = 0;
+
+	/**
+	 * Multidimensional array of group visibility (status) types and their groups.
+	 *
+	 * @link https://buddypress.trac.wordpress.org/ticket/6277
+	 * @var array
+	 */
+	public $group_type_ids = array();
+
+	/**
+	 * Constructor
+	 *
+	 * @since 1.7.0
+	 */
+	public function __construct() {
+
+		// Define singular and plural labels, as well as whether we support AJAX.
+		parent::__construct( array(
+			'ajax'     => false,
+			'plural'   => 'groups',
+			'singular' => 'group',
+		) );
+
+		// Add Group Type column and bulk change controls.
+		if ( bp_groups_get_group_types() ) {
+			// Add Group Type column.
+			add_filter( 'bp_groups_list_table_get_columns',        array( $this, 'add_type_column' )                  );
+			add_filter( 'bp_groups_admin_get_group_custom_column', array( $this, 'column_content_group_type' ), 10, 3 );
+			// Add the bulk change select.
+			add_action( 'bp_groups_list_table_after_bulk_actions', array( $this, 'add_group_type_bulk_change_select' ) );
+		}
+	}
+
+	/**
+	 * Set up items for display in the list table.
+	 *
+	 * Handles filtering of data, sorting, pagination, and any other data
+	 * manipulation required prior to rendering.
+	 *
+	 * @since 1.7.0
+	 */
+	public function prepare_items() {
+		global $groups_template;
+
+		$screen = get_current_screen();
+
+		// Option defaults.
+		$include_id   = false;
+		$search_terms = false;
+
+		// Set current page.
+		$page = $this->get_pagenum();
+
+		// Set per page from the screen options.
+		$per_page = $this->get_items_per_page( str_replace( '-', '_', "{$screen->id}_per_page" ) );
+
+		// Sort order.
+		$order = 'DESC';
+		if ( !empty( $_REQUEST['order'] ) ) {
+			$order = ( 'desc' == strtolower( $_REQUEST['order'] ) ) ? 'DESC' : 'ASC';
+		}
+
+		// Order by - default to newest.
+		$orderby = 'last_activity';
+		if ( ! empty( $_REQUEST['orderby'] ) ) {
+			switch ( $_REQUEST['orderby'] ) {
+				case 'name' :
+					$orderby = 'name';
+					break;
+				case 'id' :
+					$orderby = 'date_created';
+					break;
+				case 'members' :
+					$orderby = 'total_member_count';
+					break;
+				case 'last_active' :
+					$orderby = 'last_activity';
+					break;
+			}
+		}
+
+		// Are we doing a search?
+		if ( !empty( $_REQUEST['s'] ) )
+			$search_terms = $_REQUEST['s'];
+
+		// Check if user has clicked on a specific group (if so, fetch only that group).
+		if ( !empty( $_REQUEST['gid'] ) )
+			$include_id = (int) $_REQUEST['gid'];
+
+		// Set the current view.
+		if ( isset( $_GET['group_status'] ) && in_array( $_GET['group_status'], array( 'public', 'private', 'hidden' ) ) ) {
+			$this->view = $_GET['group_status'];
+		}
+
+		// We'll use the ids of group status types for the 'include' param.
+		$this->group_type_ids = BP_Groups_Group::get_group_type_ids();
+
+		// Pass a dummy array if there are no groups of this type.
+		$include = false;
+		if ( 'all' != $this->view && isset( $this->group_type_ids[ $this->view ] ) ) {
+			$include = ! empty( $this->group_type_ids[ $this->view ] ) ? $this->group_type_ids[ $this->view ] : array( 0 );
+		}
+
+		// Get group type counts for display in the filter tabs.
+		$this->group_counts = array();
+		foreach ( $this->group_type_ids as $group_type => $group_ids ) {
+			$this->group_counts[ $group_type ] = count( $group_ids );
+		}
+
+		// Group types
+		$group_type = false;
+		if ( isset( $_GET['bp-group-type'] ) && null !== bp_groups_get_group_type_object( $_GET['bp-group-type'] ) ) {
+			$group_type = $_GET['bp-group-type'];
+		}
+
+		// If we're viewing a specific group, flatten all activities into a single array.
+		if ( $include_id ) {
+			$groups = array( (array) groups_get_group( $include_id ) );
+		} else {
+			$groups_args = array(
+				'include'  => $include,
+				'per_page' => $per_page,
+				'page'     => $page,
+				'orderby'  => $orderby,
+				'order'    => $order
+			);
+
+			if ( $group_type ) {
+				$groups_args['group_type'] = $group_type;
+			}
+
+			$groups = array();
+			if ( bp_has_groups( $groups_args ) ) {
+				while ( bp_groups() ) {
+					bp_the_group();
+					$groups[] = (array) $groups_template->group;
+				}
+			}
+		}
+
+		// Set raw data to display.
+		$this->items = $groups;
+
+		// Store information needed for handling table pagination.
+		$this->set_pagination_args( array(
+			'per_page'    => $per_page,
+			'total_items' => $groups_template->total_group_count,
+			'total_pages' => ceil( $groups_template->total_group_count / $per_page )
+		) );
+	}
+
+	/**
+	 * Get an array of all the columns on the page.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @return array Array of column headers.
+	 */
+	public function get_column_info() {
+		$this->_column_headers = array(
+			$this->get_columns(),
+			array(),
+			$this->get_sortable_columns(),
+			$this->get_default_primary_column_name(),
+		);
+
+		return $this->_column_headers;
+	}
+
+	/**
+	 * Get name of default primary column
+	 *
+	 * @since 2.3.3
+	 *
+	 * @return string
+	 */
+	protected function get_default_primary_column_name() {
+		// Comment column is mapped to Group's name.
+		return 'comment';
+	}
+
+	/**
+	 * Display a message on screen when no items are found ("No groups found").
+	 *
+	 * @since 1.7.0
+	 */
+	public function no_items() {
+		_e( 'No groups found.', 'buddypress' );
+	}
+
+	/**
+	 * Output the Groups data table.
+	 *
+	 * @since 1.7.0
+	 */
+	public function display() {
+		$this->display_tablenav( 'top' ); ?>
+
+		<h2 class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Groups list', 'buddypress' );
+		?></h2>
+
+		<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>" cellspacing="0">
+			<thead>
+				<tr>
+					<?php $this->print_column_headers(); ?>
+				</tr>
+			</thead>
+
+			<tbody id="the-comment-list">
+				<?php $this->display_rows_or_placeholder(); ?>
+			</tbody>
+
+			<tfoot>
+				<tr>
+					<?php $this->print_column_headers( false ); ?>
+				</tr>
+			</tfoot>
+		</table>
+		<?php
+
+		$this->display_tablenav( 'bottom' );
+	}
+
+	/**
+	 * Extra controls to be displayed between bulk actions and pagination
+	 *
+	 * @since 2.7.0
+	 * @access protected
+	 *
+	 * @param string $which
+	 */
+	protected function extra_tablenav( $which ) {
+		/**
+		 * Fires just after the bulk action controls in the WP Admin groups list table.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
+		 */
+		do_action( 'bp_groups_list_table_after_bulk_actions', $which );
+	}
+
+	/**
+	 * Generate content for a single row of the table.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param object|array $item The current group item in the loop.
+	 */
+	public function single_row( $item = array() ) {
+		static $even = false;
+
+		$row_classes = array();
+
+		if ( $even ) {
+			$row_classes = array( 'even' );
+		} else {
+			$row_classes = array( 'alternate', 'odd' );
+		}
+
+		/**
+		 * Filters the classes applied to a single row in the groups list table.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param array  $row_classes Array of classes to apply to the row.
+		 * @param string $value       ID of the current group being displayed.
+		 */
+		$row_classes = apply_filters( 'bp_groups_admin_row_class', $row_classes, $item['id'] );
+		$row_class = ' class="' . implode( ' ', $row_classes ) . '"';
+
+		echo '<tr' . $row_class . ' id="group-' . esc_attr( $item['id'] ) . '" data-parent_id="' . esc_attr( $item['id'] ) . '" data-root_id="' . esc_attr( $item['id'] ) . '">';
+		echo $this->single_row_columns( $item );
+		echo '</tr>';
+
+		$even = ! $even;
+	}
+
+	/**
+	 * Get the list of views available on this table (e.g. "all", "public").
+	 *
+	 * @since 1.7.0
+	 */
+	public function get_views() {
+		$url_base = bp_get_admin_url( 'admin.php?page=bp-groups' ); ?>
+
+		<h2 class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Filter groups list', 'buddypress' );
+		?></h2>
+
+		<ul class="subsubsub">
+			<li class="all"><a href="<?php echo esc_url( $url_base ); ?>" class="<?php if ( 'all' == $this->view ) echo 'current'; ?>"><?php _e( 'All', 'buddypress' ); ?></a> |</li>
+			<li class="public"><a href="<?php echo esc_url( add_query_arg( 'group_status', 'public', $url_base ) ); ?>" class="<?php if ( 'public' == $this->view ) echo 'current'; ?>"><?php printf( _n( 'Public <span class="count">(%s)</span>', 'Public <span class="count">(%s)</span>', $this->group_counts['public'], 'buddypress' ), number_format_i18n( $this->group_counts['public'] ) ); ?></a> |</li>
+			<li class="private"><a href="<?php echo esc_url( add_query_arg( 'group_status', 'private', $url_base ) ); ?>" class="<?php if ( 'private' == $this->view ) echo 'current'; ?>"><?php printf( _n( 'Private <span class="count">(%s)</span>', 'Private <span class="count">(%s)</span>', $this->group_counts['private'], 'buddypress' ), number_format_i18n( $this->group_counts['private'] ) ); ?></a> |</li>
+			<li class="hidden"><a href="<?php echo esc_url( add_query_arg( 'group_status', 'hidden', $url_base ) ); ?>" class="<?php if ( 'hidden' == $this->view ) echo 'current'; ?>"><?php printf( _n( 'Hidden <span class="count">(%s)</span>', 'Hidden <span class="count">(%s)</span>', $this->group_counts['hidden'], 'buddypress' ), number_format_i18n( $this->group_counts['hidden'] ) ); ?></a></li>
+
+			<?php
+
+			/**
+			 * Fires inside listing of views so plugins can add their own.
+			 *
+			 * @since 1.7.0
+			 *
+			 * @param string $url_base Current URL base for view.
+			 * @param string $view     Current view being displayed.
+			 */
+			do_action( 'bp_groups_list_table_get_views', $url_base, $this->view ); ?>
+		</ul>
+	<?php
+	}
+
+	/**
+	 * Get bulk actions for single group row.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @return array Key/value pairs for the bulk actions dropdown.
+	 */
+	public function get_bulk_actions() {
+
+		/**
+		 * Filters the list of bulk actions to display on a single group row.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array $value Array of bulk actions to display.
+		 */
+		return apply_filters( 'bp_groups_list_table_get_bulk_actions', array(
+			'delete' => __( 'Delete', 'buddypress' )
+		) );
+	}
+
+	/**
+	 * Get the table column titles.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @return array Array of column titles.
+	 */
+	public function get_columns() {
+
+		/**
+		 * Filters the titles for the columns for the groups list table.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $value Array of slugs and titles for the columns.
+		 */
+		return apply_filters( 'bp_groups_list_table_get_columns', array(
+			'cb'          => '<input name type="checkbox" />',
+			'comment'     => _x( 'Name', 'Groups admin Group Name column header',               'buddypress' ),
+			'description' => _x( 'Description', 'Groups admin Group Description column header', 'buddypress' ),
+			'status'      => _x( 'Status', 'Groups admin Privacy Status column header',         'buddypress' ),
+			'members'     => _x( 'Members', 'Groups admin Members column header',               'buddypress' ),
+			'last_active' => _x( 'Last Active', 'Groups admin Last Active column header',       'buddypress' )
+		) );
+	}
+
+	/**
+	 * Get the column names for sortable columns.
+	 *
+	 * Note: It's not documented in WP, but the second item in the
+	 * nested arrays below is $desc_first. Normally, we would set
+	 * last_active to be desc_first (since you're generally interested in
+	 * the *most* recently active group, not the *least*). But because
+	 * the default sort for the Groups admin screen is DESC by last_active,
+	 * we want the first click on the Last Active column header to switch
+	 * the sort order - ie, to make it ASC. Thus last_active is set to
+	 * $desc_first = false.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @return array Array of sortable column names.
+	 */
+	public function get_sortable_columns() {
+		return array(
+			'gid'         => array( 'gid', false ),
+			'comment'     => array( 'name', false ),
+			'members'     => array( 'members', false ),
+			'last_active' => array( 'last_active', false ),
+		);
+	}
+
+	/**
+	 * Override WP_List_Table::row_actions().
+	 *
+	 * Basically a duplicate of the row_actions() method, but removes the
+	 * unnecessary <button> addition.
+	 *
+	 * @since 2.3.3
+	 * @since 2.3.4 Visibility set to public for compatibility with WP < 4.0.0.
+	 *
+	 * @param array $actions        The list of actions.
+	 * @param bool  $always_visible Whether the actions should be always visible.
+	 * @return string
+	 */
+	public function row_actions( $actions, $always_visible = false ) {
+		$action_count = count( $actions );
+		$i = 0;
+
+		if ( !$action_count )
+			return '';
+
+		$out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
+		foreach ( $actions as $action => $link ) {
+			++$i;
+			( $i == $action_count ) ? $sep = '' : $sep = ' | ';
+			$out .= "<span class='$action'>$link$sep</span>";
+		}
+		$out .= '</div>';
+
+		return $out;
+	}
+
+	/**
+	 * Markup for the Checkbox column.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	public function column_cb( $item = array() ) {
+		/* translators: accessibility text */
+		printf( '<label class="screen-reader-text" for="gid-%1$d">' . __( 'Select group %1$d', 'buddypress' ) . '</label><input type="checkbox" name="gid[]" value="%1$d" id="gid-%1$d" />', $item['id'] );
+	}
+
+	/**
+	 * Markup for the Group ID column.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	public function column_gid( $item = array() ) {
+		echo '<strong>' . absint( $item['id'] ) . '</strong>';
+	}
+
+	/**
+	 * Name column, and "quick admin" rollover actions.
+	 *
+	 * Called "comment" in the CSS so we can re-use some WP core CSS.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @see WP_List_Table::single_row_columns()
+	 *
+	 * @param array $item A singular item (one full row).
+	 */
+	public function column_comment( $item = array() ) {
+
+		// Preorder items: Edit | Delete | View.
+		$actions = array(
+			'edit'   => '',
+			'delete' => '',
+			'view'   => '',
+		);
+
+		// We need the group object for some BP functions.
+		$item_obj = (object) $item;
+
+		// Build actions URLs.
+		$base_url   = bp_get_admin_url( 'admin.php?page=bp-groups&amp;gid=' . $item['id'] );
+		$delete_url = wp_nonce_url( $base_url . "&amp;action=delete", 'bp-groups-delete' );
+		$edit_url   = $base_url . '&amp;action=edit';
+		$view_url   = bp_get_group_permalink( $item_obj );
+
+		/**
+		 * Filters the group name for a group's column content.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $value Name of the group being rendered.
+		 * @param array  $item  Array for the current group item.
+		 */
+		$group_name = apply_filters_ref_array( 'bp_get_group_name', array( $item['name'], $item ) );
+
+		// Rollover actions.
+		// Edit.
+		$actions['edit']   = sprintf( '<a href="%s">%s</a>', esc_url( $edit_url   ), __( 'Edit',   'buddypress' ) );
+
+		// Delete.
+		$actions['delete'] = sprintf( '<a href="%s">%s</a>', esc_url( $delete_url ), __( 'Delete', 'buddypress' ) );
+
+		// Visit.
+		$actions['view']   = sprintf( '<a href="%s">%s</a>', esc_url( $view_url   ), __( 'View',   'buddypress' ) );
+
+		/**
+		 * Filters the actions that will be shown for the column content.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param array $value Array of actions to be displayed for the column content.
+		 * @param array $item  The current group item in the loop.
+		 */
+		$actions = apply_filters( 'bp_groups_admin_comment_row_actions', array_filter( $actions ), $item );
+
+		// Get group name and avatar.
+		$avatar = '';
+
+		if ( buddypress()->avatar->show_avatars ) {
+			$avatar  = bp_core_fetch_avatar( array(
+				'item_id'    => $item['id'],
+				'object'     => 'group',
+				'type'       => 'thumb',
+				'avatar_dir' => 'group-avatars',
+				'alt'        => sprintf( __( 'Group logo of %s', 'buddypress' ), $group_name ),
+				'width'      => '32',
+				'height'     => '32',
+				'title'      => $group_name
+			) );
+		}
+
+		$content = sprintf( '<strong><a href="%s">%s</a></strong>', esc_url( $edit_url ), $group_name );
+
+		echo $avatar . ' ' . $content . ' ' . $this->row_actions( $actions );
+	}
+
+	/**
+	 * Markup for the Description column.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $item Information about the current row.
+	 */
+	public function column_description( $item = array() ) {
+
+		/**
+		 * Filters the markup for the Description column.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Markup for the Description column.
+		 * @param array  $item  The current group item in the loop.
+		 */
+		echo apply_filters_ref_array( 'bp_get_group_description', array( $item['description'], $item ) );
+	}
+
+	/**
+	 * Markup for the Status column.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $item Information about the current row.
+	 */
+	public function column_status( $item = array() ) {
+		$status      = $item['status'];
+		$status_desc = '';
+
+		// @todo This should be abstracted out somewhere for the whole
+		// Groups component.
+		switch ( $status ) {
+			case 'public' :
+				$status_desc = __( 'Public', 'buddypress' );
+				break;
+			case 'private' :
+				$status_desc = __( 'Private', 'buddypress' );
+				break;
+			case 'hidden' :
+				$status_desc = __( 'Hidden', 'buddypress' );
+				break;
+		}
+
+		/**
+		 * Filters the markup for the Status column.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $status_desc Markup for the Status column.
+		 * @parma array  $item        The current group item in the loop.
+		 */
+		echo apply_filters_ref_array( 'bp_groups_admin_get_group_status', array( $status_desc, $item ) );
+	}
+
+	/**
+	 * Markup for the Number of Members column.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $item Information about the current row.
+	 */
+	public function column_members( $item = array() ) {
+		$count = groups_get_groupmeta( $item['id'], 'total_member_count' );
+
+		/**
+		 * Filters the markup for the number of Members column.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param int   $count Markup for the number of Members column.
+		 * @parma array $item  The current group item in the loop.
+		 */
+		echo apply_filters_ref_array( 'bp_groups_admin_get_group_member_count', array( (int) $count, $item ) );
+	}
+
+	/**
+	 * Markup for the Last Active column.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $item Information about the current row.
+	 */
+	public function column_last_active( $item = array() ) {
+		$last_active = groups_get_groupmeta( $item['id'], 'last_activity' );
+
+		/**
+		 * Filters the markup for the Last Active column.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $last_active Markup for the Last Active column.
+		 * @parma array  $item        The current group item in the loop.
+		 */
+		echo apply_filters_ref_array( 'bp_groups_admin_get_group_last_active', array( $last_active, $item ) );
+	}
+
+	/**
+	 * Allow plugins to add their custom column.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array  $item        Information about the current row.
+	 * @param string $column_name The column name.
+	 * @return string
+	 */
+	public function column_default( $item = array(), $column_name = '' ) {
+
+		/**
+		 * Filters a string to allow plugins to add custom column content.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $value       Empty string.
+		 * @param string $column_name Name of the column being rendered.
+		 * @param array  $item        The current group item in the loop.
+		 */
+		return apply_filters( 'bp_groups_admin_get_group_custom_column', '', $column_name, $item );
+	}
+
+	// Group Types
+
+	/**
+	 * Add group type column to the WordPress admin groups list table.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $columns Groups table columns.
+	 *
+	 * @return array $columns
+	 */
+	public function add_type_column( $columns = array() ) {
+		$columns['bp_group_type'] = _x( 'Group Type', 'Label for the WP groups table group type column', 'buddypress' );
+
+		return $columns;
+	}
+
+	/**
+	 * Markup for the Group Type column.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $retval      Empty string.
+	 * @param string $column_name Name of the column being rendered.
+	 * @param array  $item        The current group item in the loop.
+	 * @return string
+	 */
+	public function column_content_group_type( $retval = '', $column_name, $item ) {
+		if ( 'bp_group_type' !== $column_name ) {
+			return $retval;
+		}
+
+		add_filter( 'bp_get_group_type_directory_permalink', array( $this, 'group_type_permalink_use_admin_filter' ), 10, 2 );
+		$retval = bp_get_group_type_list( $item['id'], array(
+			'parent_element' => '',
+			'label_element'  => '',
+			'label'          => '',
+			'show_all'       => true
+		) );
+		remove_filter( 'bp_get_group_type_directory_permalink', array( $this, 'group_type_permalink_use_admin_filter' ), 10 );
+
+		/**
+		 * Filters the markup for the Group Type column.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string $retval Markup for the Group Type column.
+		 * @parma array  $item   The current group item in the loop.
+		 */
+		echo apply_filters_ref_array( 'bp_groups_admin_get_group_type_column', array( $retval, $item ) );
+	}
+
+	/**
+	 * Filters the group type list permalink in the Group Type column.
+	 *
+	 * Changes the group type permalink to use the admin URL.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param  string $retval Current group type permalink.
+	 * @param  object $type   Group type object.
+	 * @return string
+	 */
+	public function group_type_permalink_use_admin_filter( $retval, $type ) {
+		return add_query_arg( array( 'bp-group-type' => urlencode( $type->name ) ) );
+	}
+
+	/**
+	 * Markup for the Group Type bulk change select.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
+	 */
+	public function add_group_type_bulk_change_select( $which ) {
+		// `$which` is only passed in WordPress 4.6+. Avoid duplicating controls in earlier versions.
+		static $displayed = false;
+		if ( version_compare( bp_get_major_wp_version(), '4.6', '<' ) && $displayed ) {
+			return;
+		}
+		$displayed = true;
+		$id_name = 'bottom' === $which ? 'bp_change_type2' : 'bp_change_type';
+
+		$types = bp_groups_get_group_types( array(), 'objects' );
+		?>
+		<div class="alignleft actions">
+			<label class="screen-reader-text" for="<?php echo $id_name; ?>"><?php _e( 'Change group type to&hellip;', 'buddypress' ) ?></label>
+			<select name="<?php echo $id_name; ?>" id="<?php echo $id_name; ?>" style="display:inline-block;float:none;">
+				<option value=""><?php _e( 'Change group type to&hellip;', 'buddypress' ) ?></option>
+
+				<?php foreach( $types as $type ) : ?>
+
+					<option value="<?php echo esc_attr( $type->name ); ?>"><?php echo esc_html( $type->labels['singular_name'] ); ?></option>
+
+				<?php endforeach; ?>
+
+				<option value="remove_group_type"><?php _e( 'No Group Type', 'buddypress' ) ?></option>
+
+			</select>
+			<?php
+			wp_nonce_field( 'bp-bulk-groups-change-type-' . bp_loggedin_user_id(), 'bp-bulk-groups-change-type-nonce' );
+			submit_button( __( 'Change', 'buddypress' ), 'button', 'bp_change_group_type', false );
+		?>
+		</div>
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-member-suggestions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-member-suggestions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-member-suggestions.php	(revision 12265)
@@ -0,0 +1,173 @@
+<?php
+/**
+ * BuddyPress Groups Classes.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsClasses
+ * @since 2.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Adds support for user at-mentions (for users in a specific Group) to the Suggestions API.
+ *
+ * @since 2.1.0
+ */
+class BP_Groups_Member_Suggestions extends BP_Members_Suggestions {
+
+	/**
+	 * Default arguments for this suggestions service.
+	 *
+	 * @since 2.1.0
+	 * @var array $args {
+	 *     @type int    $group_id     Positive integers will restrict the search to members in that group.
+	 *                                Negative integers will restrict the search to members in every other group.
+	 *     @type int    $limit        Maximum number of results to display. Default: 16.
+	 *     @type bool   $only_friends If true, only match the current user's friends. Default: false.
+	 *     @type string $term         The suggestion service will try to find results that contain this string.
+	 *                                Mandatory.
+	 * }
+	 */
+	protected $default_args = array(
+		'group_id'     => 0,
+		'limit'        => 16,
+		'only_friends' => false,
+		'term'         => '',
+		'type'         => '',
+	);
+
+
+	/**
+	 * Validate and sanitise the parameters for the suggestion service query.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return true|WP_Error If validation fails, return a WP_Error object. On success, return true (bool).
+	 */
+	public function validate() {
+		$this->args['group_id'] = (int) $this->args['group_id'];
+
+		/**
+		 * Filters the arguments used to validate and sanitize suggestion service query.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array                        $args  Array of arguments for the suggestion service query.
+		 * @param BP_Groups_Member_Suggestions $this  Instance of the current suggestion class.
+		 */
+		$this->args             = apply_filters( 'bp_groups_member_suggestions_args', $this->args, $this );
+
+		// Check for invalid or missing mandatory parameters.
+		if ( ! $this->args['group_id'] || ! bp_is_active( 'groups' ) ) {
+			return new WP_Error( 'missing_requirement' );
+		}
+
+		// Check that the specified group_id exists, and that the current user can access it.
+		$the_group = groups_get_group( absint( $this->args['group_id'] ) );
+
+		if ( $the_group->id === 0 || ! $the_group->user_has_access ) {
+			return new WP_Error( 'access_denied' );
+		}
+
+		/**
+		 * Filters the validation results for the suggestion service query.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param bool|WP_Error                $value True if valid, WP_Error if not.
+		 * @param BP_Groups_Member_Suggestions $this  Instance of the current suggestion class.
+		 */
+		return apply_filters( 'bp_groups_member_suggestions_validate_args', parent::validate(), $this );
+	}
+
+	/**
+	 * Find and return a list of username suggestions that match the query.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return array|WP_Error Array of results. If there were problems, returns a WP_Error object.
+	 */
+	public function get_suggestions() {
+		$user_query = array(
+			'count_total'     => '',  // Prevents total count.
+			'type'            => 'alphabetical',
+
+			'group_role'      => array( 'admin', 'member', 'mod' ),
+			'page'            => 1,
+			'per_page'        => $this->args['limit'],
+			'search_terms'    => $this->args['term'],
+			'search_wildcard' => 'right',
+		);
+
+		// Only return matches of friends of this user.
+		if ( $this->args['only_friends'] && is_user_logged_in() ) {
+			$user_query['user_id'] = get_current_user_id();
+		}
+
+		// Positive Group IDs will restrict the search to members in that group.
+		if ( $this->args['group_id'] > 0 ) {
+			$user_query['group_id'] = $this->args['group_id'];
+
+		// Negative Group IDs will restrict the search to members in every other group.
+		} else {
+			$group_query = array(
+				'count_total'     => '',  // Prevents total count.
+				'type'            => 'alphabetical',
+
+				'group_id'        => absint( $this->args['group_id'] ),
+				'group_role'      => array( 'admin', 'member', 'mod' ),
+				'page'            => 1,
+			);
+			$group_users = new BP_Group_Member_Query( $group_query );
+
+			if ( $group_users->results ) {
+				$user_query['exclude'] = wp_list_pluck( $group_users->results, 'ID' );
+			} else {
+				$user_query['include'] = array( 0 );
+			}
+		}
+
+		/**
+		 * Filters the arguments for the user query for the Suggestion API.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array                        $user_query Array of arguments for the query.
+		 * @param BP_Groups_Member_Suggestions $this       Instance of the current suggestion class.
+		 */
+		$user_query = apply_filters( 'bp_groups_member_suggestions_query_args', $user_query, $this );
+		if ( is_wp_error( $user_query ) ) {
+			return $user_query;
+		}
+
+
+		if ( isset( $user_query['group_id'] ) ) {
+			$user_query = new BP_Group_Member_Query( $user_query );
+		} else {
+			$user_query = new BP_User_Query( $user_query );
+		}
+
+		$results = array();
+		foreach ( $user_query->results as $user ) {
+			$result          = new stdClass();
+			$result->ID      = $user->user_nicename;
+			$result->image   = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
+			$result->name    = bp_core_get_user_displayname( $user->ID );
+			$result->user_id = $user->ID;
+
+			$results[] = $result;
+		}
+
+		/**
+		 * Filters the results of the member suggestions user query.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array                        $results Array of member suggestions.
+		 * @param BP_Groups_Member_Suggestions $this    Instance of the current suggestion class.
+		 */
+		return apply_filters( 'bp_groups_member_suggestions_get_suggestions', $results, $this );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-member.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-member.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-member.php	(revision 12265)
@@ -0,0 +1,1453 @@
+<?php
+/**
+ * BuddyPress Groups Classes.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsClasses
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Group Membership object.
+ */
+class BP_Groups_Member {
+
+	/**
+	 * ID of the membership.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $id;
+
+	/**
+	 * ID of the group associated with the membership.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $group_id;
+
+	/**
+	 * ID of the user associated with the membership.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $user_id;
+
+	/**
+	 * ID of the user whose invitation initiated the membership.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $inviter_id;
+
+	/**
+	 * Whether the member is an admin of the group.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $is_admin;
+
+	/**
+	 * Whether the member is a mod of the group.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $is_mod;
+
+	/**
+	 * Whether the member is banned from the group.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $is_banned;
+
+	/**
+	 * Title used to describe the group member's role in the group.
+	 *
+	 * Eg, 'Group Admin'.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $user_title;
+
+	/**
+	 * Last modified date of the membership.
+	 *
+	 * This value is updated when, eg, invitations are accepted.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	var $date_modified;
+
+	/**
+	 * Whether the membership has been confirmed.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $is_confirmed;
+
+	/**
+	 * Comments associated with the membership.
+	 *
+	 * In BP core, these are limited to the optional message users can
+	 * include when requesting membership to a private group.
+	 *
+	 * @since 1.6.0
+	 * @var string
+	 */
+	var $comments;
+
+	/**
+	 * Whether an invitation has been sent for this membership.
+	 *
+	 * The purpose of this flag is to mark when an invitation has been
+	 * "drafted" (the user has been added via the interface at Send
+	 * Invites), but the Send button has not been pressed, so the
+	 * invitee has not yet been notified.
+	 *
+	 * @since 1.6.0
+	 * @var int
+	 */
+	var $invite_sent;
+
+	/**
+	 * WP_User object representing the membership's user.
+	 *
+	 * @since 1.6.0
+	 * @var WP_User
+	 */
+	protected $user;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int      $user_id  Optional. Along with $group_id, can be used to
+	 *                           look up a membership.
+	 * @param int      $group_id Optional. Along with $user_id, can be used to
+	 *                           look up a membership.
+	 * @param int|bool $id       Optional. The unique ID of the membership object.
+	 * @param bool     $populate Whether to populate the properties of the
+	 *                           located membership. Default: true.
+	 */
+	public function __construct( $user_id = 0, $group_id = 0, $id = false, $populate = true ) {
+
+		// User and group are not empty, and ID is.
+		if ( !empty( $user_id ) && !empty( $group_id ) && empty( $id ) ) {
+			$this->user_id  = $user_id;
+			$this->group_id = $group_id;
+
+			if ( !empty( $populate ) ) {
+				$this->populate();
+			}
+		}
+
+		// ID is not empty.
+		if ( !empty( $id ) ) {
+			$this->id = $id;
+
+			if ( !empty( $populate ) ) {
+				$this->populate();
+			}
+		}
+	}
+
+	/**
+	 * Populate the object's properties.
+	 *
+	 * @since 1.6.0
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( $this->user_id && $this->group_id && !$this->id )
+			$sql = $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $this->user_id, $this->group_id );
+
+		if ( !empty( $this->id ) )
+			$sql = $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE id = %d", $this->id );
+
+		$member = $wpdb->get_row($sql);
+
+		if ( !empty( $member ) ) {
+			$this->id            = (int) $member->id;
+			$this->group_id      = (int) $member->group_id;
+			$this->user_id       = (int) $member->user_id;
+			$this->inviter_id    = (int) $member->inviter_id;
+			$this->is_admin      = (int) $member->is_admin;
+			$this->is_mod        = (int) $member->is_mod;
+			$this->is_banned     = (int) $member->is_banned;
+			$this->user_title    = $member->user_title;
+			$this->date_modified = $member->date_modified;
+			$this->is_confirmed  = (int) $member->is_confirmed;
+			$this->comments      = $member->comments;
+			$this->invite_sent   = (int) $member->invite_sent;
+		}
+	}
+
+	/**
+	 * Magic getter.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param string $key Key.
+	 * @return BP_Core_User|null
+	 */
+	public function __get( $key ) {
+		switch ( $key ) {
+			case 'user' :
+				return $this->get_user_object( $this->user_id );
+		}
+	}
+
+	/**
+	 * Magic issetter.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param string $key Key.
+	 * @return bool
+	 */
+	public function __isset( $key ) {
+		switch ( $key ) {
+			case 'user' :
+				return true;
+
+			default :
+				return isset( $this->{$key} );
+		}
+	}
+
+	/**
+	 * Get the user object corresponding to this membership.
+	 *
+	 * Used for lazyloading the protected `user` property.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @return BP_Core_User
+	 */
+	protected function get_user_object() {
+		if ( empty( $this->user ) ) {
+			$this->user = new BP_Core_User( $this->user_id );
+		}
+
+		return $this->user;
+	}
+
+	/**
+	 * Save the membership data to the database.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->user_id       = apply_filters( 'groups_member_user_id_before_save',       $this->user_id,       $this->id );
+		$this->group_id      = apply_filters( 'groups_member_group_id_before_save',      $this->group_id,      $this->id );
+		$this->inviter_id    = apply_filters( 'groups_member_inviter_id_before_save',    $this->inviter_id,    $this->id );
+		$this->is_admin      = apply_filters( 'groups_member_is_admin_before_save',      $this->is_admin,      $this->id );
+		$this->is_mod        = apply_filters( 'groups_member_is_mod_before_save',        $this->is_mod,        $this->id );
+		$this->is_banned     = apply_filters( 'groups_member_is_banned_before_save',     $this->is_banned,     $this->id );
+		$this->user_title    = apply_filters( 'groups_member_user_title_before_save',    $this->user_title,    $this->id );
+		$this->date_modified = apply_filters( 'groups_member_date_modified_before_save', $this->date_modified, $this->id );
+		$this->is_confirmed  = apply_filters( 'groups_member_is_confirmed_before_save',  $this->is_confirmed,  $this->id );
+		$this->comments      = apply_filters( 'groups_member_comments_before_save',      $this->comments,      $this->id );
+		$this->invite_sent   = apply_filters( 'groups_member_invite_sent_before_save',   $this->invite_sent,   $this->id );
+
+		/**
+		 * Fires before the current group membership item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Groups_Member $this Current instance of the group membership item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'groups_member_before_save', array( &$this ) );
+
+		// The following properties are required; bail if not met.
+		if ( empty( $this->user_id ) || empty( $this->group_id ) ) {
+			return false;
+		}
+
+		if ( !empty( $this->id ) ) {
+			$sql = $wpdb->prepare( "UPDATE {$bp->groups->table_name_members} SET inviter_id = %d, is_admin = %d, is_mod = %d, is_banned = %d, user_title = %s, date_modified = %s, is_confirmed = %d, comments = %s, invite_sent = %d WHERE id = %d", $this->inviter_id, $this->is_admin, $this->is_mod, $this->is_banned, $this->user_title, $this->date_modified, $this->is_confirmed, $this->comments, $this->invite_sent, $this->id );
+		} else {
+			// Ensure that user is not already a member of the group before inserting.
+			if ( $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 1 LIMIT 1", $this->user_id, $this->group_id ) ) ) {
+				return false;
+			}
+
+			$sql = $wpdb->prepare( "INSERT INTO {$bp->groups->table_name_members} ( user_id, group_id, inviter_id, is_admin, is_mod, is_banned, user_title, date_modified, is_confirmed, comments, invite_sent ) VALUES ( %d, %d, %d, %d, %d, %d, %s, %s, %d, %s, %d )", $this->user_id, $this->group_id, $this->inviter_id, $this->is_admin, $this->is_mod, $this->is_banned, $this->user_title, $this->date_modified, $this->is_confirmed, $this->comments, $this->invite_sent );
+		}
+
+		if ( !$wpdb->query( $sql ) )
+			return false;
+
+		$this->id = $wpdb->insert_id;
+
+		// Update the user's group count.
+		self::refresh_total_group_count_for_user( $this->user_id );
+
+		// Update the group's member count.
+		self::refresh_total_member_count_for_group( $this->group_id );
+
+		/**
+		 * Fires after the current group membership item has been saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Groups_Member $this Current instance of the group membership item has been saved. Passed by reference.
+		 */
+		do_action_ref_array( 'groups_member_after_save', array( &$this ) );
+
+		return true;
+	}
+
+	/**
+	 * Promote a member to a new status.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $status The new status. 'mod' or 'admin'.
+	 * @return bool True on success, false on failure.
+	 */
+	public function promote( $status = 'mod' ) {
+		if ( 'mod' == $status ) {
+			$this->is_admin   = 0;
+			$this->is_mod     = 1;
+			$this->user_title = __( 'Group Mod', 'buddypress' );
+		}
+
+		if ( 'admin' == $status ) {
+			$this->is_admin   = 1;
+			$this->is_mod     = 0;
+			$this->user_title = __( 'Group Admin', 'buddypress' );
+		}
+
+		return $this->save();
+	}
+
+	/**
+	 * Demote membership to Member status (non-admin, non-mod).
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function demote() {
+		$this->is_mod     = 0;
+		$this->is_admin   = 0;
+		$this->user_title = false;
+
+		return $this->save();
+	}
+
+	/**
+	 * Ban the user from the group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function ban() {
+		if ( !empty( $this->is_admin ) )
+			return false;
+
+		$this->is_mod = 0;
+		$this->is_banned = 1;
+
+		return $this->save();
+	}
+
+	/**
+	 * Unban the user from the group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function unban() {
+		if ( !empty( $this->is_admin ) )
+			return false;
+
+		$this->is_banned = 0;
+
+		return $this->save();
+	}
+
+	/**
+	 * Mark a pending invitation as accepted.
+	 *
+	 * @since 1.6.0
+	 */
+	public function accept_invite() {
+		$this->inviter_id    = 0;
+		$this->is_confirmed  = 1;
+		$this->date_modified = bp_core_current_time();
+	}
+
+	/**
+	 * Confirm a membership request.
+	 *
+	 * @since 1.6.0
+	 */
+	public function accept_request() {
+		$this->is_confirmed = 1;
+		$this->date_modified = bp_core_current_time();
+	}
+
+	/**
+	 * Remove the current membership.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function remove() {
+		global $wpdb;
+
+		/**
+		 * Fires before a member is removed from a group.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param BP_Groups_Member $this Current group membership object.
+		 */
+		do_action_ref_array( 'groups_member_before_remove', array( $this ) );
+
+		$bp  = buddypress();
+		$sql = $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $this->user_id, $this->group_id );
+
+		if ( !$result = $wpdb->query( $sql ) )
+			return false;
+
+		// Update the user's group count.
+		self::refresh_total_group_count_for_user( $this->user_id );
+
+		// Update the group's member count.
+		self::refresh_total_member_count_for_group( $this->group_id );
+
+		/**
+		 * Fires after a member is removed from a group.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param BP_Groups_Member $this Current group membership object.
+		 */
+		do_action_ref_array( 'groups_member_after_remove', array( $this ) );
+
+		return $result;
+	}
+
+	/** Static Methods ****************************************************/
+
+	/**
+	 * Refresh the total_group_count for a user.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param int $user_id ID of the user.
+	 * @return bool True on success, false on failure.
+	 */
+	public static function refresh_total_group_count_for_user( $user_id ) {
+		return bp_update_user_meta( $user_id, 'total_group_count', (int) self::total_group_count( $user_id ) );
+	}
+
+	/**
+	 * Refresh the total_member_count for a group.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return bool|int True on success, false on failure.
+	 */
+	public static function refresh_total_member_count_for_group( $group_id ) {
+		return groups_update_groupmeta( $group_id, 'total_member_count', (int) BP_Groups_Group::get_total_member_count( $group_id ) );
+	}
+
+	/**
+	 * Delete a membership, based on user + group IDs.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return True on success, false on failure.
+	 */
+	public static function delete( $user_id, $group_id ) {
+		global $wpdb;
+
+		/**
+		 * Fires before a group membership is deleted.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param int $user_id  ID of the user.
+		 * @param int $group_id ID of the group.
+		 */
+		do_action( 'bp_groups_member_before_delete', $user_id, $group_id );
+
+		$bp = buddypress();
+		$remove = $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $user_id, $group_id ) );
+
+		// Update the user's group count.
+		self::refresh_total_group_count_for_user( $user_id );
+
+		// Update the group's member count.
+		self::refresh_total_member_count_for_group( $group_id );
+
+		/**
+		 * Fires after a member is removed from a group.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param int $user_id  ID of the user.
+		 * @param int $group_id ID of the group.
+		 */
+		do_action( 'bp_groups_member_after_delete', $user_id, $group_id );
+
+		return $remove;
+	}
+
+	/**
+	 * Get the IDs of the groups of which a specified user is a member.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int      $user_id ID of the user.
+	 * @param int|bool $limit   Optional. Max number of results to return.
+	 *                          Default: false (no limit).
+	 * @param int|bool $page    Optional. Page offset of results to return.
+	 *                          Default: false (no limit).
+	 * @return array {
+	 *     @type array $groups Array of groups returned by paginated query.
+	 *     @type int   $total  Count of groups matching query.
+	 * }
+	 */
+	public static function get_group_ids( $user_id, $limit = false, $page = false ) {
+		global $wpdb;
+
+		$pag_sql = '';
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		$bp = buddypress();
+
+		// If the user is logged in and viewing their own groups, we can show hidden and private groups.
+		if ( $user_id != bp_loggedin_user_id() ) {
+			$group_sql = $wpdb->prepare( "SELECT DISTINCT m.group_id FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0{$pag_sql}", $user_id );
+			$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $user_id ) );
+		} else {
+			$group_sql = $wpdb->prepare( "SELECT DISTINCT group_id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND is_confirmed = 1 AND is_banned = 0{$pag_sql}", $user_id );
+			$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT group_id) FROM {$bp->groups->table_name_members} WHERE user_id = %d AND is_confirmed = 1 AND is_banned = 0", $user_id ) );
+		}
+
+		$groups = $wpdb->get_col( $group_sql );
+
+		return array( 'groups' => $groups, 'total' => (int) $total_groups );
+	}
+
+	/**
+	 * Get the IDs of the groups of which a specified user is a member, sorted by the date joined.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int         $user_id ID of the user.
+	 * @param int|bool    $limit   Optional. Max number of results to return.
+	 *                             Default: false (no limit).
+	 * @param int|bool    $page    Optional. Page offset of results to return.
+	 *                             Default: false (no limit).
+	 * @param string|bool $filter  Optional. Limit results to groups whose name or
+	 *                             description field matches search terms.
+	 * @return array {
+	 *     @type array $groups Array of groups returned by paginated query.
+	 *     @type int   $total  Count of groups matching query.
+	 * }
+	 */
+	public static function get_recently_joined( $user_id, $limit = false, $page = false, $filter = false ) {
+		global $wpdb;
+
+		$user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
+
+		$user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
+
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		if ( !empty( $filter ) ) {
+			$search_terms_like = '%' . bp_esc_like( $filter ) . '%';
+			$filter_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
+		}
+
+		if ( $user_id != bp_loggedin_user_id() )
+			$hidden_sql = " AND g.status != 'hidden'";
+
+		$bp = buddypress();
+
+		$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 ORDER BY m.date_modified DESC {$pag_sql}" );
+		$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_banned = 0 AND m.is_confirmed = 1 ORDER BY m.date_modified DESC" );
+
+		return array( 'groups' => $paged_groups, 'total' => $total_groups );
+	}
+
+	/**
+	 * Get the IDs of the groups of which a specified user is an admin.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int         $user_id ID of the user.
+	 * @param int|bool    $limit   Optional. Max number of results to return.
+	 *                             Default: false (no limit).
+	 * @param int|bool    $page    Optional. Page offset of results to return.
+	 *                             Default: false (no limit).
+	 * @param string|bool $filter  Optional. Limit results to groups whose name or
+	 *                             description field matches search terms.
+	 * @return array {
+	 *     @type array $groups Array of groups returned by paginated query.
+	 *     @type int   $total  Count of groups matching query.
+	 * }
+	 */
+	public static function get_is_admin_of( $user_id, $limit = false, $page = false, $filter = false ) {
+		global $wpdb;
+
+		$user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
+
+		$user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
+
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		if ( !empty( $filter ) ) {
+			$search_terms_like = '%' . bp_esc_like( $filter ) . '%';
+			$filter_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
+		}
+
+		if ( $user_id != bp_loggedin_user_id() )
+			$hidden_sql = " AND g.status != 'hidden'";
+
+		$bp = buddypress();
+
+		$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_admin = 1 ORDER BY m.date_modified ASC {$pag_sql}" );
+		$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_admin = 1 ORDER BY date_modified ASC" );
+
+		return array( 'groups' => $paged_groups, 'total' => $total_groups );
+	}
+
+	/**
+	 * Get the IDs of the groups of which a specified user is a moderator.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int         $user_id ID of the user.
+	 * @param int|bool    $limit   Optional. Max number of results to return.
+	 *                             Default: false (no limit).
+	 * @param int|bool    $page    Optional. Page offset of results to return.
+	 *                             Default: false (no limit).
+	 * @param string|bool $filter  Optional. Limit results to groups whose name or
+	 *                             description field matches search terms.
+	 * @return array {
+	 *     @type array $groups Array of groups returned by paginated query.
+	 *     @type int   $total  Count of groups matching query.
+	 * }
+	 */
+	public static function get_is_mod_of( $user_id, $limit = false, $page = false, $filter = false ) {
+		global $wpdb;
+
+		$user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
+
+		$user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
+
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		if ( !empty( $filter ) ) {
+			$search_terms_like = '%' . bp_esc_like( $filter ) . '%';
+			$filter_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
+		}
+
+		if ( $user_id != bp_loggedin_user_id() )
+			$hidden_sql = " AND g.status != 'hidden'";
+
+		$bp = buddypress();
+
+		$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_mod = 1 ORDER BY m.date_modified ASC {$pag_sql}" );
+		$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_mod = 1 ORDER BY date_modified ASC" );
+
+		return array( 'groups' => $paged_groups, 'total' => $total_groups );
+	}
+
+	/**
+	 * Get the groups of which a specified user is banned from.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param int         $user_id ID of the user.
+	 * @param int|bool    $limit   Optional. Max number of results to return.
+	 *                             Default: false (no limit).
+	 * @param int|bool    $page    Optional. Page offset of results to return.
+	 *                             Default: false (no limit).
+	 * @param string|bool $filter  Optional. Limit results to groups whose name or
+	 *                             description field matches search terms.
+	 * @return array {
+	 *     @type array $groups Array of groups returned by paginated query.
+	 *     @type int   $total  Count of groups matching query.
+	 * }
+	 */
+	public static function get_is_banned_of( $user_id, $limit = false, $page = false, $filter = false ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
+		$user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
+
+		if ( $limit && $page ) {
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit ), intval( $limit ) );
+		}
+
+		if ( $filter ) {
+			$search_terms_like = '%' . bp_esc_like( $filter ) . '%';
+			$filter_sql        = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
+		}
+
+		if ( $user_id !== bp_loggedin_user_id() && ! bp_current_user_can( 'bp_moderate' ) ) {
+			$hidden_sql = " AND g.status != 'hidden'";
+		}
+
+		$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_banned = 1  ORDER BY m.date_modified ASC {$pag_sql}" );
+		$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND {$user_id_sql} AND m.is_banned = 1 ORDER BY date_modified ASC" );
+
+		return array( 'groups' => $paged_groups, 'total' => $total_groups );
+	}
+
+	/**
+	 * Get the count of groups of which the specified user is a member.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id Optional. Default: ID of the displayed user.
+	 * @return int Group count.
+	 */
+	public static function total_group_count( $user_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			$user_id = bp_displayed_user_id();
+
+		$bp = buddypress();
+
+		if ( $user_id != bp_loggedin_user_id() && !bp_current_user_can( 'bp_moderate' ) ) {
+			return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $user_id ) );
+		} else {
+			return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $user_id ) );
+		}
+	}
+
+	/**
+	 * Get a user's outstanding group invitations.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int               $user_id ID of the invitee.
+	 * @param int|bool          $limit   Optional. Max number of results to return.
+	 *                                   Default: false (no limit).
+	 * @param int|bool          $page    Optional. Page offset of results to return.
+	 *                                   Default: false (no limit).
+	 * @param string|array|bool $exclude Optional. Array or comma-separated list
+	 *                                   of group IDs to exclude from results.
+	 * @return array {
+	 *     @type array $groups Array of groups returned by paginated query.
+	 *     @type int   $total  Count of groups matching query.
+	 * }
+	 */
+	public static function get_invites( $user_id, $limit = false, $page = false, $exclude = false ) {
+		global $wpdb;
+
+		$pag_sql = ( !empty( $limit ) && !empty( $page ) ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';
+
+		if ( !empty( $exclude ) ) {
+			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
+			$exclude_sql = " AND g.id NOT IN ({$exclude})";
+		} else {
+			$exclude_sql = '';
+		}
+
+		$bp = buddypress();
+
+		$paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND m.is_confirmed = 0 AND m.inviter_id != 0 AND m.invite_sent = 1 AND m.user_id = %d {$exclude_sql} ORDER BY m.date_modified ASC {$pag_sql}", $user_id ) );
+
+		return array( 'groups' => $paged_groups, 'total' => self::get_invite_count_for_user( $user_id ) );
+	}
+
+	/**
+	 * Gets the total group invite count for a user.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int $user_id The user ID.
+	 * @return int
+	 */
+	public static function get_invite_count_for_user( $user_id = 0 ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$count = wp_cache_get( $user_id, 'bp_group_invite_count' );
+
+		if ( false === $count ) {
+			$count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND m.is_confirmed = 0 AND m.inviter_id != 0 AND m.invite_sent = 1 AND m.user_id = %d", $user_id ) );
+			wp_cache_set( $user_id, $count, 'bp_group_invite_count' );
+		}
+
+		return $count;
+	}
+
+	/**
+	 * Gets memberships of a user for purposes of a personal data export.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param int $user_id ID of the user.
+	 * @param array $args {
+	 *    Array of optional arguments.
+	 *    @type int    $page     Page of memberships being requested. Default 1.
+	 *    @type int    $per_page Memberships to return per page. Default 20.
+	 *    @type string $type     Membership type being requested. Accepts 'membership',
+	 *                           'pending_request', 'pending_received_invitation',
+	 *                           'pending_sent_invitation'. Default 'membership'.
+	 * }
+	 *
+	 * @return array
+	 */
+	public static function get_user_memberships( $user_id, $args = array() ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$r = array_merge( array(
+			'page'     => 1,
+			'per_page' => 20,
+			'type'     => 'membership',
+		), $args );
+
+		$sql = array(
+			'select' => 'SELECT *',
+			'from'   => "FROM {$bp->groups->table_name_members}",
+			'where'  => '',
+			'limits' => '',
+		);
+
+		switch ( $r['type'] ) {
+			case 'pending_request' :
+				$sql['where'] = $wpdb->prepare( "user_id = %d AND is_confirmed = 0 AND inviter_id = 0", $user_id );
+			break;
+
+			case 'pending_received_invitation' :
+				$sql['where'] = $wpdb->prepare( "user_id = %d AND is_confirmed = 0 AND inviter_id != 0", $user_id );
+			break;
+
+			case 'pending_sent_invitation' :
+				$sql['where'] = $wpdb->prepare( "inviter_id = %d AND is_confirmed = 0", $user_id );
+			break;
+
+			case 'membership' :
+			default :
+				$sql['where'] = $wpdb->prepare( "user_id = %d AND is_confirmed = 1", $user_id );
+			break;
+		}
+
+		if ( $r['page'] && $r['per_page'] ) {
+			$sql['limits'] = $wpdb->prepare( "LIMIT %d, %d", ( $r['page'] - 1 ) * $r['per_page'], $r['per_page'] );
+		}
+
+		$memberships = $wpdb->get_results( "{$sql['select']} {$sql['from']} WHERE {$sql['where']} {$sql['limits']}" );
+
+		foreach ( $memberships as &$membership ) {
+			$membership->id           = (int) $membership->id;
+			$membership->group_id     = (int) $membership->group_id;
+			$membership->user_id      = (int) $membership->user_id;
+			$membership->inviter_id   = (int) $membership->inviter_id;
+			$membership->is_admin     = (int) $membership->is_admin;
+			$membership->is_mod       = (int) $membership->is_mod;
+			$membership->is_banned    = (int) $membership->is_banned;
+			$membership->is_confirmed = (int) $membership->is_confirmed;
+			$membership->invite_sent  = (int) $membership->invite_sent;
+		}
+
+		return $memberships;
+	}
+
+	/**
+	 * Check whether a user has an outstanding invitation to a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int    $user_id  ID of the potential invitee.
+	 * @param int    $group_id ID of the group.
+	 * @param string $type     If 'sent', results are limited to those invitations
+	 *                         that have actually been sent (non-draft). Default: 'sent'.
+	 * @return int|null The ID of the invitation if found; null if not found.
+	 */
+	public static function check_has_invite( $user_id, $group_id, $type = 'sent' ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp  = buddypress();
+		$sql = "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND inviter_id != 0";
+
+		if ( 'sent' == $type )
+			$sql .= " AND invite_sent = 1";
+
+		$query = $wpdb->get_var( $wpdb->prepare( $sql, $user_id, $group_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+
+	/**
+	 * Delete an invitation, by specifying user ID and group ID.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @global WPDB $wpdb
+	 *
+	 * @param  int $user_id  ID of the user.
+	 * @param  int $group_id ID of the group.
+	 * @return int Number of records deleted.
+	 */
+	public static function delete_invite( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires before a group invitation is deleted.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param int $user_id  ID of the user.
+		 * @param int $group_id ID of the group.
+		 */
+		do_action( 'bp_groups_member_before_delete_invite', $user_id, $group_id );
+
+		$table_name = buddypress()->groups->table_name_members;
+
+		$sql = "DELETE FROM {$table_name}
+				WHERE user_id = %d
+					AND group_id = %d
+					AND is_confirmed = 0
+					AND inviter_id != 0";
+
+		$prepared = $wpdb->prepare( $sql, $user_id, $group_id );
+
+		return $wpdb->query( $prepared );
+	}
+
+	/**
+	 * Delete an unconfirmed membership request, by user ID and group ID.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return int Number of records deleted.
+	 */
+	public static function delete_request( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND inviter_id = 0 AND invite_sent = 0", $user_id, $group_id ) );
+	}
+
+	/**
+	 * Check whether a user is an admin of a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return mixed
+	 */
+	public static function check_is_admin( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_admin = 1 AND is_banned = 0", $user_id, $group_id ) );
+	}
+
+	/**
+	 * Check whether a user is a mod of a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return mixed
+	 */
+	public static function check_is_mod( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_mod = 1 AND is_banned = 0", $user_id, $group_id ) );
+	}
+
+	/**
+	 * Check whether a user is a member of a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return mixed
+	 */
+	public static function check_is_member( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 1 AND is_banned = 0", $user_id, $group_id ) );
+	}
+
+	/**
+	 * Check whether a user is banned from a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return int|null int 1 if user is banned; int 0 if user is not banned;
+	 *                  null if user is not part of the group or if group doesn't exist.
+	 */
+	public static function check_is_banned( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		$query = $wpdb->get_var( $wpdb->prepare( "SELECT is_banned FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $user_id, $group_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+
+	/**
+	 * Is the specified user the creator of the group?
+	 *
+	 * @since 1.2.6
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return int|null int of group ID if user is the creator; null on failure.
+	 */
+	public static function check_is_creator( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		$query = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name} WHERE creator_id = %d AND id = %d", $user_id, $group_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+
+	/**
+	 * Check whether a user has an outstanding membership request for a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id  ID of the user.
+	 * @param int $group_id ID of the group.
+	 * @return int Database ID of the membership if found; int 0 on failure.
+	 */
+	public static function check_for_membership_request( $user_id, $group_id ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) )
+			return false;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND is_banned = 0 AND inviter_id = 0", $user_id, $group_id ) );
+	}
+
+	/**
+	 * Get a list of randomly selected IDs of groups that the member belongs to.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id      ID of the user.
+	 * @param int $total_groups Max number of group IDs to return. Default: 5.
+	 * @return array Group IDs.
+	 */
+	public static function get_random_groups( $user_id = 0, $total_groups = 5 ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// If the user is logged in and viewing their random groups, we can show hidden and private groups.
+		if ( bp_is_my_profile() ) {
+			return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT group_id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND is_confirmed = 1 AND is_banned = 0 ORDER BY rand() LIMIT %d", $user_id, $total_groups ) ) );
+		} else {
+			return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT m.group_id FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 ORDER BY rand() LIMIT %d", $user_id, $total_groups ) ) );
+		}
+	}
+
+	/**
+	 * Get the IDs of all a given group's members.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return array IDs of all group members.
+	 */
+	public static function get_group_member_ids( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 1 AND is_banned = 0", $group_id ) ) );
+	}
+
+	/**
+	 * Get a list of all a given group's admins.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return array Info about group admins (user_id + date_modified).
+	 */
+	public static function get_group_administrator_ids( $group_id ) {
+		global $wpdb;
+
+		if ( empty( $group_id ) ) {
+			return array();
+		}
+
+		$group_admins = wp_cache_get( $group_id, 'bp_group_admins' );
+
+		if ( false === $group_admins ) {
+			self::prime_group_admins_mods_cache( array( $group_id ) );
+			$group_admins = wp_cache_get( $group_id, 'bp_group_admins' );
+		}
+
+		if ( false === $group_admins ) {
+			// The wp_cache_get is still coming up empty. Return an empty array.
+			$group_admins = array();
+		} else {
+			// Cast the user_id property as an integer.
+			foreach ( (array) $group_admins as $key => $data ) {
+				$group_admins[ $key ]->user_id = (int) $group_admins[ $key ]->user_id;
+			}
+		}
+
+		return $group_admins;
+	}
+
+	/**
+	 * Prime the bp_group_admins cache for one or more groups.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $group_ids IDs of the groups.
+	 * @return bool True on success.
+	 */
+	public static function prime_group_admins_mods_cache( $group_ids ) {
+		global $wpdb;
+
+		$uncached = bp_get_non_cached_ids( $group_ids, 'bp_group_admins' );
+
+		if ( $uncached ) {
+			$bp = buddypress();
+			$uncached_sql = implode( ',', array_map( 'intval', $uncached ) );
+			$group_admin_mods = $wpdb->get_results( "SELECT user_id, group_id, date_modified, is_admin, is_mod FROM {$bp->groups->table_name_members} WHERE group_id IN ({$uncached_sql}) AND ( is_admin = 1 OR is_mod = 1 ) AND is_banned = 0" );
+
+			$admins = $mods = array();
+			if ( $group_admin_mods ) {
+				foreach ( $group_admin_mods as $group_admin_mod ) {
+					$obj = new stdClass();
+					$obj->user_id = $group_admin_mod->user_id;
+					$obj->date_modified = $group_admin_mod->date_modified;
+
+					if ( $group_admin_mod->is_admin ) {
+						$admins[ $group_admin_mod->group_id ][] = $obj;
+					} else {
+						$mods[ $group_admin_mod->group_id ][] = $obj;
+					}
+				}
+			}
+
+			// Prime cache for all groups, even those with no matches.
+			foreach ( $uncached as $group_id ) {
+				$group_admins = isset( $admins[ $group_id ] ) ? $admins[ $group_id ] : array();
+				wp_cache_set( $group_id, $group_admins, 'bp_group_admins' );
+
+				$group_mods = isset( $mods[ $group_id ] ) ? $mods[ $group_id ] : array();
+				wp_cache_set( $group_id, $group_mods, 'bp_group_mods' );
+			}
+		}
+	}
+
+	/**
+	 * Get a list of all a given group's moderators.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return array Info about group mods (user_id + date_modified).
+	 */
+	public static function get_group_moderator_ids( $group_id ) {
+		global $wpdb;
+
+		if ( empty( $group_id ) ) {
+			return array();
+		}
+
+		$group_mods = wp_cache_get( $group_id, 'bp_group_mods' );
+
+		if ( false === $group_mods ) {
+			self::prime_group_admins_mods_cache( array( $group_id ) );
+			$group_mods = wp_cache_get( $group_id, 'bp_group_mods' );
+		}
+
+		if ( false === $group_mods ) {
+			// The wp_cache_get is still coming up empty. Return an empty array.
+			$group_mods = array();
+		} else {
+			// Cast the user_id property as an integer.
+			foreach ( (array) $group_mods as $key => $data ) {
+				$group_mods[ $key ]->user_id = (int) $group_mods[ $key ]->user_id;
+			}
+		}
+
+		return $group_mods;
+	}
+
+	/**
+	 * Get group membership objects by ID (or an array of IDs).
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int|string|array $membership_ids Single membership ID or comma-separated/array list of membership IDs.
+	 * @return array
+	 */
+	public static function get_memberships_by_id( $membership_ids ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$membership_ids = implode( ',', wp_parse_id_list( $membership_ids ) );
+		return $wpdb->get_results( "SELECT * FROM {$bp->groups->table_name_members} WHERE id IN ({$membership_ids})" );
+	}
+
+	/**
+	 * Get the IDs users with outstanding membership requests to the group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return array IDs of users with outstanding membership requests.
+	 */
+	public static function get_all_membership_request_user_ids( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0 AND inviter_id = 0", $group_id ) ) );
+	}
+
+	/**
+	 * Get members of a group.
+	 *
+	 * @deprecated 1.6.0
+	 *
+	 * @param int        $group_id            ID of the group being queried for.
+	 * @param bool|int   $limit               Max amount to return.
+	 * @param bool|int   $page                Pagination value.
+	 * @param bool       $exclude_admins_mods Whether or not to exclude admins and moderators.
+	 * @param bool       $exclude_banned      Whether or not to exclude banned members.
+	 * @param bool|array $exclude             Array of user IDs to exclude.
+	 * @return false|array
+	 */
+	public static function get_all_for_group( $group_id, $limit = false, $page = false, $exclude_admins_mods = true, $exclude_banned = true, $exclude = false ) {
+		global $wpdb;
+
+		_deprecated_function( __METHOD__, '1.8', 'BP_Group_Member_Query' );
+
+		$pag_sql = '';
+		if ( !empty( $limit ) && !empty( $page ) )
+			$pag_sql = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
+
+		$exclude_admins_sql = '';
+		if ( !empty( $exclude_admins_mods ) )
+			$exclude_admins_sql = "AND is_admin = 0 AND is_mod = 0";
+
+		$banned_sql = '';
+		if ( !empty( $exclude_banned ) )
+			$banned_sql = " AND is_banned = 0";
+
+		$exclude_sql = '';
+		if ( !empty( $exclude ) ) {
+			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
+			$exclude_sql = " AND m.user_id NOT IN ({$exclude})";
+		}
+
+		$bp = buddypress();
+
+		if ( bp_is_active( 'xprofile' ) ) {
+
+			/**
+			 * Filters the SQL prepared statement used to fetch group members.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $value SQL prepared statement for fetching group members.
+			 */
+			$members = $wpdb->get_results( apply_filters( 'bp_group_members_user_join_filter', $wpdb->prepare( "SELECT m.user_id, m.date_modified, m.is_banned, u.user_login, u.user_nicename, u.user_email, pd.value as display_name FROM {$bp->groups->table_name_members} m, {$wpdb->users} u, {$bp->profile->table_name_data} pd WHERE u.ID = m.user_id AND u.ID = pd.user_id AND pd.field_id = 1 AND group_id = %d AND is_confirmed = 1 {$banned_sql} {$exclude_admins_sql} {$exclude_sql} ORDER BY m.date_modified DESC {$pag_sql}", $group_id ) ) );
+		} else {
+
+			/** This filter is documented in bp-groups/bp-groups-classes */
+			$members = $wpdb->get_results( apply_filters( 'bp_group_members_user_join_filter', $wpdb->prepare( "SELECT m.user_id, m.date_modified, m.is_banned, u.user_login, u.user_nicename, u.user_email, u.display_name FROM {$bp->groups->table_name_members} m, {$wpdb->users} u WHERE u.ID = m.user_id AND group_id = %d AND is_confirmed = 1 {$banned_sql} {$exclude_admins_sql} {$exclude_sql} ORDER BY m.date_modified DESC {$pag_sql}", $group_id ) ) );
+		}
+
+		if ( empty( $members ) ) {
+			return false;
+		}
+
+		if ( empty( $pag_sql ) ) {
+			$total_member_count = count( $members );
+		} else {
+
+			/**
+			 * Filters the SQL prepared statement used to fetch group members total count.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $value SQL prepared statement for fetching group member count.
+			 */
+			$total_member_count = $wpdb->get_var( apply_filters( 'bp_group_members_count_user_join_filter', $wpdb->prepare( "SELECT COUNT(user_id) FROM {$bp->groups->table_name_members} m WHERE group_id = %d AND is_confirmed = 1 {$banned_sql} {$exclude_admins_sql} {$exclude_sql}", $group_id ) ) );
+		}
+
+		// Fetch whether or not the user is a friend.
+		foreach ( (array) $members as $user )
+			$user_ids[] = $user->user_id;
+
+		$user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
+
+		if ( bp_is_active( 'friends' ) ) {
+			$friend_status = $wpdb->get_results( $wpdb->prepare( "SELECT initiator_user_id, friend_user_id, is_confirmed FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d AND friend_user_id IN ( {$user_ids} ) ) OR (initiator_user_id IN ( {$user_ids} ) AND friend_user_id = %d )", bp_loggedin_user_id(), bp_loggedin_user_id() ) );
+			for ( $i = 0, $count = count( $members ); $i < $count; ++$i ) {
+				foreach ( (array) $friend_status as $status ) {
+					if ( $status->initiator_user_id == $members[$i]->user_id || $status->friend_user_id == $members[$i]->user_id ) {
+						$members[$i]->is_friend = $status->is_confirmed;
+					}
+				}
+			}
+		}
+
+		return array( 'members' => $members, 'count' => $total_member_count );
+	}
+
+	/**
+	 * Get all membership IDs for a user.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int $user_id ID of the user.
+	 * @return array
+	 */
+	public static function get_membership_ids_for_user( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$group_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d ORDER BY id ASC", $user_id ) );
+
+		return $group_ids;
+	}
+
+	/**
+	 * Delete all memberships for a given group.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $group_id ID of the group.
+	 * @return int Number of records deleted.
+	 */
+	public static function delete_all( $group_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE group_id = %d", $group_id ) );
+	}
+
+	/**
+	 * Delete all group membership information for the specified user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id ID of the user.
+	 * @return mixed
+	 */
+	public static function delete_all_for_user( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// Get all the group ids for the current user's groups and update counts.
+		$group_ids = BP_Groups_Member::get_group_ids( $user_id );
+		foreach ( $group_ids['groups'] as $group_id ) {
+			groups_update_groupmeta( $group_id, 'total_member_count', groups_get_total_member_count( $group_id ) - 1 );
+
+			// If current user is the creator of a group and is the sole admin, delete that group to avoid counts going out-of-sync.
+			if ( groups_is_user_admin( $user_id, $group_id ) && count( groups_get_group_admins( $group_id ) ) < 2 && groups_is_user_creator( $user_id, $group_id ) )
+				groups_delete_group( $group_id );
+		}
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d", $user_id ) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-membership-requests-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-membership-requests-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-membership-requests-template.php	(revision 12265)
@@ -0,0 +1,254 @@
+<?php
+/**
+ * BuddyPress Groups membership request template loop class.
+ *
+ * @package BuddyPress
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Membership request template loop class.
+ *
+ * @since 1.0.0
+ */
+class BP_Groups_Membership_Requests_Template {
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $current_request = -1;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $request_count;
+
+	/**
+	 * @since 1.0.0
+	 * @var array
+	 */
+	public $requests;
+
+	/**
+	 * @since 1.0.0
+	 * @var object
+	 */
+	public $request;
+
+	/**
+	 * @sine 1.0.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $pag_page;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $pag_num;
+
+	/**
+	 * @since 1.0.0
+	 * @var array|string|void
+	 */
+	public $pag_links;
+
+	/**
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $total_request_count;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args {
+	 *     @type int $group_id ID of the group whose membership requests
+	 *                         are being queried. Default: current group id.
+	 *     @type int $per_page Number of records to return per page of
+	 *                         results. Default: 10.
+	 *     @type int $page     Page of results to show. Default: 1.
+	 *     @type int $max      Max items to return. Default: false (show all)
+	 * }
+	 */
+	public function __construct( $args = array() ) {
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '2.0.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'group_id',
+				1 => 'per_page',
+				2 => 'max',
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$r = bp_parse_args( $args, array(
+			'page'     => 1,
+			'per_page' => 10,
+			'page_arg' => 'mrpage',
+			'max'      => false,
+			'type'     => 'first_joined',
+			'group_id' => bp_get_current_group_id(),
+		), 'groups_membership_requests_template' );
+
+		$this->pag_arg  = sanitize_key( $r['page_arg'] );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+
+		$mquery = new BP_Group_Member_Query( array(
+			'group_id' => $r['group_id'],
+			'type'     => $r['type'],
+			'per_page' => $this->pag_num,
+			'page'     => $this->pag_page,
+
+			// These filters ensure we only get pending requests.
+			'is_confirmed' => false,
+			'inviter_id'   => 0,
+		) );
+
+		$this->requests      = array_values( $mquery->results );
+		$this->request_count = count( $this->requests );
+
+		// Compatibility with legacy format of request data objects.
+		foreach ( $this->requests as $rk => $rv ) {
+			// For legacy reasons, the 'id' property of each
+			// request must match the membership id, not the ID of
+			// the user (as it's returned by BP_Group_Member_Query).
+			$this->requests[ $rk ]->user_id = $rv->ID;
+			$this->requests[ $rk ]->id      = $rv->membership_id;
+
+			// Miscellaneous values.
+			$this->requests[ $rk ]->group_id   = $r['group_id'];
+		}
+
+		if ( empty( $r['max'] ) || ( $r['max'] >= (int) $mquery->total_users ) ) {
+			$this->total_request_count = (int) $mquery->total_users;
+		} else {
+			$this->total_request_count = (int) $r['max'];
+		}
+
+		if ( empty( $r['max'] ) || ( $r['max'] >= count( $this->requests ) ) ) {
+			$this->request_count = count( $this->requests );
+		} else {
+			$this->request_count = (int) $r['max'];
+		}
+
+		$this->pag_links = paginate_links( array(
+			'base'      => add_query_arg( $this->pag_arg, '%#%' ),
+			'format'    => '',
+			'total'     => ceil( $this->total_request_count / $this->pag_num ),
+			'current'   => $this->pag_page,
+			'prev_text' => '&larr;',
+			'next_text' => '&rarr;',
+			'mid_size'  => 1,
+			'add_args'  => array(),
+		) );
+	}
+
+	/**
+	 * Whether or not there are requests to show.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function has_requests() {
+		if ( ! empty( $this->request_count ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Moves up to the next request.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return object
+	 */
+	public function next_request() {
+		$this->current_request++;
+		$this->request = $this->requests[ $this->current_request ];
+
+		return $this->request;
+	}
+
+	/**
+	 * Rewinds the requests to the first in the list.
+	 *
+	 * @since 1.0.0
+	 */
+	public function rewind_requests() {
+		$this->current_request = -1;
+
+		if ( $this->request_count > 0 ) {
+			$this->request = $this->requests[0];
+		}
+	}
+
+	/**
+	 * Finishes up the requests to display.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function requests() {
+		$tick = intval( $this->current_request + 1 );
+		if ( $tick < $this->request_count ) {
+			return true;
+		} elseif ( $tick == $this->request_count ) {
+
+			/**
+			 * Fires right before the rewinding of group membership requests list.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'group_request_loop_end' );
+			// Do some cleaning up after the loop.
+			$this->rewind_requests();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Sets up the request to display.
+	 *
+	 * @since 1.0.0
+	 */
+	public function the_request() {
+		$this->in_the_loop = true;
+		$this->request     = $this->next_request();
+
+		// Loop has just started.
+		if ( 0 == $this->current_request ) {
+
+			/**
+			 * Fires if the current group membership request item is the first in the loop.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'group_request_loop_start' );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-template.php	(revision 12265)
@@ -0,0 +1,403 @@
+<?php
+/**
+ * BuddyPress Groups Template loop class.
+ *
+ * @package BuddyPress
+ * @since 1.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main Groups template loop class.
+ *
+ * Responsible for loading a group of groups into a loop for display.
+ *
+ * @since 1.2.0
+ */
+class BP_Groups_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @var int
+	 * @since 1.2.0
+	 */
+	public $current_group = -1;
+
+	/**
+	 * The number of groups returned by the paged query.
+	 *
+	 * @var int
+	 * @since 1.2.0
+	 */
+	public $group_count;
+
+	/**
+	 * Array of groups located by the query.
+	 *
+	 * @var array
+	 * @since 1.2.0
+	 */
+	public $groups;
+
+	/**
+	 * The group object currently being iterated on.
+	 *
+	 * @var object
+	 * @since 1.2.0
+	 */
+	public $group;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @var bool
+	 * @since 1.2.0
+	 */
+	public $in_the_loop;
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @var string
+	 * @since 1.2.0
+	 */
+	public $pag_page;
+
+	/**
+	 * The number of items being requested per page.
+	 *
+	 * @var string
+	 * @since 1.2.0
+	 */
+	public $pag_num;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @var string
+	 * @since 1.2.0
+	 */
+	public $pag_links;
+
+	/**
+	 * The total number of groups matching the query parameters.
+	 *
+	 * @var int
+	 * @since 1.2.0
+	 */
+	public $total_group_count;
+
+	/**
+	 * Whether the template loop is for a single group page.
+	 *
+	 * @var bool
+	 * @since 1.2.0
+	 */
+	public $single_group = false;
+
+	/**
+	 * Field to sort by.
+	 *
+	 * @var string
+	 * @since 1.2.0
+	 */
+	public $sort_by;
+
+	/**
+	 * Sort order.
+	 *
+	 * @var string
+	 * @since 1.2.0
+	 */
+	public $order;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @see BP_Groups_Group::get() for an in-depth description of arguments.
+	 *
+	 * @param array $args {
+	 *     Array of arguments. Accepts all arguments accepted by
+	 *     {@link BP_Groups_Group::get()}. In cases where the default
+	 *     values of the params differ, they have been discussed below.
+	 *     @type int $per_page Default: 20.
+	 *     @type int $page Default: 1.
+	 * }
+	 */
+	function __construct( $args = array() ){
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '1.7', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0  => 'user_id',
+				1  => 'type',
+				2  => 'page',
+				3  => 'per_page',
+				4  => 'max',
+				5  => 'slug',
+				6  => 'search_terms',
+				7  => 'populate_extras',
+				8  => 'include',
+				9  => 'exclude',
+				10 => 'show_hidden',
+				11 => 'page_arg',
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$defaults = array(
+			'page'               => 1,
+			'per_page'           => 20,
+			'page_arg'           => 'grpage',
+			'max'                => false,
+			'type'               => 'active',
+			'order'              => 'DESC',
+			'orderby'            => 'date_created',
+			'show_hidden'        => false,
+			'user_id'            => 0,
+			'slug'               => false,
+			'include'            => false,
+			'exclude'            => false,
+			'parent_id'          => null,
+			'search_terms'       => '',
+			'search_columns'     => array(),
+			'group_type'         => '',
+			'group_type__in'     => '',
+			'group_type__not_in' => '',
+			'meta_query'         => false,
+			'update_meta_cache'  => true,
+			'update_admin_cache' => false,
+		);
+
+		$r = bp_parse_args( $args, $defaults, 'groups_template' );
+		extract( $r );
+
+		$this->pag_arg  = sanitize_key( $r['page_arg'] );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+
+		if ( bp_current_user_can( 'bp_moderate' ) || ( is_user_logged_in() && $user_id == bp_loggedin_user_id() ) ) {
+			$show_hidden = true;
+		}
+
+		if ( 'invites' == $type ) {
+			$this->groups = groups_get_invites_for_user( $user_id, $this->pag_num, $this->pag_page, $exclude );
+		} elseif ( 'single-group' == $type ) {
+			$this->single_group = true;
+
+			if ( groups_get_current_group() ) {
+				$group = groups_get_current_group();
+
+			} else {
+				$group = groups_get_group( BP_Groups_Group::get_id_from_slug( $r['slug'] ) );
+			}
+
+			// Backwards compatibility - the 'group_id' variable is not part of the
+			// BP_Groups_Group object, but we add it here for devs doing checks against it
+			//
+			// @see https://buddypress.trac.wordpress.org/changeset/3540
+			//
+			// this is subject to removal in a future release; devs should check against
+			// $group->id instead.
+			$group->group_id = $group->id;
+
+			$this->groups = array( $group );
+
+		} else {
+			$this->groups = groups_get_groups( array(
+				'type'               => $type,
+				'order'              => $order,
+				'orderby'            => $orderby,
+				'per_page'           => $this->pag_num,
+				'page'               => $this->pag_page,
+				'user_id'            => $user_id,
+				'search_terms'       => $search_terms,
+				'search_columns'     => $search_columns,
+				'meta_query'         => $meta_query,
+				'group_type'         => $group_type,
+				'group_type__in'     => $group_type__in,
+				'group_type__not_in' => $group_type__not_in,
+				'include'            => $include,
+				'exclude'            => $exclude,
+				'parent_id'          => $parent_id,
+				'update_meta_cache'  => $update_meta_cache,
+				'update_admin_cache' => $update_admin_cache,
+				'show_hidden'        => $show_hidden,
+			) );
+		}
+
+		if ( 'invites' == $type ) {
+			$this->total_group_count = (int) $this->groups['total'];
+			$this->group_count       = (int) $this->groups['total'];
+			$this->groups            = $this->groups['groups'];
+		} elseif ( 'single-group' == $type ) {
+			if ( empty( $group->id ) ) {
+				$this->total_group_count = 0;
+				$this->group_count       = 0;
+			} else {
+				$this->total_group_count = 1;
+				$this->group_count       = 1;
+			}
+		} else {
+			if ( empty( $max ) || $max >= (int) $this->groups['total'] ) {
+				$this->total_group_count = (int) $this->groups['total'];
+			} else {
+				$this->total_group_count = (int) $max;
+			}
+
+			$this->groups = $this->groups['groups'];
+
+			if ( !empty( $max ) ) {
+				if ( $max >= count( $this->groups ) ) {
+					$this->group_count = count( $this->groups );
+				} else {
+					$this->group_count = (int) $max;
+				}
+			} else {
+				$this->group_count = count( $this->groups );
+			}
+		}
+
+		// Build pagination links.
+		if ( (int) $this->total_group_count && (int) $this->pag_num ) {
+			$pag_args = array(
+				$this->pag_arg => '%#%'
+			);
+
+			if ( defined( 'DOING_AJAX' ) && true === (bool) DOING_AJAX ) {
+				$base = remove_query_arg( 's', wp_get_referer() );
+			} else {
+				$base = '';
+			}
+
+			$add_args = array(
+				'num'     => $this->pag_num,
+				'sortby'  => $this->sort_by,
+				'order'   => $this->order,
+			);
+
+			if ( ! empty( $search_terms ) ) {
+				$query_arg = bp_core_get_component_search_query_arg( 'groups' );
+				$add_args[ $query_arg ] = urlencode( $search_terms );
+			}
+
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $pag_args, $base ),
+				'format'    => '',
+				'total'     => ceil( (int) $this->total_group_count / (int) $this->pag_num ),
+				'current'   => $this->pag_page,
+				'prev_text' => _x( '&larr;', 'Group pagination previous text', 'buddypress' ),
+				'next_text' => _x( '&rarr;', 'Group pagination next text', 'buddypress' ),
+				'mid_size'  => 1,
+				'add_args'  => $add_args,
+			) );
+		}
+	}
+
+	/**
+	 * Whether there are groups available in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @see bp_has_groups()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	function has_groups() {
+		if ( $this->group_count ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set up the next group and iterate index.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return object The next group to iterate over.
+	 */
+	function next_group() {
+		$this->current_group++;
+		$this->group = $this->groups[$this->current_group];
+
+		return $this->group;
+	}
+
+	/**
+	 * Rewind the groups and reset member index.
+	 *
+	 * @since 1.2.0
+	 */
+	function rewind_groups() {
+		$this->current_group = -1;
+		if ( $this->group_count > 0 ) {
+			$this->group = $this->groups[0];
+		}
+	}
+
+	/**
+	 * Whether there are groups left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_groups()} as part of the while loop
+	 * that controls iteration inside the groups loop, eg:
+	 *     while ( bp_groups() ) { ...
+	 *
+	 * @since 1.2.0
+	 *
+	 * @see bp_groups()
+	 *
+	 * @return bool True if there are more groups to show, otherwise false.
+	 */
+	function groups() {
+		if ( $this->current_group + 1 < $this->group_count ) {
+			return true;
+		} elseif ( $this->current_group + 1 == $this->group_count ) {
+
+			/**
+			 * Fires right before the rewinding of groups list.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action('group_loop_end');
+			// Do some cleaning up after the loop.
+			$this->rewind_groups();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Set up the current group inside the loop.
+	 *
+	 * Used by {@link bp_the_group()} to set up the current group data
+	 * while looping, so that template tags used during that iteration make
+	 * reference to the current member.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @see bp_the_group()
+	 */
+	function the_group() {
+		$this->in_the_loop = true;
+		$this->group       = $this->next_group();
+
+		if ( 0 == $this->current_group ) {
+
+			/**
+			 * Fires if the current group item is the first in the loop.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'group_loop_start' );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-theme-compat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-theme-compat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-theme-compat.php	(revision 12265)
@@ -0,0 +1,267 @@
+<?php
+/**
+ * BuddyPress Groups Theme Compat.
+ *
+ * @package BuddyPress
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main theme compat class for BuddyPress Groups.
+ *
+ * This class sets up the necessary theme compatibility actions to safely output
+ * group template parts to the_title and the_content areas of a theme.
+ *
+ * @since 1.7.0
+ */
+class BP_Groups_Theme_Compat {
+
+	/**
+	 * Set up theme compatibility for the Groups component.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __construct() {
+		add_action( 'bp_setup_theme_compat', array( $this, 'is_group' ) );
+	}
+
+	/**
+	 * Are we looking at something that needs group theme compatibility?
+	 *
+	 * @since 1.7.0
+	 */
+	public function is_group() {
+
+		// Bail if not looking at a group.
+		if ( ! bp_is_groups_component() )
+			return;
+
+		// Group Directory.
+		if ( bp_is_groups_directory() ) {
+			bp_update_is_directory( true, 'groups' );
+
+			/**
+			 * Fires at the start of the group theme compatibility setup.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'groups_directory_groups_setup' );
+
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'directory_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'directory_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'directory_content'    ) );
+
+		// Creating a group.
+		} elseif ( bp_is_groups_component() && bp_is_current_action( 'create' ) ) {
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'create_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'create_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'create_content'    ) );
+
+		// Group page.
+		} elseif ( bp_is_single_item() ) {
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'single_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'single_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'single_content'    ) );
+
+		}
+	}
+
+	/** Directory *********************************************************/
+
+	/**
+	 * Add template hierarchy to theme compat for the group directory page.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function directory_template_hierarchy( $templates ) {
+		// Set up the template hierarchy.
+		$new_templates = array();
+		if ( '' !== bp_get_current_group_directory_type() ) {
+			$new_templates[] = 'groups/index-directory-type-' . sanitize_file_name( bp_get_current_group_directory_type() ) . '.php';
+		}
+		$new_templates[] = 'groups/index-directory.php';
+
+		/**
+		 * Filters the Groups directory page template hierarchy based on priority.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of default template files to use.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_groups_directory', $new_templates );
+
+		// Merge new templates with existing stack.
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with directory data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_dummy_post() {
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => bp_get_directory_title( 'groups' ),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the groups index template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_content() {
+		return bp_buffer_template_part( 'groups/index', null, false );
+	}
+
+	/** Create ************************************************************/
+
+	/**
+	 * Add custom template hierarchy to theme compat for the group create page.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function create_template_hierarchy( $templates ) {
+
+		/**
+		 * Filters the Groups create page template hierarchy based on priority.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of default template files to use.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_groups_create', array(
+			'groups/index-create.php'
+		) );
+
+		// Merge new templates with existing stack.
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with create screen data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function create_dummy_post() {
+
+		$title = _x( 'Groups', 'Group creation page', 'buddypress' );
+
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => $title,
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the create screen template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function create_content() {
+		return bp_buffer_template_part( 'groups/create', null, false );
+	}
+
+	/** Single ************************************************************/
+
+	/**
+	 * Add custom template hierarchy to theme compat for group pages.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function single_template_hierarchy( $templates ) {
+		// Setup some variables we're going to reference in our custom templates.
+		$group = groups_get_current_group();
+
+		/**
+		 * Filters the Groups single pages template hierarchy based on priority.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of default template files to use.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_groups_single_item', array(
+			'groups/single/index-id-'     . sanitize_file_name( bp_get_current_group_id() )   . '.php',
+			'groups/single/index-slug-'   . sanitize_file_name( bp_get_current_group_slug() ) . '.php',
+			'groups/single/index-action-' . sanitize_file_name( bp_current_action() )         . '.php',
+			'groups/single/index-status-' . sanitize_file_name( $group->status )              . '.php',
+			'groups/single/index.php'
+		) );
+
+		// Merge new templates with existing stack.
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with single group data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function single_dummy_post() {
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => bp_get_current_group_name(),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the single group template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function single_content() {
+		return bp_buffer_template_part( 'groups/single/home', null, false );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/classes/class-bp-groups-widget.php	(revision 12265)
@@ -0,0 +1,234 @@
+<?php
+/**
+ * BuddyPress Groups Widget.
+ *
+ * @package BuddyPress
+ * @subpackage GroupsWidgets
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Groups widget.
+ *
+ * @since 1.0.3
+ */
+class BP_Groups_Widget extends WP_Widget {
+
+	/**
+	 * Working as a group, we get things done better.
+	 *
+	 * @since 1.0.3
+	 */
+	public function __construct() {
+		$widget_ops = array(
+			'description'                 => __( 'A dynamic list of recently active, popular, newest, or alphabetical groups', 'buddypress' ),
+			'classname'                   => 'widget_bp_groups_widget buddypress widget',
+			'customize_selective_refresh' => true,
+		);
+		parent::__construct( false, _x( '(BuddyPress) Groups', 'widget name', 'buddypress' ), $widget_ops );
+
+		if ( is_customize_preview() || is_active_widget( false, false, $this->id_base ) ) {
+			add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+		}
+	}
+
+	/**
+	 * Enqueue scripts.
+	 *
+	 * @since 2.6.0
+	 */
+	public function enqueue_scripts() {
+		$min = bp_core_get_minified_asset_suffix();
+		wp_enqueue_script( 'groups_widget_groups_list-js', buddypress()->plugin_url . "bp-groups/js/widget-groups{$min}.js", array( 'jquery' ), bp_get_version() );
+	}
+
+	/**
+	 * Extends our front-end output method.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $args     Array of arguments for the widget.
+	 * @param array $instance Widget instance data.
+	 */
+	public function widget( $args, $instance ) {
+		global $groups_template;
+
+		/**
+		 * Filters the user ID to use with the widget instance.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Empty user ID.
+		 */
+		$user_id = apply_filters( 'bp_group_widget_user_id', '0' );
+
+		extract( $args );
+
+		if ( empty( $instance['group_default'] ) ) {
+			$instance['group_default'] = 'popular';
+		}
+
+		if ( empty( $instance['title'] ) ) {
+			$instance['title'] = __( 'Groups', 'buddypress' );
+		}
+
+		/**
+		 * Filters the title of the Groups widget.
+		 *
+		 * @since 1.8.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
+
+		/**
+		 * Filters the separator of the group widget links.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param string $separator Separator string. Default '|'.
+		 */
+		$separator = apply_filters( 'bp_groups_widget_separator', '|' );
+
+		echo $before_widget;
+
+		$title = ! empty( $instance['link_title'] ) ? '<a href="' . bp_get_groups_directory_permalink() . '">' . $title . '</a>' : $title;
+
+		echo $before_title . $title . $after_title;
+
+		$max_groups = ! empty( $instance['max_groups'] ) ? (int) $instance['max_groups'] : 5;
+
+		$group_args = array(
+			'user_id'         => $user_id,
+			'type'            => $instance['group_default'],
+			'per_page'        => $max_groups,
+			'max'             => $max_groups,
+		);
+
+		// Back up the global.
+		$old_groups_template = $groups_template;
+
+		?>
+
+		<?php if ( bp_has_groups( $group_args ) ) : ?>
+			<div class="item-options" id="groups-list-options">
+				<a href="<?php bp_groups_directory_permalink(); ?>" id="newest-groups"<?php if ( $instance['group_default'] == 'newest' ) : ?> class="selected"<?php endif; ?>><?php _e("Newest", 'buddypress') ?></a>
+				<span class="bp-separator" role="separator"><?php echo esc_html( $separator ); ?></span>
+				<a href="<?php bp_groups_directory_permalink(); ?>" id="recently-active-groups"<?php if ( $instance['group_default'] == 'active' ) : ?> class="selected"<?php endif; ?>><?php _e("Active", 'buddypress') ?></a>
+				<span class="bp-separator" role="separator"><?php echo esc_html( $separator ); ?></span>
+				<a href="<?php bp_groups_directory_permalink(); ?>" id="popular-groups" <?php if ( $instance['group_default'] == 'popular' ) : ?> class="selected"<?php endif; ?>><?php _e("Popular", 'buddypress') ?></a>
+				<span class="bp-separator" role="separator"><?php echo esc_html( $separator ); ?></span>
+				<a href="<?php bp_groups_directory_permalink(); ?>" id="alphabetical-groups" <?php if ( $instance['group_default'] == 'alphabetical' ) : ?> class="selected"<?php endif; ?>><?php _e("Alphabetical", 'buddypress') ?></a>
+			</div>
+
+			<ul id="groups-list" class="item-list" aria-live="polite" aria-relevant="all" aria-atomic="true">
+				<?php while ( bp_groups() ) : bp_the_group(); ?>
+					<li <?php bp_group_class(); ?>>
+						<div class="item-avatar">
+							<a href="<?php bp_group_permalink() ?>" class="bp-tooltip" data-bp-tooltip="<?php bp_group_name() ?>"><?php bp_group_avatar_thumb() ?></a>
+						</div>
+
+						<div class="item">
+							<div class="item-title"><?php bp_group_link(); ?></div>
+							<div class="item-meta">
+								<span class="activity">
+								<?php
+									if ( 'newest' == $instance['group_default'] ) {
+										printf( __( 'created %s', 'buddypress' ), bp_get_group_date_created() );
+									} elseif ( 'popular' == $instance['group_default'] ) {
+										bp_group_member_count();
+									} else {
+										printf( __( 'active %s', 'buddypress' ), bp_get_group_last_active() );
+									}
+								?>
+								</span>
+							</div>
+						</div>
+					</li>
+
+				<?php endwhile; ?>
+			</ul>
+			<?php wp_nonce_field( 'groups_widget_groups_list', '_wpnonce-groups' ); ?>
+			<input type="hidden" name="groups_widget_max" id="groups_widget_max" value="<?php echo esc_attr( $max_groups ); ?>" />
+
+		<?php else: ?>
+
+			<div class="widget-error">
+				<?php _e('There are no groups to display.', 'buddypress') ?>
+			</div>
+
+		<?php endif; ?>
+
+		<?php echo $after_widget;
+
+		// Restore the global.
+		$groups_template = $old_groups_template;
+	}
+
+	/**
+	 * Extends our update method.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $new_instance New instance data.
+	 * @param array $old_instance Original instance data.
+	 * @return array
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance = $old_instance;
+
+		$instance['title']         = strip_tags( $new_instance['title'] );
+		$instance['max_groups']    = strip_tags( $new_instance['max_groups'] );
+		$instance['group_default'] = strip_tags( $new_instance['group_default'] );
+		$instance['link_title']    = (bool) $new_instance['link_title'];
+
+		return $instance;
+	}
+
+	/**
+	 * Extends our form method.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $instance Current instance.
+	 * @return mixed
+	 */
+	public function form( $instance ) {
+		$defaults = array(
+			'title'         => __( 'Groups', 'buddypress' ),
+			'max_groups'    => 5,
+			'group_default' => 'active',
+			'link_title'    => false
+		);
+		$instance = bp_parse_args( (array) $instance, $defaults, 'groups_widget_form' );
+
+		$title 	       = strip_tags( $instance['title'] );
+		$max_groups    = strip_tags( $instance['max_groups'] );
+		$group_default = strip_tags( $instance['group_default'] );
+		$link_title    = (bool) $instance['link_title'];
+		?>
+
+		<p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e('Title:', 'buddypress'); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" style="width: 100%" /></label></p>
+
+		<p><label for="<?php echo $this->get_field_id('link_title') ?>"><input type="checkbox" name="<?php echo $this->get_field_name('link_title') ?>" id="<?php echo $this->get_field_id('link_title') ?>" value="1" <?php checked( $link_title ) ?> /> <?php _e( 'Link widget title to Groups directory', 'buddypress' ) ?></label></p>
+
+		<p><label for="<?php echo $this->get_field_id( 'max_groups' ); ?>"><?php _e('Max groups to show:', 'buddypress'); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'max_groups' ); ?>" name="<?php echo $this->get_field_name( 'max_groups' ); ?>" type="text" value="<?php echo esc_attr( $max_groups ); ?>" style="width: 30%" /></label></p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'group_default' ); ?>"><?php _e('Default groups to show:', 'buddypress'); ?></label>
+			<select name="<?php echo $this->get_field_name( 'group_default' ); ?>" id="<?php echo $this->get_field_id( 'group_default' ); ?>">
+				<option value="newest" <?php selected( $group_default, 'newest' ); ?>><?php _e( 'Newest', 'buddypress' ) ?></option>
+				<option value="active" <?php selected( $group_default, 'active' ); ?>><?php _e( 'Active', 'buddypress' ) ?></option>
+				<option value="popular"  <?php selected( $group_default, 'popular' ); ?>><?php _e( 'Popular', 'buddypress' ) ?></option>
+				<option value="alphabetical" <?php selected( $group_default, 'alphabetical' ); ?>><?php _e( 'Alphabetical', 'buddypress' ) ?></option>
+			</select>
+		</p>
+	<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/js/widget-groups.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/js/widget-groups.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/js/widget-groups.js	(revision 12265)
@@ -0,0 +1,60 @@
+jQuery(document).ready( function() {
+	groups_widget_click_handler();
+
+	// WP 4.5 - Customizer selective refresh support.
+	if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) {
+		wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function() {
+			groups_widget_click_handler();
+		} );
+	}
+});
+
+function groups_widget_click_handler() {
+	jQuery('.widget div#groups-list-options a').on('click',
+		function() {
+			var link = this;
+			jQuery(link).addClass('loading');
+
+			jQuery('.widget div#groups-list-options a').removeClass('selected');
+			jQuery(this).addClass('selected');
+
+			jQuery.post( ajaxurl, {
+				action: 'widget_groups_list',
+				'cookie': encodeURIComponent(document.cookie),
+				'_wpnonce': jQuery('input#_wpnonce-groups').val(),
+				'max_groups': jQuery('input#groups_widget_max').val(),
+				'filter': jQuery(this).attr('id')
+			},
+			function(response)
+			{
+				jQuery(link).removeClass('loading');
+				groups_widget_response(response);
+			});
+
+			return false;
+		}
+	);
+}
+
+function groups_widget_response(response) {
+	response = response.substr(0, response.length-1);
+	response = response.split('[[SPLIT]]');
+
+	if ( response[0] !== '-1' ) {
+		jQuery('.widget ul#groups-list').fadeOut(200,
+			function() {
+				jQuery('.widget ul#groups-list').html(response[1]);
+				jQuery('.widget ul#groups-list').fadeIn(200);
+			}
+		);
+
+	} else {
+		jQuery('.widget ul#groups-list').fadeOut(200,
+			function() {
+				var message = '<p>' + response[1] + '</p>';
+				jQuery('.widget ul#groups-list').html(message);
+				jQuery('.widget ul#groups-list').fadeIn(200);
+			}
+		);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/directory.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/directory.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/directory.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Groups: Directory screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of the Groups directory index.
+ *
+ * @since 1.0.0
+ */
+function groups_directory_groups_setup() {
+	if ( bp_is_groups_directory() ) {
+		bp_update_is_directory( true, 'groups' );
+
+		/**
+		 * Fires before the loading of the Groups directory index.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'groups_directory_groups_setup' );
+
+		/**
+		 * Filters the template to load for the Groups directory index.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Path to the groups directory index template to load.
+		 */
+		bp_core_load_template( apply_filters( 'groups_template_directory_groups', 'groups/index' ) );
+	}
+}
+add_action( 'bp_screens', 'groups_directory_groups_setup', 2 );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/activity-permalink.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/activity-permalink.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/activity-permalink.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Groups: Single group activity permalink screen handler
+ *
+ * Note - This has never worked.
+ * See {@link https://buddypress.trac.wordpress.org/ticket/2579}
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a single group activity item.
+ *
+ * @since 1.2.0
+ */
+function groups_screen_group_activity_permalink() {
+	if ( !bp_is_groups_component() || !bp_is_active( 'activity' ) || ( bp_is_active( 'activity' ) && !bp_is_current_action( bp_get_activity_slug() ) ) || !bp_action_variable( 0 ) )
+		return false;
+
+	buddypress()->is_single_item = true;
+
+	/** This filter is documented in bp-groups/bp-groups-screens.php */
+	bp_core_load_template( apply_filters( 'groups_template_group_home', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_activity_permalink' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/activity.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Groups: Single group "Activity" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the loading of a single group's activity.
+ *
+ * @since 2.4.0
+ */
+function groups_screen_group_activity() {
+
+	if ( ! bp_is_single_item() ) {
+		return false;
+	}
+
+	/**
+	 * Fires before the loading of a single group's activity page.
+	 *
+	 * @since 2.4.0
+	 */
+	do_action( 'groups_screen_group_activity' );
+
+	/**
+	 * Filters the template to load for a single group's activity page.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $value Path to a single group's template to load.
+	 */
+	bp_core_load_template( apply_filters( 'groups_screen_group_activity', 'groups/single/activity' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Groups: Single group "Manage" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's Admin pages.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin() {
+	if ( !bp_is_groups_component() || !bp_is_current_action( 'admin' ) )
+		return false;
+
+	if ( bp_action_variables() )
+		return false;
+
+	bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/edit-details/' );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/delete-group.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/delete-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/delete-group.php	(revision 12265)
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Groups: Single group "Manage > Delete" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of the Delete Group page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin_delete_group() {
+
+	if ( 'delete-group' != bp_get_group_current_admin_tab() )
+		return false;
+
+	if ( ! bp_is_item_admin() && !bp_current_user_can( 'bp_moderate' ) )
+		return false;
+
+	$bp = buddypress();
+
+	if ( isset( $_REQUEST['delete-group-button'] ) && isset( $_REQUEST['delete-group-understand'] ) ) {
+
+		// Check the nonce first.
+		if ( !check_admin_referer( 'groups_delete_group' ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires before the deletion of a group from the Delete Group page.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $id ID of the group being deleted.
+		 */
+		do_action( 'groups_before_group_deleted', $bp->groups->current_group->id );
+
+		// Group admin has deleted the group, now do it.
+		if ( !groups_delete_group( $bp->groups->current_group->id ) ) {
+			bp_core_add_message( __( 'There was an error deleting the group. Please try again.', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'The group was deleted successfully.', 'buddypress' ) );
+
+			/**
+			 * Fires after the deletion of a group from the Delete Group page.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int $id ID of the group being deleted.
+			 */
+			do_action( 'groups_group_deleted', $bp->groups->current_group->id );
+
+			bp_core_redirect( trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() ) );
+		}
+
+		bp_core_redirect( trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() ) );
+	}
+
+	/**
+	 * Fires before the loading of the Delete Group page template.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group that is being displayed.
+	 */
+	do_action( 'groups_screen_group_admin_delete_group', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for the Delete Group page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to the Delete Group template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_admin_delete_group', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_delete_group' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/edit-details.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/edit-details.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/edit-details.php	(revision 12265)
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Groups: Single group "Manage > Details" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's admin/edit-details page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin_edit_details() {
+
+	if ( 'edit-details' != bp_get_group_current_admin_tab() )
+		return false;
+
+	if ( bp_is_item_admin() ) {
+
+		$bp = buddypress();
+
+		// If the edit form has been submitted, save the edited details.
+		if ( isset( $_POST['save'] ) ) {
+			// Check the nonce.
+			if ( !check_admin_referer( 'groups_edit_group_details' ) )
+				return false;
+
+			$group_notify_members = isset( $_POST['group-notify-members'] ) ? (int) $_POST['group-notify-members'] : 0;
+
+			// Name and description are required and may not be empty.
+			if ( empty( $_POST['group-name'] ) || empty( $_POST['group-desc'] ) ) {
+				bp_core_add_message( __( 'Groups must have a name and a description. Please try again.', 'buddypress' ), 'error' );
+			} elseif ( ! groups_edit_base_group_details( array(
+				'group_id'       => $_POST['group-id'],
+				'name'           => $_POST['group-name'],
+				'slug'           => null, // @TODO: Add to settings pane? If yes, editable by site admin only, or allow group admins to do this?
+				'description'    => $_POST['group-desc'],
+				'notify_members' => $group_notify_members,
+			) ) ) {
+				bp_core_add_message( __( 'There was an error updating group details. Please try again.', 'buddypress' ), 'error' );
+			} else {
+				bp_core_add_message( __( 'Group details were successfully updated.', 'buddypress' ) );
+			}
+
+			/**
+			 * Fires before the redirect if a group details has been edited and saved.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int $id ID of the group that was edited.
+			 */
+			do_action( 'groups_group_details_edited', $bp->groups->current_group->id );
+
+			bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/edit-details/' );
+		}
+
+		/**
+		 * Fires before the loading of the group admin/edit-details page template.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $id ID of the group that is being displayed.
+		 */
+		do_action( 'groups_screen_group_admin_edit_details', $bp->groups->current_group->id );
+
+		/**
+		 * Filters the template to load for a group's admin/edit-details page.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Path to a group's admin/edit-details template.
+		 */
+		bp_core_load_template( apply_filters( 'groups_template_group_admin', 'groups/single/home' ) );
+	}
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_edit_details' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-avatar.php	(revision 12265)
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Groups: Single group "Manage > Photo" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's Change Avatar page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin_avatar() {
+
+	if ( 'group-avatar' != bp_get_group_current_admin_tab() )
+		return false;
+
+	// If the logged-in user doesn't have permission or if avatar uploads are disabled, then stop here.
+	if ( ! bp_is_item_admin() || bp_disable_group_avatar_uploads() || ! buddypress()->avatar->show_avatars )
+		return false;
+
+	$bp = buddypress();
+
+	// If the group admin has deleted the admin avatar.
+	if ( bp_is_action_variable( 'delete', 1 ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'bp_group_avatar_delete' );
+
+		if ( bp_core_delete_existing_avatar( array( 'item_id' => $bp->groups->current_group->id, 'object' => 'group' ) ) ) {
+			bp_core_add_message( __( 'The group profile photo was deleted successfully!', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was a problem deleting the group profile photo. Please try again.', 'buddypress' ), 'error' );
+		}
+	}
+
+	if ( ! isset( $bp->avatar_admin ) ) {
+		$bp->avatar_admin = new stdClass();
+	}
+
+	$bp->avatar_admin->step = 'upload-image';
+
+	if ( !empty( $_FILES ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'bp_avatar_upload' );
+
+		// Pass the file to the avatar upload handler.
+		if ( bp_core_avatar_handle_upload( $_FILES, 'groups_avatar_upload_dir' ) ) {
+			$bp->avatar_admin->step = 'crop-image';
+
+			// Make sure we include the jQuery jCrop file for image cropping.
+			add_action( 'wp_print_scripts', 'bp_core_add_jquery_cropper' );
+		}
+
+	}
+
+	// If the image cropping is done, crop the image and save a full/thumb version.
+	if ( isset( $_POST['avatar-crop-submit'] ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'bp_avatar_cropstore' );
+
+		$args = array(
+			'object'        => 'group',
+			'avatar_dir'    => 'group-avatars',
+			'item_id'       => $bp->groups->current_group->id,
+			'original_file' => $_POST['image_src'],
+			'crop_x'        => $_POST['x'],
+			'crop_y'        => $_POST['y'],
+			'crop_w'        => $_POST['w'],
+			'crop_h'        => $_POST['h']
+		);
+
+		if ( !bp_core_avatar_handle_crop( $args ) ) {
+			bp_core_add_message( __( 'There was a problem cropping the group profile photo.', 'buddypress' ), 'error' );
+		} else {
+			/**
+			 * Fires after a group avatar is uploaded.
+			 *
+			 * @since 2.8.0
+			 *
+			 * @param int    $group_id ID of the group.
+			 * @param string $type     Avatar type. 'crop' or 'full'.
+			 * @param array  $args     Array of parameters passed to the avatar handler.
+			 */
+			do_action( 'groups_avatar_uploaded', bp_get_current_group_id(), 'crop', $args );
+			bp_core_add_message( __( 'The new group profile photo was uploaded successfully.', 'buddypress' ) );
+		}
+	}
+
+	/**
+	 * Fires before the loading of the group Change Avatar page template.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group that is being displayed.
+	 */
+	do_action( 'groups_screen_group_admin_avatar', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for a group's Change Avatar page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a group's Change Avatar template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_admin_avatar', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_avatar' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-cover-image.php	(revision 12265)
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Groups: Single group "Manage > Cover Image" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's Change cover image page.
+ *
+ * @since 2.4.0
+ */
+function groups_screen_group_admin_cover_image() {
+	if ( 'group-cover-image' != bp_get_group_current_admin_tab() ) {
+		return false;
+	}
+
+	// If the logged-in user doesn't have permission or if cover image uploads are disabled, then stop here.
+	if ( ! bp_is_item_admin() || ! bp_group_use_cover_image_header() ) {
+		return false;
+	}
+
+	/**
+	 * Fires before the loading of the group Change cover image page template.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param int $id ID of the group that is being displayed.
+	 */
+	do_action( 'groups_screen_group_admin_cover_image', bp_get_current_group_id() );
+
+	/**
+	 * Filters the template to load for a group's Change cover image page.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $value Path to a group's Change cover image template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_admin_cover_image', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_cover_image' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/group-settings.php	(revision 12265)
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Groups: Single group "Manage > Settings" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's admin/group-settings page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin_settings() {
+
+	if ( 'group-settings' != bp_get_group_current_admin_tab() )
+		return false;
+
+	if ( ! bp_is_item_admin() )
+		return false;
+
+	$bp = buddypress();
+
+	// If the edit form has been submitted, save the edited details.
+	if ( isset( $_POST['save'] ) ) {
+		$enable_forum   = ( isset($_POST['group-show-forum'] ) ) ? 1 : 0;
+
+		// Checked against a whitelist for security.
+		/** This filter is documented in bp-groups/bp-groups-admin.php */
+		$allowed_status = apply_filters( 'groups_allowed_status', array( 'public', 'private', 'hidden' ) );
+		$status         = ( in_array( $_POST['group-status'], (array) $allowed_status ) ) ? $_POST['group-status'] : 'public';
+
+		// Checked against a whitelist for security.
+		/** This filter is documented in bp-groups/bp-groups-admin.php */
+		$allowed_invite_status = apply_filters( 'groups_allowed_invite_status', array( 'members', 'mods', 'admins' ) );
+		$invite_status	       = isset( $_POST['group-invite-status'] ) && in_array( $_POST['group-invite-status'], (array) $allowed_invite_status ) ? $_POST['group-invite-status'] : 'members';
+
+		// Check the nonce.
+		if ( !check_admin_referer( 'groups_edit_group_settings' ) )
+			return false;
+
+		/*
+		 * Save group types.
+		 *
+		 * Ensure we keep types that have 'show_in_create_screen' set to false.
+		 */
+		$current_types = bp_groups_get_group_type( bp_get_current_group_id(), false );
+		$current_types = array_intersect( bp_groups_get_group_types( array( 'show_in_create_screen' => false ) ), (array) $current_types );
+		if ( isset( $_POST['group-types'] ) ) {
+			$current_types = array_merge( $current_types, $_POST['group-types'] );
+
+			// Set group types.
+			bp_groups_set_group_type( bp_get_current_group_id(), $current_types );
+
+		// No group types checked, so this means we want to wipe out all group types.
+		} else {
+			/*
+			 * Passing a blank string will wipe out all types for the group.
+			 *
+			 * Ensure we keep types that have 'show_in_create_screen' set to false.
+			 */
+			$current_types = empty( $current_types ) ? '' : $current_types;
+
+			// Set group types.
+			bp_groups_set_group_type( bp_get_current_group_id(), $current_types );
+		}
+
+		if ( !groups_edit_group_settings( $_POST['group-id'], $enable_forum, $status, $invite_status ) ) {
+			bp_core_add_message( __( 'There was an error updating group settings. Please try again.', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'Group settings were successfully updated.', 'buddypress' ) );
+		}
+
+		/**
+		 * Fires before the redirect if a group settings has been edited and saved.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $id ID of the group that was edited.
+		 */
+		do_action( 'groups_group_settings_edited', $bp->groups->current_group->id );
+
+		bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/group-settings/' );
+	}
+
+	/**
+	 * Fires before the loading of the group admin/group-settings page template.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group that is being displayed.
+	 */
+	do_action( 'groups_screen_group_admin_settings', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for a group's admin/group-settings page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a group's admin/group-settings template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_admin_settings', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_settings' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/manage-members.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/manage-members.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/manage-members.php	(revision 12265)
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Groups: Single group "Manage > Members" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * This function handles actions related to member management on the group admin.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin_manage_members() {
+
+	if ( 'manage-members' != bp_get_group_current_admin_tab() )
+		return false;
+
+	if ( ! bp_is_item_admin() )
+		return false;
+
+	$bp = buddypress();
+
+	if ( bp_action_variable( 1 ) && bp_action_variable( 2 ) && bp_action_variable( 3 ) ) {
+		if ( bp_is_action_variable( 'promote', 1 ) && ( bp_is_action_variable( 'mod', 2 ) || bp_is_action_variable( 'admin', 2 ) ) && is_numeric( bp_action_variable( 3 ) ) ) {
+			$user_id = bp_action_variable( 3 );
+			$status  = bp_action_variable( 2 );
+
+			// Check the nonce first.
+			if ( !check_admin_referer( 'groups_promote_member' ) )
+				return false;
+
+			// Promote a user.
+			if ( !groups_promote_member( $user_id, $bp->groups->current_group->id, $status ) )
+				bp_core_add_message( __( 'There was an error when promoting that user. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'User promoted successfully', 'buddypress' ) );
+
+			/**
+			 * Fires before the redirect after a group member has been promoted.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int $user_id ID of the user being promoted.
+			 * @param int $id      ID of the group user is promoted within.
+			 */
+			do_action( 'groups_promoted_member', $user_id, $bp->groups->current_group->id );
+
+			bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/manage-members/' );
+		}
+	}
+
+	if ( bp_action_variable( 1 ) && bp_action_variable( 2 ) ) {
+		if ( bp_is_action_variable( 'demote', 1 ) && is_numeric( bp_action_variable( 2 ) ) ) {
+			$user_id = bp_action_variable( 2 );
+
+			// Check the nonce first.
+			if ( !check_admin_referer( 'groups_demote_member' ) )
+				return false;
+
+			// Stop sole admins from abandoning their group.
+			$group_admins = groups_get_group_admins( $bp->groups->current_group->id );
+			if ( 1 == count( $group_admins ) && $group_admins[0]->user_id == $user_id )
+				bp_core_add_message( __( 'This group must have at least one admin', 'buddypress' ), 'error' );
+
+			// Demote a user.
+			elseif ( !groups_demote_member( $user_id, $bp->groups->current_group->id ) )
+				bp_core_add_message( __( 'There was an error when demoting that user. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'User demoted successfully', 'buddypress' ) );
+
+			/**
+			 * Fires before the redirect after a group member has been demoted.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int $user_id ID of the user being demoted.
+			 * @param int $id      ID of the group user is demoted within.
+			 */
+			do_action( 'groups_demoted_member', $user_id, $bp->groups->current_group->id );
+
+			bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/manage-members/' );
+		}
+
+		if ( bp_is_action_variable( 'ban', 1 ) && is_numeric( bp_action_variable( 2 ) ) ) {
+			$user_id = bp_action_variable( 2 );
+
+			// Check the nonce first.
+			if ( !check_admin_referer( 'groups_ban_member' ) )
+				return false;
+
+			// Ban a user.
+			if ( !groups_ban_member( $user_id, $bp->groups->current_group->id ) )
+				bp_core_add_message( __( 'There was an error when banning that user. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'User banned successfully', 'buddypress' ) );
+
+			/**
+			 * Fires before the redirect after a group member has been banned.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int $user_id ID of the user being banned.
+			 * @param int $id      ID of the group user is banned from.
+			 */
+			do_action( 'groups_banned_member', $user_id, $bp->groups->current_group->id );
+
+			bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/manage-members/' );
+		}
+
+		if ( bp_is_action_variable( 'unban', 1 ) && is_numeric( bp_action_variable( 2 ) ) ) {
+			$user_id = bp_action_variable( 2 );
+
+			// Check the nonce first.
+			if ( !check_admin_referer( 'groups_unban_member' ) )
+				return false;
+
+			// Remove a ban for user.
+			if ( !groups_unban_member( $user_id, $bp->groups->current_group->id ) )
+				bp_core_add_message( __( 'There was an error when unbanning that user. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'User ban removed successfully', 'buddypress' ) );
+
+			/**
+			 * Fires before the redirect after a group member has been unbanned.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int $user_id ID of the user being unbanned.
+			 * @param int $id      ID of the group user is unbanned from.
+			 */
+			do_action( 'groups_unbanned_member', $user_id, $bp->groups->current_group->id );
+
+			bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/manage-members/' );
+		}
+
+		if ( bp_is_action_variable( 'remove', 1 ) && is_numeric( bp_action_variable( 2 ) ) ) {
+			$user_id = bp_action_variable( 2 );
+
+			// Check the nonce first.
+			if ( !check_admin_referer( 'groups_remove_member' ) )
+				return false;
+
+			// Remove a user.
+			if ( !groups_remove_member( $user_id, $bp->groups->current_group->id ) )
+				bp_core_add_message( __( 'There was an error removing that user from the group. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'User removed successfully', 'buddypress' ) );
+
+			/**
+			 * Fires before the redirect after a group member has been removed.
+			 *
+			 * @since 1.2.6
+			 *
+			 * @param int $user_id ID of the user being removed.
+			 * @param int $id      ID of the group the user is removed from.
+			 */
+			do_action( 'groups_removed_member', $user_id, $bp->groups->current_group->id );
+
+			bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/manage-members/' );
+		}
+	}
+
+	/**
+	 * Fires before the loading of a group's manage members template.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group whose manage members page is being displayed.
+	 */
+	do_action( 'groups_screen_group_admin_manage_members', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for a group's manage members page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a group's manage members template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_admin_manage_members', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_manage_members' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/membership-requests.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/membership-requests.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/admin/membership-requests.php	(revision 12265)
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Groups: Single group "Manage > Requests" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of Admin > Membership Requests.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_admin_requests() {
+	$bp = buddypress();
+
+	if ( 'membership-requests' != bp_get_group_current_admin_tab() ) {
+		return false;
+	}
+
+	if ( ! bp_is_item_admin() || ( 'public' == $bp->groups->current_group->status ) ) {
+		return false;
+	}
+
+	$request_action = (string) bp_action_variable( 1 );
+	$membership_id  = (int) bp_action_variable( 2 );
+
+	if ( !empty( $request_action ) && !empty( $membership_id ) ) {
+		if ( 'accept' == $request_action && is_numeric( $membership_id ) ) {
+
+			// Check the nonce first.
+			if ( !check_admin_referer( 'groups_accept_membership_request' ) )
+				return false;
+
+			// Accept the membership request.
+			if ( !groups_accept_membership_request( $membership_id ) )
+				bp_core_add_message( __( 'There was an error accepting the membership request. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'Group membership request accepted', 'buddypress' ) );
+
+		} elseif ( 'reject' == $request_action && is_numeric( $membership_id ) ) {
+			/* Check the nonce first. */
+			if ( !check_admin_referer( 'groups_reject_membership_request' ) )
+				return false;
+
+			// Reject the membership request.
+			if ( !groups_reject_membership_request( $membership_id ) )
+				bp_core_add_message( __( 'There was an error rejecting the membership request. Please try again.', 'buddypress' ), 'error' );
+			else
+				bp_core_add_message( __( 'Group membership request rejected', 'buddypress' ) );
+		}
+
+		/**
+		 * Fires before the redirect if a group membership request has been handled.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int    $id             ID of the group that was edited.
+		 * @param string $request_action Membership request action being performed.
+		 * @param int    $membership_id  The key of the action_variables array that you want.
+		 */
+		do_action( 'groups_group_request_managed', $bp->groups->current_group->id, $request_action, $membership_id );
+		bp_core_redirect( bp_get_group_permalink( groups_get_current_group() ) . 'admin/membership-requests/' );
+	}
+
+	/**
+	 * Fires before the loading of the group membership request page template.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group that is being displayed.
+	 */
+	do_action( 'groups_screen_group_admin_requests', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for a group's membership request page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a group's membership request template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_admin_requests', 'groups/single/home' ) );
+}
+add_action( 'bp_screens', 'groups_screen_group_admin_requests' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/home.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Groups: Single group "Home" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the loading of a single group's page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_home() {
+
+	if ( ! bp_is_single_item() ) {
+		return false;
+	}
+
+	/**
+	 * Fires before the loading of a single group's page.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'groups_screen_group_home' );
+
+	/**
+	 * Filters the template to load for a single group's page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a single group's template to load.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_home', 'groups/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/members.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/members.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/members.php	(revision 12265)
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Groups: Single group "Members" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's Members page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_members() {
+
+	if ( !bp_is_single_item() )
+		return false;
+
+	$bp = buddypress();
+
+	// Refresh the group member count meta.
+	groups_update_groupmeta( $bp->groups->current_group->id, 'total_member_count', groups_get_total_member_count( $bp->groups->current_group->id ) );
+
+	/**
+	 * Fires before the loading of a group's Members page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group whose members are being displayed.
+	 */
+	do_action( 'groups_screen_group_members', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for a group's Members page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a group's Members template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_members', 'groups/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/request-membership.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/request-membership.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/request-membership.php	(revision 12265)
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Groups: Single group "Request Membership" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's Request Membership page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_request_membership() {
+
+	if ( !is_user_logged_in() )
+		return false;
+
+	$bp = buddypress();
+
+	if ( 'private' != $bp->groups->current_group->status )
+		return false;
+
+	// If the user is already invited, accept invitation.
+	if ( groups_check_user_has_invite( bp_loggedin_user_id(), $bp->groups->current_group->id ) ) {
+		if ( groups_accept_invite( bp_loggedin_user_id(), $bp->groups->current_group->id ) )
+			bp_core_add_message( __( 'Group invite accepted', 'buddypress' ) );
+		else
+			bp_core_add_message( __( 'There was an error accepting the group invitation. Please try again.', 'buddypress' ), 'error' );
+		bp_core_redirect( bp_get_group_permalink( $bp->groups->current_group ) );
+	}
+
+	// If the user has submitted a request, send it.
+	if ( isset( $_POST['group-request-send']) ) {
+
+		// Check the nonce.
+		if ( !check_admin_referer( 'groups_request_membership' ) )
+			return false;
+
+		if ( !groups_send_membership_request( bp_loggedin_user_id(), $bp->groups->current_group->id ) ) {
+			bp_core_add_message( __( 'There was an error sending your group membership request. Please try again.', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'Your membership request was sent to the group administrator successfully. You will be notified when the group administrator responds to your request.', 'buddypress' ) );
+		}
+		bp_core_redirect( bp_get_group_permalink( $bp->groups->current_group ) );
+	}
+
+	/**
+	 * Fires before the loading of a group's Request Memebership page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $id ID of the group currently being displayed.
+	 */
+	do_action( 'groups_screen_group_request_membership', $bp->groups->current_group->id );
+
+	/**
+	 * Filters the template to load for a group's Request Membership page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a group's Request Membership template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_request_membership', 'groups/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/single/send-invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/single/send-invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/single/send-invites.php	(revision 12265)
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Groups: Single group "Send Invites" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of a group's Send Invites page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_invite() {
+
+	if ( !bp_is_single_item() )
+		return false;
+
+	$bp = buddypress();
+
+	if ( bp_is_action_variable( 'send', 0 ) ) {
+
+		if ( !check_admin_referer( 'groups_send_invites', '_wpnonce_send_invites' ) )
+			return false;
+
+		if ( !empty( $_POST['friends'] ) ) {
+			foreach( (array) $_POST['friends'] as $friend ) {
+				groups_invite_user( array( 'user_id' => $friend, 'group_id' => $bp->groups->current_group->id ) );
+			}
+		}
+
+		// Send the invites.
+		groups_send_invites( bp_loggedin_user_id(), $bp->groups->current_group->id );
+		bp_core_add_message( __('Group invites sent.', 'buddypress') );
+
+		/**
+		 * Fires after the sending of a group invite inside the group's Send Invites page.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $id ID of the group whose members are being displayed.
+		 */
+		do_action( 'groups_screen_group_invite', $bp->groups->current_group->id );
+		bp_core_redirect( bp_get_group_permalink( $bp->groups->current_group ) );
+
+	} elseif ( !bp_action_variable( 0 ) ) {
+
+		/**
+		 * Filters the template to load for a group's Send Invites page.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Path to a group's Send Invites template.
+		 */
+		bp_core_load_template( apply_filters( 'groups_template_group_invite', 'groups/single/home' ) );
+
+	} else {
+		bp_do_404();
+	}
+}
+
+/**
+ * Process group invitation removal requests.
+ *
+ * Note that this function is only used when JS is disabled. Normally, clicking
+ * Remove Invite removes the invitation via AJAX.
+ *
+ * @since 2.0.0
+ */
+function groups_remove_group_invite() {
+	if ( ! bp_is_group_invites() ) {
+		return;
+	}
+
+	if ( ! bp_is_action_variable( 'remove', 0 ) || ! is_numeric( bp_action_variable( 1 ) ) ) {
+		return;
+	}
+
+	if ( ! check_admin_referer( 'groups_invite_uninvite_user' ) ) {
+		return false;
+	}
+
+	$friend_id = intval( bp_action_variable( 1 ) );
+	$group_id  = bp_get_current_group_id();
+	$message   = __( 'Invite successfully removed', 'buddypress' );
+	$redirect  = wp_get_referer();
+	$error     = false;
+
+	if ( ! bp_groups_user_can_send_invites( $group_id ) ) {
+		$message = __( 'You are not allowed to send or remove invites', 'buddypress' );
+		$error = 'error';
+	} elseif ( groups_check_for_membership_request( $friend_id, $group_id ) ) {
+		$message = __( 'The member requested to join the group', 'buddypress' );
+		$error = 'error';
+	} elseif ( ! groups_uninvite_user( $friend_id, $group_id ) ) {
+		$message = __( 'There was an error removing the invite', 'buddypress' );
+		$error = 'error';
+	}
+
+	bp_core_add_message( $message, $error );
+	bp_core_redirect( $redirect );
+}
+add_action( 'bp_screens', 'groups_remove_group_invite' );
Index: /tags/4.0.0-beta1/src/bp-groups/screens/user/invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/user/invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/user/invites.php	(revision 12265)
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Groups: User's "Groups > Invites" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the loading of a user's Groups > Invites page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_group_invites() {
+	$group_id = (int)bp_action_variable( 1 );
+
+	if ( bp_is_action_variable( 'accept' ) && is_numeric( $group_id ) ) {
+		// Check the nonce.
+		if ( !check_admin_referer( 'groups_accept_invite' ) )
+			return false;
+
+		if ( !groups_accept_invite( bp_loggedin_user_id(), $group_id ) ) {
+			bp_core_add_message( __('Group invite could not be accepted', 'buddypress'), 'error' );
+		} else {
+			// Record this in activity streams.
+			$group = groups_get_group( $group_id );
+
+			bp_core_add_message( sprintf( __( 'Group invite accepted. Visit %s.', 'buddypress' ), bp_get_group_link( $group ) ) );
+
+			if ( bp_is_active( 'activity' ) ) {
+				groups_record_activity( array(
+					'type'    => 'joined_group',
+					'item_id' => $group->id
+				) );
+			}
+		}
+
+		if ( isset( $_GET['redirect_to'] ) ) {
+			$redirect_to = urldecode( $_GET['redirect_to'] );
+		} else {
+			$redirect_to = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() . '/' . bp_current_action() );
+		}
+
+		bp_core_redirect( $redirect_to );
+
+	} elseif ( bp_is_action_variable( 'reject' ) && is_numeric( $group_id ) ) {
+		// Check the nonce.
+		if ( !check_admin_referer( 'groups_reject_invite' ) )
+			return false;
+
+		if ( !groups_reject_invite( bp_loggedin_user_id(), $group_id ) ) {
+			bp_core_add_message( __( 'Group invite could not be rejected', 'buddypress' ), 'error' );
+		} else {
+			bp_core_add_message( __( 'Group invite rejected', 'buddypress' ) );
+		}
+
+		if ( isset( $_GET['redirect_to'] ) ) {
+			$redirect_to = urldecode( $_GET['redirect_to'] );
+		} else {
+			$redirect_to = trailingslashit( bp_loggedin_user_domain() . bp_get_groups_slug() . '/' . bp_current_action() );
+		}
+
+		bp_core_redirect( $redirect_to );
+	}
+
+	/**
+	 * Fires before the loading of a users Groups > Invites template.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $group_id ID of the group being displayed
+	 */
+	do_action( 'groups_screen_group_invites', $group_id );
+
+	/**
+	 * Filters the template to load for a users Groups > Invites page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to a users Groups > Invites page template.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_group_invites', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-groups/screens/user/my-groups.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-groups/screens/user/my-groups.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-groups/screens/user/my-groups.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Groups: User's "Groups" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage GroupScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the loading of the My Groups page.
+ *
+ * @since 1.0.0
+ */
+function groups_screen_my_groups() {
+
+	/**
+	 * Fires before the loading of the My Groups page.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'groups_screen_my_groups' );
+
+	/**
+	 * Filters the template to load for the My Groups page.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Path to the My Groups page template to load.
+	 */
+	bp_core_load_template( apply_filters( 'groups_template_my_groups', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-loader.php	(revision 12265)
@@ -0,0 +1,92 @@
+<?php
+/**
+ * The BuddyPress Plugin.
+ *
+ * BuddyPress is social networking software with a twist from the creators of WordPress.
+ *
+ * @package BuddyPress
+ * @subpackage Main
+ * @since 1.0.0
+ */
+
+/**
+ * Plugin Name: BuddyPress
+ * Plugin URI:  https://buddypress.org/
+ * Description: BuddyPress adds community features to WordPress. Member Profiles, Activity Streams, Direct Messaging, Notifications, and more!
+ * Author:      The BuddyPress Community
+ * Author URI:  https://buddypress.org/
+ * Version:     4.0.0-beta1
+ * Text Domain: buddypress
+ * Domain Path: /bp-languages/
+ * License:     GPLv2 or later (license.txt)
+ */
+
+/**
+ * This files should always remain compatible with the minimum version of
+ * PHP supported by WordPress.
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+// Required PHP version.
+define( 'BP_REQUIRED_PHP_VERSION', '5.3.0' );
+
+/**
+ * The main function responsible for returning the one true BuddyPress Instance to functions everywhere.
+ *
+ * Use this function like you would a global variable, except without needing
+ * to declare the global.
+ *
+ * Example: <?php $bp = buddypress(); ?>
+ *
+ * @return BuddyPress|null The one true BuddyPress Instance.
+ */
+function buddypress() {
+	return BuddyPress::instance();
+}
+
+/**
+ * Adds an admin notice to installations that don't meet BP's minimum PHP requirement.
+ *
+ * @since 2.8.0
+ */
+function bp_php_requirements_notice() {
+	if ( ! current_user_can( 'update_core' ) ) {
+		return;
+	}
+
+	?>
+
+	<div id="message" class="error notice">
+		<p><strong><?php esc_html_e( 'Your site does not support BuddyPress.', 'buddypress' ); ?></strong></p>
+		<?php /* translators: 1: current PHP version, 2: required PHP version */ ?>
+		<p><?php printf( esc_html__( 'Your site is currently running PHP version %1$s, while BuddyPress requires version %2$s or greater.', 'buddypress' ), esc_html( phpversion() ), esc_html( BP_REQUIRED_PHP_VERSION ) ); ?> <?php printf( __( 'See <a href="%s">the Codex guide</a> for more information.', 'buddypress' ), 'https://codex.buddypress.org/getting-started/buddypress-2-8-will-require-php-5-3/' ); ?></p>
+		<p><?php esc_html_e( 'Please update your server or deactivate BuddyPress.', 'buddypress' ); ?></p>
+	</div>
+
+	<?php
+}
+
+if ( version_compare( phpversion(), BP_REQUIRED_PHP_VERSION, '<' ) ) {
+	add_action( 'admin_notices', 'bp_php_requirements_notice' );
+	add_action( 'network_admin_notices', 'bp_php_requirements_notice' );
+	return;
+} else {
+	require dirname( __FILE__ ) . '/class-buddypress.php';
+
+	/*
+	 * Hook BuddyPress early onto the 'plugins_loaded' action.
+	 *
+	 * This gives all other plugins the chance to load before BuddyPress,
+	 * to get their actions, filters, and overrides setup without
+	 * BuddyPress being in the way.
+	 */
+	if ( defined( 'BUDDYPRESS_LATE_LOAD' ) ) {
+		add_action( 'plugins_loaded', 'buddypress', (int) BUDDYPRESS_LATE_LOAD );
+
+	// "And now here's something we hope you'll really like!"
+	} else {
+		$GLOBALS['bp'] = buddypress();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/actions/random.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/actions/random.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/actions/random.php	(revision 12265)
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Members: Random member action handler
+ *
+ * @package BuddyPress
+ * @subpackage MembersActions
+ * @since 3.0.0
+ */
+
+/**
+ * Redirect to a random member page when visiting a ?random-member URL.
+ *
+ * @since 1.0.0
+ */
+function bp_core_get_random_member() {
+	if ( ! isset( $_GET['random-member'] ) )
+		return;
+
+	$user = bp_core_get_users( array( 'type' => 'random', 'per_page' => 1 ) );
+	bp_core_redirect( bp_core_get_user_domain( $user['users'][0]->id ) );
+}
+add_action( 'bp_actions', 'bp_core_get_random_member' );
Index: /tags/4.0.0-beta1/src/bp-members/admin/bp-members-admin-classes.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/admin/bp-members-admin-classes.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/admin/bp-members-admin-classes.php	(revision 12265)
@@ -0,0 +1,19 @@
+<?php
+/**
+ * BuddyPress Members List Classes.
+ *
+ * @package BuddyPress
+ * @subpackage MembersAdminClasses
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( class_exists( 'WP_Users_List_Table' ) ) {
+	require dirname( dirname( __FILE__ ) ) . '/classes/class-bp-members-list-table.php';
+}
+
+if ( class_exists( 'WP_MS_Users_List_Table' ) ) {
+	require dirname( dirname( __FILE__ ) ) . '/classes/class-bp-members-ms-list-table.php';
+}
Index: /tags/4.0.0-beta1/src/bp-members/admin/css/admin-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/admin/css/admin-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/admin/css/admin-rtl.css	(revision 12265)
@@ -0,0 +1,304 @@
+/**** BP Members Profile Administration Screens
+* TO DO: Organize and clean up stylesheet.
+****/
+
+div#profile-page.wrap form#your-profile {
+	position: relative;
+	padding-top: 50px;
+}
+
+div#profile-page.wrap form#your-profile h3:first-of-type {
+	margin-top: 5em;
+}
+
+div#profile-page.wrap form#your-profile #profile-nav {
+	position: absolute;
+	top: 0;
+	width: 97%;
+}
+
+div#community-profile-page #profile-nav {
+	margin-bottom: 1em;
+}
+
+#bp_members_admin_user_stats ul {
+	margin-bottom: 0;
+}
+
+div#community-profile-page li.bp-members-profile-stats:before,
+div#community-profile-page li.bp-friends-profile-stats:before,
+div#community-profile-page li.bp-groups-profile-stats:before,
+div#community-profile-page li.bp-blogs-profile-stats:before,
+div#community-profile-page a.bp-xprofile-avatar-user-admin:before,
+div#community-profile-page a.bp-xprofile-avatar-user-edit:before {
+	font: 400 20px/1 dashicons;
+	speak: none;
+	display: inline-block;
+	padding: 0 0 0 2px;
+	top: 0;
+	right: -1px;
+	position: relative;
+	vertical-align: top;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	text-decoration: none !important;
+	color: #888;
+}
+
+div#community-profile-page li.bp-members-profile-stats:before {
+	content: "\f130";
+}
+
+div#community-profile-page li.bp-friends-profile-stats:before {
+	content: "\f454";
+}
+
+div#community-profile-page li.bp-groups-profile-stats:before {
+	content: "\f456";
+}
+
+div#community-profile-page li.bp-blogs-profile-stats:before {
+	content: "\f120";
+}
+
+div#community-profile-page a.bp-xprofile-avatar-user-admin:before {
+	content: "\f182";
+}
+
+div#community-profile-page a.bp-xprofile-avatar-user-edit:before {
+	content: "\f107";
+}
+
+div#community-profile-page div#bp_xprofile_user_admin_avatar div.avatar {
+	width: 150px;
+	margin: 0 auto;
+}
+
+div#community-profile-page div#bp_xprofile_user_admin_avatar div.avatar img {
+	max-width: 100%;
+	height: auto;
+}
+
+div#community-profile-page div#bp_xprofile_user_admin_avatar a {
+	display: block;
+	margin: 1em 0;
+}
+
+div#community-profile-page p.not-activated {
+	margin: 1em 1em 0;
+	color: #f00;
+}
+
+#community-profile-page #submitdiv #publishing-action {
+	float: none;
+	width: 100%;
+}
+
+.bp-view-profile {
+	float: right;
+}
+
+.alt {
+	background: none;
+}
+
+.bp-profile-field {
+	border-bottom: dotted 1px #ccc;
+	font-size: 14px;
+	margin: 15px 0;
+	padding: 10px;
+}
+
+.bp-profile-field:last-child {
+	border-bottom: 0;
+}
+
+.bp-profile-field p {
+	font-size: 14px;
+}
+
+.field_type_textarea legend,
+.field_type_multiselectbox legend,
+.field_type_radio .radio legend,
+.field_type_checkbox .checkbox legend {  /* these fields are usually pretty tall, so align the label for better consistency */
+	vertical-align: top;
+}
+
+.bp-profile-field .description {  /* description also sits in the right side column */
+	margin: 10px 200px 12px 0;
+	text-align: right;
+}
+
+.bp-profile-field .wp-editor-wrap {
+	margin-right: 200px;
+}
+
+.field_type_checkbox .description,
+.field_type_datebox .description,
+.field_type_radio .description {
+	margin-top: 0;
+}
+
+.clear-value {  /* 'clear value' option also sits in the right side column */
+	display: block;
+	font-size: 12px;
+	margin-right: 200px;
+}
+
+/* force checkboxes to new lines, in the right side column */
+.field_type_checkbox label,
+.field_type_radio label {
+	display: block;
+	margin-bottom: 1em;
+	margin-right: 200px;
+	width: auto;
+}
+
+.field_type_checkbox .radio label,
+.field_type_radio .radio label {
+	margin-right: 0;
+}
+
+.field_type_datebox .datebox-selects {
+	margin-right: 200px;
+}
+
+.field_type_datebox select:nth-of-type(1) {
+	margin-right: 0;
+}
+
+.field_type_radio .radio .input-options label,
+.field_type_checkbox .checkbox .input-options label {
+
+	/* force checkboxes and radio buttons to new lines */
+	display: block;
+}
+
+.field_type_checkbox .checkbox .input-options,
+.field_type_datebox .datebox .input-options,
+.field_type_radio .radio .input-options {
+
+	/* make the checkboxes, select menus, and radio buttons sit in the
+	right side column */
+	display: inline-block;
+}
+
+.field-visibility-settings-notoggle,
+.field-visibility-settings-toggle {  /* visibility settings go in the left column */
+	margin: 10px 200px 10px 0;
+	text-align: right;
+}
+
+.field-visibility-settings {  /* visibility settings go in the left column */
+	display: none;
+	margin-right: 200px;
+	margin-top: 1.5em;
+}
+
+.field-visibility-settings .button {  /* visibility setting close button */
+	margin-bottom: 15px;
+}
+
+.field-visibility-settings label {
+	clear: right;
+	display: block;
+	margin-bottom: 1em;
+}
+
+#normal-sortables .field-visibility-settings legend {  /* id required for css selector weight */
+	font-size: 14px;
+	font-weight: 600;
+}
+
+#your-profile .bp-profile-field legend {
+	float: right; /* Firefox will not apply display: inline-block to legends */
+	font-size: 14px;
+	font-weight: 600;
+	line-height: 1.4;
+	margin-bottom: 1em;
+	text-align: right;
+	vertical-align: middle;
+	width: 192px;
+}
+
+.bp-profile-field .radio .clear-value {
+	margin-top: 10px;
+}
+
+@media screen and (max-width: 782px) {
+
+	#your-profile .bp-profile-field legend {
+		float: none;
+		clear: right;
+		display: block;
+		margin-bottom: 12px;
+	}
+
+	#your-profile .field_type_multiselectbox select {
+		height: auto;
+	}
+
+	.field_type_checkbox label,
+	.field_type_radio label {
+		margin-right: 0;
+	}
+
+	.bp-profile-field .checkbox input[type="checkbox"],
+	.bp-profile-field .radio input[type="radio"] {
+		vertical-align: top;
+	}
+
+	.bp-profile-field .clear-value,
+	.bp-profile-field .description,
+	.bp-profile-field input[type="checkbox"],
+	.bp-profile-field input[type="email"],
+	.bp-profile-field input[type="number"],
+	.bp-profile-field input[type="radio"],
+	.bp-profile-field input[type="text"],
+	.bp-profile-field input[type="url"],
+	.bp-profile-field select:nth-of-type(1),
+	.bp-profile-field .wp-editor-wrap,
+	.field-visibility-settings-notoggle,
+	.field-visibility-settings-toggle {
+		clear: right;
+		margin-right: 50px;
+	}
+
+	.field_type_datebox .datebox-selects {
+		margin-right: 0;
+	}
+
+	.field-visibility-settings {
+		margin-right: 50px;
+	}
+
+	.field-visibility-settings input[type="radio"] {
+		margin-right: 0;
+	}
+
+	#your-profile .field_multiselectbox select {
+		height: auto;
+	}
+}
+
+@media screen and (max-width: 480px) {
+
+	.bp-profile-field .clear-value,
+	.bp-profile-field .description,
+	.bp-profile-field input[type="checkbox"],
+	.bp-profile-field input[type="email"],
+	.bp-profile-field input[type="number"],
+	.bp-profile-field input[type="radio"],
+	.bp-profile-field input[type="text"],
+	.bp-profile-field input[type="url"],
+	.bp-profile-field select:nth-of-type(1),
+	.bp-profile-field .wp-editor-wrap,
+	.field-visibility-settings-notoggle,
+	.field-visibility-settings-toggle {
+		margin-right: 0;
+	}
+
+	.field-visibility-settings {
+		margin-right: 0;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/admin/css/admin.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/admin/css/admin.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/admin/css/admin.css	(revision 12265)
@@ -0,0 +1,304 @@
+/**** BP Members Profile Administration Screens
+* TO DO: Organize and clean up stylesheet.
+****/
+
+div#profile-page.wrap form#your-profile {
+	position: relative;
+	padding-top: 50px;
+}
+
+div#profile-page.wrap form#your-profile h3:first-of-type {
+	margin-top: 5em;
+}
+
+div#profile-page.wrap form#your-profile #profile-nav {
+	position: absolute;
+	top: 0;
+	width: 97%;
+}
+
+div#community-profile-page #profile-nav {
+	margin-bottom: 1em;
+}
+
+#bp_members_admin_user_stats ul {
+	margin-bottom: 0;
+}
+
+div#community-profile-page li.bp-members-profile-stats:before,
+div#community-profile-page li.bp-friends-profile-stats:before,
+div#community-profile-page li.bp-groups-profile-stats:before,
+div#community-profile-page li.bp-blogs-profile-stats:before,
+div#community-profile-page a.bp-xprofile-avatar-user-admin:before,
+div#community-profile-page a.bp-xprofile-avatar-user-edit:before {
+	font: 400 20px/1 dashicons;
+	speak: none;
+	display: inline-block;
+	padding: 0 2px 0 0;
+	top: 0;
+	left: -1px;
+	position: relative;
+	vertical-align: top;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	text-decoration: none !important;
+	color: #888;
+}
+
+div#community-profile-page li.bp-members-profile-stats:before {
+	content: "\f130";
+}
+
+div#community-profile-page li.bp-friends-profile-stats:before {
+	content: "\f454";
+}
+
+div#community-profile-page li.bp-groups-profile-stats:before {
+	content: "\f456";
+}
+
+div#community-profile-page li.bp-blogs-profile-stats:before {
+	content: "\f120";
+}
+
+div#community-profile-page a.bp-xprofile-avatar-user-admin:before {
+	content: "\f182";
+}
+
+div#community-profile-page a.bp-xprofile-avatar-user-edit:before {
+	content: "\f107";
+}
+
+div#community-profile-page div#bp_xprofile_user_admin_avatar div.avatar {
+	width: 150px;
+	margin: 0 auto;
+}
+
+div#community-profile-page div#bp_xprofile_user_admin_avatar div.avatar img {
+	max-width: 100%;
+	height: auto;
+}
+
+div#community-profile-page div#bp_xprofile_user_admin_avatar a {
+	display: block;
+	margin: 1em 0;
+}
+
+div#community-profile-page p.not-activated {
+	margin: 1em 1em 0;
+	color: #f00;
+}
+
+#community-profile-page #submitdiv #publishing-action {
+	float: none;
+	width: 100%;
+}
+
+.bp-view-profile {
+	float: left;
+}
+
+.alt {
+	background: none;
+}
+
+.bp-profile-field {
+	border-bottom: dotted 1px #ccc;
+	font-size: 14px;
+	margin: 15px 0;
+	padding: 10px;
+}
+
+.bp-profile-field:last-child {
+	border-bottom: 0;
+}
+
+.bp-profile-field p {
+	font-size: 14px;
+}
+
+.field_type_textarea legend,
+.field_type_multiselectbox legend,
+.field_type_radio .radio legend,
+.field_type_checkbox .checkbox legend {  /* these fields are usually pretty tall, so align the label for better consistency */
+	vertical-align: top;
+}
+
+.bp-profile-field .description {  /* description also sits in the right side column */
+	margin: 10px 0 12px 200px;
+	text-align: left;
+}
+
+.bp-profile-field .wp-editor-wrap {
+	margin-left: 200px;
+}
+
+.field_type_checkbox .description,
+.field_type_datebox .description,
+.field_type_radio .description {
+	margin-top: 0;
+}
+
+.clear-value {  /* 'clear value' option also sits in the right side column */
+	display: block;
+	font-size: 12px;
+	margin-left: 200px;
+}
+
+/* force checkboxes to new lines, in the right side column */
+.field_type_checkbox label,
+.field_type_radio label {
+	display: block;
+	margin-bottom: 1em;
+	margin-left: 200px;
+	width: auto;
+}
+
+.field_type_checkbox .radio label,
+.field_type_radio .radio label {
+	margin-left: 0;
+}
+
+.field_type_datebox .datebox-selects {
+	margin-left: 200px;
+}
+
+.field_type_datebox select:nth-of-type(1) {
+	margin-left: 0;
+}
+
+.field_type_radio .radio .input-options label,
+.field_type_checkbox .checkbox .input-options label {
+
+	/* force checkboxes and radio buttons to new lines */
+	display: block;
+}
+
+.field_type_checkbox .checkbox .input-options,
+.field_type_datebox .datebox .input-options,
+.field_type_radio .radio .input-options {
+
+	/* make the checkboxes, select menus, and radio buttons sit in the
+	right side column */
+	display: inline-block;
+}
+
+.field-visibility-settings-notoggle,
+.field-visibility-settings-toggle {  /* visibility settings go in the left column */
+	margin: 10px 0 10px 200px;
+	text-align: left;
+}
+
+.field-visibility-settings {  /* visibility settings go in the left column */
+	display: none;
+	margin-left: 200px;
+	margin-top: 1.5em;
+}
+
+.field-visibility-settings .button {  /* visibility setting close button */
+	margin-bottom: 15px;
+}
+
+.field-visibility-settings label {
+	clear: left;
+	display: block;
+	margin-bottom: 1em;
+}
+
+#normal-sortables .field-visibility-settings legend {  /* id required for css selector weight */
+	font-size: 14px;
+	font-weight: 600;
+}
+
+#your-profile .bp-profile-field legend {
+	float: left; /* Firefox will not apply display: inline-block to legends */
+	font-size: 14px;
+	font-weight: 600;
+	line-height: 1.4;
+	margin-bottom: 1em;
+	text-align: left;
+	vertical-align: middle;
+	width: 192px;
+}
+
+.bp-profile-field .radio .clear-value {
+	margin-top: 10px;
+}
+
+@media screen and (max-width: 782px) {
+
+	#your-profile .bp-profile-field legend {
+		float: none;
+		clear: left;
+		display: block;
+		margin-bottom: 12px;
+	}
+
+	#your-profile .field_type_multiselectbox select {
+		height: auto;
+	}
+
+	.field_type_checkbox label,
+	.field_type_radio label {
+		margin-left: 0;
+	}
+
+	.bp-profile-field .checkbox input[type="checkbox"],
+	.bp-profile-field .radio input[type="radio"] {
+		vertical-align: top;
+	}
+
+	.bp-profile-field .clear-value,
+	.bp-profile-field .description,
+	.bp-profile-field input[type="checkbox"],
+	.bp-profile-field input[type="email"],
+	.bp-profile-field input[type="number"],
+	.bp-profile-field input[type="radio"],
+	.bp-profile-field input[type="text"],
+	.bp-profile-field input[type="url"],
+	.bp-profile-field select:nth-of-type(1),
+	.bp-profile-field .wp-editor-wrap,
+	.field-visibility-settings-notoggle,
+	.field-visibility-settings-toggle {
+		clear: left;
+		margin-left: 50px;
+	}
+
+	.field_type_datebox .datebox-selects {
+		margin-left: 0;
+	}
+
+	.field-visibility-settings {
+		margin-left: 50px;
+	}
+
+	.field-visibility-settings input[type="radio"] {
+		margin-left: 0;
+	}
+
+	#your-profile .field_multiselectbox select {
+		height: auto;
+	}
+}
+
+@media screen and (max-width: 480px) {
+
+	.bp-profile-field .clear-value,
+	.bp-profile-field .description,
+	.bp-profile-field input[type="checkbox"],
+	.bp-profile-field input[type="email"],
+	.bp-profile-field input[type="number"],
+	.bp-profile-field input[type="radio"],
+	.bp-profile-field input[type="text"],
+	.bp-profile-field input[type="url"],
+	.bp-profile-field select:nth-of-type(1),
+	.bp-profile-field .wp-editor-wrap,
+	.field-visibility-settings-notoggle,
+	.field-visibility-settings-toggle {
+		margin-left: 0;
+	}
+
+	.field-visibility-settings {
+		margin-left: 0;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/admin/js/admin.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/admin/js/admin.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/admin/js/admin.js	(revision 12265)
@@ -0,0 +1,57 @@
+/* exported clear */
+
+( function( $ ) {
+	// Profile Visibility Settings
+
+	$( '.visibility-toggle-link' ).on( 'click', function( event ) {
+		event.preventDefault();
+
+		$( this ).attr( 'aria-expanded', 'true' ).parent().hide()
+			.siblings( '.field-visibility-settings' ).show();
+	} );
+
+	$( '.field-visibility-settings-close' ).on( 'click', function( event ) {
+		event.preventDefault();
+
+		 $( '.visibility-toggle-link' ).attr( 'aria-expanded', 'false' );
+
+		var settings_div = $(this).parent(),
+		vis_setting_text = settings_div.find( 'input:checked' ).parent().text();
+
+		settings_div.hide()
+			.siblings( '.field-visibility-settings-toggle' )
+				.children( '.current-visibility-level' ).text( vis_setting_text ).end()
+			.show();
+	} );
+
+} )( jQuery );
+
+
+/**
+ * Deselects any select options or input options for the specified field element.
+ *
+ * @param {String} container HTML ID of the field
+ * @since 1.0.0
+ */
+function clear( container ) {
+	container = document.getElementById( container );
+	if ( ! container ) {
+		return;
+	}
+
+	var radioButtons = container.getElementsByTagName( 'INPUT' ),
+		options = container.getElementsByTagName( 'OPTION' ),
+		i       = 0;
+
+	if ( radioButtons ) {
+		for ( i = 0; i < radioButtons.length; i++ ) {
+			radioButtons[i].checked = '';
+		}
+	}
+
+	if ( options ) {
+		for ( i = 0; i < options.length; i++ ) {
+			options[i].selected = false;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-activity.php	(revision 12265)
@@ -0,0 +1,97 @@
+<?php
+/**
+ * BuddyPress Member Activity
+ *
+ * @package BuddyPress
+ * @subpackage MembersActivity
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register the 'new member' activity type.
+ *
+ * @since 2.2.0
+ *
+ */
+function bp_members_register_activity_actions() {
+
+	bp_activity_set_action(
+		buddypress()->members->id,
+		'new_member',
+		__( 'New member registered', 'buddypress' ),
+		'bp_members_format_activity_action_new_member',
+		__( 'New Members', 'buddypress' ),
+		array( 'activity' )
+	);
+
+	/**
+	 * Fires after the default 'new member' activity types are registered.
+	 *
+	 * @since 2.2.0
+	 */
+	do_action( 'bp_members_register_activity_actions' );
+}
+add_action( 'bp_register_activity_actions', 'bp_members_register_activity_actions' );
+
+/**
+ * Format 'new_member' activity actions.
+ *
+ * @since 2.2.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity object.
+ * @return string $action
+ */
+function bp_members_format_activity_action_new_member( $action, $activity ) {
+	$userlink = bp_core_get_userlink( $activity->user_id );
+	$action   = sprintf( __( '%s became a registered member', 'buddypress' ), $userlink );
+
+	// Legacy filter - pass $user_id instead of $activity.
+	if ( has_filter( 'bp_core_activity_registered_member_action' ) ) {
+		$action = apply_filters( 'bp_core_activity_registered_member_action', $action, $activity->user_id );
+	}
+
+	/**
+	 * Filters the formatted 'new member' activity actions.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $action   Static activity action.
+	 * @param object $activity Activity object.
+	 */
+	return apply_filters( 'bp_members_format_activity_action_new_member', $action, $activity );
+}
+
+/**
+ * Create a "became a registered user" activity item when a user activates his account.
+ *
+ * @since 1.2.2
+ *
+ * @param array $user Array of userdata passed to bp_core_activated_user hook.
+ * @return bool
+ */
+function bp_core_new_user_activity( $user ) {
+	if ( empty( $user ) ) {
+		return false;
+	}
+
+	if ( is_array( $user ) ) {
+		$user_id = $user['user_id'];
+	} else {
+		$user_id = $user;
+	}
+
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	bp_activity_add( array(
+		'user_id'   => $user_id,
+		'component' => buddypress()->members->id,
+		'type'      => 'new_member'
+	) );
+}
+add_action( 'bp_core_activated_user', 'bp_core_new_user_activity' );
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-admin.php	(revision 12265)
@@ -0,0 +1,14 @@
+<?php
+/**
+ * BuddyPress Members Admin
+ *
+ * @package BuddyPress
+ * @subpackage MembersAdmin
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+// Load the BP Members admin.
+add_action( 'bp_init', array( 'BP_Members_Admin', 'register_members_admin' ) );
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-adminbar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-adminbar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-adminbar.php	(revision 12265)
@@ -0,0 +1,180 @@
+<?php
+/**
+ * BuddyPress Members Toolbar.
+ *
+ * Handles the member functions related to the WordPress Toolbar.
+ *
+ * @package BuddyPress
+ * @subpackage MembersAdminBar
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add the "My Account" menu and all submenus.
+ *
+ * @since 1.6.0
+ *
+ * @todo Deprecate WP 3.2 Toolbar compatibility when we drop 3.2 support.
+ */
+function bp_members_admin_bar_my_account_menu() {
+	global $wp_admin_bar;
+
+	// Bail if this is an ajax request.
+	if ( defined( 'DOING_AJAX' ) )
+		return;
+
+	// Logged in user.
+	if ( is_user_logged_in() ) {
+
+		$bp = buddypress();
+
+		// Stored in the global so we can add menus easily later on.
+		$bp->my_account_menu_id = 'my-account-buddypress';
+
+		// Create the main 'My Account' menu.
+		$wp_admin_bar->add_menu( array(
+			'id'     => $bp->my_account_menu_id,
+			'group'  => true,
+			'title'  => __( 'Edit My Profile', 'buddypress' ),
+			'href'   => bp_loggedin_user_domain(),
+			'meta'   => array(
+			'class'  => 'ab-sub-secondary'
+		) ) );
+
+		// Show login and sign-up links.
+	} elseif ( !empty( $wp_admin_bar ) ) {
+
+		add_filter( 'show_admin_bar', '__return_true' );
+
+		// Create the main 'My Account' menu.
+		$wp_admin_bar->add_menu( array(
+			'id'    => 'bp-login',
+			'title' => __( 'Log In', 'buddypress' ),
+			'href'  => wp_login_url( bp_get_requested_url() )
+		) );
+
+		// Sign up.
+		if ( bp_get_signup_allowed() ) {
+			$wp_admin_bar->add_menu( array(
+				'id'    => 'bp-register',
+				'title' => __( 'Register', 'buddypress' ),
+				'href'  => bp_get_signup_page()
+			) );
+		}
+	}
+}
+add_action( 'bp_setup_admin_bar', 'bp_members_admin_bar_my_account_menu', 4 );
+
+/**
+ * Add the User Admin top-level menu to user pages.
+ *
+ * @since 1.5.0
+ */
+function bp_members_admin_bar_user_admin_menu() {
+	global $wp_admin_bar;
+
+	// Only show if viewing a user.
+	if ( !bp_is_user() )
+		return false;
+
+	// Don't show this menu to non site admins or if you're viewing your own profile.
+	if ( !current_user_can( 'edit_users' ) || bp_is_my_profile() )
+		return false;
+
+	$bp = buddypress();
+
+	// Unique ID for the 'My Account' menu.
+	$bp->user_admin_menu_id = 'user-admin';
+
+	// Add the top-level User Admin button.
+	$wp_admin_bar->add_menu( array(
+		'id'    => $bp->user_admin_menu_id,
+		'title' => __( 'Edit Member', 'buddypress' ),
+		'href'  => bp_displayed_user_domain()
+	) );
+
+	if ( bp_is_active( 'xprofile' ) ) {
+		// User Admin > Edit this user's profile.
+		$wp_admin_bar->add_menu( array(
+			'parent' => $bp->user_admin_menu_id,
+			'id'     => $bp->user_admin_menu_id . '-edit-profile',
+			'title'  => __( "Edit Profile", 'buddypress' ),
+			'href'   => bp_get_members_component_link( 'profile', 'edit' )
+		) );
+
+		// User Admin > Edit this user's avatar.
+		if ( buddypress()->avatar->show_avatars ) {
+			$wp_admin_bar->add_menu( array(
+				'parent' => $bp->user_admin_menu_id,
+				'id'     => $bp->user_admin_menu_id . '-change-avatar',
+				'title'  => __( "Edit Profile Photo", 'buddypress' ),
+				'href'   => bp_get_members_component_link( 'profile', 'change-avatar' )
+			) );
+		}
+
+		// User Admin > Edit this user's cover image.
+		if ( bp_displayed_user_use_cover_image_header() ) {
+			$wp_admin_bar->add_menu( array(
+				'parent' => $bp->user_admin_menu_id,
+				'id'     => $bp->user_admin_menu_id . '-change-cover-image',
+				'title'  => __( 'Edit Cover Image', 'buddypress' ),
+				'href'   => bp_get_members_component_link( 'profile', 'change-cover-image' )
+			) );
+		}
+
+	}
+
+	if ( bp_is_active( 'settings' ) ) {
+		// User Admin > Spam/unspam.
+		$wp_admin_bar->add_menu( array(
+			'parent' => $bp->user_admin_menu_id,
+			'id'     => $bp->user_admin_menu_id . '-user-capabilities',
+			'title'  => __( 'User Capabilities', 'buddypress' ),
+			'href'   => bp_displayed_user_domain() . 'settings/capabilities/'
+		) );
+
+		// User Admin > Delete Account.
+		$wp_admin_bar->add_menu( array(
+			'parent' => $bp->user_admin_menu_id,
+			'id'     => $bp->user_admin_menu_id . '-delete-user',
+			'title'  => __( 'Delete Account', 'buddypress' ),
+			'href'   => bp_displayed_user_domain() . 'settings/delete-account/'
+		) );
+
+	}
+
+}
+add_action( 'admin_bar_menu', 'bp_members_admin_bar_user_admin_menu', 99 );
+
+/**
+ * Build the "Notifications" dropdown.
+ *
+ * @since 1.5.0
+ *
+ * @return bool
+ */
+function bp_members_admin_bar_notifications_menu() {
+
+	// Bail if notifications is not active.
+	if ( ! bp_is_active( 'notifications' ) ) {
+		return false;
+	}
+
+	bp_notifications_toolbar_menu();
+}
+add_action( 'admin_bar_menu', 'bp_members_admin_bar_notifications_menu', 90 );
+
+/**
+ * Remove rogue WP core Edit menu when viewing a single user.
+ *
+ * @since 1.6.0
+ */
+function bp_members_remove_edit_page_menu() {
+	if ( bp_is_user() ) {
+		remove_action( 'admin_bar_menu', 'wp_admin_bar_edit_menu', 80 );
+	}
+}
+add_action( 'add_admin_bar_menus', 'bp_members_remove_edit_page_menu' );
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-cache.php	(revision 12265)
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Caching functions specific to BuddyPress Members.
+ *
+ * @package BuddyPress
+ * @subpackage MembersCache
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Pre-fetch member type data when initializing a Members loop.
+ *
+ * @since 2.2.0
+ *
+ * @param BP_User_Query $bp_user_query BP_User_Query object.
+ */
+function bp_members_prefetch_member_type( BP_User_Query $bp_user_query ) {
+	$uncached_member_ids = bp_get_non_cached_ids( $bp_user_query->user_ids, 'bp_member_member_type' );
+
+	$member_types = bp_get_object_terms( $uncached_member_ids, bp_get_member_type_tax_name(), array(
+		'fields' => 'all_with_object_id',
+	) );
+
+	// Rekey by user ID.
+	$keyed_member_types = array();
+	foreach ( $member_types as $member_type ) {
+		if ( ! isset( $keyed_member_types[ $member_type->object_id ] ) ) {
+			$keyed_member_types[ $member_type->object_id ] = array();
+		}
+
+		$keyed_member_types[ $member_type->object_id ][] = $member_type->name;
+	}
+
+	$cached_member_ids = array();
+	foreach ( $keyed_member_types as $user_id => $user_member_types ) {
+		wp_cache_set( $user_id, $user_member_types, 'bp_member_member_type' );
+		$cached_member_ids[] = $user_id;
+	}
+
+	// Cache an empty value for users with no type.
+	foreach ( array_diff( $uncached_member_ids, $cached_member_ids ) as $no_type_id ) {
+		wp_cache_set( $no_type_id, '', 'bp_member_member_type' );
+	}
+}
+add_action( 'bp_user_query_populate_extras', 'bp_members_prefetch_member_type' );
+
+/**
+ * Clear the member_type cache for a user.
+ *
+ * Called when the user is deleted or marked as spam.
+ *
+ * @since 2.2.0
+ *
+ * @param int $user_id ID of the deleted user.
+ */
+function bp_members_clear_member_type_cache( $user_id ) {
+	wp_cache_delete( $user_id, 'bp_member_member_type' );
+}
+add_action( 'wpmu_delete_user', 'bp_members_clear_member_type_cache' );
+add_action( 'delete_user', 'bp_members_clear_member_type_cache' );
+
+/**
+ * Invalidate activity caches when a user's last_activity value is changed.
+ *
+ * @since 2.7.0
+ *
+ * @return bool True on success, false on failure.
+ */
+function bp_members_reset_activity_cache_incrementor() {
+	return bp_core_reset_incrementor( 'bp_activity_with_last_activity' );
+}
+add_action( 'bp_core_user_updated_last_activity', 'bp_members_reset_activity_cache_incrementor' );
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-filters.php	(revision 12265)
@@ -0,0 +1,129 @@
+<?php
+/**
+ * BuddyPress Members Filters.
+ *
+ * Filters specific to the Members component.
+ *
+ * @package BuddyPress
+ * @subpackage MembersFilters
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Escape commonly used fullname output functions.
+ */
+add_filter( 'bp_displayed_user_fullname',    'esc_html' );
+add_filter( 'bp_get_loggedin_user_fullname', 'esc_html' );
+
+// Filter the user registration URL to point to BuddyPress's registration page.
+add_filter( 'register_url', 'bp_get_signup_page' );
+
+/**
+ * Load additional sign-up sanitization filters on bp_loaded.
+ *
+ * These are used to prevent XSS in the BuddyPress sign-up process. You can
+ * unhook these to allow for customization of your registration fields;
+ * however, it is highly recommended that you leave these in place for the
+ * safety of your network.
+ *
+ * @since 1.5.0
+ */
+function bp_members_signup_sanitization() {
+
+	// Filters on sign-up fields.
+	$fields = array (
+		'bp_get_signup_username_value',
+		'bp_get_signup_email_value',
+		'bp_get_signup_with_blog_value',
+		'bp_get_signup_blog_url_value',
+		'bp_get_signup_blog_title_value',
+		'bp_get_signup_blog_privacy_value',
+		'bp_get_signup_avatar_dir_value',
+	);
+
+	// Add the filters to each field.
+	foreach( $fields as $filter ) {
+		add_filter( $filter, 'esc_html',       1 );
+		add_filter( $filter, 'wp_filter_kses', 2 );
+		add_filter( $filter, 'stripslashes',   3 );
+	}
+
+	// Sanitize email.
+	add_filter( 'bp_get_signup_email_value', 'sanitize_email' );
+}
+add_action( 'bp_loaded', 'bp_members_signup_sanitization' );
+
+/**
+ * Make sure the username is not the blog slug in case of root profile & subdirectory blog.
+ *
+ * If BP_ENABLE_ROOT_PROFILES is defined & multisite config is set to subdirectories,
+ * then there is a chance site.url/username == site.url/blogslug. If so, user's profile
+ * is not reachable, instead the blog is displayed. This filter makes sure the signup username
+ * is not the same than the blog slug for this particular config.
+ *
+ * @since 2.1.0
+ *
+ * @param array $illegal_names Array of illiegal names.
+ * @return array $illegal_names
+ */
+function bp_members_signup_with_subdirectory_blog( $illegal_names = array() ) {
+	if ( ! bp_core_enable_root_profiles() ) {
+		return $illegal_names;
+	}
+
+	if ( is_network_admin() && isset( $_POST['blog'] ) ) {
+		$blog = $_POST['blog'];
+		$domain = '';
+
+		if ( preg_match( '|^([a-zA-Z0-9-])$|', $blog['domain'] ) ) {
+			$domain = strtolower( $blog['domain'] );
+		}
+
+		if ( username_exists( $domain ) ) {
+			$illegal_names[] = $domain;
+		}
+
+	} else {
+		$illegal_names[] = buddypress()->signup->username;
+	}
+
+	return $illegal_names;
+}
+add_filter( 'subdirectory_reserved_names', 'bp_members_signup_with_subdirectory_blog', 10, 1 );
+
+/**
+ * Filter the user profile URL to point to BuddyPress profile edit.
+ *
+ * @since 1.6.0
+ *
+ * @param string $url     WP profile edit URL.
+ * @param int    $user_id ID of the user.
+ * @param string $scheme  Scheme to use.
+ * @return string
+ */
+function bp_members_edit_profile_url( $url, $user_id, $scheme = 'admin' ) {
+
+	// If xprofile is active, use profile domain link.
+	if ( ! is_admin() && bp_is_active( 'xprofile' ) ) {
+		$profile_link = trailingslashit( bp_core_get_user_domain( $user_id ) . bp_get_profile_slug() . '/edit' );
+
+	} else {
+		// Default to $url.
+		$profile_link = $url;
+	}
+
+	/**
+	 * Filters the user profile URL to point to BuddyPress profile edit.
+	 *
+	 * @since 1.5.2
+	 *
+	 * @param string $url WP profile edit URL.
+	 * @param int    $user_id ID of the user.
+	 * @param string $scheme Scheme to use.
+	 */
+	return apply_filters( 'bp_members_edit_profile_url', $profile_link, $url, $user_id, $scheme );
+}
+add_filter( 'edit_profile_url', 'bp_members_edit_profile_url', 10, 3 );
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-functions.php	(revision 12265)
@@ -0,0 +1,2773 @@
+<?php
+/**
+ * BuddyPress Member Functions.
+ *
+ * Functions specific to the members component.
+ *
+ * @package BuddyPress
+ * @subpackage MembersFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Check for the existence of a Members directory page.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if found, otherwise false.
+ */
+function bp_members_has_directory() {
+	$bp = buddypress();
+
+	return (bool) !empty( $bp->pages->members->id );
+}
+
+/**
+ * Define the slug constants for the Members component.
+ *
+ * Handles the three slug constants used in the Members component -
+ * BP_MEMBERS_SLUG, BP_REGISTER_SLUG, and BP_ACTIVATION_SLUG. If these
+ * constants are not overridden in wp-config.php or bp-custom.php, they are
+ * defined here to match the slug of the corresponding WP pages.
+ *
+ * In general, fallback values are only used during initial BP page creation,
+ * when no slugs have been explicitly defined.
+ *
+ * @since 1.5.0
+ */
+function bp_core_define_slugs() {
+	$bp = buddypress();
+
+	// No custom members slug.
+	if ( !defined( 'BP_MEMBERS_SLUG' ) ) {
+		if ( !empty( $bp->pages->members ) ) {
+			define( 'BP_MEMBERS_SLUG', $bp->pages->members->slug );
+		} else {
+			define( 'BP_MEMBERS_SLUG', 'members' );
+		}
+	}
+
+	// No custom registration slug.
+	if ( !defined( 'BP_REGISTER_SLUG' ) ) {
+		if ( !empty( $bp->pages->register ) ) {
+			define( 'BP_REGISTER_SLUG', $bp->pages->register->slug );
+		} else {
+			define( 'BP_REGISTER_SLUG', 'register' );
+		}
+	}
+
+	// No custom activation slug.
+	if ( !defined( 'BP_ACTIVATION_SLUG' ) ) {
+		if ( !empty( $bp->pages->activate ) ) {
+			define( 'BP_ACTIVATION_SLUG', $bp->pages->activate->slug );
+		} else {
+			define( 'BP_ACTIVATION_SLUG', 'activate' );
+		}
+	}
+}
+add_action( 'bp_setup_globals', 'bp_core_define_slugs', 11 );
+
+/**
+ * Fetch an array of users based on the parameters passed.
+ *
+ * Since BuddyPress 1.7, bp_core_get_users() uses BP_User_Query. If you
+ * need backward compatibility with BP_Core_User::get_users(), filter the
+ * bp_use_legacy_user_query value, returning true.
+ *
+ * @since 1.2.0
+ *
+ * @param array|string $args {
+ *     Array of arguments. All are optional. See {@link BP_User_Query} for
+ *     a more complete description of arguments.
+ *     @type string       $type                Sort order. Default: 'active'.
+ *     @type int          $user_id             Limit results to friends of a user. Default: false.
+ *     @type mixed        $exclude             IDs to exclude from results. Default: false.
+ *     @type string       $search_terms        Limit to users matching search terms. Default: false.
+ *     @type string       $meta_key            Limit to users with a meta_key. Default: false.
+ *     @type string       $meta_value          Limit to users with a meta_value (with meta_key). Default: false.
+ *     @type array|string $member_type         Array or comma-separated string of member types.
+ *     @type array|string $member_type__in     Array or comma-separated string of member types.
+ *                                             `$member_type` takes precedence over this parameter.
+ *     @type array|string $member_type__not_in Array or comma-separated string of member types to be excluded.
+ *     @type mixed        $include             Limit results by user IDs. Default: false.
+ *     @type int          $per_page            Results per page. Default: 20.
+ *     @type int          $page                Page of results. Default: 1.
+ *     @type bool         $populate_extras     Fetch optional extras. Default: true.
+ *     @type string|bool  $count_total         How to do total user count. Default: 'count_query'.
+ * }
+ * @return array
+ */
+function bp_core_get_users( $args = '' ) {
+
+	// Parse the user query arguments.
+	$r = bp_parse_args( $args, array(
+		'type'                => 'active',     // Active, newest, alphabetical, random or popular.
+		'user_id'             => false,        // Pass a user_id to limit to only friend connections for this user.
+		'exclude'             => false,        // Users to exclude from results.
+		'search_terms'        => false,        // Limit to users that match these search terms.
+		'meta_key'            => false,        // Limit to users who have this piece of usermeta.
+		'meta_value'          => false,        // With meta_key, limit to users where usermeta matches this value.
+		'member_type'         => '',
+		'member_type__in'     => '',
+		'member_type__not_in' => '',
+		'include'             => false,        // Pass comma separated list of user_ids to limit to only these users.
+		'per_page'            => 20,           // The number of results to return per page.
+		'page'                => 1,            // The page to return if limiting per page.
+		'populate_extras'     => true,         // Fetch the last active, where the user is a friend, total friend count, latest update.
+		'count_total'         => 'count_query' // What kind of total user count to do, if any. 'count_query', 'sql_calc_found_rows', or false.
+	), 'core_get_users' );
+
+	// For legacy users. Use of BP_Core_User::get_users() is deprecated.
+	if ( apply_filters( 'bp_use_legacy_user_query', false, __FUNCTION__, $r ) ) {
+		$retval = BP_Core_User::get_users(
+			$r['type'],
+			$r['per_page'],
+			$r['page'],
+			$r['user_id'],
+			$r['include'],
+			$r['search_terms'],
+			$r['populate_extras'],
+			$r['exclude'],
+			$r['meta_key'],
+			$r['meta_value']
+		);
+
+	// Default behavior as of BuddyPress 1.7.0.
+	} else {
+
+		// Get users like we were asked to do...
+		$users = new BP_User_Query( $r );
+
+		// ...but reformat the results to match bp_core_get_users() behavior.
+		$retval = array(
+			'users' => array_values( $users->results ),
+			'total' => $users->total_users
+		);
+	}
+
+	/**
+	 * Filters the results of the user query.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array $retval Array of users for the current query.
+	 * @param array $r      Array of parsed query arguments.
+	 */
+	return apply_filters( 'bp_core_get_users', $retval, $r );
+}
+
+/**
+ * Return the domain for the passed user: e.g. http://example.com/members/andy/.
+ *
+ * @since 1.0.0
+ *
+ * @param int         $user_id       The ID of the user.
+ * @param string|bool $user_nicename Optional. user_nicename of the user.
+ * @param string|bool $user_login    Optional. user_login of the user.
+ * @return string
+ */
+function bp_core_get_user_domain( $user_id = 0, $user_nicename = false, $user_login = false ) {
+
+	if ( empty( $user_id ) ) {
+		return;
+	}
+
+	$username = bp_core_get_username( $user_id, $user_nicename, $user_login );
+
+	if ( bp_is_username_compatibility_mode() ) {
+		$username = rawurlencode( $username );
+	}
+
+	$after_domain = bp_core_enable_root_profiles() ? $username : bp_get_members_root_slug() . '/' . $username;
+	$domain       = trailingslashit( bp_get_root_domain() . '/' . $after_domain );
+
+	// Don't use this filter.  Subject to removal in a future release.
+	// Use the 'bp_core_get_user_domain' filter instead.
+	$domain       = apply_filters( 'bp_core_get_user_domain_pre_cache', $domain, $user_id, $user_nicename, $user_login );
+
+	/**
+	 * Filters the domain for the passed user.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string $domain        Domain for the passed user.
+	 * @param int    $user_id       ID of the passed user.
+	 * @param string $user_nicename User nicename of the passed user.
+	 * @param string $user_login    User login of the passed user.
+	 */
+	return apply_filters( 'bp_core_get_user_domain', $domain, $user_id, $user_nicename, $user_login );
+}
+
+/**
+ * Fetch everything in the wp_users table for a user, without any usermeta.
+ *
+ * @since 1.2.0
+ *
+ * @param  int $user_id The ID of the user.
+ * @return array|bool Array of data on success, boolean false on failure.
+ */
+function bp_core_get_core_userdata( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	// Get core user data
+	$userdata = BP_Core_User::get_core_userdata( $user_id );
+
+	/**
+	 * Filters the userdata for a passed user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array|bool $userdata Array of user data for a passed user on success, boolean false on failure.
+	 */
+	return apply_filters( 'bp_core_get_core_userdata', $userdata );
+}
+
+/**
+ * Return the ID of a user, based on user_login.
+ *
+ * No longer used.
+ *
+ * @todo Deprecate.
+ *
+ * @param string $user_login user_login of the user being queried.
+ * @return int
+ */
+function bp_core_get_displayed_userid( $user_login ) {
+	return apply_filters( 'bp_core_get_displayed_userid', bp_core_get_userid( $user_login ) );
+}
+
+/**
+ * Return the user ID based on a user's user_login.
+ *
+ * @since 1.0.0
+ *
+ * @param string $username user_login to check.
+ * @return int|null The ID of the matched user on success, null on failure.
+ */
+function bp_core_get_userid( $username = '' ) {
+	if ( empty( $username ) ) {
+		return false;
+	}
+
+	$user = get_user_by( 'login', $username );
+
+	/**
+	 * Filters the ID of a user, based on user_login.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param int|null $value    ID of the user or null.
+	 * @param string   $username User login to check.
+	 */
+	return apply_filters( 'bp_core_get_userid', ! empty( $user->ID ) ? $user->ID : NULL, $username );
+}
+
+/**
+ * Return the user ID based on a user's user_nicename.
+ *
+ * @since 1.2.3
+ *
+ * @param string $user_nicename user_nicename to check.
+ * @return int|null The ID of the matched user on success, null on failure.
+ */
+function bp_core_get_userid_from_nicename( $user_nicename = '' ) {
+	if ( empty( $user_nicename ) ) {
+		return false;
+	}
+
+	$user = get_user_by( 'slug', $user_nicename );
+
+	/**
+	 * Filters the user ID based on user_nicename.
+	 *
+	 * @since 1.2.3
+	 *
+	 * @param int|null $value         ID of the user or null.
+	 * @param string   $user_nicename User nicename to check.
+	 */
+	return apply_filters( 'bp_core_get_userid_from_nicename', ! empty( $user->ID ) ? $user->ID : NULL, $user_nicename );
+}
+
+/**
+ * Return the username for a user based on their user id.
+ *
+ * This function is sensitive to the BP_ENABLE_USERNAME_COMPATIBILITY_MODE,
+ * so it will return the user_login or user_nicename as appropriate.
+ *
+ * @since 1.0.0
+ *
+ * @param int         $user_id       User ID to check.
+ * @param string|bool $user_nicename Optional. user_nicename of user being checked.
+ * @param string|bool $user_login    Optional. user_login of user being checked.
+ * @return string The username of the matched user or an empty string if no user is found.
+ */
+function bp_core_get_username( $user_id = 0, $user_nicename = false, $user_login = false ) {
+
+	if ( ! $user_nicename && ! $user_login ) {
+		// Pull an audible and maybe use the login over the nicename.
+		if ( bp_is_username_compatibility_mode() ) {
+			$username = get_the_author_meta( 'login', $user_id );
+		} else {
+			$username = get_the_author_meta( 'nicename', $user_id );
+		}
+	} else {
+		$username = bp_is_username_compatibility_mode() ? $user_login : $user_nicename;
+	}
+
+	/**
+	 * Filters the username based on originally provided user ID.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string $username Username determined by user ID.
+	 */
+	return apply_filters( 'bp_core_get_username', $username );
+}
+
+/**
+ * Return the user_nicename for a user based on their user_id.
+ *
+ * This should be used for linking to user profiles and anywhere else a
+ * sanitized and unique slug to a user is needed.
+ *
+ * @since 1.5.0
+ *
+ * @param int $user_id User ID to check.
+ * @return string The username of the matched user or an empty string if no user is found.
+ */
+function bp_members_get_user_nicename( $user_id ) {
+	/**
+	 * Filters the user_nicename based on originally provided user ID.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $username User nice name determined by user ID.
+	 */
+	return apply_filters( 'bp_members_get_user_nicename', get_the_author_meta( 'nicename', $user_id ) );
+}
+
+/**
+ * Return the email address for the user based on user ID.
+ *
+ * @since 1.0.0
+ *
+ * @param int $uid User ID to check.
+ * @return string The email for the matched user. Empty string if no user
+ *                matches the $user_id.
+ */
+function bp_core_get_user_email( $user_id ) {
+	/**
+	 * Filters the user email for user based on user ID.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string $email Email determined for the user.
+	 */
+	return apply_filters( 'bp_core_get_user_email', get_the_author_meta( 'email', $user_id ) );
+}
+
+/**
+ * Return a HTML formatted link for a user with the user's full name as the link text.
+ *
+ * Eg: <a href="http://andy.example.com/">Andy Peatling</a>
+ *
+ * Optional parameters will return just the name or just the URL.
+ *
+ * @since 1.0.0
+ *
+ * @param int  $user_id   User ID to check.
+ * @param bool $no_anchor Disable URL and HTML and just return full name.
+ *                        Default: false.
+ * @param bool $just_link Disable full name and HTML and just return the URL
+ *                        text. Default false.
+ * @return string|bool The link text based on passed parameters, or false on
+ *                     no match.
+ */
+function bp_core_get_userlink( $user_id, $no_anchor = false, $just_link = false ) {
+	$display_name = bp_core_get_user_displayname( $user_id );
+
+	if ( empty( $display_name ) ) {
+		return false;
+	}
+
+	if ( ! empty( $no_anchor ) ) {
+		return $display_name;
+	}
+
+	if ( !$url = bp_core_get_user_domain( $user_id ) ) {
+		return false;
+	}
+
+	if ( ! empty( $just_link ) ) {
+		return $url;
+	}
+
+	/**
+	 * Filters the link text for the passed in user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $value   Link text based on passed parameters.
+	 * @param int    $user_id ID of the user to check.
+	 */
+	return apply_filters( 'bp_core_get_userlink', '<a href="' . $url . '">' . $display_name . '</a>', $user_id );
+}
+
+/**
+ * Fetch the display name for a group of users.
+ *
+ * Uses the 'Name' field in xprofile if available. Falls back on WP
+ * display_name, and then user_nicename.
+ *
+ * @since 2.0.0
+ *
+ * @param array $user_ids Array of user IDs to get display names for.
+ * @return array Associative array of the format "id" => "displayname".
+ */
+function bp_core_get_user_displaynames( $user_ids ) {
+
+	// Sanitize.
+	$user_ids = wp_parse_id_list( $user_ids );
+
+	// Remove dupes and empties.
+	$user_ids = array_unique( array_filter( $user_ids ) );
+
+	if ( empty( $user_ids ) ) {
+		return array();
+	}
+
+	// Warm the WP users cache with a targeted bulk update.
+	cache_users( $user_ids );
+
+	$retval = array();
+	foreach ( $user_ids as $user_id ) {
+		$retval[ $user_id ] = bp_core_get_user_displayname( $user_id );
+	}
+
+	return $retval;
+}
+
+/**
+ * Fetch the display name for a user.
+ *
+ * @since 1.0.1
+ *
+ * @param int|string|bool $user_id_or_username User ID or username.
+ * @return string|bool The display name for the user in question, or false if
+ *                     user not found.
+ */
+function bp_core_get_user_displayname( $user_id_or_username ) {
+	if ( empty( $user_id_or_username ) ) {
+		return false;
+	}
+
+	if ( ! is_numeric( $user_id_or_username ) ) {
+		$user_id = bp_core_get_userid( $user_id_or_username );
+	} else {
+		$user_id = $user_id_or_username;
+	}
+
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters the display name for the passed in user.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string $fullname Display name for the user.
+	 * @param int    $user_id  ID of the user to check.
+	 */
+	return apply_filters( 'bp_core_get_user_displayname', get_the_author_meta( 'display_name', $user_id ), $user_id );
+}
+add_filter( 'bp_core_get_user_displayname', 'strip_tags', 1 );
+add_filter( 'bp_core_get_user_displayname', 'trim'          );
+add_filter( 'bp_core_get_user_displayname', 'stripslashes'  );
+add_filter( 'bp_core_get_user_displayname', 'esc_html'      );
+
+/**
+ * Return the user link for the user based on user email address.
+ *
+ * @since 1.0.0
+ *
+ * @param string $email The email address for the user.
+ * @return string The link to the users home base. False on no match.
+ */
+function bp_core_get_userlink_by_email( $email ) {
+	$user = get_user_by( 'email', $email );
+
+	/**
+	 * Filters the user link for the user based on user email address.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string|bool $value URL for the user if found, otherwise false.
+	 */
+	return apply_filters( 'bp_core_get_userlink_by_email', bp_core_get_userlink( $user->ID, false, false, true ) );
+}
+
+/**
+ * Return the user link for the user based on the supplied identifier.
+ *
+ * @since 1.0.0
+ *
+ * @param string $username If BP_ENABLE_USERNAME_COMPATIBILITY_MODE is set,
+ *                         this should be user_login, otherwise it should
+ *                         be user_nicename.
+ * @return string|bool The link to the user's domain, false on no match.
+ */
+function bp_core_get_userlink_by_username( $username ) {
+	if ( bp_is_username_compatibility_mode() ) {
+		$user_id = bp_core_get_userid( $username );
+	} else {
+		$user_id = bp_core_get_userid_from_nicename( $username );
+	}
+
+	/**
+	 * Filters the user link for the user based on username.
+	 *
+	 * @since 1.0.1
+	 *
+	 * @param string|bool $value URL for the user if found, otherwise false.
+	 */
+	return apply_filters( 'bp_core_get_userlink_by_username', bp_core_get_userlink( $user_id, false, false, true ) );
+}
+
+/**
+ * Return the total number of members for the installation.
+ *
+ * Note that this is a raw count of non-spam, activated users. It does not
+ * account for users who have logged activity (last_active). See
+ * {@link bp_core_get_active_member_count()}.
+ *
+ * @since 1.2.0
+ *
+ * @return int The total number of members.
+ */
+function bp_core_get_total_member_count() {
+	global $wpdb;
+
+	$count = wp_cache_get( 'bp_total_member_count', 'bp' );
+
+	if ( false === $count ) {
+		$status_sql = bp_core_get_status_sql();
+		$count = $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->users} WHERE {$status_sql}" );
+		wp_cache_set( 'bp_total_member_count', $count, 'bp' );
+	}
+
+	/**
+	 * Filters the total number of members for the installation.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $count Total number of members.
+	 */
+	return apply_filters( 'bp_core_get_total_member_count', $count );
+}
+
+/**
+ * Return the total number of members, limited to those members with last_activity.
+ *
+ * @since 1.6.0
+ *
+ * @return int The number of active members.
+ */
+function bp_core_get_active_member_count() {
+	global $wpdb;
+
+	$count = get_transient( 'bp_active_member_count' );
+	if ( false === $count ) {
+		$bp = buddypress();
+
+		// Avoid a costly join by splitting the lookup.
+		if ( is_multisite() ) {
+			$sql = "SELECT ID FROM {$wpdb->users} WHERE (user_status != 0 OR deleted != 0 OR user_status != 0)";
+		} else {
+			$sql = "SELECT ID FROM {$wpdb->users} WHERE user_status != 0";
+		}
+
+		$exclude_users     = $wpdb->get_col( $sql );
+		$exclude_users_sql = !empty( $exclude_users ) ? "AND user_id NOT IN (" . implode( ',', wp_parse_id_list( $exclude_users ) ) . ")" : '';
+		$count             = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(user_id) FROM {$bp->members->table_name_last_activity} WHERE component = %s AND type = 'last_activity' {$exclude_users_sql}", $bp->members->id ) );
+
+		set_transient( 'bp_active_member_count', $count );
+	}
+
+	/**
+	 * Filters the total number of members for the installation limited to those with last_activity.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $count Total number of active members.
+	 */
+	return apply_filters( 'bp_core_get_active_member_count', $count );
+}
+
+/**
+ * Process a spammed or unspammed user.
+ *
+ * This function is called from three places:
+ *
+ * - in bp_settings_action_capabilities() (from the front-end)
+ * - by bp_core_mark_user_spam_admin()    (from wp-admin)
+ * - bp_core_mark_user_ham_admin()        (from wp-admin)
+ *
+ * @since 1.6.0
+ *
+ * @param int    $user_id       The ID of the user being spammed/hammed.
+ * @param string $status        'spam' if being marked as spam, 'ham' otherwise.
+ * @param bool   $do_wp_cleanup True to force the cleanup of WordPress content
+ *                              and status, otherwise false. Generally, this should
+ *                              only be false if WordPress is expected to have
+ *                              performed this cleanup independently, as when hooked
+ *                              to 'make_spam_user'.
+ * @return bool True on success, false on failure.
+ */
+function bp_core_process_spammer_status( $user_id, $status, $do_wp_cleanup = true ) {
+	global $wpdb;
+
+	// Bail if no user ID.
+	if ( empty( $user_id ) ) {
+		return;
+	}
+
+	// Bail if user ID is super admin.
+	if ( is_super_admin( $user_id ) ) {
+		return;
+	}
+
+	// Get the functions file.
+	if ( is_multisite() ) {
+		require_once( ABSPATH . 'wp-admin/includes/ms.php' );
+	}
+
+	$is_spam = ( 'spam' == $status );
+
+	// Only you can prevent infinite loops.
+	remove_action( 'make_spam_user', 'bp_core_mark_user_spam_admin' );
+	remove_action( 'make_ham_user',  'bp_core_mark_user_ham_admin'  );
+
+	// Force the cleanup of WordPress content and status for multisite configs.
+	if ( $do_wp_cleanup ) {
+
+		// Get the blogs for the user.
+		$blogs = get_blogs_of_user( $user_id, true );
+
+		foreach ( (array) array_values( $blogs ) as $details ) {
+
+			// Do not mark the main or current root blog as spam.
+			if ( 1 == $details->userblog_id || bp_get_root_blog_id() == $details->userblog_id ) {
+				continue;
+			}
+
+			// Update the blog status.
+			update_blog_status( $details->userblog_id, 'spam', $is_spam );
+		}
+
+		// Finally, mark this user as a spammer.
+		if ( is_multisite() ) {
+			update_user_status( $user_id, 'spam', $is_spam );
+		}
+	}
+
+	// Update the user status.
+	$wpdb->update( $wpdb->users, array( 'user_status' => $is_spam ), array( 'ID' => $user_id ) );
+
+	// Clean user cache.
+	clean_user_cache( $user_id );
+
+	if ( ! is_multisite() ) {
+		// Call multisite actions in single site mode for good measure.
+		if ( true === $is_spam ) {
+
+			/**
+			 * Fires at end of processing spammer in Dashboard if not multisite and user is spam.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param int $value user ID.
+			 */
+			do_action( 'make_spam_user', $user_id );
+		} else {
+
+			/**
+			 * Fires at end of processing spammer in Dashboard if not multisite and user is not spam.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param int $value user ID.
+			 */
+			do_action( 'make_ham_user', $user_id );
+		}
+	}
+
+	// Hide this user's activity.
+	if ( ( true === $is_spam ) && bp_is_active( 'activity' ) ) {
+		bp_activity_hide_user_activity( $user_id );
+	}
+
+	// We need a special hook for is_spam so that components can delete data at spam time.
+	if ( true === $is_spam ) {
+
+		/**
+		 * Fires at the end of the process spammer process if the user is spam.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $value Displayed user ID.
+		 */
+		do_action( 'bp_make_spam_user', $user_id );
+	} else {
+
+		/**
+		 * Fires at the end of the process spammer process if the user is not spam.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $value Displayed user ID.
+		 */
+		do_action( 'bp_make_ham_user', $user_id );
+	}
+
+	/**
+	 * Fires at the end of the process for hanlding spammer status.
+	 *
+	 * @since 1.5.5
+	 *
+	 * @param int  $user_id ID of the processed user.
+	 * @param bool $is_spam The determined spam status of processed user.
+	 */
+	do_action( 'bp_core_process_spammer_status', $user_id, $is_spam );
+
+	// Put things back how we found them.
+	add_action( 'make_spam_user', 'bp_core_mark_user_spam_admin' );
+	add_action( 'make_ham_user',  'bp_core_mark_user_ham_admin'  );
+
+	return true;
+}
+/**
+ * Hook to WP's make_spam_user and run our custom BP spam functions.
+ *
+ * @since 1.6.0
+ *
+ * @param int $user_id The user ID passed from the make_spam_user hook.
+ */
+function bp_core_mark_user_spam_admin( $user_id ) {
+	bp_core_process_spammer_status( $user_id, 'spam', false );
+}
+add_action( 'make_spam_user', 'bp_core_mark_user_spam_admin' );
+
+/**
+ * Hook to WP's make_ham_user and run our custom BP spam functions.
+ *
+ * @since 1.6.0
+ *
+ * @param int $user_id The user ID passed from the make_ham_user hook.
+ */
+function bp_core_mark_user_ham_admin( $user_id ) {
+	bp_core_process_spammer_status( $user_id, 'ham', false );
+}
+add_action( 'make_ham_user', 'bp_core_mark_user_ham_admin' );
+
+/**
+ * Check whether a user has been marked as a spammer.
+ *
+ * @since 1.6.0
+ *
+ * @param int $user_id The ID for the user.
+ * @return bool True if spammer, otherwise false.
+ */
+function bp_is_user_spammer( $user_id = 0 ) {
+
+	// No user to check.
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	// Assume user is not spam.
+	$is_spammer = false;
+
+	// Setup our user.
+	$user = false;
+
+	// Get locally-cached data if available.
+	switch ( $user_id ) {
+		case bp_loggedin_user_id() :
+			$user = ! empty( $bp->loggedin_user->userdata ) ? $bp->loggedin_user->userdata : false;
+			break;
+
+		case bp_displayed_user_id() :
+			$user = ! empty( $bp->displayed_user->userdata ) ? $bp->displayed_user->userdata : false;
+			break;
+
+		case bp_get_member_user_id() :
+			global $members_template;
+			$user = isset( $members_template ) && isset( $members_template->member ) ? $members_template->member :  false;
+			break;
+	}
+
+	// Manually get userdata if still empty.
+	if ( empty( $user ) ) {
+		$user = get_userdata( $user_id );
+	}
+
+	// No user found.
+	if ( empty( $user ) ) {
+		$is_spammer = false;
+
+	// User found.
+	} else {
+
+		// Check if spam.
+		if ( !empty( $user->spam ) ) {
+			$is_spammer = true;
+		}
+
+		if ( 1 == $user->user_status ) {
+			$is_spammer = true;
+		}
+	}
+
+	/**
+	 * Filters whether a user is marked as a spammer.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $is_spammer Whether or not user is marked as spammer.
+	 */
+	return apply_filters( 'bp_is_user_spammer', (bool) $is_spammer );
+}
+
+/**
+ * Check whether a user has been marked as deleted.
+ *
+ * @since 1.6.0
+ *
+ * @param int $user_id The ID for the user.
+ * @return bool True if deleted, otherwise false.
+ */
+function bp_is_user_deleted( $user_id = 0 ) {
+
+	// No user to check.
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	// Assume user is not deleted.
+	$is_deleted = false;
+
+	// Setup our user.
+	$user = false;
+
+	// Get locally-cached data if available.
+	switch ( $user_id ) {
+		case bp_loggedin_user_id() :
+			$user = ! empty( $bp->loggedin_user->userdata ) ? $bp->loggedin_user->userdata : false;
+			break;
+
+		case bp_displayed_user_id() :
+			$user = ! empty( $bp->displayed_user->userdata ) ? $bp->displayed_user->userdata : false;
+			break;
+	}
+
+	// Manually get userdata if still empty.
+	if ( empty( $user ) ) {
+		$user = get_userdata( $user_id );
+	}
+
+	// No user found.
+	if ( empty( $user ) ) {
+		$is_deleted = true;
+
+	// User found.
+	} else {
+
+		// Check if deleted.
+		if ( !empty( $user->deleted ) ) {
+			$is_deleted = true;
+		}
+
+		if ( 2 == $user->user_status ) {
+			$is_deleted = true;
+		}
+	}
+
+	/**
+	 * Filters whether a user is marked as deleted.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param bool $is_deleted Whether or not user is marked as deleted.
+	 */
+	return apply_filters( 'bp_is_user_deleted', (bool) $is_deleted );
+}
+
+/**
+ * Check whether a user is "active", ie neither deleted nor spammer.
+ *
+ * @since 1.6.0
+ *
+ * @param int $user_id The user ID to check.
+ * @return bool True if active, otherwise false.
+ */
+function bp_is_user_active( $user_id = 0 ) {
+
+	// Default to current user.
+	if ( empty( $user_id ) && is_user_logged_in() ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// No user to check.
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	// Check spam.
+	if ( bp_is_user_spammer( $user_id ) ) {
+		return false;
+	}
+
+	// Check deleted.
+	if ( bp_is_user_deleted( $user_id ) ) {
+		return false;
+	}
+
+	// Assume true if not spam or deleted.
+	return true;
+}
+
+/**
+ * Check whether user is not active.
+ *
+ * @since 1.6.0
+ *
+ * @todo No need for the user fallback checks, since they're done in
+ *       bp_is_user_active().
+ *
+ * @param int $user_id The user ID to check.
+ * @return bool True if inactive, otherwise false.
+ */
+function bp_is_user_inactive( $user_id = 0 ) {
+
+	// Default to current user.
+	if ( empty( $user_id ) && is_user_logged_in() ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// No user to check.
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	// Return the inverse of active.
+	return !bp_is_user_active( $user_id );
+}
+
+/**
+ * Update a user's last activity.
+ *
+ * @since 1.9.0
+ *
+ * @param int    $user_id ID of the user being updated.
+ * @param string $time    Time of last activity, in 'Y-m-d H:i:s' format.
+ * @return bool True on success, false on failure.
+ */
+function bp_update_user_last_activity( $user_id = 0, $time = '' ) {
+
+	// Fall back on current user.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// Bail if the user id is 0, as there's nothing to update.
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	// Fall back on current time.
+	if ( empty( $time ) ) {
+		$time = bp_core_current_time();
+	}
+
+	// As of BuddyPress 2.0, last_activity is no longer stored in usermeta.
+	// However, we mirror it there for backward compatibility. Do not use!
+	// Remove our warning and re-add.
+	remove_filter( 'update_user_metadata', '_bp_update_user_meta_last_activity_warning', 10 );
+	remove_filter( 'get_user_metadata', '_bp_get_user_meta_last_activity_warning', 10 );
+	bp_update_user_meta( $user_id, 'last_activity', $time );
+	add_filter( 'update_user_metadata', '_bp_update_user_meta_last_activity_warning', 10, 4 );
+	add_filter( 'get_user_metadata', '_bp_get_user_meta_last_activity_warning', 10, 4 );
+
+	return BP_Core_User::update_last_activity( $user_id, $time );
+}
+
+/**
+ * Backward compatibility for 'last_activity' usermeta fetching.
+ *
+ * In BuddyPress 2.0, user last_activity data was moved out of usermeta. For
+ * backward compatibility, we continue to mirror the data there. This function
+ * serves two purposes: it warns plugin authors of the change, and it returns
+ * the data from the proper location.
+ *
+ * @since 2.0.0
+ * @since 2.9.3 Added the `$single` parameter.
+ *
+ * @access private For internal use only.
+ *
+ * @param null   $retval Null retval value.
+ * @param int    $object_id ID of the user.
+ * @param string $meta_key  Meta key being fetched.
+ * @param bool   $single    Whether a single key is being fetched (vs an array).
+ * @return string|null
+ */
+function _bp_get_user_meta_last_activity_warning( $retval, $object_id, $meta_key, $single ) {
+	static $warned = false;
+
+	if ( 'last_activity' === $meta_key ) {
+		// Don't send the warning more than once per pageload.
+		if ( false === $warned ) {
+			_doing_it_wrong( 'get_user_meta( $user_id, \'last_activity\' )', __( 'User last_activity data is no longer stored in usermeta. Use bp_get_user_last_activity() instead.', 'buddypress' ), '2.0.0' );
+			$warned = true;
+		}
+
+		$user_last_activity = bp_get_user_last_activity( $object_id );
+		if ( $single ) {
+			return $user_last_activity;
+		} else {
+			return array( $user_last_activity );
+		}
+	}
+
+	return $retval;
+}
+add_filter( 'get_user_metadata', '_bp_get_user_meta_last_activity_warning', 10, 4 );
+
+/**
+ * Backward compatibility for 'last_activity' usermeta setting.
+ *
+ * In BuddyPress 2.0, user last_activity data was moved out of usermeta. For
+ * backward compatibility, we continue to mirror the data there. This function
+ * serves two purposes: it warns plugin authors of the change, and it updates
+ * the data in the proper location.
+ *
+ * @since 2.0.0
+ *
+ * @access private For internal use only.
+ *
+ * @param int    $meta_id    ID of the just-set usermeta row.
+ * @param int    $object_id  ID of the user.
+ * @param string $meta_key   Meta key being fetched.
+ * @param string $meta_value Active time.
+ */
+function _bp_update_user_meta_last_activity_warning( $meta_id, $object_id, $meta_key, $meta_value ) {
+	if ( 'last_activity' === $meta_key ) {
+		_doing_it_wrong( 'update_user_meta( $user_id, \'last_activity\' )', __( 'User last_activity data is no longer stored in usermeta. Use bp_update_user_last_activity() instead.', 'buddypress' ), '2.0.0' );
+		bp_update_user_last_activity( $object_id, $meta_value );
+	}
+}
+add_filter( 'update_user_metadata', '_bp_update_user_meta_last_activity_warning', 10, 4 );
+
+/**
+ * Get the last activity for a given user.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id The ID of the user.
+ * @return string Time of last activity, in 'Y-m-d H:i:s' format, or an empty
+ *                string if none is found.
+ */
+function bp_get_user_last_activity( $user_id = 0 ) {
+	$activity = '';
+
+	$last_activity = BP_Core_User::get_last_activity( $user_id );
+	if ( ! empty( $last_activity[ $user_id ] ) ) {
+		$activity = $last_activity[ $user_id ]['date_recorded'];
+	}
+
+	/**
+	 * Filters the last activity for a given user.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $activity Time of last activity, in 'Y-m-d H:i:s' format or
+	 *                         an empty string if none found.
+	 * @param int    $user_id  ID of the user being checked.
+	 */
+	return apply_filters( 'bp_get_user_last_activity', $activity, $user_id );
+}
+
+/**
+ * Migrate last_activity data from the usermeta table to the activity table.
+ *
+ * Generally, this function is only run when BP is upgraded to 2.0. It can also
+ * be called directly from the BuddyPress Tools panel.
+ *
+ * @since 2.0.0
+ *
+ * @return bool
+ */
+function bp_last_activity_migrate() {
+	global $wpdb;
+
+	$bp = buddypress();
+
+	// Wipe out existing last_activity data in the activity table -
+	// this helps to prevent duplicates when pulling from the usermeta
+	// table.
+	$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->members->table_name_last_activity} WHERE component = %s AND type = 'last_activity'", $bp->members->id ) );
+
+	$sql = "INSERT INTO {$bp->members->table_name_last_activity} (`user_id`, `component`, `type`, `action`, `content`, `primary_link`, `item_id`, `date_recorded` ) (
+		  SELECT user_id, '{$bp->members->id}' as component, 'last_activity' as type, '' as action, '' as content, '' as primary_link, 0 as item_id, meta_value AS date_recorded
+		  FROM {$wpdb->usermeta}
+		  WHERE
+		    meta_key = 'last_activity'
+	);";
+
+	return $wpdb->query( $sql );
+}
+
+/**
+ * Fetch every post that is authored by the given user for the current blog.
+ *
+ * No longer used in BuddyPress.
+ *
+ * @todo Deprecate.
+ *
+ * @param int $user_id ID of the user being queried.
+ * @return array Post IDs.
+ */
+function bp_core_get_all_posts_for_user( $user_id = 0 ) {
+	global $wpdb;
+
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	return apply_filters( 'bp_core_get_all_posts_for_user', $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_author = %d AND post_status = 'publish' AND post_type = 'post'", $user_id ) ) );
+}
+
+/**
+ * Process account deletion requests.
+ *
+ * Primarily used for self-deletions, as requested through Settings.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id Optional. ID of the user to be deleted. Default: the
+ *                     logged-in user.
+ * @return bool True on success, false on failure.
+ */
+function bp_core_delete_account( $user_id = 0 ) {
+
+	// Use logged in user ID if none is passed.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// Site admins cannot be deleted.
+	if ( is_super_admin( $user_id ) ) {
+		return false;
+	}
+
+	// Extra checks if user is not deleting themselves.
+	if ( bp_loggedin_user_id() !== absint( $user_id ) ) {
+
+		// Bail if current user cannot delete any users.
+		if ( ! bp_current_user_can( 'delete_users' ) ) {
+			return false;
+		}
+
+		// Bail if current user cannot delete this user.
+		if ( ! current_user_can_for_blog( bp_get_root_blog_id(), 'delete_user', $user_id ) ) {
+			return false;
+		}
+	}
+
+	/**
+	 * Fires before the processing of an account deletion.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id ID of the user account being deleted.
+	 */
+	do_action( 'bp_core_pre_delete_account', $user_id );
+
+	// Specifically handle multi-site environment.
+	if ( is_multisite() ) {
+		require_once( ABSPATH . '/wp-admin/includes/ms.php'   );
+		require_once( ABSPATH . '/wp-admin/includes/user.php' );
+
+		$retval = wpmu_delete_user( $user_id );
+
+	// Single site user deletion.
+	} else {
+		require_once( ABSPATH . '/wp-admin/includes/user.php' );
+		$retval = wp_delete_user( $user_id );
+	}
+
+	/**
+	 * Fires after the deletion of an account.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $user_id ID of the user account that was deleted.
+	 */
+	do_action( 'bp_core_deleted_account', $user_id );
+
+	return $retval;
+}
+
+/**
+ * Delete a user's avatar when the user is deleted.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id ID of the user who is about to be deleted.
+ * @return bool True on success, false on failure.
+ */
+function bp_core_delete_avatar_on_user_delete( $user_id ) {
+	return bp_core_delete_existing_avatar( array(
+		'item_id' => $user_id,
+		'object'  => 'user',
+	) );
+}
+add_action( 'wpmu_delete_user', 'bp_core_delete_avatar_on_user_delete' );
+add_action( 'delete_user', 'bp_core_delete_avatar_on_user_delete' );
+
+/**
+ * Multibyte-safe ucfirst() support.
+ *
+ * Uses multibyte functions when available on the PHP build.
+ *
+ * @since 1.0.0
+ *
+ * @param string $str String to be upper-cased.
+ * @return string
+ */
+function bp_core_ucfirst( $str ) {
+	if ( function_exists( 'mb_strtoupper' ) && function_exists( 'mb_substr' ) ) {
+		$fc = mb_strtoupper( mb_substr( $str, 0, 1 ) );
+		return $fc.mb_substr( $str, 1 );
+	} else {
+		return ucfirst( $str );
+	}
+}
+
+/**
+ * Prevent spammers from logging in.
+ *
+ * When a user logs in, check if they have been marked as a spammer. If yes
+ * then simply redirect them to the home page and stop them from logging in.
+ *
+ * @since 1.1.2
+ *
+ * @param WP_User|WP_Error $user Either the WP_User object or the WP_Error
+ *                               object, as passed to the 'authenticate' filter.
+ * @return WP_User|WP_Error If the user is not a spammer, return the WP_User
+ *                          object. Otherwise a new WP_Error object.
+ */
+function bp_core_boot_spammer( $user ) {
+
+	// Check to see if the $user has already failed logging in, if so return $user as-is.
+	if ( is_wp_error( $user ) || empty( $user ) ) {
+		return $user;
+	}
+
+	// The user exists; now do a check to see if the user is a spammer
+	// if the user is a spammer, stop them in their tracks!
+	if ( is_a( $user, 'WP_User' ) && ( ( is_multisite() && (int) $user->spam ) || 1 == $user->user_status ) ) {
+		return new WP_Error( 'invalid_username', __( '<strong>ERROR</strong>: Your account has been marked as a spammer.', 'buddypress' ) );
+	}
+
+	// User is good to go!
+	return $user;
+}
+add_filter( 'authenticate', 'bp_core_boot_spammer', 30 );
+
+/**
+ * Delete last_activity data for the user when the user is deleted.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id The user ID for the user to delete usermeta for.
+ */
+function bp_core_remove_data( $user_id ) {
+
+	// Remove last_activity data.
+	BP_Core_User::delete_last_activity( $user_id );
+
+	// Flush the cache to remove the user from all cached objects.
+	wp_cache_flush();
+}
+add_action( 'wpmu_delete_user',  'bp_core_remove_data' );
+add_action( 'delete_user',       'bp_core_remove_data' );
+add_action( 'bp_make_spam_user', 'bp_core_remove_data' );
+
+/**
+ * Check whether the logged-in user can edit settings for the displayed user.
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if editing is allowed, otherwise false.
+ */
+function bp_core_can_edit_settings() {
+	$status = false;
+
+	if ( bp_is_my_profile() ) {
+		$status = true;
+	} elseif ( is_super_admin( bp_displayed_user_id() ) && ! is_super_admin() ) {
+		$status = false;
+	} elseif ( bp_current_user_can( 'bp_moderate' ) || current_user_can( 'edit_users' ) ) {
+		$status = true;
+	}
+
+	/**
+	 * Filters the status of whether the logged-in user can edit settings for the displayed user or not.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param bool True if editing is allowed, otherwise false.
+	 */
+	return apply_filters( 'bp_core_can_edit_settings', $status );
+}
+
+/** Sign-up *******************************************************************/
+
+/**
+ * Flush illegal names by getting and setting 'illegal_names' site option.
+ *
+ * @since 1.2.5
+ */
+function bp_core_flush_illegal_names() {
+	$illegal_names = get_site_option( 'illegal_names' );
+	update_site_option( 'illegal_names', $illegal_names );
+}
+
+/**
+ * Add BuddyPress-specific items to the illegal_names array.
+ *
+ * @since 1.2.7
+ *
+ * @param array|string $value    Illegal names as being saved defined in
+ *                               Multisite settings.
+ * @param array|string $oldvalue The old value of the option.
+ * @return array Merged and unique array of illegal names.
+ */
+function bp_core_get_illegal_names( $value = '', $oldvalue = '' ) {
+
+	// Make sure $value is array.
+	if ( empty( $value ) ) {
+		$db_illegal_names = array();
+	}
+
+	if ( is_array( $value ) ) {
+		$db_illegal_names = $value;
+	} elseif ( is_string( $value ) ) {
+		$db_illegal_names = explode( ' ', $value );
+	}
+
+	// Add the core components' slugs to the banned list even if their components aren't active.
+	$bp_component_slugs = array(
+		'groups',
+		'members',
+		'forums',
+		'blogs',
+		'activity',
+		'profile',
+		'friends',
+		'search',
+		'settings',
+		'notifications',
+		'register',
+		'activate'
+	);
+
+	// Core constants.
+	$slug_constants = array(
+		'BP_GROUPS_SLUG',
+		'BP_MEMBERS_SLUG',
+		'BP_FORUMS_SLUG',
+		'BP_BLOGS_SLUG',
+		'BP_ACTIVITY_SLUG',
+		'BP_XPROFILE_SLUG',
+		'BP_FRIENDS_SLUG',
+		'BP_SEARCH_SLUG',
+		'BP_SETTINGS_SLUG',
+		'BP_NOTIFICATIONS_SLUG',
+		'BP_REGISTER_SLUG',
+		'BP_ACTIVATION_SLUG',
+	);
+	foreach( $slug_constants as $constant ) {
+		if ( defined( $constant ) ) {
+			$bp_component_slugs[] = constant( $constant );
+		}
+	}
+
+	/**
+	 * Filters the array of default illegal usernames.
+	 *
+	 * @since 1.2.2
+	 *
+	 * @param array $value Merged and unique array of illegal usernames.
+	 */
+	$filtered_illegal_names = apply_filters( 'bp_core_illegal_usernames', array_merge( array( 'www', 'web', 'root', 'admin', 'main', 'invite', 'administrator' ), $bp_component_slugs ) );
+
+	/**
+	 * Filters the list of illegal usernames from WordPress.
+	 *
+	 * @since 3.0
+	 *
+	 * @param array Array of illegal usernames.
+	 */
+	$wp_filtered_illegal_names = apply_filters( 'illegal_user_logins', array() );
+
+	// First merge BuddyPress illegal names.
+	$bp_merged_names           = array_merge( (array) $filtered_illegal_names, (array) $db_illegal_names );
+
+	// Then merge WordPress and BuddyPress illegal names.
+	$merged_names              = array_merge( (array) $wp_filtered_illegal_names, (array) $bp_merged_names );
+
+	// Remove duplicates.
+	$illegal_names             = array_unique( (array) $merged_names );
+
+	/**
+	 * Filters the array of default illegal names.
+	 *
+	 * @since 1.2.5
+	 *
+	 * @param array $value Merged and unique array of illegal names.
+	 */
+	return apply_filters( 'bp_core_illegal_names', $illegal_names );
+}
+add_filter( 'pre_update_site_option_illegal_names', 'bp_core_get_illegal_names', 10, 2 );
+
+/**
+ * Check that an email address is valid for use.
+ *
+ * Performs the following checks:
+ *   - Is the email address well-formed?
+ *   - Is the email address already used?
+ *   - If there's an email domain blacklist, is the current domain on it?
+ *   - If there's an email domain whitelest, is the current domain on it?
+ *
+ * @since 1.6.2
+ *
+ * @param string $user_email The email being checked.
+ * @return bool|array True if the address passes all checks; otherwise an array
+ *                    of error codes.
+ */
+function bp_core_validate_email_address( $user_email ) {
+	$errors = array();
+
+	$user_email = sanitize_email( $user_email );
+
+	// Is the email well-formed?
+	if ( ! is_email( $user_email ) ) {
+		$errors['invalid'] = 1;
+	}
+
+	// Is the email on the Banned Email Domains list?
+	// Note: This check only works on Multisite.
+	if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $user_email ) ) {
+		$errors['domain_banned'] = 1;
+	}
+
+	// Is the email on the Limited Email Domains list?
+	// Note: This check only works on Multisite.
+	$limited_email_domains = get_site_option( 'limited_email_domains' );
+	if ( is_array( $limited_email_domains ) && empty( $limited_email_domains ) == false ) {
+		$emaildomain = substr( $user_email, 1 + strpos( $user_email, '@' ) );
+		if ( ! in_array( $emaildomain, $limited_email_domains ) ) {
+			$errors['domain_not_allowed'] = 1;
+		}
+	}
+
+	// Is the email alreday in use?
+	if ( email_exists( $user_email ) ) {
+		$errors['in_use'] = 1;
+	}
+
+	$retval = ! empty( $errors ) ? $errors : true;
+
+	return $retval;
+}
+
+/**
+ * Add the appropriate errors to a WP_Error object, given results of a validation test.
+ *
+ * Functions like bp_core_validate_email_address() return a structured array
+ * of error codes. bp_core_add_validation_error_messages() takes this array and
+ * parses, adding the appropriate error messages to the WP_Error object.
+ *
+ * @since 1.7.0
+ *
+ * @see bp_core_validate_email_address()
+ *
+ * @param WP_Error $errors             WP_Error object.
+ * @param array    $validation_results The return value of a validation function
+ *                                     like bp_core_validate_email_address().
+ */
+function bp_core_add_validation_error_messages( WP_Error $errors, $validation_results ) {
+	if ( ! empty( $validation_results['invalid'] ) ) {
+		$errors->add( 'user_email', __( 'Please check your email address.', 'buddypress' ) );
+	}
+
+	if ( ! empty( $validation_results['domain_banned'] ) ) {
+		$errors->add( 'user_email',  __( 'Sorry, that email address is not allowed!', 'buddypress' ) );
+	}
+
+	if ( ! empty( $validation_results['domain_not_allowed'] ) ) {
+		$errors->add( 'user_email', __( 'Sorry, that email address is not allowed!', 'buddypress' ) );
+	}
+
+	if ( ! empty( $validation_results['in_use'] ) ) {
+		$errors->add( 'user_email', __( 'Sorry, that email address is already used!', 'buddypress' ) );
+	}
+}
+
+/**
+ * Validate a user name and email address when creating a new user.
+ *
+ * @since 1.2.2
+ *
+ * @param string $user_name  Username to validate.
+ * @param string $user_email Email address to validate.
+ * @return array Results of user validation including errors, if any.
+ */
+function bp_core_validate_user_signup( $user_name, $user_email ) {
+
+	// Make sure illegal names include BuddyPress slugs and values.
+	bp_core_flush_illegal_names();
+
+	// WordPress Multisite has its own validation. Use it, so that we
+	// properly mirror restrictions on username, etc.
+	if ( function_exists( 'wpmu_validate_user_signup' ) ) {
+		$result = wpmu_validate_user_signup( $user_name, $user_email );
+
+	// When not running Multisite, we perform our own validation. What
+	// follows reproduces much of the logic of wpmu_validate_user_signup(),
+	// minus the multisite-specific restrictions on user_login.
+	} else {
+		$errors = new WP_Error();
+
+		/**
+		 * Filters the username before being validated.
+		 *
+		 * @since 1.5.5
+		 *
+		 * @param string $user_name Username to validate.
+		 */
+		$user_name = apply_filters( 'pre_user_login', $user_name );
+
+		// User name can't be empty.
+		if ( empty( $user_name ) ) {
+			$errors->add( 'user_name', __( 'Please enter a username', 'buddypress' ) );
+		}
+
+		// User name can't be on the blacklist.
+		$illegal_names = get_site_option( 'illegal_names' );
+		if ( in_array( $user_name, (array) $illegal_names ) ) {
+			$errors->add( 'user_name', __( 'That username is not allowed', 'buddypress' ) );
+		}
+
+		// User name must pass WP's validity check.
+		if ( ! validate_username( $user_name ) ) {
+			$errors->add( 'user_name', __( 'Usernames can contain only letters, numbers, ., -, and @', 'buddypress' ) );
+		}
+
+		// Minimum of 4 characters.
+		if ( strlen( $user_name ) < 4 ) {
+			$errors->add( 'user_name',  __( 'Username must be at least 4 characters', 'buddypress' ) );
+		}
+
+		// No underscores. @todo Why not?
+		if ( false !== strpos( ' ' . $user_name, '_' ) ) {
+			$errors->add( 'user_name', __( 'Sorry, usernames may not contain the character "_"!', 'buddypress' ) );
+		}
+
+		// No usernames that are all numeric. @todo Why?
+		$match = array();
+		preg_match( '/[0-9]*/', $user_name, $match );
+		if ( $match[0] == $user_name ) {
+			$errors->add( 'user_name', __( 'Sorry, usernames must have letters too!', 'buddypress' ) );
+		}
+
+		// Check into signups.
+		$signups = BP_Signup::get( array(
+			'user_login' => $user_name,
+		) );
+
+		$signup = isset( $signups['signups'] ) && ! empty( $signups['signups'][0] ) ? $signups['signups'][0] : false;
+
+		// Check if the username has been used already.
+		if ( username_exists( $user_name ) || ! empty( $signup ) ) {
+			$errors->add( 'user_name', __( 'Sorry, that username already exists!', 'buddypress' ) );
+		}
+
+		// Validate the email address and process the validation results into
+		// error messages.
+		$validate_email = bp_core_validate_email_address( $user_email );
+		bp_core_add_validation_error_messages( $errors, $validate_email );
+
+		// Assemble the return array.
+		$result = array(
+			'user_name'  => $user_name,
+			'user_email' => $user_email,
+			'errors'     => $errors,
+		);
+
+		// Apply WPMU legacy filter.
+		$result = apply_filters( 'wpmu_validate_user_signup', $result );
+	}
+
+	/**
+	 * Filters the result of the user signup validation.
+	 *
+	 * @since 1.2.2
+	 *
+	 * @param array $result Results of user validation including errors, if any.
+	 */
+	return apply_filters( 'bp_core_validate_user_signup', $result );
+}
+
+/**
+ * Validate blog URL and title provided at signup.
+ *
+ * @since 1.2.2
+ *
+ * @todo Why do we have this wrapper?
+ *
+ * @param string $blog_url   Blog URL requested during registration.
+ * @param string $blog_title Blog title requested during registration.
+ * @return array
+ */
+function bp_core_validate_blog_signup( $blog_url, $blog_title ) {
+	if ( ! is_multisite() || ! function_exists( 'wpmu_validate_blog_signup' ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters the validated blog url and title provided at signup.
+	 *
+	 * @since 1.2.2
+	 *
+	 * @param array $value Array with the new site data and error messages.
+	 */
+	return apply_filters( 'bp_core_validate_blog_signup', wpmu_validate_blog_signup( $blog_url, $blog_title ) );
+}
+
+/**
+ * Process data submitted at user registration and convert to a signup object.
+ *
+ * @since 1.2.0
+ *
+ * @todo There appears to be a bug in the return value on success.
+ *
+ * @param string $user_login    Login name requested by the user.
+ * @param string $user_password Password requested by the user.
+ * @param string $user_email    Email address entered by the user.
+ * @param array  $usermeta      Miscellaneous metadata about the user (blog-specific
+ *                              signup data, xprofile data, etc).
+ * @return int|false True on success, WP_Error on failure.
+ */
+function bp_core_signup_user( $user_login, $user_password, $user_email, $usermeta ) {
+	$bp = buddypress();
+
+	// We need to cast $user_id to pass to the filters.
+	$user_id = false;
+
+	// Multisite installs have their own install procedure.
+	if ( is_multisite() ) {
+		wpmu_signup_user( $user_login, $user_email, $usermeta );
+
+	} else {
+		// Format data.
+		$user_login     = preg_replace( '/\s+/', '', sanitize_user( $user_login, true ) );
+		$user_email     = sanitize_email( $user_email );
+		$activation_key = wp_generate_password( 32, false );
+
+		/**
+		 * WordPress's default behavior is to create user accounts
+		 * immediately at registration time. BuddyPress uses a system
+		 * borrowed from WordPress Multisite, where signups are stored
+		 * separately and accounts are only created at the time of
+		 * activation. For backward compatibility with plugins that may
+		 * be anticipating WP's default behavior, BP silently creates
+		 * accounts for registrations (though it does not use them). If
+		 * you know that you are not running any plugins dependent on
+		 * these pending accounts, you may want to save a little DB
+		 * clutter by defining setting the BP_SIGNUPS_SKIP_USER_CREATION
+		 * to true in your wp-config.php file.
+		 */
+		if ( ! defined( 'BP_SIGNUPS_SKIP_USER_CREATION' ) || ! BP_SIGNUPS_SKIP_USER_CREATION ) {
+			$user_id = BP_Signup::add_backcompat( $user_login, $user_password, $user_email, $usermeta );
+
+			if ( is_wp_error( $user_id ) ) {
+				return $user_id;
+			}
+
+			bp_update_user_meta( $user_id, 'activation_key', $activation_key );
+		}
+
+		$args = array(
+			'user_login'     => $user_login,
+			'user_email'     => $user_email,
+			'activation_key' => $activation_key,
+			'meta'           => $usermeta,
+		);
+
+		BP_Signup::add( $args );
+
+		/**
+		 * Filters if BuddyPress should send an activation key for a new signup.
+		 *
+		 * @since 1.2.3
+		 *
+		 * @param bool   $value          Whether or not to send the activation key.
+		 * @param int    $user_id        User ID to send activation key to.
+		 * @param string $user_email     User email to send activation key to.
+		 * @param string $activation_key Activation key to be sent.
+		 * @param array  $usermeta       Miscellaneous metadata about the user (blog-specific
+		 *                               signup data, xprofile data, etc).
+		 */
+		if ( apply_filters( 'bp_core_signup_send_activation_key', true, $user_id, $user_email, $activation_key, $usermeta ) ) {
+			bp_core_signup_send_validation_email( $user_id, $user_email, $activation_key, $user_login );
+		}
+	}
+
+	$bp->signup->username = $user_login;
+
+	/**
+	 * Fires at the end of the process to sign up a user.
+	 *
+	 * @since 1.2.2
+	 *
+	 * @param bool|WP_Error   $user_id       True on success, WP_Error on failure.
+	 * @param string          $user_login    Login name requested by the user.
+	 * @param string          $user_password Password requested by the user.
+	 * @param string          $user_email    Email address requested by the user.
+	 * @param array           $usermeta      Miscellaneous metadata about the user (blog-specific
+	 *                                       signup data, xprofile data, etc).
+	 */
+	do_action( 'bp_core_signup_user', $user_id, $user_login, $user_password, $user_email, $usermeta );
+
+	return $user_id;
+}
+
+/**
+ * Create a blog and user based on data supplied at user registration.
+ *
+ * @since 1.2.2
+ *
+ * @param string $blog_domain Domain requested by user.
+ * @param string $blog_path   Path requested by user.
+ * @param string $blog_title  Title as entered by user.
+ * @param string $user_name   user_login of requesting user.
+ * @param string $user_email  Email address of requesting user.
+ * @param string $usermeta    Miscellaneous metadata for the user.
+ * @return bool
+ */
+function bp_core_signup_blog( $blog_domain, $blog_path, $blog_title, $user_name, $user_email, $usermeta ) {
+	if ( ! is_multisite() || ! function_exists( 'wpmu_signup_blog' ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters the result of wpmu_signup_blog().
+	 *
+	 * This filter provides no value and is retained for
+	 * backwards compatibility.
+	 *
+	 * @since 1.2.2
+	 *
+	 * @param void $value
+	 */
+	return apply_filters( 'bp_core_signup_blog', wpmu_signup_blog( $blog_domain, $blog_path, $blog_title, $user_name, $user_email, $usermeta ) );
+}
+
+/**
+ * Activate a signup, as identified by an activation key.
+ *
+ * @since 1.2.2
+ *
+ * @param string $key Activation key.
+ * @return int|bool User ID on success, false on failure.
+ */
+function bp_core_activate_signup( $key ) {
+	global $wpdb;
+
+	$user = false;
+
+	// Multisite installs have their own activation routine.
+	if ( is_multisite() ) {
+		$user = wpmu_activate_signup( $key );
+
+		// If there were errors, add a message and redirect.
+		if ( ! empty( $user->errors ) ) {
+			return $user;
+		}
+
+		$user_id = $user['user_id'];
+
+	} else {
+		$signups = BP_Signup::get( array(
+			'activation_key' => $key,
+		) );
+
+		if ( empty( $signups['signups'] ) ) {
+			return new WP_Error( 'invalid_key', __( 'Invalid activation key.', 'buddypress' ) );
+		}
+
+		$signup = $signups['signups'][0];
+
+		if ( $signup->active ) {
+			if ( empty( $signup->domain ) ) {
+				return new WP_Error( 'already_active', __( 'The user is already active.', 'buddypress' ), $signup );
+			} else {
+				return new WP_Error( 'already_active', __( 'The site is already active.', 'buddypress' ), $signup );
+			}
+		}
+
+		// Password is hashed again in wp_insert_user.
+		$password = wp_generate_password( 12, false );
+
+		$user_id = username_exists( $signup->user_login );
+
+		// Create the user. This should only be necessary if BP_SIGNUPS_SKIP_USER_CREATION is true.
+		if ( ! $user_id ) {
+			$user_id = wp_create_user( $signup->user_login, $password, $signup->user_email );
+
+		// Otherwise, update the existing user's status.
+		} elseif ( $key === bp_get_user_meta( $user_id, 'activation_key', true ) || $key === wp_hash( $user_id ) ) {
+
+			// Change the user's status so they become active.
+			if ( ! $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->users} SET user_status = 0 WHERE ID = %d", $user_id ) ) ) {
+				return new WP_Error( 'invalid_key', __( 'Invalid activation key.', 'buddypress' ) );
+			}
+
+			bp_delete_user_meta( $user_id, 'activation_key' );
+
+			$user_already_created = true;
+
+		} else {
+			$user_already_exists = true;
+		}
+
+		if ( ! $user_id ) {
+			return new WP_Error( 'create_user', __( 'Could not create user', 'buddypress' ), $signup );
+		}
+
+		// Fetch the signup so we have the data later on.
+		$signups = BP_Signup::get( array(
+			'activation_key' => $key,
+		) );
+
+		$signup = isset( $signups['signups'] ) && ! empty( $signups['signups'][0] ) ? $signups['signups'][0] : false;
+
+		// Activate the signup.
+		BP_Signup::validate( $key );
+
+		if ( isset( $user_already_exists ) ) {
+			return new WP_Error( 'user_already_exists', __( 'That username is already activated.', 'buddypress' ), $signup );
+		}
+
+		// Set up data to pass to the legacy filter.
+		$user = array(
+			'user_id'  => $user_id,
+			'password' => $signup->meta['password'],
+			'meta'     => $signup->meta,
+		);
+
+		/**
+		 * Maybe notify the site admin of a new user registration.
+		 *
+		 * @since 1.2.2
+		 *
+		 * @param bool $notification Whether to send the notification or not.
+		 */
+		if ( apply_filters( 'bp_core_send_user_registration_admin_notification', true ) ) {
+			wp_new_user_notification( $user_id );
+		}
+
+		if ( isset( $user_already_created ) ) {
+
+			/**
+			 * Fires if the user has already been created.
+			 *
+			 * @since 1.2.2
+			 *
+			 * @param int    $user_id ID of the user being checked.
+			 * @param string $key     Activation key.
+			 * @param array  $user    Array of user data.
+			 */
+			do_action( 'bp_core_activated_user', $user_id, $key, $user );
+			return $user_id;
+		}
+	}
+
+	// Set any profile data.
+	if ( bp_is_active( 'xprofile' ) ) {
+		if ( ! empty( $user['meta']['profile_field_ids'] ) ) {
+			$profile_field_ids = explode( ',', $user['meta']['profile_field_ids'] );
+
+			foreach( (array) $profile_field_ids as $field_id ) {
+				$current_field = isset( $user['meta']["field_{$field_id}"] ) ? $user['meta']["field_{$field_id}"] : false;
+
+				if ( !empty( $current_field ) ) {
+					xprofile_set_field_data( $field_id, $user_id, $current_field );
+				}
+
+				/*
+				 * Save the visibility level.
+				 *
+				 * Use the field's default visibility if not present, and 'public' if a
+				 * default visibility is not defined.
+				 */
+				$key = "field_{$field_id}_visibility";
+				if ( isset( $user['meta'][ $key ] ) ) {
+					$visibility_level = $user['meta'][ $key ];
+				} else {
+					$vfield           = xprofile_get_field( $field_id );
+					$visibility_level = isset( $vfield->default_visibility ) ? $vfield->default_visibility : 'public';
+				}
+				xprofile_set_field_visibility_level( $field_id, $user_id, $visibility_level );
+			}
+		}
+	}
+
+	// Replace the password automatically generated by WordPress by the one the user chose.
+	if ( ! empty( $user['meta']['password'] ) ) {
+		$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->users} SET user_pass = %s WHERE ID = %d", $user['meta']['password'], $user_id ) );
+
+		/**
+		 * Make sure to clean the user's cache as we've
+		 * directly edited the password without using
+		 * wp_update_user().
+		 *
+		 * If we can't use wp_update_user() that's because
+		 * we already hashed the password at the signup step.
+		 */
+		$uc = wp_cache_get( $user_id, 'users' );
+
+		if ( ! empty( $uc->ID ) ) {
+			clean_user_cache( $uc->ID );
+		}
+	}
+
+	/**
+	 * Fires at the end of the user activation process.
+	 *
+	 * @since 1.2.2
+	 *
+	 * @param int    $user_id ID of the user being checked.
+	 * @param string $key     Activation key.
+	 * @param array  $user    Array of user data.
+	 */
+	do_action( 'bp_core_activated_user', $user_id, $key, $user );
+
+	return $user_id;
+}
+
+/**
+ * Add default WordPress role for new signups on the BP root blog.
+ *
+ * @since 3.0.0
+ *
+ * @param int $user_id The user ID to add the default role for.
+ */
+function bp_members_add_role_after_activation( $user_id ) {
+	// Get default role to add.
+	$role = bp_get_option( 'default_role' );
+
+	// Multisite.
+	if ( is_multisite() && ! is_user_member_of_blog( $user_id, bp_get_root_blog_id() ) ) {
+		add_user_to_blog( bp_get_root_blog_id(), $user_id, $role );
+
+	// Single-site.
+	} elseif ( ! is_multisite() ) {
+		$member = get_userdata( $user_id );
+		$member->set_role( $role );
+	}
+}
+add_action( 'bp_core_activated_user', 'bp_members_add_role_after_activation', 1 );
+
+/**
+ * Migrate signups from pre-2.0 configuration to wp_signups.
+ *
+ * @since 2.0.1
+ */
+function bp_members_migrate_signups() {
+	global $wpdb;
+
+	$status_2_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->users} WHERE user_status = '2'" );
+
+	if ( ! empty( $status_2_ids ) ) {
+		$signups = get_users( array(
+			'fields'  => array(
+				'ID',
+				'user_login',
+				'user_pass',
+				'user_registered',
+				'user_email',
+				'display_name',
+			),
+			'include' => $status_2_ids,
+		) );
+
+		// Fetch activation keys separately, to avoid the all_with_meta
+		// overhead.
+		$status_2_ids_sql = implode( ',', $status_2_ids );
+		$ak_data = $wpdb->get_results( "SELECT user_id, meta_value FROM {$wpdb->usermeta} WHERE meta_key = 'activation_key' AND user_id IN ({$status_2_ids_sql})" );
+
+		// Rekey.
+		$activation_keys = array();
+		foreach ( $ak_data as $ak_datum ) {
+			$activation_keys[ intval( $ak_datum->user_id ) ] = $ak_datum->meta_value;
+		}
+
+		unset( $status_2_ids_sql, $status_2_ids, $ak_data );
+
+		// Merge.
+		foreach ( $signups as &$signup ) {
+			if ( isset( $activation_keys[ $signup->ID ] ) ) {
+				$signup->activation_key = $activation_keys[ $signup->ID ];
+			}
+		}
+
+		// Reset the signup var as we're using it to process the migration.
+		unset( $signup );
+
+	} else {
+		return;
+	}
+
+	foreach ( $signups as $signup ) {
+		$meta = array();
+
+		// Rebuild the activation key, if missing.
+		if ( empty( $signup->activation_key ) ) {
+			$signup->activation_key = wp_generate_password( 32, false );
+		}
+
+		if ( bp_is_active( 'xprofile' ) ) {
+			$meta['field_1'] = $signup->display_name;
+		}
+
+		$meta['password'] = $signup->user_pass;
+
+		$user_login = preg_replace( '/\s+/', '', sanitize_user( $signup->user_login, true ) );
+		$user_email = sanitize_email( $signup->user_email );
+
+		BP_Signup::add( array(
+			'user_login'     => $user_login,
+			'user_email'     => $user_email,
+			'registered'     => $signup->user_registered,
+			'activation_key' => $signup->activation_key,
+			'meta'           => $meta
+		) );
+
+		// Deleting these options will remove signups from users count.
+		delete_user_option( $signup->ID, 'capabilities' );
+		delete_user_option( $signup->ID, 'user_level'   );
+	}
+}
+
+/**
+ * Map a user's WP display name to the XProfile fullname field, if necessary.
+ *
+ * This only happens when a user is registered in wp-admin by an administrator;
+ * during normal registration, XProfile data is provided directly by the user.
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id ID of the user.
+ * @return bool
+ */
+function bp_core_map_user_registration( $user_id ) {
+
+	// Only map data when the site admin is adding users, not on registration.
+	if ( ! is_admin() ) {
+		return false;
+	}
+
+	// Add the user's fullname to Xprofile.
+	if ( bp_is_active( 'xprofile' ) ) {
+		$firstname = bp_get_user_meta( $user_id, 'first_name', true );
+		$lastname = ' ' . bp_get_user_meta( $user_id, 'last_name', true );
+		$name = $firstname . $lastname;
+
+		if ( empty( $name ) || ' ' == $name ) {
+			$name = bp_get_user_meta( $user_id, 'nickname', true );
+		}
+
+		xprofile_set_field_data( 1, $user_id, $name );
+	}
+}
+add_action( 'user_register', 'bp_core_map_user_registration' );
+
+/**
+ * Get the avatar storage directory for use during registration.
+ *
+ * @since 1.1.0
+ *
+ * @return string|bool Directory path on success, false on failure.
+ */
+function bp_core_signup_avatar_upload_dir() {
+	$bp = buddypress();
+
+	if ( empty( $bp->signup->avatar_dir ) ) {
+		return false;
+	}
+
+	$directory = 'avatars/signups';
+	$path      = bp_core_avatar_upload_path() . '/' . $directory . '/' . $bp->signup->avatar_dir;
+	$newbdir   = $path;
+	$newurl    = bp_core_avatar_url() . '/' . $directory . '/' . $bp->signup->avatar_dir;
+	$newburl   = $newurl;
+	$newsubdir = '/' . $directory . '/' . $bp->signup->avatar_dir;
+
+	/**
+	 * Filters the avatar storage directory for use during registration.
+	 *
+	 * @since 1.1.1
+	 *
+	 * @param array $value Array of path and URL values for created storage directory.
+	 */
+	return apply_filters( 'bp_core_signup_avatar_upload_dir', array(
+		'path'    => $path,
+		'url'     => $newurl,
+		'subdir'  => $newsubdir,
+		'basedir' => $newbdir,
+		'baseurl' => $newburl,
+		'error'   => false
+	) );
+}
+
+/**
+ * Send activation email to a newly registered user.
+ *
+ * @since 1.2.2
+ * @since 2.5.0 Add the $user_login parameter.
+ *
+ * @param int|bool $user_id    ID of the new user, false if BP_SIGNUPS_SKIP_USER_CREATION is true.
+ * @param string   $user_email Email address of the new user.
+ * @param string   $key        Activation key.
+ * @param string   $user_login Optional. The user login name.
+ */
+function bp_core_signup_send_validation_email( $user_id, $user_email, $key, $user_login = '' ) {
+	$args = array(
+		'tokens' => array(
+			'activate.url' => esc_url( trailingslashit( bp_get_activation_page() ) . "{$key}/" ),
+			'key'          => $key,
+			'user.email'   => $user_email,
+			'user.id'      => $user_id,
+		),
+	);
+
+	if ( $user_id ) {
+		$to = $user_id;
+	} else {
+		$to = array( array( $user_email => $user_login ) );
+	}
+
+	bp_send_email( 'core-user-registration', $to, $args );
+}
+
+/**
+ * Display a "resend email" link when an unregistered user attempts to log in.
+ *
+ * @since 1.2.2
+ *
+ * @param WP_User|WP_Error|null $user     Either the WP_User or the WP_Error object.
+ * @param string                $username The inputted, attempted username.
+ * @param string                $password The inputted, attempted password.
+ * @return WP_User|WP_Error
+ */
+function bp_core_signup_disable_inactive( $user = null, $username = '', $password ='' ) {
+	// Login form not used.
+	if ( empty( $username ) && empty( $password ) ) {
+		return $user;
+	}
+
+	// An existing WP_User with a user_status of 2 is either a legacy
+	// signup, or is a user created for backward compatibility. See
+	// {@link bp_core_signup_user()} for more details.
+	if ( is_a( $user, 'WP_User' ) && 2 == $user->user_status ) {
+		$user_login = $user->user_login;
+
+	// If no WP_User is found corresponding to the username, this
+	// is a potential signup.
+	} elseif ( is_wp_error( $user ) && 'invalid_username' == $user->get_error_code() ) {
+		$user_login = $username;
+
+	// This is an activated user, so bail.
+	} else {
+		return $user;
+	}
+
+	// Look for the unactivated signup corresponding to the login name.
+	$signup = BP_Signup::get( array( 'user_login' => sanitize_user( $user_login ) ) );
+
+	// No signup or more than one, something is wrong. Let's bail.
+	if ( empty( $signup['signups'][0] ) || $signup['total'] > 1 ) {
+		return $user;
+	}
+
+	// Unactivated user account found!
+	// Set up the feedback message.
+	$signup_id = $signup['signups'][0]->signup_id;
+
+	$resend_url_params = array(
+		'action' => 'bp-resend-activation',
+		'id'     => $signup_id,
+	);
+
+	$resend_url = wp_nonce_url(
+		add_query_arg( $resend_url_params, wp_login_url() ),
+		'bp-resend-activation'
+	);
+
+	$resend_string = '<br /><br />' . sprintf( __( 'If you have not received an email yet, <a href="%s">click here to resend it</a>.', 'buddypress' ), esc_url( $resend_url ) );
+
+	return new WP_Error( 'bp_account_not_activated', __( '<strong>ERROR</strong>: Your account has not been activated. Check your email for the activation link.', 'buddypress' ) . $resend_string );
+}
+add_filter( 'authenticate', 'bp_core_signup_disable_inactive', 30, 3 );
+
+/**
+ * On the login screen, resends the activation email for a user.
+ *
+ * @since 2.0.0
+ *
+ * @see bp_core_signup_disable_inactive()
+ */
+function bp_members_login_resend_activation_email() {
+	global $error;
+
+	if ( empty( $_GET['id'] ) || empty( $_GET['_wpnonce'] ) ) {
+		return;
+	}
+
+	// Verify nonce.
+	if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'bp-resend-activation' ) ) {
+		die( 'Security check' );
+	}
+
+	$signup_id = (int) $_GET['id'];
+
+	// Resend the activation email.
+	// also updates the 'last sent' and '# of emails sent' values.
+	$resend = BP_Signup::resend( array( $signup_id ) );
+
+	// Add feedback message.
+	if ( ! empty( $resend['errors'] ) ) {
+		$error = __( '<strong>ERROR</strong>: Your account has already been activated.', 'buddypress' );
+	} else {
+		$error = __( 'Activation email resent! Please check your inbox or spam folder.', 'buddypress' );
+	}
+}
+add_action( 'login_form_bp-resend-activation', 'bp_members_login_resend_activation_email' );
+
+/**
+ * Redirect away from wp-signup.php if BP registration templates are present.
+ *
+ * @since 1.1.0
+ */
+function bp_core_wpsignup_redirect() {
+
+	// Bail in admin or if custom signup page is broken.
+	if ( is_admin() || ! bp_has_custom_signup_page() ) {
+		return;
+	}
+
+	$action = !empty( $_GET['action'] ) ? $_GET['action'] : '';
+
+	// Not at the WP core signup page and action is not register.
+	if ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false === strpos( 'wp-signup.php', $_SERVER['SCRIPT_NAME'] ) && ( 'register' != $action ) ) {
+		return;
+	}
+
+	bp_core_redirect( bp_get_signup_page() );
+}
+add_action( 'bp_init', 'bp_core_wpsignup_redirect' );
+
+/**
+ * Stop a logged-in user who is marked as a spammer.
+ *
+ * When an admin marks a live user as a spammer, that user can still surf
+ * around and cause havoc on the site until that person is logged out.
+ *
+ * This code checks to see if a logged-in user is marked as a spammer.  If so,
+ * we redirect the user back to wp-login.php with the 'reauth' parameter.
+ *
+ * This clears the logged-in spammer's cookies and will ask the spammer to
+ * reauthenticate.
+ *
+ * Note: A spammer cannot log back in - {@see bp_core_boot_spammer()}.
+ *
+ * Runs on 'bp_init' at priority 5 so the members component globals are setup
+ * before we do our spammer checks.
+ *
+ * This is important as the $bp->loggedin_user object is setup at priority 4.
+ *
+ * @since 1.8.0
+ */
+function bp_stop_live_spammer() {
+	// If we're on the login page, stop now to prevent redirect loop.
+	$is_login = false;
+	if ( isset( $GLOBALS['pagenow'] ) && ( false !== strpos( $GLOBALS['pagenow'], 'wp-login.php' ) ) ) {
+		$is_login = true;
+	} elseif ( isset( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], 'wp-login.php' ) ) {
+		$is_login = true;
+	}
+
+	if ( $is_login ) {
+		return;
+	}
+
+	// User isn't logged in, so stop!
+	if ( ! is_user_logged_in() ) {
+		return;
+	}
+
+	// If spammer, redirect to wp-login.php and reauthorize.
+	if ( bp_is_user_spammer( bp_loggedin_user_id() ) ) {
+		// Setup login args.
+		$args = array(
+			// Custom action used to throw an error message.
+			'action' => 'bp-spam',
+
+			// Reauthorize user to login.
+			'reauth' => 1
+		);
+
+		/**
+		 * Filters the url used for redirection for a logged in user marked as spam.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param string $value URL to redirect user to.
+		 */
+		$login_url = apply_filters( 'bp_live_spammer_redirect', add_query_arg( $args, wp_login_url() ) );
+
+		// Redirect user to login page.
+		wp_redirect( $login_url );
+		die();
+	}
+}
+add_action( 'bp_init', 'bp_stop_live_spammer', 5 );
+
+/**
+ * Show a custom error message when a logged-in user is marked as a spammer.
+ *
+ * @since 1.8.0
+ */
+function bp_live_spammer_login_error() {
+	global $error;
+
+	$error = __( '<strong>ERROR</strong>: Your account has been marked as a spammer.', 'buddypress' );
+
+	// Shake shake shake!
+	add_action( 'login_head', 'wp_shake_js', 12 );
+}
+add_action( 'login_form_bp-spam', 'bp_live_spammer_login_error' );
+
+/**
+ * Get the displayed user Object
+ *
+ * @since 2.6.0
+ *
+ * @return object The displayed user object, null otherwise.
+ */
+function bp_get_displayed_user() {
+	$bp = buddypress();
+
+	$displayed_user = null;
+	if ( ! empty( $bp->displayed_user->id ) ) {
+		$displayed_user = $bp->displayed_user;
+	}
+
+	/**
+	 * Filters the displayed_user object corresponding to the displayed member.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param object $displayed_user The displayed_user object.
+	 */
+	return apply_filters( 'bp_get_displayed_user', $displayed_user );
+}
+
+/** Member Types *************************************************************/
+
+/**
+ * Output the slug of the member type taxonomy.
+ *
+ * @since 2.7.0
+ */
+function bp_member_type_tax_name() {
+	echo bp_get_member_type_tax_name();
+}
+
+	/**
+	 * Return the slug of the member type taxonomy.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return string The unique member taxonomy slug.
+	 */
+	function bp_get_member_type_tax_name() {
+		/**
+		 * Filters the slug of the member type taxonomy.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string $value Member type taxonomy slug.
+		 */
+		return apply_filters( 'bp_get_member_type_tax_name', 'bp_member_type' );
+	}
+
+/**
+ * Register a member type.
+ *
+ * @since 2.2.0
+ *
+ * @param string $member_type Unique string identifier for the member type.
+ * @param array  $args {
+ *     Array of arguments describing the member type.
+ *
+ *     @type array       $labels {
+ *         Array of labels to use in various parts of the interface.
+ *
+ *         @type string $name          Default name. Should typically be plural.
+ *         @type string $singular_name Singular name.
+ *     }
+ *     @type bool|string $has_directory Whether the member type should have its own type-specific directory.
+ *                                      Pass `true` to use the `$member_type` string as the type's slug.
+ *                                      Pass a string to customize the slug. Pass `false` to disable.
+ *                                      Default: true.
+ * }
+ * @return object|WP_Error Member type object on success, WP_Error object on failure.
+ */
+function bp_register_member_type( $member_type, $args = array() ) {
+	$bp = buddypress();
+
+	if ( isset( $bp->members->types[ $member_type ] ) ) {
+		return new WP_Error( 'bp_member_type_exists', __( 'Member type already exists.', 'buddypress' ), $member_type );
+	}
+
+	$r = bp_parse_args( $args, array(
+		'labels'        => array(),
+		'has_directory' => true,
+	), 'register_member_type' );
+
+	$member_type = sanitize_key( $member_type );
+
+	/**
+	 * Filters the list of illegal member type names.
+	 *
+	 * - 'any' is a special pseudo-type, representing items unassociated with any member type.
+	 * - 'null' is a special pseudo-type, representing users without any type.
+	 * - '_none' is used internally to denote an item that should not apply to any member types.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $illegal_names Array of illegal names.
+	 */
+	$illegal_names = apply_filters( 'bp_member_type_illegal_names', array( 'any', 'null', '_none' ) );
+	if ( in_array( $member_type, $illegal_names, true ) ) {
+		return new WP_Error( 'bp_member_type_illegal_name', __( 'You may not register a member type with this name.', 'buddypress' ), $member_type );
+	}
+
+	// Store the post type name as data in the object (not just as the array key).
+	$r['name'] = $member_type;
+
+	// Make sure the relevant labels have been filled in.
+	$default_name = isset( $r['labels']['name'] ) ? $r['labels']['name'] : ucfirst( $r['name'] );
+	$r['labels'] = array_merge( array(
+		'name'          => $default_name,
+		'singular_name' => $default_name,
+	), $r['labels'] );
+
+	// Directory slug.
+	if ( $r['has_directory'] ) {
+		// A string value is intepreted as the directory slug. Otherwise fall back on member type.
+		if ( is_string( $r['has_directory'] ) ) {
+			$directory_slug = $r['has_directory'];
+		} else {
+			$directory_slug = $member_type;
+		}
+
+		// Sanitize for use in URLs.
+		$r['directory_slug'] = sanitize_title( $directory_slug );
+		$r['has_directory']  = true;
+	} else {
+		$r['directory_slug'] = '';
+		$r['has_directory']  = false;
+	}
+
+	$bp->members->types[ $member_type ] = $type = (object) $r;
+
+	/**
+	 * Fires after a member type is registered.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $member_type Member type identifier.
+	 * @param object $type        Member type object.
+	 */
+	do_action( 'bp_registered_member_type', $member_type, $type );
+
+	return $type;
+}
+
+/**
+ * Retrieve a member type object by name.
+ *
+ * @since 2.2.0
+ *
+ * @param string $member_type The name of the member type.
+ * @return object A member type object.
+ */
+function bp_get_member_type_object( $member_type ) {
+	$types = bp_get_member_types( array(), 'objects' );
+
+	if ( empty( $types[ $member_type ] ) ) {
+		return null;
+	}
+
+	return $types[ $member_type ];
+}
+
+/**
+ * Get a list of all registered member type objects.
+ *
+ * @since 2.2.0
+ *
+ * @see bp_register_member_type() for accepted arguments.
+ *
+ * @param array|string $args     Optional. An array of key => value arguments to match against
+ *                               the member type objects. Default empty array.
+ * @param string       $output   Optional. The type of output to return. Accepts 'names'
+ *                               or 'objects'. Default 'names'.
+ * @param string       $operator Optional. The logical operation to perform. 'or' means only one
+ *                               element from the array needs to match; 'and' means all elements
+ *                               must match. Accepts 'or' or 'and'. Default 'and'.
+ * @return array A list of member type names or objects.
+ */
+function bp_get_member_types( $args = array(), $output = 'names', $operator = 'and' ) {
+	$types = buddypress()->members->types;
+
+	$types = wp_filter_object_list( $types, $args, $operator );
+
+	/**
+	 * Filters the array of member type objects.
+	 *
+	 * This filter is run before the $output filter has been applied, so that
+	 * filtering functions have access to the entire member type objects.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array  $types     Member type objects, keyed by name.
+	 * @param array  $args      Array of key=>value arguments for filtering.
+	 * @param string $operator  'or' to match any of $args, 'and' to require all.
+	 */
+	$types = apply_filters( 'bp_get_member_types', $types, $args, $operator );
+
+	if ( 'names' === $output ) {
+		$types = wp_list_pluck( $types, 'name' );
+	}
+
+	return $types;
+}
+
+/**
+ * Set type for a member.
+ *
+ * @since 2.2.0
+ *
+ * @param int    $user_id     ID of the user.
+ * @param string $member_type Member type.
+ * @param bool   $append      Optional. True to append this to existing types for user,
+ *                            false to replace. Default: false.
+ * @return false|array $retval See {@see bp_set_object_terms()}.
+ */
+function bp_set_member_type( $user_id, $member_type, $append = false ) {
+	// Pass an empty $member_type to remove a user's type.
+	if ( ! empty( $member_type ) && ! bp_get_member_type_object( $member_type ) ) {
+		return false;
+	}
+
+	$retval = bp_set_object_terms( $user_id, $member_type, bp_get_member_type_tax_name(), $append );
+
+	// Bust the cache if the type has been updated.
+	if ( ! is_wp_error( $retval ) ) {
+		wp_cache_delete( $user_id, 'bp_member_member_type' );
+
+		/**
+		 * Fires just after a user's member type has been changed.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param int    $user_id     ID of the user whose member type has been updated.
+		 * @param string $member_type Member type.
+		 * @param bool   $append      Whether the type is being appended to existing types.
+		 */
+		do_action( 'bp_set_member_type', $user_id, $member_type, $append );
+	}
+
+	return $retval;
+}
+
+/**
+ * Remove type for a member.
+ *
+ * @since 2.3.0
+ *
+ * @param int    $user_id     ID of the user.
+ * @param string $member_type Member Type.
+ * @return bool|WP_Error
+ */
+function bp_remove_member_type( $user_id, $member_type ) {
+	// Bail if no valid member type was passed.
+	if ( empty( $member_type ) || ! bp_get_member_type_object( $member_type ) ) {
+		return false;
+	}
+
+	$deleted = bp_remove_object_terms( $user_id, $member_type, bp_get_member_type_tax_name() );
+
+	// Bust the cache if the type has been removed.
+	if ( ! is_wp_error( $deleted ) ) {
+		wp_cache_delete( $user_id, 'bp_member_member_type' );
+
+		/**
+		 * Fires just after a user's member type has been removed.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param int    $user_id     ID of the user whose member type has been updated.
+		 * @param string $member_type Member type.
+		 */
+		do_action( 'bp_remove_member_type', $user_id, $member_type );
+	}
+
+	return $deleted;
+}
+
+/**
+ * Get type for a member.
+ *
+ * @since 2.2.0
+ *
+ * @param int  $user_id ID of the user.
+ * @param bool $single  Optional. Whether to return a single type string. If multiple types are found
+ *                      for the user, the oldest one will be returned. Default: true.
+ * @return string|array|bool On success, returns a single member type (if $single is true) or an array of member
+ *                           types (if $single is false). Returns false on failure.
+ */
+function bp_get_member_type( $user_id, $single = true ) {
+	$types = wp_cache_get( $user_id, 'bp_member_member_type' );
+
+	if ( false === $types ) {
+		$raw_types = bp_get_object_terms( $user_id, bp_get_member_type_tax_name() );
+
+		if ( ! is_wp_error( $raw_types ) ) {
+			$types =  array();
+
+			// Only include currently registered group types.
+			foreach ( $raw_types as $mtype ) {
+				if ( bp_get_member_type_object( $mtype->name ) ) {
+					$types[] = $mtype->name;
+				}
+			}
+
+			wp_cache_set( $user_id, $types, 'bp_member_member_type' );
+		}
+	}
+
+	$type = false;
+	if ( ! empty( $types ) ) {
+		if ( $single ) {
+			$type = array_pop( $types );
+		} else {
+			$type = $types;
+		}
+	}
+
+	/**
+	 * Filters a user's member type(s).
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $type    Member type.
+	 * @param int    $user_id ID of the user.
+	 * @param bool   $single  Whether to return a single type string, or an array.
+	 */
+	return apply_filters( 'bp_get_member_type', $type, $user_id, $single );
+}
+
+/**
+ * Check whether the given user has a certain member type.
+ *
+ * @since 2.3.0
+ *
+ * @param int    $user_id     $user_id ID of the user.
+ * @param string $member_type Member Type.
+ * @return bool Whether the user has the given member type.
+ */
+function bp_has_member_type( $user_id, $member_type ) {
+	// Bail if no valid member type was passed.
+	if ( empty( $member_type ) || ! bp_get_member_type_object( $member_type ) ) {
+		return false;
+	}
+
+	// Get all user's member types.
+	$types = bp_get_member_type( $user_id, false );
+
+	if ( ! is_array( $types ) ) {
+		return false;
+	}
+
+	return in_array( $member_type, $types );
+}
+
+/**
+ * Delete a user's member type when the user when the user is deleted.
+ *
+ * @since 2.2.0
+ *
+ * @param int $user_id ID of the user.
+ * @return false|array $value See {@see bp_set_member_type()}.
+ */
+function bp_remove_member_type_on_user_delete( $user_id ) {
+	return bp_set_member_type( $user_id, '' );
+}
+add_action( 'wpmu_delete_user', 'bp_remove_member_type_on_user_delete' );
+add_action( 'delete_user', 'bp_remove_member_type_on_user_delete' );
+
+/**
+ * Get the "current" member type, if one is provided, in member directories.
+ *
+ * @since 2.3.0
+ *
+ * @return string
+ */
+function bp_get_current_member_type() {
+
+	/**
+	 * Filters the "current" member type, if one is provided, in member directories.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $value "Current" member type.
+	 */
+	return apply_filters( 'bp_get_current_member_type', buddypress()->current_member_type );
+}
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-loader.php	(revision 12265)
@@ -0,0 +1,21 @@
+<?php
+/**
+ * BuddyPress Member Loader.
+ *
+ * @package BuddyPress
+ * @subpackage Members
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-members component.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_members() {
+	buddypress()->members = new BP_Members_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_members', 1 );
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-template.php	(revision 12265)
@@ -0,0 +1,2642 @@
+<?php
+/**
+ * BuddyPress Member Template Tags.
+ *
+ * Functions that are safe to use inside your template files and themes.
+ *
+ * @package BuddyPress
+ * @subpackage Members
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the profile component slug.
+ *
+ * @since 2.4.0
+ *
+ */
+function bp_profile_slug() {
+	echo bp_get_profile_slug();
+}
+	/**
+	 * Return the profile component slug.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @return string
+	 */
+	function bp_get_profile_slug() {
+
+		/**
+		 * Filters the profile component slug.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param string $slug Profile component slug.
+		 */
+		return apply_filters( 'bp_get_profile_slug', buddypress()->profile->slug );
+	}
+
+/**
+ * Output the members component slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_members_slug() {
+	echo bp_get_members_slug();
+}
+	/**
+	 * Return the members component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_members_slug() {
+
+		/**
+		 * Filters the Members component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Members component slug.
+		 */
+		return apply_filters( 'bp_get_members_slug', buddypress()->members->slug );
+	}
+
+/**
+ * Output the members component root slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_members_root_slug() {
+	echo bp_get_members_root_slug();
+}
+	/**
+	 * Return the members component root slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_members_root_slug() {
+
+		/**
+		 * Filters the Members component root slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Members component root slug.
+		 */
+		return apply_filters( 'bp_get_members_root_slug', buddypress()->members->root_slug );
+	}
+
+/**
+ * Output the member type base slug.
+ *
+ * @since 2.5.0
+ */
+function bp_members_member_type_base() {
+	echo esc_url( bp_get_members_member_type_base() );
+}
+	/**
+	 * Get the member type base slug.
+	 *
+	 * The base slug is the string used as the base prefix when generating member type directory URLs.
+	 * For example, in example.com/members/type/foo/, 'foo' is the member type and 'type' is the
+	 * base slug.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_members_member_type_base() {
+		/**
+		 * Filters the member type URL base.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string $base
+		 */
+		return apply_filters( 'bp_members_member_type_base', _x( 'type', 'member type URL base', 'buddypress' ) );
+	}
+
+/**
+ * Output member directory permalink.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_members_directory_permalink() {
+	echo esc_url( bp_get_members_directory_permalink() );
+}
+	/**
+	 * Return member directory permalink.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_members_directory_permalink() {
+
+		/**
+		 * Filters the member directory permalink.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Members directory permalink.
+		 */
+		return apply_filters( 'bp_get_members_directory_permalink', trailingslashit( bp_get_root_domain() . '/' . bp_get_members_root_slug() ) );
+	}
+
+/**
+ * Output member type directory permalink.
+ *
+ * @since 2.5.0
+ *
+ * @param string $member_type Optional. Member type.
+ */
+function bp_member_type_directory_permalink( $member_type = '' ) {
+	echo esc_url( bp_get_member_type_directory_permalink( $member_type ) );
+}
+	/**
+	 * Return member type directory permalink.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $member_type Optional. Member type. Defaults to current member type.
+	 * @return string Member type directory URL on success, an empty string on failure.
+	 */
+	function bp_get_member_type_directory_permalink( $member_type = '' ) {
+
+		if ( $member_type ) {
+			$_member_type = $member_type;
+		} else {
+			// Fall back on the current member type.
+			$_member_type = bp_get_current_member_type();
+		}
+
+		$type = bp_get_member_type_object( $_member_type );
+
+		// Bail when member type is not found or has no directory.
+		if ( ! $type || ! $type->has_directory ) {
+			return '';
+		}
+
+		/**
+		 * Filters the member type directory permalink.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $value       Member type directory permalink.
+		 * @param object $type        Member type object.
+		 * @param string $member_type Member type name, as passed to the function.
+		 */
+		return apply_filters( 'bp_get_member_type_directory_permalink', trailingslashit( bp_get_members_directory_permalink() . bp_get_members_member_type_base() . '/' . $type->directory_slug ), $type, $member_type );
+	}
+
+/**
+ * Output the sign-up slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_signup_slug() {
+	echo bp_get_signup_slug();
+}
+	/**
+	 * Return the sign-up slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_slug() {
+		$bp = buddypress();
+
+		if ( !empty( $bp->pages->register->slug ) ) {
+			$slug = $bp->pages->register->slug;
+		} elseif ( defined( 'BP_REGISTER_SLUG' ) ) {
+			$slug = BP_REGISTER_SLUG;
+		} else {
+			$slug = 'register';
+		}
+
+		/**
+		 * Filters the sign-up slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Sign-up slug.
+		 */
+		return apply_filters( 'bp_get_signup_slug', $slug );
+	}
+
+/**
+ * Output the activation slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_activate_slug() {
+	echo bp_get_activate_slug();
+}
+	/**
+	 * Return the activation slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_activate_slug() {
+		$bp = buddypress();
+
+		if ( !empty( $bp->pages->activate->slug ) ) {
+			$slug = $bp->pages->activate->slug;
+		} elseif ( defined( 'BP_ACTIVATION_SLUG' ) ) {
+			$slug = BP_ACTIVATION_SLUG;
+		} else {
+			$slug = 'activate';
+		}
+
+		/**
+		 * Filters the activation slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Activation slug.
+		 */
+		return apply_filters( 'bp_get_activate_slug', $slug );
+	}
+
+/**
+ * Initialize the members loop.
+ *
+ * Based on the $args passed, bp_has_members() populates the $members_template
+ * global, enabling the use of BuddyPress templates and template functions to
+ * display a list of members.
+ *
+ * @since 1.2.0
+ *
+ * @global object $members_template {@link BP_Members_Template}
+ *
+ * @param array|string $args {
+ *     Arguments for limiting the contents of the members loop. Most arguments
+ *     are in the same format as {@link BP_User_Query}. However, because
+ *     the format of the arguments accepted here differs in a number of ways,
+ *     and because bp_has_members() determines some default arguments in a
+ *     dynamic fashion, we list all accepted arguments here as well.
+ *
+ *     Arguments can be passed as an associative array, or as a URL query
+ *     string (eg, 'user_id=4&per_page=3').
+ *
+ *     @type int                   $type                Sort order. Accepts 'active', 'random', 'newest', 'popular',
+ *                                                      'online', 'alphabetical'. Default: 'active'.
+ *     @type int|bool              $page                Page of results to display. Default: 1.
+ *     @type int|bool              $per_page            Number of results per page. Default: 20.
+ *     @type int|bool              $max                 Maximum number of results to return. Default: false (unlimited).
+ *     @type string                $page_arg            The string used as a query parameter in pagination links.
+ *                                                      Default: 'bpage'.
+ *     @type array|int|string|bool $include             Limit results by a list of user IDs. Accepts an array, a
+ *                                                      single integer, a comma-separated list of IDs, or false (to
+ *                                                      disable this limiting). Accepts 'active', 'alphabetical',
+ *                                                      'newest', or 'random'. Default: false.
+ *     @type array|int|string|bool $exclude             Exclude users from results by ID. Accepts an array, a single
+ *                                                      integer, a comma-separated list of IDs, or false (to disable
+ *                                                      this limiting). Default: false.
+ *     @type int                   $user_id             If provided, results are limited to the friends of the specified
+ *                                                      user. When on a user's Friends page, defaults to the ID of the
+ *                                                      displayed user. Otherwise defaults to 0.
+ *     @type string|array          $member_type         Array or comma-separated list of member types to limit
+ *                                                      results to.
+ *     @type string|array          $member_type__in     Array or comma-separated list of member types to limit
+ *                                                      results to.
+ *     @type string|array          $member_type__not_in Array or comma-separated list of member types to exclude
+ *                                                      from results.
+ *     @type string                $search_terms        Limit results by a search term. Default: value of
+ *                                                      `$_REQUEST['members_search']` or `$_REQUEST['s']`, if present.
+ *                                                      Otherwise false.
+ *     @type string                $meta_key            Limit results by the presence of a usermeta key.
+ *                                                      Default: false.
+ *     @type mixed                 $meta_value          When used with meta_key, limits results by the a matching
+ *                                                      usermeta value. Default: false.
+ *     @type bool                  $populate_extras     Whether to fetch optional data, such as friend counts.
+ *                                                      Default: true.
+ * }
+ * @return bool Returns true when blogs are found, otherwise false.
+ */
+function bp_has_members( $args = '' ) {
+	global $members_template;
+
+	// Default user ID.
+	$user_id = 0;
+
+	// User filtering.
+	if ( bp_is_user_friends() && ! bp_is_user_friend_requests() ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	$member_type = bp_get_current_member_type();
+	if ( ! $member_type && ! empty( $_GET['member_type'] ) ) {
+		if ( is_array( $_GET['member_type'] ) ) {
+			$member_type = $_GET['member_type'];
+		} else {
+			// Can be a comma-separated list.
+			$member_type = explode( ',', $_GET['member_type'] );
+		}
+	}
+
+	$search_terms_default = null;
+	$search_query_arg = bp_core_get_component_search_query_arg( 'members' );
+	if ( ! empty( $_REQUEST[ $search_query_arg ] ) ) {
+		$search_terms_default = stripslashes( $_REQUEST[ $search_query_arg ] );
+	}
+
+	// Type: active ( default ) | random | newest | popular | online | alphabetical.
+	$r = bp_parse_args( $args, array(
+		'type'                => 'active',
+		'page'                => 1,
+		'per_page'            => 20,
+		'max'                 => false,
+
+		'page_arg'            => 'upage',  // See https://buddypress.trac.wordpress.org/ticket/3679.
+
+		'include'             => false,    // Pass a user_id or a list (comma-separated or array) of user_ids to only show these users.
+		'exclude'             => false,    // Pass a user_id or a list (comma-separated or array) of user_ids to exclude these users.
+
+		'user_id'             => $user_id, // Pass a user_id to only show friends of this user.
+		'member_type'         => $member_type,
+		'member_type__in'     => '',
+		'member_type__not_in' => '',
+		'search_terms'        => $search_terms_default,
+
+		'meta_key'            => false,    // Only return users with this usermeta.
+		'meta_value'          => false,    // Only return users where the usermeta value matches. Requires meta_key.
+
+		'populate_extras'     => true      // Fetch usermeta? Friend count, last active etc.
+	), 'has_members' );
+
+	// Pass a filter if ?s= is set.
+	if ( is_null( $r['search_terms'] ) ) {
+		if ( !empty( $_REQUEST['s'] ) ) {
+			$r['search_terms'] = $_REQUEST['s'];
+		} else {
+			$r['search_terms'] = false;
+		}
+	}
+
+	// Set per_page to max if max is larger than per_page.
+	if ( !empty( $r['max'] ) && ( $r['per_page'] > $r['max'] ) ) {
+		$r['per_page'] = $r['max'];
+	}
+
+	// Query for members and populate $members_template global.
+	$members_template = new BP_Core_Members_Template(
+		$r['type'],
+		$r['page'],
+		$r['per_page'],
+		$r['max'],
+		$r['user_id'],
+		$r['search_terms'],
+		$r['include'],
+		$r['populate_extras'],
+		$r['exclude'],
+		$r['meta_key'],
+		$r['meta_value'],
+		$r['page_arg'],
+		$r['member_type'],
+		$r['member_type__in'],
+		$r['member_type__not_in']
+	);
+
+	/**
+	 * Filters whether or not BuddyPress has members to iterate over.
+	 *
+	 * @since 1.2.4
+	 * @since 2.6.0 Added the `$r` parameter
+	 *
+	 * @param bool  $value            Whether or not there are members to iterate over.
+	 * @param array $members_template Populated $members_template global.
+	 * @param array $r                Array of arguments passed into the BP_Core_Members_Template class.
+	 */
+	return apply_filters( 'bp_has_members', $members_template->has_members(), $members_template, $r );
+}
+
+/**
+ * Set up the current member inside the loop.
+ *
+ * @since 1.2.0
+ *
+ * @return object
+ */
+function bp_the_member() {
+	global $members_template;
+	return $members_template->the_member();
+}
+
+/**
+ * Check whether there are more members to iterate over.
+ *
+ * @since 1.2.0
+ *
+ * @return bool
+ */
+function bp_members() {
+	global $members_template;
+	return $members_template->members();
+}
+
+/**
+ * Output the members pagination count.
+ *
+ * @since 1.2.0
+ */
+function bp_members_pagination_count() {
+	echo bp_get_members_pagination_count();
+}
+	/**
+	 * Generate the members pagination count.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_members_pagination_count() {
+		global $members_template;
+
+		if ( empty( $members_template->type ) )
+			$members_template->type = '';
+
+		$start_num = intval( ( $members_template->pag_page - 1 ) * $members_template->pag_num ) + 1;
+		$from_num  = bp_core_number_format( $start_num );
+		$to_num    = bp_core_number_format( ( $start_num + ( $members_template->pag_num - 1 ) > $members_template->total_member_count ) ? $members_template->total_member_count : $start_num + ( $members_template->pag_num - 1 ) );
+		$total     = bp_core_number_format( $members_template->total_member_count );
+
+		if ( 'active' == $members_template->type ) {
+			if ( 1 == $members_template->total_member_count ) {
+				$pag = __( 'Viewing 1 active member', 'buddypress' );
+			} else {
+				$pag = sprintf( _n( 'Viewing %1$s - %2$s of %3$s active member', 'Viewing %1$s - %2$s of %3$s active members', $members_template->total_member_count, 'buddypress' ), $from_num, $to_num, $total );
+			}
+		} elseif ( 'popular' == $members_template->type ) {
+			if ( 1 == $members_template->total_member_count ) {
+				$pag = __( 'Viewing 1 member with friends', 'buddypress' );
+			} else {
+				$pag = sprintf( _n( 'Viewing %1$s - %2$s of %3$s member with friends', 'Viewing %1$s - %2$s of %3$s members with friends', $members_template->total_member_count, 'buddypress' ), $from_num, $to_num, $total );
+			}
+		} elseif ( 'online' == $members_template->type ) {
+			if ( 1 == $members_template->total_member_count ) {
+				$pag = __( 'Viewing 1 online member', 'buddypress' );
+			} else {
+				$pag = sprintf( _n( 'Viewing %1$s - %2$s of %3$s online member', 'Viewing %1$s - %2$s of %3$s online members', $members_template->total_member_count, 'buddypress' ), $from_num, $to_num, $total );
+			}
+		} else {
+			if ( 1 == $members_template->total_member_count ) {
+				$pag = __( 'Viewing 1 member', 'buddypress' );
+			} else {
+				$pag = sprintf( _n( 'Viewing %1$s - %2$s of %3$s member', 'Viewing %1$s - %2$s of %3$s members', $members_template->total_member_count, 'buddypress' ), $from_num, $to_num, $total );
+			}
+		}
+
+		/**
+		 * Filters the members pagination count.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $pag Pagination count string.
+		 */
+		return apply_filters( 'bp_members_pagination_count', $pag );
+	}
+
+/**
+ * Output the members pagination links.
+ *
+ * @since 1.2.0
+ */
+function bp_members_pagination_links() {
+	echo bp_get_members_pagination_links();
+}
+	/**
+	 * Fetch the members pagination links.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_members_pagination_links() {
+		global $members_template;
+
+		/**
+		 * Filters the members pagination link.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $pag_links HTML markup for pagination links.
+		 */
+		return apply_filters( 'bp_get_members_pagination_links', $members_template->pag_links );
+	}
+
+/**
+ * Output the ID of the current member in the loop.
+ *
+ * @since 1.2.0
+ *
+ */
+function bp_member_user_id() {
+	echo bp_get_member_user_id();
+}
+	/**
+	 * Get the ID of the current member in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string Member ID.
+	 */
+	function bp_get_member_user_id() {
+		global $members_template;
+		$member_id = isset( $members_template->member->id ) ? (int) $members_template->member->id : false;
+
+		/**
+		 * Filters the ID of the current member in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $member_id ID of the member being iterated over.
+		 */
+		return apply_filters( 'bp_get_member_user_id', $member_id );
+	}
+
+/**
+ * Output the row class of the current member in the loop.
+ *
+ * @since 1.7.0
+ *
+ * @param array $classes Array of custom classes.
+ */
+function bp_member_class( $classes = array() ) {
+	echo bp_get_member_class( $classes );
+}
+	/**
+	 * Return the row class of the current member in the loop.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $classes Array of custom classes.
+	 * @return string Row class of the member
+	 */
+	function bp_get_member_class( $classes = array() ) {
+		global $members_template;
+
+		// Add even/odd classes, but only if there's more than 1 member.
+		if ( $members_template->member_count > 1 ) {
+			$pos_in_loop = (int) $members_template->current_member;
+			$classes[]   = ( $pos_in_loop % 2 ) ? 'even' : 'odd';
+
+			// If we've only one member in the loop, don't bother with odd and even.
+		} else {
+			$classes[] = 'bp-single-member';
+		}
+
+		// Maybe add 'is-online' class.
+		if ( ! empty( $members_template->member->last_activity ) ) {
+
+			// Calculate some times.
+			$current_time  = bp_core_current_time( true, 'timestamp' );
+			$last_activity = strtotime( $members_template->member->last_activity );
+			$still_online  = strtotime( '+5 minutes', $last_activity );
+
+			// Has the user been active recently?
+			if ( $current_time <= $still_online ) {
+				$classes[] = 'is-online';
+			}
+		}
+
+		// Add current user class.
+		if ( bp_loggedin_user_id() === (int) $members_template->member->id ) {
+			$classes[] = 'is-current-user';
+		}
+
+		// Add current user member types.
+		if ( $member_types = bp_get_member_type( $members_template->member->id, false ) ) {
+			foreach ( $member_types as $member_type ) {
+				$classes[] = sprintf( 'member-type-%s', esc_attr( $member_type ) );
+			}
+		}
+
+		/**
+		 * Filters the determined classes to add to the HTML element.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param string $classes Classes to be added to the HTML element.
+		 */
+		$classes = apply_filters( 'bp_get_member_class', $classes );
+		$classes = array_merge( $classes, array() );
+		$retval  = 'class="' . join( ' ', $classes ) . '"';
+
+		return $retval;
+	}
+
+/**
+ * Output nicename of current member in the loop.
+ *
+ * @since 1.2.5
+ */
+function bp_member_user_nicename() {
+	echo bp_get_member_user_nicename();
+}
+	/**
+	 * Get the nicename of the current member in the loop.
+	 *
+	 * @since 1.2.5
+	 *
+	 * @return string Members nicename.
+	 */
+	function bp_get_member_user_nicename() {
+		global $members_template;
+
+		/**
+		 * Filters the nicename of the current member in the loop.
+		 *
+		 * @since 1.2.5
+		 *
+		 * @param string $user_nicename Nicename for the current member.
+		 */
+		return apply_filters( 'bp_get_member_user_nicename', $members_template->member->user_nicename );
+	}
+
+/**
+ * Output login for current member in the loop.
+ *
+ * @since 1.2.5
+ */
+function bp_member_user_login() {
+	echo bp_get_member_user_login();
+}
+	/**
+	 * Get the login of the current member in the loop.
+	 *
+	 * @since 1.2.5
+	 *
+	 * @return string Member's login.
+	 */
+	function bp_get_member_user_login() {
+		global $members_template;
+
+		/**
+		 * Filters the login of the current member in the loop.
+		 *
+		 * @since 1.2.5
+		 *
+		 * @param string $user_login Login for the current member.
+		 */
+		return apply_filters( 'bp_get_member_user_login', $members_template->member->user_login );
+	}
+
+/**
+ * Output the email address for the current member in the loop.
+ *
+ * @since 1.2.5
+ */
+function bp_member_user_email() {
+	echo bp_get_member_user_email();
+}
+	/**
+	 * Get the email address of the current member in the loop.
+	 *
+	 * @since 1.2.5
+	 *
+	 * @return string Member's email address.
+	 */
+	function bp_get_member_user_email() {
+		global $members_template;
+
+		/**
+		 * Filters the email address of the current member in the loop.
+		 *
+		 * @since 1.2.5
+		 *
+		 * @param string $user_email Email address for the current member.
+		 */
+		return apply_filters( 'bp_get_member_user_email', $members_template->member->user_email );
+	}
+
+/**
+ * Check whether the current member in the loop is the logged-in user.
+ *
+ * @since 1.2.5
+ *
+ * @return bool
+ */
+function bp_member_is_loggedin_user() {
+	global $members_template;
+
+	/**
+	 * Filters whether the current member in the loop is the logged-in user.
+	 *
+	 * @since 1.2.5
+	 *
+	 * @param bool $value Whether current member in the loop is logged in.
+	 */
+	return apply_filters( 'bp_member_is_loggedin_user', bp_loggedin_user_id() == $members_template->member->id ? true : false );
+}
+
+/**
+ * Output a member's avatar.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_member_avatar() for description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_member_avatar()}.
+ */
+function bp_member_avatar( $args = '' ) {
+
+	/**
+	 * Filters a members avatar.
+	 *
+	 * @since 1.2.0
+	 * @since 2.6.0 Added the `$args` parameter.
+	 *
+	 * @param string       $value Formatted HTML <img> element, or raw avatar URL based on $html arg.
+	 * @param array|string $args  See {@link bp_get_member_avatar()}.
+	 */
+	echo apply_filters( 'bp_member_avatar', bp_get_member_avatar( $args ), $args );
+}
+	/**
+	 * Get a member's avatar.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and
+	 *      return values.
+	 *
+	 * @param array|string $args  {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see
+	 *     {@link bp_core_fetch_avatar()}.
+	 *     @type string   $alt     Default: 'Profile picture of [user name]'.
+	 *     @type string   $class   Default: 'avatar'.
+	 *     @type string   $type    Default: 'thumb'.
+	 *     @type int|bool $width   Default: false.
+	 *     @type int|bool $height  Default: false.
+	 *     @type bool     $id      Currently unused.
+	 *     @type bool     $no_grav Default: false.
+	 * }
+	 * @return string User avatar string.
+	 */
+	function bp_get_member_avatar( $args = '' ) {
+		global $members_template;
+
+		$fullname = !empty( $members_template->member->fullname ) ? $members_template->member->fullname : $members_template->member->display_name;
+
+		$defaults = array(
+			'type'   => 'thumb',
+			'width'  => false,
+			'height' => false,
+			'class'  => 'avatar',
+			'id'     => false,
+			'alt'    => sprintf( __( 'Profile picture of %s', 'buddypress' ), $fullname )
+		);
+
+		$r = wp_parse_args( $args, $defaults );
+		extract( $r, EXTR_SKIP );
+
+		/**
+		 * Filters a members avatar.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $value Formatted HTML <img> element, or raw avatar URL based on $html arg.
+		 * @param array  $r     Array of parsed arguments. See {@link bp_get_member_avatar()}.
+		 */
+		return apply_filters( 'bp_get_member_avatar', bp_core_fetch_avatar( array( 'item_id' => $members_template->member->id, 'type' => $type, 'alt' => $alt, 'css_id' => $id, 'class' => $class, 'width' => $width, 'height' => $height, 'email' => $members_template->member->user_email ) ), $r );
+	}
+
+/**
+ * Output the permalink for the current member in the loop.
+ *
+ * @since 1.2.0
+ */
+function bp_member_permalink() {
+	echo esc_url( bp_get_member_permalink() );
+}
+	/**
+	 * Get the permalink for the current member in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_member_permalink() {
+		global $members_template;
+
+		/**
+		 * Filters the permalink for the current member in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Permalink for the current member in the loop.
+		 */
+		return apply_filters( 'bp_get_member_permalink', bp_core_get_user_domain( $members_template->member->id, $members_template->member->user_nicename, $members_template->member->user_login ) );
+	}
+
+	/**
+	 * Alias of {@link bp_member_permalink()}.
+	 *
+	 * @since 1.2.0
+	 */
+	function bp_member_link() { echo esc_url( bp_get_member_permalink() ); }
+
+	/**
+	 * Alias of {@link bp_get_member_permalink()}.
+	 *
+	 * @since 1.2.0
+	 */
+	function bp_get_member_link() { return bp_get_member_permalink(); }
+
+/**
+ * Output display name of current member in the loop.
+ *
+ * @since 1.2.0
+ */
+function bp_member_name() {
+
+	/**
+	 * Filters the display name of current member in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string $value Display name for current member.
+	 */
+	echo apply_filters( 'bp_member_name', bp_get_member_name() );
+}
+	/**
+	 * Get the display name of the current member in the loop.
+	 *
+	 * Full name is, by default, pulled from xprofile's Full Name field.
+	 * When this field is empty, we try to get an alternative name from the
+	 * WP users table, in the following order of preference: display_name,
+	 * user_nicename, user_login.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string The user's fullname for display.
+	 */
+	function bp_get_member_name() {
+		global $members_template;
+
+		// Generally, this only fires when xprofile is disabled.
+		if ( empty( $members_template->member->fullname ) ) {
+			// Our order of preference for alternative fullnames.
+			$name_stack = array(
+				'display_name',
+				'user_nicename',
+				'user_login'
+			);
+
+			foreach ( $name_stack as $source ) {
+				if ( !empty( $members_template->member->{$source} ) ) {
+					// When a value is found, set it as fullname and be done with it.
+					$members_template->member->fullname = $members_template->member->{$source};
+					break;
+				}
+			}
+		}
+
+		/**
+		 * Filters the display name of current member in the loop.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $fullname Display name for current member.
+		 */
+		return apply_filters( 'bp_get_member_name', $members_template->member->fullname );
+	}
+	add_filter( 'bp_get_member_name', 'wp_filter_kses' );
+	add_filter( 'bp_get_member_name', 'stripslashes'   );
+	add_filter( 'bp_get_member_name', 'strip_tags'     );
+	add_filter( 'bp_get_member_name', 'esc_html'       );
+
+/**
+ * Output the current member's last active time.
+ *
+ * @since 1.2.0
+ *
+ * @param array $args {@see bp_get_member_last_active()}.
+ */
+function bp_member_last_active( $args = array() ) {
+	echo bp_get_member_last_active( $args );
+}
+	/**
+	 * Return the current member's last active time.
+	 *
+	 * @since 1.2.0
+	 * @since 2.7.0 Added 'relative' as a parameter to $args.
+	 *
+	 * @param array $args {
+	 *     Array of optional arguments.
+	 *     @type mixed $active_format If true, formatted "active 5 minutes ago". If false, formatted "5 minutes
+	 *                                ago". If string, should be sprintf'able like 'last seen %s ago'.
+	 *     @type bool  $relative      If true, will return relative time "5 minutes ago". If false, will return
+	 *                                date from database. Default: true.
+	 * }
+	 * @return string
+	 */
+	function bp_get_member_last_active( $args = array() ) {
+		global $members_template;
+
+		// Parse the activity format.
+		$r = bp_parse_args( $args, array(
+			'active_format' => true,
+			'relative'      => true,
+		) );
+
+		// Backwards compatibility for anyone forcing a 'true' active_format.
+		if ( true === $r['active_format'] ) {
+			$r['active_format'] = __( 'active %s', 'buddypress' );
+		}
+
+		// Member has logged in at least one time.
+		if ( isset( $members_template->member->last_activity ) ) {
+			// We do not want relative time, so return now.
+			// @todo Should the 'bp_member_last_active' filter be applied here?
+			if ( ! $r['relative'] ) {
+				return esc_attr( $members_template->member->last_activity );
+			}
+
+			// Backwards compatibility for pre 1.5 'ago' strings.
+			$last_activity = ! empty( $r['active_format'] )
+				? bp_core_get_last_activity( $members_template->member->last_activity, $r['active_format'] )
+				: bp_core_time_since( $members_template->member->last_activity );
+
+		// Member has never logged in or been active.
+		} else {
+			$last_activity = __( 'Never active', 'buddypress' );
+		}
+
+		/**
+		 * Filters the current members last active time.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $last_activity Formatted time since last activity.
+		 * @param array  $r             Array of parsed arguments for query.
+		 */
+		return apply_filters( 'bp_member_last_active', $last_activity, $r );
+	}
+
+/**
+ * Output the latest update of the current member in the loop.
+ *
+ * @since 1.2.0
+ *
+ * @param array|string $args {@see bp_get_member_latest_update()}.
+ */
+function bp_member_latest_update( $args = '' ) {
+	echo bp_get_member_latest_update( $args );
+}
+	/**
+	 * Get the latest update from the current member in the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array|string $args {
+	 *     Array of optional arguments.
+	 *     @type int  $length    Truncation length. Default: 225.
+	 *     @type bool $view_link Whether to provide a 'View' link for
+	 *                           truncated entries. Default: false.
+	 * }
+	 * @return string
+	 */
+	function bp_get_member_latest_update( $args = '' ) {
+		global $members_template;
+
+		$defaults = array(
+			'length'    => 225,
+			'view_link' => true
+		);
+
+		$r = wp_parse_args( $args, $defaults );
+		extract( $r );
+
+		if ( !bp_is_active( 'activity' ) || empty( $members_template->member->latest_update ) || !$update = maybe_unserialize( $members_template->member->latest_update ) )
+			return false;
+
+		/**
+		 * Filters the excerpt of the latest update for current member in the loop.
+		 *
+		 * @since 1.2.5
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $value Excerpt of the latest update for current member in the loop.
+		 * @param array  $r     Array of parsed arguments.
+		 */
+		$update_content = apply_filters( 'bp_get_activity_latest_update_excerpt', trim( strip_tags( bp_create_excerpt( $update['content'], $length ) ) ), $r );
+
+		$update_content = sprintf( _x( '- &quot;%s&quot;', 'member latest update in member directory', 'buddypress' ), $update_content );
+
+		// If $view_link is true and the text returned by bp_create_excerpt() is different from the original text (ie it's
+		// been truncated), add the "View" link.
+		if ( $view_link && ( $update_content != $update['content'] ) ) {
+			$view = __( 'View', 'buddypress' );
+
+			$update_content .= '<span class="activity-read-more"><a href="' . bp_activity_get_permalink( $update['id'] ) . '" rel="nofollow">' . $view . '</a></span>';
+		}
+
+		/**
+		 * Filters the latest update from the current member in the loop.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $update_content Formatted latest update for current member.
+		 * @param array  $r              Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_member_latest_update', $update_content, $r );
+	}
+
+/**
+ * Output a piece of user profile data.
+ *
+ * @since 1.2.0
+ *
+ * @see bp_get_member_profile_data() for a description of params.
+ *
+ * @param array|string $args See {@link bp_get_member_profile_data()}.
+ */
+function bp_member_profile_data( $args = '' ) {
+	echo bp_get_member_profile_data( $args );
+}
+	/**
+	 * Get a piece of user profile data.
+	 *
+	 * When used in a bp_has_members() loop, this function will attempt
+	 * to fetch profile data cached in the template global. It is also safe
+	 * to use outside of the loop.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param array|string $args {
+	 *     Array of config parameters.
+	 *     @type string $field   Name of the profile field.
+	 *     @type int    $user_id ID of the user whose data is being fetched.
+	 *                           Defaults to the current member in the loop, or if not
+	 *                           present, to the currently displayed user.
+	 * }
+	 * @return string|bool Profile data if found, otherwise false.
+	 */
+	function bp_get_member_profile_data( $args = '' ) {
+		global $members_template;
+
+		if ( ! bp_is_active( 'xprofile' ) ) {
+			return false;
+		}
+
+		// Declare local variables.
+		$data = false;
+
+		// Guess at default $user_id.
+		$default_user_id = 0;
+		if ( ! empty( $members_template->member->id ) ) {
+			$default_user_id = $members_template->member->id;
+		} elseif ( bp_displayed_user_id() ) {
+			$default_user_id = bp_displayed_user_id();
+		}
+
+		$defaults = array(
+			'field'   => false,
+			'user_id' => $default_user_id,
+		);
+
+		$r = wp_parse_args( $args, $defaults );
+
+		// If we're in a members loop, get the data from the global.
+		if ( ! empty( $members_template->member->profile_data ) ) {
+			$profile_data = $members_template->member->profile_data;
+		}
+
+		// Otherwise query for the data.
+		if ( empty( $profile_data ) && method_exists( 'BP_XProfile_ProfileData', 'get_all_for_user' ) ) {
+			$profile_data = BP_XProfile_ProfileData::get_all_for_user( $r['user_id'] );
+		}
+
+		// If we're in the members loop, but the profile data has not
+		// been loaded into the global, cache it there for later use.
+		if ( ! empty( $members_template->member ) && empty( $members_template->member->profile_data ) ) {
+			$members_template->member->profile_data = $profile_data;
+		}
+
+		// Get the data for the specific field requested.
+		if ( ! empty( $profile_data ) && ! empty( $profile_data[ $r['field'] ]['field_type'] ) && ! empty( $profile_data[ $r['field'] ]['field_data'] ) ) {
+			$data = xprofile_format_profile_field( $profile_data[ $r['field'] ]['field_type'], $profile_data[ $r['field'] ]['field_data'] );
+		}
+
+		/**
+		 * Filters resulting piece of member profile data.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string|bool $data Profile data if found, otherwise false.
+		 * @param array       $r    Array of parsed arguments.
+		 */
+		$data = apply_filters( 'bp_get_member_profile_data', $data, $r );
+
+		/**
+		 * Filters the resulting piece of member profile data by field type.
+		 *
+		 * This is a dynamic filter based on field type of the current field requested.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param string|bool $data Profile data if found, otherwise false.
+		 * @param array       $r    Array of parsed arguments.
+		 */
+		if ( ! empty( $profile_data[ $r['field'] ]['field_type'] ) ) {
+			$data = apply_filters( 'bp_get_member_profile_data_' . $profile_data[ $r['field'] ]['field_type'], $data, $r );
+		}
+
+		return $data;
+	}
+
+/**
+ * Output the 'registered [x days ago]' string for the current member.
+ *
+ * @since 1.2.0
+ * @since 2.7.0 Added $args as a parameter.
+ *
+ * @param array $args Optional. {@see bp_get_member_registered()}
+ */
+function bp_member_registered( $args = array() ) {
+	echo bp_get_member_registered( $args );
+}
+	/**
+	 * Get the 'registered [x days ago]' string for the current member.
+	 *
+	 * @since 1.2.0
+	 * @since 2.7.0 Added $args as a parameter.
+	 *
+	 * @param array $args {
+	 *     Array of optional parameters.
+	 *
+	 *     @type bool $relative Optional. If true, returns relative registered date. eg. registered 5 months ago.
+	 *                          If false, returns registered date value from database.
+	 * }
+	 *
+	 * @return string
+	 */
+	function bp_get_member_registered( $args = array() ) {
+		global $members_template;
+
+		$r = wp_parse_args( $args, array(
+			'relative' => true,
+		) );
+
+		// We do not want relative time, so return now.
+		// @todo Should the 'bp_member_registered' filter be applied here?
+		if ( ! $r['relative'] ) {
+			return esc_attr( $members_template->member->user_registered );
+		}
+
+		$registered = esc_attr( bp_core_get_last_activity( $members_template->member->user_registered, _x( 'registered %s', 'Records the timestamp that the user registered into the activity stream', 'buddypress' ) ) );
+
+		/**
+		 * Filters the 'registered [x days ago]' string for the current member.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $registered The 'registered [x days ago]' string.
+		 */
+		return apply_filters( 'bp_member_registered', $registered );
+	}
+
+/**
+ * Output a random piece of profile data for the current member in the loop.
+ *
+ * @since 1.2.0
+ */
+function bp_member_random_profile_data() {
+	global $members_template;
+
+	if ( bp_is_active( 'xprofile' ) ) { ?>
+		<?php $random_data = xprofile_get_random_profile_data( $members_template->member->id, true ); ?>
+			<strong><?php echo wp_filter_kses( $random_data[0]->name ) ?></strong>
+			<?php echo wp_filter_kses( $random_data[0]->value ) ?>
+	<?php }
+}
+
+/**
+ * Output hidden input for preserving member search params on form submit.
+ *
+ * @since 1.2.0
+ */
+function bp_member_hidden_fields() {
+	$query_arg = bp_core_get_component_search_query_arg( 'members' );
+
+	if ( isset( $_REQUEST[ $query_arg ] ) ) {
+		echo '<input type="hidden" id="search_terms" value="' . esc_attr( $_REQUEST[ $query_arg ] ) . '" name="search_terms" />';
+	}
+
+	if ( isset( $_REQUEST['letter'] ) ) {
+		echo '<input type="hidden" id="selected_letter" value="' . esc_attr( $_REQUEST['letter'] ) . '" name="selected_letter" />';
+	}
+
+	if ( isset( $_REQUEST['members_search'] ) ) {
+		echo '<input type="hidden" id="search_terms" value="' . esc_attr( $_REQUEST['members_search'] ) . '" name="search_terms" />';
+	}
+}
+
+/**
+ * Output the Members directory search form.
+ *
+ * @since 1.0.0
+ */
+function bp_directory_members_search_form() {
+
+	$query_arg = bp_core_get_component_search_query_arg( 'members' );
+
+	if ( ! empty( $_REQUEST[ $query_arg ] ) ) {
+		$search_value = stripslashes( $_REQUEST[ $query_arg ] );
+	} else {
+		$search_value = bp_get_search_default_text( 'members' );
+	}
+
+	$search_form_html = '<form action="" method="get" id="search-members-form">
+		<label for="members_search"><input type="text" name="' . esc_attr( $query_arg ) . '" id="members_search" placeholder="'. esc_attr( $search_value ) .'" /></label>
+		<input type="submit" id="members_search_submit" name="members_search_submit" value="' . __( 'Search', 'buddypress' ) . '" />
+	</form>';
+
+	/**
+	 * Filters the Members component search form.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $search_form_html HTML markup for the member search form.
+	 */
+	echo apply_filters( 'bp_directory_members_search_form', $search_form_html );
+}
+
+/**
+ * Output the total member count.
+ *
+ * @since 1.2.0
+ */
+function bp_total_site_member_count() {
+	echo bp_get_total_site_member_count();
+}
+	/**
+	 * Get the total site member count.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return int
+	 */
+	function bp_get_total_site_member_count() {
+
+		/**
+		 * Filters the total site member count.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param int $value Number-formatted total site member count.
+		 */
+		return apply_filters( 'bp_get_total_site_member_count', bp_core_number_format( bp_core_get_total_member_count() ) );
+	}
+
+/** Navigation and other misc template tags ***********************************/
+
+/**
+ * Render the navigation markup for the logged-in user.
+ *
+ * Each component adds to this navigation array within its own
+ * [component_name]setup_nav() function.
+ *
+ * This navigation array is the top level navigation, so it contains items such as:
+ *      [Blog, Profile, Messages, Groups, Friends] ...
+ *
+ * The function will also analyze the current component the user is in, to
+ * determine whether or not to highlight a particular nav item.
+ *
+ * @since 1.1.0
+ *
+ * @todo Move to a back-compat file?
+ * @deprecated Does not seem to be called anywhere in BP core.
+ */
+function bp_get_loggedin_user_nav() {
+	$bp = buddypress();
+
+	// Loop through each navigation item.
+	foreach ( (array) $bp->members->nav->get_primary() as $nav_item ) {
+
+		$selected = '';
+
+		// If the current component matches the nav item id, then add a highlight CSS class.
+		if ( ! bp_is_directory() && ! empty( $bp->active_components[ bp_current_component() ] ) && $bp->active_components[ bp_current_component() ] == $nav_item->css_id ) {
+			$selected = ' class="current selected"';
+		}
+
+		// If we are viewing another person (current_userid does not equal
+		// loggedin_user->id then check to see if the two users are friends.
+		// if they are, add a highlight CSS class to the friends nav item
+		// if it exists.
+		if ( !bp_is_my_profile() && bp_displayed_user_id() ) {
+			$selected = '';
+
+			if ( bp_is_active( 'friends' ) ) {
+				if ( $nav_item->css_id == $bp->friends->id ) {
+					if ( friends_check_friendship( bp_loggedin_user_id(), bp_displayed_user_id() ) ) {
+						$selected = ' class="current selected"';
+					}
+				}
+			}
+		}
+
+		// Echo out the final list item.
+		echo apply_filters_ref_array( 'bp_get_loggedin_user_nav_' . $nav_item->css_id, array( '<li id="li-nav-' . $nav_item->css_id . '" ' . $selected . '><a id="my-' . $nav_item->css_id . '" href="' . $nav_item->link . '">' . $nav_item->name . '</a></li>', &$nav_item ) );
+	}
+
+	// Always add a log out list item to the end of the navigation.
+	$logout_link = '<li><a id="wp-logout" href="' .  wp_logout_url( bp_get_root_domain() ) . '">' . __( 'Log Out', 'buddypress' ) . '</a></li>';
+
+	echo apply_filters( 'bp_logout_nav_link', $logout_link );
+}
+
+/**
+ * Output the contents of the current user's home page.
+ *
+ * @since 2.6.0
+ */
+function bp_displayed_user_front_template_part() {
+	$located = bp_displayed_user_get_front_template();
+
+	if ( false !== $located ) {
+		$slug = str_replace( '.php', '', $located );
+		$name = null;
+
+		/**
+		 * Let plugins adding an action to bp_get_template_part get it from here
+		 *
+		 * @param string $slug Template part slug requested.
+		 * @param string $name Template part name requested.
+		 */
+		do_action( 'get_template_part_' . $slug, $slug, $name );
+
+		load_template( $located, true );
+	}
+
+	return $located;
+}
+
+/**
+ * Locate a custom user front template if it exists.
+ *
+ * @since 2.6.0
+ *
+ * @param  object|null $displayed_user Optional. Falls back to current user if not passed.
+ * @return string|bool                 Path to front template on success; boolean false on failure.
+ */
+function bp_displayed_user_get_front_template( $displayed_user = null ) {
+	if ( ! is_object( $displayed_user ) || empty( $displayed_user->id ) ) {
+		$displayed_user = bp_get_displayed_user();
+	}
+
+	if ( ! isset( $displayed_user->id ) ) {
+		return false;
+	}
+
+	if ( isset( $displayed_user->front_template ) ) {
+		return $displayed_user->front_template;
+	}
+
+	// Init the hierarchy
+	$template_names = array(
+		'members/single/front-id-' . sanitize_file_name( $displayed_user->id ) . '.php',
+		'members/single/front-nicename-' . sanitize_file_name( $displayed_user->userdata->user_nicename ) . '.php',
+	);
+
+	/**
+	 * Check for member types and add it to the hierarchy
+	 *
+	 * Make sure to register your member
+	 * type using the hook 'bp_register_member_types'
+	 */
+	if ( bp_get_member_types() ) {
+		$displayed_user_member_type = bp_get_member_type( $displayed_user->id );
+		if ( ! $displayed_user_member_type ) {
+			$displayed_user_member_type = 'none';
+		}
+
+		$template_names[] = 'members/single/front-member-type-' . sanitize_file_name( $displayed_user_member_type )   . '.php';
+	}
+
+	// Add The generic template to the end of the hierarchy
+	$template_names[] = 'members/single/front.php';
+
+	/**
+	 * Filters the hierarchy of user front templates corresponding to a specific user.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param array  $template_names Array of template paths.
+	 */
+	return bp_locate_template( apply_filters( 'bp_displayed_user_get_front_template', $template_names ), false, true );
+}
+
+/**
+ * Check if the displayed user has a custom front template.
+ *
+ * @since 2.6.0
+ */
+function bp_displayed_user_has_front_template() {
+	$displayed_user = bp_get_displayed_user();
+
+	return ! empty( $displayed_user->front_template );
+}
+
+/**
+ * Render the navigation markup for the displayed user.
+ *
+ * @since 1.1.0
+ */
+function bp_get_displayed_user_nav() {
+	$bp = buddypress();
+
+	foreach ( $bp->members->nav->get_primary() as $user_nav_item ) {
+		if ( empty( $user_nav_item->show_for_displayed_user ) && ! bp_is_my_profile() ) {
+			continue;
+		}
+
+		$selected = '';
+		if ( bp_is_current_component( $user_nav_item->slug ) ) {
+			$selected = ' class="current selected"';
+		}
+
+		if ( bp_loggedin_user_domain() ) {
+			$link = str_replace( bp_loggedin_user_domain(), bp_displayed_user_domain(), $user_nav_item->link );
+		} else {
+			$link = trailingslashit( bp_displayed_user_domain() . $user_nav_item->link );
+		}
+
+		/**
+		 * Filters the navigation markup for the displayed user.
+		 *
+		 * This is a dynamic filter that is dependent on the navigation tab component being rendered.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value         Markup for the tab list item including link.
+		 * @param array  $user_nav_item Array holding parts used to construct tab list item.
+		 *                              Passed by reference.
+		 */
+		echo apply_filters_ref_array( 'bp_get_displayed_user_nav_' . $user_nav_item->css_id, array( '<li id="' . $user_nav_item->css_id . '-personal-li" ' . $selected . '><a id="user-' . $user_nav_item->css_id . '" href="' . $link . '">' . $user_nav_item->name . '</a></li>', &$user_nav_item ) );
+	}
+}
+
+/** Cover image ***************************************************************/
+
+/**
+ * Should we use the cover image header
+ *
+ * @since 2.4.0
+ *
+ * @return bool True if the displayed user has a cover image,
+ *              False otherwise
+ */
+function bp_displayed_user_use_cover_image_header() {
+	return (bool) bp_is_active( 'xprofile', 'cover_image' ) && ! bp_disable_cover_image_uploads() && bp_attachments_is_wp_version_supported();
+}
+
+/** Avatars *******************************************************************/
+
+/**
+ * Output the logged-in user's avatar.
+ *
+ * @since 1.1.0
+ *
+ * @see bp_get_loggedin_user_avatar() for a description of params.
+ *
+ * @param array|string $args {@see bp_get_loggedin_user_avatar()}.
+ */
+function bp_loggedin_user_avatar( $args = '' ) {
+	echo bp_get_loggedin_user_avatar( $args );
+}
+	/**
+	 * Get the logged-in user's avatar.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and
+	 *      return values.
+	 *
+	 * @param array|string $args  {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see
+	 *     {@link bp_core_fetch_avatar()}.
+	 *     @type string   $alt    Default: 'Profile picture of [user name]'.
+	 *     @type bool     $html   Default: true.
+	 *     @type string   $type   Default: 'thumb'.
+	 *     @type int|bool $width  Default: false.
+	 *     @type int|bool $height Default: false.
+	 * }
+	 * @return string User avatar string.
+	 */
+	function bp_get_loggedin_user_avatar( $args = '' ) {
+
+		$r = wp_parse_args( $args, array(
+			'item_id' => bp_loggedin_user_id(),
+			'type'    => 'thumb',
+			'width'   => false,
+			'height'  => false,
+			'html'    => true,
+			'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_loggedin_user_fullname() )
+		) );
+
+		/**
+		 * Filters the logged in user's avatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value User avatar string.
+		 * @param array  $r     Array of parsed arguments.
+		 * @param array  $args  Array of initial arguments.
+		 */
+		return apply_filters( 'bp_get_loggedin_user_avatar', bp_core_fetch_avatar( $r ), $r, $args );
+	}
+
+/**
+ * Output the displayed user's avatar.
+ *
+ * @since 1.1.0
+ *
+ * @see bp_get_displayed_user_avatar() for a description of params.
+ *
+ * @param array|string $args {@see bp_get_displayed_user_avatar()}.
+ */
+function bp_displayed_user_avatar( $args = '' ) {
+	echo bp_get_displayed_user_avatar( $args );
+}
+	/**
+	 * Get the displayed user's avatar.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and
+	 *      return values.
+	 *
+	 * @param array|string $args  {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see
+	 *     {@link bp_core_fetch_avatar()}.
+	 *     @type string   $alt    Default: 'Profile picture of [user name]'.
+	 *     @type bool     $html   Default: true.
+	 *     @type string   $type   Default: 'thumb'.
+	 *     @type int|bool $width  Default: false.
+	 *     @type int|bool $height Default: false.
+	 * }
+	 * @return string User avatar string.
+	 */
+	function bp_get_displayed_user_avatar( $args = '' ) {
+
+		$r = wp_parse_args( $args, array(
+			'item_id' => bp_displayed_user_id(),
+			'type'    => 'thumb',
+			'width'   => false,
+			'height'  => false,
+			'html'    => true,
+			'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+		) );
+
+		/**
+		 * Filters the displayed user's avatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value User avatar string.
+		 * @param array  $r     Array of parsed arguments.
+		 * @param array  $args  Array of initial arguments.
+		 */
+		return apply_filters( 'bp_get_displayed_user_avatar', bp_core_fetch_avatar( $r ), $r, $args );
+	}
+
+/**
+ * Output the email address of the displayed user.
+ *
+ * @since 1.5.0
+ */
+function bp_displayed_user_email() {
+	echo bp_get_displayed_user_email();
+}
+	/**
+	 * Get the email address of the displayed user.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_displayed_user_email() {
+		$bp = buddypress();
+
+		// If displayed user exists, return email address.
+		if ( isset( $bp->displayed_user->userdata->user_email ) )
+			$retval = $bp->displayed_user->userdata->user_email;
+		else
+			$retval = '';
+
+		/**
+		 * Filters the email address of the displayed user.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $retval Email address for displayed user.
+		 */
+		return apply_filters( 'bp_get_displayed_user_email', esc_attr( $retval ) );
+	}
+
+/**
+ * Output the "active [x days ago]" string for a user.
+ *
+ * @since 1.0.0
+ *
+ * @see bp_get_last_activity() for a description of parameters.
+ *
+ * @param int $user_id See {@link bp_get_last_activity()}.
+ */
+function bp_last_activity( $user_id = 0 ) {
+
+	/**
+	 * Filters the 'active [x days ago]' string for a user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Formatted 'active [x days ago]' string.
+	 */
+	echo apply_filters( 'bp_last_activity', bp_get_last_activity( $user_id ) );
+}
+	/**
+	 * Get the "active [x days ago]" string for a user.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $user_id ID of the user. Default: displayed user ID.
+	 * @return string
+	 */
+	function bp_get_last_activity( $user_id = 0 ) {
+
+		if ( empty( $user_id ) )
+			$user_id = bp_displayed_user_id();
+
+		$last_activity = bp_core_get_last_activity( bp_get_user_last_activity( $user_id ), __('active %s', 'buddypress') );
+
+		/**
+		 * Filters the 'active [x days ago]' string for a user.
+		 *
+		 * @since 1.5.0
+		 * @since 2.6.0 Added the `$user_id` parameter.
+		 *
+		 * @param string $value   Formatted 'active [x days ago]' string.
+		 * @param int    $user_id ID of the user.
+		 */
+		return apply_filters( 'bp_get_last_activity', $last_activity, $user_id );
+	}
+
+/**
+ * Output the calculated first name of the displayed or logged-in user.
+ *
+ * @since 1.2.0
+ */
+function bp_user_firstname() {
+	echo bp_get_user_firstname();
+}
+	/**
+	 * Output the first name of a user.
+	 *
+	 * Simply takes all the characters before the first space in a name.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string|bool $name Full name to use when generating first name.
+	 *                          Defaults to displayed user's first name, or to
+	 *                          logged-in user's first name if it's unavailable.
+	 * @return string
+	 */
+	function bp_get_user_firstname( $name = false ) {
+
+		// Try to get displayed user.
+		if ( empty( $name ) )
+			$name = bp_get_displayed_user_fullname();
+
+		// Fall back on logged in user.
+		if ( empty( $name ) )
+			$name = bp_get_loggedin_user_fullname();
+
+		$fullname = (array) explode( ' ', $name );
+
+		/**
+		 * Filters the first name of a user.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value    First name of user.
+		 * @param string $fullname Full name of user.
+		 */
+		return apply_filters( 'bp_get_user_firstname', $fullname[0], $fullname );
+	}
+
+/**
+ * Output the link for the logged-in user's profile.
+ *
+ * @since 1.2.4
+ */
+function bp_loggedin_user_link() {
+	echo esc_url( bp_get_loggedin_user_link() );
+}
+	/**
+	 * Get the link for the logged-in user's profile.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_loggedin_user_link() {
+
+		/**
+		 * Filters the link for the logged-in user's profile.
+		 *
+		 * @since 1.2.4
+		 *
+		 * @param string $value Link for the logged-in user's profile.
+		 */
+		return apply_filters( 'bp_get_loggedin_user_link', bp_loggedin_user_domain() );
+	}
+
+/**
+ * Output the link for the displayed user's profile.
+ *
+ * @since 1.2.4
+ */
+function bp_displayed_user_link() {
+	echo esc_url( bp_get_displayed_user_link() );
+}
+	/**
+	 * Get the link for the displayed user's profile.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_displayed_user_link() {
+
+		/**
+		 * Filters the link for the displayed user's profile.
+		 *
+		 * @since 1.2.4
+		 *
+		 * @param string $value Link for the displayed user's profile.
+		 */
+		return apply_filters( 'bp_get_displayed_user_link', bp_displayed_user_domain() );
+	}
+
+	/**
+	 * Alias of {@link bp_displayed_user_domain()}.
+	 *
+	 * @deprecated
+	 */
+	function bp_user_link() { bp_displayed_user_domain(); }
+
+/**
+ * Alias of {@link bp_displayed_user_id()}.
+ *
+ * @since 1.0.0
+ */
+function bp_current_user_id() { return bp_displayed_user_id(); }
+
+/**
+ * Generate the link for the displayed user's profile.
+ *
+ * @since 1.0.0
+ *
+ * @return string
+ */
+function bp_displayed_user_domain() {
+	$bp = buddypress();
+
+	/**
+	 * Filters the generated link for the displayed user's profile.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Generated link for the displayed user's profile.
+	 */
+	return apply_filters( 'bp_displayed_user_domain', isset( $bp->displayed_user->domain ) ? $bp->displayed_user->domain : '' );
+}
+
+/**
+ * Generate the link for the logged-in user's profile.
+ *
+ * @since 1.0.0
+ *
+ * @return string
+ */
+function bp_loggedin_user_domain() {
+	$bp = buddypress();
+
+	/**
+	 * Filters the generated link for the logged-in user's profile.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $value Generated link for the logged-in user's profile.
+	 */
+	return apply_filters( 'bp_loggedin_user_domain', isset( $bp->loggedin_user->domain ) ? $bp->loggedin_user->domain : '' );
+}
+
+/**
+ * Output the displayed user's display name.
+ *
+ * @since 1.0.0
+ */
+function bp_displayed_user_fullname() {
+	echo bp_get_displayed_user_fullname();
+}
+	/**
+	 * Get the displayed user's display name.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_displayed_user_fullname() {
+		$bp = buddypress();
+
+		/**
+		 * Filters the displayed user's display name.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Displayed user's display name.
+		 */
+		return apply_filters( 'bp_displayed_user_fullname', isset( $bp->displayed_user->fullname ) ? $bp->displayed_user->fullname : '' );
+	}
+
+	/**
+	 * Alias of {@link bp_get_displayed_user_fullname()}.
+	 *
+	 * @since 1.0.0
+	 */
+	function bp_user_fullname() { echo bp_get_displayed_user_fullname(); }
+
+
+/**
+ * Output the logged-in user's display name.
+ *
+ * @since 1.0.0
+ */
+function bp_loggedin_user_fullname() {
+	echo bp_get_loggedin_user_fullname();
+}
+	/**
+	 * Get the logged-in user's display name.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_loggedin_user_fullname() {
+		$bp = buddypress();
+
+		/**
+		 * Filters the logged-in user's display name.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Logged-in user's display name.
+		 */
+		return apply_filters( 'bp_get_loggedin_user_fullname', isset( $bp->loggedin_user->fullname ) ? $bp->loggedin_user->fullname : '' );
+	}
+
+/**
+ * Output the username of the displayed user.
+ *
+ * @since 1.2.0
+ */
+function bp_displayed_user_username() {
+	echo bp_get_displayed_user_username();
+}
+	/**
+	 * Get the username of the displayed user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_displayed_user_username() {
+		$bp = buddypress();
+
+		if ( bp_displayed_user_id() ) {
+			$username = bp_core_get_username( bp_displayed_user_id(), $bp->displayed_user->userdata->user_nicename, $bp->displayed_user->userdata->user_login );
+		} else {
+			$username = '';
+		}
+
+		/**
+		 * Filters the username of the displayed user.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $username Username of the displayed user.
+		 */
+		return apply_filters( 'bp_get_displayed_user_username', $username );
+	}
+
+/**
+ * Output the username of the logged-in user.
+ *
+ * @since 1.2.0
+ */
+function bp_loggedin_user_username() {
+	echo bp_get_loggedin_user_username();
+}
+	/**
+	 * Get the username of the logged-in user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_loggedin_user_username() {
+		$bp = buddypress();
+
+		if ( bp_loggedin_user_id() ) {
+			$username = bp_core_get_username( bp_loggedin_user_id(), $bp->loggedin_user->userdata->user_nicename, $bp->loggedin_user->userdata->user_login );
+		} else {
+			$username = '';
+		}
+
+		/**
+		 * Filters the username of the logged-in user.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $username Username of the logged-in user.
+		 */
+		return apply_filters( 'bp_get_loggedin_user_username', $username );
+	}
+
+/**
+ * Echo the current member type message.
+ *
+ * @since 2.3.0
+ */
+function bp_current_member_type_message() {
+	echo bp_get_current_member_type_message();
+}
+	/**
+	 * Generate the current member type message.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return string
+	 */
+	function bp_get_current_member_type_message() {
+		$type_object = bp_get_member_type_object( bp_get_current_member_type() );
+
+		$message = sprintf( __( 'Viewing members of the type: %s', 'buddypress' ), '<strong>' . $type_object->labels['singular_name'] . '</strong>' );
+
+		/**
+		 * Filters the current member type message.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string $message Message to filter.
+		 */
+		return apply_filters( 'bp_get_current_member_type_message', $message );
+	}
+
+/** Signup Form ***************************************************************/
+
+/**
+ * Do we have a working custom sign up page?
+ *
+ * @since 1.5.0
+ *
+ * @return bool True if page and template exist, false if not.
+ */
+function bp_has_custom_signup_page() {
+	static $has_page = false;
+
+	if ( empty( $has_page ) )
+		$has_page = bp_get_signup_slug() && bp_locate_template( array( 'registration/register.php', 'members/register.php', 'register.php' ), false );
+
+	return (bool) $has_page;
+}
+
+/**
+ * Output the URL to the signup page.
+ *
+ * @since 1.0.0
+ */
+function bp_signup_page() {
+	echo esc_url( bp_get_signup_page() );
+}
+	/**
+	 * Get the URL to the signup page.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_page() {
+		if ( bp_has_custom_signup_page() ) {
+			$page = trailingslashit( bp_get_root_domain() . '/' . bp_get_signup_slug() );
+		} else {
+			$page = bp_get_root_domain() . '/wp-signup.php';
+		}
+
+		/**
+		 * Filters the URL to the signup page.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $page URL to the signup page.
+		 */
+		return apply_filters( 'bp_get_signup_page', $page );
+	}
+
+/**
+ * Do we have a working custom activation page?
+ *
+ * @since 1.5.0
+ *
+ * @return boolean True if page and template exist, false if not.
+ */
+function bp_has_custom_activation_page() {
+	static $has_page = false;
+
+	if ( empty( $has_page ) )
+		$has_page = bp_get_activate_slug() && bp_locate_template( array( 'registration/activate.php', 'members/activate.php', 'activate.php' ), false );
+
+	return (bool) $has_page;
+}
+
+/**
+ * Output the URL of the activation page.
+ *
+ * @since 1.0.0
+ */
+function bp_activation_page() {
+	echo esc_url( bp_get_activation_page() );
+}
+	/**
+	 * Get the URL of the activation page.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_activation_page() {
+		if ( bp_has_custom_activation_page() ) {
+			$page = trailingslashit( bp_get_root_domain() . '/' . bp_get_activate_slug() );
+		} else {
+			$page = trailingslashit( bp_get_root_domain() ) . 'wp-activate.php';
+		}
+
+		/**
+		 * Filters the URL of the activation page.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $page URL to the activation page.
+		 */
+		return apply_filters( 'bp_get_activation_page', $page );
+	}
+
+/**
+ * Get the activation key from the current request URL.
+ *
+ * @since 3.0.0
+ *
+ * @return string
+ */
+function bp_get_current_activation_key() {
+	$key = '';
+
+	if ( bp_is_current_component( 'activate' ) ) {
+		if ( isset( $_GET['key'] ) ) {
+			$key = wp_unslash( $_GET['key'] );
+		} else {
+			$key = bp_current_action();
+		}
+	}
+
+	/**
+	 * Filters the activation key from the current request URL.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $key Activation key.
+	 */
+	return apply_filters( 'bp_get_current_activation_key', $key );
+}
+
+/**
+ * Output the username submitted during signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_username_value() {
+	echo bp_get_signup_username_value();
+}
+	/**
+	 * Get the username submitted during signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @todo This should be properly escaped.
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_username_value() {
+		$value = '';
+		if ( isset( $_POST['signup_username'] ) )
+			$value = $_POST['signup_username'];
+
+		/**
+		 * Filters the username submitted during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Username submitted during signup.
+		 */
+		return apply_filters( 'bp_get_signup_username_value', $value );
+	}
+
+/**
+ * Output the user email address submitted during signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_email_value() {
+	echo bp_get_signup_email_value();
+}
+	/**
+	 * Get the email address submitted during signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @todo This should be properly escaped.
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_email_value() {
+		$value = '';
+		if ( isset( $_POST['signup_email'] ) )
+			$value = $_POST['signup_email'];
+
+		/**
+		 * Filters the email address submitted during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Email address submitted during signup.
+		 */
+		return apply_filters( 'bp_get_signup_email_value', $value );
+	}
+
+/**
+ * Output the 'signup_with_blog' value submitted during signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_with_blog_value() {
+	echo bp_get_signup_with_blog_value();
+}
+	/**
+	 * Get the 'signup_with_blog' value submitted during signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_with_blog_value() {
+		$value = '';
+		if ( isset( $_POST['signup_with_blog'] ) )
+			$value = $_POST['signup_with_blog'];
+
+		/**
+		 * Filters the 'signup_with_blog' value submitted during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value 'signup_with_blog' value submitted during signup.
+		 */
+		return apply_filters( 'bp_get_signup_with_blog_value', $value );
+	}
+
+/**
+ * Output the 'signup_blog_url' value submitted at signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_blog_url_value() {
+	echo bp_get_signup_blog_url_value();
+}
+	/**
+	 * Get the 'signup_blog_url' value submitted at signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @todo Should be properly escaped.
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_blog_url_value() {
+		$value = '';
+		if ( isset( $_POST['signup_blog_url'] ) )
+			$value = $_POST['signup_blog_url'];
+
+		/**
+		 * Filters the 'signup_blog_url' value submitted during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value 'signup_blog_url' value submitted during signup.
+		 */
+		return apply_filters( 'bp_get_signup_blog_url_value', $value );
+	}
+
+/**
+ * Output the base URL for subdomain installations of WordPress Multisite.
+ *
+ * @since 2.1.0
+ */
+function bp_signup_subdomain_base() {
+	echo bp_signup_get_subdomain_base();
+}
+	/**
+	 * Return the base URL for subdomain installations of WordPress Multisite.
+	 *
+	 * Replaces bp_blogs_get_subdomain_base()
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return string The base URL - eg, 'example.com' for site_url() example.com or www.example.com.
+	 */
+	function bp_signup_get_subdomain_base() {
+		global $current_site;
+
+		// In case plugins are still using this filter.
+		$subdomain_base = apply_filters( 'bp_blogs_subdomain_base', preg_replace( '|^www\.|', '', $current_site->domain ) . $current_site->path );
+
+		/**
+		 * Filters the base URL for subdomain installations of WordPress Multisite.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $subdomain_base The base URL - eg, 'example.com' for
+		 *                               site_url() example.com or www.example.com.
+		 */
+		return apply_filters( 'bp_signup_subdomain_base', $subdomain_base );
+	}
+
+/**
+ * Output the 'signup_blog_titl' value submitted at signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_blog_title_value() {
+	echo bp_get_signup_blog_title_value();
+}
+	/**
+	 * Get the 'signup_blog_title' value submitted at signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @todo Should be properly escaped.
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_blog_title_value() {
+		$value = '';
+		if ( isset( $_POST['signup_blog_title'] ) )
+			$value = $_POST['signup_blog_title'];
+
+		/**
+		 * Filters the 'signup_blog_title' value submitted during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value 'signup_blog_title' value submitted during signup.
+		 */
+		return apply_filters( 'bp_get_signup_blog_title_value', $value );
+	}
+
+/**
+ * Output the 'signup_blog_privacy' value submitted at signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_blog_privacy_value() {
+	echo bp_get_signup_blog_privacy_value();
+}
+	/**
+	 * Get the 'signup_blog_privacy' value submitted at signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @todo Should be properly escaped.
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_blog_privacy_value() {
+		$value = '';
+		if ( isset( $_POST['signup_blog_privacy'] ) )
+			$value = $_POST['signup_blog_privacy'];
+
+		/**
+		 * Filters the 'signup_blog_privacy' value submitted during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value 'signup_blog_privacy' value submitted during signup.
+		 */
+		return apply_filters( 'bp_get_signup_blog_privacy_value', $value );
+	}
+
+/**
+ * Output the avatar dir used during signup.
+ *
+ * @since 1.1.0
+ */
+function bp_signup_avatar_dir_value() {
+	echo bp_get_signup_avatar_dir_value();
+}
+	/**
+	 * Get the avatar dir used during signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_signup_avatar_dir_value() {
+		$bp = buddypress();
+
+		// Check if signup_avatar_dir is passed.
+		if ( !empty( $_POST['signup_avatar_dir'] ) )
+			$signup_avatar_dir = $_POST['signup_avatar_dir'];
+
+		// If not, check if global is set.
+		elseif ( !empty( $bp->signup->avatar_dir ) )
+			$signup_avatar_dir = $bp->signup->avatar_dir;
+
+		// If not, set false.
+		else
+			$signup_avatar_dir = false;
+
+		/**
+		 * Filters the avatar dir used during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string|bool $signup_avatar_dir Avatar dir used during signup or false.
+		 */
+		return apply_filters( 'bp_get_signup_avatar_dir_value', $signup_avatar_dir );
+	}
+
+/**
+ * Determines whether privacy policy acceptance is required for registration.
+ *
+ * @since 4.0.0
+ *
+ * @return bool
+ */
+function bp_signup_requires_privacy_policy_acceptance() {
+	// Default to true when a published Privacy Policy page exists.
+	$privacy_policy_url = get_privacy_policy_url();
+	$required           = ! empty( $privacy_policy_url );
+
+	/**
+	 * Filters whether privacy policy acceptance is required for registration.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param bool $required Whether privacy policy acceptance is required.
+	 */
+	return (bool) apply_filters( 'bp_signup_requires_privacy_policy_acceptance', $required );
+}
+
+/**
+ * Output the current signup step.
+ *
+ * @since 1.1.0
+ */
+function bp_current_signup_step() {
+	echo bp_get_current_signup_step();
+}
+	/**
+	 * Get the current signup step.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_current_signup_step() {
+		return buddypress()->signup->step;
+	}
+
+/**
+ * Output the user avatar during signup.
+ *
+ * @since 1.1.0
+ *
+ * @see bp_get_signup_avatar() for description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_signup_avatar(}.
+ */
+function bp_signup_avatar( $args = '' ) {
+	echo bp_get_signup_avatar( $args );
+}
+	/**
+	 * Get the user avatar during signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @see bp_core_fetch_avatar() for description of arguments.
+	 *
+	 * @param array|string $args {
+	 *     Array of optional arguments.
+	 *     @type int    $size  Height/weight in pixels. Default: value of
+	 *                         bp_core_avatar_full_width().
+	 *     @type string $class CSS class. Default: 'avatar'.
+	 *     @type string $alt   HTML 'alt' attribute. Default: 'Your Avatar'.
+	 * }
+	 * @return string
+	 */
+	function bp_get_signup_avatar( $args = '' ) {
+		$bp = buddypress();
+
+		$defaults = array(
+			'size' => bp_core_avatar_full_width(),
+			'class' => 'avatar',
+			'alt' => __( 'Your Profile Photo', 'buddypress' )
+		);
+
+		$r = wp_parse_args( $args, $defaults );
+		extract( $r, EXTR_SKIP );
+
+		// Avatar DIR is found.
+		if ( $signup_avatar_dir = bp_get_signup_avatar_dir_value() ) {
+			$gravatar_img = bp_core_fetch_avatar( array(
+				'item_id'    => $signup_avatar_dir,
+				'object'     => 'signup',
+				'avatar_dir' => 'avatars/signups',
+				'type'       => 'full',
+				'width'      => $size,
+				'height'     => $size,
+				'alt'        => $alt,
+				'class'      => $class
+			) );
+
+			// No avatar DIR was found.
+		} else {
+
+			// Set default gravatar type.
+			if ( empty( $bp->grav_default->user ) )
+				$default_grav = 'wavatar';
+			elseif ( 'mystery' == $bp->grav_default->user )
+				$default_grav = $bp->plugin_url . 'bp-core/images/mystery-man.jpg';
+			else
+				$default_grav = $bp->grav_default->user;
+
+			/**
+			 * Filters the base Gravatar url used for signup avatars when no avatar dir found.
+			 *
+			 * @since 1.0.2
+			 *
+			 * @param string $value Gravatar url to use.
+			 */
+			$gravatar_url    = apply_filters( 'bp_gravatar_url', '//www.gravatar.com/avatar/' );
+			$md5_lcase_email = md5( strtolower( bp_get_signup_email_value() ) );
+			$gravatar_img    = '<img src="' . $gravatar_url . $md5_lcase_email . '?d=' . $default_grav . '&amp;s=' . $size . '" width="' . $size . '" height="' . $size . '" alt="' . $alt . '" class="' . $class . '" />';
+		}
+
+		/**
+		 * Filters the user avatar during signup.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $gravatar_img Avatar HTML image tag.
+		 * @param array  $args         Array of parsed args for avatar query.
+		 */
+		return apply_filters( 'bp_get_signup_avatar', $gravatar_img, $args );
+	}
+
+/**
+ * Output whether signup is allowed.
+ *
+ * @since 1.1.0
+ *
+ * @todo Remove this function. Echoing a bool is pointless.
+ */
+function bp_signup_allowed() {
+	echo bp_get_signup_allowed();
+}
+	/**
+	 * Is user signup allowed?
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return bool
+	 */
+	function bp_get_signup_allowed() {
+		/**
+		 * Filters whether or not new signups are allowed.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param bool $signup_allowed Whether or not new signups are allowed.
+		 */
+		return apply_filters( 'bp_get_signup_allowed', (bool) bp_get_option( 'users_can_register' ) );
+	}
+
+/**
+ * Hook member activity feed to <head>.
+ *
+ * @since 1.5.0
+ */
+function bp_members_activity_feed() {
+	if ( !bp_is_active( 'activity' ) || !bp_is_user() )
+		return; ?>
+
+	<link rel="alternate" type="application/rss+xml" title="<?php bloginfo( 'name' ) ?> | <?php bp_displayed_user_fullname() ?> | <?php _e( 'Activity RSS Feed', 'buddypress' ) ?>" href="<?php bp_member_activity_feed_link() ?>" />
+
+<?php
+}
+add_action( 'bp_head', 'bp_members_activity_feed' );
+
+/**
+ * Output a link to a members component subpage.
+ *
+ * @since 1.5.0
+ *
+ * @see bp_get_members_component_link() for description of parameters.
+ *
+ * @param string      $component See {@bp_get_members_component_link()}.
+ * @param string      $action See {@bp_get_members_component_link()}.
+ * @param string      $query_args See {@bp_get_members_component_link()}.
+ * @param string|bool $nonce See {@bp_get_members_component_link()}.
+ */
+function bp_members_component_link( $component, $action = '', $query_args = '', $nonce = false ) {
+	echo esc_url( bp_get_members_component_link( $component, $action, $query_args, $nonce ) );
+}
+	/**
+	 * Generate a link to a members component subpage.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string       $component  ID of the component (eg 'friends').
+	 * @param string       $action     Optional. 'action' slug (eg 'invites').
+	 * @param array|string $query_args Optional. Array of URL params to add to the
+	 *                                 URL. See {@link add_query_arg()} for format.
+	 * @param array|bool   $nonce      Optional. If provided, the URL will be passed
+	 *                                 through wp_nonce_url() with $nonce as the
+	 *                                 action string.
+	 * @return string
+	 */
+	function bp_get_members_component_link( $component, $action = '', $query_args = '', $nonce = false ) {
+
+		// Must be displayed user.
+		if ( !bp_displayed_user_id() )
+			return;
+
+		$bp = buddypress();
+
+		// Append $action to $url if there is no $type.
+		if ( !empty( $action ) )
+			$url = bp_displayed_user_domain() . $bp->{$component}->slug . '/' . $action;
+		else
+			$url = bp_displayed_user_domain() . $bp->{$component}->slug;
+
+		// Add a slash at the end of our user url.
+		$url = trailingslashit( $url );
+
+		// Add possible query arg.
+		if ( !empty( $query_args ) && is_array( $query_args ) )
+			$url = add_query_arg( $query_args, $url );
+
+		// To nonce, or not to nonce...
+		if ( true === $nonce )
+			$url = wp_nonce_url( $url );
+		elseif ( is_string( $nonce ) )
+			$url = wp_nonce_url( $url, $nonce );
+
+		// Return the url, if there is one.
+		if ( !empty( $url ) )
+			return $url;
+	}
Index: /tags/4.0.0-beta1/src/bp-members/bp-members-widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/bp-members-widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/bp-members-widgets.php	(revision 12265)
@@ -0,0 +1,106 @@
+<?php
+/**
+ * BuddyPress Members Widgets.
+ *
+ * @package BuddyPress
+ * @subpackage MembersWidgets
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register bp-members widgets.
+ *
+ * Previously, these widgets were registered in bp-core.
+ *
+ * @since 2.2.0
+ */
+function bp_members_register_widgets() {
+	add_action( 'widgets_init', function() { return register_widget( 'BP_Core_Members_Widget' );         } );
+	add_action( 'widgets_init', function() { return register_widget( 'BP_Core_Whos_Online_Widget' );     } );
+	add_action( 'widgets_init', function() { return register_widget( 'BP_Core_Recently_Active_Widget' ); } );
+}
+add_action( 'bp_register_widgets', 'bp_members_register_widgets' );
+
+/**
+ * AJAX request handler for Members widgets.
+ *
+ * @since 1.0.0
+ *
+ * @see BP_Core_Members_Widget
+ */
+function bp_core_ajax_widget_members() {
+
+	check_ajax_referer( 'bp_core_widget_members' );
+
+	// Setup some variables to check.
+	$filter      = ! empty( $_POST['filter']      ) ? $_POST['filter']                : 'recently-active-members';
+	$max_members = ! empty( $_POST['max-members'] ) ? absint( $_POST['max-members'] ) : 5;
+
+	// Determine the type of members query to perform.
+	switch ( $filter ) {
+
+		// Newest activated.
+		case 'newest-members' :
+			$type = 'newest';
+			break;
+
+		// Popular by friends.
+		case 'popular-members' :
+			if ( bp_is_active( 'friends' ) ) {
+				$type = 'popular';
+			} else {
+				$type = 'active';
+			}
+			break;
+
+		// Default.
+		case 'recently-active-members' :
+		default :
+			$type = 'active';
+			break;
+	}
+
+	// Setup args for querying members.
+	$members_args = array(
+		'user_id'         => 0,
+		'type'            => $type,
+		'per_page'        => $max_members,
+		'max'             => $max_members,
+		'populate_extras' => true,
+		'search_terms'    => false,
+	);
+
+	// Query for members.
+	if ( bp_has_members( $members_args ) ) : ?>
+		<?php echo '0[[SPLIT]]'; // Return valid result. TODO: remove this. ?>
+		<?php while ( bp_members() ) : bp_the_member(); ?>
+			<li class="vcard">
+				<div class="item-avatar">
+					<a href="<?php bp_member_permalink(); ?>"><?php bp_member_avatar(); ?></a>
+				</div>
+
+				<div class="item">
+					<div class="item-title fn"><a href="<?php bp_member_permalink(); ?>"><?php bp_member_name(); ?></a></div>
+					<?php if ( 'active' === $type ) : ?>
+						<div class="item-meta"><span class="activity"><?php bp_member_last_active(); ?></span></div>
+					<?php elseif ( 'newest' === $type ) : ?>
+						<div class="item-meta"><span class="activity"><?php bp_member_registered(); ?></span></div>
+					<?php elseif ( bp_is_active( 'friends' ) ) : ?>
+						<div class="item-meta"><span class="activity"><?php bp_member_total_friend_count(); ?></span></div>
+					<?php endif; ?>
+				</div>
+			</li>
+
+		<?php endwhile; ?>
+
+	<?php else: ?>
+		<?php echo "-1[[SPLIT]]<li>"; ?>
+		<?php esc_html_e( 'There were no members found, please try another filter.', 'buddypress' ) ?>
+		<?php echo "</li>"; ?>
+	<?php endif;
+}
+add_action( 'wp_ajax_widget_members',        'bp_core_ajax_widget_members' );
+add_action( 'wp_ajax_nopriv_widget_members', 'bp_core_ajax_widget_members' );
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-members-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-members-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-members-template.php	(revision 12265)
@@ -0,0 +1,301 @@
+<?php
+/**
+ * BuddyPress Member Template loop class.
+ *
+ * @package BuddyPress
+ * @subpackage Members
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main member template loop class.
+ *
+ * Responsible for loading a group of members into a loop for display.
+ *
+ * @since 1.0.0
+ */
+class BP_Core_Members_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $current_member = -1;
+
+	/**
+	 * The number of members returned by the paged query.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $member_count;
+
+	/**
+	 * Array of members located by the query.
+	 *
+	 * @since 1.0.0
+	 * @var array
+	 */
+	public $members;
+
+	/**
+	 * The member object currently being iterated on.
+	 *
+	 * @since 1.0.0
+	 * @var object
+	 */
+	public $member;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @since 1.0.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * The type of member being requested. Used for ordering results.
+	 *
+	 * @since 2.3.0
+	 * @var string
+	 */
+	public $type;
+
+	/**
+	 * The unique string used for pagination queries.
+	 *
+	 * @since 2.2.0
+	 * @var string
+	 */
+	public $pag_arg;
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	public $pag_page;
+
+	/**
+	 * The number of items being requested per page.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	public $pag_num;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @since 1.0.0
+	 * @var string
+	 */
+	public $pag_links;
+
+	/**
+	 * The total number of members matching the query parameters.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $total_member_count;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_User_Query for an in-depth description of parameters.
+	 *
+	 * @param string       $type                Sort order.
+	 * @param int          $page_number         Page of results.
+	 * @param int          $per_page            Number of results per page.
+	 * @param int          $max                 Max number of results to return.
+	 * @param int          $user_id             Limit to friends of a user.
+	 * @param string       $search_terms        Limit to users matching search terms.
+	 * @param array        $include             Limit results by these user IDs.
+	 * @param bool         $populate_extras     Fetch optional extras.
+	 * @param array        $exclude             Exclude these IDs from results.
+	 * @param array        $meta_key            Limit to users with a meta_key.
+	 * @param array        $meta_value          Limit to users with a meta_value (with meta_key).
+	 * @param string       $page_arg            Optional. The string used as a query parameter in pagination links.
+	 *                                          Default: 'upage'.
+	 * @param array|string $member_type         Array or comma-separated string of member types to limit results to.
+	 * @param array|string $member_type__in     Array or comma-separated string of member types to limit results to.
+	 * @param array|string $member_type__not_in Array or comma-separated string of member types to exclude
+	 *                                          from results.
+	 */
+	function __construct( $type, $page_number, $per_page, $max, $user_id, $search_terms, $include, $populate_extras, $exclude, $meta_key, $meta_value, $page_arg = 'upage', $member_type = '', $member_type__in = '', $member_type__not_in = '' ) {
+
+		$this->pag_arg  = sanitize_key( $page_arg );
+		$this->pag_page = bp_sanitize_pagination_arg( $this->pag_arg, $page_number );
+		$this->pag_num  = bp_sanitize_pagination_arg( 'num',          $per_page    );
+		$this->type     = $type;
+
+		if ( !empty( $_REQUEST['letter'] ) )
+			$this->members = BP_Core_User::get_users_by_letter( $_REQUEST['letter'], $this->pag_num, $this->pag_page, $populate_extras, $exclude );
+		else
+			$this->members = bp_core_get_users( array( 'type' => $this->type, 'per_page' => $this->pag_num, 'page' => $this->pag_page, 'user_id' => $user_id, 'include' => $include, 'search_terms' => $search_terms, 'populate_extras' => $populate_extras, 'exclude' => $exclude, 'meta_key' => $meta_key, 'meta_value' => $meta_value, 'member_type' => $member_type, 'member_type__in' => $member_type__in, 'member_type__not_in' => $member_type__not_in ) );
+
+		if ( !$max || $max >= (int) $this->members['total'] )
+			$this->total_member_count = (int) $this->members['total'];
+		else
+			$this->total_member_count = (int) $max;
+
+		$this->members = $this->members['users'];
+
+		if ( $max ) {
+			if ( $max >= count( $this->members ) ) {
+				$this->member_count = count( $this->members );
+			} else {
+				$this->member_count = (int) $max;
+			}
+		} else {
+			$this->member_count = count( $this->members );
+		}
+
+		if ( (int) $this->total_member_count && (int) $this->pag_num ) {
+			$pag_args = array(
+				$this->pag_arg => '%#%',
+			);
+
+			if ( defined( 'DOING_AJAX' ) && true === (bool) DOING_AJAX ) {
+				$base = remove_query_arg( 's', wp_get_referer() );
+			} else {
+				$base = '';
+			}
+
+			/**
+			 * Defaults to an empty array to make sure paginate_links()
+			 * won't add the $page_arg to the links which would break
+			 * pagination in case JavaScript is disabled.
+			 */
+			$add_args = array();
+
+			if ( ! empty( $search_terms ) ) {
+				$query_arg = bp_core_get_component_search_query_arg( 'members' );
+				$add_args[ $query_arg ] = urlencode( $search_terms );
+			}
+
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $pag_args, $base ),
+				'format'    => '',
+				'total'     => ceil( (int) $this->total_member_count / (int) $this->pag_num ),
+				'current'   => (int) $this->pag_page,
+				'prev_text' => _x( '&larr;', 'Member pagination previous text', 'buddypress' ),
+				'next_text' => _x( '&rarr;', 'Member pagination next text', 'buddypress' ),
+				'mid_size'  => 1,
+				'add_args'  => $add_args,
+			) );
+		}
+	}
+
+	/**
+	 * Whether there are members available in the loop.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see bp_has_members()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	function has_members() {
+		if ( $this->member_count )
+			return true;
+
+		return false;
+	}
+
+	/**
+	 * Set up the next member and iterate index.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return object The next member to iterate over.
+	 */
+	function next_member() {
+		$this->current_member++;
+		$this->member = $this->members[$this->current_member];
+
+		return $this->member;
+	}
+
+	/**
+	 * Rewind the members and reset member index.
+	 *
+	 * @since 1.0.0
+	 */
+	function rewind_members() {
+		$this->current_member = -1;
+		if ( $this->member_count > 0 ) {
+			$this->member = $this->members[0];
+		}
+	}
+
+	/**
+	 * Whether there are members left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_members()} as part of the while loop
+	 * that controls iteration inside the members loop, eg:
+	 *     while ( bp_members() ) { ...
+	 *
+	 * @since 1.2.0
+	 *
+	 * @see bp_members()
+	 *
+	 * @return bool True if there are more members to show, otherwise false.
+	 */
+	function members() {
+		if ( $this->current_member + 1 < $this->member_count ) {
+			return true;
+		} elseif ( $this->current_member + 1 == $this->member_count ) {
+
+			/**
+			 * Fires right before the rewinding of members listing.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action('member_loop_end');
+			// Do some cleaning up after the loop.
+			$this->rewind_members();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Set up the current member inside the loop.
+	 *
+	 * Used by {@link bp_the_member()} to set up the current member data
+	 * while looping, so that template tags used during that iteration make
+	 * reference to the current member.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see bp_the_member()
+	 */
+	function the_member() {
+
+		$this->in_the_loop = true;
+		$this->member      = $this->next_member();
+
+		// Loop has just started.
+		if ( 0 == $this->current_member ) {
+
+			/**
+			 * Fires if the current member is the first in the loop.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'member_loop_start' );
+		}
+
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-members-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-members-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-members-widget.php	(revision 12265)
@@ -0,0 +1,255 @@
+<?php
+/**
+ * BuddyPress Members Widget.
+ *
+ * @package BuddyPress
+ * @subpackage MembersWidgets
+ * @since 1.0.3
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Members Widget.
+ *
+ * @since 1.0.3
+ */
+class BP_Core_Members_Widget extends WP_Widget {
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+
+		// Setup widget name & description.
+		$name        = _x( '(BuddyPress) Members', 'widget name', 'buddypress' );
+		$description = __( 'A dynamic list of recently active, popular, and newest members', 'buddypress' );
+
+		// Call WP_Widget constructor.
+		parent::__construct( false, $name, array(
+			'description'                 => $description,
+			'classname'                   => 'widget_bp_core_members_widget buddypress widget',
+			'customize_selective_refresh' => true,
+		) );
+
+		if ( is_customize_preview() || is_active_widget( false, false, $this->id_base ) ) {
+			add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+		}
+	}
+
+	/**
+	 * Enqueue scripts.
+	 *
+	 * @since 2.6.0
+	 */
+	public function enqueue_scripts() {
+		wp_enqueue_script( 'bp-widget-members' );
+	}
+
+	/**
+	 * Display the Members widget.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @see WP_Widget::widget() for description of parameters.
+	 *
+	 * @param array $args     Widget arguments.
+	 * @param array $instance Widget settings, as saved by the user.
+	 */
+	public function widget( $args, $instance ) {
+		global $members_template;
+
+		// Get widget settings.
+		$settings = $this->parse_settings( $instance );
+
+		/**
+		 * Filters the title of the Members widget.
+		 *
+		 * @since 1.8.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $settings The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $settings['title'], $settings, $this->id_base );
+		$title = $settings['link_title'] ? '<a href="' . bp_get_members_directory_permalink() . '">' . $title . '</a>' : $title;
+
+		/**
+		 * Filters the separator of the member widget links.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param string $separator Separator string. Default '|'.
+		 */
+		$separator = apply_filters( 'bp_members_widget_separator', '|' );
+
+		// Output before widget HTMl, title (and maybe content before & after it).
+		echo $args['before_widget'] . $args['before_title'] . $title . $args['after_title'];
+
+		// Setup args for querying members.
+		$members_args = array(
+			'user_id'         => 0,
+			'type'            => $settings['member_default'],
+			'per_page'        => $settings['max_members'],
+			'max'             => $settings['max_members'],
+			'populate_extras' => true,
+			'search_terms'    => false,
+		);
+
+		// Back up the global.
+		$old_members_template = $members_template;
+
+		?>
+
+		<?php if ( bp_has_members( $members_args ) ) : ?>
+
+			<div class="item-options" id="members-list-options">
+				<a href="<?php bp_members_directory_permalink(); ?>" id="newest-members" <?php if ( 'newest' === $settings['member_default'] ) : ?>class="selected"<?php endif; ?>><?php esc_html_e( 'Newest', 'buddypress' ); ?></a>
+				<span class="bp-separator" role="separator"><?php echo esc_html( $separator ); ?></span>
+				<a href="<?php bp_members_directory_permalink(); ?>" id="recently-active-members" <?php if ( 'active' === $settings['member_default'] ) : ?>class="selected"<?php endif; ?>><?php esc_html_e( 'Active', 'buddypress' ); ?></a>
+
+				<?php if ( bp_is_active( 'friends' ) ) : ?>
+					<span class="bp-separator" role="separator"><?php echo esc_html( $separator ); ?></span>
+					<a href="<?php bp_members_directory_permalink(); ?>" id="popular-members" <?php if ( 'popular' === $settings['member_default'] ) : ?>class="selected"<?php endif; ?>><?php esc_html_e( 'Popular', 'buddypress' ); ?></a>
+
+				<?php endif; ?>
+
+			</div>
+
+			<ul id="members-list" class="item-list" aria-live="polite" aria-relevant="all" aria-atomic="true">
+
+				<?php while ( bp_members() ) : bp_the_member(); ?>
+
+					<li class="vcard">
+						<div class="item-avatar">
+							<a href="<?php bp_member_permalink() ?>" class="bp-tooltip" data-bp-tooltip="<?php bp_member_name(); ?>"><?php bp_member_avatar(); ?></a>
+						</div>
+
+						<div class="item">
+							<div class="item-title fn"><a href="<?php bp_member_permalink(); ?>"><?php bp_member_name(); ?></a></div>
+							<div class="item-meta">
+								<?php if ( 'newest' == $settings['member_default'] ) : ?>
+									<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_registered( array( 'relative' => false ) ) ); ?>"><?php bp_member_registered(); ?></span>
+								<?php elseif ( 'active' == $settings['member_default'] ) : ?>
+									<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_last_active( array( 'relative' => false ) ) ); ?>"><?php bp_member_last_active(); ?></span>
+								<?php else : ?>
+									<span class="activity"><?php bp_member_total_friend_count(); ?></span>
+								<?php endif; ?>
+							</div>
+						</div>
+					</li>
+
+				<?php endwhile; ?>
+
+			</ul>
+
+			<?php wp_nonce_field( 'bp_core_widget_members', '_wpnonce-members', false ); ?>
+
+			<input type="hidden" name="members_widget_max" id="members_widget_max" value="<?php echo esc_attr( $settings['max_members'] ); ?>" />
+
+		<?php else: ?>
+
+			<div class="widget-error">
+				<?php esc_html_e( 'No one has signed up yet!', 'buddypress' ); ?>
+			</div>
+
+		<?php endif; ?>
+
+		<?php echo $args['after_widget'];
+
+		// Restore the global.
+		$members_template = $old_members_template;
+	}
+
+	/**
+	 * Update the Members widget options.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $new_instance The new instance options.
+	 * @param array $old_instance The old instance options.
+	 * @return array $instance The parsed options to be saved.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance = $old_instance;
+
+		$instance['title']          = strip_tags( $new_instance['title'] );
+		$instance['max_members']    = strip_tags( $new_instance['max_members'] );
+		$instance['member_default'] = strip_tags( $new_instance['member_default'] );
+		$instance['link_title']	    = (bool) $new_instance['link_title'];
+
+		return $instance;
+	}
+
+	/**
+	 * Output the Members widget options form.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $instance Widget instance settings.
+	 * @return void
+	 */
+	public function form( $instance ) {
+
+		// Get widget settings.
+		$settings       = $this->parse_settings( $instance );
+		$title          = strip_tags( $settings['title'] );
+		$max_members    = strip_tags( $settings['max_members'] );
+		$member_default = strip_tags( $settings['member_default'] );
+		$link_title     = (bool) $settings['link_title']; ?>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'title' ); ?>">
+				<?php esc_html_e( 'Title:', 'buddypress' ); ?>
+				<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" style="width: 100%" />
+			</label>
+		</p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'link_title' ) ?>">
+				<input type="checkbox" name="<?php echo $this->get_field_name( 'link_title' ) ?>" id="<?php echo $this->get_field_id( 'link_title' ) ?>" value="1" <?php checked( $link_title ) ?> />
+				<?php esc_html_e( 'Link widget title to Members directory', 'buddypress' ); ?>
+			</label>
+		</p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'max_members' ); ?>">
+				<?php esc_html_e( 'Max members to show:', 'buddypress' ); ?>
+				<input class="widefat" id="<?php echo $this->get_field_id( 'max_members' ); ?>" name="<?php echo $this->get_field_name( 'max_members' ); ?>" type="text" value="<?php echo esc_attr( $max_members ); ?>" style="width: 30%" />
+			</label>
+		</p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'member_default' ) ?>"><?php esc_html_e( 'Default members to show:', 'buddypress' ); ?></label>
+			<select name="<?php echo $this->get_field_name( 'member_default' ) ?>" id="<?php echo $this->get_field_id( 'member_default' ) ?>">
+				<option value="newest"  <?php if ( 'newest'  === $member_default ) : ?>selected="selected"<?php endif; ?>><?php esc_html_e( 'Newest',  'buddypress' ); ?></option>
+				<option value="active"  <?php if ( 'active'  === $member_default ) : ?>selected="selected"<?php endif; ?>><?php esc_html_e( 'Active',  'buddypress' ); ?></option>
+				<option value="popular" <?php if ( 'popular' === $member_default ) : ?>selected="selected"<?php endif; ?>><?php esc_html_e( 'Popular', 'buddypress' ); ?></option>
+			</select>
+		</p>
+
+	<?php
+	}
+
+	/**
+	 * Merge the widget settings into defaults array.
+	 *
+	 * @since 2.3.0
+	 *
+	 *
+	 * @param array $instance Widget instance settings.
+	 * @return array
+	 */
+	public function parse_settings( $instance = array() ) {
+		return bp_parse_args( $instance, array(
+			'title' 	     => __( 'Members', 'buddypress' ),
+			'max_members' 	 => 5,
+			'member_default' => 'active',
+			'link_title' 	 => false
+		), 'members_widget_settings' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-recently-active-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-recently-active-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-recently-active-widget.php	(revision 12265)
@@ -0,0 +1,173 @@
+<?php
+/**
+ * BuddyPress Members Recently Active widget.
+ *
+ * @package BuddyPress
+ * @subpackage MembersWidgets
+ * @since 1.0.3
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Recently Active Members Widget.
+ *
+ * @since 1.0.3
+ */
+class BP_Core_Recently_Active_Widget extends WP_Widget {
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		$name        = _x( '(BuddyPress) Recently Active Members', 'widget name', 'buddypress' );
+		$description = __( 'Profile photos of recently active members', 'buddypress' );
+		parent::__construct( false, $name, array(
+			'description'                 => $description,
+			'classname'                   => 'widget_bp_core_recently_active_widget buddypress widget',
+			'customize_selective_refresh' => true,
+		) );
+	}
+
+	/**
+	 * Display the Recently Active widget.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @see WP_Widget::widget() for description of parameters.
+	 *
+	 * @param array $args     Widget arguments.
+	 * @param array $instance Widget settings, as saved by the user.
+	 */
+	public function widget( $args, $instance ) {
+		global $members_template;
+
+		// Get widget settings.
+		$settings = $this->parse_settings( $instance );
+
+		/**
+		 * Filters the title of the Recently Active widget.
+		 *
+		 * @since 1.8.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $settings The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $settings['title'], $settings, $this->id_base );
+
+		echo $args['before_widget'];
+		echo $args['before_title'] . $title . $args['after_title'];
+
+		// Setup args for querying members.
+		$members_args = array(
+			'user_id'         => 0,
+			'type'            => 'active',
+			'per_page'        => $settings['max_members'],
+			'max'             => $settings['max_members'],
+			'populate_extras' => true,
+			'search_terms'    => false,
+		);
+
+		// Back up global.
+		$old_members_template = $members_template;
+
+		?>
+
+		<?php if ( bp_has_members( $members_args ) ) : ?>
+
+			<div class="avatar-block">
+
+				<?php while ( bp_members() ) : bp_the_member(); ?>
+
+					<div class="item-avatar">
+						<a href="<?php bp_member_permalink(); ?>" class="bp-tooltip" data-bp-tooltip="<?php bp_member_name(); ?>"><?php bp_member_avatar(); ?></a>
+					</div>
+
+				<?php endwhile; ?>
+
+			</div>
+
+		<?php else: ?>
+
+			<div class="widget-error">
+				<?php esc_html_e( 'There are no recently active members', 'buddypress' ); ?>
+			</div>
+
+		<?php endif; ?>
+
+		<?php echo $args['after_widget'];
+
+		// Restore the global.
+		$members_template = $old_members_template;
+	}
+
+	/**
+	 * Update the Recently Active widget options.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $new_instance The new instance options.
+	 * @param array $old_instance The old instance options.
+	 * @return array $instance The parsed options to be saved.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance                = $old_instance;
+		$instance['title']       = strip_tags( $new_instance['title'] );
+		$instance['max_members'] = strip_tags( $new_instance['max_members'] );
+
+		return $instance;
+	}
+
+	/**
+	 * Output the Recently Active widget options form.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $instance Widget instance settings.
+	 * @return void
+	 */
+	public function form( $instance ) {
+
+		// Get widget settings.
+		$settings    = $this->parse_settings( $instance );
+		$title       = strip_tags( $settings['title'] );
+		$max_members = strip_tags( $settings['max_members'] ); ?>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'title' ); ?>">
+				<?php esc_html_e( 'Title:', 'buddypress' ); ?>
+				<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" style="width: 100%" />
+			</label>
+		</p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'max_members' ); ?>">
+				<?php esc_html_e( 'Max members to show:', 'buddypress' ); ?>
+				<input class="widefat" id="<?php echo $this->get_field_id( 'max_members' ); ?>" name="<?php echo $this->get_field_name( 'max_members' ); ?>" type="text" value="<?php echo esc_attr( $max_members ); ?>" style="width: 30%" />
+			</label>
+		</p>
+
+	<?php
+	}
+
+	/**
+	 * Merge the widget settings into defaults array.
+	 *
+	 * @since 2.3.0
+	 *
+	 *
+	 * @param array $instance Widget instance settings.
+	 * @return array
+	 */
+	public function parse_settings( $instance = array() ) {
+		return bp_parse_args( $instance, array(
+			'title' 	     => __( 'Recently Active Members', 'buddypress' ),
+			'max_members' 	 => 15,
+		), 'recently_active_members_widget_settings' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-whos-online-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-whos-online-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-core-whos-online-widget.php	(revision 12265)
@@ -0,0 +1,172 @@
+<?php
+/**
+ * BuddyPress Members Who's Online Widget.
+ *
+ * @package BuddyPress
+ * @subpackage MembersWidgets
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Who's Online Widget.
+ *
+ * @since 1.0.3
+ */
+class BP_Core_Whos_Online_Widget extends WP_Widget {
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		$name        = _x( "(BuddyPress) Who's Online", 'widget name', 'buddypress' );
+		$description = __( 'Profile photos of online users', 'buddypress' );
+		parent::__construct( false, $name, array(
+			'description'                 => $description,
+			'classname'                   => 'widget_bp_core_whos_online_widget buddypress widget',
+			'customize_selective_refresh' => true,
+		) );
+	}
+
+	/**
+	 * Display the Who's Online widget.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @see WP_Widget::widget() for description of parameters.
+	 *
+	 * @param array $args     Widget arguments.
+	 * @param array $instance Widget settings, as saved by the user.
+	 */
+	public function widget( $args, $instance ) {
+		global $members_template;
+
+		// Get widget settings.
+		$settings = $this->parse_settings( $instance );
+
+		/**
+		 * Filters the title of the Who's Online widget.
+		 *
+		 * @since 1.8.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $settings The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $settings['title'], $settings, $this->id_base );
+
+		echo $args['before_widget'] . $args['before_title'] . $title . $args['after_title'];
+
+		// Setup args for querying members.
+		$members_args = array(
+			'user_id'         => 0,
+			'type'            => 'online',
+			'per_page'        => $settings['max_members'],
+			'max'             => $settings['max_members'],
+			'populate_extras' => true,
+			'search_terms'    => false,
+		);
+
+		// Back up global.
+		$old_members_template = $members_template;
+
+		?>
+
+		<?php if ( bp_has_members( $members_args ) ) : ?>
+
+			<div class="avatar-block">
+
+				<?php while ( bp_members() ) : bp_the_member(); ?>
+
+					<div class="item-avatar">
+						<a href="<?php bp_member_permalink(); ?>" class="bp-tooltip" data-bp-tooltip="<?php bp_member_name(); ?>"><?php bp_member_avatar(); ?></a>
+					</div>
+
+				<?php endwhile; ?>
+
+			</div>
+
+		<?php else: ?>
+
+			<div class="widget-error">
+				<?php esc_html_e( 'There are no users currently online', 'buddypress' ); ?>
+			</div>
+
+		<?php endif; ?>
+
+		<?php echo $args['after_widget'];
+
+		// Restore the global.
+		$members_template = $old_members_template;
+	}
+
+	/**
+	 * Update the Who's Online widget options.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $new_instance The new instance options.
+	 * @param array $old_instance The old instance options.
+	 * @return array $instance The parsed options to be saved.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance                = $old_instance;
+		$instance['title']       = strip_tags( $new_instance['title'] );
+		$instance['max_members'] = strip_tags( $new_instance['max_members'] );
+
+		return $instance;
+	}
+
+	/**
+	 * Output the Who's Online widget options form.
+	 *
+	 * @since 1.0.3
+	 *
+	 * @param array $instance Widget instance settings.
+	 * @return void
+	 */
+	public function form( $instance ) {
+
+		// Get widget settings.
+		$settings    = $this->parse_settings( $instance );
+		$title       = strip_tags( $settings['title'] );
+		$max_members = strip_tags( $settings['max_members'] ); ?>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'title' ); ?>">
+				<?php esc_html_e( 'Title:', 'buddypress' ); ?>
+				<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" style="width: 100%" />
+			</label>
+		</p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'max_members' ); ?>">
+				<?php esc_html_e( 'Max members to show:', 'buddypress' ); ?>
+				<input class="widefat" id="<?php echo $this->get_field_id( 'max_members' ); ?>" name="<?php echo $this->get_field_name( 'max_members' ); ?>" type="text" value="<?php echo esc_attr( $max_members ); ?>" style="width: 30%" />
+			</label>
+		</p>
+
+	<?php
+	}
+
+	/**
+	 * Merge the widget settings into defaults array.
+	 *
+	 * @since 2.3.0
+	 *
+	 *
+	 * @param array $instance Widget instance settings.
+	 * @return array
+	 */
+	public function parse_settings( $instance = array() ) {
+		return bp_parse_args( $instance, array(
+			'title' 	     => __( "Who's Online", 'buddypress' ),
+			'max_members' 	 => 15,
+		), 'members_widget_settings' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-admin.php	(revision 12265)
@@ -0,0 +1,2436 @@
+<?php
+/**
+ * BuddyPress Members Admin
+ *
+ * @package BuddyPress
+ * @subpackage MembersAdmin
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( !class_exists( 'BP_Members_Admin' ) ) :
+
+/**
+ * Load Members admin area.
+ *
+ * @since 2.0.0
+ */
+class BP_Members_Admin {
+
+	/** Directory *************************************************************/
+
+	/**
+	 * Path to the BP Members Admin directory.
+	 *
+	 * @var string $admin_dir
+	 */
+	public $admin_dir = '';
+
+	/** URLs ******************************************************************/
+
+	/**
+	 * URL to the BP Members Admin directory.
+	 *
+	 * @var string $admin_url
+	 */
+	public $admin_url = '';
+
+	/**
+	 * URL to the BP Members Admin CSS directory.
+	 *
+	 * @var string $css_url
+	 */
+	public $css_url = '';
+
+	/**
+	 * URL to the BP Members Admin JS directory.
+	 *
+	 * @var string
+	 */
+	public $js_url = '';
+
+	/** Other *****************************************************************/
+
+	/**
+	 * Screen id for edit user's profile page.
+	 *
+	 * @var string
+	 */
+	public $user_page = '';
+
+	/**
+	 * Setup BP Members Admin.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return BP_Members_Admin
+	 */
+	public static function register_members_admin() {
+		if ( ! is_admin() ) {
+			return;
+		}
+
+		$bp = buddypress();
+
+		if ( empty( $bp->members->admin ) ) {
+			$bp->members->admin = new self;
+		}
+
+		return $bp->members->admin;
+	}
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->setup_actions();
+	}
+
+	/**
+	 * Set admin-related globals.
+	 *
+	 * @since 2.0.0
+	 */
+	private function setup_globals() {
+		$bp = buddypress();
+
+		// Paths and URLs
+		$this->admin_dir = trailingslashit( $bp->plugin_dir  . 'bp-members/admin' ); // Admin path.
+		$this->admin_url = trailingslashit( $bp->plugin_url  . 'bp-members/admin' ); // Admin URL.
+		$this->css_url   = trailingslashit( $this->admin_url . 'css' ); // Admin CSS URL.
+		$this->js_url    = trailingslashit( $this->admin_url . 'js'  ); // Admin CSS URL.
+
+		// Capability depends on config.
+		$this->capability = bp_core_do_network_admin() ? 'manage_network_users' : 'edit_users';
+
+		// The Edit Profile Screen id.
+		$this->user_page = '';
+
+		// The Show Profile Screen id.
+		$this->user_profile = is_network_admin() ? 'users' : 'profile';
+
+		// The current user id.
+		$this->current_user_id = get_current_user_id();
+
+		// The user id being edited.
+		$this->user_id = 0;
+
+		// Is a member editing their own profile.
+		$this->is_self_profile = false;
+
+		// The screen ids to load specific css for.
+		$this->screen_id = array();
+
+		// The stats metabox default position.
+		$this->stats_metabox = new StdClass();
+
+		// BuddyPress edit user's profile args.
+		$this->edit_profile_args = array( 'page' => 'bp-profile-edit' );
+		$this->edit_profile_url  = '';
+		$this->edit_url          = '';
+
+		// Data specific to signups.
+		$this->users_page   = '';
+		$this->signups_page = '';
+		$this->users_url    = bp_get_admin_url( 'users.php' );
+		$this->users_screen = bp_core_do_network_admin() ? 'users-network' : 'users';
+
+		// Specific config: BuddyPress is not network activated.
+		$this->subsite_activated = (bool) is_multisite() && ! bp_is_network_activated();
+
+		// When BuddyPress is not network activated, only Super Admin can moderate signups.
+		if ( ! empty( $this->subsite_activated ) ) {
+			$this->capability = 'manage_network_users';
+		}
+	}
+
+	/**
+	 * Set admin-related actions and filters.
+	 *
+	 * @since 2.0.0
+	 */
+	private function setup_actions() {
+
+		/** Extended Profile *************************************************
+		 */
+
+		// Enqueue all admin JS and CSS.
+		add_action( 'bp_admin_enqueue_scripts', array( $this, 'enqueue_scripts'   )        );
+
+		// Add some page specific output to the <head>.
+		add_action( 'bp_admin_head',            array( $this, 'admin_head'        ), 999   );
+
+		// Add menu item to all users menu.
+		add_action( 'admin_menu',               array( $this, 'admin_menus'       ), 5     );
+		add_action( 'network_admin_menu',       array( $this, 'admin_menus'       ), 5     );
+		add_action( 'user_admin_menu',          array( $this, 'user_profile_menu' ), 5     );
+
+		// Create the Profile Navigation (Profile/Extended Profile).
+		add_action( 'edit_user_profile',        array( $this, 'profile_nav'       ), 99, 1 );
+		add_action( 'show_user_profile',        array( $this, 'profile_nav'       ), 99, 1 );
+
+		// Editing users of a specific site.
+		add_action( "admin_head-site-users.php", array( $this, 'profile_admin_head' ) );
+
+		// Add a row action to users listing.
+		if ( bp_core_do_network_admin() ) {
+			add_filter( 'ms_user_row_actions',        array( $this, 'row_actions'                    ), 10, 2 );
+			add_action( 'admin_init',                 array( $this, 'add_edit_profile_url_filter'    )        );
+			add_action( 'wp_after_admin_bar_render',  array( $this, 'remove_edit_profile_url_filter' )        );
+		}
+
+		// Add user row actions for single site.
+		add_filter( 'user_row_actions', array( $this, 'row_actions' ), 10, 2 );
+
+		// Process changes to member type.
+		add_action( 'bp_members_admin_load', array( $this, 'process_member_type_update' ) );
+
+		/** Signups **********************************************************
+		 */
+
+		if ( is_admin() ) {
+
+			// Filter non multisite user query to remove sign-up users.
+			if ( ! is_multisite() ) {
+				add_action( 'pre_user_query', array( $this, 'remove_signups_from_user_query' ), 10, 1 );
+			}
+
+			// Reorganise the views navigation in users.php and signups page.
+			if ( current_user_can( $this->capability ) ) {
+				$user_screen = $this->users_screen;
+
+				/**
+				 * Users screen on multiblog is users, but signups
+				 * need to be managed in the network for this case
+				 */
+				if ( bp_is_network_activated() && bp_is_multiblog_mode() && false === strpos( $user_screen, '-network' ) ) {
+					$user_screen .= '-network';
+				}
+
+				add_filter( "views_{$user_screen}", array( $this, 'signup_filter_view'    ), 10, 1 );
+				add_filter( 'set-screen-option',    array( $this, 'signup_screen_options' ), 10, 3 );
+			}
+
+			// Registration is turned on.
+			add_action( 'update_site_option_registration',  array( $this, 'multisite_registration_on' ),   10, 2 );
+			add_action( 'update_option_users_can_register', array( $this, 'single_site_registration_on' ), 10, 2 );
+		}
+
+		/** Users List - Members Types ***************************************
+		 */
+
+		if ( is_admin() && bp_get_member_types() ) {
+
+			// Add "Change type" <select> to WP admin users list table and process bulk members type changes.
+			add_action( 'restrict_manage_users', array( $this, 'users_table_output_type_change_select' ) );
+			add_action( 'load-users.php',        array( $this, 'users_table_process_bulk_type_change'  ) );
+
+			// Add the member type column to the WP admin users list table.
+			add_filter( 'manage_users_columns',       array( $this, 'users_table_add_type_column'    )        );
+			add_filter( 'manage_users_custom_column', array( $this, 'users_table_populate_type_cell' ), 10, 3 );
+
+			// Filter WP admin users list table to include users of the specified type.
+			add_filter( 'pre_get_users', array( $this, 'users_table_filter_by_type' ) );
+		}
+	}
+
+	/**
+	 * Create registration pages when multisite user registration is turned on.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $option_name Current option name; value is always 'registration'.
+	 * @param string $value
+	 */
+	public function multisite_registration_on( $option_name, $value ) {
+		if ( 'user' === $value || 'all' === $value ) {
+			bp_core_add_page_mappings( array(
+				'register' => 1,
+				'activate' => 1
+			) );
+		}
+	}
+
+	/**
+	 * Create registration pages when single site registration is turned on.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $old_value
+	 * @param string $value
+	 */
+	public function single_site_registration_on( $old_value, $value ) {
+		// Single site.
+		if ( ! is_multisite() && ! empty( $value ) ) {
+			bp_core_add_page_mappings( array(
+				'register' => 1,
+				'activate' => 1
+			) );
+		}
+	}
+
+	/**
+	 * Get the user ID.
+	 *
+	 * Look for $_GET['user_id']. If anything else, force the user ID to the
+	 * current user's ID so they aren't left without a user to edit.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return int
+	 */
+	private function get_user_id() {
+		if ( ! empty( $this->user_id ) ) {
+			return $this->user_id;
+		}
+
+		$this->user_id = (int) get_current_user_id();
+
+		// We'll need a user ID when not on self profile.
+		if ( ! empty( $_GET['user_id'] ) ) {
+			$this->user_id = (int) $_GET['user_id'];
+		}
+
+		return $this->user_id;
+	}
+
+	/**
+	 * Can the current user edit the one displayed.
+	 *
+	 * Self profile editing / or bp_moderate check.
+	 * This might be replaced by more granular capabilities
+	 * in the future.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param int $user_id ID of the user being checked for edit ability.
+	 *
+	 * @return bool
+	 */
+	private function member_can_edit( $user_id = 0 ) {
+		$retval = false;
+
+		// Bail if no user ID was passed.
+		if ( empty( $user_id ) ) {
+			return $retval;
+		}
+
+		// Member can edit if they are viewing their own profile.
+		if ( $this->current_user_id === $user_id ) {
+			$retval = true;
+
+		// Trust the 'bp_moderate' capability.
+		} else {
+			$retval = bp_current_user_can( 'bp_moderate' );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Get admin notice when saving a user or member profile.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return array
+	 */
+	private function get_user_notice() {
+
+		// Setup empty notice for return value.
+		$notice = array();
+
+		// Updates.
+		if ( ! empty( $_REQUEST['updated'] ) ) {
+			switch ( $_REQUEST['updated'] ) {
+			case 'avatar':
+				$notice = array(
+					'class'   => 'updated',
+					'message' => __( 'Profile photo was deleted.', 'buddypress' )
+				);
+				break;
+			case 'ham' :
+				$notice = array(
+					'class'   => 'updated',
+					'message' => __( 'User removed as spammer.', 'buddypress' )
+				);
+				break;
+			case 'spam' :
+				$notice = array(
+					'class'   => 'updated',
+					'message' => __( 'User marked as spammer. Spam users are visible only to site admins.', 'buddypress' )
+				);
+				break;
+			case 1 :
+				$notice = array(
+					'class'   => 'updated',
+					'message' => __( 'Profile updated.', 'buddypress' )
+				);
+				break;
+			}
+		}
+
+		// Errors.
+		if ( ! empty( $_REQUEST['error'] ) ) {
+			switch ( $_REQUEST['error'] ) {
+			case 'avatar':
+				$notice = array(
+					'class'   => 'error',
+					'message' => __( 'There was a problem deleting that profile photo. Please try again.', 'buddypress' )
+				);
+				break;
+			case 'ham' :
+				$notice = array(
+					'class'   => 'error',
+					'message' => __( 'User could not be removed as spammer.', 'buddypress' )
+				);
+				break;
+			case 'spam' :
+				$notice = array(
+					'class'   => 'error',
+					'message' => __( 'User could not be marked as spammer.', 'buddypress' )
+				);
+				break;
+			case 1 :
+				$notice = array(
+					'class'   => 'error',
+					'message' => __( 'An error occurred while trying to update the profile.', 'buddypress' )
+				);
+				break;
+			case 2:
+				$notice = array(
+					'class'   => 'error',
+					'message' => __( 'Your changes have not been saved. Please fill in all required fields, and save your changes again.', 'buddypress' )
+				);
+				break;
+			case 3:
+				$notice = array(
+					'class'   => 'error',
+					'message' => __( 'There was a problem updating some of your profile information. Please try again.', 'buddypress' )
+				);
+				break;
+			}
+		}
+
+		return $notice;
+	}
+
+	/**
+	 * Create the /user/ admin Profile submenus for all members.
+	 *
+	 * @since 2.1.0
+	 *
+	 */
+	public function user_profile_menu() {
+
+		// Setup the hooks array.
+		$hooks = array();
+
+		// Add the faux "Edit Profile" submenu page.
+		$hooks['user'] = $this->user_page = add_submenu_page(
+			'profile.php',
+			__( 'Edit Profile',  'buddypress' ),
+			__( 'Edit Profile',  'buddypress' ),
+			'exist',
+			'bp-profile-edit',
+			array( $this, 'user_admin' )
+		);
+
+		// Setup the screen ID's.
+		$this->screen_id = array(
+			$this->user_page    . '-user',
+			$this->user_profile . '-user'
+		);
+
+		// Loop through new hooks and add method actions.
+		foreach ( $hooks as $key => $hook ) {
+			add_action( "load-{$hook}", array( $this, $key . '_admin_load' ) );
+		}
+
+		// Add the profile_admin_head method to proper admin_head actions.
+		add_action( "admin_head-{$this->user_page}", array( $this, 'profile_admin_head' ) );
+		add_action( "admin_head-profile.php",        array( $this, 'profile_admin_head' ) );
+	}
+
+	/**
+	 * Create the All Users / Profile > Edit Profile and All Users Signups submenus.
+	 *
+	 * @since 2.0.0
+	 *
+	 */
+	public function admin_menus() {
+
+		// Setup the hooks array.
+		$hooks = array();
+
+		// Manage user's profile.
+		$hooks['user'] = $this->user_page = add_submenu_page(
+			$this->user_profile . '.php',
+			__( 'Edit Profile',  'buddypress' ),
+			__( 'Edit Profile',  'buddypress' ),
+			'read',
+			'bp-profile-edit',
+			array( $this, 'user_admin' )
+		);
+
+		// Only show sign-ups where they belong.
+		if ( ( ! bp_is_network_activated() && ! is_network_admin() ) || ( is_network_admin() && bp_is_network_activated() ) ) {
+
+			// Manage signups.
+			$hooks['signups'] = $this->signups_page = add_users_page(
+				__( 'Manage Signups',  'buddypress' ),
+				__( 'Manage Signups',  'buddypress' ),
+				$this->capability,
+				'bp-signups',
+				array( $this, 'signups_admin' )
+			);
+		}
+
+		$edit_page         = 'user-edit';
+		$profile_page      = 'profile';
+		$this->users_page  = 'users';
+
+		// Self profile check is needed for this pages.
+		$page_head = array(
+			$edit_page        . '.php',
+			$profile_page     . '.php',
+			$this->user_page,
+			$this->users_page . '.php',
+		);
+
+		// Append '-network' to each array item if in network admin.
+		if ( is_network_admin() ) {
+			$edit_page          .= '-network';
+			$profile_page       .= '-network';
+			$this->user_page    .= '-network';
+			$this->users_page   .= '-network';
+			$this->signups_page .= '-network';
+		}
+
+		// Setup the screen ID's.
+		$this->screen_id = array(
+			$edit_page,
+			$this->user_page,
+			$profile_page
+		);
+
+		// Loop through new hooks and add method actions.
+		foreach ( $hooks as $key => $hook ) {
+			add_action( "load-{$hook}", array( $this, $key . '_admin_load' ) );
+		}
+
+		// Add the profile_admin_head method to proper admin_head actions.
+		foreach ( $page_head as $head ) {
+			add_action( "admin_head-{$head}", array( $this, 'profile_admin_head' ) );
+		}
+	}
+
+	/**
+	 * Highlight the Users menu if on Edit Profile and check if on the user's admin profile.
+	 *
+	 * @since 2.1.0
+	 */
+	public function profile_admin_head() {
+		global $submenu_file, $parent_file;
+
+		// Is the user editing their own profile?
+		if ( is_user_admin() || ( defined( 'IS_PROFILE_PAGE' ) && IS_PROFILE_PAGE ) ) {
+			$this->is_self_profile = true;
+
+		// Is the user attempting to edit their own profile.
+		} elseif ( isset( $_GET['user_id' ] ) || ( isset( $_GET['page'] ) && ( 'bp-profile-edit' === $_GET['page'] ) ) ) {
+			$this->is_self_profile = (bool) ( $this->get_user_id() === $this->current_user_id );
+		}
+
+		// Force the parent file to users.php to open the correct top level menu
+		// but only if not editing a site via the network site editing page.
+		if ( 'sites.php' !== $parent_file ) {
+			$parent_file  = 'users.php';
+			$submenu_file = 'users.php';
+		}
+
+		// Editing your own profile, so recheck some vars.
+		if ( true === $this->is_self_profile ) {
+
+			// Use profile.php as the edit page.
+			$edit_page = 'profile.php';
+
+			// Set profile.php as the parent & sub files to correct the menu nav.
+			if ( is_blog_admin() || is_user_admin() ) {
+				$parent_file  = 'profile.php';
+				$submenu_file = 'profile.php';
+			}
+
+		// Not editing yourself, so use user-edit.php.
+		} else {
+			$edit_page = 'user-edit.php';
+		}
+
+		if ( is_user_admin() ) {
+			$this->edit_profile_url = add_query_arg( $this->edit_profile_args, user_admin_url( 'profile.php' ) );
+			$this->edit_url         = user_admin_url( 'profile.php' );
+
+		} elseif ( is_blog_admin() ) {
+			$this->edit_profile_url = add_query_arg( $this->edit_profile_args, admin_url( 'users.php' ) );
+			$this->edit_url         = admin_url( $edit_page );
+
+		} elseif ( is_network_admin() ) {
+			$this->edit_profile_url = add_query_arg( $this->edit_profile_args, network_admin_url( 'users.php' ) );
+			$this->edit_url         = network_admin_url( $edit_page );
+		}
+	}
+
+	/**
+	 * Remove the Edit Profile page.
+	 *
+	 * We add these pages in order to integrate with WP's Users panel, but
+	 * we want them to show up as a row action of the WP panel, not as separate
+	 * subnav items under the Users menu.
+	 *
+	 * @since 2.0.0
+	 */
+	public function admin_head() {
+		remove_submenu_page( 'users.php',   'bp-profile-edit' );
+		remove_submenu_page( 'profile.php', 'bp-profile-edit' );
+	}
+
+	/** Community Profile *****************************************************/
+
+	/**
+	 * Add some specific styling to the Edit User and Edit User's Profile page.
+	 *
+	 * @since 2.0.0
+	 */
+	public function enqueue_scripts() {
+		if ( ! in_array( get_current_screen()->id, $this->screen_id ) ) {
+			return;
+		}
+
+		$min = bp_core_get_minified_asset_suffix();
+		$css = $this->css_url . "admin{$min}.css";
+
+		/**
+		 * Filters the CSS URL to enqueue in the Members admin area.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $css URL to the CSS admin file to load.
+		 */
+		$css = apply_filters( 'bp_members_admin_css', $css );
+
+		wp_enqueue_style( 'bp-members-css', $css, array(), bp_get_version() );
+
+		wp_style_add_data( 'bp-members-css', 'rtl', true );
+		if ( $min ) {
+			wp_style_add_data( 'bp-members-css', 'suffix', $min );
+		}
+
+		// Only load JavaScript for BuddyPress profile.
+		if ( get_current_screen()->id == $this->user_page ) {
+			$js = $this->js_url . "admin{$min}.js";
+
+			/**
+			 * Filters the JS URL to enqueue in the Members admin area.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param string $js URL to the JavaScript admin file to load.
+			 */
+			$js = apply_filters( 'bp_members_admin_js', $js );
+			wp_enqueue_script( 'bp-members-js', $js, array( 'jquery' ), bp_get_version(), true );
+		}
+
+		/**
+		 * Fires after all of the members JavaScript and CSS are enqueued.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $id        ID of the current screen.
+		 * @param array  $screen_id Array of allowed screens to add scripts and styles to.
+		 */
+		do_action( 'bp_members_admin_enqueue_scripts', get_current_screen()->id, $this->screen_id );
+	}
+
+	/**
+	 * Create the Profile navigation in Edit User & Edit Profile pages.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $user   User to create profile navigation for.
+	 * @param string      $active Which profile to highlight.
+	 * @return string|null
+	 */
+	public function profile_nav( $user = null, $active = 'WordPress' ) {
+
+		// Bail if no user ID exists here.
+		if ( empty( $user->ID ) ) {
+			return;
+		}
+
+		// Add the user ID to query arguments when not editing yourself.
+		if ( false === $this->is_self_profile ) {
+			$query_args = array( 'user_id' => $user->ID );
+		} else {
+			$query_args = array();
+		}
+
+		// Conditionally add a referer if it exists in the existing request.
+		if ( ! empty( $_REQUEST['wp_http_referer'] ) ) {
+			$wp_http_referer = wp_unslash( $_REQUEST['wp_http_referer'] );
+			$wp_http_referer = wp_validate_redirect( esc_url_raw( $wp_http_referer ) );
+			$query_args['wp_http_referer'] = urlencode( $wp_http_referer );
+		}
+
+		// Setup the two distinct "edit" URL's.
+		$community_url = add_query_arg( $query_args, $this->edit_profile_url );
+		$wordpress_url = add_query_arg( $query_args, $this->edit_url         );
+
+		$bp_active = false;
+		$wp_active = ' nav-tab-active';
+		if ( 'BuddyPress' === $active ) {
+			$bp_active = ' nav-tab-active';
+			$wp_active = false;
+		} ?>
+
+		<h2 id="profile-nav" class="nav-tab-wrapper">
+			<?php
+			/**
+			 * In configs where BuddyPress is not network activated, as regular
+			 * admins do not have the capacity to edit other users, we must add
+			 * this check.
+			 */
+			if ( current_user_can( 'edit_user', $user->ID ) ) : ?>
+
+				<a class="nav-tab<?php echo esc_attr( $wp_active ); ?>" href="<?php echo esc_url( $wordpress_url );?>"><?php _e( 'Profile', 'buddypress' ); ?></a>
+
+			<?php endif; ?>
+
+			<a class="nav-tab<?php echo esc_attr( $bp_active ); ?>" href="<?php echo esc_url( $community_url );?>"><?php _e( 'Extended Profile', 'buddypress' ); ?></a>
+		</h2>
+
+		<?php
+	}
+
+	/**
+	 * Set up the user's profile admin page.
+	 *
+	 * Loaded before the page is rendered, this function does all initial
+	 * setup, including: processing form requests, registering contextual
+	 * help, and setting up screen options.
+	 *
+	 * @since 2.0.0
+	 */
+	public function user_admin_load() {
+
+		// Get the user ID.
+		$user_id = $this->get_user_id();
+
+		// Can current user edit this profile?
+		if ( ! $this->member_can_edit( $user_id ) ) {
+			wp_die( __( 'You cannot edit the requested user.', 'buddypress' ) );
+		}
+
+		// Build redirection URL.
+		$redirect_to = remove_query_arg( array( 'action', 'error', 'updated', 'spam', 'ham', 'delete_avatar' ), $_SERVER['REQUEST_URI'] );
+		$doaction    = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : false;
+
+		if ( ! empty( $_REQUEST['user_status'] ) ) {
+			$spam = (bool) ( 'spam' === $_REQUEST['user_status'] );
+
+			if ( $spam !== bp_is_user_spammer( $user_id ) ) {
+				$doaction = $_REQUEST['user_status'];
+			}
+		}
+
+		/**
+		 * Fires at the start of the signups admin load.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $doaction Current bulk action being processed.
+		 * @param array  $_REQUEST Current $_REQUEST global.
+		 */
+		do_action_ref_array( 'bp_members_admin_load', array( $doaction, $_REQUEST ) );
+
+		/**
+		 * Filters the allowed actions for use in the user admin page.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $value Array of allowed actions to use.
+		 */
+		$allowed_actions = apply_filters( 'bp_members_admin_allowed_actions', array( 'update', 'delete_avatar', 'spam', 'ham' ) );
+
+		// Prepare the display of the Community Profile screen.
+		if ( ! in_array( $doaction, $allowed_actions ) ) {
+			add_screen_option( 'layout_columns', array( 'default' => 2, 'max' => 2, ) );
+
+			get_current_screen()->add_help_tab( array(
+				'id'      => 'bp-profile-edit-overview',
+				'title'   => __( 'Overview', 'buddypress' ),
+				'content' =>
+				'<p>' . __( 'This is the admin view of a user&#39;s profile.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'In the main column, you can edit the fields of the user&#39;s extended profile.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'In the right-hand column, you can update the user&#39;s status, delete the user&#39;s avatar, and view recent statistics.', 'buddypress' ) . '</p>'
+			) );
+
+			// Help panel - sidebar links.
+			get_current_screen()->set_help_sidebar(
+				'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+				'<p>' . __( '<a href="https://codex.buddypress.org/administrator-guide/extended-profiles/">Managing Profiles</a>', 'buddypress' ) . '</p>' .
+				'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+			);
+
+			// Register metaboxes for the edit screen.
+			add_meta_box(
+				'submitdiv',
+				_x( 'Status', 'members user-admin edit screen', 'buddypress' ),
+				array( $this, 'user_admin_status_metabox' ),
+				get_current_screen()->id,
+				'side',
+				'core'
+			);
+
+			// In case xprofile is not active.
+			$this->stats_metabox->context  = 'normal';
+			$this->stats_metabox->priority = 'core';
+
+			/**
+			 * Fires before loading the profile fields if component is active.
+			 *
+			 * Plugins should not use this hook, please use 'bp_members_admin_user_metaboxes' instead.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param int    $user_id       Current user ID for the screen.
+			 * @param string $id            Current screen ID.
+			 * @param object $stats_metabox Object holding position data for use with the stats metabox.
+			 */
+			do_action_ref_array( 'bp_members_admin_xprofile_metabox', array( $user_id, get_current_screen()->id, $this->stats_metabox ) );
+
+			// If xProfile is inactive, difficult to know what's profile we're on.
+			if ( 'normal' === $this->stats_metabox->context ) {
+				$display_name = bp_core_get_user_displayname( $user_id );
+			} else {
+				$display_name = __( 'Member', 'buddypress' );
+			}
+
+			// User Stat metabox.
+			add_meta_box(
+				'bp_members_admin_user_stats',
+				sprintf( _x( "%s's Stats", 'members user-admin edit screen', 'buddypress' ), $display_name ),
+				array( $this, 'user_admin_stats_metabox' ),
+				get_current_screen()->id,
+				sanitize_key( $this->stats_metabox->context ),
+				sanitize_key( $this->stats_metabox->priority )
+			);
+
+			// Member Type metabox. Only added if member types have been registered.
+			$member_types = bp_get_member_types();
+			if ( ! empty( $member_types ) ) {
+				add_meta_box(
+					'bp_members_admin_member_type',
+					_x( 'Member Type', 'members user-admin edit screen', 'buddypress' ),
+					array( $this, 'user_admin_member_type_metabox' ),
+					get_current_screen()->id,
+					'side',
+					'core'
+				);
+			}
+
+			/**
+			 * Fires at the end of the Community Profile screen.
+			 *
+			 * Plugins can restrict metabox to "bp_moderate" admins by checking if
+			 * the first argument ($this->is_self_profile) is false in their callback.
+			 * They can also restrict their metabox to self profile editing
+			 * by setting it to true.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param bool $is_self_profile Whether or not it is the current user's profile.
+			 * @param int  $user_id         Current user ID.
+			 */
+			do_action( 'bp_members_admin_user_metaboxes', $this->is_self_profile, $user_id );
+
+			// Enqueue JavaScript files.
+			wp_enqueue_script( 'postbox'   );
+			wp_enqueue_script( 'dashboard' );
+
+		// Spam or Ham user.
+		} elseif ( in_array( $doaction, array( 'spam', 'ham' ) ) && empty( $this->is_self_profile ) ) {
+
+			check_admin_referer( 'edit-bp-profile_' . $user_id );
+
+			if ( bp_core_process_spammer_status( $user_id, $doaction ) ) {
+				$redirect_to = add_query_arg( 'updated', $doaction, $redirect_to );
+			} else {
+				$redirect_to = add_query_arg( 'error', $doaction, $redirect_to );
+			}
+
+			bp_core_redirect( $redirect_to );
+
+		// Update other stuff once above ones are done.
+		} else {
+			$this->redirect = $redirect_to;
+
+			/**
+			 * Fires at end of user profile admin load if doaction does not match any available actions.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param string $doaction Current bulk action being processed.
+			 * @param int    $user_id  Current user ID.
+			 * @param array  $_REQUEST Current $_REQUEST global.
+			 * @param string $redirect Determined redirect url to send user to.
+			 */
+			do_action_ref_array( 'bp_members_admin_update_user', array( $doaction, $user_id, $_REQUEST, $this->redirect ) );
+
+			bp_core_redirect( $this->redirect );
+		}
+	}
+
+	/**
+	 * Display the user's profile.
+	 *
+	 * @since 2.0.0
+	 */
+	public function user_admin() {
+
+		if ( ! bp_current_user_can( 'bp_moderate' ) && empty( $this->is_self_profile ) ) {
+			die( '-1' );
+		}
+
+		// Get the user ID.
+		$user_id = $this->get_user_id();
+		$user    = get_user_to_edit( $user_id );
+
+		// Construct title.
+		if ( true === $this->is_self_profile ) {
+			$title = __( 'Profile',   'buddypress' );
+		} else {
+			$title = __( 'Edit User', 'buddypress' );
+		}
+
+		// Construct URL for form.
+		$request_url     = remove_query_arg( array( 'action', 'error', 'updated', 'spam', 'ham' ), $_SERVER['REQUEST_URI'] );
+		$form_action_url = add_query_arg( 'action', 'update', $request_url );
+		$wp_http_referer = false;
+		if ( ! empty( $_REQUEST['wp_http_referer'] ) ) {
+			$wp_http_referer = wp_unslash( $_REQUEST['wp_http_referer'] );
+			$wp_http_referer = remove_query_arg( array( 'action', 'updated' ), $wp_http_referer );
+			$wp_http_referer = wp_validate_redirect( esc_url_raw( $wp_http_referer ) );
+		}
+
+		// Prepare notice for admin.
+		$notice = $this->get_user_notice();
+
+		if ( ! empty( $notice ) ) : ?>
+
+			<div <?php if ( 'updated' === $notice['class'] ) : ?>id="message" <?php endif; ?>class="<?php echo esc_attr( $notice['class'] ); ?>">
+
+				<p><?php echo esc_html( $notice['message'] ); ?></p>
+
+				<?php if ( !empty( $wp_http_referer ) && ( 'updated' === $notice['class'] ) ) : ?>
+
+					<p><a href="<?php echo esc_url( $wp_http_referer ); ?>"><?php esc_html_e( '&larr; Back to Users', 'buddypress' ); ?></a></p>
+
+				<?php endif; ?>
+
+			</div>
+
+		<?php endif; ?>
+
+		<div class="wrap" id="community-profile-page">
+			<?php if ( version_compare( $GLOBALS['wp_version'], '4.8', '>=' ) ) : ?>
+
+				<h1 class="wp-heading-inline"><?php echo esc_html( $title ); ?></h1>
+
+				<?php if ( empty( $this->is_self_profile ) ) : ?>
+
+					<?php if ( current_user_can( 'create_users' ) ) : ?>
+
+						<a href="user-new.php" class="page-title-action"><?php echo esc_html_x( 'Add New', 'user', 'buddypress' ); ?></a>
+
+					<?php elseif ( is_multisite() && current_user_can( 'promote_users' ) ) : ?>
+
+						<a href="user-new.php" class="page-title-action"><?php echo esc_html_x( 'Add Existing', 'user', 'buddypress' ); ?></a>
+
+					<?php endif; ?>
+
+				<?php endif; ?>
+
+				<hr class="wp-header-end">
+
+			<?php else : ?>
+
+				<h1><?php echo esc_html( $title ); ?>
+
+					<?php if ( empty( $this->is_self_profile ) ) : ?>
+
+						<?php if ( current_user_can( 'create_users' ) ) : ?>
+
+							<a href="user-new.php" class="add-new-h2"><?php echo esc_html_x( 'Add New', 'user', 'buddypress' ); ?></a>
+
+						<?php elseif ( is_multisite() && current_user_can( 'promote_users' ) ) : ?>
+
+							<a href="user-new.php" class="add-new-h2"><?php echo esc_html_x( 'Add Existing', 'user', 'buddypress' ); ?></a>
+
+						<?php endif; ?>
+
+					<?php endif; ?>
+				</h1>
+
+			<?php endif; ?>
+
+			<?php if ( ! empty( $user ) ) :
+
+				$this->profile_nav( $user, 'BuddyPress' ); ?>
+
+				<form action="<?php echo esc_url( $form_action_url ); ?>" id="your-profile" method="post">
+					<div id="poststuff">
+
+						<div id="post-body" class="metabox-holder columns-<?php echo 1 == get_current_screen()->get_columns() ? '1' : '2'; ?>">
+
+							<div id="postbox-container-1" class="postbox-container">
+								<?php do_meta_boxes( get_current_screen()->id, 'side', $user ); ?>
+							</div>
+
+							<div id="postbox-container-2" class="postbox-container">
+								<?php do_meta_boxes( get_current_screen()->id, 'normal',   $user ); ?>
+								<?php do_meta_boxes( get_current_screen()->id, 'advanced', $user ); ?>
+							</div>
+						</div><!-- #post-body -->
+
+					</div><!-- #poststuff -->
+
+					<?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
+					<?php wp_nonce_field( 'meta-box-order',  'meta-box-order-nonce', false ); ?>
+					<?php wp_nonce_field( 'edit-bp-profile_' . $user->ID ); ?>
+
+				</form>
+
+			<?php else : ?>
+
+				<p><?php
+					printf(
+						'%1$s <a href="%2$s">%3$s</a>',
+						__( 'No user found with this ID.', 'buddypress' ),
+						esc_url( bp_get_admin_url( 'users.php' ) ),
+						__( 'Go back and try again.', 'buddypress' )
+					);
+				?></p>
+
+			<?php endif; ?>
+
+		</div><!-- .wrap -->
+		<?php
+	}
+
+	/**
+	 * Render the Status metabox for user's profile screen.
+	 *
+	 * Actions are:
+	 * - Update profile fields if xProfile component is active
+	 * - Spam/Unspam user
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User|null $user The WP_User object to be edited.
+	 */
+	public function user_admin_status_metabox( $user = null ) {
+
+		// Bail if no user id or if the user has not activated their account yet.
+		if ( empty( $user->ID ) ) {
+			return;
+		}
+
+		// Bail if user has not been activated yet (how did you get here?).
+		if ( isset( $user->user_status ) && ( 2 == $user->user_status ) ) : ?>
+
+			<p class="not-activated"><?php esc_html_e( 'User account has not yet been activated', 'buddypress' ); ?></p><br/>
+
+			<?php return;
+
+		endif; ?>
+
+		<div class="submitbox" id="submitcomment">
+			<div id="minor-publishing">
+				<div id="misc-publishing-actions">
+					<?php
+
+					// Get the spam status once here to compare against below.
+					$is_spammer = bp_is_user_spammer( $user->ID );
+
+					/**
+					 * In configs where BuddyPress is not network activated,
+					 * regular admins cannot mark a user as a spammer on front
+					 * end. This prevent them to do it in the back end.
+					 *
+					 * Also prevent admins from marking themselves or other
+					 * admins as spammers.
+					 */
+					if ( ( empty( $this->is_self_profile ) && ( ! in_array( $user->user_login, get_super_admins() ) ) && empty( $this->subsite_activated ) ) || ( ! empty( $this->subsite_activated ) && current_user_can( 'manage_network_users' ) ) ) : ?>
+
+						<div class="misc-pub-section" id="comment-status-radio">
+							<label class="approved"><input type="radio" name="user_status" value="ham" <?php checked( $is_spammer, false ); ?>><?php esc_html_e( 'Active', 'buddypress' ); ?></label><br />
+							<label class="spam"><input type="radio" name="user_status" value="spam" <?php checked( $is_spammer, true ); ?>><?php esc_html_e( 'Spammer', 'buddypress' ); ?></label>
+						</div>
+
+					<?php endif ;?>
+
+					<div class="misc-pub-section curtime misc-pub-section-last">
+						<?php
+
+						// Translators: Publish box date format, see http://php.net/date.
+						$datef = __( 'M j, Y @ G:i', 'buddypress' );
+						$date  = date_i18n( $datef, strtotime( $user->user_registered ) );
+						?>
+						<span id="timestamp"><?php printf( __( 'Registered on: %s', 'buddypress' ), '<strong>' . $date . '</strong>' ); ?></span>
+					</div>
+				</div> <!-- #misc-publishing-actions -->
+
+				<div class="clear"></div>
+			</div><!-- #minor-publishing -->
+
+			<div id="major-publishing-actions">
+
+				<div id="publishing-action">
+					<a class="button bp-view-profile" href="<?php echo esc_url( bp_core_get_user_domain( $user->ID ) ); ?>" target="_blank"><?php esc_html_e( 'View Profile', 'buddypress' ); ?></a>
+					<?php submit_button( esc_html__( 'Update Profile', 'buddypress' ), 'primary', 'save', false ); ?>
+				</div>
+				<div class="clear"></div>
+			</div><!-- #major-publishing-actions -->
+
+		</div><!-- #submitcomment -->
+
+		<?php
+	}
+
+	/**
+	 * Render the fallback metabox in case a user has been marked as a spammer.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User|null $user The WP_User object to be edited.
+	 */
+	public function user_admin_spammer_metabox( $user = null ) {
+	?>
+		<p><?php printf( __( '%s has been marked as a spammer. All BuddyPress data associated with the user has been removed', 'buddypress' ), esc_html( bp_core_get_user_displayname( $user->ID ) ) ) ;?></p>
+	<?php
+	}
+
+	/**
+	 * Render the Stats metabox to moderate inappropriate images.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User|null $user The WP_User object to be edited.
+	 */
+	public function user_admin_stats_metabox( $user = null ) {
+
+		// Bail if no user ID.
+		if ( empty( $user->ID ) ) {
+			return;
+		}
+
+		// If account is not activated last activity is the time user registered.
+		if ( isset( $user->user_status ) && 2 == $user->user_status ) {
+			$last_active = $user->user_registered;
+
+		// Account is activated, getting user's last activity.
+		} else {
+			$last_active = bp_get_user_last_activity( $user->ID );
+		}
+
+		$datef = __( 'M j, Y @ G:i', 'buddypress' );
+		$date  = date_i18n( $datef, strtotime( $last_active ) ); ?>
+
+		<ul>
+			<li class="bp-members-profile-stats"><?php printf( __( 'Last active: %1$s', 'buddypress' ), '<strong>' . $date . '</strong>' ); ?></li>
+
+			<?php
+			// Loading other stats only if user has activated their account.
+			if ( empty( $user->user_status ) ) {
+
+				/**
+				 * Fires in the user stats metabox if the user has activated their account.
+				 *
+				 * @since 2.0.0
+				 *
+				 * @param array  $value Array holding the user ID.
+				 * @param object $user  Current displayed user object.
+				 */
+				do_action( 'bp_members_admin_user_stats', array( 'user_id' => $user->ID ), $user );
+			}
+			?>
+		</ul>
+
+		<?php
+	}
+
+	/**
+	 * Render the Member Type metabox.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param WP_User|null $user The WP_User object to be edited.
+	 */
+	public function user_admin_member_type_metabox( $user = null ) {
+
+		// Bail if no user ID.
+		if ( empty( $user->ID ) ) {
+			return;
+		}
+
+		$types = bp_get_member_types( array(), 'objects' );
+		$current_type = bp_get_member_type( $user->ID );
+		?>
+
+		<label for="bp-members-profile-member-type" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Select member type', 'buddypress' );
+		?></label>
+		<select name="bp-members-profile-member-type" id="bp-members-profile-member-type">
+			<option value="" <?php selected( '', $current_type ); ?>><?php
+				/* translators: no option picked in select box */
+				esc_attr_e( '----', 'buddypress' );
+			?></option>
+			<?php foreach ( $types as $type ) : ?>
+				<option value="<?php echo esc_attr( $type->name ) ?>" <?php selected( $type->name, $current_type ) ?>><?php echo esc_html( $type->labels['singular_name'] ) ?></option>
+			<?php endforeach; ?>
+		</select>
+
+		<?php
+
+		wp_nonce_field( 'bp-member-type-change-' . $user->ID, 'bp-member-type-nonce' );
+	}
+
+	/**
+	 * Process changes from the Member Type metabox.
+	 *
+	 * @since 2.2.0
+	 */
+	public function process_member_type_update() {
+		if ( ! isset( $_POST['bp-member-type-nonce'] ) || ! isset( $_POST['bp-members-profile-member-type'] ) ) {
+			return;
+		}
+
+		$user_id = $this->get_user_id();
+
+		check_admin_referer( 'bp-member-type-change-' . $user_id, 'bp-member-type-nonce' );
+
+		// Permission check.
+		if ( ! bp_current_user_can( 'bp_moderate' ) && $user_id != bp_loggedin_user_id() ) {
+			return;
+		}
+
+		// Member type string must either reference a valid member type, or be empty.
+		$member_type = stripslashes( $_POST['bp-members-profile-member-type'] );
+		if ( ! empty( $member_type ) && ! bp_get_member_type_object( $member_type ) ) {
+			return;
+		}
+
+		/*
+		 * If an invalid member type is passed, someone's doing something
+		 * fishy with the POST request, so we can fail silently.
+		 */
+		if ( bp_set_member_type( $user_id, $member_type ) ) {
+			// @todo Success messages can't be posted because other stuff happens on the page load.
+		}
+	}
+
+	/**
+	 * Add a link to Profile in Users listing row actions.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array|string $actions WordPress row actions (edit, delete).
+	 * @param object|null  $user    The object for the user row.
+	 * @return null|string|array Merged actions.
+	 */
+	public function row_actions( $actions = '', $user = null ) {
+
+		// Bail if no user ID.
+		if ( empty( $user->ID ) ) {
+			return;
+		}
+
+		// Setup args array.
+		$args = array();
+
+		// Add the user ID if it's not for the current user.
+		if ( $user->ID !== $this->current_user_id ) {
+			$args['user_id'] = $user->ID;
+		}
+
+		// Add the referer.
+		$wp_http_referer = wp_unslash( $_SERVER['REQUEST_URI'] );
+		$wp_http_referer = wp_validate_redirect( esc_url_raw( $wp_http_referer ) );
+		$args['wp_http_referer'] = urlencode( $wp_http_referer );
+
+		// Add the "Extended" link if the current user can edit this user.
+		if ( current_user_can( 'edit_user', $user->ID ) || bp_current_user_can( 'bp_moderate' ) ) {
+
+			// Add query args and setup the Extended link.
+			$edit_profile      = add_query_arg( $args, $this->edit_profile_url );
+			$edit_profile_link = sprintf( '<a href="%1$s">%2$s</a>',  esc_url( $edit_profile ), esc_html__( 'Extended', 'buddypress' ) );
+
+			/**
+			 * Check the edit action is available
+			 * and preserve the order edit | profile | remove/delete.
+			 */
+			if ( ! empty( $actions['edit'] ) ) {
+				$edit_action = $actions['edit'];
+				unset( $actions['edit'] );
+
+				$new_edit_actions = array(
+					'edit'         => $edit_action,
+					'edit-profile' => $edit_profile_link,
+				);
+
+			// If not available simply add the edit profile action.
+			} else {
+				$new_edit_actions = array( 'edit-profile' => $edit_profile_link );
+			}
+
+			$actions = array_merge( $new_edit_actions, $actions );
+		}
+
+		return $actions;
+	}
+
+	/**
+	 * Add a filter to edit profile url in WP Admin Bar.
+	 *
+	 * @since 2.1.0
+	 */
+	public function add_edit_profile_url_filter() {
+		add_filter( 'bp_members_edit_profile_url', array( $this, 'filter_adminbar_profile_link' ), 10, 3 );
+	}
+
+	/**
+	 * Filter the profile url.
+	 *
+	 * @since 2.1.0
+	 *
+	 *
+	 * @param string $profile_link Profile Link for admin bar.
+	 * @param string $url          Profile URL.
+	 * @param int    $user_id      User ID.
+	 * @return string
+	 */
+	public function filter_adminbar_profile_link( $profile_link = '', $url = '', $user_id = 0 ) {
+		if ( ! is_super_admin( $user_id ) && is_admin() ) {
+			$profile_link = user_admin_url( 'profile.php' );
+		}
+		return $profile_link;
+	}
+
+	/**
+	 * Remove the filter to edit profile url in WP Admin Bar.
+	 *
+	 * @since 2.1.0
+	 */
+	public function remove_edit_profile_url_filter() {
+		remove_filter( 'bp_members_edit_profile_url', array( $this, 'filter_adminbar_profile_link' ), 10 );
+	}
+
+	/** Signups Management ****************************************************/
+
+	/**
+	 * Display the admin preferences about signups pagination.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int    $value     Value for signup option.
+	 * @param string $option    Value for the option key.
+	 * @param int    $new_value Value for the saved option.
+	 * @return int The pagination preferences.
+	 */
+	public function signup_screen_options( $value = 0, $option = '', $new_value = 0 ) {
+		if ( 'users_page_bp_signups_network_per_page' != $option && 'users_page_bp_signups_per_page' != $option ) {
+			return $value;
+		}
+
+		// Per page.
+		$new_value = (int) $new_value;
+		if ( $new_value < 1 || $new_value > 999 ) {
+			return $value;
+		}
+
+		return $new_value;
+	}
+
+	/**
+	 * Make sure no signups will show in users list.
+	 *
+	 * This is needed to handle signups that may have not been activated
+	 * before the 2.0.0 upgrade.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User_Query|null $query The users query.
+	 * @return WP_User_Query|null The users query without the signups.
+	 */
+	public function remove_signups_from_user_query( $query = null ) {
+		global $wpdb;
+
+		// Bail if this is an ajax request.
+		if ( defined( 'DOING_AJAX' ) ) {
+			return;
+		}
+
+		// Bail if updating BuddyPress.
+		if ( bp_is_update() ) {
+			return;
+		}
+
+		// Bail if there is no current admin screen.
+		if ( ! function_exists( 'get_current_screen' ) || ! get_current_screen() ) {
+			return;
+		}
+
+		// Get current screen.
+		$current_screen = get_current_screen();
+
+		// Bail if not on a users page.
+		if ( ! isset( $current_screen->id ) || $this->users_page !== $current_screen->id ) {
+			return;
+		}
+
+		// Bail if already querying by an existing role.
+		if ( ! empty( $query->query_vars['role'] ) ) {
+			return;
+		}
+
+		$query->query_where .= " AND {$wpdb->users}.user_status != 2";
+	}
+
+	/**
+	 * Filter the WP Users List Table views to include 'bp-signups'.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $views WP List Table views.
+	 * @return array The views with the signup view added.
+	 */
+	public function signup_filter_view( $views = array() ) {
+		global $role;
+
+		// Remove the 'current' class from All if we're on the signups view.
+		if ( 'registered' === $role ) {
+			$views['all'] = str_replace( 'class="current"', '', $views['all'] );
+			$class        = 'current';
+		} else {
+			$class        = '';
+		}
+
+		$signups = BP_Signup::count_signups();
+
+		if ( is_network_admin() ) {
+			$base_url = network_admin_url( 'users.php' );
+		} else {
+			$base_url = bp_get_admin_url( 'users.php' );
+		}
+
+		$url     = add_query_arg( 'page', 'bp-signups', $base_url );
+		$text    = sprintf( _x( 'Pending %s', 'signup users', 'buddypress' ), '<span class="count">(' . number_format_i18n( $signups ) . ')</span>' );
+
+		$views['registered'] = sprintf( '<a href="%1$s" class="%2$s">%3$s</a>', esc_url( $url ), $class, $text );
+
+		return $views;
+	}
+
+	/**
+	 * Load the Signup WP Users List table.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $class    The name of the class to use.
+	 * @param string $required The parent class.
+	 * @return WP_List_Table|null The List table.
+	 */
+	public static function get_list_table_class( $class = '', $required = '' ) {
+		if ( empty( $class ) ) {
+			return;
+		}
+
+		if ( ! empty( $required ) ) {
+			require_once( ABSPATH . 'wp-admin/includes/class-wp-' . $required . '-list-table.php' );
+		}
+
+		return new $class();
+	}
+
+	/**
+	 * Set up the signups admin page.
+	 *
+	 * Loaded before the page is rendered, this function does all initial
+	 * setup, including: processing form requests, registering contextual
+	 * help, and setting up screen options.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @global $bp_members_signup_list_table
+	 */
+	public function signups_admin_load() {
+		global $bp_members_signup_list_table;
+
+		// Build redirection URL.
+		$redirect_to = remove_query_arg( array( 'action', 'error', 'updated', 'activated', 'notactivated', 'deleted', 'notdeleted', 'resent', 'notresent', 'do_delete', 'do_resend', 'do_activate', '_wpnonce', 'signup_ids' ), $_SERVER['REQUEST_URI'] );
+		$doaction    = bp_admin_list_table_current_bulk_action();
+
+		/**
+		 * Fires at the start of the signups admin load.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $doaction Current bulk action being processed.
+		 * @param array  $_REQUEST Current $_REQUEST global.
+		 */
+		do_action( 'bp_signups_admin_load', $doaction, $_REQUEST );
+
+		/**
+		 * Filters the allowed actions for use in the user signups admin page.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $value Array of allowed actions to use.
+		 */
+		$allowed_actions = apply_filters( 'bp_signups_admin_allowed_actions', array( 'do_delete', 'do_activate', 'do_resend' ) );
+
+		// Prepare the display of the Community Profile screen.
+		if ( ! in_array( $doaction, $allowed_actions ) || ( -1 == $doaction ) ) {
+
+			if ( is_network_admin() ) {
+				$bp_members_signup_list_table = self::get_list_table_class( 'BP_Members_MS_List_Table', 'ms-users' );
+			} else {
+				$bp_members_signup_list_table = self::get_list_table_class( 'BP_Members_List_Table', 'users' );
+			}
+
+			// The per_page screen option.
+			add_screen_option( 'per_page', array( 'label' => _x( 'Pending Accounts', 'Pending Accounts per page (screen options)', 'buddypress' ) ) );
+
+			get_current_screen()->add_help_tab( array(
+				'id'      => 'bp-signups-overview',
+				'title'   => __( 'Overview', 'buddypress' ),
+				'content' =>
+				'<p>' . __( 'This is the administration screen for pending accounts on your site.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'From the screen options, you can customize the displayed columns and the pagination of this screen.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'You can reorder the list of your pending accounts by clicking on the Username, Email or Registered column headers.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'Using the search form, you can find pending accounts more easily. The Username and Email fields will be included in the search.', 'buddypress' ) . '</p>'
+			) );
+
+			get_current_screen()->add_help_tab( array(
+				'id'      => 'bp-signups-actions',
+				'title'   => __( 'Actions', 'buddypress' ),
+				'content' =>
+				'<p>' . __( 'Hovering over a row in the pending accounts list will display action links that allow you to manage pending accounts. You can perform the following actions:', 'buddypress' ) . '</p>' .
+				'<ul><li>' . __( '"Email" takes you to the confirmation screen before being able to send the activation link to the desired pending account. You can only send the activation email once per day.', 'buddypress' ) . '</li>' .
+				'<li>' . __( '"Delete" allows you to delete a pending account from your site. You will be asked to confirm this deletion.', 'buddypress' ) . '</li></ul>' .
+				'<p>' . __( 'By clicking on a Username you will be able to activate a pending account from the confirmation screen.', 'buddypress' ) . '</p>' .
+				'<p>' . __( 'Bulk actions allow you to perform these 3 actions for the selected rows.', 'buddypress' ) . '</p>'
+			) );
+
+			// Help panel - sidebar links.
+			get_current_screen()->set_help_sidebar(
+				'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+				'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+			);
+
+			// Add accessible hidden headings and text for the Pending Users screen.
+			get_current_screen()->set_screen_reader_content( array(
+				/* translators: accessibility text */
+				'heading_views'      => __( 'Filter users list', 'buddypress' ),
+				/* translators: accessibility text */
+				'heading_pagination' => __( 'Pending users list navigation', 'buddypress' ),
+				/* translators: accessibility text */
+				'heading_list'       => __( 'Pending users list', 'buddypress' ),
+			) );
+
+		} else {
+			if ( ! empty( $_REQUEST['signup_ids' ] ) ) {
+				$signups = wp_parse_id_list( $_REQUEST['signup_ids' ] );
+			}
+
+			// Handle resent activation links.
+			if ( 'do_resend' == $doaction ) {
+
+				// Nonce check.
+				check_admin_referer( 'signups_resend' );
+
+				$resent = BP_Signup::resend( $signups );
+
+				if ( empty( $resent ) ) {
+					$redirect_to = add_query_arg( 'error', $doaction, $redirect_to );
+				} else {
+					$query_arg = array( 'updated' => 'resent' );
+
+					if ( ! empty( $resent['resent'] ) ) {
+						$query_arg['resent'] = count( $resent['resent'] );
+					}
+
+					if ( ! empty( $resent['errors'] ) ) {
+						$query_arg['notsent'] = count( $resent['errors'] );
+						set_transient( '_bp_admin_signups_errors', $resent['errors'], 30 );
+					}
+
+					$redirect_to = add_query_arg( $query_arg, $redirect_to );
+				}
+
+				bp_core_redirect( $redirect_to );
+
+			// Handle activated accounts.
+			} elseif ( 'do_activate' == $doaction ) {
+
+				// Nonce check.
+				check_admin_referer( 'signups_activate' );
+
+				$activated = BP_Signup::activate( $signups );
+
+				if ( empty( $activated ) ) {
+					$redirect_to = add_query_arg( 'error', $doaction, $redirect_to );
+				} else {
+					$query_arg = array( 'updated' => 'activated' );
+
+					if ( ! empty( $activated['activated'] ) ) {
+						$query_arg['activated'] = count( $activated['activated'] );
+					}
+
+					if ( ! empty( $activated['errors'] ) ) {
+						$query_arg['notactivated'] = count( $activated['errors'] );
+						set_transient( '_bp_admin_signups_errors', $activated['errors'], 30 );
+					}
+
+					$redirect_to = add_query_arg( $query_arg, $redirect_to );
+				}
+
+				bp_core_redirect( $redirect_to );
+
+			// Handle sign-ups delete.
+			} elseif ( 'do_delete' == $doaction ) {
+
+				// Nonce check.
+				check_admin_referer( 'signups_delete' );
+
+				$deleted = BP_Signup::delete( $signups );
+
+				if ( empty( $deleted ) ) {
+					$redirect_to = add_query_arg( 'error', $doaction, $redirect_to );
+				} else {
+					$query_arg = array( 'updated' => 'deleted' );
+
+					if ( ! empty( $deleted['deleted'] ) ) {
+						$query_arg['deleted'] = count( $deleted['deleted'] );
+					}
+
+					if ( ! empty( $deleted['errors'] ) ) {
+						$query_arg['notdeleted'] = count( $deleted['errors'] );
+						set_transient( '_bp_admin_signups_errors', $deleted['errors'], 30 );
+					}
+
+					$redirect_to = add_query_arg( $query_arg, $redirect_to );
+				}
+
+				bp_core_redirect( $redirect_to );
+
+			// Plugins can update other stuff from here.
+			} else {
+				$this->redirect = $redirect_to;
+
+				/**
+				 * Fires at end of signups admin load if doaction does not match any actions.
+				 *
+				 * @since 2.0.0
+				 *
+				 * @param string $doaction Current bulk action being processed.
+				 * @param array  $_REQUEST Current $_REQUEST global.
+				 * @param string $redirect Determined redirect url to send user to.
+				 */
+				do_action( 'bp_members_admin_update_signups', $doaction, $_REQUEST, $this->redirect );
+
+				bp_core_redirect( $this->redirect );
+			}
+		}
+	}
+
+	/**
+	 * Display any activation errors.
+	 *
+	 * @since 2.0.0
+	 */
+	public function signups_display_errors() {
+
+		// Look for sign-up errors.
+		$errors = get_transient( '_bp_admin_signups_errors' );
+
+		// Bail if no activation errors.
+		if ( empty( $errors ) ) {
+			return;
+		}
+
+		// Loop through errors and display them.
+		foreach ( $errors as $error ) : ?>
+
+			<li><?php echo esc_html( $error[0] );?>: <?php echo esc_html( $error[1] );?></li>
+
+		<?php endforeach;
+
+		// Delete the redirect transient.
+		delete_transient( '_bp_admin_signups_errors' );
+	}
+
+	/**
+	 * Get admin notice when viewing the sign-up page.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return array
+	 */
+	private function get_signup_notice() {
+
+		// Setup empty notice for return value.
+		$notice = array();
+
+		// Updates.
+		if ( ! empty( $_REQUEST['updated'] ) ) {
+			switch ( $_REQUEST['updated'] ) {
+				case 'resent':
+					$notice = array(
+						'class'   => 'updated',
+						'message' => ''
+					);
+
+					if ( ! empty( $_REQUEST['resent'] ) ) {
+						$notice['message'] .= sprintf(
+							_nx( '%s activation email successfully sent! ', '%s activation emails successfully sent! ',
+							 absint( $_REQUEST['resent'] ),
+							 'signup resent',
+							 'buddypress'
+							),
+							number_format_i18n( absint( $_REQUEST['resent'] ) )
+						);
+					}
+
+					if ( ! empty( $_REQUEST['notsent'] ) ) {
+						$notice['message'] .= sprintf(
+							_nx( '%s activation email was not sent.', '%s activation emails were not sent.',
+							 absint( $_REQUEST['notsent'] ),
+							 'signup notsent',
+							 'buddypress'
+							),
+							number_format_i18n( absint( $_REQUEST['notsent'] ) )
+						);
+
+						if ( empty( $_REQUEST['resent'] ) ) {
+							$notice['class'] = 'error';
+						}
+					}
+
+					break;
+
+				case 'activated':
+					$notice = array(
+						'class'   => 'updated',
+						'message' => ''
+					);
+
+					if ( ! empty( $_REQUEST['activated'] ) ) {
+						$notice['message'] .= sprintf(
+							_nx( '%s account successfully activated! ', '%s accounts successfully activated! ',
+							 absint( $_REQUEST['activated'] ),
+							 'signup resent',
+							 'buddypress'
+							),
+							number_format_i18n( absint( $_REQUEST['activated'] ) )
+						);
+					}
+
+					if ( ! empty( $_REQUEST['notactivated'] ) ) {
+						$notice['message'] .= sprintf(
+							_nx( '%s account was not activated.', '%s accounts were not activated.',
+							 absint( $_REQUEST['notactivated'] ),
+							 'signup notsent',
+							 'buddypress'
+							),
+							number_format_i18n( absint( $_REQUEST['notactivated'] ) )
+						);
+
+						if ( empty( $_REQUEST['activated'] ) ) {
+							$notice['class'] = 'error';
+						}
+					}
+
+					break;
+
+				case 'deleted':
+					$notice = array(
+						'class'   => 'updated',
+						'message' => ''
+					);
+
+					if ( ! empty( $_REQUEST['deleted'] ) ) {
+						$notice['message'] .= sprintf(
+							_nx( '%s sign-up successfully deleted!', '%s sign-ups successfully deleted!',
+							 absint( $_REQUEST['deleted'] ),
+							 'signup deleted',
+							 'buddypress'
+							),
+							number_format_i18n( absint( $_REQUEST['deleted'] ) )
+						);
+					}
+
+					if ( ! empty( $_REQUEST['notdeleted'] ) ) {
+						$notice['message'] .= sprintf(
+							_nx( '%s sign-up was not deleted.', '%s sign-ups were not deleted.',
+							 absint( $_REQUEST['notdeleted'] ),
+							 'signup notdeleted',
+							 'buddypress'
+							),
+							number_format_i18n( absint( $_REQUEST['notdeleted'] ) )
+						);
+
+						if ( empty( $_REQUEST['deleted'] ) ) {
+							$notice['class'] = 'error';
+						}
+					}
+
+					break;
+			}
+		}
+
+		// Errors.
+		if ( ! empty( $_REQUEST['error'] ) ) {
+			switch ( $_REQUEST['error'] ) {
+				case 'do_resend':
+					$notice = array(
+						'class'   => 'error',
+						'message' => esc_html__( 'There was a problem sending the activation emails. Please try again.', 'buddypress' ),
+					);
+					break;
+
+				case 'do_activate':
+					$notice = array(
+						'class'   => 'error',
+						'message' => esc_html__( 'There was a problem activating accounts. Please try again.', 'buddypress' ),
+					);
+					break;
+
+				case 'do_delete':
+					$notice = array(
+						'class'   => 'error',
+						'message' => esc_html__( 'There was a problem deleting sign-ups. Please try again.', 'buddypress' ),
+					);
+					break;
+			}
+		}
+
+		return $notice;
+	}
+
+	/**
+	 * Signups admin page router.
+	 *
+	 * Depending on the context, display
+	 * - the list of signups,
+	 * - or the delete confirmation screen,
+	 * - or the activate confirmation screen,
+	 * - or the "resend" email confirmation screen.
+	 *
+	 * Also prepare the admin notices.
+	 *
+	 * @since 2.0.0
+	 */
+	public function signups_admin() {
+		$doaction = bp_admin_list_table_current_bulk_action();
+
+		// Prepare notices for admin.
+		$notice = $this->get_signup_notice();
+
+		// Display notices.
+		if ( ! empty( $notice ) ) :
+			if ( 'updated' === $notice['class'] ) : ?>
+
+				<div id="message" class="<?php echo esc_attr( $notice['class'] ); ?>">
+
+			<?php else: ?>
+
+				<div class="<?php echo esc_attr( $notice['class'] ); ?>">
+
+			<?php endif; ?>
+
+				<p><?php echo $notice['message']; ?></p>
+
+				<?php if ( ! empty( $_REQUEST['notactivated'] ) || ! empty( $_REQUEST['notdeleted'] ) || ! empty( $_REQUEST['notsent'] ) ) :?>
+
+					<ul><?php $this->signups_display_errors();?></ul>
+
+				<?php endif ;?>
+
+			</div>
+
+		<?php endif;
+
+		// Show the proper screen.
+		switch ( $doaction ) {
+			case 'activate' :
+			case 'delete' :
+			case 'resend' :
+				$this->signups_admin_manage( $doaction );
+				break;
+
+			default:
+				$this->signups_admin_index();
+				break;
+
+		}
+	}
+
+	/**
+	 * This is the list of the Pending accounts (signups).
+	 *
+	 * @since 2.0.0
+	 *
+	 * @global $plugin_page
+	 * @global $bp_members_signup_list_table
+	 */
+	public function signups_admin_index() {
+		global $plugin_page, $bp_members_signup_list_table;
+
+		$usersearch = ! empty( $_REQUEST['s'] ) ? stripslashes( $_REQUEST['s'] ) : '';
+
+		// Prepare the group items for display.
+		$bp_members_signup_list_table->prepare_items();
+
+		if ( is_network_admin() ) {
+			$form_url = network_admin_url( 'users.php' );
+		} else {
+			$form_url = bp_get_admin_url( 'users.php' );
+		}
+
+		$form_url = add_query_arg(
+			array(
+				'page' => 'bp-signups',
+			),
+			$form_url
+		);
+
+		$search_form_url = remove_query_arg(
+			array(
+				'action',
+				'deleted',
+				'notdeleted',
+				'error',
+				'updated',
+				'delete',
+				'activate',
+				'activated',
+				'notactivated',
+				'resend',
+				'resent',
+				'notresent',
+				'do_delete',
+				'do_activate',
+				'do_resend',
+				'action2',
+				'_wpnonce',
+				'signup_ids'
+			), $_SERVER['REQUEST_URI']
+		);
+
+		?>
+
+		<div class="wrap">
+			<?php if ( version_compare( $GLOBALS['wp_version'], '4.8', '>=' ) ) : ?>
+
+				<h1 class="wp-heading-inline"><?php _e( 'Users', 'buddypress' ); ?></h1>
+
+				<?php if ( current_user_can( 'create_users' ) ) : ?>
+
+					<a href="user-new.php" class="page-title-action"><?php echo esc_html_x( 'Add New', 'user', 'buddypress' ); ?></a>
+
+				<?php elseif ( is_multisite() && current_user_can( 'promote_users' ) ) : ?>
+
+					<a href="user-new.php" class="page-title-action"><?php echo esc_html_x( 'Add Existing', 'user', 'buddypress' ); ?></a>
+
+				<?php endif;
+
+				if ( $usersearch ) {
+					printf( '<span class="subtitle">' . __( 'Search results for &#8220;%s&#8221;', 'buddypress' ) . '</span>', esc_html( $usersearch ) );
+				}
+				?>
+
+				<hr class="wp-header-end">
+
+			<?php else : ?>
+
+				<h1><?php _e( 'Users', 'buddypress' ); ?>
+
+					<?php if ( current_user_can( 'create_users' ) ) : ?>
+
+						<a href="user-new.php" class="add-new-h2"><?php echo esc_html_x( 'Add New', 'user', 'buddypress' ); ?></a>
+
+					<?php elseif ( is_multisite() && current_user_can( 'promote_users' ) ) : ?>
+
+						<a href="user-new.php" class="add-new-h2"><?php echo esc_html_x( 'Add Existing', 'user', 'buddypress' ); ?></a>
+
+					<?php endif;
+
+					if ( $usersearch ) {
+						printf( '<span class="subtitle">' . __( 'Search results for &#8220;%s&#8221;', 'buddypress' ) . '</span>', esc_html( $usersearch ) );
+					}
+
+					?>
+				</h1>
+
+			<?php endif; ?>
+
+			<?php // Display each signups on its own row. ?>
+			<?php $bp_members_signup_list_table->views(); ?>
+
+			<form id="bp-signups-search-form" action="<?php echo esc_url( $search_form_url ) ;?>">
+				<input type="hidden" name="page" value="<?php echo esc_attr( $plugin_page ); ?>" />
+				<?php $bp_members_signup_list_table->search_box( __( 'Search Pending Users', 'buddypress' ), 'bp-signups' ); ?>
+			</form>
+
+			<form id="bp-signups-form" action="<?php echo esc_url( $form_url );?>" method="post">
+				<?php $bp_members_signup_list_table->display(); ?>
+			</form>
+		</div>
+	<?php
+	}
+
+	/**
+	 * This is the confirmation screen for actions.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action Delete, activate, or resend activation link.
+	 *
+	 * @return null|false
+	 */
+	public function signups_admin_manage( $action = '' ) {
+		if ( ! current_user_can( $this->capability ) || empty( $action ) ) {
+			die( '-1' );
+		}
+
+		// Get the user IDs from the URL.
+		$ids = false;
+		if ( ! empty( $_POST['allsignups'] ) ) {
+			$ids = wp_parse_id_list( $_POST['allsignups'] );
+		} elseif ( ! empty( $_GET['signup_id'] ) ) {
+			$ids = absint( $_GET['signup_id'] );
+		}
+
+		if ( empty( $ids ) ) {
+			return false;
+		}
+
+		// Query for signups, and filter out those IDs that don't
+		// correspond to an actual signup.
+		$signups_query = BP_Signup::get( array(
+			'include' => $ids,
+		) );
+
+		$signups    = $signups_query['signups'];
+		$signup_ids = wp_list_pluck( $signups, 'signup_id' );
+
+		// Set up strings.
+		switch ( $action ) {
+			case 'delete' :
+				$header_text = __( 'Delete Pending Accounts', 'buddypress' );
+				if ( 1 == count( $signup_ids ) ) {
+					$helper_text = __( 'You are about to delete the following account:', 'buddypress' );
+				} else {
+					$helper_text = __( 'You are about to delete the following accounts:', 'buddypress' );
+				}
+				break;
+
+			case 'activate' :
+				$header_text = __( 'Activate Pending Accounts', 'buddypress' );
+				if ( 1 == count( $signup_ids ) ) {
+					$helper_text = __( 'You are about to activate the following account:', 'buddypress' );
+				} else {
+					$helper_text = __( 'You are about to activate the following accounts:', 'buddypress' );
+				}
+				break;
+
+			case 'resend' :
+				$header_text = __( 'Resend Activation Emails', 'buddypress' );
+				if ( 1 == count( $signup_ids ) ) {
+					$helper_text = __( 'You are about to resend an activation email to the following account:', 'buddypress' );
+				} else {
+					$helper_text = __( 'You are about to resend an activation email to the following accounts:', 'buddypress' );
+				}
+				break;
+		}
+
+		// These arguments are added to all URLs.
+		$url_args = array( 'page' => 'bp-signups' );
+
+		// These arguments are only added when performing an action.
+		$action_args = array(
+			'action'     => 'do_' . $action,
+			'signup_ids' => implode( ',', $signup_ids )
+		);
+
+		if ( is_network_admin() ) {
+			$base_url = network_admin_url( 'users.php' );
+		} else {
+			$base_url = bp_get_admin_url( 'users.php' );
+		}
+
+		$cancel_url = add_query_arg( $url_args, $base_url );
+		$action_url = wp_nonce_url(
+			add_query_arg(
+				array_merge( $url_args, $action_args ),
+				$base_url
+			),
+			'signups_' . $action
+		);
+
+		// Prefetch registration field data.
+		$fdata = array();
+		if ( 'activate' === $action && bp_is_active( 'xprofile' ) ) {
+			$field_groups = bp_xprofile_get_groups( array(
+				'exclude_fields'    => 1,
+				'update_meta_cache' => false,
+				'fetch_fields'      => true,
+			) );
+
+			foreach( $field_groups as $fg ) {
+				foreach( $fg->fields as $f ) {
+					$fdata[ $f->id ] = $f->name;
+				}
+			}
+		}
+
+		?>
+
+		<div class="wrap">
+			<h1><?php echo esc_html( $header_text ); ?></h1>
+			<p><?php echo esc_html( $helper_text ); ?></p>
+
+			<ol class="bp-signups-list">
+			<?php foreach ( $signups as $signup ) :
+				$last_notified = mysql2date( 'Y/m/d g:i:s a', $signup->date_sent );
+				$profile_field_ids = array();
+
+				// Get all xprofile field IDs except field 1.
+				if ( ! empty( $signup->meta['profile_field_ids'] ) ) {
+					$profile_field_ids = array_flip( explode( ',', $signup->meta['profile_field_ids'] ) );
+					unset( $profile_field_ids[1] );
+				} ?>
+
+				<li>
+					<strong><?php echo esc_html( $signup->user_login ) ?></strong>
+
+					<?php if ( 'activate' == $action ) : ?>
+						<table class="wp-list-table widefat fixed striped">
+							<tbody>
+								<tr>
+									<td class="column-fields"><?php esc_html_e( 'Display Name', 'buddypress' ); ?></td>
+									<td><?php echo esc_html( $signup->user_name ); ?></td>
+								</tr>
+
+								<tr>
+									<td class="column-fields"><?php esc_html_e( 'Email', 'buddypress' ); ?></td>
+									<td><?php echo sanitize_email( $signup->user_email ); ?></td>
+								</tr>
+
+								<?php if ( bp_is_active( 'xprofile' ) && ! empty( $profile_field_ids ) ) : ?>
+									<?php foreach ( $profile_field_ids as $pid => $noop ) :
+										$field_value = isset( $signup->meta[ "field_{$pid}" ] ) ? $signup->meta[ "field_{$pid}" ] : ''; ?>
+										<tr>
+											<td class="column-fields"><?php echo esc_html( $fdata[ $pid ] ); ?></td>
+											<td><?php echo $this->format_xprofile_field_for_display( $field_value ); ?></td>
+										</tr>
+
+									<?php endforeach;  ?>
+
+								<?php endif; ?>
+
+							</tbody>
+						</table>
+					<?php endif; ?>
+
+					<?php if ( 'resend' == $action ) : ?>
+
+						<p class="description">
+							<?php printf( esc_html__( 'Last notified: %s', 'buddypress'), $last_notified ) ;?>
+
+							<?php if ( ! empty( $signup->recently_sent ) ) : ?>
+
+								<span class="attention wp-ui-text-notification"> <?php esc_html_e( '(less than 24 hours ago)', 'buddypress' ); ?></span>
+
+							<?php endif; ?>
+						</p>
+
+					<?php endif; ?>
+
+				</li>
+
+			<?php endforeach; ?>
+			</ol>
+
+			<?php if ( 'delete' === $action ) : ?>
+
+				<p><strong><?php esc_html_e( 'This action cannot be undone.', 'buddypress' ) ?></strong></p>
+
+			<?php endif ; ?>
+
+			<a class="button-primary" href="<?php echo esc_url( $action_url ); ?>"><?php esc_html_e( 'Confirm', 'buddypress' ); ?></a>
+			<a class="button" href="<?php echo esc_url( $cancel_url ); ?>"><?php esc_html_e( 'Cancel', 'buddypress' ) ?></a>
+		</div>
+
+		<?php
+	}
+
+	/** Users List Management ****************************************************/
+
+	/**
+	 * Display a dropdown to bulk change the member type of selected user(s).
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $which Where this dropdown is displayed - top or bottom.
+	 */
+	public function users_table_output_type_change_select( $which = 'top' ) {
+
+		// Bail if current user cannot promote users.
+		if ( ! bp_current_user_can( 'promote_users' ) ) {
+			return;
+		}
+
+		// `$which` is only passed in WordPress 4.6+. Avoid duplicating controls in earlier versions.
+		static $displayed = false;
+		if ( version_compare( bp_get_major_wp_version(), '4.6', '<' ) && $displayed ) {
+			return;
+		}
+		$displayed = true;
+
+		$id_name = 'bottom' === $which ? 'bp_change_type2' : 'bp_change_type';
+
+		$types = bp_get_member_types( array(), 'objects' ); ?>
+
+		<label class="screen-reader-text" for="<?php echo $id_name; ?>"><?php _e( 'Change member type to&hellip;', 'buddypress' ) ?></label>
+		<select name="<?php echo $id_name; ?>" id="<?php echo $id_name; ?>" style="display:inline-block;float:none;">
+			<option value=""><?php _e( 'Change member type to&hellip;', 'buddypress' ) ?></option>
+
+			<?php foreach( $types as $type ) : ?>
+
+				<option value="<?php echo esc_attr( $type->name ); ?>"><?php echo esc_html( $type->labels['singular_name'] ); ?></option>
+
+			<?php endforeach; ?>
+
+			<option value="remove_member_type"><?php _e( 'No Member Type', 'buddypress' ) ?></option>
+
+		</select>
+		<?php
+		wp_nonce_field( 'bp-bulk-users-change-type-' . bp_loggedin_user_id(), 'bp-bulk-users-change-type-nonce' );
+		submit_button( __( 'Change', 'buddypress' ), 'button', 'bp_change_member_type', false );
+	}
+
+	/**
+	 * Process bulk member type change submission from the WP admin users list table.
+	 *
+	 * @since 2.7.0
+	 */
+	public function users_table_process_bulk_type_change() {
+		// Output the admin notice.
+		$this->users_type_change_notice();
+
+		// Bail if no users are specified or if this isn't a BuddyPress action.
+		if ( empty( $_REQUEST['users'] )
+			|| ( empty( $_REQUEST['bp_change_type'] ) && empty( $_REQUEST['bp_change_type2'] ) )
+			|| empty( $_REQUEST['bp_change_member_type'] )
+		) {
+			return;
+		}
+
+		// Bail if nonce check fails.
+		check_admin_referer( 'bp-bulk-users-change-type-' . bp_loggedin_user_id(), 'bp-bulk-users-change-type-nonce' );
+
+		// Bail if current user cannot promote users.
+		if ( ! bp_current_user_can( 'promote_users' ) ) {
+			return;
+		}
+
+		$new_type = '';
+		if ( ! empty( $_REQUEST['bp_change_type2'] ) ) {
+			$new_type = sanitize_text_field( $_REQUEST['bp_change_type2'] );
+		} elseif ( ! empty( $_REQUEST['bp_change_type'] ) ) {
+			$new_type = sanitize_text_field( $_REQUEST['bp_change_type'] );
+		}
+
+		// Check that the selected type actually exists.
+		if ( 'remove_member_type' != $new_type && null === bp_get_member_type_object( $new_type ) ) {
+			$error = true;
+		} else {
+			// Run through user ids.
+			$error = false;
+			foreach ( (array) $_REQUEST['users'] as $user_id ) {
+				$user_id = (int) $user_id;
+
+				// Get the old member type to check against.
+				$member_type = bp_get_member_type( $user_id );
+
+				if ( 'remove_member_type' === $new_type ) {
+					// Remove the current member type, if there's one to remove.
+					if ( $member_type ) {
+						$removed = bp_remove_member_type( $user_id, $member_type );
+						if ( false === $removed || is_wp_error( $removed ) ) {
+							$error = true;
+						}
+					}
+				} else {
+					// Set the new member type.
+					if ( $new_type !== $member_type ) {
+						$set = bp_set_member_type( $user_id, $new_type );
+						if ( false === $set || is_wp_error( $set ) ) {
+							$error = true;
+						}
+					}
+				}
+			}
+		}
+
+		// If there were any errors, show the error message.
+		if ( $error ) {
+			$redirect = add_query_arg( array( 'updated' => 'member-type-change-error' ), wp_get_referer() );
+		} else {
+			$redirect = add_query_arg( array( 'updated' => 'member-type-change-success' ), wp_get_referer() );
+		}
+
+		wp_redirect( $redirect );
+		exit();
+	}
+
+	/**
+	 * Display an admin notice upon member type bulk update.
+	 *
+	 * @since 2.7.0
+	 */
+	public function users_type_change_notice() {
+		$updated = isset( $_REQUEST['updated'] ) ? $_REQUEST['updated'] : false;
+
+		// Display feedback.
+		if ( $updated && in_array( $updated, array( 'member-type-change-error', 'member-type-change-success' ), true ) ) {
+
+			if ( 'member-type-change-error' === $updated ) {
+				$notice = __( 'There was an error while changing member type. Please try again.', 'buddypress' );
+				$type   = 'error';
+			} else {
+				$notice = __( 'Member type was changed successfully.', 'buddypress' );
+				$type   = 'updated';
+			}
+
+			bp_core_add_admin_notice( $notice, $type );
+		}
+	}
+
+	/**
+	 * Add member type column to the WordPress admin users list table.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $columns Users table columns.
+	 *
+	 * @return array $columns
+	 */
+	public function users_table_add_type_column( $columns = array() ) {
+		$columns[ bp_get_member_type_tax_name() ] = _x( 'Member Type', 'Label for the WP users table member type column', 'buddypress' );
+
+		return $columns;
+	}
+
+	/**
+	 * Return member's type for display in the WP admin users list table.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param string $retval
+	 * @param string $column_name
+	 * @param int $user_id
+	 *
+	 * @return string Member type as a link to filter all users.
+	 */
+	public function users_table_populate_type_cell( $retval = '', $column_name = '', $user_id = 0 ) {
+		// Only looking for member type column.
+		if ( bp_get_member_type_tax_name() !== $column_name ) {
+			return $retval;
+		}
+
+		// Get the member type.
+		$type = bp_get_member_type( $user_id );
+
+		// Output the
+		if ( $type_obj = bp_get_member_type_object( $type ) ) {
+			$url = add_query_arg( array( 'bp-member-type' => urlencode( $type ) ) );
+			$retval = '<a href="' . esc_url( $url ) . '">' . esc_html( $type_obj->labels['singular_name'] ) . '</a>';
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Filter WP Admin users list table to include users of the specified type.
+	 *
+	 * @param WP_Query $query
+	 *
+	 * @since 2.7.0
+	 */
+	public function users_table_filter_by_type( $query ) {
+		global $pagenow;
+
+		if ( is_admin() && 'users.php' === $pagenow && ! empty( $_REQUEST['bp-member-type'] ) ) {
+			$type_slug = sanitize_text_field( $_REQUEST['bp-member-type'] );
+
+			// Check that the type is registered.
+			if ( null == bp_get_member_type_object( $type_slug ) ) {
+				return;
+			}
+
+			// Get the list of users that are assigned to this member type.
+			$type = bp_get_term_by( 'slug', $type_slug, bp_get_member_type_tax_name() );
+
+			if ( empty( $type->term_id ) ) {
+				return;
+			}
+
+			$user_ids = bp_get_objects_in_term( $type->term_id, bp_get_member_type_tax_name() );
+
+			if ( $user_ids && ! is_wp_error( $user_ids ) ) {
+				$query->set( 'include', (array) $user_ids );
+			}
+		}
+	}
+
+	/**
+	 * Formats a signup's xprofile field data for display.
+	 *
+	 * Operates recursively on arrays, which are then imploded with commas.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param string|array $value Field value.
+	 * @return string
+	 */
+	protected function format_xprofile_field_for_display( $value ) {
+		if ( is_array( $value ) ) {
+			$value = array_map( array( $this, 'format_xprofile_field_for_display' ), $value );
+			$value = implode( ', ', $value );
+		} else {
+			$value = stripslashes( $value );
+			$value = esc_html( $value );
+		}
+
+		return $value;
+	}
+}
+endif; // End class_exists check.
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-component.php	(revision 12265)
@@ -0,0 +1,459 @@
+<?php
+/**
+ * BuddyPress Member Loader.
+ *
+ * @package BuddyPress
+ * @subpackage Members
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Defines the BuddyPress Members Component.
+ *
+ * @since 1.5.0
+ */
+class BP_Members_Component extends BP_Component {
+
+	/**
+	 * Member types.
+	 *
+	 * @see bp_register_member_type()
+	 *
+	 * @since 2.2.0
+	 * @var array
+	 */
+	public $types = array();
+
+	/**
+	 * Start the members component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'members',
+			__( 'Members', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 20,
+				'search_query_arg' => 'members_search',
+			)
+		);
+	}
+
+	/**
+	 * Include bp-members files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::includes() for description of parameters.
+	 *
+	 * @param array $includes See {@link BP_Component::includes()}.
+	 */
+	public function includes( $includes = array() ) {
+
+		// Always include these files.
+		$includes = array(
+			'filters',
+			'template',
+			'adminbar',
+			'functions',
+			'widgets',
+			'cache',
+		);
+
+		if ( bp_is_active( 'activity' ) ) {
+			$includes[] = 'activity';
+		}
+
+		// Include these only if in admin.
+		if ( is_admin() ) {
+			$includes[] = 'admin';
+		}
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		// Members.
+		if ( bp_is_members_component() ) {
+			// Actions - Random member handler.
+			if ( isset( $_GET['random-member'] ) ) {
+				require $this->path . 'bp-members/actions/random.php';
+			}
+
+			// Screens - Directory.
+			if ( bp_is_members_directory() ) {
+				require $this->path . 'bp-members/screens/directory.php';
+			}
+		}
+
+		// Members - User main nav screen.
+		if ( bp_is_user() ) {
+			require $this->path . 'bp-members/screens/profile.php';
+		}
+
+		// Members - Theme compatibility.
+		if ( bp_is_members_component() || bp_is_user() ) {
+			new BP_Members_Theme_Compat();
+		}
+
+		// Registration / Activation.
+		if ( bp_is_register_page() || bp_is_activation_page() ) {
+			if ( bp_is_register_page() ) {
+				require $this->path . 'bp-members/screens/register.php';
+			} else {
+				require $this->path . 'bp-members/screens/activate.php';
+			}
+
+			// Theme compatibility.
+			new BP_Registration_Theme_Compat();
+		}
+	}
+
+	/**
+	 * Set up bp-members global settings.
+	 *
+	 * The BP_MEMBERS_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_globals() for description of parameters.
+	 *
+	 * @param array $args See {@link BP_Component::setup_globals()}.
+	 */
+	public function setup_globals( $args = array() ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		/** Component Globals ************************************************
+		 */
+
+		// Define a slug, as a fallback for backpat.
+		if ( !defined( 'BP_MEMBERS_SLUG' ) ) {
+			define( 'BP_MEMBERS_SLUG', $this->id );
+		}
+
+		// Fetch the default directory title.
+		$default_directory_titles = bp_core_get_directory_page_default_titles();
+		$default_directory_title  = $default_directory_titles[$this->id];
+
+		// Override any passed args.
+		$args = array(
+			'slug'            => BP_MEMBERS_SLUG,
+			'root_slug'       => isset( $bp->pages->members->slug ) ? $bp->pages->members->slug : BP_MEMBERS_SLUG,
+			'has_directory'   => true,
+			'directory_title' => isset( $bp->pages->members->title ) ? $bp->pages->members->title : $default_directory_title,
+			'search_string'   => __( 'Search Members...', 'buddypress' ),
+			'global_tables'   => array(
+				'table_name_last_activity' => bp_core_get_table_prefix() . 'bp_activity',
+				'table_name_signups'       => $wpdb->base_prefix . 'signups', // Signups is a global WordPress table.
+			)
+		);
+
+		parent::setup_globals( $args );
+
+		/** Logged in user ***************************************************
+		 */
+
+		// The core userdata of the user who is currently logged in.
+		$bp->loggedin_user->userdata       = bp_core_get_core_userdata( bp_loggedin_user_id() );
+
+		// Fetch the full name for the logged in user.
+		$bp->loggedin_user->fullname       = isset( $bp->loggedin_user->userdata->display_name ) ? $bp->loggedin_user->userdata->display_name : '';
+
+		// Hits the DB on single WP installs so get this separately.
+		$bp->loggedin_user->is_super_admin = $bp->loggedin_user->is_site_admin = is_super_admin( bp_loggedin_user_id() );
+
+		// The domain for the user currently logged in. eg: http://example.com/members/andy.
+		$bp->loggedin_user->domain         = bp_core_get_user_domain( bp_loggedin_user_id() );
+
+		/** Displayed user ***************************************************
+		 */
+
+		// The core userdata of the user who is currently being displayed.
+		$bp->displayed_user->userdata = bp_core_get_core_userdata( bp_displayed_user_id() );
+
+		// Fetch the full name displayed user.
+		$bp->displayed_user->fullname = isset( $bp->displayed_user->userdata->display_name ) ? $bp->displayed_user->userdata->display_name : '';
+
+		// The domain for the user currently being displayed.
+		$bp->displayed_user->domain   = bp_core_get_user_domain( bp_displayed_user_id() );
+
+		// Initialize the nav for the members component.
+		$this->nav = new BP_Core_Nav();
+
+		// If A user is displayed, check if there is a front template
+		if ( bp_get_displayed_user() ) {
+			$bp->displayed_user->front_template = bp_displayed_user_get_front_template();
+		}
+
+		/** Signup ***********************************************************
+		 */
+
+		$bp->signup = new stdClass;
+
+		/** Profiles Fallback ************************************************
+		 */
+
+		if ( ! bp_is_active( 'xprofile' ) ) {
+			$bp->profile       = new stdClass;
+			$bp->profile->slug = 'profile';
+			$bp->profile->id   = 'profile';
+		}
+	}
+
+	/**
+	 * Set up canonical stack for this component.
+	 *
+	 * @since 2.1.0
+	 */
+	public function setup_canonical_stack() {
+		$bp = buddypress();
+
+		/** Default Profile Component ****************************************
+		 */
+		if ( bp_displayed_user_has_front_template() ) {
+			$bp->default_component = 'front';
+		} elseif ( bp_is_active( 'activity' ) && isset( $bp->pages->activity ) ) {
+			$bp->default_component = bp_get_activity_slug();
+		} else {
+			$bp->default_component = ( 'xprofile' === $bp->profile->id ) ? 'profile' : $bp->profile->id;
+		}
+
+		if ( defined( 'BP_DEFAULT_COMPONENT' ) && BP_DEFAULT_COMPONENT ) {
+			$default_component = BP_DEFAULT_COMPONENT;
+			if ( 'profile' === $default_component ) {
+				$default_component = 'xprofile';
+			}
+
+			if ( bp_is_active( $default_component ) ) {
+				$bp->default_component = BP_DEFAULT_COMPONENT;
+			}
+		}
+
+		/** Canonical Component Stack ****************************************
+		 */
+
+		if ( bp_displayed_user_id() ) {
+			$bp->canonical_stack['base_url'] = bp_displayed_user_domain();
+
+			if ( bp_current_component() ) {
+				$bp->canonical_stack['component'] = bp_current_component();
+			}
+
+			if ( bp_current_action() ) {
+				$bp->canonical_stack['action'] = bp_current_action();
+			}
+
+			if ( !empty( $bp->action_variables ) ) {
+				$bp->canonical_stack['action_variables'] = bp_action_variables();
+			}
+
+			// Looking at the single member root/home, so assume the default.
+			if ( ! bp_current_component() ) {
+				$bp->current_component = $bp->default_component;
+
+			// The canonical URL will not contain the default component.
+			} elseif ( bp_is_current_component( $bp->default_component ) && ! bp_current_action() ) {
+				unset( $bp->canonical_stack['component'] );
+			}
+
+			// If we're on a spammer's profile page, only users with the 'bp_moderate' cap
+			// can view subpages on the spammer's profile.
+			//
+			// users without the cap trying to access a spammer's subnav page will get
+			// redirected to the root of the spammer's profile page.  this occurs by
+			// by removing the component in the canonical stack.
+			if ( bp_is_user_spammer( bp_displayed_user_id() ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+				unset( $bp->canonical_stack['component'] );
+			}
+		}
+	}
+
+	/**
+	 * Set up fall-back component navigation if XProfile is inactive.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of arguments.
+	 *
+	 * @param array $main_nav Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 * @param array $sub_nav  Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Don't set up navigation if there's no member.
+		if ( ! is_user_logged_in() && ! bp_is_user() ) {
+			return;
+		}
+
+		$is_xprofile_active = bp_is_active( 'xprofile' );
+
+		// Bail if XProfile component is active and there's no custom front page for the user.
+		if ( ! bp_displayed_user_has_front_template() && $is_xprofile_active ) {
+			return;
+		}
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		// Set slug to profile in case the xProfile component is not active
+		$slug = bp_get_profile_slug();
+
+		// Defaults to empty navs
+		$this->main_nav = array();
+		$this->sub_nav  = array();
+
+		if ( ! $is_xprofile_active ) {
+			$this->main_nav = array(
+				'name'                => _x( 'Profile', 'Member profile main navigation', 'buddypress' ),
+				'slug'                => $slug,
+				'position'            => 20,
+				'screen_function'     => 'bp_members_screen_display_profile',
+				'default_subnav_slug' => 'public',
+				'item_css_id'         => buddypress()->profile->id
+			);
+		}
+
+		/**
+		 * Setup the subnav items for the member profile.
+		 *
+		 * This is required in case there's a custom front or in case the xprofile component
+		 * is not active.
+		 */
+		$this->sub_nav = array(
+			'name'            => _x( 'View', 'Member profile view', 'buddypress' ),
+			'slug'            => 'public',
+			'parent_url'      => trailingslashit( $user_domain . $slug ),
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_members_screen_display_profile',
+			'position'        => 10
+		);
+
+		/**
+		 * If there's a front template the members component nav
+		 * will be there to display the user's front page.
+		 */
+		if ( bp_displayed_user_has_front_template() ) {
+			$main_nav = array(
+				'name'                => _x( 'Home', 'Member Home page', 'buddypress' ),
+				'slug'                => 'front',
+				'position'            => 5,
+				'screen_function'     => 'bp_members_screen_display_profile',
+				'default_subnav_slug' => 'public',
+			);
+
+			// We need a dummy subnav for the front page to load.
+			$front_subnav = $this->sub_nav;
+			$front_subnav['parent_slug'] = 'front';
+
+			// In case the subnav is displayed in the front template
+			$front_subnav['parent_url'] = trailingslashit( $user_domain . 'front' );
+
+			// Set the subnav
+			$sub_nav[] = $front_subnav;
+
+			/**
+			 * If the profile component is not active, we need to create a new
+			 * nav to display the WordPress profile.
+			 */
+			if ( ! $is_xprofile_active ) {
+				add_action( 'bp_members_setup_nav', array( $this, 'setup_profile_nav' ) );
+			}
+
+		/**
+		 * If there's no front template and xProfile is not active, the members
+		 * component nav will be there to display the WordPress profile
+		 */
+		} else {
+			$main_nav  = $this->main_nav;
+			$sub_nav[] = $this->sub_nav;
+		}
+
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up a profile nav in case the xProfile
+	 * component is not active and a front template is
+	 * used.
+	 *
+	 * @since 2.6.0
+	 */
+	public function setup_profile_nav() {
+		if ( empty( $this->main_nav ) || empty( $this->sub_nav ) ) {
+			return;
+		}
+
+		// Add the main nav
+		bp_core_new_nav_item( $this->main_nav, 'members' );
+
+		// Add the sub nav item.
+		bp_core_new_subnav_item( $this->sub_nav, 'members' );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 *
+	 * @since 1.5.0
+	 */
+	public function setup_title() {
+		$bp = buddypress();
+
+		if ( bp_is_my_profile() ) {
+			$bp->bp_options_title = __( 'You', 'buddypress' );
+		} elseif ( bp_is_user() ) {
+			$bp->bp_options_title  = bp_get_displayed_user_fullname();
+			$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+				'item_id' => bp_displayed_user_id(),
+				'type'    => 'thumb',
+				'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), $bp->bp_options_title )
+			) );
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups.
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_last_activity',
+			'bp_member_type'
+		) );
+
+		parent::setup_cache_groups();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-list-table.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-list-table.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-list-table.php	(revision 12265)
@@ -0,0 +1,411 @@
+<?php
+/**
+ * BuddyPress Members List Table class.
+ *
+ * @package BuddyPress
+ * @subpackage MembersAdminClasses
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * List table class for signups admin page.
+ *
+ * @since 2.0.0
+ */
+class BP_Members_List_Table extends WP_Users_List_Table {
+
+	/**
+	 * Signup counts.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @var int
+	 */
+	public $signup_counts = 0;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		// Define singular and plural labels, as well as whether we support AJAX.
+		parent::__construct( array(
+			'ajax'     => false,
+			'plural'   => 'signups',
+			'singular' => 'signup',
+			'screen'   => get_current_screen()->id,
+		) );
+	}
+
+	/**
+	 * Set up items for display in the list table.
+	 *
+	 * Handles filtering of data, sorting, pagination, and any other data
+	 * manipulation required prior to rendering.
+	 *
+	 * @since 2.0.0
+	 */
+	public function prepare_items() {
+		global $usersearch;
+
+		$usersearch       = isset( $_REQUEST['s'] ) ? $_REQUEST['s'] : '';
+		$signups_per_page = $this->get_items_per_page( str_replace( '-', '_', "{$this->screen->id}_per_page" ) );
+		$paged            = $this->get_pagenum();
+
+		$args = array(
+			'offset'     => ( $paged - 1 ) * $signups_per_page,
+			'number'     => $signups_per_page,
+			'usersearch' => $usersearch,
+			'orderby'    => 'signup_id',
+			'order'      => 'DESC'
+		);
+
+		if ( isset( $_REQUEST['orderby'] ) ) {
+			$args['orderby'] = $_REQUEST['orderby'];
+		}
+
+		if ( isset( $_REQUEST['order'] ) ) {
+			$args['order'] = $_REQUEST['order'];
+		}
+
+		$signups = BP_Signup::get( $args );
+
+		$this->items = $signups['signups'];
+		$this->signup_counts = $signups['total'];
+
+		$this->set_pagination_args( array(
+			'total_items' => $this->signup_counts,
+			'per_page'    => $signups_per_page,
+		) );
+	}
+
+	/**
+	 * Display the users screen views
+	 *
+	 * @since 2.5.0
+	 *
+	 * @global string $role The name of role the users screens is filtered by
+	 */
+	public function views() {
+		global $role;
+
+		// Used to reset the role.
+		$reset_role = $role;
+
+		// Temporarly set the role to registered.
+		$role = 'registered';
+
+		// Used to reset the screen id once views are displayed.
+		$reset_screen_id = $this->screen->id;
+
+		// Temporarly set the screen id to the users one.
+		$this->screen->id = 'users';
+
+		// Use the parent function so that other plugins can safely add views.
+		parent::views();
+
+		// Reset the role.
+		$role = $reset_role;
+
+		// Reset the screen id.
+		$this->screen->id = $reset_screen_id;
+	}
+
+	/**
+	 * Get rid of the extra nav.
+	 *
+	 * WP_Users_List_Table will add an extra nav to change user's role.
+	 * As we're dealing with signups, we don't need this.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $which Current table nav item.
+	 */
+	public function extra_tablenav( $which ) {
+		return;
+	}
+
+	/**
+	 * Specific signups columns.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return array
+	 */
+	public function get_columns() {
+
+		/**
+		 * Filters the single site Members signup columns.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $value Array of columns to display.
+		 */
+		return apply_filters( 'bp_members_signup_columns', array(
+			'cb'         => '<input type="checkbox" />',
+			'username'   => __( 'Username',    'buddypress' ),
+			'name'       => __( 'Name',        'buddypress' ),
+			'email'      => __( 'Email',       'buddypress' ),
+			'registered' => __( 'Registered',  'buddypress' ),
+			'date_sent'  => __( 'Last Sent',   'buddypress' ),
+			'count_sent' => __( 'Emails Sent', 'buddypress' )
+		) );
+	}
+
+	/**
+	 * Specific bulk actions for signups.
+	 *
+	 * @since 2.0.0
+	 */
+	public function get_bulk_actions() {
+		$actions = array(
+			'activate' => _x( 'Activate', 'Pending signup action', 'buddypress' ),
+			'resend'   => _x( 'Email',    'Pending signup action', 'buddypress' ),
+		);
+
+		if ( current_user_can( 'delete_users' ) ) {
+			$actions['delete'] = __( 'Delete', 'buddypress' );
+		}
+
+		return $actions;
+	}
+
+	/**
+	 * The text shown when no items are found.
+	 *
+	 * Nice job, clean sheet!
+	 *
+	 * @since 2.0.0
+	 */
+	public function no_items() {
+
+		if ( bp_get_signup_allowed() ) {
+			esc_html_e( 'No pending accounts found.', 'buddypress' );
+		} else {
+			$link = false;
+
+			// Specific case when BuddyPress is not network activated.
+			if ( is_multisite() && current_user_can( 'manage_network_users') ) {
+				$link = sprintf( '<a href="%1$s">%2$s</a>', esc_url( network_admin_url( 'settings.php'       ) ), esc_html__( 'Edit settings', 'buddypress' ) );
+			} elseif ( current_user_can( 'manage_options' ) ) {
+				$link = sprintf( '<a href="%1$s">%2$s</a>', esc_url( bp_get_admin_url( 'options-general.php' ) ), esc_html__( 'Edit settings', 'buddypress' ) );
+			}
+
+			printf( __( 'Registration is disabled. %s', 'buddypress' ), $link );
+		}
+
+	}
+
+	/**
+	 * The columns signups can be reordered with.
+	 *
+	 * @since 2.0.0
+	 */
+	public function get_sortable_columns() {
+		return array(
+			'username'   => 'login',
+			'email'      => 'email',
+			'registered' => 'signup_id',
+		);
+	}
+
+	/**
+	 * Display signups rows.
+	 *
+	 * @since 2.0.0
+	 */
+	public function display_rows() {
+		$style = '';
+		foreach ( $this->items as $userid => $signup_object ) {
+
+			// Avoid a notice error appearing since 4.3.0.
+			if ( isset( $signup_object->id ) ) {
+				$signup_object->ID = $signup_object->id;
+			}
+
+			$style = ( ' class="alternate"' == $style ) ? '' : ' class="alternate"';
+			echo "\n\t" . $this->single_row( $signup_object, $style );
+		}
+	}
+
+	/**
+	 * Display a signup row.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @see WP_List_Table::single_row() for explanation of params.
+	 *
+	 * @param object|null $signup_object Signup user object.
+	 * @param string      $style         Styles for the row.
+	 * @param string      $role          Role to be assigned to user.
+	 * @param int         $numposts      Numper of posts.
+	 * @return void
+	 */
+	public function single_row( $signup_object = null, $style = '', $role = '', $numposts = 0 ) {
+		echo '<tr' . $style . ' id="signup-' . esc_attr( $signup_object->id ) . '">';
+		echo $this->single_row_columns( $signup_object );
+		echo '</tr>';
+	}
+
+	/**
+	 * Markup for the checkbox used to select items for bulk actions.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_cb( $signup_object = null ) {
+	?>
+		<label class="screen-reader-text" for="signup_<?php echo intval( $signup_object->id ); ?>"><?php
+			/* translators: accessibility text */
+			printf( esc_html__( 'Select user: %s', 'buddypress' ), $signup_object->user_login );
+		?></label>
+		<input type="checkbox" id="signup_<?php echo intval( $signup_object->id ) ?>" name="allsignups[]" value="<?php echo esc_attr( $signup_object->id ) ?>" />
+		<?php
+	}
+
+	/**
+	 * The row actions (delete/activate/email).
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_username( $signup_object = null ) {
+		$avatar	= get_avatar( $signup_object->user_email, 32 );
+
+		// Activation email link.
+		$email_link = add_query_arg(
+			array(
+				'page'	    => 'bp-signups',
+				'signup_id' => $signup_object->id,
+				'action'    => 'resend',
+			),
+			bp_get_admin_url( 'users.php' )
+		);
+
+		// Activate link.
+		$activate_link = add_query_arg(
+			array(
+				'page'      => 'bp-signups',
+				'signup_id' => $signup_object->id,
+				'action'    => 'activate',
+			),
+			bp_get_admin_url( 'users.php' )
+		);
+
+		// Delete link.
+		$delete_link = add_query_arg(
+			array(
+				'page'      => 'bp-signups',
+				'signup_id' => $signup_object->id,
+				'action'    => 'delete',
+			),
+			bp_get_admin_url( 'users.php' )
+		);
+
+		echo $avatar . sprintf( '<strong><a href="%1$s" class="edit">%2$s</a></strong><br/>', esc_url( $activate_link ), $signup_object->user_login );
+
+		$actions = array();
+
+		$actions['activate'] = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $activate_link ), __( 'Activate', 'buddypress' ) );
+		$actions['resend']   = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $email_link ), __( 'Email', 'buddypress' ) );
+
+		if ( current_user_can( 'delete_users' ) ) {
+			$actions['delete'] = sprintf( '<a href="%1$s" class="delete">%2$s</a>', esc_url( $delete_link ), __( 'Delete', 'buddypress' ) );
+		}
+
+		/**
+		 * Filters the multisite row actions for each user in list.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array  $actions       Array of actions and corresponding links.
+		 * @param object $signup_object The signup data object.
+		 */
+		$actions = apply_filters( 'bp_members_ms_signup_row_actions', $actions, $signup_object );
+
+		echo $this->row_actions( $actions );
+	}
+
+	/**
+	 * Display user name, if any.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_name( $signup_object = null ) {
+		echo esc_html( $signup_object->user_name );
+	}
+
+	/**
+	 * Display user email.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_email( $signup_object = null ) {
+		printf( '<a href="mailto:%1$s">%2$s</a>', esc_attr( $signup_object->user_email ), esc_html( $signup_object->user_email ) );
+	}
+
+	/**
+	 * Display registration date.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_registered( $signup_object = null ) {
+		echo mysql2date( 'Y/m/d', $signup_object->registered );
+	}
+
+	/**
+	 * Display the last time an activation email has been sent.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_date_sent( $signup_object = null ) {
+		echo mysql2date( 'Y/m/d', $signup_object->date_sent );
+	}
+
+	/**
+	 * Display number of time an activation email has been sent.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object Signup object instance.
+	 */
+	public function column_count_sent( $signup_object = null ) {
+		echo absint( $signup_object->count_sent );
+	}
+
+	/**
+	 * Allow plugins to add their custom column.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 * @param string      $column_name   The column name.
+	 * @return string
+	 */
+	function column_default( $signup_object = null, $column_name = '' ) {
+
+		/**
+		 * Filters the single site custom columns for plugins.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $column_name   The column name.
+		 * @param object $signup_object The signup data object.
+		 */
+		return apply_filters( 'bp_members_signup_custom_column', '', $column_name, $signup_object );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-ms-list-table.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-ms-list-table.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-ms-list-table.php	(revision 12265)
@@ -0,0 +1,413 @@
+<?php
+/**
+ * BuddyPress Members List Table for Multisite.
+ *
+ * @package BuddyPress
+ * @subpackage MembersAdminClasses
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * List table class for signups network admin page.
+ *
+ * @since 2.0.0
+ */
+class BP_Members_MS_List_Table extends WP_MS_Users_List_Table {
+
+	/**
+	 * Signup counts.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @var int
+	 */
+	public $signup_counts = 0;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		// Define singular and plural labels, as well as whether we support AJAX.
+		parent::__construct( array(
+			'ajax'     => false,
+			'plural'   => 'signups',
+			'singular' => 'signup',
+			'screen'   => get_current_screen()->id,
+		) );
+	}
+
+	/**
+	 * Set up items for display in the list table.
+	 *
+	 * Handles filtering of data, sorting, pagination, and any other data
+	 * manipulation required prior to rendering.
+	 *
+	 * @since 2.0.0
+	 */
+	public function prepare_items() {
+		global $usersearch, $mode;
+
+		$usersearch       = isset( $_REQUEST['s'] ) ? $_REQUEST['s'] : '';
+		$signups_per_page = $this->get_items_per_page( str_replace( '-', '_', "{$this->screen->id}_per_page" ) );
+		$paged            = $this->get_pagenum();
+
+		$args = array(
+			'offset'     => ( $paged - 1 ) * $signups_per_page,
+			'number'     => $signups_per_page,
+			'usersearch' => $usersearch,
+			'orderby'    => 'signup_id',
+			'order'      => 'DESC'
+		);
+
+		if ( isset( $_REQUEST['orderby'] ) ) {
+			$args['orderby'] = $_REQUEST['orderby'];
+		}
+
+		if ( isset( $_REQUEST['order'] ) ) {
+			$args['order'] = $_REQUEST['order'];
+		}
+
+		$mode    = empty( $_REQUEST['mode'] ) ? 'list' : $_REQUEST['mode'];
+		$signups = BP_Signup::get( $args );
+
+		$this->items         = $signups['signups'];
+		$this->signup_counts = $signups['total'];
+
+		$this->set_pagination_args( array(
+			'total_items' => $this->signup_counts,
+			'per_page'    => $signups_per_page,
+		) );
+	}
+
+	/**
+	 * Display the users screen views
+	 *
+	 * @since 2.5.0
+	 *
+	 * @global string $role The name of role the users screens is filtered by
+	 */
+	public function views() {
+		global $role;
+
+		// Used to reset the role.
+		$reset_role = $role;
+
+		// Temporarly set the role to registered.
+		$role = 'registered';
+
+		// Used to reset the screen id once views are displayed.
+		$reset_screen_id = $this->screen->id;
+
+		// Temporarly set the screen id to the users one.
+		$this->screen->id = 'users-network';
+
+		// Use the parent function so that other plugins can safely add views.
+		parent::views();
+
+		// Reset the role.
+		$role = $reset_role;
+
+		// Reset the screen id.
+		$this->screen->id = $reset_screen_id;
+	}
+
+	/**
+	 * Specific signups columns.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return array
+	 */
+	public function get_columns() {
+
+		/**
+		 * Filters the multisite Members signup columns.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $value Array of columns to display.
+		 */
+		return apply_filters( 'bp_members_ms_signup_columns', array(
+			'cb'         => '<input type="checkbox" />',
+			'username'   => __( 'Username',    'buddypress' ),
+			'name'       => __( 'Name',        'buddypress' ),
+			'email'      => __( 'Email',       'buddypress' ),
+			'registered' => __( 'Registered',  'buddypress' ),
+			'date_sent'  => __( 'Last Sent',   'buddypress' ),
+			'count_sent' => __( 'Emails Sent', 'buddypress' )
+		) );
+	}
+
+	/**
+	 * Specific bulk actions for signups.
+	 *
+	 * @since 2.0.0
+	 */
+	public function get_bulk_actions() {
+		$actions = array(
+			'activate' => _x( 'Activate', 'Pending signup action', 'buddypress' ),
+			'resend'   => _x( 'Email',    'Pending signup action', 'buddypress' ),
+		);
+
+		if ( current_user_can( 'delete_users' ) ) {
+			$actions['delete'] = __( 'Delete', 'buddypress' );
+		}
+
+		return $actions;
+	}
+
+	/**
+	 * The text shown when no items are found.
+	 *
+	 * Nice job, clean sheet!
+	 *
+	 * @since 2.0.0
+	 */
+	public function no_items() {
+		if ( bp_get_signup_allowed() ) {
+			esc_html_e( 'No pending accounts found.', 'buddypress' );
+		} else {
+			$link = false;
+
+			if ( current_user_can( 'manage_network_users' ) ) {
+				$link = sprintf( '<a href="%1$s">%2$s</a>', esc_url( network_admin_url( 'settings.php' ) ), esc_html__( 'Edit settings', 'buddypress' ) );
+			}
+
+			printf( __( 'Registration is disabled. %s', 'buddypress' ), $link );
+		}
+	}
+
+	/**
+	 * The columns signups can be reordered with.
+	 *
+	 * @since 2.0.0
+	 */
+	public function get_sortable_columns() {
+		return array(
+			'username'   => 'login',
+			'email'      => 'email',
+			'registered' => 'signup_id',
+		);
+	}
+
+	/**
+	 * Display signups rows.
+	 *
+	 * @since 2.0.0
+	 */
+	public function display_rows() {
+		$style = '';
+		foreach ( $this->items as $userid => $signup_object ) {
+
+			// Avoid a notice error appearing since 4.3.0.
+			if ( isset( $signup_object->id ) ) {
+				$signup_object->ID = $signup_object->id;
+			}
+
+			$style = ( ' class="alternate"' == $style ) ? '' : ' class="alternate"';
+			echo "\n\t" . $this->single_row( $signup_object, $style );
+		}
+	}
+
+	/**
+	 * Display a signup row.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @see WP_List_Table::single_row() for explanation of params.
+	 *
+	 * @param object|null $signup_object Signup user object.
+	 * @param string      $style         Styles for the row.
+	 */
+	public function single_row( $signup_object = null, $style = '' ) {
+		echo '<tr' . $style . ' id="signup-' . esc_attr( $signup_object->id ) . '">';
+		echo $this->single_row_columns( $signup_object );
+		echo '</tr>';
+	}
+
+	/**
+	 * Prevents regular users row actions to be output.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param object|null $signup_object Signup being acted upon.
+	 * @param string      $column_name   Current column name.
+	 * @param string      $primary       Primary column name.
+	 * @return string
+	 */
+	protected function handle_row_actions( $signup_object = null, $column_name = '', $primary = '' ) {
+		return '';
+	}
+
+	/**
+	 * Markup for the checkbox used to select items for bulk actions.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_cb( $signup_object = null ) {
+	?>
+		<label class="screen-reader-text" for="signup_<?php echo intval( $signup_object->id ); ?>"><?php printf(
+			/* translators: accessibility text */
+			esc_html__( 'Select user: %s', 'buddypress' ), $signup_object->user_login );
+		?></label>
+		<input type="checkbox" id="signup_<?php echo intval( $signup_object->id ) ?>" name="allsignups[]" value="<?php echo esc_attr( $signup_object->id ) ?>" />
+		<?php
+	}
+
+	/**
+	 * The row actions (delete/activate/email).
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_username( $signup_object = null ) {
+		$avatar	= get_avatar( $signup_object->user_email, 32 );
+
+		// Activation email link.
+		$email_link = add_query_arg(
+			array(
+				'page'	    => 'bp-signups',
+				'signup_id' => $signup_object->id,
+				'action'    => 'resend',
+			),
+			network_admin_url( 'users.php' )
+		);
+
+		// Activate link.
+		$activate_link = add_query_arg(
+			array(
+				'page'      => 'bp-signups',
+				'signup_id' => $signup_object->id,
+				'action'    => 'activate',
+			),
+			network_admin_url( 'users.php' )
+		);
+
+		// Delete link.
+		$delete_link = add_query_arg(
+			array(
+				'page'      => 'bp-signups',
+				'signup_id' => $signup_object->id,
+				'action'    => 'delete',
+			),
+			network_admin_url( 'users.php' )
+		);
+
+		echo $avatar . sprintf( '<strong><a href="%1$s" class="edit">%2$s</a></strong><br/>', esc_url( $activate_link ), $signup_object->user_login );
+
+		$actions = array();
+
+		$actions['activate'] = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $activate_link ), __( 'Activate', 'buddypress' ) );
+		$actions['resend']   = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $email_link    ), __( 'Email',    'buddypress' ) );
+
+		if ( current_user_can( 'delete_users' ) ) {
+			$actions['delete'] = sprintf( '<a href="%1$s" class="delete">%2$s</a>', esc_url( $delete_link ), __( 'Delete', 'buddypress' ) );
+		}
+
+		/** This filter is documented in bp-members/admin/bp-members-classes.php */
+		$actions = apply_filters( 'bp_members_ms_signup_row_actions', $actions, $signup_object );
+
+		echo $this->row_actions( $actions );
+	}
+
+	/**
+	 * Display user name, if any.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_name( $signup_object = null ) {
+		echo esc_html( $signup_object->user_name );
+	}
+
+	/**
+	 * Display user email.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_email( $signup_object = null ) {
+		printf( '<a href="mailto:%1$s">%2$s</a>', esc_attr( $signup_object->user_email ), esc_html( $signup_object->user_email ) );
+	}
+
+	/**
+	 * Display registration date.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 */
+	public function column_registered( $signup_object = null ) {
+		global $mode;
+
+		if ( 'list' === $mode ) {
+			$date = 'Y/m/d';
+		} else {
+			$date = 'Y/m/d \<\b\r \/\> g:i:s a';
+		}
+
+		echo mysql2date( $date, $signup_object->registered ) . "</td>";
+	}
+
+	/**
+	 * Display the last time an activation email has been sent.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object Signup object instance.
+	 */
+	public function column_date_sent( $signup_object = null ) {
+		global $mode;
+
+		if ( 'list' === $mode ) {
+			$date = 'Y/m/d';
+		} else {
+			$date = 'Y/m/d \<\b\r \/\> g:i:s a';
+		}
+
+		echo mysql2date( $date, $signup_object->date_sent );
+	}
+
+	/**
+	 * Display number of time an activation email has been sent.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param object|null $signup_object Signup object instance.
+	 */
+	public function column_count_sent( $signup_object = null ) {
+		echo absint( $signup_object->count_sent );
+	}
+
+	/**
+	 * Allow plugins to add their custom column.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param object|null $signup_object The signup data object.
+	 * @param string      $column_name   The column name.
+	 * @return string
+	 */
+	function column_default( $signup_object = null, $column_name = '' ) {
+
+		/**
+		 * Filters the multisite custom columns for plugins.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $column_name   The column name.
+		 * @param object $signup_object The signup data object.
+		 */
+		return apply_filters( 'bp_members_ms_signup_custom_column', '', $column_name, $signup_object );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-theme-compat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-theme-compat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-members-theme-compat.php	(revision 12265)
@@ -0,0 +1,214 @@
+<?php
+/**
+ * BuddyPress Member Theme Compat.
+ *
+ * @package BuddyPress
+ * @subpackage MembersScreens
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main theme compat class for BuddyPress Members.
+ *
+ * This class sets up the necessary theme compatibility actions to safely output
+ * member template parts to the_title and the_content areas of a theme.
+ *
+ * @since 1.7.0
+ */
+class BP_Members_Theme_Compat {
+
+	/**
+	 * Set up the members component theme compatibility.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __construct() {
+		add_action( 'bp_setup_theme_compat', array( $this, 'is_members' ) );
+	}
+
+	/**
+	 * Are we looking at something that needs members theme compatibility?
+	 *
+	 * @since 1.7.0
+	 */
+	public function is_members() {
+
+		// Bail if not looking at the members component or a user's page.
+		if ( ! bp_is_members_component() && ! bp_is_user() ) {
+			return;
+		}
+
+		// User page.
+		if ( bp_is_user() ) {
+
+			// If we're on a single activity permalink page, we shouldn't use the members
+			// template, so stop here!
+			if ( bp_is_active( 'activity' ) && bp_is_single_activity() ) {
+				return;
+			}
+
+			/**
+			 * Fires if looking at Members user page when needing theme compat.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_members_screen_display_profile' );
+
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'single_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'single_dummy_post'    ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'single_dummy_content' ) );
+
+		// Members Directory.
+		} elseif ( ! bp_current_action() && ! bp_current_item() ) {
+			bp_update_is_directory( true, 'members' );
+
+			/**
+			 * Fires if looking at Members directory when needing theme compat.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_members_screen_index' );
+
+			add_filter( 'bp_get_buddypress_template',                array( $this, 'directory_template_hierarchy' ) );
+			add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'directory_dummy_post' ) );
+			add_filter( 'bp_replace_the_content',                    array( $this, 'directory_content'    ) );
+		}
+	}
+
+	/** Directory *************************************************************/
+
+	/**
+	 * Add template hierarchy to theme compat for the members directory page.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param array $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function directory_template_hierarchy( $templates = array() ) {
+
+		// Set up the template hierarchy.
+		$new_templates = array();
+		if ( '' !== bp_get_current_member_type() ) {
+			$new_templates[] = 'members/index-directory-type-' . sanitize_file_name( bp_get_current_member_type() ) . '.php';
+		}
+		$new_templates[] = 'members/index-directory.php';
+
+		/**
+		 * Filters the template hierarchy for theme compat and members directory page.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of template paths to add to hierarchy.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_members_directory', $new_templates );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with directory data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_dummy_post() {
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => bp_get_directory_title( 'members' ),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the members index template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function directory_content() {
+		return bp_buffer_template_part( 'members/index', null, false );
+	}
+
+	/** Single ****************************************************************/
+
+	/**
+	 * Add custom template hierarchy to theme compat for member pages.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from
+	 *                          bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function single_template_hierarchy( $templates ) {
+		// Setup some variables we're going to reference in our custom templates.
+		$user_nicename = buddypress()->displayed_user->userdata->user_nicename;
+
+		/**
+		 * Filters the template hierarchy for theme compat and member pages.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of template paths to add to hierarchy.
+		 */
+		$new_templates = apply_filters( 'bp_template_hierarchy_members_single_item', array(
+			'members/single/index-id-'        . sanitize_file_name( bp_displayed_user_id() ) . '.php',
+			'members/single/index-nicename-'  . sanitize_file_name( $user_nicename )         . '.php',
+			'members/single/index-action-'    . sanitize_file_name( bp_current_action() )    . '.php',
+			'members/single/index-component-' . sanitize_file_name( bp_current_component() ) . '.php',
+			'members/single/index.php'
+		) );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with the displayed user's data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function single_dummy_post() {
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => bp_get_displayed_user_fullname(),
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with the members' single home template part.
+	 *
+	 * @since 1.7.0
+	 */
+	public function single_dummy_content() {
+		return bp_buffer_template_part( 'members/single/home', null, false );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-registration-theme-compat.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-registration-theme-compat.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-registration-theme-compat.php	(revision 12265)
@@ -0,0 +1,140 @@
+<?php
+/**
+ * BuddyPress Member Screens.
+ *
+ * Handlers for member screens that aren't handled elsewhere.
+ *
+ * @package BuddyPress
+ * @subpackage MembersScreens
+ * @since 1.7.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main theme compat class for BuddyPress Registration.
+ *
+ * This class sets up the necessary theme compatibility actions to safely output
+ * registration template parts to the_title and the_content areas of a theme.
+ *
+ * @since 1.7.0
+ */
+class BP_Registration_Theme_Compat {
+
+	/**
+	 * Setup the groups component theme compatibility.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __construct() {
+		add_action( 'bp_setup_theme_compat', array( $this, 'is_registration' ) );
+	}
+
+	/**
+	 * Are we looking at either the registration or activation pages?
+	 *
+	 * @since 1.7.0
+	 */
+	public function is_registration() {
+
+		// Bail if not looking at the registration or activation page.
+		if ( ! bp_is_register_page() && ! bp_is_activation_page() ) {
+			return;
+		}
+
+		// Not a directory.
+		bp_update_is_directory( false, 'register' );
+
+		// Setup actions.
+		add_filter( 'bp_get_buddypress_template',                array( $this, 'template_hierarchy' ) );
+		add_action( 'bp_template_include_reset_dummy_post_data', array( $this, 'dummy_post'    ) );
+		add_filter( 'bp_replace_the_content',                    array( $this, 'dummy_content' ) );
+	}
+
+	/** Template ***********************************************************/
+
+	/**
+	 * Add template hierarchy to theme compat for registration/activation pages.
+	 *
+	 * This is to mirror how WordPress has
+	 * {@link https://codex.wordpress.org/Template_Hierarchy template hierarchy}.
+	 *
+	 * @since 1.8.0
+	 *
+	 * @param string $templates The templates from bp_get_theme_compat_templates().
+	 * @return array $templates Array of custom templates to look for.
+	 */
+	public function template_hierarchy( $templates ) {
+		$component = sanitize_file_name( bp_current_component() );
+
+		/**
+		 * Filters the template hierarchy for theme compat and registration/activation pages.
+		 *
+		 * This filter is a variable filter that depends on the current component
+		 * being used.
+		 *
+		 * @since 1.8.0
+		 *
+		 * @param array $value Array of template paths to add to hierarchy.
+		 */
+		$new_templates = apply_filters( "bp_template_hierarchy_{$component}", array(
+			"members/index-{$component}.php"
+		) );
+
+		// Merge new templates with existing stack
+		// @see bp_get_theme_compat_templates().
+		$templates = array_merge( (array) $new_templates, $templates );
+
+		return $templates;
+	}
+
+	/**
+	 * Update the global $post with dummy data.
+	 *
+	 * @since 1.7.0
+	 */
+	public function dummy_post() {
+		// Registration page.
+		if ( bp_is_register_page() ) {
+			$title = __( 'Create an Account', 'buddypress' );
+
+			if ( 'completed-confirmation' == bp_get_current_signup_step() ) {
+				$title = __( 'Check Your Email To Activate Your Account!', 'buddypress' );
+			}
+
+		// Activation page.
+		} else {
+			$title = __( 'Activate Your Account', 'buddypress' );
+
+			if ( bp_account_was_activated() ) {
+				$title = __( 'Account Activated', 'buddypress' );
+			}
+		}
+
+		bp_theme_compat_reset_post( array(
+			'ID'             => 0,
+			'post_title'     => $title,
+			'post_author'    => 0,
+			'post_date'      => 0,
+			'post_content'   => '',
+			'post_type'      => 'page',
+			'post_status'    => 'publish',
+			'is_page'        => true,
+			'comment_status' => 'closed'
+		) );
+	}
+
+	/**
+	 * Filter the_content with either the register or activate templates.
+	 *
+	 * @since 1.7.0
+	 */
+	public function dummy_content() {
+		if ( bp_is_register_page() ) {
+			return bp_buffer_template_part( 'members/register', null, false );
+		} else {
+			return bp_buffer_template_part( 'members/activate', null, false );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/classes/class-bp-signup.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/classes/class-bp-signup.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/classes/class-bp-signup.php	(revision 12265)
@@ -0,0 +1,841 @@
+<?php
+/**
+ * Signups Management class.
+ *
+ * @package BuddyPress
+ * @subpackage coreClasses
+ * @since 2.0.0
+ */
+
+/**
+ * Class used to handle Signups.
+ *
+ * @since 2.0.0
+ */
+class BP_Signup {
+
+	/**
+	 * ID of the signup which the object relates to.
+	 *
+	 * @since 2.0.0
+	 * @var integer
+	 */
+	public $id;
+
+	/**
+	 * The URL to the full size of the avatar for the user.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $avatar;
+
+	/**
+	 * The username for the user.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $user_login;
+
+	/**
+	 * The email for the user.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $user_email;
+
+	/**
+	 * The full name of the user.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $user_name;
+
+	/**
+	 * Metadata associated with the signup.
+	 *
+	 * @since 2.0.0
+	 * @var array
+	 */
+	public $meta;
+
+	/**
+	 * The registered date for the user.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $registered;
+
+	/**
+	 * The activation key for the user.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $activation_key;
+
+
+	/** Public Methods *******************************************************/
+
+	/**
+	 * Class constructor.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param integer $signup_id The ID for the signup being queried.
+	 */
+	public function __construct( $signup_id = 0 ) {
+		if ( !empty( $signup_id ) ) {
+			$this->id = $signup_id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Populate the instantiated class with data based on the signup_id provided.
+	 *
+	 * @since 2.0.0
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$signups_table = buddypress()->members->table_name_signups;
+		$signup        = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$signups_table} WHERE signup_id = %d AND active = 0", $this->id ) );
+
+		$this->avatar         = get_avatar( $signup->user_email, 32 );
+		$this->user_login     = $signup->user_login;
+		$this->user_email     = $signup->user_email;
+		$this->meta           = maybe_unserialize( $signup->meta );
+		$this->user_name      = ! empty( $this->meta['field_1'] ) ? wp_unslash( $this->meta['field_1'] ) : '';
+		$this->registered     = $signup->registered;
+		$this->activation_key = $signup->activation_key;
+	}
+
+	/** Static Methods *******************************************************/
+
+	/**
+	 * Fetch signups based on parameters.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args {
+	 *     The argument to retrieve desired signups.
+	 *     @type int         $offset         Offset amount. Default 0.
+	 *     @type int         $number         How many to fetch. Default 1.
+	 *     @type bool|string $usersearch     Whether or not to search for a username. Default false.
+	 *     @type string      $orderby        Order By parameter. Default 'signup_id'.
+	 *     @type string      $order          Order direction. Default 'DESC'.
+	 *     @type bool        $include        Whether or not to include more specific query params.
+	 *     @type string      $activation_key Activation key to search for.
+	 *     @type string      $user_login     Specific user login to return.
+	 *     @type string      $fields         Which fields to return. Specify 'ids' to fetch a list of signups IDs.
+	 *                                       Default: 'all' (return BP_Signup objects).
+	 * }
+	 * @return array {
+	 *     @type array $signups Located signups. (IDs only if `fields` is set to `ids`.)
+	 *     @type int   $total   Total number of signups matching params.
+	 * }
+	 */
+	public static function get( $args = array() ) {
+		global $wpdb;
+
+		$r = bp_parse_args( $args,
+			array(
+				'offset'         => 0,
+				'number'         => 1,
+				'usersearch'     => false,
+				'orderby'        => 'signup_id',
+				'order'          => 'DESC',
+				'include'        => false,
+				'activation_key' => '',
+				'user_login'     => '',
+				'fields'         => 'all',
+			),
+			'bp_core_signups_get_args'
+		);
+
+		// @todo whitelist sanitization
+		if ( $r['orderby'] !== 'signup_id' ) {
+			$r['orderby'] = 'user_' . $r['orderby'];
+		}
+
+		$r['orderby'] = sanitize_title( $r['orderby'] );
+
+		$sql = array();
+		$signups_table  = buddypress()->members->table_name_signups;
+		$sql['select']  = "SELECT * FROM {$signups_table}";
+		$sql['where']   = array();
+		$sql['where'][] = "active = 0";
+
+		if ( empty( $r['include'] ) ) {
+
+			// Search terms.
+			if ( ! empty( $r['usersearch'] ) ) {
+				$search_terms_like = '%' . bp_esc_like( $r['usersearch'] ) . '%';
+				$sql['where'][]    = $wpdb->prepare( "( user_login LIKE %s OR user_email LIKE %s OR meta LIKE %s )", $search_terms_like, $search_terms_like, $search_terms_like );
+			}
+
+			// Activation key.
+			if ( ! empty( $r['activation_key'] ) ) {
+				$sql['where'][] = $wpdb->prepare( "activation_key = %s", $r['activation_key'] );
+			}
+
+			// User login.
+			if ( ! empty( $r['user_login'] ) ) {
+				$sql['where'][] = $wpdb->prepare( "user_login = %s", $r['user_login'] );
+			}
+
+			$sql['orderby'] = "ORDER BY {$r['orderby']}";
+			$sql['order']	= bp_esc_sql_order( $r['order'] );
+			$sql['limit']	= $wpdb->prepare( "LIMIT %d, %d", $r['offset'], $r['number'] );
+		} else {
+			$in = implode( ',', wp_parse_id_list( $r['include'] ) );
+			$sql['in'] = "AND signup_id IN ({$in})";
+		}
+
+		// Implode WHERE clauses.
+		$sql['where'] = 'WHERE ' . implode( ' AND ', $sql['where'] );
+
+		/**
+		 * Filters the Signups paged query.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $value SQL statement.
+		 * @param array  $sql   Array of SQL statement parts.
+		 * @param array  $args  Array of original arguments for get() method.
+		 * @param array  $r     Array of parsed arguments for get() method.
+		 */
+		$paged_signups = $wpdb->get_results( apply_filters( 'bp_members_signups_paged_query', join( ' ', $sql ), $sql, $args, $r ) );
+
+		if ( empty( $paged_signups ) ) {
+			return array( 'signups' => false, 'total' => false );
+		}
+
+		// We only want the IDs.
+		if ( 'ids' === $r['fields'] ) {
+			$paged_signups = wp_list_pluck( $paged_signups, 'signup_id' );
+		} else {
+
+			// Used to calculate a diff between now & last
+			// time an activation link has been resent.
+			$now = current_time( 'timestamp', true );
+
+			foreach ( (array) $paged_signups as $key => $signup ) {
+
+				$signup->id   = intval( $signup->signup_id );
+
+				$signup->meta = ! empty( $signup->meta ) ? maybe_unserialize( $signup->meta ) : false;
+
+				$signup->user_name = '';
+				if ( ! empty( $signup->meta['field_1'] ) ) {
+					$signup->user_name = wp_unslash( $signup->meta['field_1'] );
+				}
+
+				// Sent date defaults to date of registration.
+				if ( ! empty( $signup->meta['sent_date'] ) ) {
+					$signup->date_sent = $signup->meta['sent_date'];
+				} else {
+					$signup->date_sent = $signup->registered;
+				}
+
+				$sent_at = mysql2date('U', $signup->date_sent );
+				$diff    = $now - $sent_at;
+
+				/**
+				 * Add a boolean in case the last time an activation link
+				 * has been sent happened less than a day ago.
+				 */
+				if ( $diff < 1 * DAY_IN_SECONDS ) {
+					$signup->recently_sent = true;
+				}
+
+				if ( ! empty( $signup->meta['count_sent'] ) ) {
+					$signup->count_sent = absint( $signup->meta['count_sent'] );
+				} else {
+					$signup->count_sent = 1;
+				}
+
+				$paged_signups[ $key ] = $signup;
+			}
+		}
+
+		unset( $sql['limit'] );
+		$sql['select'] = preg_replace( "/SELECT.*?FROM/", "SELECT COUNT(*) FROM", $sql['select'] );
+
+		/**
+		 * Filters the Signups count query.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $value SQL statement.
+		 * @param array  $sql   Array of SQL statement parts.
+		 * @param array  $args  Array of original arguments for get() method.
+		 * @param array  $r     Array of parsed arguments for get() method.
+		 */
+		$total_signups = $wpdb->get_var( apply_filters( 'bp_members_signups_count_query', join( ' ', $sql ), $sql, $args, $r ) );
+
+		return array( 'signups' => $paged_signups, 'total' => $total_signups );
+	}
+
+	/**
+	 * Add a signup.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args {
+	 *     Array of arguments for signup addition.
+	 *     @type string     $domain         New user's domain.
+	 *     @type string     $path           New user's path.
+	 *     @type string     $title          New user's title.
+	 *     @type string     $user_login     New user's user_login.
+	 *     @type string     $user_email     New user's email address.
+	 *     @type int|string $registered     Time the user registered.
+	 *     @type string     $activation_key New user's activation key.
+	 *     @type string     $meta           New user's user meta.
+	 * }
+	 * @return int|bool ID of newly created signup on success, false on failure.
+	 */
+	public static function add( $args = array() ) {
+		global $wpdb;
+
+		$r = bp_parse_args( $args,
+			array(
+				'domain'         => '',
+				'path'           => '',
+				'title'          => '',
+				'user_login'     => '',
+				'user_email'     => '',
+				'registered'     => current_time( 'mysql', true ),
+				'activation_key' => '',
+				'meta'           => '',
+			),
+			'bp_core_signups_add_args'
+		);
+
+		$r['meta'] = maybe_serialize( $r['meta'] );
+
+		$inserted = $wpdb->insert(
+			buddypress()->members->table_name_signups,
+			$r,
+			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
+		);
+
+		if ( $inserted ) {
+			$retval = $wpdb->insert_id;
+		} else {
+			$retval = false;
+		}
+
+		/**
+		 * Filters the result of a signup addition.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int|bool $retval Newly added user ID on success, false on failure.
+		 */
+		return apply_filters( 'bp_core_signups_add', $retval );
+	}
+
+	/**
+	 * Create a WP user at signup.
+	 *
+	 * Since BP 2.0, non-multisite configurations have stored signups in
+	 * the same way as Multisite configs traditionally have: in the
+	 * wp_signups table. However, because some plugins may be looking
+	 * directly in the wp_users table for non-activated signups, we
+	 * mirror signups there by creating "phantom" users, mimicking WP's
+	 * default behavior.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $user_login    User login string.
+	 * @param string $user_password User password.
+	 * @param string $user_email    User email address.
+	 * @param array  $usermeta      Metadata associated with the signup.
+	 * @return int User id.
+	 */
+	public static function add_backcompat( $user_login = '', $user_password = '', $user_email = '', $usermeta = array() ) {
+		global $wpdb;
+
+		$user_id = wp_insert_user( array(
+			'user_login'   => $user_login,
+			'user_pass'    => $user_password,
+			'display_name' => sanitize_title( $user_login ),
+			'user_email'   => $user_email
+		) );
+
+		if ( is_wp_error( $user_id ) || empty( $user_id ) ) {
+			return $user_id;
+		}
+
+		// Update the user status to '2', ie "not activated"
+		// (0 = active, 1 = spam, 2 = not active).
+		$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->users} SET user_status = 2 WHERE ID = %d", $user_id ) );
+
+		// WordPress creates these options automatically on
+		// wp_insert_user(), but we delete them so that inactive
+		// signups don't appear in various user counts.
+		delete_user_option( $user_id, 'capabilities' );
+		delete_user_option( $user_id, 'user_level'   );
+
+		// Set any profile data.
+		if ( bp_is_active( 'xprofile' ) ) {
+			if ( ! empty( $usermeta['profile_field_ids'] ) ) {
+				$profile_field_ids = explode( ',', $usermeta['profile_field_ids'] );
+
+				foreach ( (array) $profile_field_ids as $field_id ) {
+					if ( empty( $usermeta["field_{$field_id}"] ) ) {
+						continue;
+					}
+
+					$current_field = $usermeta["field_{$field_id}"];
+					xprofile_set_field_data( $field_id, $user_id, $current_field );
+
+					/*
+					 * Save the visibility level.
+					 *
+					 * Use the field's default visibility if not present, and 'public' if a
+					 * default visibility is not defined.
+					 */
+					$key = "field_{$field_id}_visibility";
+					if ( isset( $usermeta[ $key ] ) ) {
+						$visibility_level = $usermeta[ $key ];
+					} else {
+						$vfield           = xprofile_get_field( $field_id );
+						$visibility_level = isset( $vfield->default_visibility ) ? $vfield->default_visibility : 'public';
+					}
+					xprofile_set_field_visibility_level( $field_id, $user_id, $visibility_level );
+				}
+			}
+		}
+
+		/**
+		 * Filters the user ID for the backcompat functionality.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int $user_id User ID being registered.
+		 */
+		return apply_filters( 'bp_core_signups_add_backcompat', $user_id );
+	}
+
+	/**
+	 * Check a user status (from wp_users) on a non-multisite config.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int $user_id ID of the user being checked.
+	 * @return int|bool The status if found, otherwise false.
+	 */
+	public static function check_user_status( $user_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) ) {
+			return false;
+		}
+
+		$user_status = $wpdb->get_var( $wpdb->prepare( "SELECT user_status FROM {$wpdb->users} WHERE ID = %d", $user_id ) );
+
+		/**
+		 * Filters the user status of a provided user ID.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int $value User status of the provided user ID.
+		 */
+		return apply_filters( 'bp_core_signups_check_user_status', intval( $user_status ) );
+	}
+
+	/**
+	 * Activate a signup.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $key Activation key.
+	 * @return bool True on success, false on failure.
+	 */
+	public static function validate( $key = '' ) {
+		global $wpdb;
+
+		if ( empty( $key ) ) {
+			return;
+		}
+
+		$activated = $wpdb->update(
+			// Signups table.
+			buddypress()->members->table_name_signups,
+			array(
+				'active' => 1,
+				'activated' => current_time( 'mysql', true ),
+			),
+			array(
+				'activation_key' => $key,
+			),
+			// Data sanitization format.
+			array(
+				'%d',
+				'%s',
+			),
+			// WHERE sanitization format.
+			array(
+				'%s',
+			)
+		);
+
+		/**
+		 * Filters the status of the activated user.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param bool $activated Whether or not the activation was successful.
+		 */
+		return apply_filters( 'bp_core_signups_validate', $activated );
+	}
+
+	/**
+	 * How many inactive signups do we have?
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return int The number of signups.
+	 */
+	public static function count_signups() {
+		global $wpdb;
+
+		$signups_table = buddypress()->members->table_name_signups;
+		$count_signups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) AS total FROM {$signups_table} WHERE active = %d", 0 ) );
+
+		/**
+		 * Filters the total inactive signups.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int $count_signups How many total signups there are.
+		 */
+		return apply_filters( 'bp_core_signups_count', (int) $count_signups );
+	}
+
+	/**
+	 * Update the meta for a signup.
+	 *
+	 * This is the way we use to "trace" the last date an activation
+	 * email was sent and how many times activation was sent.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args {
+	 *     Array of arguments for the signup update.
+	 *     @type int $signup_id User signup ID.
+	 *     @type array $meta Meta to update.
+	 * }
+	 * @return int The signup id.
+	 */
+	public static function update( $args = array() ) {
+		global $wpdb;
+
+		$r = bp_parse_args( $args,
+			array(
+				'signup_id'  => 0,
+				'meta'       => array(),
+			),
+			'bp_core_signups_update_args'
+		);
+
+		if ( empty( $r['signup_id'] ) || empty( $r['meta'] ) ) {
+			return false;
+		}
+
+		$wpdb->update(
+			// Signups table.
+			buddypress()->members->table_name_signups,
+			// Data to update.
+			array(
+				'meta' => serialize( $r['meta'] ),
+			),
+			// WHERE.
+			array(
+				'signup_id' => $r['signup_id'],
+			),
+			// Data sanitization format.
+			array(
+				'%s',
+			),
+			// WHERE sanitization format.
+			array(
+				'%d',
+			)
+		);
+
+		/**
+		 * Filters the signup ID which received a meta update.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int $value The signup ID.
+		 */
+		return apply_filters( 'bp_core_signups_update', $r['signup_id'] );
+	}
+
+	/**
+	 * Resend an activation email.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $signup_ids Single ID or list of IDs to resend.
+	 * @return array
+	 */
+	public static function resend( $signup_ids = array() ) {
+		if ( empty( $signup_ids ) || ! is_array( $signup_ids ) ) {
+			return false;
+		}
+
+		$to_resend = self::get( array(
+			'include' => $signup_ids,
+		) );
+
+		if ( ! $signups = $to_resend['signups'] ) {
+			return false;
+		}
+
+		$result = array();
+
+		/**
+		 * Fires before activation emails are resent.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $signup_ids Array of IDs to resend activation emails to.
+		 */
+		do_action( 'bp_core_signup_before_resend', $signup_ids );
+
+		foreach ( $signups as $signup ) {
+
+			$meta               = $signup->meta;
+			$meta['sent_date']  = current_time( 'mysql', true );
+			$meta['count_sent'] = $signup->count_sent + 1;
+
+			// Send activation email.
+			if ( is_multisite() ) {
+				wpmu_signup_user_notification( $signup->user_login, $signup->user_email, $signup->activation_key, serialize( $meta ) );
+			} else {
+
+				// Check user status before sending email.
+				$user_id = email_exists( $signup->user_email );
+
+				if ( ! empty( $user_id ) && 2 != self::check_user_status( $user_id ) ) {
+
+					// Status is not 2, so user's account has been activated.
+					$result['errors'][ $signup->signup_id ] = array( $signup->user_login, esc_html__( 'the sign-up has already been activated.', 'buddypress' ) );
+
+					// Repair signups table.
+					self::validate( $signup->activation_key );
+
+					continue;
+
+				// Send the validation email.
+				} else {
+					bp_core_signup_send_validation_email( false, $signup->user_email, $signup->activation_key, $signup->user_login );
+				}
+			}
+
+			// Update metas.
+			$result['resent'][] = self::update( array(
+				'signup_id' => $signup->signup_id,
+				'meta'      => $meta,
+			) );
+		}
+
+		/**
+		 * Fires after activation emails are resent.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $signup_ids Array of IDs to resend activation emails to.
+		 * @param array $result     Updated metadata related to activation emails.
+		 */
+		do_action( 'bp_core_signup_after_resend', $signup_ids, $result );
+
+		/**
+		 * Filters the result of the metadata for signup activation email resends.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $result Updated metadata related to activation emails.
+		 */
+		return apply_filters( 'bp_core_signup_resend', $result );
+	}
+
+	/**
+	 * Activate a pending account.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $signup_ids Single ID or list of IDs to activate.
+	 * @return array
+	 */
+	public static function activate( $signup_ids = array() ) {
+		if ( empty( $signup_ids ) || ! is_array( $signup_ids ) ) {
+			return false;
+		}
+
+		$to_activate = self::get( array(
+			'include' => $signup_ids,
+		) );
+
+		if ( ! $signups = $to_activate['signups'] ) {
+			return false;
+		}
+
+		$result = array();
+
+		/**
+		 * Fires before activation of user accounts.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $signup_ids Array of IDs to activate.
+		 */
+		do_action( 'bp_core_signup_before_activate', $signup_ids );
+
+		foreach ( $signups as $signup ) {
+
+			$user = bp_core_activate_signup( $signup->activation_key );
+
+			if ( ! empty( $user->errors ) ) {
+
+				$user_id = username_exists( $signup->user_login );
+
+				if ( 2 !== self::check_user_status( $user_id ) ) {
+					$user_id = false;
+				}
+
+				if ( empty( $user_id ) ) {
+
+					// Status is not 2, so user's account has been activated.
+					$result['errors'][ $signup->signup_id ] = array( $signup->user_login, esc_html__( 'the sign-up has already been activated.', 'buddypress' ) );
+
+					// Repair signups table.
+					self::validate( $signup->activation_key );
+
+				// We have a user id, account is not active, let's delete it.
+				} else {
+					$result['errors'][ $signup->signup_id ] = array( $signup->user_login, $user->get_error_message() );
+				}
+
+			} else {
+				$result['activated'][] = $user;
+			}
+		}
+
+		/**
+		 * Fires after activation of user accounts.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $signup_ids Array of IDs activated activate.
+		 * @param array $result     Array of data for activated accounts.
+		 */
+		do_action( 'bp_core_signup_after_activate', $signup_ids, $result );
+
+		/**
+		 * Filters the result of the metadata after user activation.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $result Updated metadata related to user activation.
+		 */
+		return apply_filters( 'bp_core_signup_activate', $result );
+	}
+
+	/**
+	 * Delete a pending account.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $signup_ids Single ID or list of IDs to delete.
+	 * @return array
+	 */
+	public static function delete( $signup_ids = array() ) {
+		global $wpdb;
+
+		if ( empty( $signup_ids ) || ! is_array( $signup_ids ) ) {
+			return false;
+		}
+
+		$to_delete = self::get( array(
+			'include' => $signup_ids,
+		) );
+
+		if ( ! $signups = $to_delete['signups'] ) {
+			return false;
+		}
+
+		$result = array();
+
+		/**
+		 * Fires before deletion of pending accounts.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $signup_ids Array of pending IDs to delete.
+		 */
+		do_action( 'bp_core_signup_before_delete', $signup_ids );
+
+		foreach ( $signups as $signup ) {
+			$user_id = username_exists( $signup->user_login );
+
+			if ( ! empty( $user_id ) && $signup->activation_key === bp_get_user_meta( $user_id, 'activation_key', true ) ) {
+
+				if ( 2 != self::check_user_status( $user_id ) ) {
+
+					// Status is not 2, so user's account has been activated.
+					$result['errors'][ $signup->signup_id ] = array( $signup->user_login, esc_html__( 'the sign-up has already been activated.', 'buddypress' ) );
+
+					// Repair signups table.
+					self::validate( $signup->activation_key );
+
+				// We have a user id, account is not active, let's delete it.
+				} else {
+					bp_core_delete_account( $user_id );
+				}
+			}
+
+			if ( empty( $result['errors'][ $signup->signup_id ] ) ) {
+				$wpdb->delete(
+					// Signups table.
+					buddypress()->members->table_name_signups,
+					// Where.
+					array( 'signup_id' => $signup->signup_id, ),
+					// WHERE sanitization format.
+					array( '%d', )
+				);
+
+				$result['deleted'][] = $signup->signup_id;
+			}
+		}
+
+		/**
+		 * Fires after deletion of pending accounts.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $signup_ids Array of pending IDs to delete.
+		 * @param array $result     Array of data for deleted accounts.
+		 */
+		do_action( 'bp_core_signup_after_delete', $signup_ids, $result );
+
+		/**
+		 * Filters the result of the metadata for deleted pending accounts.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $result Updated metadata related to deleted pending accounts.
+		 */
+		return apply_filters( 'bp_core_signup_delete', $result );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-members/screens/activate.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/screens/activate.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/screens/activate.php	(revision 12265)
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Members: Activate screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MembersScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the loading of the Activate screen.
+ *
+ * @since 1.1.0
+ */
+function bp_core_screen_activation() {
+
+	// Bail if not viewing the activation page.
+	if ( ! bp_is_current_component( 'activate' ) ) {
+		return false;
+	}
+
+	// If the user is already logged in, redirect away from here.
+	if ( is_user_logged_in() ) {
+
+		// If activation page is also front page, set to members directory to
+		// avoid an infinite loop. Otherwise, set to root domain.
+		$redirect_to = bp_is_component_front_page( 'activate' )
+			? bp_get_members_directory_permalink()
+			: bp_get_root_domain();
+
+		// Trailing slash it, as we expect these URL's to be.
+		$redirect_to = trailingslashit( $redirect_to );
+
+		/**
+		 * Filters the URL to redirect logged in users to when visiting activation page.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $redirect_to URL to redirect user to.
+		 */
+		$redirect_to = apply_filters( 'bp_loggedin_activate_page_redirect_to', $redirect_to );
+
+		// Redirect away from the activation page.
+		bp_core_redirect( $redirect_to );
+	}
+
+	// Get BuddyPress.
+	$bp = buddypress();
+
+	/**
+	 * Filters the template to load for the Member activation page screen.
+	 *
+	 * @since 1.1.1
+	 *
+	 * @param string $value Path to the Member activation template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_core_template_activate', array( 'activate', 'registration/activate' ) ) );
+}
+add_action( 'bp_screens', 'bp_core_screen_activation' );
+
+
+/**
+ * Catches and processes account activation requests.
+ *
+ * @since 3.0.0
+ */
+function bp_members_action_activate_account() {
+	if ( ! bp_is_current_component( 'activate' ) ) {
+		return;
+	}
+
+	if ( is_user_logged_in() ) {
+		return;
+	}
+
+	if ( ! empty( $_POST['key'] ) ) {
+		$key = wp_unslash( $_POST['key'] );
+
+	// Backward compatibility with templates using `method="get"` in their activation forms.
+	} elseif ( ! empty( $_GET['key'] ) ) {
+		$key = wp_unslash( $_GET['key'] );
+	}
+
+	if ( empty( $key ) ) {
+		return;
+	}
+
+	$bp = buddypress();
+
+	/**
+	 * Filters the activation signup.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool|int $value Value returned by activation.
+	 *                        Integer on success, boolean on failure.
+	 */
+	$user = apply_filters( 'bp_core_activate_account', bp_core_activate_signup( $key ) );
+
+	// If there were errors, add a message and redirect.
+	if ( ! empty( $user->errors ) ) {
+		bp_core_add_message( $user->get_error_message(), 'error' );
+		bp_core_redirect( trailingslashit( bp_get_root_domain() . '/' . $bp->pages->activate->slug ) );
+	}
+
+	bp_core_add_message( __( 'Your account is now active!', 'buddypress' ) );
+	bp_core_redirect( add_query_arg( 'activated', '1', bp_get_activation_page() ) );
+
+}
+add_action( 'bp_actions', 'bp_members_action_activate_account' );
Index: /tags/4.0.0-beta1/src/bp-members/screens/directory.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/screens/directory.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/screens/directory.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Members: Directory screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MembersScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of the members directory index.
+ *
+ * @since 1.5.0
+ */
+function bp_members_screen_index() {
+	if ( bp_is_members_directory() ) {
+		bp_update_is_directory( true, 'members' );
+
+		/**
+		 * Fires right before the loading of the Member directory index screen template file.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_members_screen_index' );
+
+		/**
+		 * Filters the template to load for the Member directory page screen.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $value Path to the member directory template to load.
+		 */
+		bp_core_load_template( apply_filters( 'bp_members_screen_index', 'members/index' ) );
+	}
+}
+add_action( 'bp_screens', 'bp_members_screen_index' );
Index: /tags/4.0.0-beta1/src/bp-members/screens/profile.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/screens/profile.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/screens/profile.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Members: Profile screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MembersScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the display of the profile page by loading the correct template file.
+ *
+ * @since 1.5.0
+ */
+function bp_members_screen_display_profile() {
+
+	/**
+	 * Fires right before the loading of the Member profile screen template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_members_screen_display_profile' );
+
+	/**
+	 * Filters the template to load for the Member profile page screen.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $template Path to the Member template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_members_screen_display_profile', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-members/screens/register.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-members/screens/register.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-members/screens/register.php	(revision 12265)
@@ -0,0 +1,240 @@
+<?php
+/**
+ * Members: Register screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MembersScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handle the loading of the signup screen.
+ *
+ * @since 1.1.0
+ */
+function bp_core_screen_signup() {
+	$bp = buddypress();
+
+	if ( ! bp_is_current_component( 'register' ) || bp_current_action() )
+		return;
+
+	// Not a directory.
+	bp_update_is_directory( false, 'register' );
+
+	// If the user is logged in, redirect away from here.
+	if ( is_user_logged_in() ) {
+
+		$redirect_to = bp_is_component_front_page( 'register' )
+			? bp_get_members_directory_permalink()
+			: bp_get_root_domain();
+
+		/**
+		 * Filters the URL to redirect logged in users to when visiting registration page.
+		 *
+		 * @since 1.5.1
+		 *
+		 * @param string $redirect_to URL to redirect user to.
+		 */
+		bp_core_redirect( apply_filters( 'bp_loggedin_register_page_redirect_to', $redirect_to ) );
+
+		return;
+	}
+
+	$bp->signup->step = 'request-details';
+
+	if ( !bp_get_signup_allowed() ) {
+		$bp->signup->step = 'registration-disabled';
+
+		// If the signup page is submitted, validate and save.
+	} elseif ( isset( $_POST['signup_submit'] ) && bp_verify_nonce_request( 'bp_new_signup' ) ) {
+
+		/**
+		 * Fires before the validation of a new signup.
+		 *
+		 * @since 2.0.0
+		 */
+		do_action( 'bp_signup_pre_validate' );
+
+		// Check the base account details for problems.
+		$account_details = bp_core_validate_user_signup( $_POST['signup_username'], $_POST['signup_email'] );
+
+		// If there are errors with account details, set them for display.
+		if ( !empty( $account_details['errors']->errors['user_name'] ) )
+			$bp->signup->errors['signup_username'] = $account_details['errors']->errors['user_name'][0];
+
+		if ( !empty( $account_details['errors']->errors['user_email'] ) )
+			$bp->signup->errors['signup_email'] = $account_details['errors']->errors['user_email'][0];
+
+		// Check that both password fields are filled in.
+		if ( empty( $_POST['signup_password'] ) || empty( $_POST['signup_password_confirm'] ) )
+			$bp->signup->errors['signup_password'] = __( 'Please make sure you enter your password twice', 'buddypress' );
+
+		// Check that the passwords match.
+		if ( ( !empty( $_POST['signup_password'] ) && !empty( $_POST['signup_password_confirm'] ) ) && $_POST['signup_password'] != $_POST['signup_password_confirm'] )
+			$bp->signup->errors['signup_password'] = __( 'The passwords you entered do not match.', 'buddypress' );
+
+		if ( bp_signup_requires_privacy_policy_acceptance() && ! empty( $_POST['signup-privacy-policy-check'] ) && empty( $_POST['signup-privacy-policy-accept'] ) ) {
+			$bp->signup->errors['signup_privacy_policy'] = __( 'You must indicate that you have read and agreed to the Privacy Policy.', 'buddypress' );
+		}
+
+		$bp->signup->username = $_POST['signup_username'];
+		$bp->signup->email = $_POST['signup_email'];
+
+		// Now we've checked account details, we can check profile information.
+		if ( bp_is_active( 'xprofile' ) ) {
+
+			// Make sure hidden field is passed and populated.
+			if ( isset( $_POST['signup_profile_field_ids'] ) && !empty( $_POST['signup_profile_field_ids'] ) ) {
+
+				// Let's compact any profile field info into an array.
+				$profile_field_ids = explode( ',', $_POST['signup_profile_field_ids'] );
+
+				// Loop through the posted fields formatting any datebox values then validate the field.
+				foreach ( (array) $profile_field_ids as $field_id ) {
+					bp_xprofile_maybe_format_datebox_post_data( $field_id );
+
+					// Trim post fields.
+					if ( isset( $_POST[ 'field_' . $field_id ] ) ) {
+						if ( is_array( $_POST[ 'field_' . $field_id ] ) ) {
+							$_POST[ 'field_' . $field_id ] = array_map( 'trim', $_POST[ 'field_' . $field_id ] );
+						} else {
+							$_POST[ 'field_' . $field_id ] = trim( $_POST[ 'field_' . $field_id ] );
+						}
+					}
+
+					// Create errors for required fields without values.
+					if ( xprofile_check_is_required_field( $field_id ) && empty( $_POST[ 'field_' . $field_id ] ) && ! bp_current_user_can( 'bp_moderate' ) )
+						$bp->signup->errors['field_' . $field_id] = __( 'This is a required field', 'buddypress' );
+				}
+
+				// This situation doesn't naturally occur so bounce to website root.
+			} else {
+				bp_core_redirect( bp_get_root_domain() );
+			}
+		}
+
+		// Finally, let's check the blog details, if the user wants a blog and blog creation is enabled.
+		if ( isset( $_POST['signup_with_blog'] ) ) {
+			$active_signup = bp_core_get_root_option( 'registration' );
+
+			if ( 'blog' == $active_signup || 'all' == $active_signup ) {
+				$blog_details = bp_core_validate_blog_signup( $_POST['signup_blog_url'], $_POST['signup_blog_title'] );
+
+				// If there are errors with blog details, set them for display.
+				if ( !empty( $blog_details['errors']->errors['blogname'] ) )
+					$bp->signup->errors['signup_blog_url'] = $blog_details['errors']->errors['blogname'][0];
+
+				if ( !empty( $blog_details['errors']->errors['blog_title'] ) )
+					$bp->signup->errors['signup_blog_title'] = $blog_details['errors']->errors['blog_title'][0];
+			}
+		}
+
+		/**
+		 * Fires after the validation of a new signup.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_signup_validate' );
+
+		// Add any errors to the action for the field in the template for display.
+		if ( !empty( $bp->signup->errors ) ) {
+			foreach ( (array) $bp->signup->errors as $fieldname => $error_message ) {
+				/**
+				 * Filters the error message in the loop.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string $value Error message wrapped in html.
+				 */
+				add_action( 'bp_' . $fieldname . '_errors', function() use ( $error_message ) {
+					echo apply_filters( 'bp_members_signup_error_message', "<div class=\"error\">" . $error_message . "</div>" );
+				} );
+			}
+		} else {
+			$bp->signup->step = 'save-details';
+
+			// No errors! Let's register those deets.
+			$active_signup = bp_core_get_root_option( 'registration' );
+
+			if ( 'none' != $active_signup ) {
+
+				// Make sure the extended profiles module is enabled.
+				if ( bp_is_active( 'xprofile' ) ) {
+					// Let's compact any profile field info into usermeta.
+					$profile_field_ids = explode( ',', $_POST['signup_profile_field_ids'] );
+
+					/*
+					 * Loop through the posted fields, formatting any
+					 * datebox values, then add to usermeta.
+					 */
+					foreach ( (array) $profile_field_ids as $field_id ) {
+						bp_xprofile_maybe_format_datebox_post_data( $field_id );
+
+						if ( !empty( $_POST['field_' . $field_id] ) )
+							$usermeta['field_' . $field_id] = $_POST['field_' . $field_id];
+
+						if ( !empty( $_POST['field_' . $field_id . '_visibility'] ) )
+							$usermeta['field_' . $field_id . '_visibility'] = $_POST['field_' . $field_id . '_visibility'];
+					}
+
+					// Store the profile field ID's in usermeta.
+					$usermeta['profile_field_ids'] = $_POST['signup_profile_field_ids'];
+				}
+
+				// Hash and store the password.
+				$usermeta['password'] = wp_hash_password( $_POST['signup_password'] );
+
+				// If the user decided to create a blog, save those details to usermeta.
+				if ( 'blog' == $active_signup || 'all' == $active_signup )
+					$usermeta['public'] = ( isset( $_POST['signup_blog_privacy'] ) && 'public' == $_POST['signup_blog_privacy'] ) ? true : false;
+
+				/**
+				 * Filters the user meta used for signup.
+				 *
+				 * @since 1.1.0
+				 *
+				 * @param array $usermeta Array of user meta to add to signup.
+				 */
+				$usermeta = apply_filters( 'bp_signup_usermeta', $usermeta );
+
+				// Finally, sign up the user and/or blog.
+				if ( isset( $_POST['signup_with_blog'] ) && is_multisite() )
+					$wp_user_id = bp_core_signup_blog( $blog_details['domain'], $blog_details['path'], $blog_details['blog_title'], $_POST['signup_username'], $_POST['signup_email'], $usermeta );
+				else
+					$wp_user_id = bp_core_signup_user( $_POST['signup_username'], $_POST['signup_password'], $_POST['signup_email'], $usermeta );
+
+				if ( is_wp_error( $wp_user_id ) ) {
+					$bp->signup->step = 'request-details';
+					bp_core_add_message( $wp_user_id->get_error_message(), 'error' );
+				} else {
+					$bp->signup->step = 'completed-confirmation';
+				}
+			}
+
+			/**
+			 * Fires after the completion of a new signup.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_complete_signup' );
+		}
+
+	}
+
+	/**
+	 * Fires right before the loading of the Member registration screen template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_core_screen_signup' );
+
+	/**
+	 * Filters the template to load for the Member registration page screen.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value Path to the Member registration template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_core_template_register', array( 'register', 'registration/register' ) ) );
+}
+add_action( 'bp_screens', 'bp_core_screen_signup' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/bulk-delete.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/bulk-delete.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/bulk-delete.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Messages: Bulk-delete action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Process a request to bulk delete messages.
+ *
+ * @return bool False on failure.
+ */
+function messages_action_bulk_delete() {
+
+	if ( ! bp_is_messages_component() || ! bp_is_action_variable( 'bulk-delete', 0 ) ) {
+		return false;
+	}
+
+	$thread_ids = $_POST['thread_ids'];
+
+	if ( !$thread_ids || !messages_check_thread_access( $thread_ids ) ) {
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() ) );
+	} else {
+		if ( !check_admin_referer( 'messages_delete_thread' ) ) {
+			return false;
+		}
+
+		if ( !messages_delete_thread( $thread_ids ) ) {
+			bp_core_add_message( __('There was an error deleting messages.', 'buddypress'), 'error' );
+		} else {
+			bp_core_add_message( __('Messages deleted.', 'buddypress') );
+		}
+
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() ) );
+	}
+}
+add_action( 'bp_actions', 'messages_action_bulk_delete' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/bulk-manage-star.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/bulk-manage-star.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/bulk-manage-star.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Messages: Bulk-manage star action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Bulk manage handler to set the star status for multiple messages.
+ *
+ * @since 2.3.0
+ */
+function bp_messages_star_bulk_manage_handler() {
+	if ( empty( $_POST['messages_bulk_nonce' ] ) ) {
+		return;
+	}
+
+	// Check the nonce.
+	if ( ! wp_verify_nonce( $_POST['messages_bulk_nonce'], 'messages_bulk_nonce' ) ) {
+		return;
+	}
+
+	// Check capability.
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		return;
+	}
+
+	$action  = ! empty( $_POST['messages_bulk_action'] ) ? $_POST['messages_bulk_action'] : '';
+	$threads = ! empty( $_POST['message_ids'] ) ? $_POST['message_ids'] : '';
+	$threads = wp_parse_id_list( $threads );
+
+	// Bail if action doesn't match our star actions or no IDs.
+	if ( false === in_array( $action, array( 'star', 'unstar' ), true ) || empty( $threads ) ) {
+		return;
+	}
+
+	// It's star time!
+	switch ( $action ) {
+		case 'star' :
+			$count = count( $threads );
+
+			// If we're starring a thread, we only star the first message in the thread.
+			foreach ( $threads as $thread ) {
+				$thread = new BP_Messages_thread( $thread );
+				$mids = wp_list_pluck( $thread->messages, 'id' );
+
+				bp_messages_star_set_action( array(
+					'action'     => 'star',
+					'message_id' => $mids[0],
+				) );
+			}
+
+			bp_core_add_message( sprintf( _n( '%s message was successfully starred', '%s messages were successfully starred', $count, 'buddypress' ), $count ) );
+			break;
+
+		case 'unstar' :
+			$count = count( $threads );
+
+			foreach ( $threads as $thread ) {
+				bp_messages_star_set_action( array(
+					'action'    => 'unstar',
+					'thread_id' => $thread,
+					'bulk'      => true
+				) );
+			}
+
+			bp_core_add_message( sprintf( _n( '%s message was successfully unstarred', '%s messages were successfully unstarred', $count, 'buddypress' ), $count ) );
+			break;
+	}
+
+	// Redirect back to message box.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() . '/' );
+	die();
+}
+add_action( 'bp_actions', 'bp_messages_star_bulk_manage_handler', 5 );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/bulk-manage.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/bulk-manage.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/bulk-manage.php	(revision 12265)
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Messages: Bulk-manage action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handle bulk management (mark as read/unread, delete) of message threads.
+ *
+ * @since 2.2.0
+ *
+ * @return bool Returns false on failure. Otherwise redirects back to the
+ *              message box URL.
+ */
+function bp_messages_action_bulk_manage() {
+
+	if ( ! bp_is_messages_component() || bp_is_current_action( 'notices' ) || ! bp_is_action_variable( 'bulk-manage', 0 ) ) {
+		return false;
+	}
+
+	$action   = ! empty( $_POST['messages_bulk_action'] ) ? $_POST['messages_bulk_action'] : '';
+	$nonce    = ! empty( $_POST['messages_bulk_nonce'] ) ? $_POST['messages_bulk_nonce'] : '';
+	$messages = ! empty( $_POST['message_ids'] ) ? $_POST['message_ids'] : '';
+
+	$messages = wp_parse_id_list( $messages );
+
+	// Bail if no action or no IDs.
+	if ( ( ! in_array( $action, array( 'delete', 'read', 'unread' ) ) ) || empty( $messages ) || empty( $nonce ) ) {
+		bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() . '/' );
+	}
+
+	// Check the nonce.
+	if ( ! wp_verify_nonce( $nonce, 'messages_bulk_nonce' ) ) {
+		return false;
+	}
+
+	// Make sure the user has access to all notifications before managing them.
+	foreach ( $messages as $message ) {
+		if ( ! messages_check_thread_access( $message ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+			bp_core_add_message( __( 'There was a problem managing your messages.', 'buddypress' ), 'error' );
+			bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() . '/' );
+		}
+	}
+
+	// Delete, mark as read or unread depending on the user 'action'.
+	switch ( $action ) {
+		case 'delete' :
+			foreach ( $messages as $message ) {
+				messages_delete_thread( $message );
+			}
+			bp_core_add_message( __( 'Messages deleted.', 'buddypress' ) );
+		break;
+
+		case 'read' :
+			foreach ( $messages as $message ) {
+				messages_mark_thread_read( $message );
+			}
+			bp_core_add_message( __( 'Messages marked as read', 'buddypress' ) );
+		break;
+
+		case 'unread' :
+			foreach ( $messages as $message ) {
+				messages_mark_thread_unread( $message );
+			}
+			bp_core_add_message( __( 'Messages marked as unread.', 'buddypress' ) );
+		break;
+	}
+
+	// Redirect back to message box.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() . '/' );
+}
+add_action( 'bp_actions', 'bp_messages_action_bulk_manage' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/compose.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/compose.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/compose.php	(revision 12265)
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Messages: Compose action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handle creating of private messages or sitewide notices
+ *
+ * @since 2.4.0 This function was split from messages_screen_compose(). See #6505.
+ *
+ * @return boolean
+ */
+function bp_messages_action_create_message() {
+
+	// Bail if not posting to the compose message screen.
+	if ( ! bp_is_post_request() || ! bp_is_messages_component() || ! bp_is_current_action( 'compose' ) ) {
+		return false;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'messages_send_message' );
+
+	// Define local variables.
+	$redirect_to = '';
+	$feedback    = '';
+	$success     = false;
+
+	// Missing subject or content.
+	if ( empty( $_POST['subject'] ) || empty( $_POST['content'] ) ) {
+		$success  = false;
+
+		if ( empty( $_POST['subject'] ) ) {
+			$feedback = __( 'Your message was not sent. Please enter a subject line.', 'buddypress' );
+		} else {
+			$feedback = __( 'Your message was not sent. Please enter some content.', 'buddypress' );
+		}
+
+	// Subject and content present.
+	} else {
+
+		// Setup the link to the logged-in user's messages.
+		$member_messages = trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() );
+
+		// Site-wide notice.
+		if ( isset( $_POST['send-notice'] ) ) {
+
+			// Attempt to save the notice and redirect to notices.
+			if ( messages_send_notice( $_POST['subject'], $_POST['content'] ) ) {
+				$success     = true;
+				$feedback    = __( 'Notice successfully created.', 'buddypress' );
+				$redirect_to = trailingslashit( $member_messages . 'notices' );
+
+			// Notice could not be sent.
+			} else {
+				$success  = false;
+				$feedback = __( 'Notice was not created. Please try again.', 'buddypress' );
+			}
+
+		// Private conversation.
+		} else {
+
+			// Filter recipients into the format we need - array( 'username/userid', 'username/userid' ).
+			$autocomplete_recipients = (array) explode( ',', $_POST['send-to-input']     );
+			$typed_recipients        = (array) explode( ' ', $_POST['send_to_usernames'] );
+			$recipients              = array_merge( $autocomplete_recipients, $typed_recipients );
+
+			/**
+			 * Filters the array of recipients to receive the composed message.
+			 *
+			 * @since 1.2.10
+			 *
+			 * @param array $recipients Array of recipients to receive message.
+			 */
+			$recipients = apply_filters( 'bp_messages_recipients', $recipients );
+
+			// Attempt to send the message.
+			$send = messages_new_message( array(
+				'recipients' => $recipients,
+				'subject'    => $_POST['subject'],
+				'content'    => $_POST['content'],
+				'error_type' => 'wp_error'
+			) );
+
+			// Send the message and redirect to it.
+			if ( true === is_int( $send ) ) {
+				$success     = true;
+				$feedback    = __( 'Message successfully sent.', 'buddypress' );
+				$view        = trailingslashit( $member_messages . 'view' );
+				$redirect_to = trailingslashit( $view . $send );
+
+			// Message could not be sent.
+			} else {
+				$success  = false;
+				$feedback = $send->get_error_message();
+			}
+		}
+	}
+
+	// Feedback.
+	if ( ! empty( $feedback ) ) {
+
+		// Determine message type.
+		$type = ( true === $success )
+			? 'success'
+			: 'error';
+
+		// Add feedback message.
+		bp_core_add_message( $feedback, $type );
+	}
+
+	// Maybe redirect.
+	if ( ! empty( $redirect_to ) ) {
+		bp_core_redirect( $redirect_to );
+	}
+}
+add_action( 'bp_actions', 'bp_messages_action_create_message' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/delete.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/delete.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/delete.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Messages: Delete action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Process a request to delete a message.
+ *
+ * @return bool False on failure.
+ */
+function messages_action_delete_message() {
+
+	if ( ! bp_is_messages_component() || bp_is_current_action( 'notices' ) || ! bp_is_action_variable( 'delete', 0 ) ) {
+		return false;
+	}
+
+	$thread_id = bp_action_variable( 1 );
+
+	if ( !$thread_id || !is_numeric( $thread_id ) || !messages_check_thread_access( $thread_id ) ) {
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() ) );
+	} else {
+		if ( ! check_admin_referer( 'messages_delete_thread' ) ) {
+			return false;
+		}
+
+		// Delete message.
+		if ( !messages_delete_thread( $thread_id ) ) {
+			bp_core_add_message( __('There was an error deleting that message.', 'buddypress'), 'error' );
+		} else {
+			bp_core_add_message( __('Message deleted.', 'buddypress') );
+		}
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() ) );
+	}
+}
+add_action( 'bp_actions', 'messages_action_delete_message' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/notices.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/notices.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/notices.php	(revision 12265)
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Messages: Edit notice action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handle editing of sitewide notices.
+ *
+ * @since 2.4.0 This function was split from messages_screen_notices(). See #6505.
+ *
+ * @return boolean
+ */
+function bp_messages_action_edit_notice() {
+
+	// Bail if not viewing a single notice URL.
+	if ( ! bp_is_messages_component() || ! bp_is_current_action( 'notices' ) ) {
+		return false;
+	}
+
+	// Get the notice ID (1|2|3).
+	$notice_id = bp_action_variable( 1 );
+
+	// Bail if notice ID is not numeric.
+	if ( empty( $notice_id ) || ! is_numeric( $notice_id ) ) {
+		return false;
+	}
+
+	// Bail if the current user doesn't have administrator privileges.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	// Get the action (deactivate|activate|delete).
+	$action = sanitize_key( bp_action_variable( 0 ) );
+
+	// Check the nonce.
+	check_admin_referer( "messages_{$action}_notice" );
+
+	// Get the notice from database.
+	$notice   = new BP_Messages_Notice( $notice_id );
+	$success  = false;
+	$feedback = '';
+
+	// Take action.
+	switch ( $action ) {
+
+		// Deactivate.
+		case 'deactivate' :
+			$success  = $notice->deactivate();
+			$feedback = true === $success
+				? __( 'Notice deactivated successfully.',              'buddypress' )
+				: __( 'There was a problem deactivating that notice.', 'buddypress' );
+			break;
+
+		// Activate.
+		case 'activate' :
+			$success  = $notice->activate();
+			$feedback = true === $success
+				? __( 'Notice activated successfully.',              'buddypress' )
+				: __( 'There was a problem activating that notice.', 'buddypress' );
+			break;
+
+		// Delete.
+		case 'delete' :
+			$success  = $notice->delete();
+			$feedback = true === $success
+				? __( 'Notice deleted successfully.',              'buddypress' )
+				: __( 'There was a problem deleting that notice.', 'buddypress' );
+			break;
+	}
+
+	// Feedback.
+	if ( ! empty( $feedback ) ) {
+
+		// Determine message type.
+		$type = ( true === $success )
+			? 'success'
+			: 'error';
+
+		// Add feedback message.
+		bp_core_add_message( $feedback, $type );
+	}
+
+	// Redirect.
+	$member_notices = trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() );
+	$redirect_to    = trailingslashit( $member_notices . 'notices' );
+
+	bp_core_redirect( $redirect_to );
+}
+add_action( 'bp_actions', 'bp_messages_action_edit_notice' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/read.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/read.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/read.php	(revision 12265)
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Messages: Read action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handle marking a single message thread as read.
+ *
+ * @since 2.2.0
+ *
+ * @return false|null Returns false on failure. Otherwise redirects back to the
+ *                   message box URL.
+ */
+function bp_messages_action_mark_read() {
+
+	if ( ! bp_is_messages_component() || bp_is_current_action( 'notices' ) || ! bp_is_action_variable( 'read', 0 ) ) {
+		return false;
+	}
+
+	$action = ! empty( $_GET['action'] ) ? $_GET['action'] : '';
+	$nonce  = ! empty( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '';
+	$id     = ! empty( $_GET['message_id'] ) ? intval( $_GET['message_id'] ) : '';
+
+	// Bail if no action or no ID.
+	if ( 'read' !== $action || empty( $id ) || empty( $nonce ) ) {
+		return false;
+	}
+
+	// Check the nonce.
+	if ( ! bp_verify_nonce_request( 'bp_message_thread_mark_read_' . $id ) ) {
+		return false;
+	}
+
+	// Check access to the message and mark as read.
+	if ( messages_check_thread_access( $id ) || bp_current_user_can( 'bp_moderate' ) ) {
+		messages_mark_thread_read( $id );
+		bp_core_add_message( __( 'Message marked as read.', 'buddypress' ) );
+	} else {
+		bp_core_add_message( __( 'There was a problem marking that message.', 'buddypress' ), 'error' );
+	}
+
+	// Redirect back to the message box.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() );
+}
+add_action( 'bp_actions', 'bp_messages_action_mark_read' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/star.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/star.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/star.php	(revision 12265)
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Messages: Star action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Action handler to set a message's star status for those not using JS.
+ *
+ * @since 2.3.0
+ */
+function bp_messages_star_action_handler() {
+	if ( ! bp_is_user_messages() ) {
+		return;
+	}
+
+	if ( false === ( bp_is_current_action( 'unstar' ) || bp_is_current_action( 'star' ) ) ) {
+		return;
+	}
+
+	if ( ! wp_verify_nonce( bp_action_variable( 1 ), 'bp-messages-star-' . bp_action_variable( 0 ) ) ) {
+		wp_die( "Oops!  That's a no-no!" );
+	}
+
+	// Check capability.
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		return;
+	}
+
+	// Mark the star.
+	bp_messages_star_set_action( array(
+		'action'     => bp_current_action(),
+		'message_id' => bp_action_variable(),
+		'bulk'       => (bool) bp_action_variable( 2 )
+	) );
+
+	// Redirect back to previous screen.
+	$redirect = wp_get_referer() ? wp_get_referer() : bp_displayed_user_domain() . bp_get_messages_slug();
+	bp_core_redirect( $redirect );
+	die();
+}
+add_action( 'bp_actions', 'bp_messages_star_action_handler' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/unread.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/unread.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/unread.php	(revision 12265)
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Messages: Unread action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handle marking a single message thread as unread.
+ *
+ * @since 2.2.0
+ *
+ * @return false|null Returns false on failure. Otherwise redirects back to the
+ *                   message box URL.
+ */
+function bp_messages_action_mark_unread() {
+
+	if ( ! bp_is_messages_component() || bp_is_current_action( 'notices' ) || ! bp_is_action_variable( 'unread', 0 ) ) {
+		return false;
+	}
+
+	$action = ! empty( $_GET['action'] ) ? $_GET['action'] : '';
+	$nonce  = ! empty( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '';
+	$id     = ! empty( $_GET['message_id'] ) ? intval( $_GET['message_id'] ) : '';
+
+	// Bail if no action or no ID.
+	if ( 'unread' !== $action || empty( $id ) || empty( $nonce ) ) {
+		return false;
+	}
+
+	// Check the nonce.
+	if ( ! bp_verify_nonce_request( 'bp_message_thread_mark_unread_' . $id ) ) {
+		return false;
+	}
+
+	// Check access to the message and mark unread.
+	if ( messages_check_thread_access( $id ) || bp_current_user_can( 'bp_moderate' ) ) {
+		messages_mark_thread_unread( $id );
+		bp_core_add_message( __( 'Message marked unread.', 'buddypress' ) );
+	} else {
+		bp_core_add_message( __( 'There was a problem marking that message.', 'buddypress' ), 'error' );
+	}
+
+	// Redirect back to the message box URL.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() );
+}
+add_action( 'bp_actions', 'bp_messages_action_mark_unread' );
Index: /tags/4.0.0-beta1/src/bp-messages/actions/view.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/actions/view.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/actions/view.php	(revision 12265)
@@ -0,0 +1,64 @@
+<?php
+/**
+ * Messages: View action handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageActions
+ * @since 3.0.0
+ */
+
+/**
+ * Process a request to view a single message thread.
+ */
+function messages_action_conversation() {
+
+	// Bail if not viewing a single conversation.
+	if ( ! bp_is_messages_component() || ! bp_is_current_action( 'view' ) ) {
+		return false;
+	}
+
+	// Get the thread ID from the action variable.
+	$thread_id = (int) bp_action_variable( 0 );
+
+	if ( ! messages_is_valid_thread( $thread_id ) || ( ! messages_check_thread_access( $thread_id ) && ! bp_current_user_can( 'bp_moderate' ) ) ) {
+		return;
+	}
+
+	// Check if a new reply has been submitted.
+	if ( isset( $_POST['send'] ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'messages_send_message', 'send_message_nonce' );
+
+		$new_reply = messages_new_message( array(
+			'thread_id' => $thread_id,
+			'subject'   => ! empty( $_POST['subject'] ) ? $_POST['subject'] : false,
+			'content'   => $_POST['content']
+		) );
+
+		// Send the reply.
+		if ( ! empty( $new_reply ) ) {
+			bp_core_add_message( __( 'Your reply was sent successfully', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was a problem sending your reply. Please try again.', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/view/' . $thread_id . '/' );
+	}
+
+	/*
+	 * Mark message read, but only run on the logged-in user's profile.
+	 * If an admin visits a thread, it shouldn't change the read status.
+	 */
+	if ( bp_is_my_profile() ) {
+		messages_mark_thread_read( $thread_id );
+	}
+
+	/**
+	 * Fires after processing a view request for a single message thread.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'messages_action_conversation' );
+}
+add_action( 'bp_actions', 'messages_action_conversation' );
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-admin.php	(revision 12265)
@@ -0,0 +1,14 @@
+<?php
+/**
+ * BuddyPress Members component admin screens.
+ *
+ * @package BuddyPress
+ * @subpackage Messages
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+// Load the Sitewide Notices Admin
+add_action( bp_core_admin_hook(), array( 'BP_Messages_Notices_Admin', 'register_notices_admin' ), 9 );
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-cache.php	(revision 12265)
@@ -0,0 +1,104 @@
+<?php
+/**
+ * BuddyPress Messages Caching.
+ *
+ * Caching functions handle the clearing of cached objects and pages on specific
+ * actions throughout BuddyPress.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesCache
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Slurp up metadata for a set of messages.
+ *
+ * It grabs all message meta associated with all of the messages passed in
+ * $message_ids and adds it to WP cache. This improves efficiency when using
+ * message meta within a loop context.
+ *
+ * @since 2.2.0
+ *
+ * @param int|string|array|bool $message_ids Accepts a single message_id, or a
+ *                                           comma-separated list or array of message ids.
+ */
+function bp_messages_update_meta_cache( $message_ids = false ) {
+	bp_update_meta_cache( array(
+		'object_ids' 	   => $message_ids,
+		'object_type' 	   => buddypress()->messages->id,
+		'cache_group'      => 'message_meta',
+		'object_column'    => 'message_id',
+		'meta_table' 	   => buddypress()->messages->table_name_meta,
+		'cache_key_prefix' => 'bp_messages_meta'
+	) );
+}
+
+// List actions to clear super cached pages on, if super cache is installed.
+add_action( 'messages_delete_thread',  'bp_core_clear_cache' );
+add_action( 'messages_send_notice',    'bp_core_clear_cache' );
+add_action( 'messages_message_sent',   'bp_core_clear_cache' );
+
+// Don't cache message inbox/sentbox/compose as it's too problematic.
+add_action( 'messages_screen_compose', 'bp_core_clear_cache' );
+add_action( 'messages_screen_sentbox', 'bp_core_clear_cache' );
+add_action( 'messages_screen_inbox',   'bp_core_clear_cache' );
+
+/**
+ * Clear message cache after a message is saved.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_Messages_Message $message Message being saved.
+ */
+function bp_messages_clear_cache_on_message_save( BP_Messages_Message $message ) {
+	// Delete thread cache.
+	wp_cache_delete( $message->thread_id, 'bp_messages_threads' );
+
+	// Delete unread count for each recipient.
+	foreach ( (array) $message->recipients as $recipient ) {
+		wp_cache_delete( $recipient->user_id, 'bp_messages_unread_count' );
+	}
+
+	// Delete thread recipient cache.
+	wp_cache_delete( 'thread_recipients_' . $message->thread_id, 'bp_messages' );
+}
+add_action( 'messages_message_after_save', 'bp_messages_clear_cache_on_message_save' );
+
+/**
+ * Clear message cache after a message thread is deleted.
+ *
+ * @since 2.0.0
+ *
+ * @param int|array $thread_ids If single thread, the thread ID.
+ *                              Otherwise, an array of thread IDs.
+ * @param int       $user_id    ID of the user that the threads were deleted for.
+ */
+function bp_messages_clear_cache_on_message_delete( $thread_ids, $user_id ) {
+	// Delete thread and thread recipient cache.
+	foreach( (array) $thread_ids as $thread_id ) {
+		wp_cache_delete( $thread_id, 'bp_messages_threads' );
+		wp_cache_delete( "thread_recipients_{$thread_id}", 'bp_messages' );
+	}
+
+	// Delete unread count for logged-in user.
+	wp_cache_delete( $user_id, 'bp_messages_unread_count' );
+}
+add_action( 'messages_delete_thread', 'bp_messages_clear_cache_on_message_delete', 10, 2 );
+
+/**
+ * Invalidate cache for notices.
+ *
+ * Currently, invalidates active notice cache.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_Messages_Notice $notice Notice that was saved.
+ */
+function bp_notices_clear_cache( $notice ) {
+	wp_cache_delete( 'active_notice', 'bp_messages' );
+}
+add_action( 'messages_notice_after_save',    'bp_notices_clear_cache' );
+add_action( 'messages_notice_before_delete', 'bp_notices_clear_cache' );
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-cssjs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-cssjs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-cssjs.php	(revision 12265)
@@ -0,0 +1,75 @@
+<?php
+/**
+ * BuddyPress Messages CSS and JS.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesScripts
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Enqueue the JS for messages autocomplete.
+ */
+function messages_add_autocomplete_js() {
+
+	// Include the autocomplete JS for composing a message.
+	if ( bp_is_messages_component() && bp_is_current_action( 'compose' ) ) {
+		add_action( 'wp_head', 'messages_autocomplete_init_jsblock' );
+
+		$min = bp_core_get_minified_asset_suffix();
+		$url = buddypress()->plugin_url . 'bp-messages/js/';
+
+		wp_enqueue_script( 'bp-jquery-autocomplete', "{$url}autocomplete/jquery.autocomplete{$min}.js", array( 'jquery' ), bp_get_version() );
+		wp_enqueue_script( 'bp-jquery-autocomplete-fb', "{$url}autocomplete/jquery.autocompletefb{$min}.js", array( 'jquery' ), bp_get_version() );
+		wp_enqueue_script( 'bp-jquery-bgiframe', "{$url}autocomplete/jquery.bgiframe{$min}.js", array( 'jquery' ), bp_get_version() );
+		wp_enqueue_script( 'bp-jquery-dimensions', "{$url}autocomplete/jquery.dimensions{$min}.js", array( 'jquery' ), bp_get_version() );
+	}
+}
+add_action( 'bp_enqueue_scripts', 'messages_add_autocomplete_js' );
+
+/**
+ * Enqueue the CSS for messages autocomplete.
+ *
+ * @todo Why do we call wp_print_styles()?
+ */
+function messages_add_autocomplete_css() {
+	if ( bp_is_messages_component() && bp_is_current_action( 'compose' ) ) {
+		$min = bp_core_get_minified_asset_suffix();
+		$url = buddypress()->plugin_url . 'bp-messages/css/';
+
+		wp_enqueue_style( 'bp-messages-autocomplete', "{$url}autocomplete/jquery.autocompletefb{$min}.css", array(), bp_get_version() );
+
+		wp_style_add_data( 'bp-messages-autocomplete', 'rtl', true );
+		if ( $min ) {
+			wp_style_add_data( 'bp-messages-autocomplete', 'suffix', $min );
+		}
+
+		wp_print_styles();
+	}
+}
+add_action( 'wp_head', 'messages_add_autocomplete_css' );
+
+/**
+ * Print inline JS for initializing the messages autocomplete.
+ *
+ * @todo Why is this here and not in a properly enqueued file?
+ */
+function messages_autocomplete_init_jsblock() {
+?>
+
+	<script type="text/javascript">
+		jQuery(document).ready(function() {
+			var acfb = jQuery("ul.first").autoCompletefb({urlLookup: ajaxurl});
+
+			jQuery('#send_message_form').submit( function() {
+				var users = document.getElementById('send-to-usernames').className;
+				document.getElementById('send-to-usernames').value = String(users);
+			});
+		});
+	</script>
+
+<?php
+}
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-filters.php	(revision 12265)
@@ -0,0 +1,148 @@
+<?php
+/**
+ * BuddyPress Messages Filters.
+ *
+ * Apply WordPress defined filters to private messages.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesFilters
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_filter( 'bp_get_message_notice_subject',        'wp_filter_kses', 1 );
+add_filter( 'bp_get_message_notice_text',           'wp_filter_kses', 1 );
+add_filter( 'bp_get_message_thread_subject',        'wp_filter_kses', 1 );
+add_filter( 'bp_get_message_thread_excerpt',        'wp_filter_kses', 1 );
+add_filter( 'bp_get_messages_subject_value',        'wp_filter_kses', 1 );
+add_filter( 'bp_get_messages_content_value',        'wp_filter_kses', 1 );
+add_filter( 'messages_message_subject_before_save', 'wp_filter_kses', 1 );
+add_filter( 'messages_notice_subject_before_save',  'wp_filter_kses', 1 );
+add_filter( 'bp_get_the_thread_subject',            'wp_filter_kses', 1 );
+
+add_filter( 'bp_get_the_thread_message_content',    'bp_messages_filter_kses', 1 );
+add_filter( 'messages_message_content_before_save', 'bp_messages_filter_kses', 1 );
+add_filter( 'messages_notice_message_before_save',  'bp_messages_filter_kses', 1 );
+add_filter( 'bp_get_message_thread_content',        'bp_messages_filter_kses', 1 );
+
+add_filter( 'messages_message_content_before_save', 'force_balance_tags' );
+add_filter( 'messages_message_subject_before_save', 'force_balance_tags' );
+add_filter( 'messages_notice_message_before_save',  'force_balance_tags' );
+add_filter( 'messages_notice_subject_before_save',  'force_balance_tags' );
+
+if ( function_exists( 'wp_encode_emoji' ) ) {
+	add_filter( 'messages_message_subject_before_save', 'wp_encode_emoji' );
+	add_filter( 'messages_message_content_before_save', 'wp_encode_emoji' );
+	add_filter( 'messages_notice_message_before_save',  'wp_encode_emoji' );
+	add_filter( 'messages_notice_subject_before_save',  'wp_encode_emoji' );
+}
+
+add_filter( 'bp_get_message_notice_subject',     'wptexturize' );
+add_filter( 'bp_get_message_notice_text',        'wptexturize' );
+add_filter( 'bp_get_message_thread_subject',     'wptexturize' );
+add_filter( 'bp_get_message_thread_excerpt',     'wptexturize' );
+add_filter( 'bp_get_the_thread_message_content', 'wptexturize' );
+add_filter( 'bp_get_message_thread_content',     'wptexturize' );
+
+add_filter( 'bp_get_message_notice_subject',     'convert_smilies', 2 );
+add_filter( 'bp_get_message_notice_text',        'convert_smilies', 2 );
+add_filter( 'bp_get_message_thread_subject',     'convert_smilies', 2 );
+add_filter( 'bp_get_message_thread_excerpt',     'convert_smilies', 2 );
+add_filter( 'bp_get_the_thread_message_content', 'convert_smilies', 2 );
+add_filter( 'bp_get_message_thread_content',     'convert_smilies', 2 );
+
+add_filter( 'bp_get_message_notice_subject',     'convert_chars' );
+add_filter( 'bp_get_message_notice_text',        'convert_chars' );
+add_filter( 'bp_get_message_thread_subject',     'convert_chars' );
+add_filter( 'bp_get_message_thread_excerpt',     'convert_chars' );
+add_filter( 'bp_get_the_thread_message_content', 'convert_chars' );
+add_filter( 'bp_get_message_thread_content',     'convert_chars' );
+
+add_filter( 'bp_get_message_notice_text',        'make_clickable', 9 );
+add_filter( 'bp_get_the_thread_message_content', 'make_clickable', 9 );
+add_filter( 'bp_get_message_thread_content',     'make_clickable', 9 );
+
+add_filter( 'bp_get_message_notice_text',        'wpautop' );
+add_filter( 'bp_get_the_thread_message_content', 'wpautop' );
+add_filter( 'bp_get_message_thread_content',     'wpautop' );
+
+add_filter( 'bp_get_message_notice_subject',          'stripslashes_deep'    );
+add_filter( 'bp_get_message_notice_text',             'stripslashes_deep'    );
+add_filter( 'bp_get_message_thread_subject',          'stripslashes_deep'    );
+add_filter( 'bp_get_message_thread_excerpt',          'stripslashes_deep'    );
+add_filter( 'bp_get_message_get_recipient_usernames', 'stripslashes_deep'    );
+add_filter( 'bp_get_messages_subject_value',          'stripslashes_deep'    );
+add_filter( 'bp_get_messages_content_value',          'stripslashes_deep'    );
+add_filter( 'bp_get_the_thread_message_content',      'stripslashes_deep'    );
+add_filter( 'bp_get_the_thread_subject',              'stripslashes_deep'    );
+add_filter( 'bp_get_message_thread_content',          'stripslashes_deep', 1 );
+
+// Personal data export.
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_messages_register_personal_data_exporter' );
+
+/**
+ * Enforce limitations on viewing private message contents
+ *
+ * @since 2.3.2
+ *
+ * @see bp_has_message_threads() for description of parameters
+ *
+ * @param array|string $args See {@link bp_has_message_threads()}.
+ * @return array|string
+ */
+function bp_messages_enforce_current_user( $args = array() ) {
+
+	// Non-community moderators can only ever see their own messages.
+	if ( is_user_logged_in() && ! bp_current_user_can( 'bp_moderate' ) ) {
+		$_user_id = (int) bp_loggedin_user_id();
+		if ( $_user_id !== (int) $args['user_id'] ) {
+			$args['user_id'] = $_user_id;
+		}
+	}
+
+	// Return possibly modified $args array.
+	return $args;
+}
+add_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_enforce_current_user', 5 );
+
+/**
+ * Custom kses filtering for message content.
+ *
+ * @since 3.0.0
+ *
+ * @param string $content The message content.
+ * @return string         The filtered message content.
+ */
+function bp_messages_filter_kses( $content ) {
+	$messages_allowedtags      = bp_get_allowedtags();
+	$messages_allowedtags['p'] = array();
+
+	/**
+	 * Filters the allowed HTML tags for BuddyPress Messages content.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value Array of allowed HTML tags and attributes.
+	 */
+	$messages_allowedtags = apply_filters( 'bp_messages_allowed_tags', $messages_allowedtags );
+	return wp_kses( $content, $messages_allowedtags );
+}
+
+/**
+ * Register Messages personal data exporter.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_messages_register_personal_data_exporter( $exporters ) {
+	$exporters['buddypress-messages'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Messages', 'buddypress' ),
+		'callback'               => 'bp_messages_personal_data_exporter',
+	);
+
+	return $exporters;
+}
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-functions.php	(revision 12265)
@@ -0,0 +1,727 @@
+<?php
+/**
+ * BuddyPress Messages Functions.
+ *
+ * Business functions are where all the magic happens in BuddyPress. They will
+ * handle the actual saving or manipulation of information. Usually they will
+ * hand off to a database class for data access, then return
+ * true or false on success or failure.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Create a new message.
+ *
+ * @since 2.4.0 Added 'error_type' as an additional $args parameter.
+ *
+ * @param array|string $args {
+ *     Array of arguments.
+ *     @type int    $sender_id  Optional. ID of the user who is sending the
+ *                              message. Default: ID of the logged-in user.
+ *     @type int    $thread_id  Optional. ID of the parent thread. Leave blank to
+ *                              create a new thread for the message.
+ *     @type array  $recipients IDs or usernames of message recipients. If this
+ *                              is an existing thread, it is unnecessary to pass a $recipients
+ *                              argument - existing thread recipients will be assumed.
+ *     @type string $subject    Optional. Subject line for the message. For
+ *                              existing threads, the existing subject will be used. For new
+ *                              threads, 'No Subject' will be used if no $subject is provided.
+ *     @type string $content    Content of the message. Cannot be empty.
+ *     @type string $date_sent  Date sent, in 'Y-m-d H:i:s' format. Default: current date/time.
+ *     @type string $error_type Optional. Error type. Either 'bool' or 'wp_error'. Default: 'bool'.
+ * }
+ *
+ * @return int|bool|WP_Error ID of the message thread on success, false on failure.
+ */
+function messages_new_message( $args = '' ) {
+
+	// Parse the default arguments.
+	$r = bp_parse_args( $args, array(
+		'sender_id'  => bp_loggedin_user_id(),
+		'thread_id'  => false,   // False for a new message, thread id for a reply to a thread.
+		'recipients' => array(), // Can be an array of usernames, user_ids or mixed.
+		'subject'    => false,
+		'content'    => false,
+		'date_sent'  => bp_core_current_time(),
+		'error_type' => 'bool'
+	), 'messages_new_message' );
+
+	// Bail if no sender or no content.
+	if ( empty( $r['sender_id'] ) || empty( $r['content'] ) ) {
+		if ( 'wp_error' === $r['error_type'] ) {
+			if ( empty( $r['sender_id'] ) ) {
+				$error_code = 'messages_empty_sender';
+				$feedback   = __( 'Your message was not sent. Please use a valid sender.', 'buddypress' );
+			} else {
+				$error_code = 'messages_empty_content';
+				$feedback   = __( 'Your message was not sent. Please enter some content.', 'buddypress' );
+			}
+
+			return new WP_Error( $error_code, $feedback );
+
+		} else {
+			return false;
+		}
+	}
+
+	// Create a new message object.
+	$message            = new BP_Messages_Message;
+	$message->thread_id = $r['thread_id'];
+	$message->sender_id = $r['sender_id'];
+	$message->subject   = $r['subject'];
+	$message->message   = $r['content'];
+	$message->date_sent = $r['date_sent'];
+
+	// If we have a thread ID...
+	if ( ! empty( $r['thread_id'] ) ) {
+
+		// ...use the existing recipients
+		$thread              = new BP_Messages_Thread( $r['thread_id'] );
+		$message->recipients = $thread->get_recipients();
+
+		// Strip the sender from the recipient list, and unset them if they are
+		// not alone. If they are alone, let them talk to themselves.
+		if ( isset( $message->recipients[ $r['sender_id'] ] ) && ( count( $message->recipients ) > 1 ) ) {
+			unset( $message->recipients[ $r['sender_id'] ] );
+		}
+
+		// Set a default reply subject if none was sent.
+		if ( empty( $message->subject ) ) {
+			$message->subject = sprintf( __( 'Re: %s', 'buddypress' ), $thread->messages[0]->subject );
+		}
+
+	// ...otherwise use the recipients passed
+	} else {
+
+		// Bail if no recipients.
+		if ( empty( $r['recipients'] ) ) {
+			if ( 'wp_error' === $r['error_type'] ) {
+				return new WP_Error( 'message_empty_recipients', __( 'Message could not be sent. Please enter a recipient.', 'buddypress' ) );
+			} else {
+				return false;
+			}
+		}
+
+		// Set a default subject if none exists.
+		if ( empty( $message->subject ) ) {
+			$message->subject = __( 'No Subject', 'buddypress' );
+		}
+
+		// Setup the recipients array.
+		$recipient_ids = array();
+
+		// Invalid recipients are added to an array, for future enhancements.
+		$invalid_recipients = array();
+
+		// Loop the recipients and convert all usernames to user_ids where needed.
+		foreach ( (array) $r['recipients'] as $recipient ) {
+
+			// Trim spaces and skip if empty.
+			$recipient = trim( $recipient );
+			if ( empty( $recipient ) ) {
+				continue;
+			}
+
+			// Check user_login / nicename columns first
+			// @see http://buddypress.trac.wordpress.org/ticket/5151.
+			if ( bp_is_username_compatibility_mode() ) {
+				$recipient_id = bp_core_get_userid( urldecode( $recipient ) );
+			} else {
+				$recipient_id = bp_core_get_userid_from_nicename( $recipient );
+			}
+
+			// Check against user ID column if no match and if passed recipient is numeric.
+			if ( empty( $recipient_id ) && is_numeric( $recipient ) ) {
+				if ( bp_core_get_core_userdata( (int) $recipient ) ) {
+					$recipient_id = (int) $recipient;
+				}
+			}
+
+			// Decide which group to add this recipient to.
+			if ( empty( $recipient_id ) ) {
+				$invalid_recipients[] = $recipient;
+			} else {
+				$recipient_ids[] = (int) $recipient_id;
+			}
+		}
+
+		// Strip the sender from the recipient list, and unset them if they are
+		// not alone. If they are alone, let them talk to themselves.
+		$self_send = array_search( $r['sender_id'], $recipient_ids );
+		if ( ! empty( $self_send ) && ( count( $recipient_ids ) > 1 ) ) {
+			unset( $recipient_ids[ $self_send ] );
+		}
+
+		// Remove duplicates & bail if no recipients.
+		$recipient_ids = array_unique( $recipient_ids );
+		if ( empty( $recipient_ids ) ) {
+			if ( 'wp_error' === $r['error_type'] ) {
+				return new WP_Error( 'message_invalid_recipients', __( 'Message could not be sent because you have entered an invalid username. Please try again.', 'buddypress' ) );
+			} else {
+				return false;
+			}
+		}
+
+		// Format this to match existing recipients.
+		foreach ( (array) $recipient_ids as $i => $recipient_id ) {
+			$message->recipients[ $i ]          = new stdClass;
+			$message->recipients[ $i ]->user_id = $recipient_id;
+		}
+	}
+
+	// Bail if message failed to send.
+	$send = $message->send();
+	if ( false === is_int( $send ) ) {
+		if ( 'wp_error' === $r['error_type'] ) {
+			if ( is_wp_error( $send ) ) {
+				return $send;
+			} else {
+				return new WP_Error( 'message_generic_error', __( 'Message was not sent. Please try again.', 'buddypress' ) );
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Fires after a message has been successfully sent.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param BP_Messages_Message $message Message object. Passed by reference.
+	 */
+	do_action_ref_array( 'messages_message_sent', array( &$message ) );
+
+	// Return the thread ID.
+	return $message->thread_id;
+}
+
+/**
+ * Send a notice.
+ *
+ * @param string $subject Subject of the notice.
+ * @param string $message Content of the notice.
+ * @return bool True on success, false on failure.
+ */
+function messages_send_notice( $subject, $message ) {
+	if ( !bp_current_user_can( 'bp_moderate' ) || empty( $subject ) || empty( $message ) ) {
+		return false;
+
+	// Has access to send notices, lets do it.
+	} else {
+		$notice            = new BP_Messages_Notice;
+		$notice->subject   = $subject;
+		$notice->message   = $message;
+		$notice->date_sent = bp_core_current_time();
+		$notice->is_active = 1;
+		$notice->save(); // Send it.
+
+		/**
+		 * Fires after a notice has been successfully sent.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $subject Subject of the notice.
+		 * @param string $message Content of the notice.
+		 */
+		do_action_ref_array( 'messages_send_notice', array( $subject, $message ) );
+
+		return true;
+	}
+}
+
+/**
+ * Deletes message thread(s) for a given user.
+ *
+ * Note that "deleting" a thread for a user means removing it from the user's
+ * message boxes. A thread is not deleted from the database until it's been
+ * "deleted" by all recipients.
+ *
+ * @since 2.7.0 The $user_id parameter was added. Previously the current user
+ *              was always assumed.
+ *
+ * @param int|array $thread_ids Thread ID or array of thread IDs.
+ * @param int       $user_id    ID of the user to delete the threads for. Defaults
+ *                              to the current logged-in user.
+ * @return bool True on success, false on failure.
+ */
+function messages_delete_thread( $thread_ids, $user_id = 0 ) {
+
+	if ( empty( $user_id ) ) {
+		$user_id =
+			bp_displayed_user_id() ?
+			bp_displayed_user_id() :
+			bp_loggedin_user_id();
+	}
+
+	/**
+	 * Fires before specified thread IDs have been deleted.
+	 *
+	 * @since 1.5.0
+	 * @since 2.7.0 The $user_id parameter was added.
+	 *
+	 * @param int|array $thread_ids Thread ID or array of thread IDs to be deleted.
+	 * @param int       $user_id    ID of the user the threads are being deleted for.
+	 */
+	do_action( 'messages_before_delete_thread', $thread_ids, $user_id );
+
+	if ( is_array( $thread_ids ) ) {
+		$error = 0;
+		for ( $i = 0, $count = count( $thread_ids ); $i < $count; ++$i ) {
+			if ( ! BP_Messages_Thread::delete( $thread_ids[$i], $user_id ) ) {
+				$error = 1;
+			}
+		}
+
+		if ( ! empty( $error ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires after specified thread IDs have been deleted.
+		 *
+		 * @since 1.0.0
+		 * @since 2.7.0 The $user_id parameter was added.
+		 *
+		 * @param int|array Thread ID or array of thread IDs that were deleted.
+		 * @param int       ID of the user that the threads were deleted for.
+		 */
+		do_action( 'messages_delete_thread', $thread_ids, $user_id );
+
+		return true;
+	} else {
+		if ( ! BP_Messages_Thread::delete( $thread_ids, $user_id ) ) {
+			return false;
+		}
+
+		/** This action is documented in bp-messages/bp-messages-functions.php */
+		do_action( 'messages_delete_thread', $thread_ids, $user_id );
+
+		return true;
+	}
+}
+
+/**
+ * Check whether a user has access to a thread.
+ *
+ * @param int $thread_id ID of the thread.
+ * @param int $user_id   Optional. ID of the user. Default: ID of the logged-in user.
+ * @return int|null Message ID if the user has access, otherwise null.
+ */
+function messages_check_thread_access( $thread_id, $user_id = 0 ) {
+	return BP_Messages_Thread::check_access( $thread_id, $user_id );
+}
+
+/**
+ * Mark a thread as read.
+ *
+ * Wrapper for {@link BP_Messages_Thread::mark_as_read()}.
+ *
+ * @param int $thread_id ID of the thread.
+ *
+ * @return false|int Number of threads marked as read or false on error.
+ */
+function messages_mark_thread_read( $thread_id ) {
+	return BP_Messages_Thread::mark_as_read( $thread_id );
+}
+
+/**
+ * Mark a thread as unread.
+ *
+ * Wrapper for {@link BP_Messages_Thread::mark_as_unread()}.
+ *
+ * @param int $thread_id ID of the thread.
+ *
+ * @return false|int Number of threads marked as unread or false on error.
+ */
+function messages_mark_thread_unread( $thread_id ) {
+	return BP_Messages_Thread::mark_as_unread( $thread_id );
+}
+
+/**
+ * Set messages-related cookies.
+ *
+ * Saves the 'bp_messages_send_to', 'bp_messages_subject', and
+ * 'bp_messages_content' cookies, which are used when setting up the default
+ * values on the messages page.
+ *
+ * @param string $recipients Comma-separated list of recipient usernames.
+ * @param string $subject    Subject of the message.
+ * @param string $content    Content of the message.
+ */
+function messages_add_callback_values( $recipients, $subject, $content ) {
+	@setcookie( 'bp_messages_send_to', $recipients, time() + 60 * 60 * 24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	@setcookie( 'bp_messages_subject', $subject,    time() + 60 * 60 * 24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	@setcookie( 'bp_messages_content', $content,    time() + 60 * 60 * 24, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+}
+
+/**
+ * Unset messages-related cookies.
+ *
+ * @see messages_add_callback_values()
+ */
+function messages_remove_callback_values() {
+	@setcookie( 'bp_messages_send_to', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	@setcookie( 'bp_messages_subject', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+	@setcookie( 'bp_messages_content', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+}
+
+/**
+ * Get the unread messages count for a user.
+ *
+ * @param int $user_id Optional. ID of the user. Default: ID of the logged-in user.
+ * @return int
+ */
+function messages_get_unread_count( $user_id = 0 ) {
+	return BP_Messages_Thread::get_inbox_count( $user_id );
+}
+
+/**
+ * Check whether a user is the sender of a message.
+ *
+ * @param int $user_id    ID of the user.
+ * @param int $message_id ID of the message.
+ * @return int|null Returns the ID of the message if the user is the
+ *                  sender, otherwise null.
+ */
+function messages_is_user_sender( $user_id, $message_id ) {
+	return BP_Messages_Message::is_user_sender( $user_id, $message_id );
+}
+
+/**
+ * Get the ID of the sender of a message.
+ *
+ * @param int $message_id ID of the message.
+ * @return int|null The ID of the sender if found, otherwise null.
+ */
+function messages_get_message_sender( $message_id ) {
+	return BP_Messages_Message::get_message_sender( $message_id );
+}
+
+/**
+ * Check whether a message thread exists.
+ *
+ * @param int $thread_id ID of the thread.
+ * @return false|int|null The message thread ID on success, null on failure.
+ */
+function messages_is_valid_thread( $thread_id ) {
+	return BP_Messages_Thread::is_valid( $thread_id );
+}
+
+/**
+ * Get the thread ID from a message ID.
+ *
+ * @since 2.3.0
+ *
+ * @param  int $message_id ID of the message.
+ * @return int The ID of the thread if found, otherwise 0.
+ */
+function messages_get_message_thread_id( $message_id = 0 ) {
+	global $wpdb;
+
+	$bp = buddypress();
+
+	return (int) $wpdb->get_var( $wpdb->prepare( "SELECT thread_id FROM {$bp->messages->table_name_messages} WHERE id = %d", $message_id ) );
+}
+
+/** Messages Meta *******************************************************/
+
+/**
+ * Delete metadata for a message.
+ *
+ * If $meta_key is false, this will delete all meta for the message ID.
+ *
+ * @since 2.2.0
+ *
+ * @see delete_metadata() for full documentation excluding $meta_type variable.
+ *
+ * @param int         $message_id ID of the message to have meta deleted for.
+ * @param string|bool $meta_key   Meta key to delete. Default false.
+ * @param string|bool $meta_value Meta value to delete. Default false.
+ * @param bool        $delete_all Whether or not to delete all meta data.
+ *
+ * @return bool True on successful delete, false on failure.
+ */
+function bp_messages_delete_meta( $message_id, $meta_key = false, $meta_value = false, $delete_all = false ) {
+	global $wpdb;
+
+	// Legacy - if no meta_key is passed, delete all for the item.
+	if ( empty( $meta_key ) ) {
+		$table_name = buddypress()->messages->table_name_meta;
+		$sql        = "SELECT meta_key FROM {$table_name} WHERE message_id = %d";
+		$query      = $wpdb->prepare( $sql, $message_id );
+		$keys       = $wpdb->get_col( $query );
+
+		// With no meta_key, ignore $delete_all.
+		$delete_all = false;
+	} else {
+		$keys = array( $meta_key );
+	}
+
+	$retval = false;
+
+	// No keys, so stop now!
+	if ( empty( $keys ) ) {
+		return $retval;
+	}
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	foreach ( $keys as $key ) {
+		$retval = delete_metadata( 'message', $message_id, $key, $meta_value, $delete_all );
+	}
+
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Get a piece of message metadata.
+ *
+ * @since 2.2.0
+ *
+ * @see get_metadata() for full documentation excluding $meta_type variable.
+ *
+ * @param int    $message_id ID of the message to retrieve meta for.
+ * @param string $meta_key   Meta key to retrieve. Default empty string.
+ * @param bool   $single     Whether or not to fetch all or a single value.
+ * @return mixed
+ */
+function bp_messages_get_meta( $message_id, $meta_key = '', $single = true ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = get_metadata( 'message', $message_id, $meta_key, $single );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Update a piece of message metadata.
+ *
+ * @since 2.2.0
+ *
+ * @see update_metadata() for full documentation excluding $meta_type variable.
+ *
+ * @param int         $message_id ID of the message to have meta deleted for.
+ * @param string|bool $meta_key   Meta key to update.
+ * @param string|bool $meta_value Meta value to update.
+ * @param string      $prev_value If specified, only update existing metadata entries with
+ *                                the specified value. Otherwise, update all entries.
+ * @return mixed
+ */
+function bp_messages_update_meta( $message_id, $meta_key, $meta_value, $prev_value = '' ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = update_metadata( 'message', $message_id, $meta_key, $meta_value, $prev_value );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Add a piece of message metadata.
+ *
+ * @since 2.2.0
+ *
+ * @see add_metadata() for full documentation excluding $meta_type variable.
+ *
+ * @param int         $message_id ID of the message to have meta deleted for.
+ * @param string|bool $meta_key   Meta key to update.
+ * @param string|bool $meta_value Meta value to update.
+ * @param bool        $unique     Whether the specified metadata key should be
+ *                                unique for the object. If true, and the object
+ *                                already has a value for the specified metadata key,
+ *                                no change will be made.
+ * @return mixed
+ */
+function bp_messages_add_meta( $message_id, $meta_key, $meta_value, $unique = false ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = add_metadata( 'message', $message_id, $meta_key, $meta_value, $unique );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/** Email *********************************************************************/
+
+/**
+ * Email message recipients to alert them of a new unread private message.
+ *
+ * @since 1.0.0
+ *
+ * @param array|BP_Messages_Message $raw_args {
+ *     Array of arguments. Also accepts a BP_Messages_Message object.
+ *     @type array  $recipients    User IDs of recipients.
+ *     @type string $email_subject Subject line of message.
+ *     @type string $email_content Content of message.
+ *     @type int    $sender_id     User ID of sender.
+ * }
+ */
+function messages_notification_new_message( $raw_args = array() ) {
+	if ( is_object( $raw_args ) ) {
+		$args = (array) $raw_args;
+	} else {
+		$args = $raw_args;
+	}
+
+	// These should be extracted below.
+	$recipients    = array();
+	$email_subject = $email_content = '';
+	$sender_id     = 0;
+
+	// Barf.
+	extract( $args );
+
+	if ( empty( $recipients ) ) {
+		return;
+	}
+
+	$sender_name = bp_core_get_user_displayname( $sender_id );
+
+	if ( isset( $message ) ) {
+		$message = wpautop( $message );
+	} else {
+		$message = '';
+	}
+
+	// Send an email to each recipient.
+	foreach ( $recipients as $recipient ) {
+		if ( $sender_id == $recipient->user_id || 'no' == bp_get_user_meta( $recipient->user_id, 'notification_messages_new_message', true ) ) {
+			continue;
+		}
+
+		// User data and links.
+		$ud = get_userdata( $recipient->user_id );
+		if ( empty( $ud ) ) {
+			continue;
+		}
+
+		$unsubscribe_args = array(
+			'user_id'           => $recipient->user_id,
+			'notification_type' => 'messages-unread',
+		);
+
+		bp_send_email( 'messages-unread', $ud, array(
+			'tokens' => array(
+				'usermessage' => wp_strip_all_tags( stripslashes( $message ) ),
+				'message.url' => esc_url( bp_core_get_user_domain( $recipient->user_id ) . bp_get_messages_slug() . '/view/' . $thread_id . '/' ),
+				'sender.name' => $sender_name,
+				'usersubject' => sanitize_text_field( stripslashes( $subject ) ),
+				'unsubscribe' => esc_url( bp_email_get_unsubscribe_link( $unsubscribe_args ) ),
+			),
+		) );
+	}
+
+	/**
+	 * Fires after the sending of a new message email notification.
+	 *
+	 * @since 1.5.0
+	 * @deprecated 2.5.0 Use the filters in BP_Email.
+	 *                   $email_subject and $email_content arguments unset and deprecated.
+	 *
+	 * @param array  $recipients    User IDs of recipients.
+	 * @param string $email_subject Deprecated in 2.5; now an empty string.
+	 * @param string $email_content Deprecated in 2.5; now an empty string.
+	 * @param array  $args          Array of originally provided arguments.
+	 */
+	do_action( 'bp_messages_sent_notification_email', $recipients, '', '', $args );
+}
+add_action( 'messages_message_sent', 'messages_notification_new_message', 10 );
+
+/**
+ * Finds and exports personal data associated with an email address from the Messages tables.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_messages_personal_data_exporter( $email_address, $page ) {
+	$number = 10;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$user_data_to_export = array();
+
+	$user_threads = BP_Messages_Thread::get_current_threads_for_user( array(
+		'user_id' => $user->ID,
+		'box'     => 'sentbox',
+		'type'    => null,
+		'limit'   => $number,
+		'page'    => $page,
+	) );
+
+	foreach ( $user_threads['threads'] as $thread ) {
+		$recipient_links = array();
+		foreach ( $thread->recipients as $recipient ) {
+			if ( $recipient->user_id === $user->ID ) {
+				continue;
+			}
+
+			$recipient_links[] = bp_core_get_userlink( $recipient->user_id );
+		}
+		$recipients = implode( ', ', $recipient_links );
+
+		$thread_link = bp_get_message_thread_view_link( $thread->thread_id, $user->ID );
+
+		foreach ( $thread->messages as $message_index => $message ) {
+			// Only include messages written by the user.
+			if ( $recipient->user_id !== $message->sender_id ) {
+				continue;
+			}
+
+			$message_data = array(
+				array(
+					'name'  => __( 'Message Subject', 'buddypress' ),
+					'value' => $message->subject,
+				),
+				array(
+					'name'  => __( 'Message Content', 'buddypress' ),
+					'value' => $message->message,
+				),
+				array(
+					'name'  => __( 'Date Sent', 'buddypress' ),
+					'value' => $message->date_sent,
+				),
+				array(
+					'name' => __( 'Recipients', 'buddypress' ),
+					'value' => $recipients,
+				),
+				array(
+					'name'  => __( 'Thread URL', 'buddypress' ),
+					'value' => $thread_link,
+				),
+			);
+
+			$data_to_export[] = array(
+				'group_id'    => 'bp_messages',
+				'group_label' => __( 'Private Messages', 'buddypress' ),
+				'item_id'     => "bp-messages-{$message->id}",
+				'data'        => $message_data,
+			);
+		}
+	}
+
+	return array(
+		'data' => $data_to_export,
+		'done' => true,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-loader.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BuddyPress Messages Loader.
+ *
+ * A private messages component, for users to send messages to each other.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-messages component.
+ *
+ * @since 1.5.0
+ */
+function bp_setup_messages() {
+	buddypress()->messages = new BP_Messages_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_messages', 6 );
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-notifications.php	(revision 12265)
@@ -0,0 +1,296 @@
+<?php
+/**
+ * BuddyPress Messages Notifications.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesNotifications
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Format notifications for the Messages component.
+ *
+ * @since 1.0.0
+ *
+ * @param string $action            The kind of notification being rendered.
+ * @param int    $item_id           The primary item id.
+ * @param int    $secondary_item_id The secondary item id.
+ * @param int    $total_items       The total number of messaging-related notifications
+ *                                  waiting for the user.
+ * @param string $format            Return value format. 'string' for BuddyBar-compatible
+ *                                  notifications; 'array' for WP Toolbar. Default: 'string'.
+ * @return string|array Formatted notifications.
+ */
+function messages_format_notifications( $action, $item_id, $secondary_item_id, $total_items, $format = 'string' ) {
+	$total_items = (int) $total_items;
+	$text        = '';
+	$link        = trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() . '/inbox' );
+	$title       = __( 'Inbox', 'buddypress' );
+	$amount      = 'single';
+
+	if ( 'new_message' === $action ) {
+		if ( $total_items > 1 ) {
+			$amount = 'multiple';
+			$text   = sprintf( __( 'You have %d new messages', 'buddypress' ), $total_items );
+
+		} else {
+			// Get message thread ID.
+			$message   = new BP_Messages_Message( $item_id );
+			$thread_id = $message->thread_id;
+			$link      = ( ! empty( $thread_id ) )
+				? bp_get_message_thread_view_link( $thread_id )
+				: false;
+
+			if ( ! empty( $secondary_item_id ) ) {
+				$text = sprintf( __( '%s sent you a new private message', 'buddypress' ), bp_core_get_user_displayname( $secondary_item_id ) );
+			} else {
+				$text = sprintf( _n( 'You have %s new private message', 'You have %s new private messages', $total_items, 'buddypress' ), bp_core_number_format( $total_items ) );
+			}
+		}
+
+		if ( 'string' === $format ) {
+			if ( ! empty( $link ) ) {
+				$return = '<a href="' . esc_url( $link ) . '">' . esc_html( $text ) . '</a>';
+			} else {
+				$return = esc_html( $text );
+			}
+
+			/**
+			 * Filters the new message notification text before the notification is created.
+			 *
+			 * This is a dynamic filter. Possible filter names are:
+			 *   - 'bp_messages_multiple_new_message_notification'.
+			 *   - 'bp_messages_single_new_message_notification'.
+			 *
+			 * @param string $return            Notification text.
+			 * @param int    $total_items       Number of messages referred to by the notification.
+			 * @param string $text              The raw notification test (ie, not wrapped in a link).
+			 * @param int    $item_id           ID of the associated item.
+			 * @param int    $secondary_item_id ID of the secondary associated item.
+			 */
+			$return = apply_filters( 'bp_messages_' . $amount . '_new_message_notification', $return, (int) $total_items, $text, $link, $item_id, $secondary_item_id );
+		} else {
+			/** This filter is documented in bp-messages/bp-messages-notifications.php */
+			$return = apply_filters( 'bp_messages_' . $amount . '_new_message_notification', array(
+				'text' => $text,
+				'link' => $link
+			), $link, (int) $total_items, $text, $link, $item_id, $secondary_item_id );
+		}
+
+	// Custom notification action for the Messages component
+	} else {
+		if ( 'string' === $format ) {
+			$return = $text;
+		} else {
+			$return = array(
+				'text' => $text,
+				'link' => $link
+			);
+		}
+
+		/**
+		 * Backcompat for plugins that used to filter bp_messages_single_new_message_notification
+		 * for their custom actions. These plugins should now use 'bp_messages_' . $action . '_notification'
+		 */
+		if ( has_filter( 'bp_messages_single_new_message_notification' ) ) {
+			if ( 'string' === $format ) {
+				/** This filter is documented in bp-messages/bp-messages-notifications.php */
+				$return = apply_filters( 'bp_messages_single_new_message_notification', $return, (int) $total_items, $text, $link, $item_id, $secondary_item_id );
+
+			// Notice that there are seven parameters instead of six? Ugh...
+			} else {
+				/** This filter is documented in bp-messages/bp-messages-notifications.php */
+				$return = apply_filters( 'bp_messages_single_new_message_notification', $return, $link, (int) $total_items, $text, $link, $item_id, $secondary_item_id );
+			}
+		}
+
+		/**
+		 * Filters the custom action notification before the notification is created.
+		 *
+		 * This is a dynamic filter based on the message notification action.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param array  $value             An associative array containing the text and the link of the notification
+		 * @param int    $item_id           ID of the associated item.
+		 * @param int    $secondary_item_id ID of the secondary associated item.
+		 * @param int    $total_items       Number of messages referred to by the notification.
+		 * @param string $format            Return value format. 'string' for BuddyBar-compatible
+		 *                                  notifications; 'array' for WP Toolbar. Default: 'string'.
+		 */
+		$return = apply_filters( "bp_messages_{$action}_notification", $return, $item_id, $secondary_item_id, $total_items, $format );
+	}
+
+	/**
+	 * Fires right before returning the formatted message notifications.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $action            The type of message notification.
+	 * @param int    $item_id           The primary item ID.
+	 * @param int    $secondary_item_id The secondary item ID.
+	 * @param int    $total_items       Total amount of items to format.
+	 */
+	do_action( 'messages_format_notifications', $action, $item_id, $secondary_item_id, $total_items );
+
+	return $return;
+}
+
+/**
+ * Send notifications to message recipients.
+ *
+ * @since 1.9.0
+ *
+ * @param BP_Messages_Message $message Message object.
+ */
+function bp_messages_message_sent_add_notification( $message ) {
+	if ( ! empty( $message->recipients ) ) {
+		foreach ( (array) $message->recipients as $recipient ) {
+			bp_notifications_add_notification( array(
+				'user_id'           => $recipient->user_id,
+				'item_id'           => $message->id,
+				'secondary_item_id' => $message->sender_id,
+				'component_name'    => buddypress()->messages->id,
+				'component_action'  => 'new_message',
+				'date_notified'     => bp_core_current_time(),
+				'is_new'            => 1,
+			) );
+		}
+	}
+}
+add_action( 'messages_message_sent', 'bp_messages_message_sent_add_notification', 10 );
+
+/**
+ * Mark new message notification when member reads a message thread directly.
+ *
+ * @since 1.9.0
+ */
+function bp_messages_screen_conversation_mark_notifications() {
+	global $thread_template;
+
+	/*
+	 * Only run on the logged-in user's profile.
+	 * If an admin visits a thread, it shouldn't change the read status.
+	 */
+	if ( ! bp_is_my_profile() ) {
+		return;
+	}
+
+	// Get unread PM notifications for the user.
+	$new_pm_notifications = BP_Notifications_Notification::get( array(
+		'user_id'           => bp_loggedin_user_id(),
+		'component_name'    => buddypress()->messages->id,
+		'component_action'  => 'new_message',
+		'is_new'            => 1,
+	) );
+	$unread_message_ids = wp_list_pluck( $new_pm_notifications, 'item_id' );
+
+	// No unread PMs, so stop!
+	if ( empty( $unread_message_ids ) ) {
+		return;
+	}
+
+	// Get the unread message ids for this thread only.
+	$message_ids = array_intersect( $unread_message_ids, wp_list_pluck( $thread_template->thread->messages, 'id' ) );
+
+	// Mark each notification for each PM message as read.
+	foreach ( $message_ids as $message_id ) {
+		bp_notifications_mark_notifications_by_item_id( bp_loggedin_user_id(), (int) $message_id, buddypress()->messages->id, 'new_message' );
+	}
+}
+add_action( 'thread_loop_start', 'bp_messages_screen_conversation_mark_notifications', 10 );
+
+/**
+ * Mark new message notification as read when the corresponding message is mark read.
+ *
+ * This callback covers mark-as-read bulk actions.
+ *
+ * @since 3.0.0
+ *
+ * @param int $thread_id ID of the thread being marked as read.
+ */
+function bp_messages_mark_notification_on_mark_thread( $thread_id ) {
+	$thread_messages = BP_Messages_Thread::get_messages( $thread_id );
+
+	foreach ( $thread_messages as $thread_message ) {
+		bp_notifications_mark_notifications_by_item_id( bp_loggedin_user_id(), $thread_message->id, buddypress()->messages->id, 'new_message' );
+	}
+}
+add_action( 'messages_thread_mark_as_read', 'bp_messages_mark_notification_on_mark_thread' );
+
+/**
+ * When a message is deleted, delete corresponding notifications.
+ *
+ * @since 2.0.0
+ *
+ * @param int   $thread_id   ID of the thread.
+ * @param array $message_ids IDs of the messages.
+ */
+function bp_messages_message_delete_notifications( $thread_id, $message_ids ) {
+	// For each recipient, delete notifications corresponding to each message.
+	$thread = new BP_Messages_Thread( $thread_id );
+	foreach ( $thread->get_recipients() as $recipient ) {
+		foreach ( $message_ids as $message_id ) {
+			bp_notifications_delete_notifications_by_item_id( $recipient->user_id, (int) $message_id, buddypress()->messages->id, 'new_message' );
+		}
+	}
+}
+add_action( 'bp_messages_thread_after_delete', 'bp_messages_message_delete_notifications', 10, 2 );
+
+/**
+ * Render the markup for the Messages section of Settings > Notifications.
+ *
+ * @since 1.0.0
+ */
+function messages_screen_notification_settings() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	if ( !$new_messages = bp_get_user_meta( bp_displayed_user_id(), 'notification_messages_new_message', true ) ) {
+		$new_messages = 'yes';
+	} ?>
+
+	<table class="notification-settings" id="messages-notification-settings">
+		<thead>
+			<tr>
+				<th class="icon"></th>
+				<th class="title"><?php _e( 'Messages', 'buddypress' ) ?></th>
+				<th class="yes"><?php _e( 'Yes', 'buddypress' ) ?></th>
+				<th class="no"><?php _e( 'No', 'buddypress' )?></th>
+			</tr>
+		</thead>
+
+		<tbody>
+			<tr id="messages-notification-settings-new-message">
+				<td></td>
+				<td><?php _e( 'A member sends you a new message', 'buddypress' ) ?></td>
+				<td class="yes"><input type="radio" name="notifications[notification_messages_new_message]" id="notification-messages-new-messages-yes" value="yes" <?php checked( $new_messages, 'yes', true ) ?>/><label for="notification-messages-new-messages-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_messages_new_message]" id="notification-messages-new-messages-no" value="no" <?php checked( $new_messages, 'no', true ) ?>/><label for="notification-messages-new-messages-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 messages screen notification settings.
+			 *
+			 * @since 1.0.0
+			 */
+			do_action( 'messages_screen_notification_settings' ); ?>
+		</tbody>
+	</table>
+
+<?php
+}
+add_action( 'bp_notification_settings', 'messages_screen_notification_settings', 2 );
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-star.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-star.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-star.php	(revision 12265)
@@ -0,0 +1,384 @@
+<?php
+/**
+ * Functions related to starring private messages.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesStar
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** UTILITY **************************************************************/
+
+/**
+ * Return the starred messages slug. Defaults to 'starred'.
+ *
+ * @since 2.3.0
+ *
+ * @return string
+ */
+function bp_get_messages_starred_slug() {
+	/**
+	 * Filters the starred message slug.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string
+	 */
+	return sanitize_title( apply_filters( 'bp_get_messages_starred_slug', 'starred' ) );
+}
+
+/**
+ * Function to determine if a message ID is starred.
+ *
+ * @since 2.3.0
+ *
+ * @param  int $mid     The message ID. Please note that this isn't the message thread ID.
+ * @param  int $user_id The user ID.
+ * @return bool
+ */
+function bp_messages_is_message_starred( $mid = 0, $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	if ( empty( $mid ) ) {
+		return false;
+	}
+
+	$starred = array_flip( (array) bp_messages_get_meta( $mid, 'starred_by_user', false ) );
+
+	if ( isset( $starred[$user_id] ) ) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
+/**
+ * Output the link or raw URL for starring or unstarring a message.
+ *
+ * @since 2.3.0
+ *
+ * @param array $args See bp_get_the_message_star_action_link() for full documentation.
+ */
+function bp_the_message_star_action_link( $args = array() ) {
+	echo bp_get_the_message_star_action_link( $args );
+}
+	/**
+	 * Return the link or raw URL for starring or unstarring a message.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $args {
+	 *     Array of arguments.
+	 *     @type int    $user_id       The user ID. Defaults to the logged-in user ID.
+	 *     @type int    $thread_id     The message thread ID. Default: 0. If not zero, this takes precedence over
+	 *                                 $message_id.
+	 *     @type int    $message_id    The individual message ID. If on a single thread page, defaults to the
+	 *                                 current message ID in the message loop.
+	 *     @type bool   $url_only      Whether to return the URL only. If false, returns link with markup.
+	 *                                 Default: false.
+	 *     @type string $text_unstar   Link text for the 'unstar' action. Only applicable if $url_only is false.
+	 *     @type string $text_star     Link text for the 'star' action. Only applicable if $url_only is false.
+	 *     @type string $title_unstar  Link title for the 'unstar' action. Only applicable if $url_only is false.
+	 *     @type string $title_star    Link title for the 'star' action. Only applicable if $url_only is false.
+	 *     @type string $title_unstar_thread Link title for the 'unstar' action when displayed in a thread loop.
+	 *                                       Only applicable if $message_id is set and if $url_only is false.
+	 *     @type string $title_star_thread   Link title for the 'star' action when displayed in a thread loop.
+	 *                                       Only applicable if $message_id is set and if $url_only is false.
+	 * }
+	 * @return string
+	 */
+	function bp_get_the_message_star_action_link( $args = array() ) {
+
+		// Default user ID.
+		$user_id = bp_displayed_user_id()
+			? bp_displayed_user_id()
+			: bp_loggedin_user_id();
+
+		$r = bp_parse_args( $args, array(
+			'user_id'             => (int) $user_id,
+			'thread_id'           => 0,
+			'message_id'          => (int) bp_get_the_thread_message_id(),
+			'url_only'            => false,
+			'text_unstar'         => __( 'Unstar',      'buddypress' ),
+			'text_star'           => __( 'Star',        'buddypress' ),
+			'title_unstar'        => __( 'Starred',     'buddypress' ),
+			'title_star'          => __( 'Not starred', 'buddypress' ),
+			'title_unstar_thread' => __( 'Remove all starred messages in this thread', 'buddypress' ),
+			'title_star_thread'   => __( 'Star the first message in this thread',      'buddypress' ),
+		), 'messages_star_action_link' );
+
+		// Check user ID and determine base user URL.
+		switch ( $r['user_id'] ) {
+
+			// Current user.
+			case bp_loggedin_user_id() :
+				$user_domain = bp_loggedin_user_domain();
+				break;
+
+			// Displayed user.
+			case bp_displayed_user_id() :
+				$user_domain = bp_displayed_user_domain();
+				break;
+
+			// Empty or other.
+			default :
+				$user_domain = bp_core_get_user_domain( $r['user_id'] );
+				break;
+		}
+
+		// Bail if no user domain was calculated.
+		if ( empty( $user_domain ) ) {
+			return '';
+		}
+
+		// Define local variables.
+		$retval = $bulk_attr = '';
+
+		// Thread ID.
+		if ( (int) $r['thread_id'] > 0 ) {
+
+			// See if we're in the loop.
+			if ( bp_get_message_thread_id() == $r['thread_id'] ) {
+
+				// Grab all message ids.
+				$mids = wp_list_pluck( $GLOBALS['messages_template']->thread->messages, 'id' );
+
+				// Make sure order is ASC.
+				// Order is DESC when used in the thread loop by default.
+				$mids = array_reverse( $mids );
+
+			// Pull up the thread.
+			} else {
+				$thread = new BP_Messages_Thread( $r['thread_id'] );
+				$mids   = wp_list_pluck( $thread->messages, 'id' );
+			}
+
+			$is_starred = false;
+			$message_id = 0;
+			foreach ( $mids as $mid ) {
+
+				// Try to find the first msg that is starred in a thread.
+				if ( true === bp_messages_is_message_starred( $mid ) ) {
+					$is_starred = true;
+					$message_id = $mid;
+					break;
+				}
+			}
+
+			// No star, so default to first message in thread.
+			if ( empty( $message_id ) ) {
+				$message_id = $mids[0];
+			}
+
+			$message_id = (int) $message_id;
+
+			// Nonce.
+			$nonce = wp_create_nonce( "bp-messages-star-{$message_id}" );
+
+			if ( true === $is_starred ) {
+				$action    = 'unstar';
+				$bulk_attr = ' data-star-bulk="1"';
+				$retval    = $user_domain . bp_get_messages_slug() . '/unstar/' . $message_id . '/' . $nonce . '/all/';
+			} else {
+				$action    = 'star';
+				$retval    = $user_domain . bp_get_messages_slug() . '/star/' . $message_id . '/' . $nonce . '/';
+			}
+
+			$title = $r["title_{$action}_thread"];
+
+		// Message ID.
+		} else {
+			$message_id = (int) $r['message_id'];
+			$is_starred = bp_messages_is_message_starred( $message_id );
+			$nonce      = wp_create_nonce( "bp-messages-star-{$message_id}" );
+
+			if ( true === $is_starred ) {
+				$action = 'unstar';
+				$retval = $user_domain . bp_get_messages_slug() . '/unstar/' . $message_id . '/' . $nonce . '/';
+			} else {
+				$action = 'star';
+				$retval = $user_domain . bp_get_messages_slug() . '/star/' . $message_id . '/' . $nonce . '/';
+			}
+
+			$title = $r["title_{$action}"];
+		}
+
+		/**
+		 * Filters the star action URL for starring / unstarring a message.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string $retval URL for starring / unstarring a message.
+		 * @param array  $r      Parsed link arguments. See $args in bp_get_the_message_star_action_link().
+		 */
+		$retval = esc_url( apply_filters( 'bp_get_the_message_star_action_urlonly', $retval, $r ) );
+		if ( true === (bool) $r['url_only'] ) {
+			return $retval;
+		}
+
+		/**
+		 * Filters the star action link, including markup.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param string $retval Link for starring / unstarring a message, including markup.
+		 * @param array  $r      Parsed link arguments. See $args in bp_get_the_message_star_action_link().
+		 */
+		return apply_filters( 'bp_get_the_message_star_action_link', '<a data-bp-tooltip="' . esc_attr( $title ) . '" class="bp-tooltip message-action-' . esc_attr( $action ) . '" data-star-status="' . esc_attr( $action ) .'" data-star-nonce="' . esc_attr( $nonce ) . '"' . $bulk_attr . ' data-message-id="' . esc_attr( (int) $message_id ) . '" href="' . $retval . '" role="button" aria-pressed="false"><span class="icon"></span> <span class="bp-screen-reader-text">' . $r['text_' . $action] . '</span></a>', $r );
+	}
+
+/**
+ * Save or delete star message meta according to a message's star status.
+ *
+ * @since 2.3.0
+ *
+ * @param array $args {
+ *     Array of arguments.
+ *     @type string $action     The star action. Either 'star' or 'unstar'. Default: 'star'.
+ *     @type int    $thread_id  The message thread ID. Default: 0. If not zero, this takes precedence over
+ *                              $message_id.
+ *     @type int    $message_id The indivudal message ID to star or unstar.  Default: 0.
+ *     @type int    $user_id    The user ID. Defaults to the logged-in user ID.
+ *     @type bool   $bulk       Whether to mark all messages in a thread as a certain action. Only relevant
+ *                              when $action is 'unstar' at the moment. Default: false.
+ * }
+ * @return bool
+ */
+function bp_messages_star_set_action( $args = array() ) {
+	$r = wp_parse_args( $args, array(
+		'action'     => 'star',
+		'thread_id'  => 0,
+		'message_id' => 0,
+		'user_id'    => bp_displayed_user_id(),
+		'bulk'       => false
+	) );
+
+	// Set thread ID.
+	if ( ! empty( $r['thread_id'] ) ) {
+		$thread_id = (int) $r['thread_id'];
+	} else {
+		$thread_id = messages_get_message_thread_id( $r['message_id'] );
+	}
+	if ( empty( $thread_id ) ) {
+		return false;
+	}
+
+	// Check if user has access to thread.
+	if( ! messages_check_thread_access( $thread_id, $r['user_id'] ) ) {
+		return false;
+	}
+
+	$is_starred = bp_messages_is_message_starred( $r['message_id'], $r['user_id'] );
+
+	// Star.
+	if ( 'star' == $r['action'] ) {
+		if ( true === $is_starred ) {
+			return true;
+		} else {
+			bp_messages_add_meta( $r['message_id'], 'starred_by_user', $r['user_id'] );
+			return true;
+		}
+	// Unstar.
+	} else {
+		// Unstar one message.
+		if ( false === $r['bulk'] ) {
+			if ( false === $is_starred ) {
+				return true;
+			} else {
+				bp_messages_delete_meta( $r['message_id'], 'starred_by_user', $r['user_id'] );
+				return true;
+			}
+
+		// Unstar all messages in a thread.
+		} else {
+			$thread = new BP_Messages_Thread( $thread_id );
+			$mids = wp_list_pluck( $thread->messages, 'id' );
+
+			foreach ( $mids as $mid ) {
+				if ( true === bp_messages_is_message_starred( $mid, $r['user_id'] ) ) {
+					bp_messages_delete_meta( $mid, 'starred_by_user', $r['user_id'] );
+				}
+			}
+
+			return true;
+		}
+	}
+}
+
+/** HOOKS ****************************************************************/
+
+/**
+ * Enqueues the dashicons font.
+ *
+ * The dashicons font is used for the star / unstar icon.
+ *
+ * @since 2.3.0
+ */
+function bp_messages_star_enqueue_scripts() {
+	if ( ! bp_is_user_messages() ) {
+		return;
+	}
+
+	wp_enqueue_style( 'dashicons' );
+}
+add_action( 'bp_enqueue_scripts', 'bp_messages_star_enqueue_scripts' );
+
+/**
+ * Add the "Add star" and "Remove star" options to the bulk management list.
+ *
+ * @since 2.3.0
+ */
+function bp_messages_star_bulk_management_dropdown() {
+?>
+
+	<option value="star"><?php _e( 'Add star', 'buddypress' ); ?></option>
+	<option value="unstar"><?php _e( 'Remove star', 'buddypress' ); ?></option>
+
+<?php
+}
+add_action( 'bp_messages_bulk_management_dropdown', 'bp_messages_star_bulk_management_dropdown', 1 );
+
+/**
+ * Add CSS class for the current message depending on starred status.
+ *
+ * @since 2.3.0
+ *
+ * @param  array $retval Current CSS classes.
+ * @return array
+ */
+function bp_messages_star_message_css_class( $retval = array() ) {
+	if ( true === bp_messages_is_message_starred( bp_get_the_thread_message_id() ) ) {
+		$status = 'starred';
+	} else {
+		$status = 'not-starred';
+	}
+
+	// Add css class based on star status for the current message.
+	$retval[] = "message-{$status}";
+
+	return $retval;
+}
+add_filter( 'bp_get_the_thread_message_css_class', 'bp_messages_star_message_css_class' );
+
+/**
+ * Filter message threads by those starred by the logged-in user.
+ *
+ * @since 2.3.0
+ *
+ * @param  array $r Current message thread arguments.
+ * @return array $r Array of starred message threads.
+ */
+function bp_messages_filter_starred_message_threads( $r = array() ) {
+	$r['box'] = 'starred';
+	$r['meta_query'] = array( array(
+		'key'   => 'starred_by_user',
+		'value' => $r['user_id']
+	) );
+
+	return $r;
+}
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-template.php	(revision 12265)
@@ -0,0 +1,2163 @@
+<?php
+/**
+ * BuddyPress Messages Template Tags.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Retrieve private message threads for display in inbox/sentbox/notices.
+ *
+ * Similar to WordPress's have_posts() function, this function is responsible
+ * for querying the database and retrieving private messages for display inside
+ * the theme via individual template parts for a member's inbox/sentbox/notices.
+ *
+ * @since 1.0.0
+ *
+ * @global BP_Messages_Box_Template $messages_template
+ *
+ * @param array|string $args {
+ *     Array of arguments. All are optional.
+ *     @type int    $user_id      ID of the user whose threads are being loaded.
+ *                                Default: ID of the logged-in user.
+ *     @type string $box          Current "box" view. If not provided here, the current
+ *                                view will be inferred from the URL.
+ *     @type int    $per_page     Number of results to return per page. Default: 10.
+ *     @type int    $max          Max results to return. Default: false.
+ *     @type string $type         Type of messages to return. Values: 'all', 'read', 'unread'
+ *                                Default: 'all'
+ *     @type string $search_terms Terms to which to limit results. Default:
+ *                                the value of $_REQUEST['s'].
+ *     @type string $page_arg     URL argument used for the pagination param.
+ *                                Default: 'mpage'.
+ *     @type array  $meta_query   Meta query arguments. Only applicable if $box is
+ *                                not 'notices'. See WP_Meta_Query more details.
+ * }
+ * @return bool True if there are threads to display, otherwise false.
+ */
+function bp_has_message_threads( $args = array() ) {
+	global $messages_template;
+
+	// The default box the user is looking at.
+	$current_action = bp_current_action();
+	switch ( $current_action ) {
+		case 'sentbox' :
+		case 'notices' :
+		case 'inbox'   :
+			$default_box = $current_action;
+			break;
+		default :
+			$default_box = 'inbox';
+			break;
+	}
+
+	// User ID
+	// @todo displayed user for moderators that get this far?
+	$user_id = bp_displayed_user_id();
+
+	// Search Terms.
+	$search_terms = isset( $_REQUEST['s'] ) ? stripslashes( $_REQUEST['s'] ) : '';
+
+	// Parse the arguments.
+	$r = bp_parse_args( $args, array(
+		'user_id'      => $user_id,
+		'box'          => $default_box,
+		'per_page'     => 10,
+		'max'          => false,
+		'type'         => 'all',
+		'search_terms' => $search_terms,
+		'page_arg'     => 'mpage', // See https://buddypress.trac.wordpress.org/ticket/3679.
+		'meta_query'   => array()
+	), 'has_message_threads' );
+
+	// Load the messages loop global up with messages.
+	$messages_template = new BP_Messages_Box_Template( $r );
+
+	/**
+	 * Filters if there are any message threads to display in inbox/sentbox/notices.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool                     $value             Whether or not the message has threads.
+	 * @param BP_Messages_Box_Template $messages_template Current message box template object.
+	 * @param array                    $r                 Array of parsed arguments passed into function.
+	 */
+	return apply_filters( 'bp_has_message_threads', $messages_template->has_threads(), $messages_template, $r );
+}
+
+/**
+ * Check whether there are more threads to iterate over.
+ *
+ * @return bool
+ */
+function bp_message_threads() {
+	global $messages_template;
+	return $messages_template->message_threads();
+}
+
+/**
+ * Set up the current thread inside the loop.
+ *
+ * @return object
+ */
+function bp_message_thread() {
+	global $messages_template;
+	return $messages_template->the_message_thread();
+}
+
+/**
+ * Output the ID of the current thread in the loop.
+ */
+function bp_message_thread_id() {
+	echo bp_get_message_thread_id();
+}
+	/**
+	 * Get the ID of the current thread in the loop.
+	 *
+	 * @return int
+	 */
+	function bp_get_message_thread_id() {
+		global $messages_template;
+
+		/**
+		 * Filters the ID of the current thread in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $thread_id ID of the current thread in the loop.
+		 */
+		return apply_filters( 'bp_get_message_thread_id', $messages_template->thread->thread_id );
+	}
+
+/**
+ * Output the subject of the current thread in the loop.
+ */
+function bp_message_thread_subject() {
+	echo bp_get_message_thread_subject();
+}
+	/**
+	 * Get the subject of the current thread in the loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_thread_subject() {
+		global $messages_template;
+
+		/**
+		 * Filters the subject of the current thread in the loop.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Subject of the current thread in the loop.
+		 */
+		return apply_filters( 'bp_get_message_thread_subject', $messages_template->thread->last_message_subject );
+	}
+
+/**
+ * Output an excerpt from the current message in the loop.
+ */
+function bp_message_thread_excerpt() {
+	echo bp_get_message_thread_excerpt();
+}
+	/**
+	 * Generate an excerpt from the current message in the loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_thread_excerpt() {
+		global $messages_template;
+
+		/**
+		 * Filters the excerpt of the current thread in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Excerpt of the current thread in the loop.
+		 */
+		return apply_filters( 'bp_get_message_thread_excerpt', strip_tags( bp_create_excerpt( $messages_template->thread->last_message_content, 75 ) ) );
+	}
+
+/**
+ * Output the thread's last message content.
+ *
+ * When viewing your Inbox, the last message is the most recent message in
+ * the thread of which you are *not* the author.
+ *
+ * When viewing your Sentbox, last message is the most recent message in
+ * the thread of which you *are* the member.
+ *
+ * @since 2.0.0
+ */
+function bp_message_thread_content() {
+	echo bp_get_message_thread_content();
+}
+	/**
+	 * Return the thread's last message content.
+	 *
+	 * When viewing your Inbox, the last message is the most recent message in
+	 * the thread of which you are *not* the author.
+	 *
+	 * When viewing your Sentbox, last message is the most recent message in
+	 * the thread of which you *are* the member.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return string The raw content of the last message in the thread.
+	 */
+	function bp_get_message_thread_content() {
+		global $messages_template;
+
+		/**
+		 * Filters the content of the last message in the thread.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $last_message_content Content of the last message in the thread.
+		 */
+		return apply_filters( 'bp_get_message_thread_content', $messages_template->thread->last_message_content );
+	}
+
+/**
+ * Output a link to the page of the current thread's last author.
+ */
+function bp_message_thread_from() {
+	echo bp_get_message_thread_from();
+}
+	/**
+	 * Get a link to the page of the current thread's last author.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_thread_from() {
+		global $messages_template;
+
+		/**
+		 * Filters the link to the page of the current thread's last author.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Link to the page of the current thread's last author.
+		 */
+		return apply_filters( 'bp_get_message_thread_from', bp_core_get_userlink( $messages_template->thread->last_sender_id ) );
+	}
+
+/**
+ * Output links to the pages of the current thread's recipients.
+ */
+function bp_message_thread_to() {
+	echo bp_get_message_thread_to();
+}
+	/**
+	 * Generate HTML links to the pages of the current thread's recipients.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_thread_to() {
+		global $messages_template;
+
+		/**
+		 * Filters the HTML links to the pages of the current thread's recipients.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value HTML links to the pages of the current thread's recipients.
+		 */
+		return apply_filters( 'bp_message_thread_to', BP_Messages_Thread::get_recipient_links($messages_template->thread->recipients ) );
+	}
+
+/**
+ * Output the permalink for a particular thread.
+ *
+ * @since 2.9.0 Introduced `$user_id` parameter.
+ *
+ * @param int $thread_id Optional. ID of the thread. Default: current thread
+ *                       being iterated on in the loop.
+ * @param int $user_id   Optional. ID of the user relative to whom the link
+ *                       should be generated. Default: ID of logged-in user.
+ */
+function bp_message_thread_view_link( $thread_id = 0, $user_id = null ) {
+	echo bp_get_message_thread_view_link( $thread_id, $user_id );
+}
+	/**
+	 * Get the permalink of a particular thread.
+	 *
+	 * @since 2.9.0 Introduced `$user_id` parameter.
+	 *
+	 * @param int $thread_id Optional. ID of the thread. Default: current
+	 *                       thread being iterated on in the loop.
+	 * @param int $user_id   Optional. ID of the user relative to whom the link
+	 *                       should be generated. Default: ID of logged-in user.
+	 * @return string
+	 */
+	function bp_get_message_thread_view_link( $thread_id = 0, $user_id = null ) {
+		global $messages_template;
+
+		if ( empty( $messages_template ) && (int) $thread_id > 0 ) {
+			$thread_id = (int) $thread_id;
+		} elseif ( ! empty( $messages_template->thread->thread_id ) ) {
+			$thread_id = $messages_template->thread->thread_id;
+		}
+
+		if ( null === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$domain = bp_core_get_user_domain( $user_id );
+
+		/**
+		 * Filters the permalink of a particular thread.
+		 *
+		 * @since 1.0.0
+		 * @since 2.6.0 Added the `$thread_id` parameter.
+		 * @since 2.9.0 Added the `$user_id` parameter.
+		 *
+		 * @param string $value     Permalink of a particular thread.
+		 * @param int    $thread_id ID of the thread.
+		 * @param int    $user_id   ID of the user.
+		 */
+		return apply_filters( 'bp_get_message_thread_view_link', trailingslashit( $domain . bp_get_messages_slug() . '/view/' . $thread_id ), $thread_id, $user_id );
+	}
+
+/**
+ * Output the URL for deleting the current thread.
+ *
+ * @since 2.9.0 Introduced `$user_id` parameter.
+ *
+ * @param int $user_id Optional. ID of the user relative to whom the link
+ *                     should be generated. Default: ID of logged-in user.
+ */
+function bp_message_thread_delete_link( $user_id = null ) {
+	echo esc_url( bp_get_message_thread_delete_link( $user_id ) );
+}
+	/**
+	 * Generate the URL for deleting the current thread.
+	 *
+	 * @since 2.9.0 Introduced `$user_id` parameter.
+	 *
+	 * @param int $user_id Optional. ID of the user relative to whom the link
+	 *                     should be generated. Default: ID of logged-in user.
+	 * @return string
+	 */
+	function bp_get_message_thread_delete_link( $user_id = null ) {
+		global $messages_template;
+
+		if ( null === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$domain = bp_core_get_user_domain( $user_id );
+
+		/**
+		 * Filters the URL for deleting the current thread.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value   URL for deleting the current thread.
+		 * @param int    $user_id ID of the user relative to whom the link should be generated.
+		 */
+		return apply_filters( 'bp_get_message_thread_delete_link', wp_nonce_url( trailingslashit( $domain . bp_get_messages_slug() . '/' . bp_current_action() . '/delete/' . $messages_template->thread->thread_id ), 'messages_delete_thread' ), $user_id );
+	}
+
+/**
+ * Output the URL used for marking a single message thread as unread.
+ *
+ * Since this function directly outputs a URL, it is escaped.
+ *
+ * @since 2.2.0
+ * @since 2.9.0 Introduced `$user_id` parameter.
+ *
+ * @param int $user_id Optional. ID of the user relative to whom the link
+ *                     should be generated. Default: ID of logged-in user.
+ */
+function bp_the_message_thread_mark_unread_url( $user_id = null ) {
+	echo esc_url( bp_get_the_message_thread_mark_unread_url( $user_id ) );
+}
+	/**
+	 * Return the URL used for marking a single message thread as unread.
+	 *
+	 * @since 2.2.0
+	 * @since 2.9.0 Introduced `$user_id` parameter.
+	 *
+	 * @param int $user_id Optional. ID of the user relative to whom the link
+	 *                     should be generated. Default: ID of logged-in user.
+	 * @return string
+	 */
+	function bp_get_the_message_thread_mark_unread_url( $user_id = null ) {
+
+		// Get the message ID.
+		$id = bp_get_message_thread_id();
+
+		// Get the args to add to the URL.
+		$args = array(
+			'action'     => 'unread',
+			'message_id' => $id
+		);
+
+		if ( null === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$domain = bp_core_get_user_domain( $user_id );
+
+		// Base unread URL.
+		$url = trailingslashit( $domain . bp_get_messages_slug() . '/' . bp_current_action() . '/unread' );
+
+		// Add the args to the URL.
+		$url = add_query_arg( $args, $url );
+
+		// Add the nonce.
+		$url = wp_nonce_url( $url, 'bp_message_thread_mark_unread_' . $id );
+
+		/**
+		 * Filters the URL used for marking a single message thread as unread.
+		 *
+		 * @since 2.2.0
+		 * @since 2.9.0 Added `$user_id` parameter.
+		 *
+		 * @param string $url     URL used for marking a single message thread as unread.
+		 * @param int    $user_id ID of the user relative to whom the link should be generated.
+		 */
+		return apply_filters( 'bp_get_the_message_thread_mark_unread_url', $url, $user_id );
+	}
+
+/**
+ * Output the URL used for marking a single message thread as read.
+ *
+ * Since this function directly outputs a URL, it is escaped.
+ *
+ * @since 2.2.0
+ * @since 2.9.0 Introduced `$user_id` parameter.
+ *
+ * @param int $user_id Optional. ID of the user relative to whom the link
+ *                     should be generated. Default: ID of logged-in user.
+ */
+function bp_the_message_thread_mark_read_url( $user_id = null ) {
+	echo esc_url( bp_get_the_message_thread_mark_read_url( $user_id ) );
+}
+	/**
+	 * Return the URL used for marking a single message thread as read.
+	 *
+	 * @since 2.2.0
+	 * @since 2.9.0 Introduced `$user_id` parameter.
+	 *
+	 * @param int $user_id Optional. ID of the user relative to whom the link
+	 *                     should be generated. Default: ID of logged-in user.
+	 * @return string
+	 */
+	function bp_get_the_message_thread_mark_read_url( $user_id = null ) {
+
+		// Get the message ID.
+		$id = bp_get_message_thread_id();
+
+		// Get the args to add to the URL.
+		$args = array(
+			'action'     => 'read',
+			'message_id' => $id
+		);
+
+		if ( null === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$domain = bp_core_get_user_domain( $user_id );
+
+		// Base read URL.
+		$url = trailingslashit( $domain . bp_get_messages_slug() . '/' . bp_current_action() . '/read' );
+
+		// Add the args to the URL.
+		$url = add_query_arg( $args, $url );
+
+		// Add the nonce.
+		$url = wp_nonce_url( $url, 'bp_message_thread_mark_read_' . $id );
+
+		/**
+		 * Filters the URL used for marking a single message thread as read.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param string $url     URL used for marking a single message thread as read.
+		 * @param int    $user_id ID of the user relative to whom the link should be generated.
+		 */
+		return apply_filters( 'bp_get_the_message_thread_mark_read_url', $url );
+	}
+
+/**
+ * Output the CSS class for the current thread.
+ */
+function bp_message_css_class() {
+	echo esc_attr( bp_get_message_css_class() );
+}
+	/**
+	 * Generate the CSS class for the current thread.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_css_class() {
+		global $messages_template;
+
+		$class = false;
+
+		if ( $messages_template->current_thread % 2 == 1 ) {
+			$class .= 'alt';
+		}
+
+		/**
+		 * Filters the CSS class for the current thread.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param string $class Class string to be added to the list of classes.
+		 */
+		return apply_filters( 'bp_get_message_css_class', trim( $class ) );
+	}
+
+/**
+ * Check whether the current thread has unread items.
+ *
+ * @return bool True if there are unread items, otherwise false.
+ */
+function bp_message_thread_has_unread() {
+	global $messages_template;
+
+	$retval = ! empty( $messages_template->thread->unread_count )
+		? true
+		: false;
+
+	/**
+	 * Filters whether or not a message thread has unread items.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not a message thread has unread items.
+	 */
+	return apply_filters( 'bp_message_thread_has_unread', $retval );
+}
+
+/**
+ * Output the current thread's unread count.
+ */
+function bp_message_thread_unread_count() {
+	echo esc_html( bp_get_message_thread_unread_count() );
+}
+	/**
+	 * Get the current thread's unread count.
+	 *
+	 * @return int
+	 */
+	function bp_get_message_thread_unread_count() {
+		global $messages_template;
+
+		$count = ! empty( $messages_template->thread->unread_count )
+			? (int) $messages_template->thread->unread_count
+			: false;
+
+		/**
+		 * Filters the current thread's unread count.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $count Current thread unread count.
+		 */
+		return apply_filters( 'bp_get_message_thread_unread_count', $count );
+	}
+
+/**
+ * Output a thread's total message count.
+ *
+ * @since 2.2.0
+ *
+ * @param int|bool $thread_id Optional. ID of the thread. Defaults to current thread ID.
+ */
+function bp_message_thread_total_count( $thread_id = false ) {
+	echo bp_get_message_thread_total_count( $thread_id );
+}
+	/**
+	 * Get the current thread's total message count.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param int|bool $thread_id Optional. ID of the thread.
+	 *                            Defaults to current thread ID.
+	 * @return int
+	 */
+	function bp_get_message_thread_total_count( $thread_id = false ) {
+		if ( false === $thread_id ) {
+			$thread_id = bp_get_message_thread_id();
+		}
+
+		$thread_template = new BP_Messages_Thread_Template( $thread_id, 'ASC', array(
+			'update_meta_cache' => false
+		) );
+
+		$count = 0;
+		if ( ! empty( $thread_template->message_count ) ) {
+			$count = intval( $thread_template->message_count );
+		}
+
+		/**
+		 * Filters the current thread's total message count.
+		 *
+		 * @since 2.2.0
+		 * @since 2.6.0 Added the `$thread_id` parameter.
+		 *
+		 * @param int $count     Current thread total message count.
+		 * @param int $thread_id ID of the queried thread.
+		 */
+		return apply_filters( 'bp_get_message_thread_total_count', $count, $thread_id );
+	}
+
+/**
+ * Output markup for the current thread's total and unread count.
+ *
+ * @since 2.2.0
+ *
+ * @param int|bool $thread_id Optional. ID of the thread. Default: current thread ID.
+ */
+function bp_message_thread_total_and_unread_count( $thread_id = false ) {
+	echo bp_get_message_thread_total_and_unread_count( $thread_id );
+}
+	/**
+	 * Get markup for the current thread's total and unread count.
+	 *
+	 * @param int|bool $thread_id Optional. ID of the thread. Default: current thread ID.
+	 * @return string Markup displaying the total and unread count for the thread.
+	 */
+	function bp_get_message_thread_total_and_unread_count( $thread_id = false ) {
+		if ( false === $thread_id ) {
+			$thread_id = bp_get_message_thread_id();
+		}
+
+		$total  = bp_get_message_thread_total_count( $thread_id );
+		$unread = bp_get_message_thread_unread_count( $thread_id );
+
+		return sprintf(
+			/* translators: 1: total number, 2: accessibility text: number of unread messages */
+			'<span class="thread-count">(%1$s)</span> <span class="bp-screen-reader-text">%2$s</span>',
+			number_format_i18n( $total ),
+			sprintf( _n( '%d unread', '%d unread', $unread, 'buddypress' ), number_format_i18n( $unread ) )
+		);
+	}
+
+/**
+ * Output the unformatted date of the last post in the current thread.
+ */
+function bp_message_thread_last_post_date_raw() {
+	echo bp_get_message_thread_last_post_date_raw();
+}
+	/**
+	 * Get the unformatted date of the last post in the current thread.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_thread_last_post_date_raw() {
+		global $messages_template;
+
+		/**
+		 * Filters the unformatted date of the last post in the current thread.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $last_message_date Unformatted date of the last post in the current thread.
+		 */
+		return apply_filters( 'bp_get_message_thread_last_message_date', $messages_template->thread->last_message_date );
+	}
+
+/**
+ * Output the nicely formatted date of the last post in the current thread.
+ */
+function bp_message_thread_last_post_date() {
+	echo bp_get_message_thread_last_post_date();
+}
+	/**
+	 * Get the nicely formatted date of the last post in the current thread.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_thread_last_post_date() {
+
+		/**
+		 * Filters the nicely formatted date of the last post in the current thread.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $value Formatted date of the last post in the current thread.
+		 */
+		return apply_filters( 'bp_get_message_thread_last_post_date', bp_format_time( strtotime( bp_get_message_thread_last_post_date_raw() ) ) );
+	}
+
+/**
+ * Output the avatar for the last sender in the current message thread.
+ *
+ * @see bp_get_message_thread_avatar() for a description of arguments.
+ *
+ * @param array|string $args See {@link bp_get_message_thread_avatar()}.
+ */
+function bp_message_thread_avatar( $args = '' ) {
+	echo bp_get_message_thread_avatar( $args );
+}
+	/**
+	 * Return the avatar for the last sender in the current message thread.
+	 *
+	 * @see bp_core_fetch_avatar() For a description of arguments and
+	 *      return values.
+	 *
+	 * @param array|string $args {
+	 *     Arguments are listed here with an explanation of their defaults.
+	 *     For more information about the arguments, see
+	 *     {@link bp_core_fetch_avatar()}.
+	 *     @type string      $type   Default: 'thumb'.
+	 *     @type int|bool    $width  Default: false.
+	 *     @type int|bool    $height Default: false.
+	 *     @type string      $class  Default: 'avatar'.
+	 *     @type string|bool $id     Default: false.
+	 *     @type string      $alt    Default: 'Profile picture of [display name]'.
+	 * }
+	 * @return string User avatar string.
+	 */
+	function bp_get_message_thread_avatar( $args = '' ) {
+		global $messages_template;
+
+		$fullname = bp_core_get_user_displayname( $messages_template->thread->last_sender_id );
+		$alt      = sprintf( __( 'Profile picture of %s', 'buddypress' ), $fullname );
+
+		$r = bp_parse_args( $args, array(
+			'type'   => 'thumb',
+			'width'  => false,
+			'height' => false,
+			'class'  => 'avatar',
+			'id'     => false,
+			'alt'    => $alt
+		) );
+
+		/**
+		 * Filters the avatar for the last sender in the current message thread.
+		 *
+		 * @since 1.0.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $value User avatar string.
+		 * @param array  $r     Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_message_thread_avatar', bp_core_fetch_avatar( array(
+			'item_id' => $messages_template->thread->last_sender_id,
+			'type'    => $r['type'],
+			'alt'     => $r['alt'],
+			'css_id'  => $r['id'],
+			'class'   => $r['class'],
+			'width'   => $r['width'],
+			'height'  => $r['height'],
+		) ), $r );
+	}
+
+/**
+ * Output the unread messages count for the current inbox.
+ *
+ * @since 2.6.x Added $user_id argument.
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return int $unread_count Total inbox unread count for user.
+ */
+function bp_total_unread_messages_count( $user_id = 0 ) {
+	echo bp_get_total_unread_messages_count( $user_id );
+}
+	/**
+	 * Get the unread messages count for the current inbox.
+	 *
+	 * @since 2.6.x Added $user_id argument.
+	 *
+	 * @param int $user_id The user ID.
+	 *
+	 * @return int $unread_count Total inbox unread count for user.
+	 */
+	function bp_get_total_unread_messages_count( $user_id = 0 ) {
+
+		/**
+		 * Filters the unread messages count for the current inbox.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $value Unread messages count for the current inbox.
+		 */
+		return apply_filters( 'bp_get_total_unread_messages_count', BP_Messages_Thread::get_inbox_count( $user_id ) );
+	}
+
+/**
+ * Output the pagination HTML for the current thread loop.
+ */
+function bp_messages_pagination() {
+	echo bp_get_messages_pagination();
+}
+	/**
+	 * Get the pagination HTML for the current thread loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_messages_pagination() {
+		global $messages_template;
+
+		/**
+		 * Filters the pagination HTML for the current thread loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $pag_links Pagination HTML for the current thread loop.
+		 */
+		return apply_filters( 'bp_get_messages_pagination', $messages_template->pag_links );
+	}
+
+/**
+ * Generate the "Viewing message x to y (of z messages)" string for a loop.
+ */
+function bp_messages_pagination_count() {
+	global $messages_template;
+
+	$start_num = intval( ( $messages_template->pag_page - 1 ) * $messages_template->pag_num ) + 1;
+	$from_num  = bp_core_number_format( $start_num );
+	$to_num    = bp_core_number_format( ( $start_num + ( $messages_template->pag_num - 1 ) > $messages_template->total_thread_count ) ? $messages_template->total_thread_count : $start_num + ( $messages_template->pag_num - 1 ) );
+	$total     = bp_core_number_format( $messages_template->total_thread_count );
+
+	if ( 1 == $messages_template->total_thread_count ) {
+		$message = __( 'Viewing 1 message', 'buddypress' );
+	} else {
+		$message = sprintf( _n( 'Viewing %1$s - %2$s of %3$s message', 'Viewing %1$s - %2$s of %3$s messages', $messages_template->total_thread_count, 'buddypress' ), $from_num, $to_num, $total );
+	}
+
+	echo esc_html( $message );
+}
+
+/**
+ * Output the Private Message search form.
+ *
+ * @todo  Move markup to template part in: /members/single/messages/search.php
+ * @since 1.6.0
+ */
+function bp_message_search_form() {
+
+	// Get the default search text.
+	$default_search_value = bp_get_search_default_text( 'messages' );
+
+	// Setup a few values based on what's being searched for.
+	$search_submitted     = ! empty( $_REQUEST['s'] ) ? stripslashes( $_REQUEST['s'] ) : $default_search_value;
+	$search_placeholder   = ( $search_submitted === $default_search_value ) ? ' placeholder="' .  esc_attr( $search_submitted ) . '"' : '';
+	$search_value         = ( $search_submitted !== $default_search_value ) ? ' value="'       .  esc_attr( $search_submitted ) . '"' : '';
+
+	// Start the output buffer, so form can be filtered.
+	ob_start(); ?>
+
+	<form action="" method="get" id="search-message-form">
+		<label for="messages_search" class="bp-screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Search Messages', 'buddypress' );
+		?></label>
+		<input type="text" name="s" id="messages_search"<?php echo $search_placeholder . $search_value; ?> />
+		<input type="submit" class="button" id="messages_search_submit" name="messages_search_submit" value="<?php esc_html_e( 'Search', 'buddypress' ); ?>" />
+	</form>
+
+	<?php
+
+	// Get the search form from the above output buffer.
+	$search_form_html = ob_get_clean();
+
+	/**
+	 * Filters the private message component search form.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $search_form_html HTML markup for the message search form.
+	 */
+	echo apply_filters( 'bp_message_search_form', $search_form_html );
+}
+
+/**
+ * Echo the form action for Messages HTML forms.
+ */
+function bp_messages_form_action() {
+	echo esc_url( bp_get_messages_form_action() );
+}
+	/**
+	 * Return the form action for Messages HTML forms.
+	 *
+	 * @return string The form action.
+	 */
+	function bp_get_messages_form_action() {
+
+		/**
+		 * Filters the form action for Messages HTML forms.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value The form action.
+		 */
+		return apply_filters( 'bp_get_messages_form_action', trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() . '/' . bp_action_variable( 0 ) ) );
+	}
+
+/**
+ * Output the default username for the recipient box.
+ */
+function bp_messages_username_value() {
+	echo esc_attr( bp_get_messages_username_value() );
+}
+	/**
+	 * Get the default username for the recipient box.
+	 *
+	 * @return string
+	 */
+	function bp_get_messages_username_value() {
+		if ( isset( $_COOKIE['bp_messages_send_to'] ) ) {
+
+			/**
+			 * Filters the default username for the recipient box.
+			 *
+			 * Value passed into filter is dependent on if the 'bp_messages_send_to'
+			 * cookie or 'r' $_GET parameter is set.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param string $value Default user name.
+			 */
+			return apply_filters( 'bp_get_messages_username_value', $_COOKIE['bp_messages_send_to'] );
+		} elseif ( isset( $_GET['r'] ) && !isset( $_COOKIE['bp_messages_send_to'] ) ) {
+			/** This filter is documented in bp-messages-template.php */
+			return apply_filters( 'bp_get_messages_username_value', $_GET['r'] );
+		}
+	}
+
+/**
+ * Output the default value for the Subject field.
+ */
+function bp_messages_subject_value() {
+	echo esc_attr( bp_get_messages_subject_value() );
+}
+	/**
+	 * Get the default value for the Subject field.
+	 *
+	 * Will get a value out of $_POST['subject'] if available (ie after a
+	 * failed submission).
+	 *
+	 * @return string
+	 */
+	function bp_get_messages_subject_value() {
+
+		// Sanitized in bp-messages-filters.php.
+		$subject = ! empty( $_POST['subject'] )
+			? $_POST['subject']
+			: '';
+
+		/**
+		 * Filters the default value for the subject field.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $subject The default value for the subject field.
+		 */
+		return apply_filters( 'bp_get_messages_subject_value', $subject );
+	}
+
+/**
+ * Output the default value for the Compose content field.
+ */
+function bp_messages_content_value() {
+	echo esc_textarea( bp_get_messages_content_value() );
+}
+	/**
+	 * Get the default value fo the Compose content field.
+	 *
+	 * Will get a value out of $_POST['content'] if available (ie after a
+	 * failed submission).
+	 *
+	 * @return string
+	 */
+	function bp_get_messages_content_value() {
+
+		// Sanitized in bp-messages-filters.php.
+		$content = ! empty( $_POST['content'] )
+			? $_POST['content']
+			: '';
+
+		/**
+		 * Filters the default value for the content field.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $content The default value for the content field.
+		 */
+		return apply_filters( 'bp_get_messages_content_value', $content );
+	}
+
+/**
+ * Output the markup for the message type dropdown.
+ */
+function bp_messages_options() {
+?>
+
+	<label for="message-type-select" class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Select:', 'buddypress' );
+	?></label>
+	<select name="message-type-select" id="message-type-select">
+		<option value=""><?php _e( 'Select', 'buddypress' ); ?></option>
+		<option value="read"><?php _ex('Read', 'Message dropdown filter', 'buddypress') ?></option>
+		<option value="unread"><?php _ex('Unread', 'Message dropdown filter', 'buddypress') ?></option>
+		<option value="all"><?php _ex('All', 'Message dropdown filter', 'buddypress') ?></option>
+	</select> &nbsp;
+
+	<?php if ( ! bp_is_current_action( 'sentbox' ) && ! bp_is_current_action( 'notices' ) ) : ?>
+
+		<a href="#" id="mark_as_read"><?php _ex('Mark as Read', 'Message management markup', 'buddypress') ?></a> &nbsp;
+		<a href="#" id="mark_as_unread"><?php _ex('Mark as Unread', 'Message management markup', 'buddypress') ?></a> &nbsp;
+
+		<?php wp_nonce_field( 'bp_messages_mark_messages_read', 'mark-messages-read-nonce', false ); ?>
+		<?php wp_nonce_field( 'bp_messages_mark_messages_unread', 'mark-messages-unread-nonce', false ); ?>
+
+	<?php endif; ?>
+
+	<a href="#" id="delete_<?php echo bp_current_action(); ?>_messages"><?php _e( 'Delete Selected', 'buddypress' ); ?></a> &nbsp;
+	<?php wp_nonce_field( 'bp_messages_delete_selected', 'delete-selected-nonce', false ); ?>
+<?php
+}
+
+/**
+ * Output the dropdown for bulk management of messages.
+ *
+ * @since 2.2.0
+ */
+function bp_messages_bulk_management_dropdown() {
+	?>
+	<label class="bp-screen-reader-text" for="messages-select"><?php
+		_e( 'Select Bulk Action', 'buddypress' );
+	?></label>
+	<select name="messages_bulk_action" id="messages-select">
+		<option value="" selected="selected"><?php _e( 'Bulk Actions', 'buddypress' ); ?></option>
+		<option value="read"><?php _e( 'Mark read', 'buddypress' ); ?></option>
+		<option value="unread"><?php _e( 'Mark unread', 'buddypress' ); ?></option>
+		<option value="delete"><?php _e( 'Delete', 'buddypress' ); ?></option>
+		<?php
+			/**
+			 * Action to add additional options to the messages bulk management dropdown.
+			 *
+			 * @since 2.3.0
+			 */
+			do_action( 'bp_messages_bulk_management_dropdown' );
+		?>
+	</select>
+	<input type="submit" id="messages-bulk-manage" class="button action" value="<?php esc_attr_e( 'Apply', 'buddypress' ); ?>">
+	<?php
+}
+
+/**
+ * Return whether or not the notice is currently active.
+ *
+ * @since 1.6.0
+ *
+ * @return bool
+ */
+function bp_messages_is_active_notice() {
+	global $messages_template;
+
+	$retval = ! empty( $messages_template->thread->is_active )
+		? true
+		: false;
+
+	/**
+	 * Filters whether or not the notice is currently active.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $retval Whether or not the notice is currently active.
+	 */
+	return apply_filters( 'bp_messages_is_active_notice', $retval );
+}
+
+/**
+ * Output a string for the active notice.
+ *
+ * Since 1.6 this function has been deprecated in favor of text in the theme.
+ *
+ * @since 1.0.0
+ * @deprecated 1.6.0
+ * @return bool
+ */
+function bp_message_is_active_notice() {
+	echo bp_get_message_is_active_notice();
+}
+	/**
+	 * Returns a string for the active notice.
+	 *
+	 * Since 1.6 this function has been deprecated in favor of text in the
+	 * theme.
+	 *
+	 * @since 1.0.0
+	 * @deprecated 1.6.0
+	 * @return string
+	 */
+	function bp_get_message_is_active_notice() {
+
+		$string = bp_messages_is_active_notice()
+			? __( 'Currently Active', 'buddypress' )
+			: '';
+
+		return apply_filters( 'bp_get_message_is_active_notice', $string );
+	}
+
+/**
+ * Output the ID of the current notice in the loop.
+ */
+function bp_message_notice_id() {
+	echo (int) bp_get_message_notice_id();
+}
+	/**
+	 * Get the ID of the current notice in the loop.
+	 *
+	 * @return int
+	 */
+	function bp_get_message_notice_id() {
+		global $messages_template;
+
+		/**
+		 * Filters the ID of the current notice in the loop.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $id ID of the current notice in the loop.
+		 */
+		return apply_filters( 'bp_get_message_notice_id', $messages_template->thread->id );
+	}
+
+/**
+ * Output the post date of the current notice in the loop.
+ */
+function bp_message_notice_post_date() {
+	echo bp_get_message_notice_post_date();
+}
+	/**
+	 * Get the post date of the current notice in the loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_notice_post_date() {
+		global $messages_template;
+
+		/**
+		 * Filters the post date of the current notice in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Formatted post date of the current notice in the loop.
+		 */
+		return apply_filters( 'bp_get_message_notice_post_date', bp_format_time( strtotime( $messages_template->thread->date_sent ) ) );
+	}
+
+/**
+ * Output the subject of the current notice in the loop.
+ */
+function bp_message_notice_subject() {
+	echo bp_get_message_notice_subject();
+}
+	/**
+	 * Get the subject of the current notice in the loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_notice_subject() {
+		global $messages_template;
+
+		/**
+		 * Filters the subject of the current notice in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $subject Subject of the current notice in the loop.
+		 */
+		return apply_filters( 'bp_get_message_notice_subject', $messages_template->thread->subject );
+	}
+
+/**
+ * Output the text of the current notice in the loop.
+ */
+function bp_message_notice_text() {
+	echo bp_get_message_notice_text();
+}
+	/**
+	 * Get the text of the current notice in the loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_notice_text() {
+		global $messages_template;
+
+		/**
+		 * Filters the text of the current notice in the loop.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $message Text for the current notice in the loop.
+		 */
+		return apply_filters( 'bp_get_message_notice_text', $messages_template->thread->message );
+	}
+
+/**
+ * Output the URL for deleting the current notice.
+ */
+function bp_message_notice_delete_link() {
+	echo esc_url( bp_get_message_notice_delete_link() );
+}
+	/**
+	 * Get the URL for deleting the current notice.
+	 *
+	 * @return string Delete URL.
+	 */
+	function bp_get_message_notice_delete_link() {
+		global $messages_template;
+
+		/**
+		 * Filters the URL for deleting the current notice.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value URL for deleting the current notice.
+		 * @param string $value Text indicating action being executed.
+		 */
+		return apply_filters( 'bp_get_message_notice_delete_link', wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() . '/notices/delete/' . $messages_template->thread->id ), 'messages_delete_notice' ) );
+	}
+
+/**
+ * Output the URL for deactivating the current notice.
+ */
+function bp_message_activate_deactivate_link() {
+	echo esc_url( bp_get_message_activate_deactivate_link() );
+}
+	/**
+	 * Get the URL for deactivating the current notice.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_activate_deactivate_link() {
+		global $messages_template;
+
+		if ( 1 === (int) $messages_template->thread->is_active ) {
+			$link = wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() . '/notices/deactivate/' . $messages_template->thread->id ), 'messages_deactivate_notice' );
+		} else {
+			$link = wp_nonce_url( trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() . '/notices/activate/' . $messages_template->thread->id ), 'messages_activate_notice' );
+		}
+
+		/**
+		 * Filters the URL for deactivating the current notice.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $link URL for deactivating the current notice.
+		 */
+		return apply_filters( 'bp_get_message_activate_deactivate_link', $link );
+	}
+
+/**
+ * Output the Deactivate/Activate text for the notice action link.
+ */
+function bp_message_activate_deactivate_text() {
+	echo esc_html( bp_get_message_activate_deactivate_text() );
+}
+	/**
+	 * Generate the text ('Deactivate' or 'Activate') for the notice action link.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_activate_deactivate_text() {
+		global $messages_template;
+
+		if ( 1 === (int) $messages_template->thread->is_active  ) {
+			$text = __('Deactivate', 'buddypress');
+		} else {
+			$text = __('Activate', 'buddypress');
+		}
+
+		/**
+		 * Filters the "Deactivate" or "Activate" text for notice action links.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $text Text used for notice action links.
+		 */
+		return apply_filters( 'bp_message_activate_deactivate_text', $text );
+	}
+
+/**
+ * Output the messages component slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_messages_slug() {
+	echo bp_get_messages_slug();
+}
+	/**
+	 * Return the messages component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_messages_slug() {
+
+		/**
+		 * Filters the messages component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Messages component slug.
+		 */
+		return apply_filters( 'bp_get_messages_slug', buddypress()->messages->slug );
+	}
+
+/**
+ * Generate markup for currently active notices.
+ */
+function bp_message_get_notices() {
+	$notice = BP_Messages_Notice::get_active();
+
+	if ( empty( $notice ) ) {
+		return false;
+	}
+
+	$closed_notices = bp_get_user_meta( bp_loggedin_user_id(), 'closed_notices', true );
+
+	if ( empty( $closed_notices ) ) {
+		$closed_notices = array();
+	}
+
+	if ( is_array( $closed_notices ) ) {
+		if ( !in_array( $notice->id, $closed_notices ) && $notice->id ) {
+			?>
+			<div id="message" class="info notice" rel="n-<?php echo esc_attr( $notice->id ); ?>">
+				<p>
+					<strong><?php echo stripslashes( wp_filter_kses( $notice->subject ) ) ?></strong><br />
+					<?php echo stripslashes( wp_filter_kses( $notice->message) ) ?>
+					<button type="button" id="close-notice" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( 'Dismiss this notice', 'buddypress' ) ?>"><span class="bp-screen-reader-text"><?php _e( 'Dismiss this notice', 'buddypress' ) ?></span> <span aria-hidden="true">&Chi;</span></button>
+					<?php wp_nonce_field( 'bp_messages_close_notice', 'close-notice-nonce' ); ?>
+				</p>
+			</div>
+			<?php
+		}
+	}
+}
+
+/**
+ * Output the URL for the Private Message link in member profile headers.
+ */
+function bp_send_private_message_link() {
+	echo esc_url( bp_get_send_private_message_link() );
+}
+	/**
+	 * Generate the URL for the Private Message link in member profile headers.
+	 *
+	 * @return bool|string False on failure, otherwise the URL.
+	 */
+	function bp_get_send_private_message_link() {
+
+		if ( bp_is_my_profile() || ! is_user_logged_in() ) {
+			return false;
+		}
+
+		/**
+		 * Filters the URL for the Private Message link in member profile headers.
+		 *
+		 * @since 1.2.10
+		 *
+		 * @param string $value URL for the Private Message link in member profile headers.
+		 */
+		return apply_filters( 'bp_get_send_private_message_link', wp_nonce_url( bp_loggedin_user_domain() . bp_get_messages_slug() . '/compose/?r=' . bp_core_get_username( bp_displayed_user_id() ) ) );
+	}
+
+/**
+ * Output the 'Private Message' button for member profile headers.
+ *
+ * Explicitly named function to avoid confusion with public messages.
+ *
+ * @since 1.2.6
+ *
+ */
+function bp_send_private_message_button() {
+	echo bp_get_send_message_button();
+}
+
+/**
+ * Output the 'Private Message' button for member profile headers.
+ *
+ * @since 1.2.0
+ * @since 3.0.0 Added `$args` parameter.
+ *
+ * @param array|string $args See {@link bp_get_send_message_button()}.
+ */
+function bp_send_message_button( $args = '' ) {
+	echo bp_get_send_message_button( $args );
+}
+	/**
+	 * Generate the 'Private Message' button for member profile headers.
+	 *
+	 * @since 1.2.0
+	 * @since 3.0.0 Added `$args` parameter.
+	 *
+	 * @param array|string $args {
+	 *     All arguments are optional. See {@link BP_Button} for complete
+	 *     descriptions.
+	 *     @type string $id                Default: 'private_message'.
+	 *     @type string $component         Default: 'messages'.
+	 *     @type bool   $must_be_logged_in Default: true.
+	 *     @type bool   $block_self        Default: true.
+	 *     @type string $wrapper_id        Default: 'send-private-message'.
+	 *     @type string $link_href         Default: the private message link for
+	 *                                     the current member in the loop.
+	 *     @type string $link_text         Default: 'Private Message'.
+	 *     @type string $link_class        Default: 'send-message'.
+	 * }
+	 * @return string
+	 */
+	function bp_get_send_message_button( $args = '' ) {
+
+		$r = bp_parse_args( $args, array(
+			'id'                => 'private_message',
+			'component'         => 'messages',
+			'must_be_logged_in' => true,
+			'block_self'        => true,
+			'wrapper_id'        => 'send-private-message',
+			'link_href'         => bp_get_send_private_message_link(),
+			'link_text'         => __( 'Private Message', 'buddypress' ),
+			'link_class'        => 'send-message',
+		) );
+
+
+		// Note: 'bp_get_send_message_button' is a legacy filter. Use
+		// 'bp_get_send_message_button_args' instead. See #4536.
+		return apply_filters( 'bp_get_send_message_button',
+
+			/**
+			 * Filters the "Private Message" button for member profile headers.
+			 *
+			 * @since 1.8.0
+			 *
+			 * @param array $value See {@link BP_Button}.
+			 */
+			bp_get_button( apply_filters( 'bp_get_send_message_button_args', $r ) )
+		);
+	}
+
+/**
+ * Output the URL of the Messages AJAX loader gif.
+ */
+function bp_message_loading_image_src() {
+	echo esc_url( bp_get_message_loading_image_src() );
+}
+	/**
+	 * Get the URL of the Messages AJAX loader gif.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_loading_image_src() {
+
+		/**
+		 * Filters the URL of the Messages AJAX loader gif.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value URL of the Messages AJAX loader gif.
+		 */
+		return apply_filters( 'bp_get_message_loading_image_src', buddypress()->messages->image_base . '/ajax-loader.gif' );
+	}
+
+/**
+ * Output the markup for the message recipient tabs.
+ */
+function bp_message_get_recipient_tabs() {
+	$recipients = explode( ' ', bp_get_message_get_recipient_usernames() );
+
+	foreach ( $recipients as $recipient ) {
+
+		$user_id = bp_is_username_compatibility_mode()
+			? bp_core_get_userid( $recipient )
+			: bp_core_get_userid_from_nicename( $recipient );
+
+		if ( ! empty( $user_id ) ) : ?>
+
+			<li id="un-<?php echo esc_attr( $recipient ); ?>" class="friend-tab">
+				<span><?php
+					echo bp_core_fetch_avatar( array( 'item_id' => $user_id, 'type' => 'thumb', 'width' => 15, 'height' => 15 ) );
+					echo bp_core_get_userlink( $user_id );
+				?></span>
+			</li>
+
+		<?php endif;
+	}
+}
+
+/**
+ * Output recipient usernames for prefilling the 'To' field on the Compose screen.
+ */
+function bp_message_get_recipient_usernames() {
+	echo esc_attr( bp_get_message_get_recipient_usernames() );
+}
+	/**
+	 * Get the recipient usernames for prefilling the 'To' field on the Compose screen.
+	 *
+	 * @return string
+	 */
+	function bp_get_message_get_recipient_usernames() {
+
+		// Sanitized in bp-messages-filters.php.
+		$recipients = isset( $_GET['r'] )
+			? $_GET['r']
+			: '';
+
+		/**
+		 * Filters the recipients usernames for prefilling the 'To' field on the Compose screen.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $recipients Recipients usernames for 'To' field prefilling.
+		 */
+		return apply_filters( 'bp_get_message_get_recipient_usernames', $recipients );
+	}
+
+/**
+ * Initialize the messages template loop for a specific thread.
+ *
+ * @param array|string $args {
+ *     Array of arguments. All are optional.
+ *     @type int    $thread_id         ID of the thread whose messages you are displaying.
+ *                                     Default: if viewing a thread, the thread ID will be parsed from
+ *                                     the URL (bp_action_variable( 0 )).
+ *     @type string $order             'ASC' or 'DESC'. Default: 'ASC'.
+ *     @type bool   $update_meta_cache Whether to pre-fetch metadata for
+ *                                     queried message items. Default: true.
+ * }
+ * @return bool True if there are messages to display, otherwise false.
+ */
+function bp_thread_has_messages( $args = '' ) {
+	global $thread_template;
+
+	$r = bp_parse_args( $args, array(
+		'thread_id'         => false,
+		'order'             => 'ASC',
+		'update_meta_cache' => true,
+	), 'thread_has_messages' );
+
+	if ( empty( $r['thread_id'] ) && bp_is_messages_component() && bp_is_current_action( 'view' ) ) {
+		$r['thread_id'] = (int) bp_action_variable( 0 );
+	}
+
+	// Set up extra args.
+	$extra_args = $r;
+	unset( $extra_args['thread_id'], $extra_args['order'] );
+
+	$thread_template = new BP_Messages_Thread_Template( $r['thread_id'], $r['order'], $extra_args );
+
+	return $thread_template->has_messages();
+}
+
+/**
+ * Output the 'ASC' or 'DESC' messages order string for this loop.
+ */
+function bp_thread_messages_order() {
+	echo esc_attr( bp_get_thread_messages_order() );
+}
+	/**
+	 * Get the 'ASC' or 'DESC' messages order string for this loop.
+	 *
+	 * @return string
+	 */
+	function bp_get_thread_messages_order() {
+		global $thread_template;
+		return $thread_template->thread->messages_order;
+	}
+
+/**
+ * Check whether there are more messages to iterate over.
+ *
+ * @return bool
+ */
+function bp_thread_messages() {
+	global $thread_template;
+
+	return $thread_template->messages();
+}
+
+/**
+ * Set up the current thread inside the loop.
+ *
+ * @return object
+ */
+function bp_thread_the_message() {
+	global $thread_template;
+
+	return $thread_template->the_message();
+}
+
+/**
+ * Output the ID of the thread that the current loop belongs to.
+ */
+function bp_the_thread_id() {
+	echo (int) bp_get_the_thread_id();
+}
+	/**
+	 * Get the ID of the thread that the current loop belongs to.
+	 *
+	 * @return int
+	 */
+	function bp_get_the_thread_id() {
+		global $thread_template;
+
+		/**
+		 * Filters the ID of the thread that the current loop belongs to.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $thread_id ID of the thread.
+		 */
+		return apply_filters( 'bp_get_the_thread_id', $thread_template->thread->thread_id );
+	}
+
+/**
+ * Output the subject of the thread currently being iterated over.
+ */
+function bp_the_thread_subject() {
+	echo bp_get_the_thread_subject();
+}
+	/**
+	 * Get the subject of the thread currently being iterated over.
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_subject() {
+		global $thread_template;
+
+		/**
+		 * Filters the subject of the thread currently being iterated over.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @return string $last_message_subject Subject of the thread currently being iterated over.
+		 */
+		return apply_filters( 'bp_get_the_thread_subject', $thread_template->thread->last_message_subject );
+	}
+
+/**
+ * Get a list of thread recipients or a "x recipients" string.
+ *
+ * In BuddyPress 2.2.0, this parts of this functionality were moved into the
+ * members/single/messages/single.php template. This function is no longer used
+ * by BuddyPress.
+ *
+ * @return string
+ */
+function bp_get_the_thread_recipients() {
+	if ( 5 <= bp_get_thread_recipients_count() ) {
+		$recipients = sprintf( __( '%s recipients', 'buddypress' ), number_format_i18n( bp_get_thread_recipients_count() ) );
+	} else {
+		$recipients = bp_get_thread_recipients_list();
+	}
+
+	return apply_filters( 'bp_get_the_thread_recipients', $recipients );
+}
+
+/**
+ * Get the number of recipients in the current thread.
+ *
+ * @since 2.2.0
+ *
+ * @return int
+ */
+function bp_get_thread_recipients_count() {
+	global $thread_template;
+	/**
+	 * Filters the total number of recipients in a thread.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param int $count Total recipients number.
+	 */
+	return (int) apply_filters( 'bp_get_thread_recipients_count', count( $thread_template->thread->recipients ) );
+}
+
+/**
+ * Get the max number of recipients to list in the 'Conversation between...' gloss.
+ *
+ * @since 2.3.0
+ *
+ * @return int
+ */
+function bp_get_max_thread_recipients_to_list() {
+	/**
+	 * Filters the max number of recipients to list in the 'Conversation between...' gloss.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param int $count Recipient count. Default: 5.
+	 */
+	return (int) apply_filters( 'bp_get_max_thread_recipients_to_list', 5 );
+}
+
+/**
+ * Output HTML links to recipients in the current thread.
+ *
+ * @since 2.2.0
+ */
+function bp_the_thread_recipients_list() {
+	echo bp_get_thread_recipients_list();
+}
+	/**
+	 * Generate HTML links to the profiles of recipients in the current thread.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @return string
+	 */
+	function bp_get_thread_recipients_list() {
+		global $thread_template;
+
+		$recipient_links = array();
+
+		foreach( (array) $thread_template->thread->recipients as $recipient ) {
+			if ( (int) $recipient->user_id !== bp_loggedin_user_id() ) {
+				$recipient_link = bp_core_get_userlink( $recipient->user_id );
+
+				if ( empty( $recipient_link ) ) {
+					$recipient_link = __( 'Deleted User', 'buddypress' );
+				}
+
+				$recipient_links[] = $recipient_link;
+			} else {
+				$recipient_links[] = __( 'you', 'buddypress' );
+			}
+		}
+
+		// Concatenate to natural language string.
+		$recipient_links = wp_sprintf_l( '%l', $recipient_links );
+
+		/**
+		 * Filters the HTML links to the profiles of recipients in the current thread.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param string $value Comma-separated list of recipient HTML links for current thread.
+		 */
+		return apply_filters( 'bp_get_the_thread_recipients_list', $recipient_links );
+	}
+
+/**
+ * Echo the ID of the current message in the thread.
+ *
+ * @since 1.9.0
+ */
+function bp_the_thread_message_id() {
+	echo (int) bp_get_the_thread_message_id();
+}
+	/**
+	 * Get the ID of the current message in the thread.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return int
+	 */
+	function bp_get_the_thread_message_id() {
+		global $thread_template;
+
+		$thread_message_id = isset( $thread_template->message->id )
+			? (int) $thread_template->message->id
+			: null;
+
+		/**
+		 * Filters the ID of the current message in the thread.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int $thread_message_id ID of the current message in the thread.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_id', $thread_message_id );
+	}
+
+/**
+ * Output the CSS classes for messages within a single thread.
+ *
+ * @since 2.1.0
+ */
+function bp_the_thread_message_css_class() {
+	echo esc_attr( bp_get_the_thread_message_css_class() );
+}
+	/**
+	 * Generate the CSS classes for messages within a single thread.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_message_css_class() {
+		global $thread_template;
+
+		$classes = array();
+
+		// Zebra-striping.
+		$classes[] = bp_get_the_thread_message_alt_class();
+
+		// ID of the sender.
+		$classes[] = 'sent-by-' . intval( $thread_template->message->sender_id );
+
+		// Whether the sender is the same as the logged-in user.
+		if ( bp_loggedin_user_id() == $thread_template->message->sender_id ) {
+			$classes[] = 'sent-by-me';
+		}
+
+		/**
+		 * Filters the CSS classes for messages within a single thread.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param array $classes Array of classes to add to the HTML class attribute.
+		 */
+		$classes = apply_filters( 'bp_get_the_thread_message_css_class', $classes );
+
+		return implode( ' ', $classes );
+	}
+
+/**
+ * Output the CSS class used for message zebra striping.
+ */
+function bp_the_thread_message_alt_class() {
+	echo esc_attr( bp_get_the_thread_message_alt_class() );
+}
+	/**
+	 * Get the CSS class used for message zebra striping.
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_message_alt_class() {
+		global $thread_template;
+
+		if ( $thread_template->current_message % 2 == 1 ) {
+			$class = 'even alt';
+		} else {
+			$class = 'odd';
+		}
+
+		/**
+		 * Filters the CSS class used for message zebra striping.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $class Class determined to be next for zebra striping effect.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_alt_class', $class );
+	}
+
+/**
+ * Output the ID for message sender within a single thread.
+ *
+ * @since 2.1.0
+ */
+function bp_the_thread_message_sender_id() {
+	echo (int) bp_get_the_thread_message_sender_id();
+}
+	/**
+	 * Return the ID for message sender within a single thread.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return int
+	 */
+	function bp_get_the_thread_message_sender_id() {
+		global $thread_template;
+
+		$user_id = ! empty( $thread_template->message->sender_id )
+			? $thread_template->message->sender_id
+			: 0;
+
+		/**
+		 * Filters the ID for message sender within a single thread.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param int $user_id ID of the message sender.
+		 */
+		return (int) apply_filters( 'bp_get_the_thread_message_sender_id', (int) $user_id );
+	}
+
+/**
+ * Output the avatar for the current message sender.
+ *
+ * @param array|string $args See {@link bp_get_the_thread_message_sender_avatar_thumb()}
+ *                           for a description.
+ */
+function bp_the_thread_message_sender_avatar( $args = '' ) {
+	echo bp_get_the_thread_message_sender_avatar_thumb( $args );
+}
+	/**
+	 * Get the avatar for the current message sender.
+	 *
+	 * @param array|string $args {
+	 *     Array of arguments. See {@link bp_core_fetch_avatar()} for more
+	 *     complete details. All arguments are optional.
+	 *     @type string $type   Avatar type. Default: 'thumb'.
+	 *     @type int    $width  Avatar width. Default: default for your $type.
+	 *     @type int    $height Avatar height. Default: default for your $type.
+	 * }
+	 * @return string <img> tag containing the avatar.
+	 */
+	function bp_get_the_thread_message_sender_avatar_thumb( $args = '' ) {
+		global $thread_template;
+
+		$r = bp_parse_args( $args, array(
+			'type'   => 'thumb',
+			'width'  => false,
+			'height' => false,
+		) );
+
+		/**
+		 * Filters the avatar for the current message sender.
+		 *
+		 * @since 1.1.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $value <img> tag containing the avatar value.
+		 * @param array  $r     Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_sender_avatar_thumb', bp_core_fetch_avatar( array(
+			'item_id' => $thread_template->message->sender_id,
+			'type'    => $r['type'],
+			'width'   => $r['width'],
+			'height'  => $r['height'],
+			'alt'     => bp_core_get_user_displayname( $thread_template->message->sender_id )
+		) ), $r );
+	}
+
+/**
+ * Output a link to the sender of the current message.
+ *
+ * @since 1.1.0
+ */
+function bp_the_thread_message_sender_link() {
+	echo esc_url( bp_get_the_thread_message_sender_link() );
+}
+	/**
+	 * Get a link to the sender of the current message.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_message_sender_link() {
+		global $thread_template;
+
+		/**
+		 * Filters the link to the sender of the current message.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Link to the sender of the current message.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_sender_link', bp_core_get_userlink( $thread_template->message->sender_id, false, true ) );
+	}
+
+/**
+ * Output the display name of the sender of the current message.
+ *
+ * @since 1.1.0
+ */
+function bp_the_thread_message_sender_name() {
+	echo esc_html( bp_get_the_thread_message_sender_name() );
+}
+	/**
+	 * Get the display name of the sender of the current message.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_message_sender_name() {
+		global $thread_template;
+
+		$display_name = bp_core_get_user_displayname( $thread_template->message->sender_id );
+
+		if ( empty( $display_name ) ) {
+			$display_name = __( 'Deleted User', 'buddypress' );
+		}
+
+		/**
+		 * Filters the display name of the sender of the current message.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $display_name Display name of the sender of the current message.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_sender_name', $display_name );
+	}
+
+/**
+ * Output the URL for deleting the current thread.
+ *
+ * @since 1.5.0
+ */
+function bp_the_thread_delete_link() {
+	echo esc_url( bp_get_the_thread_delete_link() );
+}
+	/**
+	 * Get the URL for deleting the current thread.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string URL
+	 */
+	function bp_get_the_thread_delete_link() {
+
+		/**
+		 * Filters the URL for deleting the current thread.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value URL for deleting the current thread.
+		 * @param string $value Text indicating action being executed.
+		 */
+		return apply_filters( 'bp_get_message_thread_delete_link', wp_nonce_url( bp_displayed_user_domain() . bp_get_messages_slug() . '/inbox/delete/' . bp_get_the_thread_id(), 'messages_delete_thread' ) );
+	}
+
+/**
+ * Output the 'Sent x hours ago' string for the current message.
+ *
+ * @since 1.1.0
+ */
+function bp_the_thread_message_time_since() {
+	echo bp_get_the_thread_message_time_since();
+}
+	/**
+	 * Generate the 'Sent x hours ago' string for the current message.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_message_time_since() {
+
+		/**
+		 * Filters the 'Sent x hours ago' string for the current message.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Default text of 'Sent x hours ago'.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_time_since', sprintf( __( 'Sent %s', 'buddypress' ), bp_core_time_since( bp_get_the_thread_message_date_sent() ) ) );
+	}
+
+/**
+ * Output the timestamp for the current message.
+ *
+ * @since 2.1.0
+ */
+function bp_the_thread_message_date_sent() {
+	echo bp_get_the_thread_message_date_sent();
+}
+	/**
+	 * Generate the 'Sent x hours ago' string for the current message.
+	 *
+	 * @since 2.1.0
+	 *
+	 *
+	 * @return int
+	 */
+	function bp_get_the_thread_message_date_sent() {
+		global $thread_template;
+
+		/**
+		 * Filters the date sent value for the current message as a timestamp.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $value Timestamp of the date sent value for the current message.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_date_sent', strtotime( $thread_template->message->date_sent ) );
+	}
+
+/**
+ * Output the content of the current message in the loop.
+ *
+ * @since 1.1.0
+ */
+function bp_the_thread_message_content() {
+	echo bp_get_the_thread_message_content();
+}
+	/**
+	 * Get the content of the current message in the loop.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_thread_message_content() {
+		global $thread_template;
+
+		$content = $thread_template->message->message;
+
+		// If user was deleted, mark content as deleted.
+		if ( false === bp_core_get_core_userdata( bp_get_the_thread_message_sender_id() ) ) {
+			$content = esc_html__( '[deleted]', 'buddypress' );
+		}
+
+		/**
+		 * Filters the content of the current message in the loop.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $message The content of the current message in the loop.
+		 */
+		return apply_filters( 'bp_get_the_thread_message_content', $content );
+	}
+
+/** Embeds *******************************************************************/
+
+/**
+ * Enable oEmbed support for Messages.
+ *
+ * @since 1.5.0
+ *
+ * @see BP_Embed
+ */
+function bp_messages_embed() {
+	add_filter( 'embed_post_id',         'bp_get_the_thread_message_id' );
+	add_filter( 'bp_embed_get_cache',    'bp_embed_message_cache',      10, 3 );
+	add_action( 'bp_embed_update_cache', 'bp_embed_message_save_cache', 10, 3 );
+}
+add_action( 'thread_loop_start', 'bp_messages_embed' );
+
+/**
+ * Fetch a private message item's cached embeds.
+ *
+ * Used during {@link BP_Embed::parse_oembed()} via {@link bp_messages_embed()}.
+ *
+ * @since 2.2.0
+ *
+ * @param string $cache    An empty string passed by BP_Embed::parse_oembed() for
+ *                         functions like this one to filter.
+ * @param int    $id       The ID of the message item.
+ * @param string $cachekey The cache key generated in BP_Embed::parse_oembed().
+ * @return mixed The cached embeds for this message item.
+ */
+function bp_embed_message_cache( $cache, $id, $cachekey ) {
+	return bp_messages_get_meta( $id, $cachekey );
+}
+
+/**
+ * Set a private message item's embed cache.
+ *
+ * Used during {@link BP_Embed::parse_oembed()} via {@link bp_messages_embed()}.
+ *
+ * @since 2.2.0
+ *
+ * @param string $cache    An empty string passed by BP_Embed::parse_oembed() for
+ *                         functions like this one to filter.
+ * @param string $cachekey The cache key generated in BP_Embed::parse_oembed().
+ * @param int    $id       The ID of the message item.
+ */
+function bp_embed_message_save_cache( $cache, $cachekey, $id ) {
+	bp_messages_update_meta( $id, $cachekey, $cache );
+}
Index: /tags/4.0.0-beta1/src/bp-messages/bp-messages-widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/bp-messages-widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/bp-messages-widgets.php	(revision 12265)
@@ -0,0 +1,21 @@
+<?php
+/**
+ * BuddyPress Messages Widgets.
+ *
+ * @package BuddyPress
+ * @subpackage Messages
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register widgets for the Messages component.
+ *
+ * @since 1.9.0
+ */
+function bp_messages_register_widgets() {
+	add_action( 'widgets_init', function() { register_widget( 'BP_Messages_Sitewide_Notices_Widget' ); } );
+}
+add_action( 'bp_register_widgets', 'bp_messages_register_widgets' );
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-box-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-box-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-box-template.php	(revision 12265)
@@ -0,0 +1,346 @@
+<?php
+/**
+ * BuddyPress Messages Box Template Class.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Message Box Template Class
+ */
+class BP_Messages_Box_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @var int
+	 */
+	public $current_thread = -1;
+
+	/**
+	 * The number of threads returned by the paged query.
+	 *
+	 * @var int
+	 */
+	public $current_thread_count = 0;
+
+	/**
+	 * Total number of threads matching the query params.
+	 *
+	 * @var int
+	 */
+	public $total_thread_count = 0;
+
+	/**
+	 * Array of threads located by the query.
+	 *
+	 * @var array
+	 */
+	public $threads = array();
+
+	/**
+	 * The thread object currently being iterated on.
+	 *
+	 * @var object
+	 */
+	public $thread = false;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @var bool
+	 */
+	public $in_the_loop = false;
+
+	/**
+	 * User ID of the current inbox.
+	 *
+	 * @var int
+	 */
+	public $user_id = 0;
+
+	/**
+	 * The current "box" view ('notices', 'sentbox', 'inbox').
+	 *
+	 * @var string
+	 */
+	public $box = 'inbox';
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @var int
+	 */
+	public $pag_page = 1;
+
+	/**
+	 * The number of items being requested per page.
+	 *
+	 * @var int
+	 */
+	public $pag_num = 10;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @var string
+	 */
+	public $pag_links = '';
+
+	/**
+	 * Search terms for limiting the thread query.
+	 *
+	 * @var string
+	 */
+	public $search_terms = '';
+
+	/**
+	 * Constructor method.
+	 *
+	 * @param array $args {
+	 *     Array of arguments. See bp_has_message_threads() for full description.
+	 * }
+	 */
+	public function __construct( $args = array() ) {
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '2.2.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'user_id',
+				1 => 'box',
+				2 => 'per_page',
+				3 => 'max',
+				4 => 'type',
+				5 => 'search_terms',
+				6 => 'page_arg'
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$r = wp_parse_args( $args, array(
+			'page'         => 1,
+			'per_page'     => 10,
+			'page_arg'     => 'mpage',
+			'box'          => 'inbox',
+			'type'         => 'all',
+			'user_id'      => bp_loggedin_user_id(),
+			'max'          => false,
+			'search_terms' => '',
+			'meta_query'   => array(),
+		) );
+
+		$this->pag_arg      = sanitize_key( $r['page_arg'] );
+		$this->pag_page     = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num      = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+		$this->user_id      = $r['user_id'];
+		$this->box          = $r['box'];
+		$this->type         = $r['type'];
+		$this->search_terms = $r['search_terms'];
+
+		if ( 'notices' === $this->box ) {
+			$this->threads = BP_Messages_Notice::get_notices( array(
+				'pag_num'  => $this->pag_num,
+				'pag_page' => $this->pag_page
+			) );
+		} else {
+			$threads = BP_Messages_Thread::get_current_threads_for_user( array(
+				'user_id'      => $this->user_id,
+				'box'          => $this->box,
+				'type'         => $this->type,
+				'limit'        => $this->pag_num,
+				'page'         => $this->pag_page,
+				'search_terms' => $this->search_terms,
+				'meta_query'   => $r['meta_query'],
+			) );
+
+			$this->threads            = $threads['threads'];
+			$this->total_thread_count = $threads['total'];
+		}
+
+		if ( !$this->threads ) {
+			$this->thread_count       = 0;
+			$this->total_thread_count = 0;
+		} else {
+			$total_notice_count = BP_Messages_Notice::get_total_notice_count();
+
+			if ( empty( $r['max'] ) || ( (int) $r['max'] >= (int) $total_notice_count ) ) {
+				if ( 'notices' === $this->box ) {
+					$this->total_thread_count = (int) $total_notice_count;
+				}
+			} else {
+				$this->total_thread_count = (int) $r['max'];
+			}
+
+			if ( ! empty( $r['max'] ) ) {
+				if ( (int) $r['max'] >= count( $this->threads ) ) {
+					$this->thread_count = count( $this->threads );
+				} else {
+					$this->thread_count = (int) $r['max'];
+				}
+			} else {
+				$this->thread_count = count( $this->threads );
+			}
+		}
+
+		if ( (int) $this->total_thread_count && (int) $this->pag_num ) {
+			$pag_args = array(
+				$r['page_arg'] => '%#%',
+			);
+
+			if ( defined( 'DOING_AJAX' ) && true === (bool) DOING_AJAX ) {
+				$base = remove_query_arg( 's', wp_get_referer() );
+			} else {
+				$base = '';
+			}
+
+			$add_args = array();
+
+			if ( ! empty( $this->search_terms ) ) {
+				$add_args['s'] = $this->search_terms;
+			}
+
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $pag_args, $base ),
+				'format'    => '',
+				'total'     => ceil( (int) $this->total_thread_count / (int) $this->pag_num ),
+				'current'   => $this->pag_page,
+				'prev_text' => _x( '&larr;', 'Message pagination previous text', 'buddypress' ),
+				'next_text' => _x( '&rarr;', 'Message pagination next text', 'buddypress' ),
+				'mid_size'  => 1,
+				'add_args'  => $add_args,
+			) );
+		}
+	}
+
+	/**
+	 * Whether there are threads available in the loop.
+	 *
+	 * @see bp_has_message_threads()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	public function has_threads() {
+		if ( $this->thread_count ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set up the next member and iterate index.
+	 *
+	 * @return object The next member to iterate over.
+	 */
+	public function next_thread() {
+		$this->current_thread++;
+		$this->thread = $this->threads[$this->current_thread];
+
+		return $this->thread;
+	}
+
+	/**
+	 * Rewind the threads and reset thread index.
+	 */
+	public function rewind_threads() {
+		$this->current_thread = -1;
+		if ( $this->thread_count > 0 ) {
+			$this->thread = $this->threads[0];
+		}
+	}
+
+	/**
+	 * Whether there are threads left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_message_threads()} as part of the
+	 * while loop that controls iteration inside the threads loop, eg:
+	 *     while ( bp_message_threads() ) { ...
+	 *
+	 * @see bp_message_threads()
+	 *
+	 * @return bool True if there are more threads to show, otherwise false.
+	 */
+	function message_threads() {
+		if ( $this->current_thread + 1 < $this->thread_count ) {
+			return true;
+		} elseif ( $this->current_thread + 1 == $this->thread_count ) {
+
+			/**
+			 * Fires when at the end of threads to iterate over.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'messages_box_loop_end' );
+			// Do some cleaning up after the loop.
+			$this->rewind_threads();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Set up the current thread inside the loop.
+	 *
+	 * Used by {@link bp_message_thread()} to set up the current thread data
+	 * while looping, so that template tags used during that iteration make
+	 * reference to the current thread.
+	 *
+	 * @see bp_message_thread()
+	 */
+	public function the_message_thread() {
+
+		$this->in_the_loop = true;
+		$this->thread      = $this->next_thread();
+
+		if ( ! bp_is_current_action( 'notices' ) ) {
+			$last_message_index     = count( $this->thread->messages ) - 1;
+			$this->thread->messages = array_reverse( (array) $this->thread->messages );
+
+			// Set up the last message data.
+			if ( count($this->thread->messages) > 1 ) {
+				if ( 'inbox' == $this->box ) {
+					foreach ( (array) $this->thread->messages as $key => $message ) {
+						if ( bp_loggedin_user_id() != $message->sender_id ) {
+							$last_message_index = $key;
+							break;
+						}
+					}
+
+				} elseif ( 'sentbox' == $this->box ) {
+					foreach ( (array) $this->thread->messages as $key => $message ) {
+						if ( bp_loggedin_user_id() == $message->sender_id ) {
+							$last_message_index = $key;
+							break;
+						}
+					}
+				}
+			}
+
+			$this->thread->last_message_id      = $this->thread->messages[ $last_message_index ]->id;
+			$this->thread->last_message_date    = $this->thread->messages[ $last_message_index ]->date_sent;
+			$this->thread->last_sender_id       = $this->thread->messages[ $last_message_index ]->sender_id;
+			$this->thread->last_message_subject = $this->thread->messages[ $last_message_index ]->subject;
+			$this->thread->last_message_content = $this->thread->messages[ $last_message_index ]->message;
+		}
+
+		// Loop has just started.
+		if ( 0 == $this->current_thread ) {
+
+			/**
+			 * Fires if at the start of the message thread loop.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'messages_box_loop_start' );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-component.php	(revision 12265)
@@ -0,0 +1,436 @@
+<?php
+/**
+ * BuddyPress Messages Loader.
+ *
+ * A private messages component, for users to send messages to each other.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Implementation of BP_Component for the Messages component.
+ *
+ * @since 1.5.0
+ */
+class BP_Messages_Component extends BP_Component {
+
+	/**
+	 * If this is true, the Message autocomplete will return friends only, unless
+	 * this is set to false, in which any matching users will be returned.
+	 *
+	 * @since 1.5.0
+	 * @var bool
+	 */
+	public $autocomplete_all;
+
+	/**
+	 * Start the messages component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'messages',
+			__( 'Private Messages', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 50,
+				'features'                 => array( 'star' )
+			)
+		);
+	}
+
+	/**
+	 * Include files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $includes See {BP_Component::includes()} for details.
+	 */
+	public function includes( $includes = array() ) {
+
+		// Files to include.
+		$includes = array(
+			'cssjs',
+			'cache',
+			'filters',
+			'template',
+			'functions',
+			'widgets',
+		);
+
+		// Conditional includes.
+		if ( bp_is_active( 'notifications' ) ) {
+			$includes[] = 'notifications';
+		}
+		if ( bp_is_active( $this->id, 'star' ) ) {
+			$includes[] = 'star';
+		}
+		if ( is_admin() ) {
+			$includes[] = 'admin';
+		}
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		if ( bp_is_messages_component() ) {
+			// Authenticated actions.
+			if ( is_user_logged_in() &&
+				in_array( bp_current_action(), array( 'compose', 'notices', 'view' ), true )
+			) {
+				require $this->path . 'bp-messages/actions/' . bp_current_action() . '.php';
+			}
+
+			// Authenticated action variables.
+			if ( is_user_logged_in() && bp_action_variable( 0 ) &&
+				in_array( bp_action_variable( 0 ), array( 'delete', 'read', 'unread', 'bulk-manage', 'bulk-delete' ), true )
+			) {
+				require $this->path . 'bp-messages/actions/' . bp_action_variable( 0 ) . '.php';
+			}
+
+			// Authenticated actions - Star.
+			if ( is_user_logged_in() && bp_is_active( $this->id, 'star' ) ) {
+				// Single action.
+				if ( in_array( bp_current_action(), array( 'star', 'unstar' ), true ) ) {
+					require $this->path . 'bp-messages/actions/star.php';
+				}
+
+				// Bulk-manage.
+				if ( bp_is_action_variable( 'bulk-manage' ) ) {
+					require $this->path . 'bp-messages/actions/bulk-manage-star.php';
+				}
+			}
+
+			// Screens - User profile integration.
+			if ( bp_is_user() ) {
+				require $this->path . 'bp-messages/screens/inbox.php';
+
+				/*
+				 * Nav items.
+				 *
+				 * 'view' is not a registered nav item, but we add a screen handler manually.
+				 */
+				if ( bp_is_user_messages() && in_array( bp_current_action(), array( 'sentbox', 'compose', 'notices', 'view' ), true ) ) {
+					require $this->path . 'bp-messages/screens/' . bp_current_action() . '.php';
+				}
+
+				// Nav item - Starred.
+				if ( bp_is_active( $this->id, 'star' ) && bp_is_current_action( bp_get_messages_starred_slug() ) ) {
+					require $this->path . 'bp-messages/screens/starred.php';
+				}
+			}
+		}
+	}
+
+	/**
+	 * Set up globals for the Messages component.
+	 *
+	 * The BP_MESSAGES_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args Not used.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		// Define a slug, if necessary.
+		if ( ! defined( 'BP_MESSAGES_SLUG' ) ) {
+			define( 'BP_MESSAGES_SLUG', $this->id );
+		}
+
+		// Global tables for messaging component.
+		$global_tables = array(
+			'table_name_notices'    => $bp->table_prefix . 'bp_messages_notices',
+			'table_name_messages'   => $bp->table_prefix . 'bp_messages_messages',
+			'table_name_recipients' => $bp->table_prefix . 'bp_messages_recipients',
+			'table_name_meta'       => $bp->table_prefix . 'bp_messages_meta',
+		);
+
+		// Metadata tables for messaging component.
+		$meta_tables = array(
+			'message' => $bp->table_prefix . 'bp_messages_meta',
+		);
+
+		$this->autocomplete_all = defined( 'BP_MESSAGES_AUTOCOMPLETE_ALL' );
+
+		// All globals for messaging component.
+		// Note that global_tables is included in this array.
+		parent::setup_globals( array(
+			'slug'                  => BP_MESSAGES_SLUG,
+			'has_directory'         => false,
+			'notification_callback' => 'messages_format_notifications',
+			'search_string'         => __( 'Search Messages...', 'buddypress' ),
+			'global_tables'         => $global_tables,
+			'meta_tables'           => $meta_tables
+		) );
+	}
+
+	/**
+	 * Set up navigation for user pages.
+	 *
+	 * @param array $main_nav See {BP_Component::setup_nav()} for details.
+	 * @param array $sub_nav  See {BP_Component::setup_nav()} for details.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$access        = bp_core_can_edit_settings();
+		$slug          = bp_get_messages_slug();
+		$messages_link = trailingslashit( $user_domain . $slug );
+
+		// Only grab count if we're on a user page and current user has access.
+		if ( bp_is_user() && bp_user_has_access() ) {
+			$count    = bp_get_total_unread_messages_count( bp_displayed_user_id() );
+			$class    = ( 0 === $count ) ? 'no-count' : 'count';
+			$nav_name = sprintf(
+				/* translators: %s: Unread message count for the current user */
+				__( 'Messages %s', 'buddypress' ),
+				sprintf(
+					'<span class="%s">%s</span>',
+					esc_attr( $class ),
+					bp_core_number_format( $count )
+				)
+			);
+		} else {
+			$nav_name = __( 'Messages', 'buddypress' );
+		}
+
+		// Add 'Messages' to the main navigation.
+		$main_nav = array(
+			'name'                    => $nav_name,
+			'slug'                    => $slug,
+			'position'                => 50,
+			'show_for_displayed_user' => $access,
+			'screen_function'         => 'messages_screen_inbox',
+			'default_subnav_slug'     => 'inbox',
+			'item_css_id'             => $this->id
+		);
+
+		// Add the subnav items to the profile.
+		$sub_nav[] = array(
+			'name'            => __( 'Inbox', 'buddypress' ),
+			'slug'            => 'inbox',
+			'parent_url'      => $messages_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'messages_screen_inbox',
+			'position'        => 10,
+			'user_has_access' => $access
+		);
+
+		if ( bp_is_active( $this->id, 'star' ) ) {
+			$sub_nav[] = array(
+				'name'            => __( 'Starred', 'buddypress' ),
+				'slug'            => bp_get_messages_starred_slug(),
+				'parent_url'      => $messages_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_messages_star_screen',
+				'position'        => 11,
+				'user_has_access' => $access
+			);
+		}
+
+		$sub_nav[] = array(
+			'name'            => __( 'Sent', 'buddypress' ),
+			'slug'            => 'sentbox',
+			'parent_url'      => $messages_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'messages_screen_sentbox',
+			'position'        => 20,
+			'user_has_access' => $access
+		);
+
+		// Show certain screens only if the current user is the displayed user.
+		if ( bp_is_my_profile() ) {
+
+			// Show "Compose" on the logged-in user's profile only.
+			$sub_nav[] = array(
+				'name'            => __( 'Compose', 'buddypress' ),
+				'slug'            => 'compose',
+				'parent_url'      => $messages_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'messages_screen_compose',
+				'position'        => 30,
+				'user_has_access' => $access
+			);
+
+			/*
+			 * Show "Notices" on the logged-in user's profile only
+			 * and then only if the user can create notices.
+			 */
+			if ( bp_current_user_can( 'bp_moderate' ) ) {
+				$sub_nav[] = array(
+					'name'            => __( 'Notices', 'buddypress' ),
+					'slug'            => 'notices',
+					'parent_url'      => $messages_link,
+					'parent_slug'     => $slug,
+					'screen_function' => 'messages_screen_notices',
+					'position'        => 90,
+					'user_has_access' => true
+				);
+			}
+		}
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up the Toolbar.
+	 *
+	 * @param array $wp_admin_nav See {BP_Component::setup_admin_bar()} for details.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$messages_link = trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() );
+
+			// Unread message count.
+			$count = messages_get_unread_count( bp_loggedin_user_id() );
+			if ( !empty( $count ) ) {
+				$title = sprintf(
+					/* translators: %s: Unread message count for the current user */
+					__( 'Messages %s', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+				$inbox = sprintf(
+					/* translators: %s: Unread message count for the current user */
+					__( 'Inbox %s', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+			} else {
+				$title = __( 'Messages', 'buddypress' );
+				$inbox = __( 'Inbox',    'buddypress' );
+			}
+
+			// Add main Messages menu.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => $title,
+				'href'   => $messages_link
+			);
+
+			// Inbox.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-inbox',
+				'title'    => $inbox,
+				'href'     => $messages_link,
+				'position' => 10
+			);
+
+			// Starred.
+			if ( bp_is_active( $this->id, 'star' ) ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-starred',
+					'title'    => __( 'Starred', 'buddypress' ),
+					'href'     => trailingslashit( $messages_link . bp_get_messages_starred_slug() ),
+					'position' => 11
+				);
+			}
+
+			// Sent Messages.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-sentbox',
+				'title'    => __( 'Sent', 'buddypress' ),
+				'href'     => trailingslashit( $messages_link . 'sentbox' ),
+				'position' => 20
+			);
+
+			// Compose Message.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-compose',
+				'title'    => __( 'Compose', 'buddypress' ),
+				'href'     => trailingslashit( $messages_link . 'compose' ),
+				'position' => 30
+			);
+
+			// Site Wide Notices.
+			if ( bp_current_user_can( 'bp_moderate' ) ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-notices',
+					'title'    => __( 'Site Notices', 'buddypress' ),
+					'href'     => trailingslashit( $messages_link . 'notices' ),
+					'position' => 90
+				);
+			}
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 */
+	public function setup_title() {
+
+		if ( bp_is_messages_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() ) {
+				$bp->bp_options_title = __( 'My Messages', 'buddypress' );
+			} else {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title = bp_get_displayed_user_fullname();
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_messages',
+			'bp_messages_threads',
+			'bp_messages_unread_count',
+			'message_meta'
+		) );
+
+		parent::setup_cache_groups();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-message.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-message.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-message.php	(revision 12265)
@@ -0,0 +1,281 @@
+<?php
+/**
+ * BuddyPress Messages Classes.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Single message class.
+ */
+class BP_Messages_Message {
+	/**
+	 * ID of the message.
+	 *
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * ID of the message thread.
+	 *
+	 * @var int
+	 */
+	public $thread_id;
+
+	/**
+	 * ID of the sender.
+	 *
+	 * @var int
+	 */
+	public $sender_id;
+
+	/**
+	 * Subject line of the message.
+	 *
+	 * @var string
+	 */
+	public $subject;
+
+	/**
+	 * Content of the message.
+	 *
+	 * @var string
+	 */
+	public $message;
+
+	/**
+	 * Date the message was sent.
+	 *
+	 * @var string
+	 */
+	public $date_sent;
+
+	/**
+	 * Message recipients.
+	 *
+	 * @var bool|array
+	 */
+	public $recipients = false;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param int|null $id Optional. ID of the message.
+	 */
+	public function __construct( $id = null ) {
+		$this->date_sent = bp_core_current_time();
+		$this->sender_id = bp_loggedin_user_id();
+
+		if ( ! empty( $id ) ) {
+			$this->populate( $id );
+		}
+	}
+
+	/**
+	 * Set up data related to a specific message object.
+	 *
+	 * @param int $id ID of the message.
+	 */
+	public function populate( $id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( $message = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_messages} WHERE id = %d", $id ) ) ) {
+			$this->id        = (int) $message->id;
+			$this->thread_id = (int) $message->thread_id;
+			$this->sender_id = (int) $message->sender_id;
+			$this->subject   = $message->subject;
+			$this->message   = $message->message;
+			$this->date_sent = $message->date_sent;
+		}
+	}
+
+	/**
+	 * Send a message.
+	 *
+	 * @return int|bool ID of the newly created message on success, false on failure.
+	 */
+	public function send() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->sender_id = apply_filters( 'messages_message_sender_id_before_save', $this->sender_id, $this->id );
+		$this->thread_id = apply_filters( 'messages_message_thread_id_before_save', $this->thread_id, $this->id );
+		$this->subject   = apply_filters( 'messages_message_subject_before_save', $this->subject, $this->id );
+		$this->message   = apply_filters( 'messages_message_content_before_save', $this->message, $this->id );
+		$this->date_sent = apply_filters( 'messages_message_date_sent_before_save', $this->date_sent, $this->id );
+
+		/**
+		 * Fires before the current message item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Messages_Message $this Current instance of the message item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'messages_message_before_save', array( &$this ) );
+
+		// Make sure we have at least one recipient before sending.
+		if ( empty( $this->recipients ) ) {
+			return false;
+		}
+
+		$new_thread = false;
+
+		// If we have no thread_id then this is the first message of a new thread.
+		if ( empty( $this->thread_id ) ) {
+			$this->thread_id = (int) $wpdb->get_var( "SELECT MAX(thread_id) FROM {$bp->messages->table_name_messages}" ) + 1;
+			$new_thread      = true;
+		}
+
+		// First insert the message into the messages table.
+		if ( ! $wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_messages} ( thread_id, sender_id, subject, message, date_sent ) VALUES ( %d, %d, %s, %s, %s )", $this->thread_id, $this->sender_id, $this->subject, $this->message, $this->date_sent ) ) ) {
+			return false;
+		}
+
+		$this->id = $wpdb->insert_id;
+
+		$recipient_ids = array();
+
+		if ( $new_thread ) {
+			// Add an recipient entry for all recipients.
+			foreach ( (array) $this->recipients as $recipient ) {
+				$wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_recipients} ( user_id, thread_id, unread_count ) VALUES ( %d, %d, 1 )", $recipient->user_id, $this->thread_id ) );
+				$recipient_ids[] = $recipient->user_id;
+			}
+
+			// Add a sender recipient entry if the sender is not in the list of recipients.
+			if ( ! in_array( $this->sender_id, $recipient_ids ) ) {
+				$wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_recipients} ( user_id, thread_id, sender_only ) VALUES ( %d, %d, 1 )", $this->sender_id, $this->thread_id ) );
+			}
+		} else {
+			// Update the unread count for all recipients.
+			$wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET unread_count = unread_count + 1, sender_only = 0, is_deleted = 0 WHERE thread_id = %d AND user_id != %d", $this->thread_id, $this->sender_id ) );
+		}
+
+		messages_remove_callback_values();
+
+		/**
+		 * Fires after the current message item has been saved.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Messages_Message $this Current instance of the message item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'messages_message_after_save', array( &$this ) );
+
+		return $this->id;
+	}
+
+	/**
+	 * Get a list of recipients for a message.
+	 *
+	 * @return object $value List of recipients for a message.
+	 */
+	public function get_recipients() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_results( $wpdb->prepare( "SELECT user_id FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d", $this->thread_id ) );
+	}
+
+	/** Static Functions **************************************************/
+
+	/**
+	 * Get list of recipient IDs from their usernames.
+	 *
+	 * @param array $recipient_usernames Usernames of recipients.
+	 *
+	 * @return bool|array $recipient_ids Array of Recepient IDs.
+	 */
+	public static function get_recipient_ids( $recipient_usernames ) {
+		$recipient_ids = false;
+
+		if ( ! $recipient_usernames ) {
+			return $recipient_ids;
+		}
+
+		if ( is_array( $recipient_usernames ) ) {
+			$rec_un_count = count( $recipient_usernames );
+
+			for ( $i = 0, $count = $rec_un_count; $i < $count; ++ $i ) {
+				if ( $rid = bp_core_get_userid( trim( $recipient_usernames[ $i ] ) ) ) {
+					$recipient_ids[] = $rid;
+				}
+			}
+		}
+
+		/**
+		 * Filters the array of recipients IDs.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param array $recipient_ids Array of recipients IDs that were retrieved based on submitted usernames.
+		 * @param array $recipient_usernames Array of recipients usernames that were submitted by a user.
+		 */
+		return apply_filters( 'messages_message_get_recipient_ids', $recipient_ids, $recipient_usernames );
+	}
+
+	/**
+	 * Get the ID of the message last sent by the logged-in user for a given thread.
+	 *
+	 * @param int $thread_id ID of the thread.
+	 *
+	 * @return int|null ID of the message if found, otherwise null.
+	 */
+	public static function get_last_sent_for_user( $thread_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$query = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_messages} WHERE sender_id = %d AND thread_id = %d ORDER BY date_sent DESC LIMIT 1", bp_loggedin_user_id(), $thread_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+
+	/**
+	 * Check whether a user is the sender of a message.
+	 *
+	 * @param int $user_id ID of the user.
+	 * @param int $message_id ID of the message.
+	 *
+	 * @return int|null Returns the ID of the message if the user is the
+	 *                  sender, otherwise null.
+	 */
+	public static function is_user_sender( $user_id, $message_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$query = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_messages} WHERE sender_id = %d AND id = %d", $user_id, $message_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+
+	/**
+	 * Get the ID of the sender of a message.
+	 *
+	 * @param int $message_id ID of the message.
+	 *
+	 * @return int|null The ID of the sender if found, otherwise null.
+	 */
+	public static function get_message_sender( $message_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$query = $wpdb->get_var( $wpdb->prepare( "SELECT sender_id FROM {$bp->messages->table_name_messages} WHERE id = %d", $message_id ) );
+
+		return is_numeric( $query ) ? (int) $query : $query;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notice.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notice.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notice.php	(revision 12265)
@@ -0,0 +1,310 @@
+<?php
+/**
+ * BuddyPress Messages Classes.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Notices class.
+ *
+ * Use this class to create, activate, deactivate or delete notices.
+ *
+ * @since 1.0.0
+ */
+class BP_Messages_Notice {
+	/**
+	 * The notice ID.
+	 *
+	 * @var int
+	 */
+	public $id = null;
+
+	/**
+	 * The subject line for the notice.
+	 *
+	 * @var string
+	 */
+	public $subject;
+
+	/**
+	 * The content of the notice.
+	 *
+	 * @var string
+	 */
+	public $message;
+
+	/**
+	 * The date the notice was created.
+	 *
+	 * @var string
+	 */
+	public $date_sent;
+
+	/**
+	 * Whether the notice is active or not.
+	 *
+	 * @var int
+	 */
+	public $is_active;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int|null $id Optional. The ID of the current notice.
+	 */
+	public function __construct( $id = null ) {
+		if ( $id ) {
+			$this->id = (int) $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Populate method.
+	 *
+	 * Runs during constructor.
+	 *
+	 * @since 1.0.0
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$notice = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_notices} WHERE id = %d", $this->id ) );
+
+		if ( $notice ) {
+			$this->subject   = $notice->subject;
+			$this->message   = $notice->message;
+			$this->date_sent = $notice->date_sent;
+			$this->is_active = (int) $notice->is_active;
+		}
+	}
+
+	/**
+	 * Saves a notice.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->subject = apply_filters( 'messages_notice_subject_before_save', $this->subject, $this->id );
+		$this->message = apply_filters( 'messages_notice_message_before_save', $this->message, $this->id );
+
+		/**
+		 * Fires before the current message notice item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Messages_Notice $this Current instance of the message notice item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'messages_notice_before_save', array( &$this ) );
+
+		if ( empty( $this->id ) ) {
+			$sql = $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_notices} (subject, message, date_sent, is_active) VALUES (%s, %s, %s, %d)", $this->subject, $this->message, $this->date_sent, $this->is_active );
+		} else {
+			$sql = $wpdb->prepare( "UPDATE {$bp->messages->table_name_notices} SET subject = %s, message = %s, is_active = %d WHERE id = %d", $this->subject, $this->message, $this->is_active, $this->id );
+		}
+
+		if ( ! $wpdb->query( $sql ) ) {
+			return false;
+		}
+
+		if ( ! $id = $this->id ) {
+			$id = $wpdb->insert_id;
+		}
+
+		// Now deactivate all notices apart from the new one.
+		$wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_notices} SET is_active = 0 WHERE id != %d", $id ) );
+
+		bp_update_user_last_activity( bp_loggedin_user_id(), bp_core_current_time() );
+
+		/**
+		 * Fires after the current message notice item has been saved.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Messages_Notice $this Current instance of the message item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'messages_notice_after_save', array( &$this ) );
+
+		return true;
+	}
+
+	/**
+	 * Activates a notice.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function activate() {
+		$this->is_active = 1;
+		return (bool) $this->save();
+	}
+
+	/**
+	 * Deactivates a notice.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function deactivate() {
+		$this->is_active = 0;
+		return (bool) $this->save();
+	}
+
+	/**
+	 * Deletes a notice.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function delete() {
+		global $wpdb;
+
+		/**
+		 * Fires before the current message item has been deleted.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_Messages_Notice $this Current instance of the message notice item being deleted.
+		 */
+		do_action( 'messages_notice_before_delete', $this );
+
+		$bp  = buddypress();
+		$sql = $wpdb->prepare( "DELETE FROM {$bp->messages->table_name_notices} WHERE id = %d", $this->id );
+
+		if ( ! $wpdb->query( $sql ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires after the current message item has been deleted.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param BP_Messages_Notice $this Current instance of the message notice item being deleted.
+		 */
+		do_action( 'messages_notice_after_delete', $this );
+
+		return true;
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Pulls up a list of notices.
+	 *
+	 * To get all notices, pass a value of -1 to pag_num.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $args {
+	 *     Array of parameters.
+	 *     @type int $pag_num  Number of notices per page. Defaults to 20.
+	 *     @type int $pag_page The page number.  Defaults to 1.
+	 * }
+	 * @return object List of notices to display.
+	 */
+	public static function get_notices( $args = array() ) {
+		global $wpdb;
+
+		$r = wp_parse_args( $args, array(
+			'pag_num'  => 20, // Number of notices per page.
+			'pag_page' => 1   // Page number.
+		) );
+
+		$limit_sql = '';
+		if ( (int) $r['pag_num'] >= 0 ) {
+			$limit_sql = $wpdb->prepare( "LIMIT %d, %d", (int) ( ( $r['pag_page'] - 1 ) * $r['pag_num'] ), (int) $r['pag_num'] );
+		}
+
+		$bp = buddypress();
+
+		$notices = $wpdb->get_results( "SELECT * FROM {$bp->messages->table_name_notices} ORDER BY date_sent DESC {$limit_sql}" );
+
+		// Integer casting.
+		foreach ( (array) $notices as $key => $data ) {
+			$notices[ $key ]->id        = (int) $notices[ $key ]->id;
+			$notices[ $key ]->is_active = (int) $notices[ $key ]->is_active;
+		}
+
+		/**
+		 * Filters the array of notices, sorted by date and paginated.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param array $r Array of parameters.
+		 */
+		return apply_filters( 'messages_notice_get_notices', $notices, $r );
+	}
+
+	/**
+	 * Returns the total number of recorded notices.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return int
+	 */
+	public static function get_total_notice_count() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$notice_count = $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->messages->table_name_notices}" );
+
+		/**
+		 * Filters the total number of notices.
+		 *
+		 * @since 2.8.0
+		 */
+		return (int) apply_filters( 'messages_notice_get_total_notice_count', $notice_count );
+	}
+
+	/**
+	 * Returns the active notice that should be displayed on the front end.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return object The BP_Messages_Notice object.
+	 */
+	public static function get_active() {
+		$notice = wp_cache_get( 'active_notice', 'bp_messages' );
+
+		if ( false === $notice ) {
+			global $wpdb;
+
+			$bp = buddypress();
+
+			$notice_id = $wpdb->get_var( "SELECT id FROM {$bp->messages->table_name_notices} WHERE is_active = 1" );
+			$notice    = new BP_Messages_Notice( $notice_id );
+
+			wp_cache_set( 'active_notice', $notice, 'bp_messages' );
+		}
+
+		/**
+		 * Gives ability to filter the active notice that should be displayed on the front end.
+		 *
+		 * @since 2.8.0
+		 */
+		return apply_filters( 'messages_notice_get_active', $notice );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notices-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notices-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notices-admin.php	(revision 12265)
@@ -0,0 +1,252 @@
+<?php
+/**
+ * BuddyPress messages component Site-wide Notices admin screen.
+ *
+ *
+ * @package BuddyPress
+ * @subpackage Messages
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+class BP_Messages_Notices_Admin {
+
+	/**
+	 * The ID returned by `add_users_page()`.
+	 *
+	 * @since 3.0.0
+	 * @var string
+	 */
+	public $screen_id = '';
+
+	/**
+	 * The URL of the admin screen.
+	 *
+	 * @since 3.0.0
+	 * @var string
+	 */
+	public $url = '';
+
+	/**
+	 * The current instance of the BP_Messages_Notices_List_Table class.
+	 *
+	 * @since 3.0.0
+	 * @var object
+	 */
+	public $list_table = '';
+
+
+	/**
+     * Create a new instance or access the current instance of this class.
+     *
+     * @since 3.0.0
+     */
+	public static function register_notices_admin() {
+
+		if ( ! is_admin() || ! bp_is_active( 'messages' ) || ! bp_current_user_can( 'bp_moderate' ) ) {
+			return;
+		}
+
+		$bp = buddypress();
+
+		if ( empty( $bp->messages->admin ) ) {
+			$bp->messages->admin = new self;
+		}
+
+		return $bp->messages->admin;
+	}
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->setup_actions();
+	}
+
+	/**
+	 * Populate the classs variables.
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->url = add_query_arg( array( 'page' => 'bp-notices' ), bp_get_admin_url( 'users.php' ) );
+	}
+
+	/**
+	 * Add action hooks.
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		add_action( bp_core_admin_hook(), array( $this, 'admin_menu' ) );
+	}
+
+	/**
+	 * Add the 'Site Notices' admin menu item.
+	 *
+	 * @since 3.0.0
+	 */
+	public function admin_menu() {
+		// Bail if current user cannot moderate community.
+		if ( ! bp_current_user_can( 'bp_moderate' ) || ! bp_is_active( 'messages' ) ) {
+			return false;
+		}
+
+		$this->screen_id = add_users_page(
+			_x( 'Site Notices', 'Notices admin page title', 'buddypress' ),
+			_x( 'Site Notices', 'Admin Users menu', 'buddypress' ),
+			'manage_options',
+			'bp-notices',
+			array( $this, 'admin_index' )
+		);
+
+		add_action( 'load-' . $this->screen_id, array( $this, 'admin_load' ) );
+	}
+
+	/**
+	 * Catch save/update requests or load the screen.
+	 *
+	 * @since 3.0.0
+	 */
+	public function admin_load() {
+		$redirect_to = false;
+
+		// Catch new notice saves.
+		if ( ! empty( $_POST['bp_notice']['send'] ) ) {
+
+			check_admin_referer( 'new-notice', 'ns-nonce' );
+
+			$notice = wp_parse_args( $_POST['bp_notice'], array(
+				'subject' => '',
+				'content' => ''
+			) );
+
+			if ( messages_send_notice( $notice['subject'], $notice['content'] ) ) {
+				$redirect_to = add_query_arg( 'success', 'create', $this->url );
+
+			// Notice could not be sent.
+			} else {
+				$redirect_to = add_query_arg( 'error', 'create', $this->url );
+			}
+		}
+
+		// Catch activation/deactivation/delete requests
+		if ( ! empty( $_GET['notice_id'] ) && ! empty( $_GET['notice_action'] ) ) {
+			$notice_id = absint( $_GET['notice_id'] );
+
+			check_admin_referer( 'messages-' . $_GET['notice_action'] . '-notice-' . $notice_id );
+
+			$success = false;
+			switch ( $_GET['notice_action'] ) {
+				case 'activate':
+					$notice = new BP_Messages_Notice( $notice_id );
+					$success = $notice->activate();
+					break;
+				case 'deactivate':
+					$notice = new BP_Messages_Notice( $notice_id );
+					$success = $notice->deactivate();
+					break;
+				case 'delete':
+					$notice = new BP_Messages_Notice( $notice_id );
+					$success = $notice->delete();
+					break;
+			}
+			if ( $success ) {
+				$redirect_to = add_query_arg( 'success', 'update', $this->url );
+
+			// Notice could not be updated.
+			} else {
+				$redirect_to = add_query_arg( 'error', 'update', $this->url );
+			}
+
+		}
+
+		if ( $redirect_to ) {
+			wp_safe_redirect( $redirect_to );
+			exit();
+		}
+
+		$this->list_table = new BP_Messages_Notices_List_Table( array( 'screen' => get_current_screen()->id ) );
+	}
+
+	/**
+	 * Generate content for the bp-notices admin screen.
+	 *
+	 * @since 3.0.0
+	 */
+	public function admin_index() {
+		$this->list_table->prepare_items();
+		?>
+		<div class="wrap">
+			<?php if ( version_compare( $GLOBALS['wp_version'], '4.8', '>=' ) ) : ?>
+
+				<h1 class="wp-heading-inline"><?php echo esc_html_x( 'Site Notices', 'Notices admin page title', 'buddypress' ); ?></h1>
+				<hr class="wp-header-end">
+
+			<?php else : ?>
+
+				<h1><?php echo esc_html_x( 'Site Notices', 'Notices admin page title', 'buddypress' ); ?></h1>
+
+			<?php endif; ?>
+
+			<p class="bp-notice-about"><?php esc_html_e( 'Manage notices shown at front end of your site to all logged-in users.', 'buddypress' ); ?></p>
+
+			<div class="bp-new-notice-panel">
+
+				<h2 class="bp-new-notice"><?php esc_html_e( 'Add New Notice', 'buddypress' ); ?></h2>
+
+				<form action="<?php echo esc_url( wp_nonce_url( $this->url, 'new-notice', 'ns-nonce' ) ); ?>" method="post">
+
+					<div>
+						<label for="bp_notice_subject"><?php esc_html_e( 'Subject', 'buddypress' ); ?></label>
+						<input type="text" class="bp-panel-input" id="bp_notice_subject" name="bp_notice[subject]"/>
+
+						<label for="bp_notice_content"><?php esc_html_e( 'Content', 'buddypress' ); ?></label>
+						<textarea class="bp-panel-textarea" id="bp_notice_content" name="bp_notice[content]"></textarea>
+					</div>
+
+					<input type="submit" value="<?php esc_attr_e( 'Publish Notice', 'buddypress' ); ?>" name="bp_notice[send]" class="button button-primary save alignleft">
+
+				</form>
+
+			</div>
+
+			<?php if ( isset( $_GET['success'] ) || isset( $_GET['error'] ) ) : ?>
+
+				<div id="message" class="<?php echo isset( $_GET['success'] ) ? 'updated' : 'error'; ?>">
+
+					<p>
+						<?php
+						if ( isset( $_GET['error'] ) ) {
+							if ( 'create' === $_GET['error'] ) {
+								esc_html_e( 'Notice was not created. Please try again.', 'buddypress' );
+							} else {
+								esc_html_e( 'Notice was not updated. Please try again.', 'buddypress' );
+							}
+						 } else {
+							if ( 'create' === $_GET['success'] ) {
+								esc_html_e( 'Notice successfully created.', 'buddypress' );
+							} else {
+								esc_html_e( 'Notice successfully updated.', 'buddypress' );
+							}
+						}
+						?>
+					</p>
+
+				</div>
+
+			<?php endif; ?>
+
+			<h2 class="bp-notices-list"><?php esc_html_e( 'Notices List', 'buddypress' ); ?></h2>
+
+			<?php $this->list_table->display(); ?>
+
+		</div>
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notices-list-table.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notices-list-table.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-notices-list-table.php	(revision 12265)
@@ -0,0 +1,166 @@
+<?php
+/**
+ * BuddyPress messages admin site-wide notices list table class.
+ *
+ * @package BuddyPress
+ * @subpackage Messages
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+// Include WP's list table class.
+if ( ! class_exists( 'WP_List_Table' ) ) {
+	require( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
+}
+
+class BP_Messages_Notices_List_Table extends WP_List_Table {
+	
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct( $args = array() ) {
+		parent::__construct(
+			array(
+				'plural'   => 'notices',
+				'singular' => 'notice',
+				'ajax'     => true,
+				'screen'   => isset( $args['screen'] ) ? $args['screen'] : null,
+			)
+		);
+	}
+
+	/**
+	 * Checks the current user's permissions
+	 *
+	 * @since 3.0.0
+	 */
+	public function ajax_user_can() {
+		return bp_current_user_can( 'bp_moderate' );
+	}
+
+	/**
+	 * Set up items for display in the list table.
+	 *
+	 * Handles filtering of data, sorting, pagination, and any other data
+	 * manipulation required prior to rendering.
+	 *
+	 * @since 3.0.0
+	 */
+	public function prepare_items() {
+		$page     = $this->get_pagenum();
+		$per_page = $this->get_items_per_page( 'bp_notices_per_page' );
+
+		$this->items = BP_Messages_Notice::get_notices( array(
+			'pag_num'  => $per_page,
+			'pag_page' => $page
+		) );
+
+		$this->set_pagination_args( array(
+			'total_items' => BP_Messages_Notice::get_total_notice_count(),
+			'per_page' => $per_page,
+		) );
+	}
+
+	/**
+	 * Get a list of columns. The format is:
+	 * 'internal-name' => 'Title'
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array
+	 */
+	public function get_columns() {
+		return apply_filters( 'bp_notices_list_table_get_columns', array(
+			'subject'   => _x( 'Subject', 'Admin Notices column header', 'buddypress' ),
+			'message'   => _x( 'Content', 'Admin Notices column header', 'buddypress' ),
+			'date_sent' => _x( 'Created', 'Admin Notices column header', 'buddypress' ),
+		) );
+	}
+
+	/**
+	 * Generates content for a single row of the table
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param object $item The current item
+	 */
+	public function single_row( $item ) {
+		$class = '';
+
+		if ( ! empty( $item->is_active ) ) {
+			$class = ' class="notice-active"';
+		}
+
+		echo "<tr{$class}>";
+		$this->single_row_columns( $item );
+		echo '</tr>';
+	}
+
+	/**
+	 * Generates content for the "subject" column.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param object $item The current item
+	 */
+	public function column_subject( $item ) {
+		$actions = array(
+			'activate_deactivate' => sprintf( '<a href="%s" data-bp-notice-id="%d" data-bp-action="activate">%s</a>', 
+				esc_url( wp_nonce_url( add_query_arg( array(
+					'page'          => 'bp-notices',
+					'notice_action' => 'activate',
+					'notice_id'     => $item->id
+				), bp_get_admin_url( 'users.php' ) ), 'messages-activate-notice-' . $item->id ) ),
+				(int) $item->id,
+				esc_html__( 'Activate Notice', 'buddypress' ) ),
+			'delete' => sprintf( '<a href="%s" data-bp-notice-id="%d" data-bp-action="delete">%s</a>', 
+				esc_url( wp_nonce_url( add_query_arg( array(
+					'page'          => 'bp-notices',
+					'notice_action' => 'delete',
+					'notice_id'     => $item->id
+				), bp_get_admin_url( 'users.php' ) ), 'messages-delete-notice-' . $item->id ) ),
+				(int) $item->id,
+				esc_html__( 'Delete Notice', 'buddypress' ) )
+		);
+
+		if ( ! empty( $item->is_active ) ) {
+			$item->subject = sprintf( _x( 'Active: %s', 'Tag prepended to active site-wide notice titles on WP Admin notices list table', 'buddypress' ), $item->subject );
+			$actions['activate_deactivate'] = sprintf( '<a href="%s" data-bp-notice-id="%d" data-bp-action="deactivate">%s</a>', 
+				esc_url( wp_nonce_url( add_query_arg( array(
+					'page'          => 'bp-notices',
+					'notice_action' => 'deactivate',
+					'notice_id'     => $item->id
+				), bp_get_admin_url( 'users.php' ) ), 'messages-deactivate-notice-' . $item->id ) ),
+				(int) $item->id,
+				esc_html__( 'Deactivate Notice', 'buddypress' ) );
+		}
+
+		echo '<strong>' . apply_filters( 'bp_get_message_notice_subject', $item->subject ) . '</strong> ' . $this->row_actions( $actions );
+	}
+
+	/**
+	 * Generates content for the "message" column.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param object $item The current item
+	 */
+	public function column_message( $item ) {
+		echo apply_filters( 'bp_get_message_notice_text', $item->message );
+	}
+
+	/**
+	 * Generates content for the "date_sent" column.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param object $item The current item
+	 */
+	public function column_date_sent( $item ) {
+		echo apply_filters( 'bp_get_message_notice_post_date', bp_format_time( strtotime( $item->date_sent ) ) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-sitewide-notices-widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-sitewide-notices-widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-sitewide-notices-widget.php	(revision 12265)
@@ -0,0 +1,122 @@
+<?php
+/**
+ * BuddyPress Messages Sitewide Notices Widget.
+ *
+ * @package BuddyPress
+ * @subpackage Messages
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * A widget that displays sitewide notices.
+ *
+ * @since 1.9.0
+ */
+class BP_Messages_Sitewide_Notices_Widget extends WP_Widget {
+
+	/**
+	 * Constructor method.
+	 */
+	function __construct() {
+		parent::__construct(
+			'bp_messages_sitewide_notices_widget',
+			__( '(BuddyPress) Sitewide Notices', 'buddypress' ),
+			array(
+				'classname'                   => 'widget_bp_core_sitewide_messages buddypress widget',
+				'description'                 => __( 'Display Sitewide Notices posted by the site administrator', 'buddypress' ),
+				'customize_selective_refresh' => true,
+			)
+		);
+	}
+
+	/**
+	 * Render the widget.
+	 *
+	 * @see WP_Widget::widget() for a description of parameters.
+	 *
+	 * @param array $args     See {@WP_Widget::widget()}.
+	 * @param array $instance See {@WP_Widget::widget()}.
+	 */
+	public function widget( $args, $instance ) {
+
+		if ( ! is_user_logged_in() ) {
+			return;
+		}
+
+		// Don't display the widget if there are no Notices to show.
+		$notices = BP_Messages_Notice::get_active();
+		if ( empty( $notices ) ) {
+			return;
+		}
+
+		extract( $args );
+
+		$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
+
+		/**
+		 * Filters the title of the Messages widget.
+		 *
+		 * @since 1.9.0
+		 * @since 2.3.0 Added 'instance' and 'id_base' to arguments passed to filter.
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
+
+		echo $before_widget;
+		echo $before_title . $title . $after_title; ?>
+
+		<div class="bp-site-wide-message">
+			<?php bp_message_get_notices(); ?>
+		</div>
+
+		<?php
+
+		echo $after_widget;
+	}
+
+	/**
+	 * Process the saved settings for the widget.
+	 *
+	 * @see WP_Widget::update() for a description of parameters and
+	 *      return values.
+	 *
+	 * @param array $new_instance See {@WP_Widget::update()}.
+	 * @param array $old_instance See {@WP_Widget::update()}.
+	 * @return array $instance See {@WP_Widget::update()}.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance = $old_instance;
+		$instance['title'] = strip_tags( $new_instance['title'] );
+		return $instance;
+	}
+
+	/**
+	 * Render the settings form for Appearance > Widgets.
+	 *
+	 * @see WP_Widget::form() for a description of parameters.
+	 *
+	 * @param array $instance See {@WP_Widget::form()}.
+	 *
+	 * @return string|null Widget form output.
+	 */
+	public function form( $instance ) {
+		$instance = wp_parse_args( (array) $instance, array(
+			'title' => '',
+		) );
+
+		$title = strip_tags( $instance['title'] ); ?>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'buddypress' ); ?></label>
+			<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+		</p>
+
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-thread-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-thread-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-thread-template.php	(revision 12265)
@@ -0,0 +1,186 @@
+<?php
+/**
+ * BuddyPress Messages Thread Template Class.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Message Thread Template Class
+ */
+class BP_Messages_Thread_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @var int
+	 */
+	public $current_message = -1;
+
+	/**
+	 * Number of messages returned by the paged query.
+	 *
+	 * @var int
+	 */
+	public $message_count = 0;
+
+	/**
+	 * The message object currently being iterated on.
+	 *
+	 * @var object
+	 */
+	public $message;
+
+	/**
+	 * Thread that the current messages belong to.
+	 *
+	 * @var BP_Messages_Thread
+	 */
+	public $thread;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @var bool
+	 */
+	public $in_the_loop = false;
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @var int
+	 */
+	public $pag_page = 1;
+
+	/**
+	 * The number of items being requested per page.
+	 *
+	 * @var int
+	 */
+	public $pag_num = 10;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @var string
+	 */
+	public $pag_links = '';
+
+	/**
+	 * The total number of messages matching the query.
+	 *
+	 * @var int
+	 */
+	public $total_message_count = 0;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @see BP_Messages_Thread::populate() for full parameter info.
+	 *
+	 * @param int    $thread_id ID of the message thread to display.
+	 * @param string $order     Order to show the thread's messages in.
+	 * @param array  $args      Array of arguments for the query.
+	 */
+	public function __construct( $thread_id = 0, $order = 'ASC', $args = array() ) {
+		$this->thread        = new BP_Messages_Thread( $thread_id, $order, $args );
+		$this->message_count = count( $this->thread->messages );
+	}
+
+	/**
+	 * Whether there are messages available in the loop.
+	 *
+	 * @see bp_thread_has_messages()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	public function has_messages() {
+		if ( ! empty( $this->message_count ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set up the next member and iterate index.
+	 *
+	 * @return object The next member to iterate over.
+	 */
+	public function next_message() {
+		$this->current_message++;
+		$this->message = $this->thread->messages[ $this->current_message ];
+
+		return $this->message;
+	}
+
+	/**
+	 * Rewind the messages and reset message index.
+	 */
+	public function rewind_messages() {
+		$this->current_message = -1;
+		if ( $this->message_count > 0 ) {
+			$this->message = $this->thread->messages[0];
+		}
+	}
+
+	/**
+	 * Whether there are messages left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_thread_messages()} as part of the
+	 * while loop that controls iteration inside the messages loop, eg:
+	 *     while ( bp_thread_messages() ) { ...
+	 *
+	 * @see bp_thread_messages()
+	 *
+	 * @return bool True if there are more messages to show, otherwise false.
+	 */
+	public function messages() {
+		if ( ( $this->current_message + 1 ) < $this->message_count ) {
+			return true;
+		} elseif ( ( $this->current_message + 1 ) === $this->message_count ) {
+
+			/**
+			 * Fires when at the end of messages to iterate over.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'thread_loop_end' );
+			// Do some cleaning up after the loop.
+			$this->rewind_messages();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Set up the current message inside the loop.
+	 *
+	 * Used by {@link bp_thread_the_message()} to set up the current
+	 * message data while looping, so that template tags used during
+	 * that iteration make reference to the current message.
+	 *
+	 * @see bp_thread_the_message()
+	 */
+	public function the_message() {
+		$this->in_the_loop = true;
+		$this->message     = $this->next_message();
+
+		// Loop has just started.
+		if ( 0 === $this->current_message ) {
+
+			/**
+			 * Fires if at the start of the message loop.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'thread_loop_start' );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-thread.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-thread.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/classes/class-bp-messages-thread.php	(revision 12265)
@@ -0,0 +1,907 @@
+<?php
+/**
+ * BuddyPress Messages Classes.
+ *
+ * @package BuddyPress
+ * @subpackage MessagesClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Message Thread class.
+ *
+ * @since 1.0.0
+ */
+class BP_Messages_Thread {
+
+	/**
+	 * The message thread ID.
+	 *
+	 * @since 1.0.0
+	 * @var int
+	 */
+	public $thread_id;
+
+	/**
+	 * The current messages.
+	 *
+	 * @since 1.0.0
+	 * @var array
+	 */
+	public $messages;
+
+	/**
+	 * The current recipients in the message thread.
+	 *
+	 * @since 1.0.0
+	 * @var array
+	 */
+	public $recipients;
+
+	/**
+	 * The user IDs of all messages in the message thread.
+	 *
+	 * @since 1.2.0
+	 * @var array
+	 */
+	public $sender_ids;
+
+	/**
+	 * The unread count for the logged-in user.
+	 *
+	 * @since 1.2.0
+	 * @var int
+	 */
+	public $unread_count;
+
+	/**
+	 * The content of the last message in this thread.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	public $last_message_content;
+
+	/**
+	 * The date of the last message in this thread.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	public $last_message_date;
+
+	/**
+	 * The ID of the last message in this thread.
+	 *
+	 * @since 1.2.0
+	 * @var int
+	 */
+	public $last_message_id;
+
+	/**
+	 * The subject of the last message in this thread.
+	 *
+	 * @since 1.2.0
+	 * @var string
+	 */
+	public $last_message_subject;
+
+	/**
+	 * The user ID of the author of the last message in this thread.
+	 *
+	 * @since 1.2.0
+	 * @var int
+	 */
+	public $last_sender_id;
+
+	/**
+	 * Sort order of the messages in this thread (ASC or DESC).
+	 *
+	 * @since 1.5.0
+	 * @var string
+	 */
+	public $messages_order;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see BP_Messages_Thread::populate() for full description of parameters.
+	 *
+	 * @param bool   $thread_id ID for the message thread.
+	 * @param string $order     Order to display the messages in.
+	 * @param array  $args      Array of arguments for thread querying.
+	 */
+	public function __construct( $thread_id = false, $order = 'ASC', $args = array() ) {
+		if ( $thread_id ) {
+			$this->populate( $thread_id, $order, $args );
+		}
+	}
+
+	/**
+	 * Populate method.
+	 *
+	 * Used in constructor.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int    $thread_id The message thread ID.
+	 * @param string $order     The order to sort the messages. Either 'ASC' or 'DESC'.
+	 * @param array  $args {
+	 *     Array of arguments.
+	 *     @type bool $update_meta_cache Whether to pre-fetch metadata for
+	 *                                   queried message items. Default: true.
+	 * }
+	 * @return bool False on failure.
+	 */
+	public function populate( $thread_id = 0, $order = 'ASC', $args = array() ) {
+
+		if ( 'ASC' !== $order && 'DESC' !== $order ) {
+			$order = 'ASC';
+		}
+
+		$user_id =
+			bp_displayed_user_id() ?
+			bp_displayed_user_id() :
+			bp_loggedin_user_id();
+
+		// Merge $args with our defaults.
+		$r = wp_parse_args( $args, array(
+			'user_id'           => $user_id,
+			'update_meta_cache' => true
+		) );
+
+		$this->messages_order = $order;
+		$this->thread_id      = (int) $thread_id;
+
+		// Get messages for thread.
+		$this->messages = self::get_messages( $this->thread_id );
+
+		if ( empty( $this->messages ) || is_wp_error( $this->messages ) ) {
+			return false;
+		}
+
+		// Flip if order is DESC.
+		if ( 'DESC' === $order ) {
+			$this->messages = array_reverse( $this->messages );
+		}
+
+		$last_message_index         = count( $this->messages ) - 1;
+		$this->last_message_id      = $this->messages[ $last_message_index ]->id;
+		$this->last_message_date    = $this->messages[ $last_message_index ]->date_sent;
+		$this->last_sender_id       = $this->messages[ $last_message_index ]->sender_id;
+		$this->last_message_subject = $this->messages[ $last_message_index ]->subject;
+		$this->last_message_content = $this->messages[ $last_message_index ]->message;
+
+		foreach ( (array) $this->messages as $key => $message ) {
+			$this->sender_ids[ $message->sender_id ] = $message->sender_id;
+		}
+
+		// Fetch the recipients.
+		$this->recipients = $this->get_recipients();
+
+		// Get the unread count for the logged in user.
+		if ( isset( $this->recipients[ $r['user_id'] ] ) ) {
+			$this->unread_count = $this->recipients[ $r['user_id'] ]->unread_count;
+		}
+
+		// Grab all message meta.
+		if ( true === (bool) $r['update_meta_cache'] ) {
+			bp_messages_update_meta_cache( wp_list_pluck( $this->messages, 'id' ) );
+		}
+
+		/**
+		 * Fires after a BP_Messages_Thread object has been populated.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param BP_Messages_Thread $this Message thread object.
+		 */
+		do_action( 'bp_messages_thread_post_populate', $this );
+	}
+
+	/**
+	 * Mark a thread initialized in this class as read.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see BP_Messages_Thread::mark_as_read()
+	 */
+	public function mark_read() {
+		BP_Messages_Thread::mark_as_read( $this->thread_id );
+	}
+
+	/**
+	 * Mark a thread initialized in this class as unread.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @see BP_Messages_Thread::mark_as_unread()
+	 */
+	public function mark_unread() {
+		BP_Messages_Thread::mark_as_unread( $this->thread_id );
+	}
+
+	/**
+	 * Returns recipients for a message thread.
+	 *
+	 * @since 1.0.0
+	 * @since 2.3.0 Added $thread_id as a parameter.
+	 *
+	 * @param int $thread_id The thread ID.
+	 * @return array
+	 */
+	public function get_recipients( $thread_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $thread_id ) ) {
+			$thread_id = $this->thread_id;
+		}
+
+		$thread_id = (int) $thread_id;
+
+		$recipients = wp_cache_get( 'thread_recipients_' . $thread_id, 'bp_messages' );
+		if ( false === $recipients ) {
+			$bp = buddypress();
+
+			$recipients = array();
+			$sql        = $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d", $thread_id );
+			$results    = $wpdb->get_results( $sql );
+
+			foreach ( (array) $results as $recipient ) {
+				$recipients[ $recipient->user_id ] = $recipient;
+			}
+
+			wp_cache_set( 'thread_recipients_' . $thread_id, $recipients, 'bp_messages' );
+		}
+
+		// Cast all items from the messages DB table as integers.
+		foreach ( (array) $recipients as $key => $data ) {
+			$recipients[ $key ] = (object) array_map( 'intval', (array) $data );
+		}
+
+		/**
+		 * Filters the recipients of a message thread.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param array $recipients Array of recipient objects.
+		 * @param int   $thread_id  ID of the current thread.
+		 */
+		return apply_filters( 'bp_messages_thread_get_recipients', $recipients, $thread_id );
+	}
+
+	/** Static Functions ******************************************************/
+
+	/**
+	 * Get all messages associated with a thread.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 *
+	 * @return object List of messages associated with a thread.
+	 */
+	public static function get_messages( $thread_id = 0 ) {
+		$thread_id = (int) $thread_id;
+		$messages  = wp_cache_get( $thread_id, 'bp_messages_threads' );
+
+		if ( false === $messages ) {
+			global $wpdb;
+
+			$bp = buddypress();
+
+			// Always sort by ASC by default.
+			$messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_messages} WHERE thread_id = %d ORDER BY date_sent ASC", $thread_id ) );
+
+			wp_cache_set( $thread_id, (array) $messages, 'bp_messages_threads' );
+		}
+
+		// Integer casting.
+		foreach ( $messages as $key => $data ) {
+			$messages[ $key ]->id        = (int) $messages[ $key ]->id;
+			$messages[ $key ]->thread_id = (int) $messages[ $key ]->thread_id;
+			$messages[ $key ]->sender_id = (int) $messages[ $key ]->sender_id;
+		}
+
+		return $messages;
+	}
+
+	/**
+	 * Static method to get message recipients by thread ID.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param int $thread_id The thread ID.
+	 * @return array
+	 */
+	public static function get_recipients_for_thread( $thread_id = 0 ) {
+		$thread = new self( false );
+		return $thread->get_recipients( $thread_id );
+	}
+
+	/**
+	 * Mark messages in a thread as deleted or delete all messages in a thread.
+	 *
+	 * Note: All messages in a thread are deleted once every recipient in a thread
+	 * has marked the thread as deleted.
+	 *
+	 * @since 1.0.0
+	 * @since 2.7.0 The $user_id parameter was added. Previously the current user
+	 *              was always assumed.
+	 *
+	 * @param int $thread_id The message thread ID.
+	 * @param int $user_id The ID of the user in the thread to mark messages as
+	 *                     deleted for. Defaults to the current logged-in user.
+	 *
+	 * @return bool
+	 */
+	public static function delete( $thread_id = 0, $user_id = 0 ) {
+		global $wpdb;
+
+		$thread_id = (int) $thread_id;
+		$user_id = (int) $user_id;
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		/**
+		 * Fires before a message thread is marked as deleted.
+		 *
+		 * @since 2.2.0
+		 * @since 2.7.0 The $user_id parameter was added.
+		 *
+		 * @param int $thread_id ID of the thread being deleted.
+		 * @param int $user_id   ID of the user that the thread is being deleted for.
+		 */
+		do_action( 'bp_messages_thread_before_mark_delete', $thread_id, $user_id );
+
+		$bp = buddypress();
+
+		// Mark messages as deleted
+		$wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET is_deleted = 1 WHERE thread_id = %d AND user_id = %d", $thread_id, $user_id ) );
+
+		// Get the message ids in order to pass to the action.
+		$message_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d", $thread_id ) );
+
+		// Check to see if any more recipients remain for this message.
+		$recipients = $wpdb->get_results( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d AND is_deleted = 0", $thread_id ) );
+
+		// No more recipients so delete all messages associated with the thread.
+		if ( empty( $recipients ) ) {
+
+			/**
+			 * Fires before an entire message thread is deleted.
+			 *
+			 * @since 2.2.0
+			 *
+			 * @param int   $thread_id   ID of the thread being deleted.
+			 * @param array $message_ids IDs of messages being deleted.
+			 */
+			do_action( 'bp_messages_thread_before_delete', $thread_id, $message_ids );
+
+			// Delete all the messages.
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->messages->table_name_messages} WHERE thread_id = %d", $thread_id ) );
+
+			// Do something for each message ID.
+			foreach ( $message_ids as $message_id ) {
+
+				// Delete message meta.
+				bp_messages_delete_meta( $message_id );
+
+				/**
+				 * Fires after a message is deleted. This hook is poorly named.
+				 *
+				 * @since 1.0.0
+				 *
+				 * @param int $message_id ID of the message.
+				 */
+				do_action( 'messages_thread_deleted_thread', $message_id );
+			}
+
+			// Delete all the recipients.
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d", $thread_id ) );
+		}
+
+		/**
+		 * Fires after a message thread is either marked as deleted or deleted.
+		 *
+		 * @since 2.2.0
+		 * @since 2.7.0 The $user_id parameter was added.
+		 *
+		 * @param int   $thread_id   ID of the thread being deleted.
+		 * @param array $message_ids IDs of messages being deleted.
+		 * @param int   $user_id     ID of the user the threads were deleted for.
+		 */
+		do_action( 'bp_messages_thread_after_delete', $thread_id, $message_ids, $user_id );
+
+		return true;
+	}
+
+	/**
+	 * Get current message threads for a user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $args {
+	 *     Array of arguments.
+	 *     @type int    $user_id      The user ID.
+	 *     @type string $box          The type of mailbox to get. Either 'inbox' or 'sentbox'.
+	 *                                Defaults to 'inbox'.
+	 *     @type string $type         The type of messages to get. Either 'all' or 'unread'
+	 *                                or 'read'. Defaults to 'all'.
+	 *     @type int    $limit        The number of messages to get. Defaults to null.
+	 *     @type int    $page         The page number to get. Defaults to null.
+	 *     @type string $search_terms The search term to use. Defaults to ''.
+	 *     @type array  $meta_query   Meta query arguments. See WP_Meta_Query for more details.
+	 * }
+	 * @return array|bool Array on success. Boolean false on failure.
+	 */
+	public static function get_current_threads_for_user( $args = array() ) {
+		global $wpdb;
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '2.2.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'user_id',
+				1 => 'box',
+				2 => 'type',
+				3 => 'limit',
+				4 => 'page',
+				5 => 'search_terms',
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$r = bp_parse_args( $args, array(
+			'user_id'      => false,
+			'box'          => 'inbox',
+			'type'         => 'all',
+			'limit'        => null,
+			'page'         => null,
+			'search_terms' => '',
+			'meta_query'   => array()
+		) );
+
+		$pag_sql = $type_sql = $search_sql = $user_id_sql = $sender_sql = '';
+		$meta_query_sql = array(
+			'join'  => '',
+			'where' => ''
+		);
+
+		if ( $r['limit'] && $r['page'] ) {
+			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $r['page'] - 1 ) * $r['limit'] ), intval( $r['limit'] ) );
+		}
+
+		if ( $r['type'] == 'unread' ) {
+			$type_sql = " AND r.unread_count != 0 ";
+		} elseif ( $r['type'] == 'read' ) {
+			$type_sql = " AND r.unread_count = 0 ";
+		}
+
+		if ( ! empty( $r['search_terms'] ) ) {
+			$search_terms_like = '%' . bp_esc_like( $r['search_terms'] ) . '%';
+			$search_sql        = $wpdb->prepare( "AND ( subject LIKE %s OR message LIKE %s )", $search_terms_like, $search_terms_like );
+		}
+
+		$r['user_id'] = (int) $r['user_id'];
+
+		// Default deleted SQL.
+		$deleted_sql = 'r.is_deleted = 0';
+
+		switch ( $r['box'] ) {
+			case 'sentbox' :
+				$user_id_sql = 'AND ' . $wpdb->prepare( 'm.sender_id = %d', $r['user_id'] );
+				$sender_sql  = 'AND m.sender_id = r.user_id';
+				break;
+
+			case 'inbox' :
+				$user_id_sql = 'AND ' . $wpdb->prepare( 'r.user_id = %d', $r['user_id'] );
+				$sender_sql  = 'AND r.sender_only = 0';
+				break;
+
+			default :
+				// Omit user-deleted threads from all other custom message boxes.
+				$deleted_sql = $wpdb->prepare( '( r.user_id = %d AND r.is_deleted = 0 )', $r['user_id'] );
+				break;
+		}
+
+		// Process meta query into SQL.
+		$meta_query = self::get_meta_query_sql( $r['meta_query'] );
+		if ( ! empty( $meta_query['join'] ) ) {
+			$meta_query_sql['join'] = $meta_query['join'];
+		}
+		if ( ! empty( $meta_query['where'] ) ) {
+			$meta_query_sql['where'] = $meta_query['where'];
+		}
+
+		$bp = buddypress();
+
+		// Set up SQL array.
+		$sql = array();
+		$sql['select'] = 'SELECT m.thread_id, MAX(m.date_sent) AS date_sent';
+		$sql['from']   = "FROM {$bp->messages->table_name_recipients} r INNER JOIN {$bp->messages->table_name_messages} m ON m.thread_id = r.thread_id {$meta_query_sql['join']}";
+		$sql['where']  = "WHERE {$deleted_sql} {$user_id_sql} {$sender_sql} {$type_sql} {$search_sql} {$meta_query_sql['where']}";
+		$sql['misc']   = "GROUP BY m.thread_id ORDER BY date_sent DESC {$pag_sql}";
+
+		// Get thread IDs.
+		$thread_ids = $wpdb->get_results( implode( ' ', $sql ) );
+		if ( empty( $thread_ids ) ) {
+			return false;
+		}
+
+		// Adjust $sql to work for thread total.
+		$sql['select'] = 'SELECT COUNT( DISTINCT m.thread_id )';
+		unset( $sql['misc'] );
+		$total_threads = $wpdb->get_var( implode( ' ', $sql ) );
+
+		// Sort threads by date_sent.
+		foreach( (array) $thread_ids as $thread ) {
+			$sorted_threads[ $thread->thread_id ] = strtotime( $thread->date_sent );
+		}
+
+		arsort( $sorted_threads );
+
+		$threads = array();
+		foreach ( (array) $sorted_threads as $thread_id => $date_sent ) {
+			$threads[] = new BP_Messages_Thread( $thread_id, 'ASC', array(
+				'update_meta_cache' => false
+			) );
+		}
+
+		/**
+		 * Filters the results of the query for a user's message threads.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param array $value {
+		 *     @type array $threads       Array of threads. Passed by reference.
+		 *     @type int   $total_threads Number of threads found by the query.
+		 * }
+		 */
+		return apply_filters( 'bp_messages_thread_current_threads', array(
+			'threads' => &$threads,
+			'total'   => (int) $total_threads
+		) );
+	}
+
+	/**
+	 * Get the SQL for the 'meta_query' param in BP_Messages_Thread::get_current_threads_for_user().
+	 *
+	 * We use WP_Meta_Query to do the heavy lifting of parsing the meta_query array
+	 * and creating the necessary SQL clauses.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $meta_query An array of meta_query filters. See the
+	 *                          documentation for WP_Meta_Query for details.
+	 * @return array $sql_array 'join' and 'where' clauses.
+	 */
+	public static function get_meta_query_sql( $meta_query = array() ) {
+		global $wpdb;
+
+		$sql_array = array(
+			'join'  => '',
+			'where' => '',
+		);
+
+		if ( ! empty( $meta_query ) ) {
+			$meta_query = new WP_Meta_Query( $meta_query );
+
+			// WP_Meta_Query expects the table name at
+			// $wpdb->messagemeta.
+			$wpdb->messagemeta = buddypress()->messages->table_name_meta;
+
+			return $meta_query->get_sql( 'message', 'm', 'id' );
+		}
+
+		return $sql_array;
+	}
+
+	/**
+	 * Mark a thread as read.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 *
+	 * @return false|int Number of threads marked as read or false on error.
+	 */
+	public static function mark_as_read( $thread_id = 0 ) {
+		global $wpdb;
+
+		$user_id =
+			bp_displayed_user_id() ?
+			bp_displayed_user_id() :
+			bp_loggedin_user_id();
+
+		$bp     = buddypress();
+		$retval = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET unread_count = 0 WHERE user_id = %d AND thread_id = %d", $user_id, $thread_id ) );
+
+		wp_cache_delete( 'thread_recipients_' . $thread_id, 'bp_messages' );
+		wp_cache_delete( $user_id, 'bp_messages_unread_count' );
+
+		/**
+		 * Fires when messages thread was marked as read.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param int $thread_id The message thread ID.
+		 */
+		do_action( 'messages_thread_mark_as_read', $thread_id );
+
+		return $retval;
+	}
+
+	/**
+	 * Mark a thread as unread.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 *
+	 * @return false|int Number of threads marked as unread or false on error.
+	 */
+	public static function mark_as_unread( $thread_id = 0 ) {
+		global $wpdb;
+
+		$user_id =
+			bp_displayed_user_id() ?
+			bp_displayed_user_id() :
+			bp_loggedin_user_id();
+
+		$bp     = buddypress();
+		$retval = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET unread_count = 1 WHERE user_id = %d AND thread_id = %d", $user_id, $thread_id ) );
+
+		wp_cache_delete( 'thread_recipients_' . $thread_id, 'bp_messages' );
+		wp_cache_delete( $user_id, 'bp_messages_unread_count' );
+
+		/**
+		 * Fires when messages thread was marked as unread.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @param int $thread_id The message thread ID.
+		 */
+		do_action( 'messages_thread_mark_as_unread', $thread_id );
+
+		return $retval;
+	}
+
+	/**
+	 * Returns the total number of message threads for a user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int    $user_id The user ID.
+	 * @param string $box     The type of mailbox to get. Either 'inbox' or 'sentbox'.
+	 *                        Defaults to 'inbox'.
+	 * @param string $type    The type of messages to get. Either 'all' or 'unread'.
+	 *                        or 'read'. Defaults to 'all'.
+	 * @return int $value Total thread count for the provided user.
+	 */
+	public static function get_total_threads_for_user( $user_id, $box = 'inbox', $type = 'all' ) {
+		global $wpdb;
+
+		$exclude_sender = $type_sql = '';
+		if ( $box !== 'sentbox' ) {
+			$exclude_sender = 'AND sender_only != 1';
+		}
+
+		if ( $type === 'unread' ) {
+			$type_sql = 'AND unread_count != 0';
+		} elseif ( $type === 'read' ) {
+			$type_sql = 'AND unread_count = 0';
+		}
+
+		$bp = buddypress();
+
+		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(thread_id) FROM {$bp->messages->table_name_recipients} WHERE user_id = %d AND is_deleted = 0 {$exclude_sender} {$type_sql}", $user_id ) );
+	}
+
+	/**
+	 * Determine if the logged-in user is a sender of any message in a thread.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 * @return bool
+	 */
+	public static function user_is_sender( $thread_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$sender_ids = $wpdb->get_col( $wpdb->prepare( "SELECT sender_id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d", $thread_id ) );
+
+		if ( empty( $sender_ids ) ) {
+			return false;
+		}
+
+		return in_array( bp_loggedin_user_id(), $sender_ids );
+	}
+
+	/**
+	 * Returns the userlink of the last sender in a message thread.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 * @return string|bool The user link on success. Boolean false on failure.
+	 */
+	public static function get_last_sender( $thread_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( ! $sender_id = $wpdb->get_var( $wpdb->prepare( "SELECT sender_id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d GROUP BY sender_id ORDER BY date_sent LIMIT 1", $thread_id ) ) ) {
+			return false;
+		}
+
+		return bp_core_get_userlink( $sender_id, true );
+	}
+
+	/**
+	 * Gets the unread message count for a user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id The user ID.
+	 * @return int $unread_count Total inbox unread count for user.
+	 */
+	public static function get_inbox_count( $user_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$unread_count = wp_cache_get( $user_id, 'bp_messages_unread_count' );
+
+		if ( false === $unread_count ) {
+			$bp = buddypress();
+
+			$unread_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT SUM(unread_count) FROM {$bp->messages->table_name_recipients} WHERE user_id = %d AND is_deleted = 0 AND sender_only = 0", $user_id ) );
+
+			wp_cache_set( $user_id, $unread_count, 'bp_messages_unread_count' );
+		}
+
+		/**
+		 * Filters a user's unread message count.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param int $unread_count Unread message count.
+		 * @param int $user_id      ID of the user.
+		 */
+		return apply_filters( 'messages_thread_get_inbox_count', (int) $unread_count, $user_id );
+	}
+
+	/**
+	 * Checks whether a user is a part of a message thread discussion.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 * @param int $user_id   The user ID.
+	 * @return int|null The recorded recipient ID on success, null on failure.
+	 */
+	public static function check_access( $thread_id, $user_id = 0 ) {
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_loggedin_user_id();
+		}
+
+		$recipients = self::get_recipients_for_thread( $thread_id );
+
+		if ( isset( $recipients[ $user_id ] ) && 0 == $recipients[ $user_id ]->is_deleted ) {
+			return $recipients[ $user_id ]->id;
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Checks whether a message thread exists.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $thread_id The message thread ID.
+	 * @return false|int|null The message thread ID on success, null on failure.
+	 */
+	public static function is_valid( $thread_id = 0 ) {
+
+		// Bail if no thread ID is passed.
+		if ( empty( $thread_id ) ) {
+			return false;
+		}
+
+		$thread = self::get_messages( $thread_id );
+
+		if ( ! empty( $thread ) ) {
+			return $thread_id;
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Returns a string containing all the message recipient userlinks.
+	 *
+	 * String is comma-delimited.
+	 *
+	 * If a message thread has more than four users, the returned string is simply
+	 * "X Recipients" where "X" is the number of recipients in the message thread.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $recipients Array containing the message recipients (array of objects).
+	 * @return string $value String of message recipent userlinks.
+	 */
+	public static function get_recipient_links( $recipients ) {
+
+		if ( count( $recipients ) >= 5 ) {
+			return sprintf( __( '%s Recipients', 'buddypress' ), number_format_i18n( count( $recipients ) ) );
+		}
+
+		$recipient_links = array();
+
+		foreach ( (array) $recipients as $recipient ) {
+			$recipient_link = bp_core_get_userlink( $recipient->user_id );
+
+			if ( empty( $recipient_link ) ) {
+				$recipient_link = __( 'Deleted User', 'buddypress' );
+			}
+
+			$recipient_links[] = $recipient_link;
+		}
+
+		return implode( ', ', (array) $recipient_links );
+	}
+
+	/**
+	 * Upgrade method for the older BP message thread DB table.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @todo We should remove this.  No one is going to upgrade from v1.1, right?
+	 * @return bool
+	 */
+	public static function update_tables() {
+		global $wpdb;
+
+		$bp_prefix = bp_core_get_table_prefix();
+		$errors    = false;
+		$threads   = $wpdb->get_results( "SELECT * FROM {$bp_prefix}bp_messages_threads" );
+
+		// Nothing to update, just return true to remove the table.
+		if ( empty( $threads ) ) {
+			return true;
+		}
+
+		$bp = buddypress();
+
+		foreach( (array) $threads as $thread ) {
+			$message_ids = maybe_unserialize( $thread->message_ids );
+
+			if ( ! empty( $message_ids ) ) {
+				$message_ids = implode( ',', $message_ids );
+
+				// Add the thread_id to the messages table.
+				if ( ! $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_messages} SET thread_id = %d WHERE id IN ({$message_ids})", $thread->id ) ) ) {
+					$errors = true;
+				}
+			}
+		}
+
+		return (bool) ! $errors;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-messages/css/autocomplete/jquery.autocompletefb-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/css/autocomplete/jquery.autocompletefb-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/css/autocomplete/jquery.autocompletefb-rtl.css	(revision 12265)
@@ -0,0 +1,80 @@
+.ac_results {
+	padding: 0;
+	overflow: hidden;
+	z-index: 99999;
+	background: #fff;
+	border: 1px solid #ccc;
+	border-bottom-right-radius: 3px;
+	border-bottom-left-radius: 3px;
+}
+
+.ac_results ul {
+	width: 100%;
+	list-style: none;
+	list-style-position: outside;
+	padding: 0;
+	margin: 0;
+}
+
+.ac_results li {
+	margin: 0;
+	padding: 5px 10px;
+	cursor: pointer;
+	display: block;
+	font-size: 1em;
+	line-height: 16px;
+	overflow: hidden;
+}
+
+.ac_results li img {
+	margin-left: 5px;
+}
+
+.ac_odd {
+	background-color: #f0f0f0;
+}
+
+.ac_over {
+	background-color: #888;
+	color: #fff;
+}
+
+ul.acfb-holder {
+	margin: 0;
+	height: auto !important;
+	height: 1%;
+	overflow: hidden;
+	padding: 0;
+	list-style: none;
+}
+
+ul.acfb-holder li {
+	float: right;
+	margin: 0 0 4px 5px;
+	list-style-type: none;
+}
+
+ul.acfb-holder li.friend-tab {
+	border-radius: 3px;
+	-moz-border-radius: 3px;
+	-webkit-border-radius: 3px;
+	border: 1px solid #ffe7c7;
+	padding: 2px 7px;
+	background: #fff9df;
+	font-size: 1em;
+}
+
+li.friend-tab img.avatar {
+	border-width: 2px !important;
+	vertical-align: middle;
+}
+
+li.friend-tab span.p {
+	padding-right: 5px;
+	font-size: 0.8em;
+	cursor: pointer;
+}
+
+input#send-to-input {
+	width: 275px;
+}
Index: /tags/4.0.0-beta1/src/bp-messages/css/autocomplete/jquery.autocompletefb.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/css/autocomplete/jquery.autocompletefb.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/css/autocomplete/jquery.autocompletefb.css	(revision 12265)
@@ -0,0 +1,80 @@
+.ac_results {
+	padding: 0;
+	overflow: hidden;
+	z-index: 99999;
+	background: #fff;
+	border: 1px solid #ccc;
+	border-bottom-left-radius: 3px;
+	border-bottom-right-radius: 3px;
+}
+
+.ac_results ul {
+	width: 100%;
+	list-style: none;
+	list-style-position: outside;
+	padding: 0;
+	margin: 0;
+}
+
+.ac_results li {
+	margin: 0;
+	padding: 5px 10px;
+	cursor: pointer;
+	display: block;
+	font-size: 1em;
+	line-height: 16px;
+	overflow: hidden;
+}
+
+.ac_results li img {
+	margin-right: 5px;
+}
+
+.ac_odd {
+	background-color: #f0f0f0;
+}
+
+.ac_over {
+	background-color: #888;
+	color: #fff;
+}
+
+ul.acfb-holder {
+	margin: 0;
+	height: auto !important;
+	height: 1%;
+	overflow: hidden;
+	padding: 0;
+	list-style: none;
+}
+
+ul.acfb-holder li {
+	float: left;
+	margin: 0 5px 4px 0;
+	list-style-type: none;
+}
+
+ul.acfb-holder li.friend-tab {
+	border-radius: 3px;
+	-moz-border-radius: 3px;
+	-webkit-border-radius: 3px;
+	border: 1px solid #ffe7c7;
+	padding: 2px 7px;
+	background: #fff9df;
+	font-size: 1em;
+}
+
+li.friend-tab img.avatar {
+	border-width: 2px !important;
+	vertical-align: middle;
+}
+
+li.friend-tab span.p {
+	padding-left: 5px;
+	font-size: 0.8em;
+	cursor: pointer;
+}
+
+input#send-to-input {
+	width: 275px;
+}
Index: /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.autocomplete.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.autocomplete.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.autocomplete.js	(revision 12265)
@@ -0,0 +1,754 @@
+/*!
+ * Autocomplete - jQuery plugin 1.0 Beta
+ *
+ * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.autocomplete.js 4485 2008-01-20 13:52:47Z joern.zaefferer $
+ *
+ * Namespaced for BuddyPress as $.fn.autocompletebp, to avoid conflicts with other autocomplete scripts.
+ */
+
+;(function($) {
+
+$.fn.extend({
+	autocompletebp: function(urlOrData, options) {
+		var isUrl = typeof urlOrData == "string";
+		options = $.extend({}, $.Autocompleter.defaults, {
+			url: isUrl ? urlOrData : null,
+			data: isUrl ? null : urlOrData,
+			delay: isUrl ? $.Autocompleter.defaults.delay : 10,
+			max: options && !options.scroll ? 10 : 150
+		}, options);
+
+		// if highlight is set to false, replace it with a do-nothing function
+		options.highlight = options.highlight || function(value) { return value; };
+
+		return this.each(function() {
+			new $.Autocompleter(this, options);
+		});
+	},
+	result: function(handler) {
+		return this.bind("result", handler);
+	},
+	search: function(handler) {
+		return this.trigger("search", [handler]);
+	},
+	flushCache: function() {
+		return this.trigger("flushCache");
+	},
+	setOptions: function(options){
+		return this.trigger("setOptions", [options]);
+	},
+	unautocomplete: function() {
+		return this.trigger("unautocomplete");
+	}
+});
+
+$.Autocompleter = function(input, options) {
+
+	var KEY = {
+		UP: 38,
+		DOWN: 40,
+		DEL: 46,
+		TAB: 9,
+		RETURN: 13,
+		ESC: 27,
+		COMMA: 188,
+		PAGEUP: 33,
+		PAGEDOWN: 34
+	};
+
+	// Create $ object for input element
+	var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
+
+	var timeout;
+	var previousValue = "";
+	var cache = $.Autocompleter.Cache(options);
+	var hasFocus = 0;
+	var lastKeyPressCode;
+	var config = {
+		mouseDownOnSelect: false
+	};
+	var select = $.Autocompleter.Select(options, input, selectCurrent, config);
+
+	$input.keydown(function(event) {
+		// track last key pressed
+		lastKeyPressCode = event.keyCode;
+		switch(event.keyCode) {
+
+			case KEY.UP:
+				event.preventDefault();
+				if ( select.visible() ) {
+					select.prev();
+				} else {
+					onChange(0, true);
+				}
+				break;
+
+			case KEY.DOWN:
+				event.preventDefault();
+				if ( select.visible() ) {
+					select.next();
+				} else {
+					onChange(0, true);
+				}
+				break;
+
+			case KEY.PAGEUP:
+				event.preventDefault();
+				if ( select.visible() ) {
+					select.pageUp();
+				} else {
+					onChange(0, true);
+				}
+				break;
+
+			case KEY.PAGEDOWN:
+				event.preventDefault();
+				if ( select.visible() ) {
+					select.pageDown();
+				} else {
+					onChange(0, true);
+				}
+				break;
+
+			// matches also semicolon
+			case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
+			case KEY.TAB:
+			case KEY.RETURN:
+				if( selectCurrent() ){
+					// make sure to blur off the current field
+					if( !options.multiple )
+						$input.blur();
+					event.preventDefault();
+						$input.focus();
+				}
+				break;
+
+			case KEY.ESC:
+				select.hide();
+				break;
+
+			default:
+				clearTimeout(timeout);
+				timeout = setTimeout(onChange, options.delay);
+				break;
+		}
+	}).keypress(function() {
+		// having fun with opera - remove this binding and Opera submits the form when we select an entry via return
+	}).focus(function(){
+		// track whether the field has focus, we shouldn't process any
+		// results if the field no longer has focus
+		hasFocus++;
+	}).blur(function() {
+		hasFocus = 0;
+		if (!config.mouseDownOnSelect) {
+			hideResults();
+		}
+	}).click(function() {
+		// show select when clicking in a focused field
+		if ( hasFocus++ > 1 && !select.visible() ) {
+			onChange(0, true);
+		}
+	}).bind("search", function() {
+		// TODO why not just specifying both arguments?
+		var fn = (arguments.length > 1) ? arguments[1] : null;
+		function findValueCallback(q, data) {
+			var result;
+			if( data && data.length ) {
+				for (var i=0; i < data.length; i++) {
+					if( data[i].result.toLowerCase() == q.toLowerCase() ) {
+						result = data[i];
+						break;
+					}
+				}
+			}
+			if( typeof fn == "function" ) fn(result);
+			else $input.trigger("result", result && [result.data, result.value]);
+		}
+		$.each(trimWords($input.val()), function(i, value) {
+			request(value, findValueCallback, findValueCallback);
+		});
+	}).bind("flushCache", function() {
+		cache.flush();
+	}).bind("setOptions", function() {
+		$.extend(options, arguments[1]);
+		// if we've updated the data, repopulate
+		if ( "data" in arguments[1] )
+			cache.populate();
+	}).bind("unautocomplete", function() {
+		select.unbind();
+		$input.unbind();
+	});
+
+
+	function selectCurrent() {
+		var selected = select.selected();
+		if( !selected )
+			return false;
+
+		var v = selected.result;
+		previousValue = v;
+
+		if ( options.multiple ) {
+			var words = trimWords($input.val());
+			if ( words.length > 1 ) {
+				v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
+			}
+			v += options.multipleSeparator;
+		}
+
+		$input.val(v);
+		hideResultsNow();
+		$input.trigger("result", [selected.data, selected.value]);
+		return true;
+	}
+
+	function onChange(crap, skipPrevCheck) {
+		if( lastKeyPressCode == KEY.DEL ) {
+			select.hide();
+			return;
+		}
+
+		var currentValue = $input.val();
+
+		if ( !skipPrevCheck && currentValue == previousValue )
+			return;
+
+		previousValue = currentValue;
+
+		currentValue = lastWord(currentValue);
+		if ( currentValue.length >= options.minChars) {
+			$input.addClass(options.loadingClass);
+			jQuery('#send-to-input').addClass('loading');
+			if (!options.matchCase)
+				currentValue = currentValue.toLowerCase();
+			request(currentValue, receiveData, hideResultsNow);
+		} else {
+			stopLoading();
+			select.hide();
+		}
+	};
+
+	function trimWords(value) {
+		if ( !value ) {
+			return [""];
+		}
+		var words = value.split( $.trim( options.multipleSeparator ) );
+		var result = [];
+		$.each(words, function(i, value) {
+			if ( $.trim(value) )
+				result[i] = $.trim(value);
+		});
+		return result;
+	}
+
+	function lastWord(value) {
+		if ( !options.multiple )
+			return value;
+		var words = trimWords(value);
+		return words[words.length - 1];
+	}
+
+	// fills in the input box w/the first match (assumed to be the best match)
+	function autoFill(q, sValue){
+		// autofill in the complete box w/the first match as long as the user hasn't entered in more data
+		// if the last user key pressed was backspace, don't autofill
+		if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != 8 ) {
+			// fill in the value (keep the case the user has typed)
+			$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
+			// select the portion of the value not typed by the user (so the next character will erase)
+			$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
+		}
+	};
+
+	function hideResults() {
+		clearTimeout(timeout);
+		timeout = setTimeout(hideResultsNow, 200);
+	};
+
+	function hideResultsNow() {
+		select.hide();
+		clearTimeout(timeout);
+		stopLoading();
+		if (options.mustMatch) {
+			// call search and run callback
+			$input.search(
+				function (result){
+					// if no value found, clear the input box
+					if( !result ) $input.val("");
+				}
+			);
+		}
+	};
+
+	function receiveData(q, data) {
+		if ( data && data.length && hasFocus ) {
+			stopLoading();
+			select.display(data, q);
+
+			var newData = data[0].value.split(';');
+			data.value = newData[0];
+
+			autoFill(q, data.value);
+			select.show();
+		} else {
+			hideResultsNow();
+		}
+	};
+
+	function request(term, success, failure) {
+		if (!options.matchCase)
+			term = term.toLowerCase();
+		var data = cache.load(term);
+		// recieve the cached data
+		if (data && data.length) {
+			success(term, data);
+		// if an AJAX url has been supplied, try loading the data now
+		} else if( (typeof options.url == "string") && (options.url.length > 0) ){
+
+			var extraParams = {};
+			$.each(options.extraParams, function(key, param) {
+				extraParams[key] = typeof param == "function" ? param() : param;
+			});
+
+			$.ajax({
+				// try to leverage ajaxQueue plugin to abort previous requests
+				mode: "abort",
+				// limit abortion to this input
+				port: "autocomplete" + input.name,
+				dataType: options.dataType,
+				url: options.url,
+				data: $.extend({
+					q: lastWord(term),
+					limit: options.max,
+					action: 'messages_autocomplete_results',
+					'cookie': getAutocompleteCookies()
+				}, extraParams),
+				success: function(data) {
+					var parsed = options.parse && options.parse(data) || parse(data);
+					cache.add(term, parsed);
+					success(term, parsed);
+				}
+			});
+		} else {
+			failure(term);
+		}
+	};
+
+	function parse(data) {
+		var parsed = [];
+		var rows = data.split("\n");
+		for (var i=0; i < rows.length; i++) {
+			var row = $.trim(rows[i]);
+			if (row) {
+				row = row.split("|");
+				parsed[parsed.length] = {
+					data: row,
+					value: row[0],
+					result: options.formatResult && options.formatResult(row, row[0]) || row[0]
+				};
+			}
+		}
+		return parsed;
+	};
+
+	function stopLoading() {
+		$input.removeClass(options.loadingClass);
+		jQuery('#send-to-input').removeClass('loading');
+	};
+
+	/* Returns a querystring of BP cookies (cookies beginning with 'bp-') */
+	function getAutocompleteCookies() {
+		var allCookies = document.cookie.split(';'),  // get all cookies and split into an array
+			bpCookies      = {},
+			cookiePrefix   = 'bp-',
+			i, cookie, delimiter, name, value;
+
+		// loop through cookies
+		for (i = 0; i < allCookies.length; i++) {
+			cookie    = allCookies[i];
+			delimiter = cookie.indexOf('=');
+			name      = jq.trim( unescape( cookie.slice(0, delimiter) ) );
+			value     = unescape( cookie.slice(delimiter + 1) );
+
+			// if BP cookie, store it
+			if ( name.indexOf(cookiePrefix) === 0 ) {
+				bpCookies[name] = value;
+			}
+		}
+
+		// returns BP cookies as querystring
+		return encodeURIComponent( jq.param(bpCookies) );
+	}
+};
+
+$.Autocompleter.defaults = {
+	inputClass: "ac_input",
+	resultsClass: "ac_results",
+	loadingClass: "ac_loading",
+	minChars: 1,
+	delay: 400,
+	matchCase: false,
+	matchSubset: true,
+	matchContains: false,
+	cacheLength: 10,
+	max: 100,
+	mustMatch: false,
+	extraParams: {},
+	selectFirst: true,
+	formatItem: function(row) { return row[0]; },
+	autoFill: false,
+	width: 0,
+	multiple: false,
+	multipleSeparator: ", ",
+	highlight: function(value, term) {
+		return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
+	},
+    scroll: true,
+    scrollHeight: 250,
+	attachTo: 'body'
+};
+
+$.Autocompleter.Cache = function(options) {
+
+	var data = {};
+	var length = 0;
+
+	function matchSubset(s, sub) {
+		if (!options.matchCase)
+			s = s.toLowerCase();
+		var i = s.indexOf(sub);
+		if (i == -1) return false;
+		return i == 0 || options.matchContains;
+	};
+
+	function add(q, value) {
+		if (length > options.cacheLength){
+			flush();
+		}
+		if (!data[q]){
+			length++;
+		}
+		data[q] = value;
+	}
+
+	function populate(){
+		if( !options.data ) return false;
+		// track the matches
+		var stMatchSets = {},
+			nullData = 0;
+
+		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
+		if( !options.url ) options.cacheLength = 1;
+
+		// track all options for minChars = 0
+		stMatchSets[""] = [];
+
+		// loop through the array and create a lookup structure
+		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
+			var rawValue = options.data[i];
+			// if rawValue is a string, make an array otherwise just reference the array
+			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
+
+			var value = options.formatItem(rawValue, i+1, options.data.length);
+			if ( value === false )
+				continue;
+
+			var firstChar = value.charAt(0).toLowerCase();
+			// if no lookup array for this character exists, look it up now
+			if( !stMatchSets[firstChar] )
+				stMatchSets[firstChar] = [];
+
+			// if the match is a string
+			var row = {
+				value: value,
+				data: rawValue,
+				result: options.formatResult && options.formatResult(rawValue) || value
+			};
+
+			// push the current match into the set list
+			stMatchSets[firstChar].push(row);
+
+			// keep track of minChars zero items
+			if ( nullData++ < options.max ) {
+				stMatchSets[""].push(row);
+			}
+		};
+
+		// add the data items to the cache
+		$.each(stMatchSets, function(i, value) {
+			// increase the cache size
+			options.cacheLength++;
+			// add to the cache
+			add(i, value);
+		});
+	}
+
+	// populate any existing data
+	setTimeout(populate, 25);
+
+	function flush(){
+		data = {};
+		length = 0;
+	}
+
+	return {
+		flush: flush,
+		add: add,
+		populate: populate,
+		load: function(q) {
+			if (!options.cacheLength || !length)
+				return null;
+			/*
+			 * if dealing w/local data and matchContains than we must make sure
+			 * to loop through all the data collections looking for matches
+			 */
+			if( !options.url && options.matchContains ){
+				// track all matches
+				var csub = [];
+				// loop through all the data grids for matches
+				for( var k in data ){
+					// don't search through the stMatchSets[""] (minChars: 0) cache
+					// this prevents duplicates
+					if( k.length > 0 ){
+						var c = data[k];
+						$.each(c, function(i, x) {
+							// if we've got a match, add it to the array
+							if (matchSubset(x.value, q)) {
+								csub.push(x);
+							}
+						});
+					}
+				}
+				return csub;
+			} else
+			// if the exact item exists, use it
+			if (data[q]){
+				return data[q];
+			} else
+			if (options.matchSubset) {
+				for (var i = q.length - 1; i >= options.minChars; i--) {
+					var c = data[q.substr(0, i)];
+					if (c) {
+						var csub = [];
+						$.each(c, function(i, x) {
+							if (matchSubset(x.value, q)) {
+								csub[csub.length] = x;
+							}
+						});
+						return csub;
+					}
+				}
+			}
+			return null;
+		}
+	};
+};
+
+$.Autocompleter.Select = function (options, input, select, config) {
+	var CLASSES = {
+		ACTIVE: "ac_over"
+	};
+
+	var listItems,
+		active = -1,
+		data,
+		term = "",
+		needsInit = true,
+		element,
+		list;
+
+	// Create results
+	function init() {
+		if (!needsInit)
+			return;
+		element = $("<div/>")
+		.hide()
+		.addClass(options.resultsClass)
+		.css("position", "absolute")
+		.appendTo(options.attachTo);
+
+		list = $("<ul>").appendTo(element).mouseover( function(event) {
+			if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
+	            active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
+			    $(target(event)).addClass(CLASSES.ACTIVE);
+	        }
+		}).click(function(event) {
+			$(target(event)).addClass(CLASSES.ACTIVE);
+			select();
+			input.focus();
+			return false;
+		}).mousedown(function() {
+			config.mouseDownOnSelect = true;
+		}).mouseup(function() {
+			config.mouseDownOnSelect = false;
+		});
+
+		if( options.width > 0 )
+			element.css("width", options.width);
+
+		needsInit = false;
+	}
+
+	function target(event) {
+		var element = event.target;
+		while(element && element.tagName != "LI")
+			element = element.parentNode;
+		// more fun with IE, sometimes event.target is empty, just ignore it then
+		if(!element)
+			return [];
+		return element;
+	}
+
+	function moveSelect(step) {
+		listItems.slice(active, active + 1).removeClass();
+		movePosition(step);
+        var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
+        if(options.scroll) {
+            var offset = 0;
+            listItems.slice(0, active).each(function() {
+				offset += this.offsetHeight;
+			});
+            if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
+                list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
+            } else if(offset < list.scrollTop()) {
+                list.scrollTop(offset);
+            }
+        }
+	};
+
+	function movePosition(step) {
+		active += step;
+		if (active < 0) {
+			active = listItems.size() - 1;
+		} else if (active >= listItems.size()) {
+			active = 0;
+		}
+	}
+
+	function limitNumberOfItems(available) {
+		return options.max && options.max < available
+			? options.max
+			: available;
+	}
+
+	function fillList() {
+		list.empty();
+		var max = limitNumberOfItems(data.length);
+		for (var i=0; i < max; i++) {
+			if (!data[i])
+				continue;
+			var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
+			if ( formatted === false )
+				continue;
+			var li = $("<li>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_event" : "ac_odd").appendTo(list)[0];
+			$.data(li, "ac_data", data[i]);
+		}
+		listItems = list.find("li");
+		if ( options.selectFirst ) {
+			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
+			active = 0;
+		}
+		list.bgiframe();
+	}
+
+	return {
+		display: function(d, q) {
+			init();
+			data = d;
+			term = q;
+			fillList();
+		},
+		next: function() {
+			moveSelect(1);
+		},
+		prev: function() {
+			moveSelect(-1);
+		},
+		pageUp: function() {
+			if (active != 0 && active - 8 < 0) {
+				moveSelect( -active );
+			} else {
+				moveSelect(-8);
+			}
+		},
+		pageDown: function() {
+			if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
+				moveSelect( listItems.size() - 1 - active );
+			} else {
+				moveSelect(8);
+			}
+		},
+		hide: function() {
+			element && element.hide();
+			active = -1;
+		},
+		visible : function() {
+			return element && element.is(":visible");
+		},
+		current: function() {
+			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
+		},
+		show: function() {
+			var offset = $(input).offset();
+			element.css({
+				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
+				top: offset.top + input.offsetHeight,
+				left: offset.left
+			}).show();
+            if(options.scroll) {
+                list.scrollTop(0);
+                list.css({
+					maxHeight: options.scrollHeight,
+					overflow: 'auto'
+				});
+
+                if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
+					var listHeight = 0;
+					listItems.each(function() {
+						listHeight += this.offsetHeight;
+					});
+					var scrollbarsVisible = listHeight > options.scrollHeight;
+                    list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
+					if (!scrollbarsVisible) {
+						// IE doesn't recalculate width when scrollbar disappears
+						listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
+					}
+                }
+
+            }
+		},
+		selected: function() {
+			var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
+			return selected && selected.length && $.data(selected[0], "ac_data");
+		},
+		unbind: function() {
+			element && element.remove();
+		}
+	};
+};
+
+$.Autocompleter.Selection = function(field, start, end) {
+	if( field.createTextRange ){
+		var selRange = field.createTextRange();
+		selRange.collapse(true);
+		selRange.moveStart("character", start);
+		selRange.moveEnd("character", end);
+		selRange.select();
+	} else if( field.setSelectionRange ){
+		field.setSelectionRange(start, end);
+	} else {
+		if( field.selectionStart ){
+			field.selectionStart = start;
+			field.selectionEnd = end;
+		}
+	}
+	field.focus();
+};
+
+})(jQuery);
Index: /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.autocompletefb.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.autocompletefb.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.autocompletefb.js	(revision 12265)
@@ -0,0 +1,75 @@
+/*!
+ * jQuery plugin: autoCompletefb(AutoComplete Facebook)
+ * @requires jQuery v1.2.2 or later
+ * using plugin:jquery.autocomplete.js
+ *
+ * Credits:
+ * - Idea: Facebook
+ * - Guillermo Rauch: Original MooTools script
+ * - InteRiders <http://interiders.com/>
+ *
+ * Copyright (c) 2008 Widi Harsojo <wharsojo@gmail.com>, http://wharsojo.wordpress.com/
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ */
+jQuery.fn.autoCompletefb = function(options)
+{
+	var tmp = this;
+	var settings =
+	{
+		ul         : tmp,
+		urlLookup  : [""],
+		acOptions  : {},
+		foundClass : ".friend-tab",
+		inputClass : ".send-to-input"
+	}
+
+	if(options) jQuery.extend(settings, options);
+
+	var acfb =
+	{
+		params  : settings,
+		removeFind : function(o){
+			acfb.removeUsername(o);
+			jQuery(o).unbind('click').parent().remove();
+			jQuery(settings.inputClass,tmp).focus();
+			return tmp.acfb;
+		},
+		removeUsername: function(o){
+			var newID = o.parentNode.id.substr( o.parentNode.id.indexOf('-')+1 );
+			jQuery('#send-to-usernames').removeClass(newID);
+		}
+	}
+
+	jQuery(settings.foundClass+" img.p").click(function(){
+		acfb.removeFind(this);
+	});
+
+	jQuery(settings.inputClass,tmp).autocompletebp(settings.urlLookup,settings.acOptions);
+	jQuery(settings.inputClass,tmp).result(function(e,d,f){
+		var f = settings.foundClass.replace(/\./,'');
+		var d = String(d).split(' (');
+		var un = d[1].substr(0, d[1].length-1);
+
+		/* Don't add the same user multiple times */
+		if( 0 === jQuery(settings.inputClass).siblings('#un-' + un).length ) {
+			var ln = '#link-' + un;
+			var l = jQuery(ln).attr('href');
+			var v = '<li class="'+f+'" id="un-'+un+'"><span><a href="'+l+'">'+d[0]+'</a></span> <span class="p">X</span></li>';
+
+			var x = jQuery(settings.inputClass,tmp).before(v);
+			jQuery('#send-to-usernames').addClass(un);
+
+			jQuery('.p',x[0].previousSibling).click(function(){
+				acfb.removeFind(this);
+			});
+		}
+
+		jQuery(settings.inputClass,tmp).val('');
+
+	});
+
+	jQuery(settings.inputClass,tmp).focus();
+	return acfb;
+}
Index: /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.bgiframe.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.bgiframe.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.bgiframe.js	(revision 12265)
@@ -0,0 +1,69 @@
+/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
+ * Licensed under the MIT License (LICENSE.txt).
+ *
+ * Version 3.0.1
+ *
+ * Requires jQuery >= 1.2.6
+ */
+
+(function (factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(['jquery'], factory);
+    } else if (typeof exports === 'object') {
+        // Node/CommonJS style for Browserify
+        module.exports = factory;
+    } else {
+        // Browser globals
+        factory(jQuery);
+    }
+}(function ($) {
+    $.fn.bgiframe = function(s) {
+        s = $.extend({
+            top         : 'auto', // auto == borderTopWidth
+            left        : 'auto', // auto == borderLeftWidth
+            width       : 'auto', // auto == offsetWidth
+            height      : 'auto', // auto == offsetHeight
+            opacity     : true,
+            src         : 'javascript:false;',
+            conditional : /MSIE 6\.0/.test(navigator.userAgent) // expresion or function. return false to prevent iframe insertion
+        }, s);
+
+        // wrap conditional in a function if it isn't already
+        if (!$.isFunction(s.conditional)) {
+            var condition = s.conditional;
+            s.conditional = function() { return condition; };
+        }
+
+        var $iframe = $('<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+
+                           'style="display:block;position:absolute;z-index:-1;"/>');
+
+        return this.each(function() {
+            var $this = $(this);
+            if ( s.conditional(this) === false ) { return; }
+            var existing = $this.children('iframe.bgiframe');
+            var $el = existing.length === 0 ? $iframe.clone() : existing;
+            $el.css({
+                'top': s.top == 'auto' ?
+                    ((parseInt($this.css('borderTopWidth'),10)||0)*-1)+'px' : prop(s.top),
+                'left': s.left == 'auto' ?
+                    ((parseInt($this.css('borderLeftWidth'),10)||0)*-1)+'px' : prop(s.left),
+                'width': s.width == 'auto' ? (this.offsetWidth + 'px') : prop(s.width),
+                'height': s.height == 'auto' ? (this.offsetHeight + 'px') : prop(s.height),
+                'opacity': s.opacity === true ? 0 : undefined
+            });
+
+            if ( existing.length === 0 ) {
+                $this.prepend($el);
+            }
+        });
+    };
+
+    // old alias
+    $.fn.bgIframe = $.fn.bgiframe;
+
+    function prop(n) {
+        return n && n.constructor === Number ? n + 'px' : n;
+    }
+
+}));
Index: /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.dimensions.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.dimensions.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/jquery.dimensions.js	(revision 12265)
@@ -0,0 +1,117 @@
+/*!
+ * Copyright (c) 2007 Paul Bakaus (paul.bakaus@googlemail.com) and Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net)
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * $LastChangedDate: 2007-09-11 05:38:31 +0300 (Вт, 11 сен 2007) $
+ * $Rev: 3238 $
+ *
+ * Version: @VERSION
+ *
+ * Requires: jQuery 1.2+
+ */
+
+(function($){
+
+$.dimensions = {
+	version: '@VERSION'
+};
+
+// Create innerHeight, innerWidth, outerHeight and outerWidth methods
+$.each( [ 'Height', 'Width' ], function(i, name){
+
+	// innerHeight and innerWidth
+	$.fn[ 'inner' + name ] = function() {
+		if (!this[0]) return;
+
+		var torl = name == 'Height' ? 'Top'    : 'Left',  // top or left
+		    borr = name == 'Height' ? 'Bottom' : 'Right'; // bottom or right
+
+		return this[ name.toLowerCase() ]() + num(this, 'padding' + torl) + num(this, 'padding' + borr);
+	};
+
+	// outerHeight and outerWidth
+	$.fn[ 'outer' + name ] = function(options) {
+		if (!this[0]) return;
+
+		var torl = name == 'Height' ? 'Top'    : 'Left',  // top or left
+		    borr = name == 'Height' ? 'Bottom' : 'Right'; // bottom or right
+
+		options = $.extend({ margin: false }, options || {});
+
+		return this[ name.toLowerCase() ]()
+				+ num(this, 'border' + torl + 'Width') + num(this, 'border' + borr + 'Width')
+				+ num(this, 'padding' + torl) + num(this, 'padding' + borr)
+				+ (options.margin ? (num(this, 'margin' + torl) + num(this, 'margin' + borr)) : 0);
+	};
+});
+
+// Create scrollLeft and scrollTop methods
+$.each( ['Left', 'Top'], function(i, name) {
+	$.fn[ 'scroll' + name ] = function(val) {
+		if (!this[0]) return;
+
+		return val != undefined ?
+
+			// Set the scroll offset
+			this.each(function() {
+				this == window || this == document ?
+					window.scrollTo(
+						name == 'Left' ? val : $(window)[ 'scrollLeft' ](),
+						name == 'Top'  ? val : $(window)[ 'scrollTop'  ]()
+					) :
+					this[ 'scroll' + name ] = val;
+			}) :
+
+			// Return the scroll offset
+			this[0] == window || this[0] == document ?
+				self[ (name == 'Left' ? 'pageXOffset' : 'pageYOffset') ] ||
+					$.boxModel && document.documentElement[ 'scroll' + name ] ||
+					document.body[ 'scroll' + name ] :
+				this[0][ 'scroll' + name ];
+	};
+});
+
+$.fn.extend({
+	position: function() {
+		var left = 0, top = 0, elem = this[0], offset, parentOffset, offsetParent, results;
+
+		if (elem) {
+			// Get *real* offsetParent
+			offsetParent = this.offsetParent();
+
+			// Get correct offsets
+			offset       = this.offset();
+			parentOffset = offsetParent.offset();
+
+			// Subtract element margins
+			offset.top  -= num(elem, 'marginTop');
+			offset.left -= num(elem, 'marginLeft');
+
+			// Add offsetParent borders
+			parentOffset.top  += num(offsetParent, 'borderTopWidth');
+			parentOffset.left += num(offsetParent, 'borderLeftWidth');
+
+			// Subtract the two offsets
+			results = {
+				top:  offset.top  - parentOffset.top,
+				left: offset.left - parentOffset.left
+			};
+		}
+
+		return results;
+	},
+
+	offsetParent: function() {
+		var offsetParent = this[0].offsetParent;
+		while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && $.css(offsetParent, 'position') == 'static') )
+			offsetParent = offsetParent.offsetParent;
+		return $(offsetParent);
+	}
+});
+
+var num = function(el, prop) {
+	return parseInt($.css(el.jquery?el[0]:el,prop))||0;
+};
+
+})(jQuery);
Index: /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/license.bgiframe.txt
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/license.bgiframe.txt	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/js/autocomplete/license.bgiframe.txt	(revision 12265)
@@ -0,0 +1,20 @@
+Copyright 2013, Brandon Aaron (http://brandonaaron.net/)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Index: /tags/4.0.0-beta1/src/bp-messages/screens/compose.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/screens/compose.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/screens/compose.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Messages: User's "Messages > Compose" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the Messages > Compose screen.
+ *
+ * @since 1.0.0
+ */
+function messages_screen_compose() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	// Remove any saved message data from a previous session.
+	messages_remove_callback_values();
+
+	/**
+	 * Fires right before the loading of the Messages compose screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'messages_screen_compose' );
+
+	/**
+	 * Filters the template to load for the Messages compose screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the messages template to load.
+	 */
+	bp_core_load_template( apply_filters( 'messages_template_compose', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-messages/screens/inbox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/screens/inbox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/screens/inbox.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Messages: User's "Messages" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the Messages > Inbox screen.
+ *
+ * @since 1.0.0
+ */
+function messages_screen_inbox() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Fires right before the loading of the Messages inbox screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'messages_screen_inbox' );
+
+	/**
+	 * Filters the template to load for the Messages inbox screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the messages template to load.
+	 */
+	bp_core_load_template( apply_filters( 'messages_template_inbox', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-messages/screens/notices.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/screens/notices.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/screens/notices.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Messages: User's "Messages > Notices" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the Messages > Notices screen.
+ *
+ * @since 1.0.0
+ *
+ * @return false|null False on failure.
+ */
+function messages_screen_notices() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Fires right before the loading of the Messages notices screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'messages_screen_notices' );
+
+	/**
+	 * Filters the template to load for the Messages notices screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the messages template to load.
+	 */
+	bp_core_load_template( apply_filters( 'messages_template_notices', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-messages/screens/sentbox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/screens/sentbox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/screens/sentbox.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Messages: User's "Messages > Sent" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load the Messages > Sent screen.
+ *
+ * @since 1.0.0
+ */
+function messages_screen_sentbox() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Fires right before the loading of the Messages sentbox screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'messages_screen_sentbox' );
+
+	/**
+	 * Filters the template to load for the Messages sentbox screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the messages template to load.
+	 */
+	bp_core_load_template( apply_filters( 'messages_template_sentbox', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-messages/screens/starred.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/screens/starred.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/screens/starred.php	(revision 12265)
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Messages: User's "Messages > Starred" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Screen handler to display a user's "Starred" private messages page.
+ *
+ * @since 2.3.0
+ */
+function bp_messages_star_screen() {
+	add_action( 'bp_template_content', 'bp_messages_star_content' );
+
+	/**
+	 * Fires right before the loading of the "Starred" messages box.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_messages_screen_star' );
+
+	bp_core_load_template( 'members/single/plugins' );
+}
+
+/**
+ * Screen content callback to display a user's "Starred" messages page.
+ *
+ * @since 2.3.0
+ */
+function bp_messages_star_content() {
+	// Add our message thread filter.
+	add_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+
+	// Load the message loop template part.
+	bp_get_template_part( 'members/single/messages/messages-loop' );
+
+	// Remove our filter.
+	remove_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+}
Index: /tags/4.0.0-beta1/src/bp-messages/screens/view.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-messages/screens/view.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-messages/screens/view.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Messages: Conversation thread screen handler
+ *
+ * @package BuddyPress
+ * @subpackage MessageScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Load an individual conversation screen.
+ *
+ * @since 1.0.0
+ *
+ * @return false|null False on failure.
+ */
+function messages_screen_conversation() {
+
+	// Bail if not viewing a single message.
+	if ( ! bp_is_messages_component() || ! bp_is_current_action( 'view' ) ) {
+		return false;
+	}
+
+	$thread_id = (int) bp_action_variable( 0 );
+
+	if ( empty( $thread_id ) || ! messages_is_valid_thread( $thread_id ) ) {
+		if ( is_user_logged_in() ) {
+			bp_core_add_message( __( 'The conversation you tried to access is no longer available', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() ) );
+	}
+
+	// No access.
+	if ( ( ! messages_check_thread_access( $thread_id ) || ! bp_is_my_profile() ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+		// If not logged in, prompt for login.
+		if ( ! is_user_logged_in() ) {
+			bp_core_no_access();
+			return;
+
+		// Redirect away.
+		} else {
+			bp_core_add_message( __( 'You do not have access to that conversation.', 'buddypress' ), 'error' );
+			bp_core_redirect( trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() ) );
+		}
+	}
+
+	// Load up BuddyPress one time.
+	$bp = buddypress();
+
+	// Decrease the unread count in the nav before it's rendered.
+	$count    = bp_get_total_unread_messages_count();
+	$class    = ( 0 === $count ) ? 'no-count' : 'count';
+	$nav_name = sprintf( __( 'Messages <span class="%s">%s</span>', 'buddypress' ), esc_attr( $class ), bp_core_number_format( $count ) );
+
+	// Edit the Navigation name.
+	$bp->members->nav->edit_nav( array(
+		'name' => $nav_name,
+	), $bp->messages->slug );
+
+	/**
+	 * Fires right before the loading of the Messages view screen template file.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'messages_screen_conversation' );
+
+	/**
+	 * Filters the template to load for the Messages view screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the messages template to load.
+	 */
+	bp_core_load_template( apply_filters( 'messages_template_view_message', 'members/single/home' ) );
+}
+add_action( 'bp_screens', 'messages_screen_conversation' );
Index: /tags/4.0.0-beta1/src/bp-notifications/actions/bulk-manage.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/actions/bulk-manage.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/actions/bulk-manage.php	(revision 12265)
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Notifications: Bulk-manage action handler
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handles bulk management (mark as read/unread, delete) of notifications.
+ *
+ * @since 2.2.0
+ *
+ * @return bool
+ */
+function bp_notifications_action_bulk_manage() {
+
+	// Bail if not the read or unread screen.
+	if ( ! bp_is_notifications_component() || ! ( bp_is_current_action( 'read' ) || bp_is_current_action( 'unread' ) ) ) {
+		return false;
+	}
+
+	// Get the action.
+	$action = !empty( $_POST['notification_bulk_action'] ) ? $_POST['notification_bulk_action'] : '';
+	$nonce  = !empty( $_POST['notifications_bulk_nonce'] ) ? $_POST['notifications_bulk_nonce'] : '';
+	$notifications = !empty( $_POST['notifications'] ) ? $_POST['notifications'] : '';
+
+	// Bail if no action or no IDs.
+	if ( ( ! in_array( $action, array( 'delete', 'read', 'unread' ) ) ) || empty( $notifications ) || empty( $nonce ) ) {
+		return false;
+	}
+
+	// Check the nonce.
+	if ( ! wp_verify_nonce( $nonce, 'notifications_bulk_nonce' ) ) {
+		bp_core_add_message( __( 'There was a problem managing your notifications.', 'buddypress' ), 'error' );
+		return false;
+	}
+
+	$notifications = wp_parse_id_list( $notifications );
+
+	// Delete, mark as read or unread depending on the user 'action'.
+	switch ( $action ) {
+		case 'delete' :
+			foreach ( $notifications as $notification ) {
+				bp_notifications_delete_notification( $notification );
+			}
+			bp_core_add_message( __( 'Notifications deleted.', 'buddypress' ) );
+		break;
+
+		case 'read' :
+			foreach ( $notifications as $notification ) {
+				bp_notifications_mark_notification( $notification, false );
+			}
+			bp_core_add_message( __( 'Notifications marked as read', 'buddypress' ) );
+		break;
+
+		case 'unread' :
+			foreach ( $notifications as $notification ) {
+				bp_notifications_mark_notification( $notification, true );
+			}
+			bp_core_add_message( __( 'Notifications marked as unread.', 'buddypress' ) );
+		break;
+	}
+
+	// Redirect.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_notifications_slug() . '/' . bp_current_action() . '/' );
+}
+add_action( 'bp_actions', 'bp_notifications_action_bulk_manage' );
Index: /tags/4.0.0-beta1/src/bp-notifications/actions/delete.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/actions/delete.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/actions/delete.php	(revision 12265)
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Notifications: Delete action handler
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handle deleting single notifications.
+ *
+ * @since 1.9.0
+ *
+ * @return bool
+ */
+function bp_notifications_action_delete() {
+
+	// Bail if not the read or unread screen.
+	if ( ! bp_is_notifications_component() || ! ( bp_is_current_action( 'read' ) || bp_is_current_action( 'unread' ) ) ) {
+		return false;
+	}
+
+	// Get the action.
+	$action = !empty( $_GET['action']          ) ? $_GET['action']          : '';
+	$nonce  = !empty( $_GET['_wpnonce']        ) ? $_GET['_wpnonce']        : '';
+	$id     = !empty( $_GET['notification_id'] ) ? $_GET['notification_id'] : '';
+
+	// Bail if no action or no ID.
+	if ( ( 'delete' !== $action ) || empty( $id ) || empty( $nonce ) ) {
+		return false;
+	}
+
+	// Check the nonce and delete the notification.
+	if ( bp_verify_nonce_request( 'bp_notification_delete_' . $id ) && bp_notifications_delete_notification( $id ) ) {
+		bp_core_add_message( __( 'Notification successfully deleted.',              'buddypress' )          );
+	} else {
+		bp_core_add_message( __( 'There was a problem deleting that notification.', 'buddypress' ), 'error' );
+	}
+
+	// Redirect.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_notifications_slug() . '/' . bp_current_action() . '/' );
+}
+add_action( 'bp_actions', 'bp_notifications_action_delete' );
Index: /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-adminbar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-adminbar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-adminbar.php	(revision 12265)
@@ -0,0 +1,63 @@
+<?php
+/**
+ * BuddyPress Notifications Admin Bar functions.
+ *
+ * Admin Bar functions for the Notifications component.
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsToolbar
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Build the "Notifications" dropdown.
+ *
+ * @since 1.9.0
+ *
+ * @return bool
+ */
+function bp_notifications_toolbar_menu() {
+	global $wp_admin_bar;
+
+	if ( ! is_user_logged_in() ) {
+		return false;
+	}
+
+	$notifications = bp_notifications_get_notifications_for_user( bp_loggedin_user_id(), 'object' );
+	$count         = ! empty( $notifications ) ? count( $notifications ) : 0;
+	$alert_class   = (int) $count > 0 ? 'pending-count alert' : 'count no-alert';
+	$menu_title    = '<span id="ab-pending-notifications" class="' . $alert_class . '">' . number_format_i18n( $count ) . '</span>';
+	$menu_link     = trailingslashit( bp_loggedin_user_domain() . bp_get_notifications_slug() );
+
+	// Add the top-level Notifications button.
+	$wp_admin_bar->add_menu( array(
+		'parent'    => 'top-secondary',
+		'id'        => 'bp-notifications',
+		'title'     => $menu_title,
+		'href'      => $menu_link,
+	) );
+
+	if ( ! empty( $notifications ) ) {
+		foreach ( (array) $notifications as $notification ) {
+			$wp_admin_bar->add_menu( array(
+				'parent' => 'bp-notifications',
+				'id'     => 'notification-' . $notification->id,
+				'title'  => $notification->content,
+				'href'   => $notification->href,
+			) );
+		}
+	} else {
+		$wp_admin_bar->add_menu( array(
+			'parent' => 'bp-notifications',
+			'id'     => 'no-notifications',
+			'title'  => __( 'No new notifications', 'buddypress' ),
+			'href'   => $menu_link,
+		) );
+	}
+
+	return;
+}
+add_action( 'admin_bar_menu', 'bp_members_admin_bar_notifications_menu', 90 );
Index: /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-cache.php	(revision 12265)
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Functions related to notifications caching.
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsCache
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Slurp up metadata for a set of notifications.
+ *
+ * It grabs all notification meta associated with all of the notifications
+ * passed in $notification_ids and adds it to WP cache. This improves efficiency
+ * when using notification meta within a loop context.
+ *
+ * @since 2.3.0
+ *
+ * @param int|string|array|bool $notification_ids Accepts a single notification_id, or a
+ *                                                comma-separated list or array of
+ *                                                notification ids.
+ */
+function bp_notifications_update_meta_cache( $notification_ids = false ) {
+	bp_update_meta_cache( array(
+		'object_ids'       => $notification_ids,
+		'object_type'      => buddypress()->notifications->id,
+		'cache_group'      => 'notification_meta',
+		'object_column'    => 'notification_id',
+		'meta_table'       => buddypress()->notifications->table_name_meta,
+		'cache_key_prefix' => 'bp_notifications_meta'
+	) );
+}
+
+/**
+ * Clear all notifications cache for a given user ID.
+ *
+ * @since 2.3.0
+ *
+ * @param int $user_id The user ID's cache to clear.
+ */
+function bp_notifications_clear_all_for_user_cache( $user_id = 0 ) {
+	wp_cache_delete( 'all_for_user_' . $user_id, 'bp_notifications' );
+	wp_cache_delete( $user_id, 'bp_notifications_unread_count' );
+	wp_cache_delete( $user_id, 'bp_notifications_grouped_notifications' );
+}
+
+/**
+ * Invalidate 'all_for_user_' cache when saving.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_Notifications_Notification $n Notification object.
+ */
+function bp_notifications_clear_all_for_user_cache_after_save( BP_Notifications_Notification $n ) {
+	bp_notifications_clear_all_for_user_cache( $n->user_id );
+}
+add_action( 'bp_notification_after_save', 'bp_notifications_clear_all_for_user_cache_after_save' );
+
+/**
+ * Invalidate the 'all_for_user_' cache when deleting.
+ *
+ * @since 2.0.0
+ *
+ * @param int $args Notification deletion arguments.
+ */
+function bp_notifications_clear_all_for_user_cache_before_delete( $args ) {
+
+	// Pull up a list of items matching the args (those about te be deleted).
+	$ns = BP_Notifications_Notification::get( $args );
+
+	$user_ids = array();
+	foreach ( $ns as $n ) {
+		$user_ids[] = $n->user_id;
+	}
+
+	$user_ids = array_unique( $user_ids );
+	foreach ( $user_ids as $user_id ) {
+		bp_notifications_clear_all_for_user_cache( $user_id );
+	}
+}
+add_action( 'bp_notification_before_delete', 'bp_notifications_clear_all_for_user_cache_before_delete' );
+
+/**
+ * Invalidates 'all_for_user_' cache when updating.
+ *
+ * @since 2.3.0
+ *
+ * @param array $update_args See BP_Notifications_Notification::update() for description.
+ * @param array $where_args  See BP_Notifications_Notification::update() for description.
+ */
+function bp_notifications_clear_all_for_user_cache_before_update( $update_args, $where_args ) {
+
+	// User ID is passed in where arugments.
+	if ( ! empty( $where_args['user_id'] ) ) {
+		bp_notifications_clear_all_for_user_cache( $where_args['user_id'] );
+
+	// Get user ID from Notification ID.
+	} elseif ( ! empty( $where_args['id'] ) ) {
+		$n = bp_notifications_get_notification( $where_args['id'] );
+		bp_notifications_clear_all_for_user_cache( $n->user_id );
+	}
+}
+add_action( 'bp_notification_before_update', 'bp_notifications_clear_all_for_user_cache_before_update', 10, 2 );
Index: /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-filters.php	(revision 12265)
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Filters related to the Activity component.
+ *
+ * @package BuddyPress
+ * @subpackage Notifications
+ * @since 4.0.0
+ */
+
+// Privacy data export.
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_register_notifications_personal_data_exporter' );
+
+/**
+ * Register Notifications personal data exporter.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_register_notifications_personal_data_exporter( $exporters ) {
+	$exporters['buddypress-notifications'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Notifications Data', 'buddypress' ),
+		'callback'               => 'bp_notifications_personal_data_exporter',
+	);
+
+	return $exporters;
+}
Index: /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-functions.php	(revision 12265)
@@ -0,0 +1,890 @@
+<?php
+/**
+ * BuddyPress Member Notifications Functions.
+ *
+ * Functions and filters used in the Notifications component.
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsFunctions
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Add a notification for a specific user, from a specific component.
+ *
+ * @since 1.9.0
+ *
+ * @param array $args {
+ *     Array of arguments describing the notification. All are optional.
+ *     @type int    $user_id           ID of the user to associate the notification with.
+ *     @type int    $item_id           ID of the item to associate the notification with.
+ *     @type int    $secondary_item_id ID of the secondary item to associate the
+ *                                     notification with.
+ *     @type string $component_name    Name of the component to associate the
+ *                                     notification with.
+ *     @type string $component_action  Name of the action to associate the
+ *                                     notification with.
+ *     @type string $date_notified     Timestamp for the notification.
+ * }
+ * @return int|bool ID of the newly created notification on success, false
+ *                  on failure.
+ */
+function bp_notifications_add_notification( $args = array() ) {
+
+	$r = bp_parse_args( $args, array(
+		'user_id'           => 0,
+		'item_id'           => 0,
+		'secondary_item_id' => 0,
+		'component_name'    => '',
+		'component_action'  => '',
+		'date_notified'     => bp_core_current_time(),
+		'is_new'            => 1,
+		'allow_duplicate'   => false,
+	), 'notifications_add_notification' );
+
+	// Check for existing duplicate notifications.
+	if ( ! $r['allow_duplicate'] ) {
+		// Date_notified, allow_duplicate don't count toward
+		// duplicate status.
+		$existing = BP_Notifications_Notification::get( array(
+			'user_id'           => $r['user_id'],
+			'item_id'           => $r['item_id'],
+			'secondary_item_id' => $r['secondary_item_id'],
+			'component_name'    => $r['component_name'],
+			'component_action'  => $r['component_action'],
+			'is_new'            => $r['is_new'],
+		) );
+
+		if ( ! empty( $existing ) ) {
+			return false;
+		}
+	}
+
+	// Setup the new notification.
+	$notification                    = new BP_Notifications_Notification;
+	$notification->user_id           = $r['user_id'];
+	$notification->item_id           = $r['item_id'];
+	$notification->secondary_item_id = $r['secondary_item_id'];
+	$notification->component_name    = $r['component_name'];
+	$notification->component_action  = $r['component_action'];
+	$notification->date_notified     = $r['date_notified'];
+	$notification->is_new            = $r['is_new'];
+
+	// Save the new notification.
+	return $notification->save();
+}
+
+/**
+ * Get a specific notification by its ID.
+ *
+ * @since 1.9.0
+ *
+ * @param int $id ID of the notification.
+ * @return BP_Notifications_Notification Notification object for ID specified.
+ */
+function bp_notifications_get_notification( $id ) {
+	return new BP_Notifications_Notification( $id );
+}
+
+/**
+ * Delete a specific notification by its ID.
+ *
+ * @since 1.9.0
+ *
+ * @param int $id ID of the notification to delete.
+ * @return false|int True on success, false on failure.
+ */
+function bp_notifications_delete_notification( $id ) {
+	if ( ! bp_notifications_check_notification_access( bp_displayed_user_id(), $id ) ) {
+		return false;
+	}
+
+	return BP_Notifications_Notification::delete( array( 'id' => $id ) );
+}
+
+/**
+ * Mark notification read/unread for a user by ID.
+ *
+ * Used when clearing out notifications for a specific notification item.
+ *
+ * @since 1.9.0
+ *
+ * @param int      $id     ID of the notification.
+ * @param int|bool $is_new 0 for read, 1 for unread.
+ * @return false|int True on success, false on failure.
+ */
+function bp_notifications_mark_notification( $id, $is_new = false ) {
+	if ( ! bp_notifications_check_notification_access( bp_displayed_user_id(), $id ) ) {
+		return false;
+	}
+
+	return BP_Notifications_Notification::update(
+		array( 'is_new' => $is_new ),
+		array( 'id'     => $id     )
+	);
+}
+
+/**
+ * Get all notifications for a user and cache them.
+ *
+ * @since 2.1.0
+ *
+ * @param int $user_id ID of the user whose notifications are being fetched.
+ * @return array $notifications Array of notifications for user.
+ */
+function bp_notifications_get_all_notifications_for_user( $user_id = 0 ) {
+
+	// Default to displayed user if no ID is passed.
+	if ( empty( $user_id ) ) {
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+	}
+
+	// Get notifications out of the cache, or query if necessary.
+	$notifications = wp_cache_get( 'all_for_user_' . $user_id, 'bp_notifications' );
+	if ( false === $notifications ) {
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $user_id
+		) );
+		wp_cache_set( 'all_for_user_' . $user_id, $notifications, 'bp_notifications' );
+	}
+
+	/**
+	 * Filters all notifications for a user.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $notifications Array of notifications for user.
+	 * @param int   $user_id       ID of the user being fetched.
+	 */
+	return apply_filters( 'bp_notifications_get_all_notifications_for_user', $notifications, $user_id );
+}
+
+/**
+ * Get a user's unread notifications, grouped by component and action.
+ *
+ * This function returns a list of notifications collapsed by component + action.
+ * See BP_Notifications_Notification::get_grouped_notifications_for_user() for
+ * more details.
+ *
+ * @since 3.0.0
+ *
+ * @param int $user_id ID of the user whose notifications are being fetched.
+ * @return array $notifications
+ */
+function bp_notifications_get_grouped_notifications_for_user( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+	}
+
+	$notifications = wp_cache_get( $user_id, 'bp_notifications_grouped_notifications' );
+	if ( false === $notifications ) {
+		$notifications = BP_Notifications_Notification::get_grouped_notifications_for_user( $user_id );
+		wp_cache_set( $user_id, $notifications, 'bp_notifications_grouped_notifications' );
+	}
+
+	return $notifications;
+}
+
+/**
+ * Get notifications for a specific user.
+ *
+ * @since 1.9.0
+ *
+ * @param int    $user_id ID of the user whose notifications are being fetched.
+ * @param string $format  Format of the returned values. 'string' returns HTML,
+ *                        while 'object' returns a structured object for parsing.
+ * @return mixed Object or array on success, false on failure.
+ */
+function bp_notifications_get_notifications_for_user( $user_id, $format = 'string' ) {
+	$bp = buddypress();
+
+	$notifications = bp_notifications_get_grouped_notifications_for_user( $user_id );
+
+	// Calculate a renderable output for each notification type.
+	foreach ( $notifications as $notification_item ) {
+
+		$component_name = $notification_item->component_name;
+		// We prefer that extended profile component-related notifications use
+		// the component_name of 'xprofile'. However, the extended profile child
+		// object in the $bp object is keyed as 'profile', which is where we need
+		// to look for the registered notification callback.
+		if ( 'xprofile' == $notification_item->component_name ) {
+			$component_name = 'profile';
+		}
+
+		// Callback function exists.
+		if ( isset( $bp->{$component_name}->notification_callback ) && is_callable( $bp->{$component_name}->notification_callback ) ) {
+
+			// Function should return an object.
+			if ( 'object' === $format ) {
+
+				// Retrieve the content of the notification using the callback.
+				$content = call_user_func( $bp->{$component_name}->notification_callback, $notification_item->component_action, $notification_item->item_id, $notification_item->secondary_item_id, $notification_item->total_count, 'array', $notification_item->id );
+
+				// Create the object to be returned.
+				$notification_object = $notification_item;
+
+				// Minimal backpat with non-compatible notification
+				// callback functions.
+				if ( is_string( $content ) ) {
+					$notification_object->content = $content;
+					$notification_object->href    = bp_loggedin_user_domain();
+				} else {
+					$notification_object->content = $content['text'];
+					$notification_object->href    = $content['link'];
+				}
+
+				$renderable[] = $notification_object;
+
+				// Return an array of content strings.
+			} else {
+				$content      = call_user_func( $bp->{$component_name}->notification_callback, $notification_item->component_action, $notification_item->item_id, $notification_item->secondary_item_id, $notification_item->total_count, 'string', $notification_item->id );
+				$renderable[] = $content;
+			}
+
+			// @deprecated format_notification_function - 1.5
+		} elseif ( isset( $bp->{$component_name}->format_notification_function ) && function_exists( $bp->{$component_name}->format_notification_function ) ) {
+			$renderable[] = call_user_func( $bp->{$component_name}->notification_callback, $notification_item->component_action, $notification_item->item_id, $notification_item->secondary_item_id, $notification_item->total_count );
+
+			// Allow non BuddyPress components to hook in.
+		} else {
+
+			// The array to reference with apply_filters_ref_array().
+			$ref_array = array(
+				$notification_item->component_action,
+				$notification_item->item_id,
+				$notification_item->secondary_item_id,
+				$notification_item->total_count,
+				$format,
+				$notification_item->component_action, // Duplicated so plugins can check the canonical action name.
+				$component_name,
+				$notification_item->id,
+			);
+
+			// Function should return an object.
+			if ( 'object' === $format ) {
+
+				/**
+				 * Filters the notification content for notifications created by plugins.
+				 * If your plugin extends the {@link BP_Component} class, you should use the
+				 * 'notification_callback' parameter in your extended
+				 * {@link BP_Component::setup_globals()} method instead.
+				 *
+				 * @since 1.9.0
+				 * @since 2.6.0 Added $component_action_name, $component_name, $id as parameters.
+				 *
+				 * @param string $content               Component action. Deprecated. Do not do checks against this! Use
+				 *                                      the 6th parameter instead - $component_action_name.
+				 * @param int    $item_id               Notification item ID.
+				 * @param int    $secondary_item_id     Notification secondary item ID.
+				 * @param int    $action_item_count     Number of notifications with the same action.
+				 * @param string $format                Format of return. Either 'string' or 'object'.
+				 * @param string $component_action_name Canonical notification action.
+				 * @param string $component_name        Notification component ID.
+				 * @param int    $id                    Notification ID.
+				 *
+				 * @return string|array If $format is 'string', return a string of the notification content.
+				 *                      If $format is 'object', return an array formatted like:
+				 *                      array( 'text' => 'CONTENT', 'link' => 'LINK' )
+				 */
+				$content = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
+
+				// Create the object to be returned.
+				$notification_object = $notification_item;
+
+				// Minimal backpat with non-compatible notification
+				// callback functions.
+				if ( is_string( $content ) ) {
+					$notification_object->content = $content;
+					$notification_object->href    = bp_loggedin_user_domain();
+				} else {
+					$notification_object->content = $content['text'];
+					$notification_object->href    = $content['link'];
+				}
+
+				$renderable[] = $notification_object;
+
+				// Return an array of content strings.
+			} else {
+
+				/** This filters is documented in bp-notifications/bp-notifications-functions.php */
+				$renderable[] = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
+			}
+		}
+	}
+
+	// If renderable is empty array, set to false.
+	if ( empty( $renderable ) ) {
+		$renderable = false;
+	}
+
+	/**
+	 * Filters the final array of notifications to be displayed for a user.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array|bool $renderable Array of notifications to render or false if no notifications.
+	 * @param int        $user_id    ID of the user whose notifications are being displayed.
+	 * @param string     $format     Display format requested for the notifications.
+	 */
+	return apply_filters( 'bp_core_get_notifications_for_user', $renderable, $user_id, $format );
+}
+
+/** Delete ********************************************************************/
+
+/**
+ * Delete notifications for a user by type.
+ *
+ * Used when clearing out notifications for a specific component when the user
+ * has visited that component.
+ *
+ * @since 1.9.0
+ *
+ * @param int    $user_id          ID of the user whose notifications are being deleted.
+ * @param string $component_name   Name of the associated component.
+ * @param string $component_action Name of the associated action.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_delete_notifications_by_type( $user_id, $component_name, $component_action ) {
+	return BP_Notifications_Notification::delete( array(
+		'user_id'          => $user_id,
+		'component_name'   => $component_name,
+		'component_action' => $component_action,
+	) );
+}
+
+/**
+ * Delete notifications for an item ID.
+ *
+ * Used when clearing out notifications for a specific component when the user
+ * has visited that component.
+ *
+ * @since 1.9.0
+ *
+ * @param int      $user_id           ID of the user whose notifications are being deleted.
+ * @param int      $item_id           ID of the associated item.
+ * @param string   $component_name    Name of the associated component.
+ * @param string   $component_action  Name of the associated action.
+ * @param int|bool $secondary_item_id ID of the secondary associated item.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_delete_notifications_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id = false ) {
+	return BP_Notifications_Notification::delete( array(
+		'user_id'           => $user_id,
+		'item_id'           => $item_id,
+		'secondary_item_id' => $secondary_item_id,
+		'component_name'    => $component_name,
+		'component_action'  => $component_action,
+	) );
+}
+
+/**
+ * Delete all notifications by type.
+ *
+ * Used when clearing out notifications for an entire component.
+ *
+ * @since 1.9.0
+ *
+ * @param int         $item_id           ID of the user whose notifications are being deleted.
+ * @param string      $component_name    Name of the associated component.
+ * @param string|bool $component_action  Optional. Name of the associated action.
+ * @param int|bool    $secondary_item_id Optional. ID of the secondary associated item.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_delete_all_notifications_by_type( $item_id, $component_name, $component_action = false, $secondary_item_id = false ) {
+	return BP_Notifications_Notification::delete( array(
+		'item_id'           => $item_id,
+		'secondary_item_id' => $secondary_item_id,
+		'component_name'    => $component_name,
+		'component_action'  => $component_action,
+	) );
+}
+
+/**
+ * Delete all notifications from a user.
+ *
+ * Used when clearing out all notifications for a user, when deleted or spammed.
+ *
+ * @todo This function assumes that items with the user_id in the item_id slot
+ *       are associated with that user. However, this will only be true with
+ *       certain components (such as Friends). Use with caution!
+ *
+ * @since 1.9.0
+ *
+ * @param int    $user_id          ID of the user whose associated items are being deleted.
+ * @param string $component_name   Name of the associated component.
+ * @param string $component_action Name of the associated action.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_delete_notifications_from_user( $user_id, $component_name, $component_action ) {
+	return BP_Notifications_Notification::delete( array(
+		'item_id'           => $user_id,
+		'component_name'    => $component_name,
+		'component_action'  => $component_action,
+	) );
+}
+
+/**
+ * Delete a user's notifications when the user is deleted.
+ *
+ * @since 2.5.0
+ *
+ * @param int $user_id ID of the user who is about to be deleted.
+ * @return int|false The number of rows deleted, or false on error.
+ */
+function bp_notifications_delete_notifications_on_user_delete( $user_id ) {
+	return BP_Notifications_Notification::delete( array(
+		'user_id'           => $user_id,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'component_action'  => false,
+		'component_name'    => false,
+	) );
+}
+add_action( 'wpmu_delete_user', 'bp_notifications_delete_notifications_on_user_delete' );
+add_action( 'delete_user', 'bp_notifications_delete_notifications_on_user_delete' );
+
+/** Mark **********************************************************************/
+
+/**
+ * Mark notifications read/unread for a user by type.
+ *
+ * Used when clearing out notifications for a specific component when the user
+ * has visited that component.
+ *
+ * @since 1.9.0
+ *
+ * @param int      $user_id          ID of the user whose notifications are being deleted.
+ * @param string   $component_name   Name of the associated component.
+ * @param string   $component_action Name of the associated action.
+ * @param int|bool $is_new           0 for read, 1 for unread.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_mark_notifications_by_type( $user_id, $component_name, $component_action, $is_new = false ) {
+	return BP_Notifications_Notification::update(
+		array(
+			'is_new' => $is_new
+		),
+		array(
+			'user_id'          => $user_id,
+			'component_name'   => $component_name,
+			'component_action' => $component_action
+		)
+	);
+}
+
+/**
+ * Mark notifications read/unread for an item ID.
+ *
+ * Used when clearing out notifications for a specific component when the user
+ * has visited that component.
+ *
+ * @since 1.9.0
+ *
+ * @param int      $user_id           ID of the user whose notifications are being deleted.
+ * @param int      $item_id           ID of the associated item.
+ * @param string   $component_name    Name of the associated component.
+ * @param string   $component_action  Name of the associated action.
+ * @param int|bool $secondary_item_id ID of the secondary associated item.
+ * @param int|bool $is_new            0 for read, 1 for unread.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_mark_notifications_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id = false, $is_new = false ) {
+	return BP_Notifications_Notification::update(
+		array(
+			'is_new' => $is_new
+		),
+		array(
+			'user_id'           => $user_id,
+			'item_id'           => $item_id,
+			'secondary_item_id' => $secondary_item_id,
+			'component_name'    => $component_name,
+			'component_action'  => $component_action
+		)
+	);
+}
+
+/**
+ * Mark all notifications read/unread by type.
+ *
+ * Used when clearing out notifications for an entire component.
+ *
+ * @since 1.9.0
+ *
+ * @param int         $item_id           ID of the user whose notifications are being deleted.
+ * @param string      $component_name    Name of the associated component.
+ * @param string|bool $component_action  Optional. Name of the associated action.
+ * @param int|bool    $secondary_item_id Optional. ID of the secondary associated item.
+ * @param int|bool    $is_new            0 for read, 1 for unread.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_mark_all_notifications_by_type( $item_id, $component_name, $component_action = false, $secondary_item_id = false, $is_new = false ) {
+	return BP_Notifications_Notification::update(
+		array(
+			'is_new' => $is_new
+		),
+		array(
+			'item_id'           => $item_id,
+			'secondary_item_id' => $secondary_item_id,
+			'component_name'    => $component_name,
+			'component_action'  => $component_action
+		)
+	);
+}
+
+/**
+ * Mark all notifications read/unread from a user.
+ *
+ * Used when clearing out all notifications for a user, when deleted or spammed.
+ *
+ * @todo This function assumes that items with the user_id in the item_id slot
+ *       are associated with that user. However, this will only be true with
+ *       certain components (such as Friends). Use with caution!
+ *
+ * @since 1.9.0
+ *
+ * @param int      $user_id          ID of the user whose associated items are being deleted.
+ * @param string   $component_name   Name of the associated component.
+ * @param string   $component_action Name of the associated action.
+ * @param int|bool $is_new           0 for read, 1 for unread.
+ * @return int|false True on success, false on failure.
+ */
+function bp_notifications_mark_notifications_from_user( $user_id, $component_name, $component_action, $is_new = false ) {
+	return BP_Notifications_Notification::update(
+		array(
+			'is_new' => $is_new
+		),
+		array(
+			'item_id'          => $user_id,
+			'component_name'   => $component_name,
+			'component_action' => $component_action
+		)
+	);
+}
+
+/** Helpers *******************************************************************/
+
+/**
+ * Check if a user has access to a specific notification.
+ *
+ * Used before deleting a notification for a user.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id         ID of the user being checked.
+ * @param int $notification_id ID of the notification being checked.
+ * @return bool True if the notification belongs to the user, otherwise false.
+ */
+function bp_notifications_check_notification_access( $user_id, $notification_id ) {
+	return (bool) BP_Notifications_Notification::check_access( $user_id, $notification_id );
+}
+
+/**
+ * Get a count of unread notification items for a user.
+ *
+ * @since 1.9.0
+ *
+ * @param int $user_id ID of the user whose unread notifications are being
+ *                     counted.
+ * @return int Unread notification count.
+ */
+function bp_notifications_get_unread_notification_count( $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+	}
+
+	$count = wp_cache_get( $user_id, 'bp_notifications_unread_count' );
+	if ( false === $count ) {
+		$count = BP_Notifications_Notification::get_total_count( array(
+			'user_id' => $user_id,
+			'is_new'  => true,
+		) );
+		wp_cache_set( $user_id, $count, 'bp_notifications_unread_count' );
+	}
+
+	/**
+	 * Filters the count of unread notification items for a user.
+	 *
+	 * @since 1.9.0
+	 * @since 2.7.0 Added user ID parameter.
+	 *
+	 * @param int $count   Count of unread notification items for a user.
+	 * @param int $user_id User ID for notifications count.
+	 */
+	return apply_filters( 'bp_notifications_get_total_notification_count', (int) $count, $user_id );
+}
+
+/**
+ * Return an array of component names that are currently active and have
+ * registered Notifications callbacks.
+ *
+ * @since 1.9.1
+ *
+ * @see http://buddypress.trac.wordpress.org/ticket/5300
+ *
+ * @return array $component_names Array of registered components.
+ */
+function bp_notifications_get_registered_components() {
+
+	// Load BuddyPress.
+	$bp = buddypress();
+
+	// Setup return value.
+	$component_names = array();
+
+	// Get the active components.
+	$active_components = array_keys( $bp->active_components );
+
+	// Loop through components, look for callbacks, add to return value.
+	foreach ( $active_components as $component ) {
+		if ( !empty( $bp->$component->notification_callback ) ) {
+			$component_names[] = $component;
+		}
+		// The extended profile component is identified in the active_components array as 'xprofile'.
+		// However, the extended profile child object has the key 'profile' in the $bp object.
+		if ( 'xprofile' == $component && ! empty( $bp->profile->notification_callback ) ) {
+			$component_names[] = $component;
+		}
+	}
+
+	/**
+	 * Filters active components with registered notifications callbacks.
+	 *
+	 * @since 1.9.1
+	 *
+	 * @param array $component_names   Array of registered component names.
+	 * @param array $active_components Array of active components.
+	 */
+	return apply_filters( 'bp_notifications_get_registered_components', $component_names, $active_components );
+}
+
+/**
+ * Catch and route the 'settings' notifications screen.
+ *
+ * This is currently unused.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_screen_settings() {}
+
+/** Meta **********************************************************************/
+
+/**
+ * Delete a meta entry from the DB for a notification item.
+ *
+ * @since 2.3.0
+ *
+ * @global object $wpdb WordPress database access object.
+ *
+ * @param int    $notification_id ID of the notification item whose metadata is being deleted.
+ * @param string $meta_key        Optional. The key of the metadata being deleted. If
+ *                                omitted, all metadata associated with the notification
+ *                                item will be deleted.
+ * @param string $meta_value      Optional. If present, the metadata will only be
+ *                                deleted if the meta_value matches this parameter.
+ * @param bool   $delete_all      Optional. If true, delete matching metadata entries
+ *                                for all objects, ignoring the specified object_id. Otherwise,
+ *                                only delete matching metadata entries for the specified
+ *                                notification item. Default: false.
+ * @return bool                   True on success, false on failure.
+ */
+function bp_notifications_delete_meta( $notification_id, $meta_key = '', $meta_value = '', $delete_all = false ) {
+
+	// Legacy - if no meta_key is passed, delete all for the item.
+	if ( empty( $meta_key ) ) {
+		$all_meta = bp_notifications_get_meta( $notification_id );
+		$keys     = ! empty( $all_meta )
+			? array_keys( $all_meta )
+			: array();
+
+		// With no meta_key, ignore $delete_all.
+		$delete_all = false;
+	} else {
+		$keys = array( $meta_key );
+	}
+
+	$retval = true;
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	foreach ( $keys as $key ) {
+		$retval = delete_metadata( 'notification', $notification_id, $key, $meta_value, $delete_all );
+	}
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Get metadata for a given notification item.
+ *
+ * @since 2.3.0
+ *
+ * @param int    $notification_id ID of the notification item whose metadata is being requested.
+ * @param string $meta_key        Optional. If present, only the metadata matching
+ *                                that meta key will be returned. Otherwise, all metadata for the
+ *                                notification item will be fetched.
+ * @param bool   $single          Optional. If true, return only the first value of the
+ *                                specified meta_key. This parameter has no effect if meta_key is not
+ *                                specified. Default: true.
+ * @return mixed                  The meta value(s) being requested.
+ */
+function bp_notifications_get_meta( $notification_id = 0, $meta_key = '', $single = true ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = get_metadata( 'notification', $notification_id, $meta_key, $single );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	/**
+	 * Filters the metadata for a specified notification item.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param mixed  $retval          The meta values for the notification item.
+	 * @param int    $notification_id ID of the notification item.
+	 * @param string $meta_key        Meta key for the value being requested.
+	 * @param bool   $single          Whether to return one matched meta key row or all.
+	 */
+	return apply_filters( 'bp_notifications_get_meta', $retval, $notification_id, $meta_key, $single );
+}
+
+/**
+ * Update a piece of notification meta.
+ *
+ * @since 1.2.0
+ *
+ * @param  int    $notification_id ID of the notification item whose metadata is being
+ *                                 updated.
+ * @param  string $meta_key        Key of the metadata being updated.
+ * @param  mixed  $meta_value      Value to be set.
+ * @param  mixed  $prev_value      Optional. If specified, only update existing
+ *                                 metadata entries with the specified value.
+ *                                 Otherwise, update all entries.
+ * @return bool|int                Returns false on failure. On successful
+ *                                 update of existing metadata, returns true. On
+ *                                 successful creation of new metadata,  returns
+ *                                 the integer ID of the new metadata row.
+ */
+function bp_notifications_update_meta( $notification_id, $meta_key, $meta_value, $prev_value = '' ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = update_metadata( 'notification', $notification_id, $meta_key, $meta_value, $prev_value );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Add a piece of notification metadata.
+ *
+ * @since 2.3.0
+ *
+ * @param int    $notification_id ID of the notification item.
+ * @param string $meta_key        Metadata key.
+ * @param mixed  $meta_value      Metadata value.
+ * @param bool   $unique          Optional. Whether to enforce a single metadata value
+ *                                for the given key. If true, and the object already has a value for
+ *                                the key, no change will be made. Default: false.
+ * @return int|bool               The meta ID on successful update, false on failure.
+ */
+function bp_notifications_add_meta( $notification_id, $meta_key, $meta_value, $unique = false ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	$retval = add_metadata( 'notification', $notification_id, $meta_key, $meta_value, $unique );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Finds and exports personal data associated with an email address from the Notifications tables.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The users email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_notifications_personal_data_exporter( $email_address, $page ) {
+	$number = 50;
+
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$notifications = BP_Notifications_Notification::get( array(
+		'is_new'   => null,
+		'per_page' => $number,
+		'page'     => $page,
+		'user_id'  => $user->ID,
+		'order'    => 'DESC',
+	) );
+
+	$user_data_to_export = array();
+
+	foreach ( $notifications as $notification ) {
+		if ( 'xprofile' === $notification->component_name ) {
+			$component_name = 'profile';
+		} else {
+			$component_name = $notification->component_name;
+		}
+
+		// Format notifications.
+		if ( isset( buddypress()->{$component_name}->notification_callback ) && is_callable( buddypress()->{$component_name}->notification_callback ) ) {
+			$content = call_user_func( buddypress()->{$component_name}->notification_callback, $notification->component_action, $notification->item_id, $notification->secondary_item_id, 1, 'string', $notification->id );
+		} else {
+			// The array to reference with apply_filters_ref_array().
+			$ref_array = array(
+				$notification->component_action,
+				$notification->item_id,
+				$notification->secondary_item_id,
+				$notification->total_count,
+				'string',
+				$notification->component_action,
+				$component_name,
+				$notification_item->id,
+			);
+
+			/** This filter is documented in bp-notifications/bp-notifications-functions.php */
+			$content = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
+		}
+
+		$item_data = array(
+			array(
+				'name'  => __( 'Notification Content', 'buddypress' ),
+				'value' => $content,
+			),
+			array(
+				'name'  => __( 'Notification Date', 'buddypress' ),
+				'value' => $notification->date_notified,
+			),
+			array(
+				'name'  => __( 'Status', 'buddypress' ),
+				'value' => $notification->is_new ? __( 'Unread', 'buddypress' ) : __( 'Read', 'buddypress' ),
+			),
+		);
+
+		$data_to_export[] = array(
+			'group_id'    => 'bp_notifications',
+			'group_label' => __( 'Notifications', 'buddypress' ),
+			'item_id'     => "bp-notifications-{$notification->id}",
+			'data'        => $item_data,
+		);
+	}
+
+	// Tell core if we have more items to process.
+	$done = count( $notifications ) < $number;
+
+	return array(
+		'data' => $data_to_export,
+		'done' => $done,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-loader.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BuddyPress Member Notifications Loader.
+ *
+ * Initializes the Notifications component.
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsLoader
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-notifications component.
+ *
+ * @since 1.9.0
+ */
+function bp_setup_notifications() {
+	buddypress()->notifications = new BP_Notifications_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_notifications', 6 );
Index: /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/bp-notifications-template.php	(revision 12265)
@@ -0,0 +1,1038 @@
+<?php
+/**
+ * BuddyPress Notifications Template Functions.
+ *
+ * @package BuddyPress
+ * @subpackage TonificationsTemplate
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the notifications component slug.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_slug() {
+	echo bp_get_notifications_slug();
+}
+	/**
+	 * Return the notifications component slug.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string Slug of the Notifications component.
+	 */
+	function bp_get_notifications_slug() {
+
+		/**
+		 * Filters the notifications component slug.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $slug Notifications component slug.
+		 */
+		return apply_filters( 'bp_get_notifications_slug', buddypress()->notifications->slug );
+	}
+
+/**
+ * Output the notifications permalink for a user.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_notifications_permalink( $user_id = 0 ) {
+	echo bp_get_notifications_permalink( $user_id );
+}
+	/**
+	 * Return the notifications permalink.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string Notifications permalink.
+	 */
+	function bp_get_notifications_permalink( $user_id = 0 ) {
+		if ( 0 === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+			$domain  = bp_loggedin_user_domain();
+		} else {
+			$domain = bp_core_get_user_domain( (int) $user_id );
+		}
+
+		$retval = trailingslashit( $domain . bp_get_notifications_slug() );
+
+		/**
+		 * Filters the notifications permalink.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  Permalink for the notifications.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_notifications_permalink', $retval, $user_id );
+	}
+
+/**
+ * Output the unread notifications permalink for a user.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_notifications_unread_permalink( $user_id = 0 ) {
+	echo bp_get_notifications_unread_permalink( $user_id );
+}
+	/**
+	 * Return the unread notifications permalink.
+	 *
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string Unread notifications permalink.
+	 */
+	function bp_get_notifications_unread_permalink( $user_id = 0 ) {
+		if ( 0 === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+			$domain  = bp_loggedin_user_domain();
+		} else {
+			$domain = bp_core_get_user_domain( (int) $user_id );
+		}
+
+		$retval = trailingslashit( $domain . bp_get_notifications_slug() . '/unread' );
+
+		/**
+		 * Filters the unread notifications permalink.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  Permalink for the unread notifications.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_notifications_unread_permalink', $retval, $user_id );
+	}
+
+/**
+ * Output the read notifications permalink for a user.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_notifications_read_permalink( $user_id = 0 ) {
+	echo bp_get_notifications_read_permalink( $user_id );
+}
+	/**
+	 * Return the read notifications permalink.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string Read notifications permalink.
+	 */
+	function bp_get_notifications_read_permalink( $user_id = 0 ) {
+		if ( 0 === $user_id ) {
+			$user_id = bp_loggedin_user_id();
+			$domain  = bp_loggedin_user_domain();
+		} else {
+			$domain = bp_core_get_user_domain( (int) $user_id );
+		}
+
+		$retval = trailingslashit( $domain . bp_get_notifications_slug() . '/read' );
+
+		/**
+		 * Filters the read notifications permalink.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  Permalink for the read notifications.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_notifications_unread_permalink', $retval, $user_id );
+	}
+
+/** The Loop ******************************************************************/
+
+/**
+ * Initialize the notifications loop.
+ *
+ * Based on the $args passed, bp_has_notifications() populates
+ * buddypress()->notifications->query_loop global, enabling the use of BP
+ * templates and template functions to display a list of notifications.
+ *
+ * @since 1.9.0
+ *
+ * @param array|string $args {
+ *     Arguments for limiting the contents of the notifications loop. Can be
+ *     passed as an associative array, or as a URL query string.
+ *
+ *     See {@link BP_Notifications_Notification::get()} for detailed
+ *     information on the arguments.  In addition, also supports:
+ *
+ *     @type int    $max      Optional. Max items to display. Default: false.
+ *     @type string $page_arg URL argument to use for pagination.
+ *                            Default: 'npage'.
+ * }
+ * @return bool
+ */
+function bp_has_notifications( $args = '' ) {
+
+	// Get the default is_new argument.
+	if ( bp_is_current_action( 'unread' ) ) {
+		$is_new = 1;
+	} elseif ( bp_is_current_action( 'read' ) ) {
+		$is_new = 0;
+
+	// Not on a notifications page? default to fetch new notifications.
+	} else {
+		$is_new = 1;
+	}
+
+	// Get the user ID.
+	if ( bp_displayed_user_id() ) {
+		$user_id = bp_displayed_user_id();
+	} else {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	// Set the component action (by default false to get all actions)
+	$component_action = false;
+
+	if ( isset( $_REQUEST['type'] ) ) {
+		$component_action = sanitize_key( $_REQUEST['type'] );
+	}
+
+	// Set the search terms (by default an empty string to get all notifications)
+	$search_terms = '';
+
+	if ( isset( $_REQUEST['s'] ) ) {
+		$search_terms = stripslashes( $_REQUEST['s'] );
+	}
+
+	// Parse the args.
+	$r = bp_parse_args( $args, array(
+		'id'                => false,
+		'user_id'           => $user_id,
+		'secondary_item_id' => false,
+		'component_name'    => bp_notifications_get_registered_components(),
+		'component_action'  => $component_action,
+		'is_new'            => $is_new,
+		'search_terms'      => $search_terms,
+		'order_by'          => 'date_notified',
+		'sort_order'        => 'DESC',
+		'meta_query'        => false,
+		'date_query'        => false,
+		'page'              => 1,
+		'per_page'          => 25,
+
+		// These are additional arguments that are not available in
+		// BP_Notifications_Notification::get().
+		'max'               => false,
+		'page_arg'          => 'npage',
+	), 'has_notifications' );
+
+	// Get the notifications.
+	$query_loop = new BP_Notifications_Template( $r );
+
+	// Setup the global query loop.
+	buddypress()->notifications->query_loop = $query_loop;
+
+	/**
+	 * Filters whether or not the user has notifications to display.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added the `$r` parameter.
+	 *
+	 * @param bool                      $value      Whether or not there are notifications to display.
+	 * @param BP_Notifications_Template $query_loop BP_Notifications_Template object instance.
+	 * @param array                     $r          Array of arguments passed into the BP_Notifications_Template class.
+	 */
+	return apply_filters( 'bp_has_notifications', $query_loop->has_notifications(), $query_loop, $r );
+}
+
+/**
+ * Get the notifications returned by the template loop.
+ *
+ * @since 1.9.0
+ *
+ * @return array List of notifications.
+ */
+function bp_the_notifications() {
+	return buddypress()->notifications->query_loop->notifications();
+}
+
+/**
+ * Get the current notification object in the loop.
+ *
+ * @since 1.9.0
+ *
+ * @return object The current notification within the loop.
+ */
+function bp_the_notification() {
+	return buddypress()->notifications->query_loop->the_notification();
+}
+
+/** Loop Output ***************************************************************/
+
+/**
+ * Output the ID of the notification currently being iterated on.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_id() {
+	echo bp_get_the_notification_id();
+}
+	/**
+	 * Return the ID of the notification currently being iterated on.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return int ID of the current notification.
+	 */
+	function bp_get_the_notification_id() {
+
+		/**
+		 * Filters the ID of the notification currently being iterated on.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int $id ID of the notification being iterated on.
+		 */
+		return apply_filters( 'bp_get_the_notification_id', buddypress()->notifications->query_loop->notification->id );
+	}
+
+/**
+ * Output the associated item ID of the notification currently being iterated on.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_item_id() {
+	echo bp_get_the_notification_item_id();
+}
+	/**
+	 * Return the associated item ID of the notification currently being iterated on.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return int ID of the item associated with the current notification.
+	 */
+	function bp_get_the_notification_item_id() {
+
+		/**
+		 * Filters the associated item ID of the notification currently being iterated on.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int $item_id ID of the associated item.
+		 */
+		return apply_filters( 'bp_get_the_notification_item_id', buddypress()->notifications->query_loop->notification->item_id );
+	}
+
+/**
+ * Output the secondary associated item ID of the notification currently being iterated on.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_secondary_item_id() {
+	echo bp_get_the_notification_secondary_item_id();
+}
+	/**
+	 * Return the secondary associated item ID of the notification currently being iterated on.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return int ID of the secondary item associated with the current notification.
+	 */
+	function bp_get_the_notification_secondary_item_id() {
+
+		/**
+		 * Filters the secondary associated item ID of the notification currently being iterated on.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int $secondary_item_id ID of the secondary associated item.
+		 */
+		return apply_filters( 'bp_get_the_notification_secondary_item_id', buddypress()->notifications->query_loop->notification->secondary_item_id );
+	}
+
+/**
+ * Output the name of the component associated with the notification currently being iterated on.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_component_name() {
+	echo bp_get_the_notification_component_name();
+}
+	/**
+	 * Return the name of the component associated with the notification currently being iterated on.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return int Name of the component associated with the current notification.
+	 */
+	function bp_get_the_notification_component_name() {
+
+		/**
+		 * Filters the name of the component associated with the notification currently being iterated on.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int $component_name Name of the component associated with the current notification.
+		 */
+		return apply_filters( 'bp_get_the_notification_component_name', buddypress()->notifications->query_loop->notification->component_name );
+	}
+
+/**
+ * Output the name of the action associated with the notification currently being iterated on.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_component_action() {
+	echo bp_get_the_notification_component_action();
+}
+	/**
+	 * Return the name of the action associated with the notification currently being iterated on.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return int Name of the action associated with the current notification.
+	 */
+	function bp_get_the_notification_component_action() {
+
+		/**
+		 * Filters the name of the action associated with the notification currently being iterated on.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param int $component_action Name of the action associated with the current notification.
+		 */
+		return apply_filters( 'bp_get_the_notification_component_action', buddypress()->notifications->query_loop->notification->component_action );
+	}
+
+/**
+ * Output the timestamp of the current notification.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_date_notified() {
+	echo bp_get_the_notification_date_notified();
+}
+	/**
+	 * Return the timestamp of the current notification.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string Timestamp of the current notification.
+	 */
+	function bp_get_the_notification_date_notified() {
+
+		/**
+		 * Filters the timestamp of the current notification.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $date_notified Timestamp of the current notification.
+		 */
+		return apply_filters( 'bp_get_the_notification_date_notified', buddypress()->notifications->query_loop->notification->date_notified );
+	}
+
+/**
+ * Output the timestamp of the current notification.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_time_since() {
+	echo bp_get_the_notification_time_since();
+}
+	/**
+	 * Return the timestamp of the current notification.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string Timestamp of the current notification.
+	 */
+	function bp_get_the_notification_time_since() {
+
+		// Get the notified date.
+		$date_notified = bp_get_the_notification_date_notified();
+
+		// Notified date has legitimate data.
+		if ( '0000-00-00 00:00:00' !== $date_notified ) {
+			$retval = bp_core_time_since( $date_notified );
+
+		// Notified date is empty, so return a fun string.
+		} else {
+			$retval = __( 'Date not found', 'buddypress' );
+		}
+
+		/**
+		 * Filters the time since value of the current notification.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $retval Time since value for current notification.
+		 */
+		return apply_filters( 'bp_get_the_notification_time_since', $retval );
+	}
+
+/**
+ * Output full-text description for a specific notification.
+ *
+ * @since 1.9.0
+ */
+function bp_the_notification_description() {
+	echo bp_get_the_notification_description();
+}
+
+	/**
+	 * Get full-text description for a specific notification.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_notification_description() {
+		$bp           = buddypress();
+		$notification = $bp->notifications->query_loop->notification;
+
+		// Callback function exists.
+		if ( isset( $bp->{ $notification->component_name }->notification_callback ) && is_callable( $bp->{ $notification->component_name }->notification_callback ) ) {
+			$description = call_user_func( $bp->{ $notification->component_name }->notification_callback, $notification->component_action, $notification->item_id, $notification->secondary_item_id, 1, 'string', $notification->id );
+
+		// @deprecated format_notification_function - 1.5
+		} elseif ( isset( $bp->{ $notification->component_name }->format_notification_function ) && function_exists( $bp->{ $notification->component_name }->format_notification_function ) ) {
+			$description = call_user_func( $bp->{ $notification->component_name }->format_notification_function, $notification->component_action, $notification->item_id, $notification->secondary_item_id, 1 );
+
+		// Allow non BuddyPress components to hook in.
+		} else {
+
+			/** This filter is documented in bp-notifications/bp-notifications-functions.php */
+			$description = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', array( $notification->component_action, $notification->item_id, $notification->secondary_item_id, 1, 'string', $notification->component_action, $notification->component_name, $notification->id ) );
+		}
+
+		/**
+		 * Filters the full-text description for a specific notification.
+		 *
+		 * @since 1.9.0
+		 * @since 2.3.0 Added the `$notification` parameter.
+		 *
+		 * @param string $description  Full-text description for a specific notification.
+		 * @param object $notification Notification object.
+		 */
+		return apply_filters( 'bp_get_the_notification_description', $description, $notification );
+	}
+
+/**
+ * Output the mark read link for the current notification.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_mark_read_link( $user_id = 0 ) {
+	echo bp_get_the_notification_mark_read_link( $user_id );
+}
+	/**
+	 * Return the mark read link for the current notification.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_mark_read_link( $user_id = 0 ) {
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		$retval = sprintf( '<a href="%1$s" class="mark-read primary">%2$s</a>', esc_url( bp_get_the_notification_mark_read_url( $user_id ) ), __( 'Read', 'buddypress' ) );
+
+		/**
+		 * Filters the mark read link for the current notification.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  HTML for the mark read link for the current notification.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_mark_read_link', $retval, $user_id );
+	}
+
+/**
+ * Output the URL used for marking a single notification as read.
+ *
+ * Since this function directly outputs a URL, it is escaped.
+ *
+ * @since 2.1.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_mark_read_url( $user_id = 0 ) {
+	echo esc_url( bp_get_the_notification_mark_read_url( $user_id ) );
+}
+	/**
+	 * Return the URL used for marking a single notification as read.
+	 *
+	 * @since 2.1.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_mark_read_url( $user_id = 0 ) {
+
+		// Get the notification ID.
+		$id   = bp_get_the_notification_id();
+
+		// Get the args to add to the URL.
+		$args = array(
+			'action'          => 'read',
+			'notification_id' => $id
+		);
+
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		// Add the args to the URL.
+		$url = add_query_arg( $args, bp_get_notifications_unread_permalink( $user_id ) );
+
+		// Add the nonce.
+		$url = wp_nonce_url( $url, 'bp_notification_mark_read_' . $id );
+
+		/**
+		 * Filters the URL used for marking a single notification as read.
+		 *
+		 * @since 2.1.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $url     URL to use for marking the single notification as read.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_mark_read_url', $url, $user_id );
+	}
+
+/**
+ * Output the mark unread link for the current notification.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_mark_unread_link( $user_id = 0 ) {
+	echo bp_get_the_notification_mark_unread_link( $user_id );
+}
+	/**
+	 * Return the mark unread link for the current notification.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_mark_unread_link( $user_id = 0 ) {
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		$retval = sprintf( '<a href="%1$s" class="mark-unread primary bp-tooltip">%2$s</a>', esc_url( bp_get_the_notification_mark_unread_url( $user_id ) ), __( 'Unread', 'buddypress' ) );
+
+		/**
+		 * Filters the link used for marking a single notification as unread.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  HTML for the mark unread link for the current notification.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_mark_unread_link', $retval, $user_id );
+	}
+
+/**
+ * Output the URL used for marking a single notification as unread.
+ *
+ * Since this function directly outputs a URL, it is escaped.
+ *
+ * @since 2.1.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_mark_unread_url( $user_id = 0 ) {
+	echo esc_url( bp_get_the_notification_mark_unread_url( $user_id ) );
+}
+	/**
+	 * Return the URL used for marking a single notification as unread.
+	 *
+	 * @since 2.1.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_mark_unread_url( $user_id = 0 ) {
+
+		// Get the notification ID.
+		$id   = bp_get_the_notification_id();
+
+		// Get the args to add to the URL.
+		$args = array(
+			'action'          => 'unread',
+			'notification_id' => $id
+		);
+
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		// Add the args to the URL.
+		$url = add_query_arg( $args, bp_get_notifications_read_permalink( $user_id ) );
+
+		// Add the nonce.
+		$url = wp_nonce_url( $url, 'bp_notification_mark_unread_' . $id );
+
+		/**
+		 * Filters the URL used for marking a single notification as unread.
+		 *
+		 * @since 2.1.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $url     URL to use for marking the single notification as unread.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_mark_unread_url', $url, $user_id );
+	}
+
+/**
+ * Output the mark link for the current notification.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_mark_link( $user_id = 0 ) {
+	echo bp_get_the_notification_mark_link( $user_id );
+}
+	/**
+	 * Return the mark link for the current notification.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_mark_link( $user_id = 0 ) {
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		if ( bp_is_current_action( 'read' ) ) {
+			$retval = bp_get_the_notification_mark_unread_link( $user_id );
+		} else {
+			$retval = bp_get_the_notification_mark_read_link( $user_id );
+		}
+
+		/**
+		 * Filters the mark link for the current notification.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  The mark link for the current notification.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_mark_link', $retval, $user_id );
+	}
+
+/**
+ * Output the delete link for the current notification.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_delete_link( $user_id = 0 ) {
+	echo bp_get_the_notification_delete_link( $user_id );
+}
+	/**
+	 * Return the delete link for the current notification.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_delete_link( $user_id = 0 ) {
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		$retval = sprintf( '<a href="%1$s" class="delete secondary confirm bp-tooltip">%2$s</a>', esc_url( bp_get_the_notification_delete_url( $user_id ) ), __( 'Delete', 'buddypress' ) );
+
+		/**
+		 * Filters the delete link for the current notification.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $retval  HTML for the delete link for the current notification.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_delete_link', $retval, $user_id );
+	}
+
+/**
+ * Output the URL used for deleting a single notification.
+ *
+ * Since this function directly outputs a URL, it is escaped.
+ *
+ * @since 2.1.0
+ * @since 2.6.0 Added $user_id as a parameter.
+ *
+ * @param int $user_id The user ID.
+ */
+function bp_the_notification_delete_url( $user_id = 0 ) {
+	echo esc_url( bp_get_the_notification_delete_url( $user_id ) );
+}
+	/**
+	 * Return the URL used for deleting a single notification.
+	 *
+	 * @since 2.1.0
+	 * @since 2.6.0 Added $user_id as a parameter.
+	 *
+	 * @param int $user_id The user ID.
+	 * @return string
+	 */
+	function bp_get_the_notification_delete_url( $user_id = 0 ) {
+		// Set default user ID to use.
+		$user_id = 0 === $user_id ? bp_displayed_user_id() : $user_id;
+
+		// URL to add nonce to.
+		if ( bp_is_current_action( 'unread' ) ) {
+			$link = bp_get_notifications_unread_permalink( $user_id );
+		} elseif ( bp_is_current_action( 'read' ) ) {
+			$link = bp_get_notifications_read_permalink( $user_id );
+		}
+
+		// Get the ID.
+		$id = bp_get_the_notification_id();
+
+		// Get the args to add to the URL.
+		$args = array(
+			'action'          => 'delete',
+			'notification_id' => $id
+		);
+
+		// Add the args.
+		$url = add_query_arg( $args, $link );
+
+		// Add the nonce.
+		$url = wp_nonce_url( $url, 'bp_notification_delete_' . $id );
+
+		/**
+		 * Filters the URL used for deleting a single notification.
+		 *
+		 * @since 2.1.0
+		 * @since 2.6.0 Added $user_id as a parameter.
+		 *
+		 * @param string $url     URL used for deleting a single notification.
+		 * @param int    $user_id The user ID.
+		 */
+		return apply_filters( 'bp_get_the_notification_delete_url', $url, $user_id );
+	}
+
+/**
+ * Output the action links for the current notification.
+ *
+ * @since 1.9.0
+ * @since 2.6.0 Added $user_id as a parameter to $args.
+ *
+ * @param array|string $args Array of arguments.
+ */
+function bp_the_notification_action_links( $args = '' ) {
+	echo bp_get_the_notification_action_links( $args );
+}
+	/**
+	 * Return the action links for the current notification.
+	 *
+	 * @since 1.9.0
+	 * @since 2.6.0 Added $user_id as a parameter to $args.
+	 *
+	 * @param array|string $args {
+	 *     @type string $before  HTML before the links.
+	 *     @type string $after   HTML after the links.
+	 *     @type string $sep     HTML between the links.
+	 *     @type array  $links   Array of links to implode by 'sep'.
+	 *     @type int    $user_id User ID to fetch action links for. Defaults to displayed user ID.
+	 * }
+	 * @return string HTML links for actions to take on single notifications.
+	 */
+	function bp_get_the_notification_action_links( $args = '' ) {
+		// Set default user ID to use.
+		$user_id = isset( $args['user_id'] ) ? $args['user_id'] : bp_displayed_user_id();
+
+		// Parse.
+		$r = wp_parse_args( $args, array(
+			'before' => '',
+			'after'  => '',
+			'sep'    => ' | ',
+			'links'  => array(
+				bp_get_the_notification_mark_link( $user_id ),
+				bp_get_the_notification_delete_link( $user_id )
+			)
+		) );
+
+		// Build the links.
+		$retval = $r['before'] . implode( $r['links'], $r['sep'] ) . $r['after'];
+
+		/**
+		 * Filters the action links for the current notification.
+		 *
+		 * @since 1.9.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param string $retval HTML links for actions to take on single notifications.
+		 * @param array  $r      Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_the_notification_action_links', $retval, $r );
+	}
+
+/**
+ * Output the pagination count for the current notification loop.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_pagination_count() {
+	echo bp_get_notifications_pagination_count();
+}
+	/**
+	 * Return the pagination count for the current notification loop.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string HTML for the pagination count.
+	 */
+	function bp_get_notifications_pagination_count() {
+		$query_loop = buddypress()->notifications->query_loop;
+		$start_num  = intval( ( $query_loop->pag_page - 1 ) * $query_loop->pag_num ) + 1;
+		$from_num   = bp_core_number_format( $start_num );
+		$to_num     = bp_core_number_format( ( $start_num + ( $query_loop->pag_num - 1 ) > $query_loop->total_notification_count ) ? $query_loop->total_notification_count : $start_num + ( $query_loop->pag_num - 1 ) );
+		$total      = bp_core_number_format( $query_loop->total_notification_count );
+
+		if ( 1 == $query_loop->total_notification_count ) {
+			$pag = __( 'Viewing 1 notification', 'buddypress' );
+		} else {
+			$pag = sprintf( _n( 'Viewing %1$s - %2$s of %3$s notification', 'Viewing %1$s - %2$s of %3$s notifications', $query_loop->total_notification_count, 'buddypress' ), $from_num, $to_num, $total );
+		}
+
+		/**
+		 * Filters the pagination count for the current notification loop.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $pag HTML for the pagination count.
+		 */
+		return apply_filters( 'bp_notifications_pagination_count', $pag );
+	}
+
+/**
+ * Output the pagination links for the current notification loop.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_pagination_links() {
+	echo bp_get_notifications_pagination_links();
+}
+	/**
+	 * Return the pagination links for the current notification loop.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return string HTML for the pagination links.
+	 */
+	function bp_get_notifications_pagination_links() {
+
+		/**
+		 * Filters the pagination links for the current notification loop.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $pag_links HTML for the pagination links.
+		 */
+		return apply_filters( 'bp_get_notifications_pagination_links', buddypress()->notifications->query_loop->pag_links );
+	}
+
+/** Form Helpers **************************************************************/
+
+/**
+ * Output the form for changing the sort order of notifications.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_sort_order_form() {
+
+	// Setup local variables.
+	$orders   = array( 'DESC', 'ASC' );
+	$selected = 'DESC';
+
+	// Check for a custom sort_order.
+	if ( !empty( $_REQUEST['sort_order'] ) ) {
+		if ( in_array( $_REQUEST['sort_order'], $orders ) ) {
+			$selected = $_REQUEST['sort_order'];
+		}
+	} ?>
+
+	<form action="" method="get" id="notifications-sort-order">
+		<label for="notifications-sort-order-list"><?php esc_html_e( 'Order By:', 'buddypress' ); ?></label>
+
+		<select id="notifications-sort-order-list" name="sort_order" onchange="this.form.submit();">
+			<option value="DESC" <?php selected( $selected, 'DESC' ); ?>><?php _e( 'Newest First', 'buddypress' ); ?></option>
+			<option value="ASC"  <?php selected( $selected, 'ASC'  ); ?>><?php _e( 'Oldest First', 'buddypress' ); ?></option>
+		</select>
+
+		<noscript>
+			<input id="submit" type="submit" name="form-submit" class="submit" value="<?php esc_attr_e( 'Go', 'buddypress' ); ?>" />
+		</noscript>
+	</form>
+
+<?php
+}
+
+/**
+ * Output the dropdown for bulk management of notifications.
+ *
+ * @since 2.2.0
+ */
+function bp_notifications_bulk_management_dropdown() {
+	?>
+	<label class="bp-screen-reader-text" for="notification-select"><?php
+		/* translators: accessibility text */
+		_e( 'Select Bulk Action', 'buddypress' );
+	?></label>
+	<select name="notification_bulk_action" id="notification-select">
+		<option value="" selected="selected"><?php _e( 'Bulk Actions', 'buddypress' ); ?></option>
+
+		<?php if ( bp_is_current_action( 'unread' ) ) : ?>
+			<option value="read"><?php _e( 'Mark read', 'buddypress' ); ?></option>
+		<?php elseif ( bp_is_current_action( 'read' ) ) : ?>
+			<option value="unread"><?php _e( 'Mark unread', 'buddypress' ); ?></option>
+		<?php endif; ?>
+		<option value="delete"><?php _e( 'Delete', 'buddypress' ); ?></option>
+	</select>
+	<input type="submit" id="notification-bulk-manage" class="button action" value="<?php esc_attr_e( 'Apply', 'buddypress' ); ?>">
+	<?php
+}
Index: /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-component.php	(revision 12265)
@@ -0,0 +1,318 @@
+<?php
+/**
+ * BuddyPress Member Notifications Loader.
+ *
+ * Initializes the Notifications component.
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsLoader
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Extends the component class to set up the Notifications component.
+ */
+class BP_Notifications_Component extends BP_Component {
+
+	/**
+	 * Start the notifications component creation process.
+	 *
+	 * @since 1.9.0
+	 */
+	public function __construct() {
+		parent::start(
+			'notifications',
+			_x( 'Notifications', 'Page <title>', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 30
+			)
+		);
+	}
+
+	/**
+	 * Include notifications component files.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Component::includes() for a description of arguments.
+	 *
+	 * @param array $includes See BP_Component::includes() for a description.
+	 */
+	public function includes( $includes = array() ) {
+		$includes = array(
+			'adminbar',
+			'template',
+			'filters',
+			'functions',
+			'cache',
+		);
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		// Bail if not on a notifications page or logged in.
+		if ( ! bp_is_user_notifications() || ! is_user_logged_in() ) {
+			return;
+		}
+
+		// Actions.
+		if ( bp_is_post_request() ) {
+			require $this->path . 'bp-notifications/actions/bulk-manage.php';
+		} elseif ( bp_is_get_request() ) {
+			require $this->path . 'bp-notifications/actions/delete.php';
+		}
+
+		// Screens.
+		require $this->path . 'bp-notifications/screens/unread.php';
+		if ( bp_is_current_action( 'read' ) ) {
+			require $this->path . 'bp-notifications/screens/read.php';
+		}
+	}
+
+	/**
+	 * Set up component global data.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Component::setup_globals() for a description of arguments.
+	 *
+	 * @param array $args See BP_Component::setup_globals() for a description.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		// Define a slug, if necessary.
+		if ( ! defined( 'BP_NOTIFICATIONS_SLUG' ) ) {
+			define( 'BP_NOTIFICATIONS_SLUG', $this->id );
+		}
+
+		// Global tables for the notifications component.
+		$global_tables = array(
+			'table_name'      => $bp->table_prefix . 'bp_notifications',
+			'table_name_meta' => $bp->table_prefix . 'bp_notifications_meta',
+		);
+
+		// Metadata tables for notifications component.
+		$meta_tables = array(
+			'notification' => $bp->table_prefix . 'bp_notifications_meta',
+		);
+
+		// All globals for the notifications component.
+		// Note that global_tables is included in this array.
+		$args = array(
+			'slug'          => BP_NOTIFICATIONS_SLUG,
+			'has_directory' => false,
+			'search_string' => __( 'Search Notifications...', 'buddypress' ),
+			'global_tables' => $global_tables,
+			'meta_tables'   => $meta_tables
+		);
+
+		parent::setup_globals( $args );
+	}
+
+	/**
+	 * Set up component navigation.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of arguments.
+	 *
+	 * @param array $main_nav Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 * @param array $sub_nav  Optional. See BP_Component::setup_nav() for
+	 *                        description.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$access             = bp_core_can_edit_settings();
+		$slug               = bp_get_notifications_slug();
+		$notifications_link = trailingslashit( $user_domain . $slug );
+
+		// Only grab count if we're on a user page and current user has access.
+		if ( bp_is_user() && bp_user_has_access() ) {
+			$count    = bp_notifications_get_unread_notification_count( bp_displayed_user_id() );
+			$class    = ( 0 === $count ) ? 'no-count' : 'count';
+			$nav_name = sprintf(
+				/* translators: %s: Unread notification count for the current user */
+				_x( 'Notifications %s', 'Profile screen nav', 'buddypress' ),
+				sprintf(
+					'<span class="%s">%s</span>',
+					esc_attr( $class ),
+					bp_core_number_format( $count )
+				)
+			);
+		} else {
+			$nav_name = _x( 'Notifications', 'Profile screen nav', 'buddypress' );
+		}
+
+		// Add 'Notifications' to the main navigation.
+		$main_nav = array(
+			'name'                    => $nav_name,
+			'slug'                    => $slug,
+			'position'                => 30,
+			'show_for_displayed_user' => $access,
+			'screen_function'         => 'bp_notifications_screen_unread',
+			'default_subnav_slug'     => 'unread',
+			'item_css_id'             => $this->id,
+		);
+
+		// Add the subnav items to the notifications nav item.
+		$sub_nav[] = array(
+			'name'            => _x( 'Unread', 'Notification screen nav', 'buddypress' ),
+			'slug'            => 'unread',
+			'parent_url'      => $notifications_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_notifications_screen_unread',
+			'position'        => 10,
+			'item_css_id'     => 'notifications-my-notifications',
+			'user_has_access' => $access,
+		);
+
+		$sub_nav[] = array(
+			'name'            => _x( 'Read', 'Notification screen nav', 'buddypress' ),
+			'slug'            => 'read',
+			'parent_url'      => $notifications_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_notifications_screen_read',
+			'position'        => 20,
+			'user_has_access' => $access,
+		);
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up the component entries in the WordPress Admin Bar.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Component::setup_nav() for a description of the $wp_admin_nav
+	 *      parameter array.
+	 *
+	 * @param array $wp_admin_nav See BP_Component::setup_admin_bar() for a
+	 *                            description.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$notifications_link = trailingslashit( bp_loggedin_user_domain() . bp_get_notifications_slug() );
+
+			// Pending notification requests.
+			$count = bp_notifications_get_unread_notification_count( bp_loggedin_user_id() );
+			if ( ! empty( $count ) ) {
+				$title = sprintf(
+					/* translators: %s: Unread notification count for the current user */
+					_x( 'Notifications %s', 'My Account Notification pending', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+				$unread = sprintf(
+					/* translators: %s: Unread notification count for the current user */
+					_x( 'Unread %s', 'My Account Notification pending', 'buddypress' ),
+					'<span class="count">' . bp_core_number_format( $count ) . '</span>'
+				);
+			} else {
+				$title  = _x( 'Notifications', 'My Account Notification',         'buddypress' );
+				$unread = _x( 'Unread',        'My Account Notification sub nav', 'buddypress' );
+			}
+
+			// Add the "My Account" sub menus.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => $title,
+				'href'   => $notifications_link
+			);
+
+			// Unread.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-unread',
+				'title'    => $unread,
+				'href'     => $notifications_link,
+				'position' => 10
+			);
+
+			// Read.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-read',
+				'title'    => _x( 'Read', 'My Account Notification sub nav', 'buddypress' ),
+				'href'     => trailingslashit( $notifications_link . 'read' ),
+				'position' => 20
+			);
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Set up the title for pages and <title>.
+	 *
+	 * @since 1.9.0
+	 */
+	public function setup_title() {
+
+		// Adjust title.
+		if ( bp_is_notifications_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() ) {
+				$bp->bp_options_title = __( 'Notifications', 'buddypress' );
+			} else {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'     => sprintf( __( 'Profile picture of %s', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title = bp_get_displayed_user_fullname();
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups.
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_notifications',
+			'notification_meta'
+		) );
+
+		parent::setup_cache_groups();
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-notification.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-notification.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-notification.php	(revision 12265)
@@ -0,0 +1,1180 @@
+<?php
+/**
+ * BuddyPress Notifications Classes.
+ *
+ * Classes used for the Notifications component.
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsClasses
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress Notification items.
+ *
+ * Use this class to create, access, edit, or delete BuddyPress Notifications.
+ *
+ * @since 1.9.0
+ */
+class BP_Notifications_Notification {
+
+	/**
+	 * The notification ID.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * The ID of the item associated with the notification.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $item_id;
+
+	/**
+	 * The ID of the secondary item associated with the notification.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $secondary_item_id = null;
+
+	/**
+	 * The ID of the user the notification is associated with.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $user_id;
+
+	/**
+	 * The name of the component that the notification is for.
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $component_name;
+
+	/**
+	 * The component action which the notification is related to.
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $component_action;
+
+	/**
+	 * The date the notification was created.
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $date_notified;
+
+	/**
+	 * Is the notification new, or has it already been read.
+	 *
+	 * @since 1.9.0
+	 * @var bool
+	 */
+	public $is_new;
+
+	/** Public Methods ********************************************************/
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int $id Optional. Provide an ID to access an existing
+	 *                notification item.
+	 */
+	public function __construct( $id = 0 ) {
+		if ( ! empty( $id ) ) {
+			$this->id = (int) $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Update or insert notification details into the database.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function save() {
+		$retval = false;
+
+		/**
+		 * Fires before the current notification item gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_Notifications_Notification $value Current instance of the notification item being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_notification_before_save', array( &$this ) );
+
+		$data = array(
+			'user_id'           => $this->user_id,
+			'item_id'           => $this->item_id,
+			'secondary_item_id' => $this->secondary_item_id,
+			'component_name'    => $this->component_name,
+			'component_action'  => $this->component_action,
+			'date_notified'     => $this->date_notified,
+			'is_new'            => $this->is_new,
+		);
+		$data_format = array( '%d', '%d', '%d', '%s', '%s', '%s', '%d' );
+
+		// Update.
+		if ( ! empty( $this->id ) ) {
+			$result = self::_update( $data, array( 'ID' => $this->id ), $data_format, array( '%d' ) );
+
+		// Insert.
+		} else {
+			$result = self::_insert( $data, $data_format );
+		}
+
+		// Set the notification ID if successful.
+		if ( ! empty( $result ) && ! is_wp_error( $result ) ) {
+			global $wpdb;
+
+			$this->id = $wpdb->insert_id;
+			$retval   = $wpdb->insert_id;
+		}
+
+		/**
+		 * Fires after the current notification item gets saved.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_Notifications_Notification $value Current instance of the notification item being saved.
+		 *                                             Passed by reference.
+		 */
+		do_action_ref_array( 'bp_notification_after_save', array( &$this ) );
+
+		// Return the result.
+		return $retval;
+	}
+
+	/**
+	 * Fetch data for an existing notification from the database.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @global BuddyPress $bp The one true BuddyPress instance.
+	 * @global wpdb $wpdb WordPress database object.
+	 */
+	public function populate() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		// Look for a notification.
+		$notification = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->notifications->table_name} WHERE id = %d", $this->id ) );
+
+		// Setup the notification data.
+		if ( ! empty( $notification ) && ! is_wp_error( $notification ) ) {
+			$this->item_id           = (int) $notification->item_id;
+			$this->secondary_item_id = (int) $notification->secondary_item_id;
+			$this->user_id           = (int) $notification->user_id;
+			$this->component_name    = $notification->component_name;
+			$this->component_action  = $notification->component_action;
+			$this->date_notified     = $notification->date_notified;
+			$this->is_new            = (int) $notification->is_new;
+		}
+	}
+
+	/** Protected Static Methods **********************************************/
+
+	/**
+	 * Create a notification entry.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $data {
+	 *     Array of notification data, passed to {@link wpdb::insert()}.
+	 *     @type int    $user_id           ID of the associated user.
+	 *     @type int    $item_id           ID of the associated item.
+	 *     @type int    $secondary_item_id ID of the secondary associated item.
+	 *     @type string $component_name    Name of the associated component.
+	 *     @type string $component_action  Name of the associated component
+	 *                                     action.
+	 *     @type string $date_notified     Timestamp of the notification.
+	 *     @type bool   $is_new            True if the notification is unread, otherwise false.
+	 * }
+	 * @param array $data_format See {@link wpdb::insert()}.
+	 * @return int|false The number of rows inserted, or false on error.
+	 */
+	protected static function _insert( $data = array(), $data_format = array() ) {
+		global $wpdb;
+		return $wpdb->insert( buddypress()->notifications->table_name, $data, $data_format );
+	}
+
+	/**
+	 * Update notifications.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see wpdb::update() for further description of paramater formats.
+	 *
+	 * @param array $data         Array of notification data to update, passed to
+	 *                            {@link wpdb::update()}. Accepts any property of a
+	 *                            BP_Notification_Notification object.
+	 * @param array $where        The WHERE params as passed to wpdb::update().
+	 *                            Typically consists of array( 'ID' => $id ) to specify the ID
+	 *                            of the item being updated. See {@link wpdb::update()}.
+	 * @param array $data_format  See {@link wpdb::insert()}.
+	 * @param array $where_format See {@link wpdb::insert()}.
+	 * @return int|false The number of rows updated, or false on error.
+	 */
+	protected static function _update( $data = array(), $where = array(), $data_format = array(), $where_format = array() ) {
+		global $wpdb;
+		return $wpdb->update( buddypress()->notifications->table_name, $data, $where, $data_format, $where_format );
+	}
+
+	/**
+	 * Delete notifications.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see wpdb::update() for further description of paramater formats.
+	 *
+	 * @param array $where        Array of WHERE clauses to filter by, passed to
+	 *                            {@link wpdb::delete()}. Accepts any property of a
+	 *                            BP_Notification_Notification object.
+	 * @param array $where_format See {@link wpdb::insert()}.
+	 * @return int|false The number of rows updated, or false on error.
+	 */
+	protected static function _delete( $where = array(), $where_format = array() ) {
+		global $wpdb;
+		return $wpdb->delete( buddypress()->notifications->table_name, $where, $where_format );
+	}
+
+	/**
+	 * Assemble the WHERE clause of a get() SQL statement.
+	 *
+	 * Used by BP_Notifications_Notification::get() to create its WHERE
+	 * clause.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array  $args           See {@link BP_Notifications_Notification::get()}
+	 *                               for more details.
+	 * @param string $select_sql     SQL SELECT fragment.
+	 * @param string $from_sql       SQL FROM fragment.
+	 * @param string $join_sql       SQL JOIN fragment.
+	 * @param string $meta_query_sql SQL meta query fragment.
+	 * @return string WHERE clause.
+	 */
+	protected static function get_where_sql( $args = array(), $select_sql = '', $from_sql = '', $join_sql = '', $meta_query_sql = '' ) {
+		global $wpdb;
+
+		$where_conditions = array();
+		$where            = '';
+
+		// The id.
+		if ( ! empty( $args['id'] ) ) {
+			$id_in = implode( ',', wp_parse_id_list( $args['id'] ) );
+			$where_conditions['id'] = "id IN ({$id_in})";
+		}
+
+		// The user_id.
+		if ( ! empty( $args['user_id'] ) ) {
+			$user_id_in = implode( ',', wp_parse_id_list( $args['user_id'] ) );
+			$where_conditions['user_id'] = "user_id IN ({$user_id_in})";
+		}
+
+		// The item_id.
+		if ( ! empty( $args['item_id'] ) ) {
+			$item_id_in = implode( ',', wp_parse_id_list( $args['item_id'] ) );
+			$where_conditions['item_id'] = "item_id IN ({$item_id_in})";
+		}
+
+		// The secondary_item_id.
+		if ( ! empty( $args['secondary_item_id'] ) ) {
+			$secondary_item_id_in = implode( ',', wp_parse_id_list( $args['secondary_item_id'] ) );
+			$where_conditions['secondary_item_id'] = "secondary_item_id IN ({$secondary_item_id_in})";
+		}
+
+		// The component_name.
+		if ( ! empty( $args['component_name'] ) ) {
+			if ( ! is_array( $args['component_name'] ) ) {
+				$component_names = explode( ',', $args['component_name'] );
+			} else {
+				$component_names = $args['component_name'];
+			}
+
+			$cn_clean = array();
+			foreach ( $component_names as $cn ) {
+				$cn_clean[] = $wpdb->prepare( '%s', $cn );
+			}
+
+			$cn_in = implode( ',', $cn_clean );
+			$where_conditions['component_name'] = "component_name IN ({$cn_in})";
+		}
+
+		// The component_action.
+		if ( ! empty( $args['component_action'] ) ) {
+			if ( ! is_array( $args['component_action'] ) ) {
+				$component_actions = explode( ',', $args['component_action'] );
+			} else {
+				$component_actions = $args['component_action'];
+			}
+
+			$ca_clean = array();
+			foreach ( $component_actions as $ca ) {
+				$ca_clean[] = $wpdb->prepare( '%s', $ca );
+			}
+
+			$ca_in = implode( ',', $ca_clean );
+			$where_conditions['component_action'] = "component_action IN ({$ca_in})";
+		}
+
+		// If is_new.
+		if ( ! empty( $args['is_new'] ) && 'both' !== $args['is_new'] ) {
+			$where_conditions['is_new'] = "is_new = 1";
+		} elseif ( isset( $args['is_new'] ) && ( 0 === $args['is_new'] || false === $args['is_new'] ) ) {
+			$where_conditions['is_new'] = "is_new = 0";
+		}
+
+		// The search_terms.
+		if ( ! empty( $args['search_terms'] ) ) {
+			$search_terms_like = '%' . bp_esc_like( $args['search_terms'] ) . '%';
+			$where_conditions['search_terms'] = $wpdb->prepare( "( component_name LIKE %s OR component_action LIKE %s )", $search_terms_like, $search_terms_like );
+		}
+
+		// The date query.
+		if ( ! empty( $args['date_query'] ) ) {
+			$where_conditions['date_query'] = self::get_date_query_sql( $args['date_query'] );
+		}
+
+		// The meta query.
+		if ( ! empty( $meta_query_sql['where'] ) ) {
+			$where_conditions['meta_query'] = $meta_query_sql['where'];
+		}
+
+		/**
+		 * Filters the MySQL WHERE conditions for the Notifications items get method.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array  $where_conditions Current conditions for MySQL WHERE statement.
+		 * @param array  $args             Parsed arguments passed into method.
+		 * @param string $select_sql       Current SELECT MySQL statement at point of execution.
+		 * @param string $from_sql         Current FROM MySQL statement at point of execution.
+		 * @param string $join_sql         Current INNER JOIN MySQL statement at point of execution.
+		 * @param string $meta_query_sql   Current meta query WHERE statement at point of execution.
+		 */
+		$where_conditions = apply_filters( 'bp_notifications_get_where_conditions', $where_conditions, $args, $select_sql, $from_sql, $join_sql, $meta_query_sql );
+
+		// Custom WHERE.
+		if ( ! empty( $where_conditions ) ) {
+			$where = 'WHERE ' . implode( ' AND ', $where_conditions );
+		}
+
+		return $where;
+	}
+
+	/**
+	 * Assemble the ORDER BY clause of a get() SQL statement.
+	 *
+	 * Used by BP_Notifications_Notification::get() to create its ORDER BY
+	 * clause.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args See {@link BP_Notifications_Notification::get()}
+	 *                    for more details.
+	 * @return string ORDER BY clause.
+	 */
+	protected static function get_order_by_sql( $args = array() ) {
+
+		// Setup local variable.
+		$conditions = array();
+		$retval     = '';
+
+		// Order by.
+		if ( ! empty( $args['order_by'] ) ) {
+			$order_by               = implode( ', ', (array) $args['order_by'] );
+			$conditions['order_by'] = "{$order_by}";
+		}
+
+		// Sort order direction.
+		if ( ! empty( $args['sort_order'] ) && in_array( $args['sort_order'], array( 'ASC', 'DESC' ) ) ) {
+			$sort_order               = $args['sort_order'];
+			$conditions['sort_order'] = "{$sort_order}";
+		}
+
+		// Custom ORDER BY.
+		if ( ! empty( $conditions ) ) {
+			$retval = 'ORDER BY ' . implode( ' ', $conditions );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Assemble the LIMIT clause of a get() SQL statement.
+	 *
+	 * Used by BP_Notifications_Notification::get() to create its LIMIT clause.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args See {@link BP_Notifications_Notification::get()}
+	 *                    for more details.
+	 * @return string $retval LIMIT clause.
+	 */
+	protected static function get_paged_sql( $args = array() ) {
+		global $wpdb;
+
+		// Setup local variable.
+		$retval = '';
+
+		// Custom LIMIT.
+		if ( ! empty( $args['page'] ) && ! empty( $args['per_page'] ) ) {
+			$page     = absint( $args['page']     );
+			$per_page = absint( $args['per_page'] );
+			$offset   = $per_page * ( $page - 1 );
+			$retval   = $wpdb->prepare( "LIMIT %d, %d", $offset, $per_page );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Assemble query clauses, based on arguments, to pass to $wpdb methods.
+	 *
+	 * The insert(), update(), and delete() methods of {@link wpdb} expect
+	 * arguments of the following forms:
+	 *
+	 * - associative arrays whose key/value pairs are column => value, to
+	 *   be used in WHERE, SET, or VALUES clauses.
+	 * - arrays of "formats", which tell $wpdb->prepare() which type of
+	 *   value to expect when sanitizing (eg, array( '%s', '%d' ))
+	 *
+	 * This utility method can be used to assemble both kinds of params,
+	 * out of a single set of associative array arguments, such as:
+	 *
+	 *     $args = array(
+	 *         'user_id' => 4,
+	 *         'component_name' => 'groups',
+	 *     );
+	 *
+	 * This will be converted to:
+	 *
+	 *     array(
+	 *         'data' => array(
+	 *             'user_id' => 4,
+	 *             'component_name' => 'groups',
+	 *         ),
+	 *         'format' => array(
+	 *             '%d',
+	 *             '%s',
+	 *         ),
+	 *     )
+	 *
+	 * which can easily be passed as arguments to the $wpdb methods.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args Associative array of filter arguments.
+	 *                    See {@BP_Notifications_Notification::get()}
+	 *                    for a breakdown.
+	 * @return array Associative array of 'data' and 'format' args.
+	 */
+	protected static function get_query_clauses( $args = array() ) {
+		$where_clauses = array(
+			'data'   => array(),
+			'format' => array(),
+		);
+
+		// The id.
+		if ( ! empty( $args['id'] ) ) {
+			$where_clauses['data']['id'] = absint( $args['id'] );
+			$where_clauses['format'][] = '%d';
+		}
+
+		// The user_id.
+		if ( ! empty( $args['user_id'] ) ) {
+			$where_clauses['data']['user_id'] = absint( $args['user_id'] );
+			$where_clauses['format'][] = '%d';
+		}
+
+		// The item_id.
+		if ( ! empty( $args['item_id'] ) ) {
+			$where_clauses['data']['item_id'] = absint( $args['item_id'] );
+			$where_clauses['format'][] = '%d';
+		}
+
+		// The secondary_item_id.
+		if ( ! empty( $args['secondary_item_id'] ) ) {
+			$where_clauses['data']['secondary_item_id'] = absint( $args['secondary_item_id'] );
+			$where_clauses['format'][] = '%d';
+		}
+
+		// The component_name.
+		if ( ! empty( $args['component_name'] ) ) {
+			$where_clauses['data']['component_name'] = $args['component_name'];
+			$where_clauses['format'][] = '%s';
+		}
+
+		// The component_action.
+		if ( ! empty( $args['component_action'] ) ) {
+			$where_clauses['data']['component_action'] = $args['component_action'];
+			$where_clauses['format'][] = '%s';
+		}
+
+		// If is_new.
+		if ( isset( $args['is_new'] ) ) {
+			$where_clauses['data']['is_new'] = ! empty( $args['is_new'] ) ? 1 : 0;
+			$where_clauses['format'][] = '%d';
+		}
+
+		return $where_clauses;
+	}
+
+	/** Public Static Methods *************************************************/
+
+	/**
+	 * Check that a specific notification is for a specific user.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int $user_id         ID of the user being checked.
+	 * @param int $notification_id ID of the notification being checked.
+	 * @return bool True if the notification belongs to the user, otherwise
+	 *              false.
+	 */
+	public static function check_access( $user_id, $notification_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->core->table_name_notifications} WHERE id = %d AND user_id = %d", $notification_id, $user_id ) );
+	}
+
+	/**
+	 * Parse notifications query arguments.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param mixed $args Args to parse.
+	 * @return array
+	 */
+	public static function parse_args( $args = '' ) {
+		return wp_parse_args( $args, array(
+			'id'                => false,
+			'user_id'           => false,
+			'item_id'           => false,
+			'secondary_item_id' => false,
+			'component_name'    => bp_notifications_get_registered_components(),
+			'component_action'  => false,
+			'is_new'            => true,
+			'search_terms'      => '',
+			'order_by'          => false,
+			'sort_order'        => false,
+			'page'              => false,
+			'per_page'          => false,
+			'meta_query'        => false,
+			'date_query'        => false,
+			'update_meta_cache' => true
+		) );
+	}
+
+	/**
+	 * Get notifications, based on provided filter parameters.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args {
+	 *     Associative array of arguments. All arguments but $page and
+	 *     $per_page can be treated as filter values for get_where_sql()
+	 *     and get_query_clauses(). All items are optional.
+	 *     @type int|array    $id                ID of notification being updated. Can be an
+	 *                                           array of IDs.
+	 *     @type int|array    $user_id           ID of user being queried. Can be an
+	 *                                           array of user IDs.
+	 *     @type int|array    $item_id           ID of associated item. Can be an array
+	 *                                           of multiple item IDs.
+	 *     @type int|array    $secondary_item_id ID of secondary associated
+	 *                                           item. Can be an array of multiple IDs.
+	 *     @type string|array $component_name    Name of the component to
+	 *                                           filter by. Can be an array of component names.
+	 *     @type string|array $component_action  Name of the action to
+	 *                                           filter by. Can be an array of actions.
+	 *     @type bool         $is_new            Whether to limit to new notifications. True
+	 *                                           returns only new notifications, false returns only non-new
+	 *                                           notifications. 'both' returns all. Default: true.
+	 *     @type string       $search_terms      Term to match against component_name
+	 *                                           or component_action fields.
+	 *     @type string       $order_by          Database column to order notifications by.
+	 *     @type string       $sort_order        Either 'ASC' or 'DESC'.
+	 *     @type string       $order_by          Field to order results by.
+	 *     @type string       $sort_order        ASC or DESC.
+	 *     @type int          $page              Number of the current page of results. Default:
+	 *                                           false (no pagination - all items).
+	 *     @type int          $per_page          Number of items to show per page. Default:
+	 *                                           false (no pagination - all items).
+	 *     @type array        $meta_query        Array of meta_query conditions. See WP_Meta_Query::queries.
+	 *     @type array        $date_query        Array of date_query conditions. See first parameter of
+	 *                                           WP_Date_Query::__construct().
+	 *     @type bool         $update_meta_cache Whether to update meta cache. Default: true.
+	 * }
+	 * @return array Located notifications.
+	 */
+	public static function get( $args = array() ) {
+		global $wpdb;
+
+		// Parse the arguments.
+		$r = self::parse_args( $args );
+
+		// Get BuddyPress.
+		$bp = buddypress();
+
+		// METADATA.
+		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
+
+		// SELECT.
+		$select_sql = "SELECT *";
+
+		// FROM.
+		$from_sql   = "FROM {$bp->notifications->table_name} n ";
+
+		// JOIN.
+		$join_sql   = $meta_query_sql['join'];
+
+		// WHERE.
+		$where_sql  = self::get_where_sql( array(
+			'id'                => $r['id'],
+			'user_id'           => $r['user_id'],
+			'item_id'           => $r['item_id'],
+			'secondary_item_id' => $r['secondary_item_id'],
+			'component_name'    => $r['component_name'],
+			'component_action'  => $r['component_action'],
+			'is_new'            => $r['is_new'],
+			'search_terms'      => $r['search_terms'],
+			'date_query'        => $r['date_query']
+		), $select_sql, $from_sql, $join_sql, $meta_query_sql );
+
+		// ORDER BY.
+		$order_sql  = self::get_order_by_sql( array(
+			'order_by'   => $r['order_by'],
+			'sort_order' => $r['sort_order']
+		) );
+
+		// LIMIT %d, %d.
+		$pag_sql    = self::get_paged_sql( array(
+			'page'     => $r['page'],
+			'per_page' => $r['per_page']
+		) );
+
+		// Concatenate query parts.
+		$sql = "{$select_sql} {$from_sql} {$join_sql} {$where_sql} {$order_sql} {$pag_sql}";
+
+		$results = $wpdb->get_results( $sql );
+
+		// Integer casting.
+		foreach ( $results as $key => $result ) {
+			$results[$key]->id                = (int) $results[$key]->id;
+			$results[$key]->user_id           = (int) $results[$key]->user_id;
+			$results[$key]->item_id           = (int) $results[$key]->item_id;
+			$results[$key]->secondary_item_id = (int) $results[$key]->secondary_item_id;
+			$results[$key]->is_new            = (int) $results[$key]->is_new;
+		}
+
+		// Update meta cache.
+		if ( true === $r['update_meta_cache'] ) {
+			bp_notifications_update_meta_cache( wp_list_pluck( $results, 'id' ) );
+		}
+
+		return $results;
+	}
+
+	/**
+	 * Get a count of total notifications matching a set of arguments.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Notifications_Notification::get() for a description of arguments.
+	 *
+	 * @param array $args See {@link BP_Notifications_Notification::get()}.
+	 * @return int Count of located items.
+	 */
+	public static function get_total_count( $args ) {
+		global $wpdb;
+
+		// Parse the arguments.
+		$r = self::parse_args( $args );
+
+		// Load BuddyPress.
+		$bp = buddypress();
+
+		// METADATA.
+		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
+
+		// SELECT.
+		$select_sql = "SELECT COUNT(*)";
+
+		// FROM.
+		$from_sql   = "FROM {$bp->notifications->table_name} n ";
+
+		// JOIN.
+		$join_sql   = $meta_query_sql['join'];
+
+		// WHERE.
+		$where_sql  = self::get_where_sql( array(
+			'id'                => $r['id'],
+			'user_id'           => $r['user_id'],
+			'item_id'           => $r['item_id'],
+			'secondary_item_id' => $r['secondary_item_id'],
+			'component_name'    => $r['component_name'],
+			'component_action'  => $r['component_action'],
+			'is_new'            => $r['is_new'],
+			'search_terms'      => $r['search_terms'],
+			'date_query'        => $r['date_query']
+		), $select_sql, $from_sql, $join_sql, $meta_query_sql );
+
+		// Concatenate query parts.
+		$sql = "{$select_sql} {$from_sql} {$join_sql} {$where_sql}";
+
+		// Return the queried results.
+		return (int) $wpdb->get_var( $sql );
+	}
+
+	/**
+	 * Get the SQL for the 'meta_query' param in BP_Notifications_Notification::get().
+	 *
+	 * We use WP_Meta_Query to do the heavy lifting of parsing the
+	 * meta_query array and creating the necessary SQL clauses. However,
+	 * since BP_Notifications_Notification::get() builds its SQL differently than
+	 * WP_Query, we have to alter the return value (stripping the leading
+	 * AND keyword from the 'where' clause).
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param  array $meta_query An array of meta_query filters. See the
+	 *                           documentation for WP_Meta_Query for details.
+	 * @return array $sql_array 'join' and 'where' clauses.
+	 */
+	public static function get_meta_query_sql( $meta_query = array() ) {
+
+		// Default array keys & empty values.
+		$sql_array = array(
+			'join'  => '',
+			'where' => '',
+		);
+
+		// Bail if no meta query.
+		if ( empty( $meta_query ) ) {
+			return $sql_array;
+		}
+
+		// WP_Meta_Query expects the table name at $wpdb->notificationmeta.
+		$GLOBALS['wpdb']->notificationmeta = buddypress()->notifications->table_name_meta;
+
+		$n_meta_query = new WP_Meta_Query( $meta_query );
+		$meta_sql     = $n_meta_query->get_sql( 'notification', 'n', 'id' );
+
+		// Strip the leading AND - it's handled in get().
+		$sql_array['where'] = preg_replace( '/^\sAND/', '', $meta_sql['where'] );
+		$sql_array['join']  = $meta_sql['join'];
+
+		return $sql_array;
+	}
+
+	/**
+	 * Get the SQL for the 'date_query' param in BP_Notifications_Notification::get().
+	 *
+	 * We use BP_Date_Query, which extends WP_Date_Query, to do the heavy lifting
+	 * of parsing the date_query array and creating the necessary SQL clauses.
+	 * However, since BP_Notifications_Notification::get() builds its SQL
+	 * differently than WP_Query, we have to alter the return value (stripping
+	 * the leading AND keyword from the query).
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $date_query An array of date_query parameters. See the
+	 *                          documentation for the first parameter of WP_Date_Query.
+	 * @return string
+	 */
+	public static function get_date_query_sql( $date_query = array() ) {
+
+		// Bail if not a proper date query format.
+		if ( empty( $date_query ) || ! is_array( $date_query ) ) {
+			return '';
+		}
+
+		// Date query.
+		$date_query = new BP_Date_Query( $date_query, 'date_notified' );
+
+		// Strip the leading AND - it's handled in get().
+		return preg_replace( '/^\sAND/', '', $date_query->get_sql() );
+	}
+
+	/**
+	 * Update notifications.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Notifications_Notification::get() for a description of
+	 *      accepted update/where arguments.
+	 *
+	 * @param array $update_args Associative array of fields to update,
+	 *                           and the values to update them to. Of the format
+	 *                           array( 'user_id' => 4, 'component_name' => 'groups', ).
+	 * @param array $where_args  Associative array of columns/values, to
+	 *                           determine which rows should be updated. Of the format
+	 *                           array( 'item_id' => 7, 'component_action' => 'members', ).
+	 * @return int|false Number of rows updated on success, false on failure.
+	 */
+	public static function update( $update_args = array(), $where_args = array() ) {
+		$update = self::get_query_clauses( $update_args );
+		$where  = self::get_query_clauses( $where_args  );
+
+		/**
+		 * Fires before the update of a notification item.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array $update_args See BP_Notifications_Notification::update().
+		 * @param array $where_args  See BP_Notifications_Notification::update().
+		 */
+		do_action( 'bp_notification_before_update', $update_args, $where_args );
+
+		return self::_update(
+			$update['data'],
+			$where['data'],
+			$update['format'],
+			$where['format']
+		);
+	}
+
+	/**
+	 * Delete notifications.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Notifications_Notification::get() for a description of
+	 *      accepted update/where arguments.
+	 *
+	 * @param array $args Associative array of columns/values, to determine
+	 *                    which rows should be deleted.  Of the format
+	 *                    array( 'item_id' => 7, 'component_action' => 'members', ).
+	 * @return int|false Number of rows deleted on success, false on failure.
+	 */
+	public static function delete( $args = array() ) {
+		$where = self::get_query_clauses( $args );
+
+		/**
+		 * Fires before the deletion of a notification item.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $args Associative array of columns/values, to determine
+		 *                    which rows should be deleted. Of the format
+		 *                    array( 'item_id' => 7, 'component_action' => 'members' ).
+		 */
+		do_action( 'bp_notification_before_delete', $args );
+
+		return self::_delete( $where['data'], $where['format'] );
+	}
+
+	/** Convenience methods ***************************************************/
+
+	/**
+	 * Delete a single notification by ID.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see BP_Notifications_Notification::delete() for explanation of
+	 *      return value.
+	 *
+	 * @param int $id ID of the notification item to be deleted.
+	 * @return int|false True on success, false on failure.
+	 */
+	public static function delete_by_id( $id ) {
+		return self::delete( array(
+			'id' => $id,
+		) );
+	}
+
+	/**
+	 * Fetch all the notifications in the database for a specific user.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int    $user_id ID of the user whose notifications are being
+	 *                        fetched.
+	 * @param string $status  Optional. Status of notifications to fetch.
+	 *                        'is_new' to get only unread items, 'all' to get all.
+	 * @return array Associative array of notification items.
+	 */
+	public static function get_all_for_user( $user_id, $status = 'is_new' ) {
+		return self::get( array(
+			'user_id' => $user_id,
+			'is_new'  => 'is_new' === $status,
+		) );
+	}
+
+	/**
+	 * Fetch all the unread notifications in the database for a specific user.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int $user_id ID of the user whose notifications are being
+	 *                     fetched.
+	 * @return array Associative array of unread notification items.
+	 */
+	public static function get_unread_for_user( $user_id = 0 ) {
+		return self::get( array(
+			'user_id' => $user_id,
+			'is_new'  => true,
+		) );
+	}
+
+	/**
+	 * Fetch all the read notifications in the database for a specific user.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int $user_id ID of the user whose notifications are being
+	 *                     fetched.
+	 * @return array Associative array of unread notification items.
+	 */
+	public static function get_read_for_user( $user_id = 0 ) {
+		return self::get( array(
+			'user_id' => $user_id,
+			'is_new'  => false,
+		) );
+	}
+
+	/**
+	 * Get unread notifications for a user, in a pagination-friendly format.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args {
+	 *     Array of arguments.
+	 *     @type int    $user_id      ID of the user for whom the notifications are
+	 *                                being fetched. Default: logged-in user ID.
+	 *     @type bool   $is_new       Whether to limit the query to unread
+	 *                                notifications. Default: true.
+	 *     @type int    $page         Number of the page to return. Default: 1.
+	 *     @type int    $per_page     Number of results to display per page.
+	 *                                Default: 10.
+	 *     @type string $search_terms Optional. A term to search against in
+	 *                                the 'component_name' and 'component_action' columns.
+	 * }
+	 * @return array {
+	 *     @type array $notifications Array of notification results.
+	 *     @type int   $total         Count of all located notifications matching
+	 *                                the query params.
+	 * }
+	 */
+	public static function get_current_notifications_for_user( $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'user_id'      => bp_loggedin_user_id(),
+			'is_new'       => true,
+			'page'         => 1,
+			'per_page'     => 25,
+			'search_terms' => '',
+		) );
+
+		$notifications = self::get( $r );
+
+		// Bail if no notifications.
+		if ( empty( $notifications ) ) {
+			return false;
+		}
+
+		$total_count = self::get_total_count( $r );
+
+		return array( 'notifications' => &$notifications, 'total' => $total_count );
+	}
+
+	/** Mark ******************************************************************/
+
+	/**
+	 * Mark all user notifications as read.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int    $user_id           The ID of the user who the notifications are for.
+	 * @param int    $is_new            Mark as read (1) or unread (0).
+	 * @param int    $item_id           Item ID being acted on.
+	 * @param string $component_name    Name of component the notifications are for.
+	 * @param string $component_action  Name of the component action.
+	 * @param int    $secondary_item_id The ID of the secondary item.
+	 * @return int|false False on failure to update. ID on success.
+	 */
+	public static function mark_all_for_user( $user_id, $is_new = 0, $item_id = 0, $component_name = '', $component_action = '', $secondary_item_id = 0 ) {
+
+		// Values to be updated.
+		$update_args = array(
+			'is_new' => $is_new,
+		);
+
+		// WHERE clauses.
+		$where_args = array(
+			'user_id' => $user_id,
+		);
+
+		if ( ! empty( $item_id ) ) {
+			$where_args['item_id'] = $item_id;
+		}
+
+		if ( ! empty( $component_name ) ) {
+			$where_args['component_name'] = $component_name;
+		}
+
+		if ( ! empty( $component_action ) ) {
+			$where_args['component_action'] = $component_action;
+		}
+
+		if ( ! empty( $secondary_item_id ) ) {
+			$where_args['secondary_item_id'] = $secondary_item_id;
+		}
+
+		return self::update( $update_args, $where_args );
+	}
+
+	/**
+	 * Mark all notifications from a user as read.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int    $user_id           The ID of the user who the notifications are from.
+	 * @param int    $is_new            Mark as read (1) or unread (0).
+	 * @param string $component_name    Name of component the notifications are for.
+	 * @param string $component_action  Name of the component action.
+	 * @param int    $secondary_item_id The ID of the secondary item.
+	 * @return int|false
+	 */
+	public static function mark_all_from_user( $user_id, $is_new = 0, $component_name = '', $component_action = '', $secondary_item_id = 0 ) {
+
+		// Values to be updated.
+		$update_args = array(
+			'is_new' => $is_new,
+		);
+
+		// WHERE clauses.
+		$where_args = array(
+			'item_id' => $user_id,
+		);
+
+		if ( ! empty( $component_name ) ) {
+			$where_args['component_name'] = $component_name;
+		}
+
+		if ( ! empty( $component_action ) ) {
+			$where_args['component_action'] = $component_action;
+		}
+
+		if ( ! empty( $secondary_item_id ) ) {
+			$where_args['secondary_item_id'] = $secondary_item_id;
+		}
+
+		return self::update( $update_args, $where_args );
+	}
+
+	/**
+	 * Mark all notifications for all users as read by item id, and optional
+	 * secondary item id, and component name and action.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param int    $item_id           The ID of the item associated with the
+	 *                                  notifications.
+	 * @param int    $is_new            Mark as read (1) or unread (0).
+	 * @param string $component_name    The component that the notifications
+	 *                                  are associated with.
+	 * @param string $component_action  The action that the notifications
+	 *                                  are associated with.
+	 * @param int    $secondary_item_id Optional. ID of the secondary
+	 *                                  associated item.
+	 * @return int|false
+	 */
+	public static function mark_all_by_type( $item_id, $is_new = 0, $component_name = '', $component_action = '', $secondary_item_id = 0 ) {
+
+		// Values to be updated.
+		$update_args = array(
+			'is_new' => $is_new,
+		);
+
+		// WHERE clauses.
+		$where_args = array(
+			'item_id' => $item_id,
+		);
+
+		if ( ! empty( $component_name ) ) {
+			$where_args['component_name'] = $component_name;
+		}
+
+		if ( ! empty( $component_action ) ) {
+			$where_args['component_action'] = $component_action;
+		}
+
+		if ( ! empty( $secondary_item_id ) ) {
+			$where_args['secondary_item_id'] = $secondary_item_id;
+		}
+
+		return self::update( $update_args, $where_args );
+	}
+
+	/**
+	 * Get a user's unread notifications, grouped by component and action.
+	 *
+	 * Multiple notifications of the same type (those that share the same component_name
+	 * and component_action) are collapsed for formatting as "You have 5 pending
+	 * friendship requests", etc. See bp_notifications_get_notifications_for_user().
+	 * For a full-fidelity list of user notifications, use
+	 * bp_notifications_get_all_notifications_for_user().
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param int $user_id ID of the user whose notifications are being fetched.
+	 * @return array Notifications items for formatting into a list.
+	 */
+	public static function get_grouped_notifications_for_user( $user_id ) {
+		global $wpdb;
+
+		// Load BuddyPress.
+		$bp = buddypress();
+
+		// SELECT.
+		$select_sql = "SELECT id, user_id, item_id, secondary_item_id, component_name, component_action, date_notified, is_new, COUNT(id) as total_count ";
+
+		// FROM.
+		$from_sql = "FROM {$bp->notifications->table_name} n ";
+
+		// WHERE.
+		$where_sql = self::get_where_sql( array(
+			'user_id'        => $user_id,
+			'is_new'         => 1,
+			'component_name' => bp_notifications_get_registered_components(),
+		), $select_sql, $from_sql );
+
+		// GROUP
+		$group_sql = "GROUP BY user_id, component_name, component_action";
+
+		// SORT
+		$order_sql = "ORDER BY date_notified desc";
+
+		// Concatenate query parts.
+		$sql = "{$select_sql} {$from_sql} {$where_sql} {$group_sql} {$order_sql}";
+
+		// Return the queried results.
+		return $wpdb->get_results( $sql );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/classes/class-bp-notifications-template.php	(revision 12265)
@@ -0,0 +1,356 @@
+<?php
+/**
+ * BuddyPress Notifications Template Loop Class.
+ *
+ * @package BuddyPress
+ * @subpackage TonificationsTemplate
+ * @since 1.9.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main notifications template loop class.
+ *
+ * Responsible for loading a group of notifications into a loop for display.
+ *
+ * @since 1.9.0
+ */
+class BP_Notifications_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $current_notification = -1;
+
+	/**
+	 * The number of notifications returned by the paged query.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $current_notification_count;
+
+	/**
+	 * Total number of notifications matching the query.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $total_notification_count;
+
+	/**
+	 * Array of notifications located by the query.
+	 *
+	 * @since 1.9.0
+	 * @var array
+	 */
+	public $notifications;
+
+	/**
+	 * The notification object currently being iterated on.
+	 *
+	 * @since 1.9.0
+	 * @var object
+	 */
+	public $notification;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @since 1.9.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * The ID of the user to whom the displayed notifications belong.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $user_id;
+
+	/**
+	 * The page number being requested.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $pag_page;
+
+	/**
+	 * The $_GET argument used in URLs for determining pagination.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $pag_arg;
+
+	/**
+	 * The number of items to display per page of results.
+	 *
+	 * @since 1.9.0
+	 * @var int
+	 */
+	public $pag_num;
+
+	/**
+	 * An HTML string containing pagination links.
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $pag_links;
+
+	/**
+	 * A string to match against.
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $search_terms;
+
+	/**
+	 * A database column to order the results by.
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $order_by;
+
+	/**
+	 * The direction to sort the results (ASC or DESC).
+	 *
+	 * @since 1.9.0
+	 * @var string
+	 */
+	public $sort_order;
+
+	/**
+	 * Array of variables used in this notification query.
+	 *
+	 * @since 2.2.2
+	 * @var array
+	 */
+	public $query_vars;
+
+	/**
+	 * Constructor method.
+	 *
+	 * @see bp_has_notifications() For information on the array format.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param array $args {
+	 *     An array of arguments. See {@link bp_has_notifications()}
+	 *     for more details.
+	 * }
+	 */
+	public function __construct( $args = array() ) {
+
+		// Parse arguments.
+		$r = wp_parse_args( $args, array(
+			'id'                => false,
+			'user_id'           => 0,
+			'item_id'           => false,
+			'secondary_item_id' => false,
+			'component_name'    => bp_notifications_get_registered_components(),
+			'component_action'  => false,
+			'is_new'            => true,
+			'search_terms'      => '',
+			'order_by'          => 'date_notified',
+			'sort_order'        => 'DESC',
+			'page_arg'          => 'npage',
+			'page'              => 1,
+			'per_page'          => 25,
+			'max'               => null,
+			'meta_query'        => false,
+			'date_query'        => false
+		) );
+
+		// Sort order direction.
+		$orders = array( 'ASC', 'DESC' );
+		if ( ! empty( $_GET['sort_order'] ) && in_array( $_GET['sort_order'], $orders ) ) {
+			$r['sort_order'] = $_GET['sort_order'];
+		} else {
+			$r['sort_order'] = in_array( $r['sort_order'], $orders ) ? $r['sort_order'] : 'DESC';
+		}
+
+		// Setup variables.
+		$this->pag_arg      = sanitize_key( $r['page_arg'] );
+		$this->pag_page     = bp_sanitize_pagination_arg( $this->pag_arg, $r['page']     );
+		$this->pag_num      = bp_sanitize_pagination_arg( 'num',          $r['per_page'] );
+		$this->user_id      = $r['user_id'];
+		$this->is_new       = $r['is_new'];
+		$this->search_terms = $r['search_terms'];
+		$this->order_by     = $r['order_by'];
+		$this->sort_order   = $r['sort_order'];
+		$this->query_vars   = array(
+			'id'                => $r['id'],
+			'user_id'           => $this->user_id,
+			'item_id'           => $r['item_id'],
+			'secondary_item_id' => $r['secondary_item_id'],
+			'component_name'    => $r['component_name'],
+			'component_action'  => $r['component_action'],
+			'meta_query'        => $r['meta_query'],
+			'date_query'        => $r['date_query'],
+			'is_new'            => $this->is_new,
+			'search_terms'      => $this->search_terms,
+			'order_by'          => $this->order_by,
+			'sort_order'        => $this->sort_order,
+			'page'              => $this->pag_page,
+			'per_page'          => $this->pag_num,
+		);
+
+		// Setup the notifications to loop through.
+		$this->notifications            = BP_Notifications_Notification::get( $this->query_vars );
+		$this->total_notification_count = BP_Notifications_Notification::get_total_count( $this->query_vars );
+
+		if ( empty( $this->notifications ) ) {
+			$this->notification_count       = 0;
+			$this->total_notification_count = 0;
+
+		} else {
+			if ( ! empty( $r['max'] ) ) {
+				if ( $r['max'] >= count( $this->notifications ) ) {
+					$this->notification_count = count( $this->notifications );
+				} else {
+					$this->notification_count = (int) $r['max'];
+				}
+			} else {
+				$this->notification_count = count( $this->notifications );
+			}
+		}
+
+		if ( (int) $this->total_notification_count && (int) $this->pag_num ) {
+			$add_args = array(
+				'sort_order' => $this->sort_order,
+			);
+
+			$this->pag_links = paginate_links( array(
+				'base'      => add_query_arg( $this->pag_arg, '%#%' ),
+				'format'    => '',
+				'total'     => ceil( (int) $this->total_notification_count / (int) $this->pag_num ),
+				'current'   => $this->pag_page,
+				'prev_text' => _x( '&larr;', 'Notifications pagination previous text', 'buddypress' ),
+				'next_text' => _x( '&rarr;', 'Notifications pagination next text',     'buddypress' ),
+				'mid_size'  => 1,
+				'add_args'  => $add_args,
+			) );
+		}
+	}
+
+	/**
+	 * Whether there are notifications available in the loop.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see bp_has_notifications()
+	 *
+	 * @return bool True if there are items in the loop, otherwise false.
+	 */
+	public function has_notifications() {
+		if ( $this->notification_count ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set up the next notification and iterate index.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @return object The next notification to iterate over.
+	 */
+	public function next_notification() {
+
+		$this->current_notification++;
+
+		$this->notification = $this->notifications[ $this->current_notification ];
+
+		return $this->notification;
+	}
+
+	/**
+	 * Rewind the blogs and reset blog index.
+	 *
+	 * @since 1.9.0
+	 */
+	public function rewind_notifications() {
+
+		$this->current_notification = -1;
+
+		if ( $this->notification_count > 0 ) {
+			$this->notification = $this->notifications[0];
+		}
+	}
+
+	/**
+	 * Whether there are notifications left in the loop to iterate over.
+	 *
+	 * This method is used by {@link bp_notifications()} as part of the
+	 * while loop that controls iteration inside the notifications loop, eg:
+	 *     while ( bp_notifications() ) { ...
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see bp_notifications()
+	 *
+	 * @return bool True if there are more notifications to show,
+	 *              otherwise false.
+	 */
+	public function notifications() {
+
+		if ( $this->current_notification + 1 < $this->notification_count ) {
+			return true;
+
+		} elseif ( $this->current_notification + 1 == $this->notification_count ) {
+
+			/**
+			 * Fires right before the rewinding of notification posts.
+			 *
+			 * @since 1.9.0
+			 */
+			do_action( 'notifications_loop_end');
+
+			$this->rewind_notifications();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Set up the current notification inside the loop.
+	 *
+	 * Used by {@link bp_the_notification()} to set up the current
+	 * notification data while looping, so that template tags used during
+	 * that iteration make reference to the current notification.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @see bp_the_notification()
+	 */
+	public function the_notification() {
+		$this->in_the_loop  = true;
+		$this->notification = $this->next_notification();
+
+		// Loop has just started.
+		if ( 0 === $this->current_notification ) {
+
+			/**
+			 * Fires if the current notification item is the first in the notification loop.
+			 *
+			 * @since 1.9.0
+			 */
+			do_action( 'notifications_loop_start' );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-notifications/screens/read.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/screens/read.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/screens/read.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Notifications: User's "Notifications > Read" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and route the 'read' notifications screen.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_screen_read() {
+
+	/**
+	 * Fires right before the loading of the notifications read screen template file.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( 'bp_notifications_screen_read' );
+
+	/**
+	 * Filters the template to load for the notifications read screen.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $template Path to the notifications read template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_notifications_template_read', 'members/single/home' ) );
+}
+
+/**
+ * Handle marking single notifications as unread.
+ *
+ * @since 1.9.0
+ *
+ * @return bool
+ */
+function bp_notifications_action_mark_unread() {
+
+	// Bail if not the read screen.
+	if ( ! bp_is_notifications_component() || ! bp_is_current_action( 'read' ) ) {
+		return false;
+	}
+
+	// Get the action.
+	$action = !empty( $_GET['action']          ) ? $_GET['action']          : '';
+	$nonce  = !empty( $_GET['_wpnonce']        ) ? $_GET['_wpnonce']        : '';
+	$id     = !empty( $_GET['notification_id'] ) ? $_GET['notification_id'] : '';
+
+	// Bail if no action or no ID.
+	if ( ( 'unread' !== $action ) || empty( $id ) || empty( $nonce ) ) {
+		return false;
+	}
+
+	// Check the nonce and mark the notification.
+	if ( bp_verify_nonce_request( 'bp_notification_mark_unread_' . $id ) && bp_notifications_mark_notification( $id, true ) ) {
+		bp_core_add_message( __( 'Notification successfully marked unread.',       'buddypress' )          );
+	} else {
+		bp_core_add_message( __( 'There was a problem marking that notification.', 'buddypress' ), 'error' );
+	}
+
+	// Redirect.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_notifications_slug() . '/read/' );
+}
+add_action( 'bp_actions', 'bp_notifications_action_mark_unread' );
Index: /tags/4.0.0-beta1/src/bp-notifications/screens/unread.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-notifications/screens/unread.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-notifications/screens/unread.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Notifications: User's "Notifications" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage NotificationsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Catch and route the 'unread' notifications screen.
+ *
+ * @since 1.9.0
+ */
+function bp_notifications_screen_unread() {
+
+	/**
+	 * Fires right before the loading of the notifications unread screen template file.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( 'bp_notifications_screen_unread' );
+
+	/**
+	 * Filters the template to load for the notifications unread screen.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $template Path to the notifications unread template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_notifications_template_unread', 'members/single/home' ) );
+}
+
+/**
+ * Handle marking single notifications as read.
+ *
+ * @since 1.9.0
+ *
+ * @return bool
+ */
+function bp_notifications_action_mark_read() {
+
+	// Bail if not the unread screen.
+	if ( ! bp_is_notifications_component() || ! bp_is_current_action( 'unread' ) ) {
+		return false;
+	}
+
+	// Get the action.
+	$action = !empty( $_GET['action']          ) ? $_GET['action']          : '';
+	$nonce  = !empty( $_GET['_wpnonce']        ) ? $_GET['_wpnonce']        : '';
+	$id     = !empty( $_GET['notification_id'] ) ? $_GET['notification_id'] : '';
+
+	// Bail if no action or no ID.
+	if ( ( 'read' !== $action ) || empty( $id ) || empty( $nonce ) ) {
+		return false;
+	}
+
+	// Check the nonce and mark the notification.
+	if ( bp_verify_nonce_request( 'bp_notification_mark_read_' . $id ) && bp_notifications_mark_notification( $id, false ) ) {
+		bp_core_add_message( __( 'Notification successfully marked read.',         'buddypress' )          );
+	} else {
+		bp_core_add_message( __( 'There was a problem marking that notification.', 'buddypress' ), 'error' );
+	}
+
+	// Redirect.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_notifications_slug() . '/unread/' );
+}
+add_action( 'bp_actions', 'bp_notifications_action_mark_read' );
Index: /tags/4.0.0-beta1/src/bp-settings/actions/capabilities.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/actions/capabilities.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/actions/capabilities.php	(revision 12265)
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Settings: Capabilities action handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the setting of user capabilities, spamming, hamming, role, etc...
+ *
+ * @since 1.6.0
+ */
+function bp_settings_action_capabilities() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no submit action.
+	if ( ! isset( $_POST['capabilities-submit'] ) ) {
+		return;
+	}
+
+	// Bail if not in settings.
+	if ( ! bp_is_settings_component() || ! bp_is_current_action( 'capabilities' ) ) {
+		return false;
+	}
+
+	// 404 if there are any additional action variables attached
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	// Only super admins can currently spam users (but they can't spam
+	// themselves).
+	if ( ! is_super_admin() || bp_is_my_profile() ) {
+		return;
+	}
+
+	// Nonce check.
+	check_admin_referer( 'capabilities' );
+
+	/**
+	 * Fires before the capabilities settings have been saved.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_settings_capabilities_before_save' );
+
+	/* Spam **************************************************************/
+
+	$is_spammer = !empty( $_POST['user-spammer'] ) ? true : false;
+
+	if ( bp_is_user_spammer( bp_displayed_user_id() ) != $is_spammer ) {
+		$status = ( true == $is_spammer ) ? 'spam' : 'ham';
+		bp_core_process_spammer_status( bp_displayed_user_id(), $status );
+
+		/**
+		 * Fires after processing a user as a spammer.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int    $value  ID of the currently displayed user.
+		 * @param string $status Determined status of "spam" or "ham" for the displayed user.
+		 */
+		do_action( 'bp_core_action_set_spammer_status', bp_displayed_user_id(), $status );
+	}
+
+	/* Other *************************************************************/
+
+	/**
+	 * Fires after the capabilities settings have been saved and before redirect.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_settings_capabilities_after_save' );
+
+	// Redirect to the root domain.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_settings_slug() . '/capabilities/' );
+}
+add_action( 'bp_actions', 'bp_settings_action_capabilities' );
Index: /tags/4.0.0-beta1/src/bp-settings/actions/delete-account.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/actions/delete-account.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/actions/delete-account.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Settings: Account deletion action handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the deleting of a user.
+ *
+ * @since 1.6.0
+ */
+function bp_settings_action_delete_account() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no submit action.
+	if ( ! isset( $_POST['delete-account-understand'] ) ) {
+		return;
+	}
+
+	// Bail if not in settings.
+	if ( ! bp_is_settings_component() || ! bp_is_current_action( 'delete-account' ) ) {
+		return false;
+	}
+
+	// 404 if there are any additional action variables attached
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	// Bail if account deletion is disabled.
+	if ( bp_disable_account_deletion() && ! bp_current_user_can( 'delete_users' ) ) {
+		return false;
+	}
+
+	// Nonce check.
+	check_admin_referer( 'delete-account' );
+
+	// Get username now because it might be gone soon!
+	$username = bp_get_displayed_user_fullname();
+
+	// Delete the users account.
+	if ( bp_core_delete_account( bp_displayed_user_id() ) ) {
+
+		// Add feedback after deleting a user.
+		bp_core_add_message( sprintf( __( '%s was successfully deleted.', 'buddypress' ), $username ), 'success' );
+
+		// Redirect to the root domain.
+		bp_core_redirect( bp_get_root_domain() );
+	}
+}
+add_action( 'bp_actions', 'bp_settings_action_delete_account' );
Index: /tags/4.0.0-beta1/src/bp-settings/actions/general.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/actions/general.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/actions/general.php	(revision 12265)
@@ -0,0 +1,311 @@
+<?php
+/**
+ * Settings: Email address and password action handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the changing and saving of user email addresses and passwords.
+ *
+ * We do quite a bit of logic and error handling here to make sure that users
+ * do not accidentally lock themselves out of their accounts. We also try to
+ * provide as accurate of feedback as possible without exposing anyone else's
+ * information to them.
+ *
+ * Special considerations are made for super admins that are able to edit any
+ * users accounts already, without knowing their existing password.
+ *
+ * @since 1.6.0
+ *
+ * @global BuddyPress $bp
+ */
+function bp_settings_action_general() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no submit action.
+	if ( ! isset( $_POST['submit'] ) ) {
+		return;
+	}
+
+	// Bail if not in settings.
+	if ( ! bp_is_settings_component() || ! bp_is_current_action( 'general' ) ) {
+		return;
+	}
+
+	// 404 if there are any additional action variables attached
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	// Define local defaults
+	$bp            = buddypress(); // The instance
+	$email_error   = false;        // invalid|blocked|taken|empty|nochange
+	$pass_error    = false;        // invalid|mismatch|empty|nochange
+	$pass_changed  = false;        // true if the user changes their password
+	$email_changed = false;        // true if the user changes their email
+	$feedback_type = 'error';      // success|error
+	$feedback      = array();      // array of strings for feedback.
+
+	// Nonce check.
+	check_admin_referer('bp_settings_general');
+
+	// Validate the user again for the current password when making a big change.
+	if ( ( is_super_admin() ) || ( !empty( $_POST['pwd'] ) && wp_check_password( $_POST['pwd'], $bp->displayed_user->userdata->user_pass, bp_displayed_user_id() ) ) ) {
+
+		$update_user = get_userdata( bp_displayed_user_id() );
+
+		/* Email Change Attempt ******************************************/
+
+		if ( !empty( $_POST['email'] ) ) {
+
+			// What is missing from the profile page vs signup -
+			// let's double check the goodies.
+			$user_email     = sanitize_email( esc_html( trim( $_POST['email'] ) ) );
+			$old_user_email = $bp->displayed_user->userdata->user_email;
+
+			// User is changing email address.
+			if ( $old_user_email != $user_email ) {
+
+				// Run some tests on the email address.
+				$email_checks = bp_core_validate_email_address( $user_email );
+
+				if ( true !== $email_checks ) {
+					if ( isset( $email_checks['invalid'] ) ) {
+						$email_error = 'invalid';
+					}
+
+					if ( isset( $email_checks['domain_banned'] ) || isset( $email_checks['domain_not_allowed'] ) ) {
+						$email_error = 'blocked';
+					}
+
+					if ( isset( $email_checks['in_use'] ) ) {
+						$email_error = 'taken';
+					}
+				}
+
+				// Store a hash to enable email validation.
+				if ( false === $email_error ) {
+					$hash = wp_generate_password( 32, false );
+
+					$pending_email = array(
+						'hash'     => $hash,
+						'newemail' => $user_email,
+					);
+
+					bp_update_user_meta( bp_displayed_user_id(), 'pending_email_change', $pending_email );
+					$verify_link = bp_displayed_user_domain() . bp_get_settings_slug() . '/?verify_email_change=' . $hash;
+
+					// Send the verification email.
+					$args = array(
+						'tokens' => array(
+							'displayname'    => bp_core_get_user_displayname( bp_displayed_user_id() ),
+							'old-user.email' => $old_user_email,
+							'user.email'     => $user_email,
+							'verify.url'     => esc_url( $verify_link ),
+						),
+					);
+					bp_send_email( 'settings-verify-email-change', bp_displayed_user_id(), $args );
+
+					// We mark that the change has taken place so as to ensure a
+					// success message, even though verification is still required.
+					$_POST['email'] = $update_user->user_email;
+					$email_changed = true;
+				}
+
+			// No change.
+			} else {
+				$email_error = false;
+			}
+
+		// Email address cannot be empty.
+		} else {
+			$email_error = 'empty';
+		}
+
+		/* Password Change Attempt ***************************************/
+
+		if ( !empty( $_POST['pass1'] ) && !empty( $_POST['pass2'] ) ) {
+
+			if ( ( $_POST['pass1'] == $_POST['pass2'] ) && !strpos( " " . wp_unslash( $_POST['pass1'] ), "\\" ) ) {
+
+				// Password change attempt is successful.
+				if ( ( ! empty( $_POST['pwd'] ) && $_POST['pwd'] != $_POST['pass1'] ) || is_super_admin() )  {
+					$update_user->user_pass = $_POST['pass1'];
+					$pass_changed = true;
+
+				// The new password is the same as the current password.
+				} else {
+					$pass_error = 'same';
+				}
+
+			// Password change attempt was unsuccessful.
+			} else {
+				$pass_error = 'mismatch';
+			}
+
+		// Both password fields were empty.
+		} elseif ( empty( $_POST['pass1'] ) && empty( $_POST['pass2'] ) ) {
+			$pass_error = false;
+
+		// One of the password boxes was left empty.
+		} elseif ( ( empty( $_POST['pass1'] ) && !empty( $_POST['pass2'] ) ) || ( !empty( $_POST['pass1'] ) && empty( $_POST['pass2'] ) ) ) {
+			$pass_error = 'empty';
+		}
+
+		// The structure of the $update_user object changed in WP 3.3, but
+		// wp_update_user() still expects the old format.
+		if ( isset( $update_user->data ) && is_object( $update_user->data ) ) {
+			$update_user = $update_user->data;
+			$update_user = get_object_vars( $update_user );
+
+			// Unset the password field to prevent it from emptying out the
+			// user's user_pass field in the database.
+			// @see wp_update_user().
+			if ( false === $pass_changed ) {
+				unset( $update_user['user_pass'] );
+			}
+		}
+
+		// Clear cached data, so that the changed settings take effect
+		// on the current page load.
+		if ( ( false === $email_error ) && ( false === $pass_error ) && ( wp_update_user( $update_user ) ) ) {
+			$bp->displayed_user->userdata = bp_core_get_core_userdata( bp_displayed_user_id() );
+		}
+
+	// Password Error.
+	} else {
+		$pass_error = 'invalid';
+	}
+
+	// Email feedback.
+	switch ( $email_error ) {
+		case 'invalid' :
+			$feedback['email_invalid']  = __( 'That email address is invalid. Check the formatting and try again.', 'buddypress' );
+			break;
+		case 'blocked' :
+			$feedback['email_blocked']  = __( 'That email address is currently unavailable for use.', 'buddypress' );
+			break;
+		case 'taken' :
+			$feedback['email_taken']    = __( 'That email address is already taken.', 'buddypress' );
+			break;
+		case 'empty' :
+			$feedback['email_empty']    = __( 'Email address cannot be empty.', 'buddypress' );
+			break;
+		case false :
+			// No change.
+			break;
+	}
+
+	// Password feedback.
+	switch ( $pass_error ) {
+		case 'invalid' :
+			$feedback['pass_error']    = __( 'Your current password is invalid.', 'buddypress' );
+			break;
+		case 'mismatch' :
+			$feedback['pass_mismatch'] = __( 'The new password fields did not match.', 'buddypress' );
+			break;
+		case 'empty' :
+			$feedback['pass_empty']    = __( 'One of the password fields was empty.', 'buddypress' );
+			break;
+		case 'same' :
+			$feedback['pass_same'] 	   = __( 'The new password must be different from the current password.', 'buddypress' );
+			break;
+		case false :
+			// No change.
+			break;
+	}
+
+	// No errors so show a simple success message.
+	if ( ( ( false === $email_error ) || ( false == $pass_error ) ) && ( ( true === $pass_changed ) || ( true === $email_changed ) ) ) {
+		$feedback[]    = __( 'Your settings have been saved.', 'buddypress' );
+		$feedback_type = 'success';
+
+	// Some kind of errors occurred.
+	} elseif ( ( ( false === $email_error ) || ( false === $pass_error ) ) && ( ( false === $pass_changed ) || ( false === $email_changed ) ) ) {
+		if ( bp_is_my_profile() ) {
+			$feedback['nochange'] = __( 'No changes were made to your account.', 'buddypress' );
+		} else {
+			$feedback['nochange'] = __( 'No changes were made to this account.', 'buddypress' );
+		}
+	}
+
+	// Set the feedback.
+	bp_core_add_message( implode( "\n", $feedback ), $feedback_type );
+
+	/**
+	 * Fires after the general settings have been saved, and before redirect.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_core_general_settings_after_save' );
+
+	// Redirect to prevent issues with browser back button.
+	bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_settings_slug() . '/general' ) );
+}
+add_action( 'bp_actions', 'bp_settings_action_general' );
+
+/**
+ * Process email change verification or cancel requests.
+ *
+ * @since 2.1.0
+ */
+function bp_settings_verify_email_change() {
+	if ( ! bp_is_settings_component() ) {
+		return;
+	}
+
+	if ( ! bp_is_my_profile() ) {
+		return;
+	}
+
+	$redirect_to = trailingslashit( bp_displayed_user_domain() . bp_get_settings_slug() );
+
+	// Email change is being verified.
+	if ( isset( $_GET['verify_email_change'] ) ) {
+		$pending_email = bp_get_user_meta( bp_displayed_user_id(), 'pending_email_change', true );
+
+		// Bail if the hash provided doesn't match the one saved in the database.
+		if ( ! hash_equals( urldecode( $_GET['verify_email_change'] ), $pending_email['hash'] ) ) {
+			return;
+		}
+
+		$email_changed = wp_update_user( array(
+			'ID'         => bp_displayed_user_id(),
+			'user_email' => trim( $pending_email['newemail'] ),
+		) );
+
+		if ( $email_changed ) {
+
+			// Delete the pending email change key.
+			bp_delete_user_meta( bp_displayed_user_id(), 'pending_email_change' );
+
+			// Post a success message and redirect.
+			bp_core_add_message( __( 'You have successfully verified your new email address.', 'buddypress' ) );
+		} else {
+			// Unknown error.
+			bp_core_add_message( __( 'There was a problem verifying your new email address. Please try again.', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( $redirect_to );
+		die();
+
+	// Email change is being dismissed.
+	} elseif ( ! empty( $_GET['dismiss_email_change'] ) ) {
+		$nonce_check = isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'bp_dismiss_email_change' );
+
+		if ( $nonce_check ) {
+			bp_delete_user_meta( bp_displayed_user_id(), 'pending_email_change' );
+			bp_core_add_message( __( 'You have successfully dismissed your pending email change.', 'buddypress' ) );
+		}
+
+		bp_core_redirect( $redirect_to );
+		die();
+	}
+}
+add_action( 'bp_actions', 'bp_settings_verify_email_change' );
Index: /tags/4.0.0-beta1/src/bp-settings/actions/notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/actions/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/actions/notifications.php	(revision 12265)
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Settings: Email notifications action handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsActions
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the changing and saving of user notification settings.
+ *
+ * @since 1.6.0
+ */
+function bp_settings_action_notifications() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no submit action.
+	if ( ! isset( $_POST['submit'] ) ) {
+		return;
+	}
+
+	// Bail if not in settings.
+	if ( ! bp_is_settings_component() || ! bp_is_current_action( 'notifications' ) ) {
+		return false;
+	}
+
+	// 404 if there are any additional action variables attached
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	check_admin_referer( 'bp_settings_notifications' );
+
+	bp_settings_update_notification_settings( bp_displayed_user_id(), (array) $_POST['notifications'] );
+
+	// Switch feedback for super admins.
+	if ( bp_is_my_profile() ) {
+		bp_core_add_message( __( 'Your notification settings have been saved.',        'buddypress' ), 'success' );
+	} else {
+		bp_core_add_message( __( "This user's notification settings have been saved.", 'buddypress' ), 'success' );
+	}
+
+	/**
+	 * Fires after the notification settings have been saved, and before redirect.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_core_notification_settings_after_save' );
+
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_settings_slug() . '/notifications/' );
+}
+add_action( 'bp_actions', 'bp_settings_action_notifications' );
Index: /tags/4.0.0-beta1/src/bp-settings/bp-settings-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/bp-settings-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/bp-settings-filters.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Filters related to the Settings component.
+ *
+ * @since 4.0.0
+ */
+
+// Personal data export.
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_settings_register_personal_data_exporter' );
+
+/**
+ * Registers Settings personal data exporter.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_settings_register_personal_data_exporter( $exporters ) {
+	$exporters['buddypress-settings'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress Settings Data', 'buddypress' ),
+		'callback'               => 'bp_settings_personal_data_exporter',
+	);
+
+	return $exporters;
+}
Index: /tags/4.0.0-beta1/src/bp-settings/bp-settings-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/bp-settings-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/bp-settings-functions.php	(revision 12265)
@@ -0,0 +1,217 @@
+<?php
+/**
+ * BuddyPress Settings Functions
+ *
+ * @package BuddyPress
+ * @subpackage SettingsFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Update email notification settings for a specific user.
+ *
+ * @since 2.3.5
+ *
+ * @param int   $user_id  ID of the user whose settings are being updated.
+ * @param array $settings Settings array.
+ */
+function bp_settings_update_notification_settings( $user_id, $settings ) {
+	$user_id = (int) $user_id;
+
+	$settings = bp_settings_sanitize_notification_settings( $settings );
+	foreach ( $settings as $setting_key => $setting_value ) {
+		bp_update_user_meta( $user_id, $setting_key, $setting_value );
+	}
+}
+
+/**
+ * Sanitize email notification settings as submitted by a user.
+ *
+ * @since 2.3.5
+ *
+ * @param array $settings Array of settings.
+ * @return array Sanitized settings.
+ */
+function bp_settings_sanitize_notification_settings( $settings = array() ) {
+	$sanitized_settings = array();
+
+	if ( empty( $settings ) ) {
+		return $sanitized_settings;
+	}
+
+	// Get registered notification keys.
+	$registered_notification_settings = bp_settings_get_registered_notification_keys();
+
+	/*
+	 * We sanitize values for core notification keys.
+	 *
+	 * @todo use register_meta()
+	 */
+	$core_notification_settings = array(
+		'notification_messages_new_message',
+		'notification_activity_new_mention',
+		'notification_activity_new_reply',
+		'notification_groups_invite',
+		'notification_groups_group_updated',
+		'notification_groups_admin_promotion',
+		'notification_groups_membership_request',
+		'notification_membership_request_completed',
+		'notification_friends_friendship_request',
+		'notification_friends_friendship_accepted',
+	);
+
+	foreach ( (array) $settings as $key => $value ) {
+		// Skip if not a registered setting.
+		if ( ! in_array( $key, $registered_notification_settings, true ) ) {
+			continue;
+		}
+
+		// Force core keys to 'yes' or 'no' values.
+		if ( in_array( $key, $core_notification_settings, true ) ) {
+			$value = 'yes' === $value ? 'yes' : 'no';
+		}
+
+		$sanitized_settings[ $key ] = $value;
+	}
+
+	return $sanitized_settings;
+}
+
+/**
+ * Build a dynamic whitelist of notification keys, based on what's hooked to 'bp_notification_settings'.
+ *
+ * @since 2.3.5
+ *
+ * @return array
+ */
+function bp_settings_get_registered_notification_keys() {
+
+	ob_start();
+	/**
+	 * Fires at the start of the notification keys whitelisting.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_notification_settings' );
+	$screen = ob_get_clean();
+
+	$matched = preg_match_all( '/<input[^>]+name="notifications\[([^\]]+)\]/', $screen, $matches );
+
+	if ( $matched && isset( $matches[1] ) ) {
+		$key_whitelist = $matches[1];
+	} else {
+		$key_whitelist = array();
+	}
+
+	return $key_whitelist;
+}
+
+/**
+ * Finds and exports personal data associated with an email address from the Settings component.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The user's email address.
+ * @param int    $page           Batch number.
+ * @return array An array of personal data.
+ */
+function bp_settings_personal_data_exporter( $email_address, $page ) {
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$yes = __( 'Yes', 'buddypress' );
+	$no  = __( 'No', 'buddypress' );
+
+	$user_settings = array();
+
+	// These settings all default to 'yes' when nothing is saved, so we have to do some pre-processing.
+	$notification_settings = array();
+
+	if ( bp_is_active( 'activity' ) ) {
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when a member mentions you in an update?', 'buddypress' ),
+			'key'  => 'notification_activity_new_mention',
+		);
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when a member replies to an update or comment you\'ve posted?', 'buddypress' ),
+			'key'  => 'notification_activity_new_reply',
+		);
+	}
+
+	if ( bp_is_active( 'messages' ) ) {
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when a member sends you a new message?', 'buddypress' ),
+			'key'  => 'notification_messages_new_message',
+		);
+	}
+
+	if ( bp_is_active( 'friends' ) ) {
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when a member invites you to join a group?', 'buddypress' ),
+			'key'  => 'notification_groups_invite',
+		);
+	}
+
+	if ( bp_is_active( 'groups' ) ) {
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when group information is updated?', 'buddypress' ),
+			'key'  => 'notification_groups_group_updated',
+		);
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when you are promoted to a group administrator or moderator?', 'buddypress' ),
+			'key'  => 'notification_groups_admin_promoted',
+		);
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when a member requests to join a private group for which you are an admin?', 'buddypress' ),
+			'key'  => 'notification_groups_membership_request',
+		);
+		$notification_settings[] = array(
+			'name' => __( 'Receive email when your request to join a group has been approved or denied?', 'buddypress' ),
+			'key'  => 'notification_membership_request_completed',
+		);
+	}
+
+	foreach ( $notification_settings as $notification_setting ) {
+		$user_notification_setting = bp_get_user_meta( $user->ID, $notification_setting['key'], true );
+		if ( empty( $user_notification_setting ) ) {
+			$user_notification_setting = 'yes';
+		}
+
+		$user_settings[] = array(
+			'name'  => $notification_setting['name'],
+			'value' => 'yes' === $user_notification_setting ? $yes : $no,
+		);
+	}
+
+	if ( function_exists( 'bp_nouveau_groups_get_group_invites_setting' ) ) {
+		$user_settings[] = array(
+			'name'  => __( 'Receive group invitations from my friends only?', 'buddypress' ),
+			'value' => bp_nouveau_groups_get_group_invites_setting() ? $yes : $no,
+		);
+	}
+
+	$data_to_export[] = array(
+		'group_id'    => 'bp_settings',
+		'group_label' => __( 'Settings', 'buddypress' ),
+		'item_id'     => "bp-settings-{$user->ID}",
+		'data'        => $user_settings,
+	);
+
+	return array(
+		'data' => $data_to_export,
+		'done' => true,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-settings/bp-settings-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/bp-settings-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/bp-settings-loader.php	(revision 12265)
@@ -0,0 +1,21 @@
+<?php
+/**
+ * BuddyPress Settings Loader.
+ *
+ * @package BuddyPress
+ * @subpackage SettingsLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-settings component.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_settings() {
+	buddypress()->settings = new BP_Settings_Component();
+}
+add_action( 'bp_setup_components', 'bp_setup_settings', 6 );
Index: /tags/4.0.0-beta1/src/bp-settings/bp-settings-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/bp-settings-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/bp-settings-template.php	(revision 12265)
@@ -0,0 +1,102 @@
+<?php
+/**
+ * BuddyPress Settings Template Functions.
+ *
+ * @package BuddyPress
+ * @subpackage SettingsTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Output the settings component slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_settings_slug() {
+	echo bp_get_settings_slug();
+}
+	/**
+	 * Return the settings component slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_settings_slug() {
+
+		/**
+		 * Filters the Settings component slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $slug Settings component slug.
+		 */
+		return apply_filters( 'bp_get_settings_slug', buddypress()->settings->slug );
+	}
+
+/**
+ * Output the settings component root slug.
+ *
+ * @since 1.5.0
+ *
+ */
+function bp_settings_root_slug() {
+	echo bp_get_settings_root_slug();
+}
+	/**
+	 * Return the settings component root slug.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string
+	 */
+	function bp_get_settings_root_slug() {
+
+		/**
+		 * Filters the Settings component root slug.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string $root_slug Settings component root slug.
+		 */
+		return apply_filters( 'bp_get_settings_root_slug', buddypress()->settings->root_slug );
+	}
+
+/**
+ * Add the 'pending email change' message to the settings page.
+ *
+ * @since 2.1.0
+ */
+function bp_settings_pending_email_notice() {
+	$pending_email = bp_get_user_meta( bp_displayed_user_id(), 'pending_email_change', true );
+
+	if ( empty( $pending_email['newemail'] ) ) {
+		return;
+	}
+
+	if ( bp_get_displayed_user_email() == $pending_email['newemail'] ) {
+		return;
+	}
+
+	?>
+
+	<div id="message" class="bp-template-notice error">
+		<p><?php printf(
+			__( 'There is a pending change of your email address to %s.', 'buddypress' ),
+			'<code>' . esc_html( $pending_email['newemail'] ) . '</code>'
+		); ?>
+		<br />
+		<?php printf(
+			__( 'Check your email (%1$s) for the verification link, or <a href="%2$s">cancel the pending change</a>.', 'buddypress' ),
+			'<code>' . esc_html( bp_get_displayed_user_email() ) . '</code>',
+			esc_url( wp_nonce_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/?dismiss_email_change=1', 'bp_dismiss_email_change' ) )
+		); ?></p>
+	</div>
+
+	<?php
+}
+add_action( 'bp_before_member_settings_template', 'bp_settings_pending_email_notice' );
Index: /tags/4.0.0-beta1/src/bp-settings/classes/class-bp-settings-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/classes/class-bp-settings-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/classes/class-bp-settings-component.php	(revision 12265)
@@ -0,0 +1,259 @@
+<?php
+/**
+ * BuddyPress Settings Loader.
+ *
+ * @package BuddyPress
+ * @subpackage SettingsLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Creates our Settings component.
+ *
+ * @since 1.5.0
+ */
+class BP_Settings_Component extends BP_Component {
+
+	/**
+	 * Start the settings component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'settings',
+			__( 'Settings', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 100
+			)
+		);
+	}
+
+	/**
+	 * Include files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $includes Array of values to include. Not used.
+	 */
+	public function includes( $includes = array() ) {
+		parent::includes( array(
+			'template',
+			'filters',
+			'functions',
+		) );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		// Bail if not on Settings component.
+		if ( ! bp_is_settings_component() ) {
+			return;
+		}
+
+		$actions = array( 'notifications', 'capabilities', 'delete-account' );
+
+		// Authenticated actions.
+		if ( is_user_logged_in() ) {
+			if ( ! bp_current_action() || bp_is_current_action( 'general' ) ) {
+				require $this->path . 'bp-settings/actions/general.php';
+
+			// Specific to post requests.
+			} elseif ( bp_is_post_request() && in_array( bp_current_action(), $actions, true ) ) {
+				require $this->path . 'bp-settings/actions/' . bp_current_action() . '.php';
+			}
+		}
+
+		// Screens - User profile integration.
+		if ( bp_is_user() ) {
+			require $this->path . 'bp-settings/screens/general.php';
+
+			// Sub-nav items.
+			if ( in_array( bp_current_action(), $actions, true ) ) {
+				require $this->path . 'bp-settings/screens/' . bp_current_action() . '.php';
+			}
+		}
+	}
+
+	/**
+	 * Setup globals.
+	 *
+	 * The BP_SETTINGS_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args Array of arguments.
+	 */
+	public function setup_globals( $args = array() ) {
+
+		// Define a slug, if necessary.
+		if ( ! defined( 'BP_SETTINGS_SLUG' ) ) {
+			define( 'BP_SETTINGS_SLUG', $this->id );
+		}
+
+		// All globals for settings component.
+		parent::setup_globals( array(
+			'slug'          => BP_SETTINGS_SLUG,
+			'has_directory' => false,
+		) );
+	}
+
+	/**
+	 * Set up navigation.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $main_nav Array of main nav items.
+	 * @param array $sub_nav  Array of sub nav items.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$access        = bp_core_can_edit_settings();
+		$slug          = bp_get_settings_slug();
+		$settings_link = trailingslashit( $user_domain . $slug );
+
+		// Add the settings navigation item.
+		$main_nav = array(
+			'name'                    => __( 'Settings', 'buddypress' ),
+			'slug'                    => $slug,
+			'position'                => 100,
+			'show_for_displayed_user' => $access,
+			'screen_function'         => 'bp_settings_screen_general',
+			'default_subnav_slug'     => 'general'
+		);
+
+		// Add General Settings nav item.
+		$sub_nav[] = array(
+			'name'            => __( 'General', 'buddypress' ),
+			'slug'            => 'general',
+			'parent_url'      => $settings_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_settings_screen_general',
+			'position'        => 10,
+			'user_has_access' => $access
+		);
+
+		// Add Email nav item. Formerly called 'Notifications', we
+		// retain the old slug and function names for backward compat.
+		$sub_nav[] = array(
+			'name'            => __( 'Email', 'buddypress' ),
+			'slug'            => 'notifications',
+			'parent_url'      => $settings_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'bp_settings_screen_notification',
+			'position'        => 20,
+			'user_has_access' => $access
+		);
+
+		// Add Spam Account nav item.
+		if ( bp_current_user_can( 'bp_moderate' ) ) {
+			$sub_nav[] = array(
+				'name'            => __( 'Capabilities', 'buddypress' ),
+				'slug'            => 'capabilities',
+				'parent_url'      => $settings_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_settings_screen_capabilities',
+				'position'        => 80,
+				'user_has_access' => ! bp_is_my_profile()
+			);
+		}
+
+		// Add Delete Account nav item.
+		if ( ( ! bp_disable_account_deletion() && bp_is_my_profile() ) || bp_current_user_can( 'delete_users' ) ) {
+			$sub_nav[] = array(
+				'name'            => __( 'Delete Account', 'buddypress' ),
+				'slug'            => 'delete-account',
+				'parent_url'      => $settings_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'bp_settings_screen_delete_account',
+				'position'        => 90,
+				'user_has_access' => ! is_super_admin( bp_displayed_user_id() )
+			);
+		}
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up the Toolbar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $wp_admin_nav Array of Admin Bar items.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Setup the logged in user variables.
+			$settings_link = trailingslashit( bp_loggedin_user_domain() . bp_get_settings_slug() );
+
+			// Add main Settings menu.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => __( 'Settings', 'buddypress' ),
+				'href'   => $settings_link
+			);
+
+			// General Account.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-general',
+				'title'    => __( 'General', 'buddypress' ),
+				'href'     => $settings_link,
+				'position' => 10
+			);
+
+			// Notifications - only add the tab when there is something to display there.
+			if ( has_action( 'bp_notification_settings' ) ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-notifications',
+					'title'    => __( 'Email', 'buddypress' ),
+					'href'     => trailingslashit( $settings_link . 'notifications' ),
+					'position' => 20
+				);
+			}
+
+			// Delete Account
+			if ( !bp_current_user_can( 'bp_moderate' ) && ! bp_core_get_root_option( 'bp-disable-account-deletion' ) ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-delete-account',
+					'title'    => __( 'Delete Account', 'buddypress' ),
+					'href'     => trailingslashit( $settings_link . 'delete-account' ),
+					'position' => 90
+				);
+			}
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-settings/screens/capabilities.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/screens/capabilities.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/screens/capabilities.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Settings: User's "Settings > Capabilities" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Show the capabilities settings template.
+ *
+ * @since 1.6.0
+ */
+function bp_settings_screen_capabilities() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Filters the template file path to use for the capabilities settings screen.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value Directory path to look in for the template file.
+	 */
+	bp_core_load_template( apply_filters( 'bp_settings_screen_capabilities', 'members/single/settings/capabilities' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-settings/screens/delete-account.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/screens/delete-account.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/screens/delete-account.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Settings: User's "Settings > Delete Account" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Show the delete-account settings template.
+ *
+ * @since 1.5.0
+ */
+function bp_settings_screen_delete_account() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Filters the template file path to use for the delete-account settings screen.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value Directory path to look in for the template file.
+	 */
+	bp_core_load_template( apply_filters( 'bp_settings_screen_delete_account', 'members/single/settings/delete-account' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-settings/screens/general.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/screens/general.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/screens/general.php	(revision 12265)
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Settings: User's "Settings" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Show the general settings template.
+ *
+ * @since 1.5.0
+ */
+function bp_settings_screen_general() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Filters the template file path to use for the general settings screen.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value Directory path to look in for the template file.
+	 */
+	bp_core_load_template( apply_filters( 'bp_settings_screen_general_settings', 'members/single/settings/general' ) );
+}
+
+/**
+ * Removes 'Email' sub nav, if no component has registered options there.
+ *
+ * @since 2.2.0
+ */
+function bp_settings_remove_email_subnav() {
+	if ( ! has_action( 'bp_notification_settings' ) ) {
+		bp_core_remove_subnav_item( BP_SETTINGS_SLUG, 'notifications' );
+	}
+}
+add_action( 'bp_actions', 'bp_settings_remove_email_subnav' );
Index: /tags/4.0.0-beta1/src/bp-settings/screens/notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-settings/screens/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-settings/screens/notifications.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Settings: User's "Settings > Email" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage SettingsScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Show the notifications settings template.
+ *
+ * @since 1.5.0
+ */
+function bp_settings_screen_notification() {
+
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Filters the template file path to use for the notification settings screen.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value Directory path to look in for the template file.
+	 */
+	bp_core_load_template( apply_filters( 'bp_settings_screen_notification_settings', 'members/single/settings/notifications' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress-functions.php	(revision 12265)
@@ -0,0 +1,1993 @@
+<?php
+/**
+ * Functions of BuddyPress's Legacy theme.
+ *
+ * @since 1.7.0
+ *
+ * @package BuddyPress
+ * @subpackage BP_Theme_Compat
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Theme Setup ***************************************************************/
+
+if ( !class_exists( 'BP_Legacy' ) ) :
+
+/**
+ * Loads BuddyPress Legacy Theme functionality.
+ *
+ * This is not a real theme by WordPress standards, and is instead used as the
+ * fallback for any WordPress theme that does not have BuddyPress templates in it.
+ *
+ * To make your custom theme BuddyPress compatible and customize the templates, you
+ * can copy these files into your theme without needing to merge anything
+ * together; BuddyPress should safely handle the rest.
+ *
+ * See @link BP_Theme_Compat() for more.
+ *
+ * @since 1.7.0
+ *
+ * @package BuddyPress
+ * @subpackage BP_Theme_Compat
+ */
+class BP_Legacy extends BP_Theme_Compat {
+
+	/** Functions *************************************************************/
+
+	/**
+	 * The main BuddyPress (Legacy) Loader.
+	 *
+	 * @since 1.7.0
+	 *
+	 */
+	public function __construct() {
+		parent::start();
+	}
+
+	/**
+	 * Component global variables.
+	 *
+	 * You'll want to customize the values in here, so they match whatever your
+	 * needs are.
+	 *
+	 * @since 1.7.0
+	 */
+	protected function setup_globals() {
+		$bp            = buddypress();
+		$this->id      = 'legacy';
+		$this->name    = __( 'BuddyPress Legacy', 'buddypress' );
+		$this->version = bp_get_version();
+		$this->dir     = trailingslashit( $bp->themes_dir . '/bp-legacy' );
+		$this->url     = trailingslashit( $bp->themes_url . '/bp-legacy' );
+	}
+
+	/**
+	 * Setup the theme hooks.
+	 *
+	 * @since 1.7.0
+	 *
+	 */
+	protected function setup_actions() {
+
+		// Template Output.
+		add_filter( 'bp_get_activity_action_pre_meta', array( $this, 'secondary_avatars' ), 10, 2 );
+
+		// Filter BuddyPress template hierarchy and look for page templates.
+		add_filter( 'bp_get_buddypress_template', array( $this, 'theme_compat_page_templates' ), 10, 1 );
+
+		/** Scripts ***********************************************************/
+
+		add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_styles'   ) ); // Enqueue theme CSS
+		add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_scripts'  ) ); // Enqueue theme JS
+		add_filter( 'bp_enqueue_scripts', array( $this, 'localize_scripts' ) ); // Enqueue theme script localization
+
+		/** Body no-js Class **************************************************/
+
+		add_filter( 'body_class', array( $this, 'add_nojs_body_class' ), 20, 1 );
+
+		/** Buttons ***********************************************************/
+
+		if ( ! is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+			// Register buttons for the relevant component templates
+			// Friends button.
+			if ( bp_is_active( 'friends' ) )
+				add_action( 'bp_member_header_actions',    'bp_add_friend_button',           5 );
+
+			// Activity button.
+			if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() )
+				add_action( 'bp_member_header_actions',    'bp_send_public_message_button',  20 );
+
+			// Messages button.
+			if ( bp_is_active( 'messages' ) )
+				add_action( 'bp_member_header_actions',    'bp_send_private_message_button', 20 );
+
+			// Group buttons.
+			if ( bp_is_active( 'groups' ) ) {
+				add_action( 'bp_group_header_actions',          'bp_group_join_button',               5           );
+				add_action( 'bp_directory_groups_actions',      'bp_group_join_button'                            );
+				add_action( 'bp_groups_directory_group_filter', 'bp_legacy_theme_group_create_nav', 999           );
+				add_action( 'bp_after_group_admin_content',     'bp_legacy_groups_admin_screen_hidden_input'      );
+				add_action( 'bp_before_group_admin_form',       'bp_legacy_theme_group_manage_members_add_search' );
+			}
+
+			// Blog button.
+			if ( bp_is_active( 'blogs' ) ) {
+				add_action( 'bp_directory_blogs_actions',    'bp_blogs_visit_blog_button'           );
+				add_action( 'bp_blogs_directory_blog_types', 'bp_legacy_theme_blog_create_nav', 999 );
+			}
+		}
+
+		/** Notices ***********************************************************/
+
+		// Only hook the 'sitewide_notices' overlay if the Sitewide
+		// Notices widget is not in use (to avoid duplicate content).
+		if ( bp_is_active( 'messages' ) && ! is_active_widget( false, false, 'bp_messages_sitewide_notices_widget', true ) ) {
+			add_action( 'wp_footer', array( $this, 'sitewide_notices' ), 9999 );
+		}
+
+		/** Ajax **************************************************************/
+
+		$actions = array(
+
+			// Directory filters.
+			'blogs_filter'    => 'bp_legacy_theme_object_template_loader',
+			'forums_filter'   => 'bp_legacy_theme_object_template_loader',
+			'groups_filter'   => 'bp_legacy_theme_object_template_loader',
+			'members_filter'  => 'bp_legacy_theme_object_template_loader',
+			'messages_filter' => 'bp_legacy_theme_messages_template_loader',
+			'invite_filter'   => 'bp_legacy_theme_invite_template_loader',
+			'requests_filter' => 'bp_legacy_theme_requests_template_loader',
+
+			// Friends.
+			'accept_friendship' => 'bp_legacy_theme_ajax_accept_friendship',
+			'addremove_friend'  => 'bp_legacy_theme_ajax_addremove_friend',
+			'reject_friendship' => 'bp_legacy_theme_ajax_reject_friendship',
+
+			// Activity.
+			'activity_get_older_updates'  => 'bp_legacy_theme_activity_template_loader',
+			'activity_mark_fav'           => 'bp_legacy_theme_mark_activity_favorite',
+			'activity_mark_unfav'         => 'bp_legacy_theme_unmark_activity_favorite',
+			'activity_widget_filter'      => 'bp_legacy_theme_activity_template_loader',
+			'delete_activity'             => 'bp_legacy_theme_delete_activity',
+			'delete_activity_comment'     => 'bp_legacy_theme_delete_activity_comment',
+			'get_single_activity_content' => 'bp_legacy_theme_get_single_activity_content',
+			'new_activity_comment'        => 'bp_legacy_theme_new_activity_comment',
+			'post_update'                 => 'bp_legacy_theme_post_update',
+			'bp_spam_activity'            => 'bp_legacy_theme_spam_activity',
+			'bp_spam_activity_comment'    => 'bp_legacy_theme_spam_activity',
+
+			// Groups.
+			'groups_invite_user' => 'bp_legacy_theme_ajax_invite_user',
+			'joinleave_group'    => 'bp_legacy_theme_ajax_joinleave_group',
+
+			// Messages.
+			'messages_autocomplete_results' => 'bp_legacy_theme_ajax_messages_autocomplete_results',
+			'messages_close_notice'         => 'bp_legacy_theme_ajax_close_notice',
+			'messages_delete'               => 'bp_legacy_theme_ajax_messages_delete',
+			'messages_markread'             => 'bp_legacy_theme_ajax_message_markread',
+			'messages_markunread'           => 'bp_legacy_theme_ajax_message_markunread',
+			'messages_send_reply'           => 'bp_legacy_theme_ajax_messages_send_reply',
+		);
+
+		// Conditional actions.
+		if ( bp_is_active( 'messages', 'star' ) ) {
+			$actions['messages_star'] = 'bp_legacy_theme_ajax_messages_star_handler';
+		}
+
+		/**
+		 * Register all of these AJAX handlers.
+		 *
+		 * The "wp_ajax_" action is used for logged in users, and "wp_ajax_nopriv_"
+		 * executes for users that aren't logged in. This is for backpat with BP <1.6.
+		 */
+		foreach( $actions as $name => $function ) {
+			add_action( 'wp_ajax_'        . $name, $function );
+			add_action( 'wp_ajax_nopriv_' . $name, $function );
+		}
+
+		add_filter( 'bp_ajax_querystring', 'bp_legacy_theme_ajax_querystring', 10, 2 );
+
+		/** Override **********************************************************/
+
+		/**
+		 * Fires after all of the BuddyPress theme compat actions have been added.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param BP_Legacy $this Current BP_Legacy instance.
+		 */
+		do_action_ref_array( 'bp_theme_compat_actions', array( &$this ) );
+	}
+
+	/**
+	 * Load the theme CSS
+	 *
+	 * @since 1.7.0
+	 * @since 2.3.0 Support custom CSS file named after the current theme or parent theme.
+	 *
+	 */
+	public function enqueue_styles() {
+		$min = bp_core_get_minified_asset_suffix();
+
+		// Locate the BP stylesheet.
+		$ltr = $this->locate_asset_in_stack( "buddypress{$min}.css",     'css' );
+
+		// LTR.
+		if ( ! is_rtl() && isset( $ltr['location'], $ltr['handle'] ) ) {
+			wp_enqueue_style( $ltr['handle'], $ltr['location'], array(), $this->version, 'screen' );
+
+			if ( $min ) {
+				wp_style_add_data( $ltr['handle'], 'suffix', $min );
+			}
+		}
+
+		// RTL.
+		if ( is_rtl() ) {
+			$rtl = $this->locate_asset_in_stack( "buddypress-rtl{$min}.css", 'css' );
+
+			if ( isset( $rtl['location'], $rtl['handle'] ) ) {
+				$rtl['handle'] = str_replace( '-css', '-css-rtl', $rtl['handle'] );  // Backwards compatibility.
+				wp_enqueue_style( $rtl['handle'], $rtl['location'], array(), $this->version, 'screen' );
+
+				if ( $min ) {
+					wp_style_add_data( $rtl['handle'], 'suffix', $min );
+				}
+			}
+		}
+
+		// Compatibility stylesheets for specific themes.
+		$theme = $this->locate_asset_in_stack( get_template() . "{$min}.css", 'css' );
+		if ( ! is_rtl() && isset( $theme['location'] ) ) {
+			// Use a unique handle.
+			$theme['handle'] = 'bp-' . get_template();
+			wp_enqueue_style( $theme['handle'], $theme['location'], array(), $this->version, 'screen' );
+
+			if ( $min ) {
+				wp_style_add_data( $theme['handle'], 'suffix', $min );
+			}
+		}
+
+		// Compatibility stylesheet for specific themes, RTL-version.
+		if ( is_rtl() ) {
+			$theme_rtl = $this->locate_asset_in_stack( get_template() . "-rtl{$min}.css", 'css' );
+
+			if ( isset( $theme_rtl['location'] ) ) {
+				$theme_rtl['handle'] = $theme['handle'] . '-rtl';
+				wp_enqueue_style( $theme_rtl['handle'], $theme_rtl['location'], array(), $this->version, 'screen' );
+
+				if ( $min ) {
+					wp_style_add_data( $theme_rtl['handle'], 'suffix', $min );
+				}
+			}
+		}
+	}
+
+	/**
+	 * Enqueue the required JavaScript files
+	 *
+	 * @since 1.7.0
+	 */
+	public function enqueue_scripts() {
+		$min = bp_core_get_minified_asset_suffix();
+
+		// Locate the BP JS file.
+		$asset = $this->locate_asset_in_stack( "buddypress{$min}.js", 'js' );
+
+		// Enqueue the global JS, if found - AJAX will not work
+		// without it.
+		if ( isset( $asset['location'], $asset['handle'] ) ) {
+			wp_enqueue_script( $asset['handle'], $asset['location'], bp_core_get_js_dependencies(), $this->version );
+		}
+
+		/**
+		 * Filters whether directory filter settings ('scope', etc) should be stored in a persistent cookie.
+		 *
+		 * @since 4.0.0
+		 *
+		 * @param bool $store_filter_settings Whether to store settings. Defaults to true for logged-in users.
+		 */
+		$store_filter_settings = apply_filters( 'bp_legacy_store_filter_settings', is_user_logged_in() );
+
+		/**
+		 * Filters core JavaScript strings for internationalization before AJAX usage.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array $value Array of key/value pairs for AJAX usage.
+		 */
+		$params = apply_filters( 'bp_core_get_js_strings', array(
+			// Strings for display.
+			'accepted'            => __( 'Accepted', 'buddypress' ),
+			'close'               => __( 'Close', 'buddypress' ),
+			'comments'            => __( 'comments', 'buddypress' ),
+			'leave_group_confirm' => __( 'Are you sure you want to leave this group?', 'buddypress' ),
+			'mark_as_fav'	      => __( 'Favorite', 'buddypress' ),
+			'my_favs'             => __( 'My Favorites', 'buddypress' ),
+			'rejected'            => __( 'Rejected', 'buddypress' ),
+			'remove_fav'	      => __( 'Remove Favorite', 'buddypress' ),
+			'show_all'            => __( 'Show all', 'buddypress' ),
+			'show_all_comments'   => __( 'Show all comments for this thread', 'buddypress' ),
+			'show_x_comments'     => __( 'Show all comments (%d)', 'buddypress' ),
+			'unsaved_changes'     => __( 'Your profile has unsaved changes. If you leave the page, the changes will be lost.', 'buddypress' ),
+			'view'                => __( 'View', 'buddypress' ),
+
+			// Settings.
+			'store_filter_settings' => $store_filter_settings,
+		) );
+		wp_localize_script( $asset['handle'], 'BP_DTheme', $params );
+
+		// Maybe enqueue comment reply JS.
+		if ( is_singular() && bp_is_blog_page() && get_option( 'thread_comments' ) ) {
+			wp_enqueue_script( 'comment-reply' );
+		}
+
+		// Maybe enqueue password verify JS (register page or user settings page).
+		if ( bp_is_register_page() || ( function_exists( 'bp_is_user_settings_general' ) && bp_is_user_settings_general() ) ) {
+
+			// Locate the Register Page JS file.
+			$asset = $this->locate_asset_in_stack( "password-verify{$min}.js", 'js', 'bp-legacy-password-verify' );
+
+			$dependencies = array_merge( bp_core_get_js_dependencies(), array(
+				'password-strength-meter',
+			) );
+
+			// Enqueue script.
+			wp_enqueue_script( $asset['handle'] . '-password-verify', $asset['location'], $dependencies, $this->version);
+		}
+
+		// Star private messages.
+		if ( bp_is_active( 'messages', 'star' ) && bp_is_user_messages() ) {
+			wp_localize_script( $asset['handle'], 'BP_PM_Star', array(
+				'strings' => array(
+					'text_unstar'  => __( 'Unstar', 'buddypress' ),
+					'text_star'    => __( 'Star', 'buddypress' ),
+					'title_unstar' => __( 'Starred', 'buddypress' ),
+					'title_star'   => __( 'Not starred', 'buddypress' ),
+					'title_unstar_thread' => __( 'Remove all starred messages in this thread', 'buddypress' ),
+					'title_star_thread'   => __( 'Star the first message in this thread', 'buddypress' ),
+				),
+				'is_single_thread' => (int) bp_is_messages_conversation(),
+				'star_counter'     => 0,
+				'unstar_counter'   => 0
+			) );
+		}
+	}
+
+	/**
+	 * Get the URL and handle of a web-accessible CSS or JS asset
+	 *
+	 * We provide two levels of customizability with respect to where CSS
+	 * and JS files can be stored: (1) the child theme/parent theme/theme
+	 * compat hierarchy, and (2) the "template stack" of /buddypress/css/,
+	 * /community/css/, and /css/. In this way, CSS and JS assets can be
+	 * overloaded, and default versions provided, in exactly the same way
+	 * as corresponding PHP templates.
+	 *
+	 * We are duplicating some of the logic that is currently found in
+	 * bp_locate_template() and the _template_stack() functions. Those
+	 * functions were built with PHP templates in mind, and will require
+	 * refactoring in order to provide "stack" functionality for assets
+	 * that must be accessible both using file_exists() (the file path)
+	 * and at a public URI.
+	 *
+	 * This method is marked private, with the understanding that the
+	 * implementation is subject to change or removal in an upcoming
+	 * release, in favor of a unified _template_stack() system. Plugin
+	 * and theme authors should not attempt to use what follows.
+	 *
+	 * @since 1.8.0
+	 * @param string $file A filename like buddypress.css.
+	 * @param string $type Optional. Either "js" or "css" (the default).
+	 * @param string $script_handle Optional. If set, used as the script name in `wp_enqueue_script`.
+	 * @return array An array of data for the wp_enqueue_* function:
+	 *   'handle' (eg 'bp-child-css') and a 'location' (the URI of the
+	 *   asset)
+	 */
+	private function locate_asset_in_stack( $file, $type = 'css', $script_handle = '' ) {
+		$locations = array();
+
+		// Ensure the assets can be located when running from /src/.
+		if ( defined( 'BP_SOURCE_SUBDIRECTORY' ) && BP_SOURCE_SUBDIRECTORY === 'src' ) {
+			$file = str_replace( '.min', '', $file );
+		}
+
+		// No need to check child if template == stylesheet.
+		if ( is_child_theme() ) {
+			$locations[] = array(
+				'type' => 'bp-child',
+				'dir'  => get_stylesheet_directory(),
+				'uri'  => get_stylesheet_directory_uri(),
+				'file' => $file,
+			);
+
+			$locations[] = array(
+				'type' => 'bp-child',
+				'dir'  => get_stylesheet_directory(),
+				'uri'  => get_stylesheet_directory_uri(),
+				'file' => str_replace( '.min', '', $file ),
+			);
+		}
+
+		$locations[] = array(
+			'type' => 'bp-parent',
+			'dir'  => get_template_directory(),
+			'uri'  => get_template_directory_uri(),
+			'file' => str_replace( '.min', '', $file ),
+		);
+
+		$locations[] = array(
+			'type' => 'bp-legacy',
+			'dir'  => bp_get_theme_compat_dir(),
+			'uri'  => bp_get_theme_compat_url(),
+			'file' => $file,
+		);
+
+		// Subdirectories within the top-level $locations directories.
+		$subdirs = array(
+			'buddypress/' . $type,
+			'community/' . $type,
+			$type,
+		);
+
+		$retval = array();
+
+		foreach ( $locations as $location ) {
+			foreach ( $subdirs as $subdir ) {
+				if ( file_exists( trailingslashit( $location['dir'] ) . trailingslashit( $subdir ) . $location['file'] ) ) {
+					$retval['location'] = trailingslashit( $location['uri'] ) . trailingslashit( $subdir ) . $location['file'];
+					$retval['handle']   = ( $script_handle ) ? $script_handle : "{$location['type']}-{$type}";
+
+					break 2;
+				}
+			}
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Adds the no-js class to the body tag.
+	 *
+	 * This function ensures that the <body> element will have the 'no-js' class by default. If you're
+	 * using JavaScript for some visual functionality in your theme, and you want to provide noscript
+	 * support, apply those styles to body.no-js.
+	 *
+	 * The no-js class is removed by the JavaScript created in buddypress.js.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param array $classes Array of classes to append to body tag.
+	 * @return array $classes
+	 */
+	public function add_nojs_body_class( $classes ) {
+		if ( ! in_array( 'no-js', $classes ) )
+			$classes[] = 'no-js';
+
+		return array_unique( $classes );
+	}
+
+	/**
+	 * Load localizations for topic script.
+	 *
+	 * These localizations require information that may not be loaded even by init.
+	 *
+	 * @since 1.7.0
+	 */
+	public function localize_scripts() {
+	}
+
+	/**
+	 * Outputs sitewide notices markup in the footer.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @see https://buddypress.trac.wordpress.org/ticket/4802
+	 */
+	public function sitewide_notices() {
+		// Do not show notices if user is not logged in.
+		if ( ! is_user_logged_in() )
+			return;
+
+		// Add a class to determine if the admin bar is on or not.
+		$class = did_action( 'admin_bar_menu' ) ? 'admin-bar-on' : 'admin-bar-off';
+
+		echo '<div id="sitewide-notice" class="' . $class . '">';
+		bp_message_get_notices();
+		echo '</div>';
+	}
+
+	/**
+	 * Add secondary avatar image to this activity stream's record, if supported.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string               $action   The text of this activity.
+	 * @param BP_Activity_Activity $activity Activity object.
+	 * @return string
+	 */
+	function secondary_avatars( $action, $activity ) {
+		switch ( $activity->component ) {
+			case 'groups' :
+			case 'friends' :
+				// Only insert avatar if one exists.
+				if ( $secondary_avatar = bp_get_activity_secondary_avatar() ) {
+					$reverse_content = strrev( $action );
+					$position        = strpos( $reverse_content, 'a<' );
+					$action          = substr_replace( $action, $secondary_avatar, -$position - 2, 0 );
+				}
+				break;
+		}
+
+		return $action;
+	}
+
+	/**
+	 * Filter the default theme compatibility root template hierarchy, and prepend
+	 * a page template to the front if it's set.
+	 *
+	 * @see https://buddypress.trac.wordpress.org/ticket/6065
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param  array $templates Array of templates.
+	 *                         to use the defined page template for component's directory and its single items
+	 * @return array
+	 */
+	public function theme_compat_page_templates( $templates = array() ) {
+
+		/**
+		 * Filters whether or not we are looking at a directory to determine if to return early.
+		 *
+		 * @since 2.2.0
+		 *
+		 * @param bool $value Whether or not we are viewing a directory.
+		 */
+		if ( true === (bool) apply_filters( 'bp_legacy_theme_compat_page_templates_directory_only', ! bp_is_directory() ) ) {
+			return $templates;
+		}
+
+		// No page ID yet.
+		$page_id = 0;
+
+		// Get the WordPress Page ID for the current view.
+		foreach ( (array) buddypress()->pages as $component => $bp_page ) {
+
+			// Handles the majority of components.
+			if ( bp_is_current_component( $component ) ) {
+				$page_id = (int) $bp_page->id;
+			}
+
+			// Stop if not on a user page.
+			if ( ! bp_is_user() && ! empty( $page_id ) ) {
+				break;
+			}
+
+			// The Members component requires an explicit check due to overlapping components.
+			if ( bp_is_user() && ( 'members' === $component ) ) {
+				$page_id = (int) $bp_page->id;
+				break;
+			}
+		}
+
+		// Bail if no directory page set.
+		if ( 0 === $page_id ) {
+			return $templates;
+		}
+
+		// Check for page template.
+		$page_template = get_page_template_slug( $page_id );
+
+		// Add it to the beginning of the templates array so it takes precedence
+		// over the default hierarchy.
+		if ( ! empty( $page_template ) ) {
+
+			/**
+			 * Check for existence of template before adding it to template
+			 * stack to avoid accidentally including an unintended file.
+			 *
+			 * @see: https://buddypress.trac.wordpress.org/ticket/6190
+			 */
+			if ( '' !== locate_template( $page_template ) ) {
+				array_unshift( $templates, $page_template );
+			}
+		}
+
+		return $templates;
+	}
+}
+new BP_Legacy();
+endif;
+
+/**
+ * Add the Create a Group button to the Groups directory title.
+ *
+ * The bp-legacy puts the Create a Group button into the page title, to mimic
+ * the behavior of bp-default.
+ *
+ * @since 2.0.0
+ * @todo Deprecate
+ *
+ * @param string $title Groups directory title.
+ * @return string
+ */
+function bp_legacy_theme_group_create_button( $title ) {
+	return $title . ' ' . bp_get_group_create_button();
+}
+
+/**
+ * Add the Create a Group nav to the Groups directory navigation.
+ *
+ * The bp-legacy puts the Create a Group nav at the last position of
+ * the Groups directory navigation.
+ *
+ * @since 2.2.0
+ *
+ */
+function bp_legacy_theme_group_create_nav() {
+	bp_group_create_nav_item();
+}
+
+/**
+ * Renders the group ID hidden input on group admin screens.
+ *
+ * @since 2.7.0
+ *
+ * @return string|null html
+ */
+function bp_legacy_groups_admin_screen_hidden_input() {
+	?>
+ 	<input type="hidden" name="group-id" id="group-id" value="<?php bp_group_id(); ?>" />
+	<?php
+}
+
+/**
+ * Add the Create a Site button to the Sites directory title.
+ *
+ * The bp-legacy puts the Create a Site button into the page title, to mimic
+ * the behavior of bp-default.
+ *
+ * @since 2.0.0
+ * @todo Deprecate
+ *
+ * @param string $title Sites directory title.
+ * @return string
+ */
+function bp_legacy_theme_blog_create_button( $title ) {
+	return $title . ' ' . bp_get_blog_create_button();
+}
+
+/**
+ * Add the Create a Site nav to the Sites directory navigation.
+ *
+ * The bp-legacy puts the Create a Site nav at the last position of
+ * the Sites directory navigation.
+ *
+ * @since 2.2.0
+ *
+ */
+function bp_legacy_theme_blog_create_nav() {
+	bp_blog_create_nav_item();
+}
+
+/**
+ * This function looks scarier than it actually is. :)
+ * Each object loop (activity/members/groups/blogs/forums) contains default
+ * parameters to show specific information based on the page we are currently
+ * looking at.
+ *
+ * The following function will take into account any cookies set in the JS and
+ * allow us to override the parameters sent. That way we can change the results
+ * returned without reloading the page.
+ *
+ * By using cookies we can also make sure that user settings are retained
+ * across page loads.
+ *
+ * @since 1.2.0
+ *
+ * @param string $query_string Query string for the current request.
+ * @param string $object       Object for cookie.
+ * @return string Query string for the component loops.
+ */
+function bp_legacy_theme_ajax_querystring( $query_string, $object ) {
+	if ( empty( $object ) )
+		return '';
+
+	// Set up the cookies passed on this AJAX request. Store a local var to avoid conflicts.
+	if ( ! empty( $_POST['cookie'] ) ) {
+		$_BP_COOKIE = wp_parse_args( str_replace( '; ', '&', urldecode( $_POST['cookie'] ) ) );
+	} else {
+		$_BP_COOKIE = &$_COOKIE;
+	}
+
+	$qs = array();
+
+	/**
+	 * Check if any cookie values are set. If there are then override the
+	 * default params passed to the template loop.
+	 */
+
+	// Activity stream filtering on action.
+	if ( ! empty( $_BP_COOKIE['bp-' . $object . '-filter'] ) && '-1' != $_BP_COOKIE['bp-' . $object . '-filter'] ) {
+		$qs[] = 'type=' . urlencode( $_BP_COOKIE['bp-' . $object . '-filter'] );
+
+		if ( bp_is_active( 'activity' ) ) {
+			$actions = bp_activity_get_actions_for_context();
+
+			// Handle multiple actions (eg. 'friendship_accepted,friendship_created')
+			$action_filter = explode( ',', $_BP_COOKIE['bp-' . $object . '-filter'] );
+
+			// See if action filter matches registered actions. If so, add it to qs.
+			if ( ! array_diff( $action_filter, wp_list_pluck( $actions, 'key' ) ) ) {
+				$qs[] = 'action=' . join( ',', $action_filter );
+			}
+		}
+	}
+
+	if ( ! empty( $_BP_COOKIE['bp-' . $object . '-scope'] ) ) {
+		if ( 'personal' == $_BP_COOKIE['bp-' . $object . '-scope'] ) {
+			$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+			$qs[] = 'user_id=' . $user_id;
+		}
+
+		// Activity stream scope only on activity directory.
+		if ( 'all' != $_BP_COOKIE['bp-' . $object . '-scope'] && ! bp_displayed_user_id() && ! bp_is_single_item() )
+			$qs[] = 'scope=' . urlencode( $_BP_COOKIE['bp-' . $object . '-scope'] );
+	}
+
+	// If page and search_terms have been passed via the AJAX post request, use those.
+	if ( ! empty( $_POST['page'] ) && '-1' != $_POST['page'] )
+		$qs[] = 'page=' . absint( $_POST['page'] );
+
+	// Excludes activity just posted and avoids duplicate ids.
+	if ( ! empty( $_POST['exclude_just_posted'] ) ) {
+		$just_posted = wp_parse_id_list( $_POST['exclude_just_posted'] );
+		$qs[] = 'exclude=' . implode( ',', $just_posted );
+	}
+
+	// To get newest activities.
+	if ( ! empty( $_POST['offset'] ) ) {
+		$qs[] = 'offset=' . intval( $_POST['offset'] );
+	}
+
+	$object_search_text = bp_get_search_default_text( $object );
+	if ( ! empty( $_POST['search_terms'] ) && is_string( $_POST['search_terms'] ) && $object_search_text != $_POST['search_terms'] && 'false' != $_POST['search_terms'] && 'undefined' != $_POST['search_terms'] )
+		$qs[] = 'search_terms=' . urlencode( $_POST['search_terms'] );
+
+	// Now pass the querystring to override default values.
+	$query_string = empty( $qs ) ? '' : join( '&', (array) $qs );
+
+	$object_filter = '';
+	if ( isset( $_BP_COOKIE['bp-' . $object . '-filter'] ) )
+		$object_filter = $_BP_COOKIE['bp-' . $object . '-filter'];
+
+	$object_scope = '';
+	if ( isset( $_BP_COOKIE['bp-' . $object . '-scope'] ) )
+		$object_scope = $_BP_COOKIE['bp-' . $object . '-scope'];
+
+	$object_page = '';
+	if ( isset( $_BP_COOKIE['bp-' . $object . '-page'] ) )
+		$object_page = $_BP_COOKIE['bp-' . $object . '-page'];
+
+	$object_search_terms = '';
+	if ( isset( $_BP_COOKIE['bp-' . $object . '-search-terms'] ) )
+		$object_search_terms = $_BP_COOKIE['bp-' . $object . '-search-terms'];
+
+	$object_extras = '';
+	if ( isset( $_BP_COOKIE['bp-' . $object . '-extras'] ) )
+		$object_extras = $_BP_COOKIE['bp-' . $object . '-extras'];
+
+	/**
+	 * Filters the AJAX query string for the component loops.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $query_string        The query string we are working with.
+	 * @param string $object              The type of page we are on.
+	 * @param string $object_filter       The current object filter.
+	 * @param string $object_scope        The current object scope.
+	 * @param string $object_page         The current object page.
+	 * @param string $object_search_terms The current object search terms.
+	 * @param string $object_extras       The current object extras.
+	 */
+	return apply_filters( 'bp_legacy_theme_ajax_querystring', $query_string, $object, $object_filter, $object_scope, $object_page, $object_search_terms, $object_extras );
+}
+
+/**
+ * Load the template loop for the current object.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null Prints template loop for the specified object
+ */
+function bp_legacy_theme_object_template_loader() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no object passed.
+	if ( empty( $_POST['object'] ) ) {
+		return;
+	}
+
+	// Sanitize the object.
+	$object = sanitize_title( $_POST['object'] );
+
+	// Bail if object is not an active component to prevent arbitrary file inclusion.
+	if ( ! bp_is_active( $object ) ) {
+		return;
+	}
+
+	/**
+	 * AJAX requests happen too early to be seen by bp_update_is_directory()
+	 * so we do it manually here to ensure templates load with the correct
+	 * context. Without this check, templates will load the 'single' version
+	 * of themselves rather than the directory version.
+	 */
+	if ( ! bp_current_action() )
+		bp_update_is_directory( true, bp_current_component() );
+
+	// The template part can be overridden by the calling JS function.
+	if ( ! empty( $_POST['template'] ) && 'groups/single/members' === $_POST['template'] ) {
+		$template_part = 'groups/single/members.php';
+	} else {
+		$template_part = $object . '/' . $object . '-loop.php';
+	}
+
+	$template_path = bp_locate_template( array( $template_part ), false );
+
+	$template_path = apply_filters( 'bp_legacy_object_template_path', $template_path );
+
+	load_template( $template_path );
+	exit();
+}
+
+/**
+ * Load messages template loop when searched on the private message page
+ *
+ * @since 1.6.0
+ *
+ * @return string|null Prints template loop for the Messages component.
+ */
+function bp_legacy_theme_messages_template_loader() {
+	bp_get_template_part( 'members/single/messages/messages-loop' );
+	exit();
+}
+
+/**
+ * Load group invitations loop to handle pagination requests sent via AJAX.
+ *
+ * @since 2.0.0
+ */
+function bp_legacy_theme_invite_template_loader() {
+	bp_get_template_part( 'groups/single/invites-loop' );
+	exit();
+}
+
+/**
+ * Load group membership requests loop to handle pagination requests sent via AJAX.
+ *
+ * @since 2.0.0
+ */
+function bp_legacy_theme_requests_template_loader() {
+	bp_get_template_part( 'groups/single/requests-loop' );
+	exit();
+}
+
+/**
+ * Load the activity loop template when activity is requested via AJAX.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null JSON object containing 'contents' (output of the template loop
+ *                     for the Activity component) and 'feed_url' (URL to the relevant RSS feed).
+ */
+function bp_legacy_theme_activity_template_loader() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	$scope = '';
+	if ( ! empty( $_POST['scope'] ) )
+		$scope = $_POST['scope'];
+
+	// We need to calculate and return the feed URL for each scope.
+	switch ( $scope ) {
+		case 'friends':
+			$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/friends/feed/';
+			break;
+		case 'groups':
+			$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/groups/feed/';
+			break;
+		case 'favorites':
+			$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/favorites/feed/';
+			break;
+		case 'mentions':
+			$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/mentions/feed/';
+
+			if ( isset( $_POST['_wpnonce_activity_filter'] ) && wp_verify_nonce( wp_unslash( $_POST['_wpnonce_activity_filter'] ), 'activity_filter' ) ) {
+				bp_activity_clear_new_mentions( bp_loggedin_user_id() );
+			}
+
+			break;
+		default:
+			$feed_url = home_url( bp_get_activity_root_slug() . '/feed/' );
+			break;
+	}
+
+	// Buffer the loop in the template to a var for JS to spit out.
+	ob_start();
+	bp_get_template_part( 'activity/activity-loop' );
+	$result['contents'] = ob_get_contents();
+
+	/**
+	 * Filters the feed URL for when activity is requested via AJAX.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $feed_url URL for the feed to be used.
+	 * @param string $scope    Scope for the activity request.
+	 */
+	$result['feed_url'] = apply_filters( 'bp_legacy_theme_activity_feed_url', $feed_url, $scope );
+	ob_end_clean();
+
+	exit( json_encode( $result ) );
+}
+
+/**
+ * Processes Activity updates received via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_post_update() {
+	$bp = buddypress();
+
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'post_update', '_wpnonce_post_update' );
+
+	if ( ! is_user_logged_in() )
+		exit( '-1' );
+
+	if ( empty( $_POST['content'] ) )
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . __( 'Please enter some content to post.', 'buddypress' ) . '</p></div>' );
+
+	$activity_id = 0;
+	$item_id     = 0;
+	$object      = '';
+
+
+	// Try to get the item id from posted variables.
+	if ( ! empty( $_POST['item_id'] ) ) {
+		$item_id = (int) $_POST['item_id'];
+	}
+
+	// Try to get the object from posted variables.
+	if ( ! empty( $_POST['object'] ) ) {
+		$object  = sanitize_key( $_POST['object'] );
+
+	// If the object is not set and we're in a group, set the item id and the object
+	} elseif ( bp_is_group() ) {
+		$item_id = bp_get_current_group_id();
+		$object = 'groups';
+	}
+
+	if ( ! $object && bp_is_active( 'activity' ) ) {
+		$activity_id = bp_activity_post_update( array( 'content' => $_POST['content'], 'error_type' => 'wp_error' ) );
+
+	} elseif ( 'groups' === $object ) {
+		if ( $item_id && bp_is_active( 'groups' ) )
+			$activity_id = groups_post_update( array( 'content' => $_POST['content'], 'group_id' => $item_id, 'error_type' => 'wp_error' ) );
+
+	} else {
+
+		/** This filter is documented in bp-activity/bp-activity-actions.php */
+		$activity_id = apply_filters( 'bp_activity_custom_update', false, $object, $item_id, $_POST['content'] );
+	}
+
+	if ( false === $activity_id ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . __( 'There was a problem posting your update. Please try again.', 'buddypress' ) . '</p></div>' );
+	} elseif ( is_wp_error( $activity_id ) && $activity_id->get_error_code() ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . $activity_id->get_error_message() . '</p></div>' );
+	}
+
+	$last_recorded = ! empty( $_POST['since'] ) ? date( 'Y-m-d H:i:s', intval( $_POST['since'] ) ) : 0;
+	if ( $last_recorded ) {
+		$activity_args = array( 'since' => $last_recorded );
+		$bp->activity->last_recorded = $last_recorded;
+		add_filter( 'bp_get_activity_css_class', 'bp_activity_newest_class', 10, 1 );
+	} else {
+		$activity_args = array( 'include' => $activity_id );
+	}
+
+	if ( bp_has_activities ( $activity_args ) ) {
+		while ( bp_activities() ) {
+			bp_the_activity();
+			bp_get_template_part( 'activity/entry' );
+		}
+	}
+
+	if ( ! empty( $last_recorded ) ) {
+		remove_filter( 'bp_get_activity_css_class', 'bp_activity_newest_class', 10 );
+	}
+
+	exit;
+}
+
+/**
+ * Posts new Activity comments received via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @global BP_Activity_Template $activities_template
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_new_activity_comment() {
+	global $activities_template;
+
+	$bp = buddypress();
+
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'new_activity_comment', '_wpnonce_new_activity_comment' );
+
+	if ( ! is_user_logged_in() ) {
+		exit( '-1' );
+	}
+
+	$feedback = __( 'There was an error posting your reply. Please try again.', 'buddypress' );
+
+	if ( empty( $_POST['content'] ) ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . esc_html__( 'Please do not leave the comment area blank.', 'buddypress' ) . '</p></div>' );
+	}
+
+	if ( empty( $_POST['form_id'] ) || empty( $_POST['comment_id'] ) || ! is_numeric( $_POST['form_id'] ) || ! is_numeric( $_POST['comment_id'] ) ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . esc_html( $feedback ) . '</p></div>' );
+	}
+
+	$comment_id = bp_activity_new_comment( array(
+		'activity_id' => $_POST['form_id'],
+		'content'     => $_POST['content'],
+		'parent_id'   => $_POST['comment_id'],
+		'error_type'  => 'wp_error'
+	) );
+
+	if ( is_wp_error( $comment_id ) ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . esc_html( $comment_id->get_error_message() ) . '</p></div>' );
+	}
+
+	// Load the new activity item into the $activities_template global.
+	bp_has_activities( 'display_comments=stream&hide_spam=false&show_hidden=true&include=' . $comment_id );
+
+	// Swap the current comment with the activity item we just loaded.
+	if ( isset( $activities_template->activities[0] ) ) {
+		$activities_template->activity = new stdClass();
+		$activities_template->activity->id              = $activities_template->activities[0]->item_id;
+		$activities_template->activity->current_comment = $activities_template->activities[0];
+
+		// Because the whole tree has not been loaded, we manually
+		// determine depth.
+		$depth = 1;
+		$parent_id = (int) $activities_template->activities[0]->secondary_item_id;
+		while ( $parent_id !== (int) $activities_template->activities[0]->item_id ) {
+			$depth++;
+			$p_obj = new BP_Activity_Activity( $parent_id );
+			$parent_id = (int) $p_obj->secondary_item_id;
+		}
+		$activities_template->activity->current_comment->depth = $depth;
+	}
+
+	// Get activity comment template part.
+	bp_get_template_part( 'activity/comment' );
+
+	unset( $activities_template );
+	exit;
+}
+
+/**
+ * Deletes an Activity item received via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_delete_activity() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'bp_activity_delete_link' );
+
+	if ( ! is_user_logged_in() )
+		exit( '-1' );
+
+	if ( empty( $_POST['id'] ) || ! is_numeric( $_POST['id'] ) )
+		exit( '-1' );
+
+	$activity = new BP_Activity_Activity( (int) $_POST['id'] );
+
+	// Check access.
+	if ( ! bp_activity_user_can_delete( $activity ) )
+		exit( '-1' );
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_before_action_delete_activity', $activity->id, $activity->user_id );
+
+	if ( ! bp_activity_delete( array( 'id' => $activity->id, 'user_id' => $activity->user_id ) ) )
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . __( 'There was a problem when deleting. Please try again.', 'buddypress' ) . '</p></div>' );
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_action_delete_activity', $activity->id, $activity->user_id );
+	exit;
+}
+
+/**
+ * Deletes an Activity comment received via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_delete_activity_comment() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'bp_activity_delete_link' );
+
+	if ( empty( $_POST['id'] ) || ! is_numeric( $_POST['id'] ) ) {
+		exit( '-1' );
+	}
+
+	if ( ! is_user_logged_in() ) {
+		exit( '-1' );
+	}
+
+	$comment = new BP_Activity_Activity( $_POST['id'] );
+
+	// Check access.
+	if ( ! bp_current_user_can( 'bp_moderate' ) && $comment->user_id != bp_loggedin_user_id() )
+		exit( '-1' );
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_before_action_delete_activity', $_POST['id'], $comment->user_id );
+
+	if ( ! bp_activity_delete_comment( $comment->item_id, $comment->id ) )
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . __( 'There was a problem when deleting. Please try again.', 'buddypress' ) . '</p></div>' );
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_action_delete_activity', $_POST['id'], $comment->user_id );
+	exit;
+}
+
+/**
+ * AJAX spam an activity item or comment.
+ *
+ * @since 1.6.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_spam_activity() {
+	$bp = buddypress();
+
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Check that user is logged in, Activity Streams are enabled, and Akismet is present.
+	if ( ! is_user_logged_in() || ! bp_is_active( 'activity' ) || empty( $bp->activity->akismet ) )
+		exit( '-1' );
+
+	// Check an item ID was passed.
+	if ( empty( $_POST['id'] ) || ! is_numeric( $_POST['id'] ) )
+		exit( '-1' );
+
+	// Is the current user allowed to spam items?
+	if ( ! bp_activity_user_can_mark_spam() )
+		exit( '-1' );
+
+	// Load up the activity item.
+	$activity = new BP_Activity_Activity( (int) $_POST['id'] );
+	if ( empty( $activity->component ) )
+		exit( '-1' );
+
+	// Check nonce.
+	check_admin_referer( 'bp_activity_akismet_spam_' . $activity->id );
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_before_action_spam_activity', $activity->id, $activity );
+
+	// Mark as spam.
+	bp_activity_mark_as_spam( $activity );
+	$activity->save();
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_action_spam_activity', $activity->id, $activity->user_id );
+	exit;
+}
+
+/**
+ * Mark an activity as a favourite via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_mark_activity_favorite() {
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	if ( ! isset( $_POST['nonce'] ) ) {
+		return;
+	}
+
+	// Either the 'mark' or 'unmark' nonce is accepted, for backward compatibility.
+	$nonce = wp_unslash( $_POST['nonce'] );
+	if ( ! wp_verify_nonce( $nonce, 'mark_favorite' ) && ! wp_verify_nonce( $nonce, 'unmark_favorite' ) ) {
+		return;
+	}
+
+	if ( bp_activity_add_user_favorite( $_POST['id'] ) )
+		_e( 'Remove Favorite', 'buddypress' );
+	else
+		_e( 'Favorite', 'buddypress' );
+
+	exit;
+}
+
+/**
+ * Un-favourite an activity via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_unmark_activity_favorite() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	if ( ! isset( $_POST['nonce'] ) ) {
+		return;
+	}
+
+	// Either the 'mark' or 'unmark' nonce is accepted, for backward compatibility.
+	$nonce = wp_unslash( $_POST['nonce'] );
+	if ( ! wp_verify_nonce( $nonce, 'mark_favorite' ) && ! wp_verify_nonce( $nonce, 'unmark_favorite' ) ) {
+		return;
+	}
+
+	if ( bp_activity_remove_user_favorite( $_POST['id'] ) )
+		_e( 'Favorite', 'buddypress' );
+	else
+		_e( 'Remove Favorite', 'buddypress' );
+
+	exit;
+}
+
+/**
+ * Fetches an activity's full, non-excerpted content via a POST request.
+ * Used for the 'Read More' link on long activity items.
+ *
+ * @since 1.5.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_get_single_activity_content() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	$activity_array = bp_activity_get_specific( array(
+		'activity_ids'     => $_POST['activity_id'],
+		'display_comments' => 'stream'
+	) );
+
+	$activity = ! empty( $activity_array['activities'][0] ) ? $activity_array['activities'][0] : false;
+
+	if ( empty( $activity ) )
+		exit; // @todo: error?
+
+	/**
+	 * Fires before the return of an activity's full, non-excerpted content via a POST request.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $activity Activity content. Passed by reference.
+	 */
+	do_action_ref_array( 'bp_legacy_theme_get_single_activity_content', array( &$activity ) );
+
+	// Activity content retrieved through AJAX should run through normal filters, but not be truncated.
+	remove_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+
+	/** This filter is documented in bp-activity/bp-activity-template.php */
+	$content = apply_filters_ref_array( 'bp_get_activity_content_body', array( $activity->content, &$activity ) );
+
+	exit( $content );
+}
+
+/**
+ * Invites a friend to join a group via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @todo Audit return types
+ */
+function bp_legacy_theme_ajax_invite_user() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	check_ajax_referer( 'groups_invite_uninvite_user' );
+
+	if ( ! $_POST['friend_id'] || ! $_POST['friend_action'] || ! $_POST['group_id'] )
+		return;
+
+	if ( ! bp_groups_user_can_send_invites( $_POST['group_id'] ) )
+		return;
+
+	$group_id = (int) $_POST['group_id'];
+	$friend_id = (int) $_POST['friend_id'];
+
+	if ( 'invite' == $_POST['friend_action'] ) {
+		if ( ! friends_check_friendship( bp_loggedin_user_id(), $_POST['friend_id'] ) ) {
+			return;
+		}
+
+		$group = groups_get_group( $group_id );
+
+		// Users who have previously requested membership do not need
+		// another invitation created for them.
+		if ( groups_check_for_membership_request( $friend_id, $group_id ) ) {
+			$user_status = 'is_pending';
+
+		// Create the user invitation.
+		} elseif ( groups_invite_user( array( 'user_id' => $friend_id, 'group_id' => $group_id ) ) ) {
+			$user_status = 'is_invited';
+
+		// Miscellaneous failure.
+		} else {
+			return;
+		}
+
+		$user = new BP_Core_User( $friend_id );
+
+		$uninvite_url = bp_is_current_action( 'create' )
+			? bp_get_groups_directory_permalink() . 'create/step/group-invites/?user_id=' . $friend_id
+			: bp_get_group_permalink( $group )    . 'send-invites/remove/' . $friend_id;
+
+		echo '<li id="uid-' . esc_attr( $user->id ) . '">';
+		echo $user->avatar_thumb;
+		echo '<h4>' . $user->user_link . '</h4>';
+		echo '<span class="activity">' . esc_attr( $user->last_active ) . '</span>';
+		echo '<div class="action">
+				<a class="button remove" href="' . wp_nonce_url( $uninvite_url, 'groups_invite_uninvite_user' ) . '" id="uid-' . esc_attr( $user->id ) . '">' . __( 'Remove Invite', 'buddypress' ) . '</a>
+			  </div>';
+
+		if ( 'is_pending' == $user_status ) {
+			echo '<p class="description">' . sprintf( __( '%s has previously requested to join this group. Sending an invitation will automatically add the member to the group.', 'buddypress' ), $user->user_link ) . '</p>';
+		}
+
+		echo '</li>';
+		exit;
+
+	} elseif ( 'uninvite' == $_POST['friend_action'] ) {
+		// Users who have previously requested membership should not
+		// have their requests deleted on the "uninvite" action.
+		if ( BP_Groups_Member::check_for_membership_request( $friend_id, $group_id ) ) {
+			return;
+		}
+
+		// Remove the unsent invitation.
+		if ( ! groups_uninvite_user( $friend_id, $group_id ) ) {
+			return;
+		}
+
+		exit;
+
+	} else {
+		return;
+	}
+}
+
+/**
+ * Friend/un-friend a user via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_ajax_addremove_friend() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Cast fid as an integer.
+	$friend_id = (int) $_POST['fid'];
+
+	$user = get_user_by( 'id', $friend_id );
+	if ( ! $user ) {
+		die( __( 'No member found by that ID.', 'buddypress' ) );
+	}
+
+	// Trying to cancel friendship.
+	if ( 'is_friend' == BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $friend_id ) ) {
+		check_ajax_referer( 'friends_remove_friend' );
+
+		if ( ! friends_remove_friend( bp_loggedin_user_id(), $friend_id ) ) {
+			echo __( 'Friendship could not be canceled.', 'buddypress' );
+		} else {
+			echo '<a id="friend-' . esc_attr( $friend_id ) . '" class="friendship-button not_friends add" rel="add" href="' . wp_nonce_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/add-friend/' . $friend_id, 'friends_add_friend' ) . '">' . __( 'Add Friend', 'buddypress' ) . '</a>';
+		}
+
+	// Trying to request friendship.
+	} elseif ( 'not_friends' == BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $friend_id ) ) {
+		check_ajax_referer( 'friends_add_friend' );
+
+		if ( ! friends_add_friend( bp_loggedin_user_id(), $friend_id ) ) {
+			echo __(' Friendship could not be requested.', 'buddypress' );
+		} else {
+			echo '<a id="friend-' . esc_attr( $friend_id ) . '" class="remove friendship-button pending_friend requested" rel="remove" href="' . wp_nonce_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/requests/cancel/' . $friend_id . '/', 'friends_withdraw_friendship' ) . '" class="requested">' . __( 'Cancel Friendship Request', 'buddypress' ) . '</a>';
+		}
+
+	// Trying to cancel pending request.
+	} elseif ( 'pending' == BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $friend_id ) ) {
+		check_ajax_referer( 'friends_withdraw_friendship' );
+
+		if ( friends_withdraw_friendship( bp_loggedin_user_id(), $friend_id ) ) {
+			echo '<a id="friend-' . esc_attr( $friend_id ) . '" class="friendship-button not_friends add" rel="add" href="' . wp_nonce_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/add-friend/' . $friend_id, 'friends_add_friend' ) . '">' . __( 'Add Friend', 'buddypress' ) . '</a>';
+		} else {
+			echo __("Friendship request could not be cancelled.", 'buddypress');
+		}
+
+	// Request already pending.
+	} else {
+		echo __( 'Request Pending', 'buddypress' );
+	}
+
+	exit;
+}
+
+/**
+ * Accept a user friendship request via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_ajax_accept_friendship() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	check_admin_referer( 'friends_accept_friendship' );
+
+	if ( ! friends_accept_friendship( (int) $_POST['id'] ) )
+		echo "-1<div id='message' class='error'><p>" . __( 'There was a problem accepting that request. Please try again.', 'buddypress' ) . '</p></div>';
+
+	exit;
+}
+
+/**
+ * Reject a user friendship request via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_ajax_reject_friendship() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	check_admin_referer( 'friends_reject_friendship' );
+
+	if ( ! friends_reject_friendship( (int) $_POST['id'] ) )
+		echo "-1<div id='message' class='error'><p>" . __( 'There was a problem rejecting that request. Please try again.', 'buddypress' ) . '</p></div>';
+
+	exit;
+}
+
+/**
+ * Join or leave a group when clicking the "join/leave" button via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_ajax_joinleave_group() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Cast gid as integer.
+	$group_id = (int) $_POST['gid'];
+
+	if ( groups_is_user_banned( bp_loggedin_user_id(), $group_id ) )
+		return;
+
+	if ( ! $group = groups_get_group( $group_id ) )
+		return;
+
+	// Client doesn't distinguish between different request types, so we infer from user status.
+	if ( groups_is_user_member( bp_loggedin_user_id(), $group->id ) ) {
+		$request_type = 'leave_group';
+	} elseif ( groups_check_user_has_invite( bp_loggedin_user_id(), $group->id ) ) {
+		$request_type = 'accept_invite';
+	} elseif ( 'private' === $group->status ) {
+		$request_type = 'request_membership';
+	} else {
+		$request_type = 'join_group';
+	}
+
+	switch ( $request_type ) {
+		case 'join_group' :
+			if ( ! bp_current_user_can( 'groups_join_group', array( 'group_id' => $group->id ) ) ) {
+				esc_html_e( 'Error joining group', 'buddypress' );
+			}
+
+			check_ajax_referer( 'groups_join_group' );
+
+			if ( ! groups_join_group( $group->id ) ) {
+				_e( 'Error joining group', 'buddypress' );
+			} else {
+				echo '<a id="group-' . esc_attr( $group->id ) . '" class="group-button leave-group" rel="leave" href="' . wp_nonce_url( bp_get_group_permalink( $group ) . 'leave-group', 'groups_leave_group' ) . '">' . __( 'Leave Group', 'buddypress' ) . '</a>';
+			}
+		break;
+
+		case 'accept_invite' :
+			if ( ! bp_current_user_can( 'groups_request_membership', array( 'group_id' => $group->id ) ) ) {
+				esc_html_e( 'Error accepting invitation', 'buddypress' );
+			}
+
+			check_ajax_referer( 'groups_accept_invite' );
+
+			if ( ! groups_accept_invite( bp_loggedin_user_id(), $group->id ) ) {
+				_e( 'Error requesting membership', 'buddypress' );
+			} else {
+				echo '<a id="group-' . esc_attr( $group->id ) . '" class="group-button leave-group" rel="leave" href="' . wp_nonce_url( bp_get_group_permalink( $group ) . 'leave-group', 'groups_leave_group' ) . '">' . __( 'Leave Group', 'buddypress' ) . '</a>';
+			}
+		break;
+
+		case 'request_membership' :
+			check_ajax_referer( 'groups_request_membership' );
+
+			if ( ! groups_send_membership_request( bp_loggedin_user_id(), $group->id ) ) {
+				_e( 'Error requesting membership', 'buddypress' );
+			} else {
+				echo '<a id="group-' . esc_attr( $group->id ) . '" class="group-button disabled pending membership-requested" rel="membership-requested" href="' . bp_get_group_permalink( $group ) . '">' . __( 'Request Sent', 'buddypress' ) . '</a>';
+			}
+		break;
+
+		case 'leave_group' :
+			check_ajax_referer( 'groups_leave_group' );
+
+			if ( ! groups_leave_group( $group->id ) ) {
+				_e( 'Error leaving group', 'buddypress' );
+			} elseif ( 'public' === $group->status ) {
+				echo '<a id="group-' . esc_attr( $group->id ) . '" class="group-button join-group" rel="join" href="' . wp_nonce_url( bp_get_group_permalink( $group ) . 'join', 'groups_join_group' ) . '">' . __( 'Join Group', 'buddypress' ) . '</a>';
+			} else {
+				echo '<a id="group-' . esc_attr( $group->id ) . '" class="group-button request-membership" rel="join" href="' . wp_nonce_url( bp_get_group_permalink( $group ) . 'request-membership', 'groups_request_membership' ) . '">' . __( 'Request Membership', 'buddypress' ) . '</a>';
+			}
+		break;
+	}
+
+	exit;
+}
+
+/**
+ * Close and keep closed site wide notices from an admin in the sidebar, via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_ajax_close_notice() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	$nonce_check = isset( $_POST['nonce'] ) && wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'bp_messages_close_notice' );
+
+	if ( ! $nonce_check || ! isset( $_POST['notice_id'] ) ) {
+		echo "-1<div id='message' class='error'><p>" . __( 'There was a problem closing the notice.', 'buddypress' ) . '</p></div>';
+
+	} else {
+		$user_id    = get_current_user_id();
+		$notice_ids = bp_get_user_meta( $user_id, 'closed_notices', true );
+		if ( ! is_array( $notice_ids ) ) {
+			$notice_ids = array();
+		}
+
+		$notice_ids[] = (int) $_POST['notice_id'];
+
+		bp_update_user_meta( $user_id, 'closed_notices', $notice_ids );
+	}
+
+	exit;
+}
+
+/**
+ * Send a private message reply to a thread via a POST request.
+ *
+ * @since 1.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_ajax_messages_send_reply() {
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	check_ajax_referer( 'messages_send_message' );
+
+	$result = messages_new_message( array( 'thread_id' => (int) $_REQUEST['thread_id'], 'content' => $_REQUEST['content'] ) );
+
+	if ( !empty( $result ) ) {
+
+		// Pretend we're in the message loop.
+		global $thread_template;
+
+		bp_thread_has_messages( array( 'thread_id' => (int) $_REQUEST['thread_id'] ) );
+
+		// Set the current message to the 2nd last.
+		$thread_template->message = end( $thread_template->thread->messages );
+		$thread_template->message = prev( $thread_template->thread->messages );
+
+		// Set current message to current key.
+		$thread_template->current_message = key( $thread_template->thread->messages );
+
+		// Now manually iterate message like we're in the loop.
+		bp_thread_the_message();
+
+		// Manually call oEmbed
+		// this is needed because we're not at the beginning of the loop.
+		bp_messages_embed();
+
+		// Add new-message css class.
+		add_filter( 'bp_get_the_thread_message_css_class', function( $retval ) {
+			$retval[] = 'new-message';
+			return $retval;
+		} );
+
+		// Output single message template part.
+		bp_get_template_part( 'members/single/messages/message' );
+
+		// Clean up the loop.
+		bp_thread_messages();
+
+	} else {
+		echo "-1<div id='message' class='error'><p>" . __( 'There was a problem sending that reply. Please try again.', 'buddypress' ) . '</p></div>';
+	}
+
+	exit;
+}
+
+/**
+ * Mark a private message as unread in your inbox via a POST request.
+ *
+ * No longer used.
+ *
+ * @since 1.2.0
+ * @deprecated 2.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_ajax_message_markunread() {
+	die( '-1' );
+}
+
+/**
+ * Mark a private message as read in your inbox via a POST request.
+ *
+ * No longer used.
+ *
+ * @since 1.2.0
+ * @deprecated 2.2.0
+ *
+ * @return mixed String on error, void on success.
+ */
+function bp_legacy_theme_ajax_message_markread() {
+	die( '-1' );
+}
+
+/**
+ * Delete a private message(s) in your inbox via a POST request.
+ *
+ * No longer used.
+ *
+ * @since 1.2.0
+ * @deprecated 2.2.0
+ *
+ * @return string|null HTML
+ */
+function bp_legacy_theme_ajax_messages_delete() {
+	die( '-1' );
+}
+
+/**
+ * AJAX handler for autocomplete.
+ *
+ * Displays friends only, unless BP_MESSAGES_AUTOCOMPLETE_ALL is defined.
+ *
+ * @since 1.2.0
+ */
+function bp_legacy_theme_ajax_messages_autocomplete_results() {
+
+	/**
+	 * Filters the max results default value for ajax messages autocomplete results.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $value Max results for autocomplete. Default 10.
+	 */
+	$limit = isset( $_GET['limit'] ) ? absint( $_GET['limit'] )          : (int) apply_filters( 'bp_autocomplete_max_results', 10 );
+	$term  = isset( $_GET['q'] )     ? sanitize_text_field( $_GET['q'] ) : '';
+
+	// Include everyone in the autocomplete, or just friends?
+	if ( bp_is_current_component( bp_get_messages_slug() ) ) {
+		$only_friends = ( buddypress()->messages->autocomplete_all === false );
+	} else {
+		$only_friends = true;
+	}
+
+	$suggestions = bp_core_get_suggestions( array(
+		'limit'        => $limit,
+		'only_friends' => $only_friends,
+		'term'         => $term,
+		'type'         => 'members',
+	) );
+
+	if ( $suggestions && ! is_wp_error( $suggestions ) ) {
+		foreach ( $suggestions as $user ) {
+
+			// Note that the final line break acts as a delimiter for the
+			// autocomplete JavaScript and thus should not be removed.
+			printf( '<span id="%s" href="#"></span><img src="%s" style="width: 15px"> &nbsp; %s (%s)' . "\n",
+				esc_attr( 'link-' . $user->ID ),
+				esc_url( $user->image ),
+				esc_html( $user->name ),
+				esc_html( $user->ID )
+			);
+		}
+	}
+
+	exit;
+}
+
+/**
+ * AJAX callback to set a message's star status.
+ *
+ * @since 2.3.0
+ */
+function bp_legacy_theme_ajax_messages_star_handler() {
+	if ( false === bp_is_active( 'messages', 'star' ) || empty( $_POST['message_id'] ) ) {
+		return;
+	}
+
+	// Check nonce.
+	check_ajax_referer( 'bp-messages-star-' . (int) $_POST['message_id'], 'nonce' );
+
+	// Check capability.
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		return;
+	}
+
+	if ( true === bp_messages_star_set_action( array(
+		'action'     => $_POST['star_status'],
+		'message_id' => (int) $_POST['message_id'],
+		'bulk'       => ! empty( $_POST['bulk'] ) ? true : false
+	 ) ) ) {
+		echo '1';
+		die();
+	}
+
+	echo '-1';
+	die();
+}
+
+/**
+ * BP Legacy's callback for the cover image feature.
+ *
+ * @since  2.4.0
+ *
+ * @param  array $params the current component's feature parameters.
+ * @return null|string An array to inform about the css handle to attach the css rules to
+ */
+function bp_legacy_theme_cover_image( $params = array() ) {
+	if ( empty( $params ) ) {
+		return;
+	}
+
+	// Avatar height - padding - 1/2 avatar height.
+	$avatar_offset = $params['height'] - 5 - round( (int) bp_core_avatar_full_height() / 2 );
+
+	// Header content offset + spacing.
+	$top_offset  = bp_core_avatar_full_height() - 10;
+	$left_offset = bp_core_avatar_full_width() + 20;
+
+	$cover_image = ( !empty( $params['cover_image'] ) ) ? 'background-image: url(' . $params['cover_image'] . ');' : '';
+
+	$hide_avatar_style = '';
+
+	// Adjust the cover image header, in case avatars are completely disabled.
+	if ( ! buddypress()->avatar->show_avatars ) {
+		$hide_avatar_style = '
+			#buddypress #item-header-cover-image #item-header-avatar {
+				display:  none;
+			}
+		';
+
+		if ( bp_is_user() ) {
+			$hide_avatar_style = '
+				#buddypress #item-header-cover-image #item-header-avatar a {
+					display: block;
+					height: ' . $top_offset . 'px;
+					margin: 0 15px 19px 0;
+				}
+
+				#buddypress div#item-header #item-header-cover-image #item-header-content {
+					margin-left: auto;
+				}
+			';
+		}
+	}
+
+	return '
+		/* Cover image */
+		#buddypress #header-cover-image {
+			height: ' . $params["height"] . 'px;
+			' . $cover_image . '
+		}
+
+		#buddypress #create-group-form #header-cover-image {
+			margin: 1em 0;
+			position: relative;
+		}
+
+		.bp-user #buddypress #item-header {
+			padding-top: 0;
+		}
+
+		#buddypress #item-header-cover-image #item-header-avatar {
+			margin-top: '. $avatar_offset .'px;
+			float: left;
+			overflow: visible;
+			width: auto;
+		}
+
+		#buddypress div#item-header #item-header-cover-image #item-header-content {
+			clear: both;
+			float: left;
+			margin-left: ' . $left_offset . 'px;
+			margin-top: -' . $top_offset . 'px;
+			width: auto;
+		}
+
+		body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content,
+		body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+			clear: none;
+			margin-top: ' . $params["height"] . 'px;
+			margin-left: 0;
+			max-width: 50%;
+		}
+
+		body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+			max-width: 20%;
+			padding-top: 20px;
+		}
+
+		' . $hide_avatar_style . '
+
+		#buddypress div#item-header-cover-image .user-nicename a,
+		#buddypress div#item-header-cover-image .user-nicename {
+			font-size: 200%;
+			color: #fff;
+			margin: 0 0 0.6em;
+			text-rendering: optimizelegibility;
+			text-shadow: 0 0 3px rgba( 0, 0, 0, 0.8 );
+		}
+
+		#buddypress #item-header-cover-image #item-header-avatar img.avatar {
+			background: rgba( 255, 255, 255, 0.8 );
+			border: solid 2px #fff;
+		}
+
+		#buddypress #item-header-cover-image #item-header-avatar a {
+			border: 0;
+			text-decoration: none;
+		}
+
+		#buddypress #item-header-cover-image #item-buttons {
+			margin: 0 0 10px;
+			padding: 0 0 5px;
+		}
+
+		#buddypress #item-header-cover-image #item-buttons:after {
+			clear: both;
+			content: "";
+			display: table;
+		}
+
+		@media screen and (max-width: 782px) {
+			#buddypress #item-header-cover-image #item-header-avatar,
+			.bp-user #buddypress #item-header #item-header-cover-image #item-header-avatar,
+			#buddypress div#item-header #item-header-cover-image #item-header-content {
+				width: 100%;
+				text-align: center;
+			}
+
+			#buddypress #item-header-cover-image #item-header-avatar a {
+				display: inline-block;
+			}
+
+			#buddypress #item-header-cover-image #item-header-avatar img {
+				margin: 0;
+			}
+
+			#buddypress div#item-header #item-header-cover-image #item-header-content,
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content,
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+				margin: 0;
+			}
+
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content,
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+				max-width: 100%;
+			}
+
+			#buddypress div#item-header-cover-image h2 a,
+			#buddypress div#item-header-cover-image h2 {
+				color: inherit;
+				text-shadow: none;
+				margin: 25px 0 0;
+				font-size: 200%;
+			}
+
+			#buddypress #item-header-cover-image #item-buttons div {
+				float: none;
+				display: inline-block;
+			}
+
+			#buddypress #item-header-cover-image #item-buttons:before {
+				content: "";
+			}
+
+			#buddypress #item-header-cover-image #item-buttons {
+				margin: 5px 0;
+			}
+		}
+	';
+}
+
+/**
+ * Add a search box to a single group's manage members screen.
+ *
+ * @since 2.7.0
+ *
+ * @return string|null HTML for the search form.
+ */
+function bp_legacy_theme_group_manage_members_add_search() {
+	if ( bp_is_action_variable( 'manage-members' ) ) :
+		?>
+		<div id="members-dir-search" class="dir-search no-ajax" role="search">
+			<?php bp_directory_members_search_form(); ?>
+		</div>
+		<?php
+	endif;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/activity-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/activity-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/activity-loop.php	(revision 12265)
@@ -0,0 +1,70 @@
+<?php
+/**
+ * BuddyPress - Activity Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the start of the activity loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_activity_loop' ); ?>
+
+<?php if ( bp_has_activities( bp_ajax_querystring( 'activity' ) ) ) : ?>
+
+	<?php if ( empty( $_POST['page'] ) ) : ?>
+
+		<ul id="activity-stream" class="activity-list item-list">
+
+	<?php endif; ?>
+
+	<?php while ( bp_activities() ) : bp_the_activity(); ?>
+
+		<?php bp_get_template_part( 'activity/entry' ); ?>
+
+	<?php endwhile; ?>
+
+	<?php if ( bp_activity_has_more_items() ) : ?>
+
+		<li class="load-more">
+			<a href="<?php bp_activity_load_more_link() ?>"><?php _e( 'Load More', 'buddypress' ); ?></a>
+		</li>
+
+	<?php endif; ?>
+
+	<?php if ( empty( $_POST['page'] ) ) : ?>
+
+		</ul>
+
+	<?php endif; ?>
+
+<?php else : ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'Sorry, there was no activity found. Please try a different filter.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the finish of the activity loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_activity_loop' ); ?>
+
+<?php if ( empty( $_POST['page'] ) ) : ?>
+
+	<form action="" name="activity-loop-form" id="activity-loop-form" method="post">
+
+		<?php wp_nonce_field( 'activity_filter', '_wpnonce_activity_filter' ); ?>
+
+	</form>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/comment.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/comment.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/comment.php	(revision 12265)
@@ -0,0 +1,71 @@
+<?php
+/**
+ * BuddyPress - Activity Stream Comment
+ *
+ * This template is used by bp_activity_comments() functions to show
+ * each activity.
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of an activity comment.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_before_activity_comment' ); ?>
+
+<li id="acomment-<?php bp_activity_comment_id(); ?>">
+	<div class="acomment-avatar">
+		<a href="<?php bp_activity_comment_user_link(); ?>">
+			<?php bp_activity_avatar( 'type=thumb&user_id=' . bp_get_activity_comment_user_id() ); ?>
+		</a>
+	</div>
+
+	<div class="acomment-meta">
+		<?php
+		/* translators: 1: user profile link, 2: user name, 3: activity permalink, 4: ISO8601 timestamp, 5: activity relative timestamp */
+		printf( __( '<a href="%1$s">%2$s</a> replied <a href="%3$s" class="activity-time-since"><span class="time-since" data-livestamp="%4$s">%5$s</span></a>', 'buddypress' ), bp_get_activity_comment_user_link(), bp_get_activity_comment_name(), bp_get_activity_comment_permalink(), bp_core_get_iso8601_date( bp_get_activity_comment_date_recorded() ), bp_get_activity_comment_date_recorded() );
+		?>
+	</div>
+
+	<div class="acomment-content"><?php bp_activity_comment_content(); ?></div>
+
+	<div class="acomment-options">
+
+		<?php if ( is_user_logged_in() && bp_activity_can_comment_reply( bp_activity_current_comment() ) ) : ?>
+
+			<a href="#acomment-<?php bp_activity_comment_id(); ?>" class="acomment-reply bp-primary-action" id="acomment-reply-<?php bp_activity_id(); ?>-from-<?php bp_activity_comment_id(); ?>"><?php _e( 'Reply', 'buddypress' ); ?></a>
+
+		<?php endif; ?>
+
+		<?php if ( bp_activity_user_can_delete() ) : ?>
+
+			<a href="<?php bp_activity_comment_delete_link(); ?>" class="delete acomment-delete confirm bp-secondary-action" rel="nofollow"><?php _e( 'Delete', 'buddypress' ); ?></a>
+
+		<?php endif; ?>
+
+		<?php
+
+		/**
+		 * Fires after the defualt comment action options display.
+		 *
+		 * @since 1.6.0
+		 */
+		do_action( 'bp_activity_comment_options' ); ?>
+
+	</div>
+
+	<?php bp_activity_recurse_comments( bp_activity_current_comment() ); ?>
+</li>
+
+<?php
+
+/**
+ * Fires after the display of an activity comment.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_after_activity_comment' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/entry.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/entry.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/entry.php	(revision 12265)
@@ -0,0 +1,171 @@
+<?php
+/**
+ * BuddyPress - Activity Stream (Single Item)
+ *
+ * This template is used by activity-loop.php and AJAX functions to show
+ * each activity.
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of an activity entry.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_activity_entry' ); ?>
+
+<li class="<?php bp_activity_css_class(); ?>" id="activity-<?php bp_activity_id(); ?>">
+	<div class="activity-avatar">
+		<a href="<?php bp_activity_user_link(); ?>">
+
+			<?php bp_activity_avatar(); ?>
+
+		</a>
+	</div>
+
+	<div class="activity-content">
+
+		<div class="activity-header">
+
+			<?php bp_activity_action(); ?>
+
+		</div>
+
+		<?php if ( bp_activity_has_content() ) : ?>
+
+			<div class="activity-inner">
+
+				<?php bp_activity_content_body(); ?>
+
+			</div>
+
+		<?php endif; ?>
+
+		<?php
+
+		/**
+		 * Fires after the display of an activity entry content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_activity_entry_content' ); ?>
+
+		<div class="activity-meta">
+
+			<?php if ( bp_get_activity_type() == 'activity_comment' ) : ?>
+
+				<a href="<?php bp_activity_thread_permalink(); ?>" class="button view bp-secondary-action"><?php _e( 'View Conversation', 'buddypress' ); ?></a>
+
+			<?php endif; ?>
+
+			<?php if ( is_user_logged_in() ) : ?>
+
+				<?php if ( bp_activity_can_comment() ) : ?>
+
+					<a href="<?php bp_activity_comment_link(); ?>" class="button acomment-reply bp-primary-action" id="acomment-comment-<?php bp_activity_id(); ?>"><?php printf( __( 'Comment %s', 'buddypress' ), '<span>' . bp_activity_get_comment_count() . '</span>' ); ?></a>
+
+				<?php endif; ?>
+
+				<?php if ( bp_activity_can_favorite() ) : ?>
+
+					<?php if ( !bp_get_activity_is_favorite() ) : ?>
+
+						<a href="<?php bp_activity_favorite_link(); ?>" class="button fav bp-secondary-action"><?php _e( 'Favorite', 'buddypress' ); ?></a>
+
+					<?php else : ?>
+
+						<a href="<?php bp_activity_unfavorite_link(); ?>" class="button unfav bp-secondary-action"><?php _e( 'Remove Favorite', 'buddypress' ); ?></a>
+
+					<?php endif; ?>
+
+				<?php endif; ?>
+
+				<?php if ( bp_activity_user_can_delete() ) bp_activity_delete_link(); ?>
+
+				<?php
+
+				/**
+				 * Fires at the end of the activity entry meta data area.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_activity_entry_meta' ); ?>
+
+			<?php endif; ?>
+
+		</div>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires before the display of the activity entry comments.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_activity_entry_comments' ); ?>
+
+	<?php if ( ( bp_activity_get_comment_count() || bp_activity_can_comment() ) || bp_is_single_activity() ) : ?>
+
+		<div class="activity-comments">
+
+			<?php bp_activity_comments(); ?>
+
+			<?php if ( is_user_logged_in() && bp_activity_can_comment() ) : ?>
+
+				<form action="<?php bp_activity_comment_form_action(); ?>" method="post" id="ac-form-<?php bp_activity_id(); ?>" class="ac-form"<?php bp_activity_comment_form_nojs_display(); ?>>
+					<div class="ac-reply-avatar"><?php bp_loggedin_user_avatar( 'width=' . BP_AVATAR_THUMB_WIDTH . '&height=' . BP_AVATAR_THUMB_HEIGHT ); ?></div>
+					<div class="ac-reply-content">
+						<div class="ac-textarea">
+							<label for="ac-input-<?php bp_activity_id(); ?>" class="bp-screen-reader-text"><?php
+								/* translators: accessibility text */
+								_e( 'Comment', 'buddypress' );
+							?></label>
+							<textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input bp-suggestions" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
+						</div>
+						<input type="submit" name="ac_form_submit" value="<?php esc_attr_e( 'Post', 'buddypress' ); ?>" /> &nbsp; <a href="#" class="ac-reply-cancel"><?php _e( 'Cancel', 'buddypress' ); ?></a>
+						<input type="hidden" name="comment_form_id" value="<?php bp_activity_id(); ?>" />
+					</div>
+
+					<?php
+
+					/**
+					 * Fires after the activity entry comment form.
+					 *
+					 * @since 1.5.0
+					 */
+					do_action( 'bp_activity_entry_comments' ); ?>
+
+					<?php wp_nonce_field( 'new_activity_comment', '_wpnonce_new_activity_comment' ); ?>
+
+				</form>
+
+			<?php endif; ?>
+
+		</div>
+
+	<?php endif; ?>
+
+	<?php
+
+	/**
+	 * Fires after the display of the activity entry comments.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_activity_entry_comments' ); ?>
+
+</li>
+
+<?php
+
+/**
+ * Fires after the display of an activity entry.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_activity_entry' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/index.php	(revision 12265)
@@ -0,0 +1,237 @@
+<?php
+/**
+ * BuddyPress Activity templates
+ *
+ * @since 2.3.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the activity directory listing.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_before_directory_activity' ); ?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the activity directory display content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_directory_activity_content' ); ?>
+
+	<?php if ( is_user_logged_in() ) : ?>
+
+		<?php bp_get_template_part( 'activity/post-form' ); ?>
+
+	<?php endif; ?>
+
+	<div id="template-notices" role="alert" aria-atomic="true">
+		<?php
+
+		/**
+		 * Fires towards the top of template pages for notice display.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'template_notices' ); ?>
+
+	</div>
+
+	<div class="item-list-tabs activity-type-tabs" aria-label="<?php esc_attr_e( 'Sitewide activities navigation', 'buddypress' ); ?>" role="navigation">
+		<ul>
+			<?php
+
+			/**
+			 * Fires before the listing of activity type tabs.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_before_activity_type_tab_all' ); ?>
+
+			<li class="selected" id="activity-all"><a href="<?php bp_activity_directory_permalink(); ?>"><?php printf( __( 'All Members %s', 'buddypress' ), '<span>' . bp_get_total_member_count() . '</span>' ); ?></a></li>
+
+			<?php if ( is_user_logged_in() ) : ?>
+
+				<?php
+
+				/**
+				 * Fires before the listing of friends activity type tab.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_before_activity_type_tab_friends' ); ?>
+
+				<?php if ( bp_is_active( 'friends' ) ) : ?>
+
+					<?php if ( bp_get_total_friend_count( bp_loggedin_user_id() ) ) : ?>
+
+						<li id="activity-friends"><a href="<?php echo bp_loggedin_user_domain() . bp_get_activity_slug() . '/' . bp_get_friends_slug() . '/'; ?>"><?php printf( __( 'My Friends %s', 'buddypress' ), '<span>' . bp_get_total_friend_count( bp_loggedin_user_id() ) . '</span>' ); ?></a></li>
+
+					<?php endif; ?>
+
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires before the listing of groups activity type tab.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_before_activity_type_tab_groups' ); ?>
+
+				<?php if ( bp_is_active( 'groups' ) ) : ?>
+
+					<?php if ( bp_get_total_group_count_for_user( bp_loggedin_user_id() ) ) : ?>
+
+						<?php
+						printf(
+							'<li id="activity-groups"><a href="%1$s">%2$s</a></li>',
+							esc_url( bp_loggedin_user_domain() . bp_get_activity_slug() . '/' . bp_get_groups_slug() . '/' ),
+							sprintf(
+								/* translators: %s: total joined groups count for the current user */
+								__( 'My Groups %s', 'buddypress' ),
+								'<span>' . bp_get_total_group_count_for_user( bp_loggedin_user_id() ) . '</span>'
+							)
+						);
+						?>
+
+					<?php endif; ?>
+
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires before the listing of favorites activity type tab.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_before_activity_type_tab_favorites' ); ?>
+
+				<?php if ( bp_get_total_favorite_count_for_user( bp_loggedin_user_id() ) ) : ?>
+
+					<li id="activity-favorites"><a href="<?php echo bp_loggedin_user_domain() . bp_get_activity_slug() . '/favorites/'; ?>"><?php printf( __( 'My Favorites %s', 'buddypress' ), '<span>' . bp_get_total_favorite_count_for_user( bp_loggedin_user_id() ) . '</span>' ); ?></a></li>
+
+				<?php endif; ?>
+
+				<?php if ( bp_activity_do_mentions() ) : ?>
+
+					<?php
+
+					/**
+					 * Fires before the listing of mentions activity type tab.
+					 *
+					 * @since 1.2.0
+					 */
+					do_action( 'bp_before_activity_type_tab_mentions' ); ?>
+
+					<li id="activity-mentions"><a href="<?php echo bp_loggedin_user_domain() . bp_get_activity_slug() . '/mentions/'; ?>"><?php _e( 'Mentions', 'buddypress' ); ?><?php if ( bp_get_total_mention_count_for_user( bp_loggedin_user_id() ) ) : ?> <strong><span><?php printf( _nx( '%s new', '%s new', bp_get_total_mention_count_for_user( bp_loggedin_user_id() ), 'Number of new activity mentions', 'buddypress' ), bp_get_total_mention_count_for_user( bp_loggedin_user_id() ) ); ?></span></strong><?php endif; ?></a></li>
+
+				<?php endif; ?>
+
+			<?php endif; ?>
+
+			<?php
+
+			/**
+			 * Fires after the listing of activity type tabs.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_activity_type_tabs' ); ?>
+		</ul>
+	</div><!-- .item-list-tabs -->
+
+	<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Activity secondary navigation', 'buddypress' ); ?>" role="navigation">
+		<ul>
+			<li class="feed"><a href="<?php bp_sitewide_activity_feed_link(); ?>" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( 'RSS Feed', 'buddypress' ); ?>" aria-label="<?php esc_attr_e( 'RSS Feed', 'buddypress' ); ?>"><?php _e( 'RSS', 'buddypress' ); ?></a></li>
+
+			<?php
+
+			/**
+			 * Fires before the display of the activity syndication options.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_activity_syndication_options' ); ?>
+
+			<li id="activity-filter-select" class="last">
+				<label for="activity-filter-by"><?php _e( 'Show:', 'buddypress' ); ?></label>
+				<select id="activity-filter-by">
+					<option value="-1"><?php _e( '&mdash; Everything &mdash;', 'buddypress' ); ?></option>
+
+					<?php bp_activity_show_filters(); ?>
+
+					<?php
+
+					/**
+					 * Fires inside the select input for activity filter by options.
+					 *
+					 * @since 1.2.0
+					 */
+					do_action( 'bp_activity_filter_options' ); ?>
+
+				</select>
+			</li>
+		</ul>
+	</div><!-- .item-list-tabs -->
+
+	<?php
+
+	/**
+	 * Fires before the display of the activity list.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_before_directory_activity_list' ); ?>
+
+	<div class="activity" aria-live="polite" aria-atomic="true" aria-relevant="all">
+
+		<?php bp_get_template_part( 'activity/activity-loop' ); ?>
+
+	</div><!-- .activity -->
+
+	<?php
+
+	/**
+	 * Fires after the display of the activity list.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_activity_list' ); ?>
+
+	<?php
+
+	/**
+	 * Fires inside and displays the activity directory display content.
+	 */
+	do_action( 'bp_directory_activity_content' ); ?>
+
+	<?php
+
+	/**
+	 * Fires after the activity directory display content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_directory_activity_content' ); ?>
+
+	<?php
+
+	/**
+	 * Fires after the activity directory listing.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_activity' ); ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/post-form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/post-form.php	(revision 12265)
@@ -0,0 +1,105 @@
+<?php
+/**
+ * BuddyPress - Activity Post Form
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<form action="<?php bp_activity_post_form_action(); ?>" method="post" id="whats-new-form" name="whats-new-form">
+
+	<?php
+
+	/**
+	 * Fires before the activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_activity_post_form' ); ?>
+
+	<div id="whats-new-avatar">
+		<a href="<?php echo bp_loggedin_user_domain(); ?>">
+			<?php bp_loggedin_user_avatar( 'width=' . bp_core_avatar_thumb_width() . '&height=' . bp_core_avatar_thumb_height() ); ?>
+		</a>
+	</div>
+
+	<p class="activity-greeting"><?php if ( bp_is_group() )
+		printf( __( "What's new in %s, %s?", 'buddypress' ), bp_get_group_name(), bp_get_user_firstname( bp_get_loggedin_user_fullname() ) );
+	else
+		printf( __( "What's new, %s?", 'buddypress' ), bp_get_user_firstname( bp_get_loggedin_user_fullname() ) );
+	?></p>
+
+	<div id="whats-new-content">
+		<div id="whats-new-textarea">
+			<label for="whats-new" class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'Post what\'s new', 'buddypress' );
+			?></label>
+			<textarea class="bp-suggestions" name="whats-new" id="whats-new" cols="50" rows="10"
+				<?php if ( bp_is_group() ) : ?>data-suggestions-group-id="<?php echo esc_attr( (int) bp_get_current_group_id() ); ?>" <?php endif; ?>
+			><?php if ( isset( $_GET['r'] ) ) : ?>@<?php echo esc_textarea( $_GET['r'] ); ?> <?php endif; ?></textarea>
+		</div>
+
+		<div id="whats-new-options">
+			<div id="whats-new-submit">
+				<input type="submit" name="aw-whats-new-submit" id="aw-whats-new-submit" value="<?php esc_attr_e( 'Post Update', 'buddypress' ); ?>" />
+			</div>
+
+			<?php if ( bp_is_active( 'groups' ) && !bp_is_my_profile() && !bp_is_group() ) : ?>
+
+				<div id="whats-new-post-in-box">
+
+					<?php _e( 'Post in', 'buddypress' ); ?>:
+
+					<label for="whats-new-post-in" class="bp-screen-reader-text"><?php
+						/* translators: accessibility text */
+						_e( 'Post in', 'buddypress' );
+					?></label>
+					<select id="whats-new-post-in" name="whats-new-post-in">
+						<option selected="selected" value="0"><?php _e( 'My Profile', 'buddypress' ); ?></option>
+
+						<?php if ( bp_has_groups( 'user_id=' . bp_loggedin_user_id() . '&type=alphabetical&max=100&per_page=100&populate_extras=0&update_meta_cache=0' ) ) :
+							while ( bp_groups() ) : bp_the_group(); ?>
+
+								<option value="<?php bp_group_id(); ?>"><?php bp_group_name(); ?></option>
+
+							<?php endwhile;
+						endif; ?>
+
+					</select>
+				</div>
+				<input type="hidden" id="whats-new-post-object" name="whats-new-post-object" value="groups" />
+
+			<?php elseif ( bp_is_group_activity() ) : ?>
+
+				<input type="hidden" id="whats-new-post-object" name="whats-new-post-object" value="groups" />
+				<input type="hidden" id="whats-new-post-in" name="whats-new-post-in" value="<?php bp_group_id(); ?>" />
+
+			<?php endif; ?>
+
+			<?php
+
+			/**
+			 * Fires at the end of the activity post form markup.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_activity_post_form_options' ); ?>
+
+		</div><!-- #whats-new-options -->
+	</div><!-- #whats-new-content -->
+
+	<?php wp_nonce_field( 'post_update', '_wpnonce_post_update' ); ?>
+	<?php
+
+	/**
+	 * Fires after the activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_activity_post_form' ); ?>
+
+</form><!-- #whats-new-form -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/activity/single/home.php	(revision 12265)
@@ -0,0 +1,34 @@
+<?php
+/**
+ * BuddyPress - Home
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div id="buddypress">
+
+	<div id="template-notices" role="alert" aria-atomic="true">
+		<?php
+
+		/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+		do_action( 'template_notices' ); ?>
+
+	</div>
+
+	<div class="activity no-ajax">
+		<?php if ( bp_has_activities( 'display_comments=threaded&show_hidden=true&include=' . bp_current_action() ) ) : ?>
+
+			<ul id="activity-stream" class="activity-list item-list">
+			<?php while ( bp_activities() ) : bp_the_activity(); ?>
+
+				<?php bp_get_template_part( 'activity/entry' ); ?>
+
+			<?php endwhile; ?>
+			</ul>
+
+		<?php endif; ?>
+	</div>
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/camera.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/camera.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/camera.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * BuddyPress Avatars camera template.
+ *
+ * This template is used to create the camera Backbone views.
+ *
+ * @since 2.3.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-attachments
+ * @version 3.0.0
+ */
+
+?>
+<script id="tmpl-bp-avatar-webcam" type="text/html">
+	<# if ( ! data.user_media ) { #>
+		<div id="bp-webcam-message">
+			<p class="warning"><?php esc_html_e( 'Your browser does not support this feature.', 'buddypress' );?></p>
+		</div>
+	<# } else { #>
+		<div id="avatar-to-crop"></div>
+		<div class="avatar-crop-management">
+			<div id="avatar-crop-pane" class="avatar" style="width:{{data.w}}px; height:{{data.h}}px"></div>
+			<div id="avatar-crop-actions">
+				<a class="button avatar-webcam-capture" href="#"><?php esc_html_e( 'Capture', 'buddypress' );?></a>
+				<a class="button avatar-webcam-save" href="#"><?php esc_html_e( 'Save', 'buddypress' );?></a>
+			</div>
+		</div>
+	<# } #>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/crop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/crop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/crop.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * BuddyPress Avatars crop template.
+ *
+ * This template is used to create the crop Backbone views.
+ *
+ * @since 2.3.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-attachments
+ * @version 3.0.0
+ */
+
+?>
+<script id="tmpl-bp-avatar-item" type="text/html">
+	<div id="avatar-to-crop">
+		<img src="{{data.url}}"/>
+	</div>
+	<div class="avatar-crop-management">
+		<div id="avatar-crop-pane" class="avatar" style="width:{{data.full_w}}px; height:{{data.full_h}}px">
+			<img src="{{data.url}}" id="avatar-crop-preview"/>
+		</div>
+		<div id="avatar-crop-actions">
+			<a class="button avatar-crop-submit" href="#"><?php esc_html_e( 'Crop Image', 'buddypress' ); ?></a>
+		</div>
+	</div>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/avatars/index.php	(revision 12265)
@@ -0,0 +1,48 @@
+<?php
+/**
+ * BuddyPress Avatars main template.
+ *
+ * This template is used to inject the BuddyPress Backbone views
+ * dealing with avatars.
+ *
+ * It's also used to create the common Backbone views.
+ *
+ * @since 2.3.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-attachments
+ * @version 3.0.0
+ */
+
+/**
+ * This action is for internal use, please do not use it
+ */
+do_action( 'bp_attachments_avatar_check_template' );
+?>
+<div class="bp-avatar-nav"></div>
+<div class="bp-avatar"></div>
+<div class="bp-avatar-status"></div>
+
+<script type="text/html" id="tmpl-bp-avatar-nav">
+	<a href="{{data.href}}" class="bp-avatar-nav-item" data-nav="{{data.id}}">{{data.name}}</a>
+</script>
+
+<?php bp_attachments_get_template_part( 'uploader' ); ?>
+
+<?php bp_attachments_get_template_part( 'avatars/crop' ); ?>
+
+<?php bp_attachments_get_template_part( 'avatars/camera' ); ?>
+
+<script id="tmpl-bp-avatar-delete" type="text/html">
+	<# if ( 'user' === data.object ) { #>
+		<p><?php _e( "If you'd like to delete your current profile photo but not upload a new one, please use the delete profile photo button.", 'buddypress' ); ?></p>
+		<p><a class="button edit" id="bp-delete-avatar" href="#"><?php esc_html_e( 'Delete My Profile Photo', 'buddypress' ); ?></a></p>
+	<# } else if ( 'group' === data.object ) { #>
+		<p><?php _e( "If you'd like to remove the existing group profile photo but not upload a new one, please use the delete group profile photo button.", 'buddypress' ); ?></p>
+		<p><a class="button edit" id="bp-delete-avatar" href="#"><?php esc_html_e( 'Delete Group Profile Photo', 'buddypress' ); ?></a></p>
+	<# } else { #>
+		<?php do_action( 'bp_attachments_avatar_delete_template' ); ?>
+	<# } #>
+</script>
+
+<?php do_action( 'bp_attachments_avatar_main_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/cover-images/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/cover-images/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/cover-images/index.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress Cover Images main template.
+ *
+ * This template is used to inject the BuddyPress Backbone views
+ * dealing with cover images.
+ *
+ * It's also used to create the common Backbone views.
+ *
+ * @since 2.4.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-attachments
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="bp-cover-image"></div>
+<div class="bp-cover-image-status"></div>
+<div class="bp-cover-image-manage"></div>
+
+<?php bp_attachments_get_template_part( 'uploader' ); ?>
+
+<script id="tmpl-bp-cover-image-delete" type="text/html">
+	<# if ( 'user' === data.object ) { #>
+		<p><?php _e( "If you'd like to delete your current cover image but not upload a new one, please use the delete Cover Image button.", 'buddypress' ); ?></p>
+		<p><a class="button edit" id="bp-delete-cover-image" href="#"><?php esc_html_e( 'Delete My Cover Image', 'buddypress' ); ?></a></p>
+	<# } else if ( 'group' === data.object ) { #>
+		<p><?php _e( "If you'd like to remove the existing group cover image but not upload a new one, please use the delete group cover image button.", 'buddypress' ); ?></p>
+		<p><a class="button edit" id="bp-delete-cover-image" href="#"><?php esc_html_e( 'Delete Group Cover Image', 'buddypress' ); ?></a></p>
+	<# } else { #>
+		<?php do_action( 'bp_attachments_cover_image_delete_template' ); ?>
+	<# } #>
+</script>
+
+<?php do_action( 'bp_attachments_cover_image_main_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/uploader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/uploader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/_attachments/uploader.php	(revision 12265)
@@ -0,0 +1,43 @@
+<?php
+/**
+ * BuddyPress Uploader templates.
+ *
+ * This template is used to create the BuddyPress Uploader Backbone views.
+ *
+ * @since 2.3.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-attachments
+ * @version 3.0.0
+ */
+
+?>
+<script type="text/html" id="tmpl-upload-window">
+	<?php if ( ! _device_can_upload() ) : ?>
+		<h3 class="upload-instructions"><?php esc_html_e( 'The web browser on your device cannot be used to upload files.', 'buddypress' ); ?></h3>
+	<?php elseif ( is_multisite() && ! is_upload_space_available() ) : ?>
+		<h3 class="upload-instructions"><?php esc_html_e( 'Upload Limit Exceeded', 'buddypress' ); ?></h3>
+	<?php else : ?>
+		<div id="{{data.container}}">
+			<div id="{{data.drop_element}}">
+				<div class="drag-drop-inside">
+					<p class="drag-drop-info"><?php esc_html_e( 'Drop your file here', 'buddypress' ); ?></p>
+					<p><?php _ex( 'or', 'Uploader: Drop your file here - or - Select your File', 'buddypress' ); ?></p>
+					<p class="drag-drop-buttons"><label for="{{data.browse_button}}" class="<?php echo is_admin() ? 'screen-reader-text' : 'bp-screen-reader-text' ;?>"><?php
+						/* translators: accessibility text */
+						esc_html_e( 'Select your File', 'buddypress' );
+					?></label><input id="{{data.browse_button}}" type="button" value="<?php esc_attr_e( 'Select your File', 'buddypress' ); ?>" class="button" /></p>
+				</div>
+			</div>
+		</div>
+	<?php endif; ?>
+</script>
+
+<script type="text/html" id="tmpl-progress-window">
+	<div id="{{data.id}}">
+		<div class="bp-progress">
+			<div class="bp-bar"></div>
+		</div>
+		<div class="filename">{{data.filename}}</div>
+	</div>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/emails/single-bp-email.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/emails/single-bp-email.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/emails/single-bp-email.php	(revision 12265)
@@ -0,0 +1,245 @@
+<?php
+/**
+ * BuddyPress email template.
+ *
+ * Magic numbers:
+ *  1.618 = golden mean.
+ *  1.35  = default body_text_size multipler. Gives default heading of 20px.
+ *
+ * @since 2.5.0
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @version 3.0.0
+ */
+
+/*
+Based on the Cerberus "Fluid" template by Ted Goas (http://tedgoas.github.io/Cerberus/).
+License for the original template:
+
+
+The MIT License (MIT)
+
+Copyright (c) 2017 Ted Goas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+$settings = bp_email_get_appearance_settings();
+
+?><!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+<head>
+	<meta charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>">
+	<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
+	<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
+	<meta name="x-apple-disable-message-reformatting">  <!-- Disable auto-scale in iOS 10 Mail entirely -->
+	<title></title> <!-- The title tag shows in email notifications, like Android 4.4. -->
+
+	<!-- CSS Reset -->
+	<style type="text/css">
+		/* What it does: Remove spaces around the email design added by some email clients. */
+		/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
+		html,
+		body {
+			Margin: 0 !important;
+			padding: 0 !important;
+			height: 100% !important;
+			width: 100% !important;
+		}
+
+		/* What it does: Stops email clients resizing small text. */
+		* {
+			-ms-text-size-adjust: 100%;
+			-webkit-text-size-adjust: 100%;
+		}
+
+		/* What is does: Centers email on Android 4.4 */
+		div[style*="margin: 16px 0"] {
+			margin: 0 !important;
+		}
+
+		/* What it does: Stops Outlook from adding extra spacing to tables. */
+		table,
+		td {
+			mso-table-lspace: 0pt !important;
+			mso-table-rspace: 0pt !important;
+		}
+
+		/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
+		table {
+			border-spacing: 0 !important;
+			border-collapse: collapse !important;
+			table-layout: fixed !important;
+			Margin: 0 auto !important;
+		}
+		table table table {
+			table-layout: auto;
+		}
+
+		/* What it does: Uses a better rendering method when resizing images in IE. */
+		/* & manages img max widths to ensure content body images don't exceed template width. */
+		img {
+			-ms-interpolation-mode:bicubic;
+			height: auto;
+			max-width: 100%;
+		}
+
+		/* What it does: A work-around for email clients meddling in triggered links. */
+		*[x-apple-data-detectors],  /* iOS */
+		.x-gmail-data-detectors,    /* Gmail */
+		.x-gmail-data-detectors *,
+		.aBn {
+			border-bottom: 0 !important;
+			cursor: default !important;
+			color: inherit !important;
+			text-decoration: none !important;
+			font-size: inherit !important;
+			font-family: inherit !important;
+			font-weight: inherit !important;
+			line-height: inherit !important;
+		}
+
+		/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
+		.a6S {
+			display: none !important;
+			opacity: 0.01 !important;
+		}
+
+		/* If the above doesn't work, add a .g-img class to any image in question. */
+			img.g-img + div {
+			display: none !important;
+		}
+
+		/* What it does: Prevents underlining the button text in Windows 10 */
+		.button-link {
+			text-decoration: none !important;
+		}
+	</style>
+
+</head>
+<body class="email_bg" width="100%" bgcolor="<?php echo esc_attr( $settings['email_bg'] ); ?>" style="margin: 0; mso-line-height-rule: exactly;">
+<table cellpadding="0" cellspacing="0" border="0" height="100%" width="100%" bgcolor="<?php echo esc_attr( $settings['email_bg'] ); ?>" style="border-collapse:collapse;" class="email_bg"><tr><td valign="top">
+	<center style="width: 100%; text-align: <?php echo esc_attr( $settings['direction'] ); ?>;">
+
+		<!-- Visually Hidden Preheader Text : BEGIN -->
+		<div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
+			{{email.preheader}}
+		</div>
+		<!-- Visually Hidden Preheader Text : END -->
+
+		<div style="max-width: 600px; margin: auto;" class="email-container">
+			<!--[if mso]>
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" align="center">
+			<tr>
+			<td>
+			<![endif]-->
+
+			<!-- Email Header : BEGIN -->
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 600px; border-top: 7px solid <?php echo esc_attr( $settings['highlight_color'] ); ?>" bgcolor="<?php echo esc_attr( $settings['header_bg'] ); ?>" class="header_bg">
+				<tr>
+					<td style="text-align: center; padding: 15px 0; font-family: sans-serif; mso-height-rule: exactly; font-weight: bold; color: <?php echo esc_attr( $settings['header_text_color'] ); ?>; font-size: <?php echo esc_attr( $settings['header_text_size'] . 'px' ); ?>" class="header_text_color header_text_size">
+						<?php
+						/**
+						 * Fires before the display of the email template header.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_before_email_header' );
+
+						echo bp_get_option( 'blogname' );
+
+						/**
+						 * Fires after the display of the email template header.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_after_email_header' );
+						?>
+					</td>
+				</tr>
+			</table>
+			<!-- Email Header : END -->
+
+			<!-- Email Body : BEGIN -->
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" bgcolor="<?php echo esc_attr( $settings['body_bg'] ); ?>" width="100%" style="max-width: 600px; border-radius: 5px;" class="body_bg">
+
+				<!-- 1 Column Text : BEGIN -->
+				<tr>
+					<td>
+						<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
+							<tr>
+								<td style="padding: 20px; font-family: sans-serif; mso-height-rule: exactly; line-height: <?php echo esc_attr( floor( $settings['body_text_size'] * 1.618 ) . 'px' ) ?>; color: <?php echo esc_attr( $settings['body_text_color'] ); ?>; font-size: <?php echo esc_attr( $settings['body_text_size'] . 'px' ); ?>" class="body_text_color body_text_size">
+									<span style="font-weight: bold; font-size: <?php echo esc_attr( floor( $settings['body_text_size'] * 1.35 ) . 'px' ); ?>" class="welcome"><?php bp_email_the_salutation( $settings ); ?></span>
+									<hr color="<?php echo esc_attr( $settings['email_bg'] ); ?>"><br>
+									{{{content}}}
+								</td>
+							</tr>
+						</table>
+					</td>
+				</tr>
+				<!-- 1 Column Text : BEGIN -->
+
+			</table>
+			<!-- Email Body : END -->
+
+			<!-- Email Footer : BEGIN -->
+			<br>
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="<?php echo esc_attr( $settings['direction'] ); ?>" width="100%" style="max-width: 600px; border-radius: 5px;" bgcolor="<?php echo esc_attr( $settings['footer_bg'] ); ?>" class="footer_bg">
+				<tr>
+					<td style="padding: 20px; width: 100%; font-size: <?php echo esc_attr( $settings['footer_text_size'] . 'px' ); ?>; font-family: sans-serif; mso-height-rule: exactly; line-height: <?php echo esc_attr( floor( $settings['footer_text_size'] * 1.618 ) . 'px' ) ?>; text-align: <?php echo esc_attr( $settings['direction'] ); ?>; color: <?php echo esc_attr( $settings['footer_text_color'] ); ?>;" class="footer_text_color footer_text_size">
+						<?php
+						/**
+						 * Fires before the display of the email template footer.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_before_email_footer' );
+						?>
+
+						<span class="footer_text"><?php echo nl2br( stripslashes( $settings['footer_text'] ) ); ?></span>
+						<br><br>
+						<a href="{{{unsubscribe}}}" style="text-decoration: underline;"><?php _ex( 'unsubscribe', 'email', 'buddypress' ); ?></a>
+
+						<?php
+						/**
+						 * Fires after the display of the email template footer.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_after_email_footer' );
+						?>
+					</td>
+				</tr>
+			</table>
+			<!-- Email Footer : END -->
+
+			<!--[if mso]>
+			</td>
+			</tr>
+			</table>
+			<![endif]-->
+		</div>
+	</center>
+</td></tr></table>
+<?php if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) wp_footer(); ?>
+</body>
+</html>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/activity.php	(revision 12265)
@@ -0,0 +1,15 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+		<?php if ( bp_activity_embed_has_activity( bp_current_action() ) ) : ?>
+
+			<?php while ( bp_activities() ) : bp_the_activity(); ?>
+				<div class="bp-embed-excerpt"><?php bp_activity_embed_excerpt(); ?></div>
+
+				<?php bp_activity_embed_media(); ?>
+
+			<?php endwhile; ?>
+
+		<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/footer.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/footer.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/footer.php	(revision 12265)
@@ -0,0 +1,14 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+			<div class="wp-embed-footer">
+				<?php the_embed_site_title() ?>
+
+				<div class="wp-embed-meta">
+					<?php
+					/** This action is documented in wp-includes/theme-compat/embed-content.php */
+					do_action( 'embed_content_meta'); ?>
+				</div>
+			</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/header-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/header-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/header-activity.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+
+		<div id="bp-embed-header">
+			<div class="bp-embed-avatar">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_avatar( 'type=thumb&width=45&height=45' ); ?>
+				</a>
+			</div>
+
+			<?php if ( bp_activity_embed_has_activity( bp_current_action() ) ) : ?>
+
+				<?php while ( bp_activities() ) : bp_the_activity(); ?>
+					<p class="bp-embed-activity-action">
+						<?php bp_activity_action( array( 'no_timestamp' => true ) ); ?>
+					</p>
+				<?php endwhile; ?>
+
+			<?php endif; ?>
+
+			<p class="bp-embed-header-meta">
+				<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+					<span class="bp-embed-mentionname">@<?php bp_displayed_user_mentionname(); ?> &middot; </span>
+				<?php endif; ?>
+
+				<span class="bp-embed-timestamp"><a href="<?php bp_activity_thread_permalink(); ?>"><?php echo date_i18n( get_option( 'time_format' ) . ' - ' . get_option( 'date_format' ), strtotime( bp_get_activity_date_recorded() ) ); ?></a></span>
+			</p>
+		</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/assets/embeds/header.php	(revision 12265)
@@ -0,0 +1,22 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+		<div id="bp-embed-header">
+			<div class="bp-embed-avatar">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_avatar( 'type=thumb&width=36&height=36' ); ?>
+				</a>
+			</div>
+
+			<p class="wp-embed-heading">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_fullname(); ?>
+				</a>
+			</p>
+
+			<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+				<p class="bp-embed-mentionname">@<?php bp_displayed_user_mentionname(); ?></p>
+			<?php endif; ?>
+		</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/blogs-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/blogs-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/blogs-loop.php	(revision 12265)
@@ -0,0 +1,133 @@
+<?php
+/**
+ * BuddyPress - Blogs Loop
+ *
+ * Querystring is set via AJAX in _inc/ajax.php - bp_legacy_theme_object_filter().
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the start of the blogs loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_blogs_loop' ); ?>
+
+<?php if ( bp_has_blogs( bp_ajax_querystring( 'blogs' ) ) ) : ?>
+
+	<div id="pag-top" class="pagination">
+
+		<div class="pag-count" id="blog-dir-count-top">
+			<?php bp_blogs_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="blog-dir-pag-top">
+			<?php bp_blogs_pagination_links(); ?>
+		</div>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires before the blogs directory list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_blogs_list' ); ?>
+
+	<ul id="blogs-list" class="item-list">
+
+	<?php while ( bp_blogs() ) : bp_the_blog(); ?>
+
+		<li <?php bp_blog_class() ?>>
+			<div class="item-avatar">
+				<a href="<?php bp_blog_permalink(); ?>"><?php bp_blog_avatar( 'type=thumb' ); ?></a>
+			</div>
+
+			<div class="item">
+				<div class="item-title"><a href="<?php bp_blog_permalink(); ?>"><?php bp_blog_name(); ?></a></div>
+				<div class="item-meta"><span class="activity"><?php bp_blog_last_active(); ?></span></div>
+
+				<?php
+
+				/**
+				 * Fires after the listing of a blog item in the blogs loop.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_directory_blogs_item' ); ?>
+			</div>
+
+			<div class="action">
+
+				<?php
+
+				/**
+				 * Fires inside the blogs action listing area.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_directory_blogs_actions' ); ?>
+
+				<div class="meta">
+
+					<?php bp_blog_latest_post(); ?>
+
+				</div>
+
+			</div>
+
+			<div class="clear"></div>
+		</li>
+
+	<?php endwhile; ?>
+
+	</ul>
+
+	<?php
+
+	/**
+	 * Fires after the blogs directory list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_blogs_list' ); ?>
+
+	<?php bp_blog_hidden_fields(); ?>
+
+	<div id="pag-bottom" class="pagination">
+
+		<div class="pag-count" id="blog-dir-count-bottom">
+
+			<?php bp_blogs_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="blog-dir-pag-bottom">
+
+			<?php bp_blogs_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'Sorry, there were no sites found.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of the blogs loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_blogs_loop' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/create.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/create.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/create.php	(revision 12265)
@@ -0,0 +1,62 @@
+<?php
+/**
+ * BuddyPress - Blogs Create
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires at the top of the blog creation template file.
+ *
+ * @since 1.6.0
+ */
+do_action( 'bp_before_create_blog_content_template' ); ?>
+
+<div id="template-notices" role="alert" aria-atomic="true">
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+	do_action( 'template_notices' ); ?>
+
+</div>
+
+<?php
+
+/**
+ * Fires before the display of the blog creation form.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_create_blog_content' ); ?>
+
+<?php if ( bp_blog_signup_enabled() ) : ?>
+
+	<?php bp_show_blog_signup_form(); ?>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'Site registration is currently disabled', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of the blog creation form.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_create_blog_content' ); ?>
+
+<?php
+
+/**
+ * Fires at the bottom of the blog creation template file.
+ *
+ * @since 1.6.0
+ */
+do_action( 'bp_after_create_blog_content_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/blogs/index.php	(revision 12265)
@@ -0,0 +1,168 @@
+<?php
+/**
+ * BuddyPress - Blogs
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires at the top of the blogs directory template file.
+ *
+ * @since 2.3.0
+ */
+do_action( 'bp_before_directory_blogs_page' ); ?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the display of the blogs.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_before_directory_blogs' ); ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the blogs listing content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_blogs_content' ); ?>
+
+	<?php /* Backward compatibility for inline search form. Use template part instead. */ ?>
+	<?php if ( has_filter( 'bp_directory_blogs_search_form' ) ) : ?>
+
+		<div id="blog-dir-search" class="dir-search" role="search">
+			<?php bp_directory_blogs_search_form(); ?>
+		</div><!-- #blog-dir-search -->
+
+	<?php else : ?>
+
+		<?php bp_get_template_part( 'common/search/dir-search-form' ); ?>
+
+	<?php endif; ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the blogs list tabs.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_before_directory_blogs_tabs' ); ?>
+
+	<form action="" method="post" id="blogs-directory-form" class="dir-form">
+
+		<div class="item-list-tabs" aria-label="<?php esc_attr_e( 'Sites directory main navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+				<li class="selected" id="blogs-all"><a href="<?php bp_root_domain(); ?>/<?php bp_blogs_root_slug(); ?>"><?php printf( __( 'All Sites %s', 'buddypress' ), '<span>' . bp_get_total_blog_count() . '</span>' ); ?></a></li>
+
+				<?php if ( is_user_logged_in() && bp_get_total_blog_count_for_user( bp_loggedin_user_id() ) ) : ?>
+
+					<li id="blogs-personal"><a href="<?php echo bp_loggedin_user_domain() . bp_get_blogs_slug(); ?>"><?php printf( __( 'My Sites %s', 'buddypress' ), '<span>' . bp_get_total_blog_count_for_user( bp_loggedin_user_id() ) . '</span>' ); ?></a></li>
+
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires inside the unordered list displaying blog types.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_blogs_directory_blog_types' ); ?>
+
+			</ul>
+		</div><!-- .item-list-tabs -->
+
+		<div class="item-list-tabs" id="subnav" aria-label="<?php esc_attr_e( 'Sites directory secondary navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+
+				<?php
+
+				/**
+				 * Fires inside the unordered list displaying blog sub-types.
+				 *
+				 * @since 1.5.0
+				 */
+				do_action( 'bp_blogs_directory_blog_sub_types' ); ?>
+
+				<li id="blogs-order-select" class="last filter">
+
+					<label for="blogs-order-by"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+					<select id="blogs-order-by">
+						<option value="active"><?php _e( 'Last Active', 'buddypress' ); ?></option>
+						<option value="newest"><?php _e( 'Newest', 'buddypress' ); ?></option>
+						<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+
+						<?php
+
+						/**
+						 * Fires inside the select input listing blogs orderby options.
+						 *
+						 * @since 1.2.0
+						 */
+						do_action( 'bp_blogs_directory_order_options' ); ?>
+
+					</select>
+				</li>
+			</ul>
+		</div>
+
+		<h2 class="bp-screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Sites directory', 'buddypress' );
+		?></h2>
+
+		<div id="blogs-dir-list" class="blogs dir-list">
+
+			<?php bp_get_template_part( 'blogs/blogs-loop' ); ?>
+
+		</div><!-- #blogs-dir-list -->
+
+		<?php
+
+		/**
+		 * Fires inside and displays the blogs content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_directory_blogs_content' ); ?>
+
+		<?php wp_nonce_field( 'directory_blogs', '_wpnonce-blogs-filter' ); ?>
+
+		<?php
+
+		/**
+		 * Fires after the display of the blogs listing content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_directory_blogs_content' ); ?>
+
+	</form><!-- #blogs-directory-form -->
+
+	<?php
+
+	/**
+	 * Fires at the bottom of the blogs directory template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_blogs' ); ?>
+
+</div>
+
+<?php
+
+/**
+ * Fires at the bottom of the blogs directory template file.
+ *
+ * @since 2.3.0
+ */
+do_action( 'bp_after_directory_blogs_page' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/common/search/dir-search-form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/common/search/dir-search-form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/common/search/dir-search-form.php	(revision 12265)
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Output the search form markup.
+ *
+ * @since 2.7.0
+ * @version 3.0.0
+ */
+?>
+
+<div id="<?php echo esc_attr( bp_current_component() ); ?>-dir-search" class="dir-search" role="search">
+	<form action="" method="get" id="search-<?php echo esc_attr( bp_current_component() ); ?>-form">
+		<label for="<?php bp_search_input_name(); ?>" class="bp-screen-reader-text"><?php bp_search_placeholder(); ?></label>
+		<input type="text" name="<?php echo esc_attr( bp_core_get_component_search_query_arg() ); ?>" id="<?php bp_search_input_name(); ?>" placeholder="<?php bp_search_placeholder(); ?>" />
+
+		<input type="submit" id="<?php echo esc_attr( bp_get_search_input_name() ); ?>_submit" name="<?php bp_search_input_name(); ?>_submit" value="<?php esc_attr_e( 'Search', 'buddypress' ); ?>" />
+	</form>
+</div><!-- #<?php echo esc_attr( bp_current_component() ); ?>-dir-search -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/create.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/create.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/create.php	(revision 12265)
@@ -0,0 +1,521 @@
+<?php
+/**
+ * BuddyPress - Groups Create
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires at the top of the groups creation template file.
+ *
+ * @since 1.7.0
+ */
+do_action( 'bp_before_create_group_page' ); ?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the display of group creation content.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_before_create_group_content_template' ); ?>
+
+	<form action="<?php bp_group_creation_form_action(); ?>" method="post" id="create-group-form" class="standard-form" enctype="multipart/form-data">
+
+		<?php
+
+		/**
+		 * Fires before the display of group creation.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_create_group' ); ?>
+
+		<div class="item-list-tabs no-ajax" id="group-create-tabs">
+			<ul>
+
+				<?php bp_group_creation_tabs(); ?>
+
+			</ul>
+		</div>
+
+		<div id="template-notices" role="alert" aria-atomic="true">
+			<?php
+
+			/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+			do_action( 'template_notices' ); ?>
+
+		</div>
+
+		<div class="item-body" id="group-create-body">
+
+			<?php /* Group creation step 1: Basic group details */ ?>
+			<?php if ( bp_is_group_creation_step( 'group-details' ) ) : ?>
+
+				<h2 class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'Group Details', 'buddypress' );
+				?></h2>
+
+				<?php
+
+				/**
+				 * Fires before the display of the group details creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_before_group_details_creation_step' ); ?>
+
+				<div>
+					<label for="group-name"><?php _e( 'Group Name (required)', 'buddypress' ); ?></label>
+					<input type="text" name="group-name" id="group-name" aria-required="true" value="<?php bp_new_group_name(); ?>" />
+				</div>
+
+				<div>
+					<label for="group-desc"><?php _e( 'Group Description (required)', 'buddypress' ); ?></label>
+					<textarea name="group-desc" id="group-desc" aria-required="true"><?php bp_new_group_description(); ?></textarea>
+				</div>
+
+				<?php
+
+				/**
+				 * Fires after the display of the group details creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_group_details_creation_step' );
+				do_action( 'groups_custom_group_fields_editable' ); // @Deprecated
+
+				wp_nonce_field( 'groups_create_save_group-details' ); ?>
+
+			<?php endif; ?>
+
+			<?php /* Group creation step 2: Group settings */ ?>
+			<?php if ( bp_is_group_creation_step( 'group-settings' ) ) : ?>
+
+				<h2 class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'Group Settings', 'buddypress' );
+				?></h2>
+
+				<?php
+
+				/**
+				 * Fires before the display of the group settings creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_before_group_settings_creation_step' ); ?>
+
+				<fieldset class="group-create-privacy">
+
+					<legend><?php _e( 'Privacy Options', 'buddypress' ); ?></legend>
+
+					<div class="radio">
+
+						<label for="group-status-public"><input type="radio" name="group-status" id="group-status-public" value="public"<?php if ( 'public' == bp_get_new_group_status() || !bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="public-group-description" /> <?php _e( 'This is a public group', 'buddypress' ); ?></label>
+
+						<ul id="public-group-description">
+							<li><?php _e( 'Any site member can join this group.', 'buddypress' ); ?></li>
+							<li><?php _e( 'This group will be listed in the groups directory and in search results.', 'buddypress' ); ?></li>
+							<li><?php _e( 'Group content and activity will be visible to any site member.', 'buddypress' ); ?></li>
+						</ul>
+
+						<label for="group-status-private"><input type="radio" name="group-status" id="group-status-private" value="private"<?php if ( 'private' == bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="private-group-description" /> <?php _e( 'This is a private group', 'buddypress' ); ?></label>
+
+						<ul id="private-group-description">
+							<li><?php _e( 'Only users who request membership and are accepted can join the group.', 'buddypress' ); ?></li>
+							<li><?php _e( 'This group will be listed in the groups directory and in search results.', 'buddypress' ); ?></li>
+							<li><?php _e( 'Group content and activity will only be visible to members of the group.', 'buddypress' ); ?></li>
+						</ul>
+
+						<label for="group-status-hidden"><input type="radio" name="group-status" id="group-status-hidden" value="hidden"<?php if ( 'hidden' == bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="hidden-group-description" /> <?php _e('This is a hidden group', 'buddypress' ); ?></label>
+
+						<ul id="hidden-group-description">
+							<li><?php _e( 'Only users who are invited can join the group.', 'buddypress' ); ?></li>
+							<li><?php _e( 'This group will not be listed in the groups directory or search results.', 'buddypress' ); ?></li>
+							<li><?php _e( 'Group content and activity will only be visible to members of the group.', 'buddypress' ); ?></li>
+						</ul>
+
+					</div>
+
+				</fieldset>
+
+				<?php // Group type selection ?>
+				<?php if ( $group_types = bp_groups_get_group_types( array( 'show_in_create_screen' => true ), 'objects' ) ): ?>
+
+					<fieldset class="group-create-types">
+						<legend><?php _e( 'Group Types', 'buddypress' ); ?></legend>
+
+						<p><?php _e( 'Select the types this group should be a part of.', 'buddypress' ); ?></p>
+
+						<?php foreach ( $group_types as $type ) : ?>
+							<div class="checkbox">
+								<label for="<?php printf( 'group-type-%s', $type->name ); ?>"><input type="checkbox" name="group-types[]" id="<?php printf( 'group-type-%s', $type->name ); ?>" value="<?php echo esc_attr( $type->name ); ?>" <?php checked( true, ! empty( $type->create_screen_checked ) ); ?> /> <?php echo esc_html( $type->labels['name'] ); ?>
+									<?php
+										if ( ! empty( $type->description ) ) {
+											/* translators: Group type description shown when creating a group. */
+											printf( __( '&ndash; %s', 'buddypress' ), '<span class="bp-group-type-desc">' . esc_html( $type->description ) . '</span>' );
+										}
+									?>
+								</label>
+							</div>
+
+						<?php endforeach; ?>
+
+					</fieldset>
+
+				<?php endif; ?>
+
+				<fieldset class="group-create-invitations">
+
+					<legend><?php _e( 'Group Invitations', 'buddypress' ); ?></legend>
+
+					<p><?php _e( 'Which members of this group are allowed to invite others?', 'buddypress' ); ?></p>
+
+					<div class="radio">
+
+						<label for="group-invite-status-members"><input type="radio" name="group-invite-status" id="group-invite-status-members" value="members"<?php bp_group_show_invite_status_setting( 'members' ); ?> /> <?php _e( 'All group members', 'buddypress' ); ?></label>
+
+						<label for="group-invite-status-mods"><input type="radio" name="group-invite-status" id="group-invite-status-mods" value="mods"<?php bp_group_show_invite_status_setting( 'mods' ); ?> /> <?php _e( 'Group admins and mods only', 'buddypress' ); ?></label>
+
+						<label for="group-invite-status-admins"><input type="radio" name="group-invite-status" id="group-invite-status-admins" value="admins"<?php bp_group_show_invite_status_setting( 'admins' ); ?> /> <?php _e( 'Group admins only', 'buddypress' ); ?></label>
+
+					</div>
+
+				</fieldset>
+
+				<?php
+
+				/**
+				 * Fires after the display of the group settings creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_group_settings_creation_step' ); ?>
+
+				<?php wp_nonce_field( 'groups_create_save_group-settings' ); ?>
+
+			<?php endif; ?>
+
+			<?php /* Group creation step 3: Avatar Uploads */ ?>
+			<?php if ( bp_is_group_creation_step( 'group-avatar' ) ) : ?>
+
+				<h2 class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'Group Avatar', 'buddypress' );
+				?></h2>
+
+				<?php
+
+				/**
+				 * Fires before the display of the group avatar creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_before_group_avatar_creation_step' ); ?>
+
+				<?php if ( 'upload-image' == bp_get_avatar_admin_step() ) : ?>
+
+					<div class="left-menu">
+
+						<?php bp_new_group_avatar(); ?>
+
+					</div><!-- .left-menu -->
+
+					<div class="main-column">
+						<p><?php _e( "Upload an image to use as a profile photo for this group. The image will be shown on the main group page, and in search results.", 'buddypress' ); ?></p>
+
+						<p>
+							<label for="file" class="bp-screen-reader-text"><?php
+								/* translators: accessibility text */
+								_e( 'Select an image', 'buddypress' );
+							?></label>
+							<input type="file" name="file" id="file" />
+							<input type="submit" name="upload" id="upload" value="<?php esc_attr_e( 'Upload Image', 'buddypress' ); ?>" />
+							<input type="hidden" name="action" id="action" value="bp_avatar_upload" />
+						</p>
+
+						<p><?php _e( 'To skip the group profile photo upload process, hit the "Next Step" button.', 'buddypress' ); ?></p>
+					</div><!-- .main-column -->
+
+					<?php
+					/**
+					 * Load the Avatar UI templates
+					 *
+					 * @since 2.3.0
+					 */
+					bp_avatar_get_templates(); ?>
+
+				<?php endif; ?>
+
+				<?php if ( 'crop-image' == bp_get_avatar_admin_step() ) : ?>
+
+					<h4><?php _e( 'Crop Group Profile Photo', 'buddypress' ); ?></h4>
+
+					<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-to-crop" class="avatar" alt="<?php esc_attr_e( 'Profile photo to crop', 'buddypress' ); ?>" />
+
+					<div id="avatar-crop-pane">
+						<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-crop-preview" class="avatar" alt="<?php esc_attr_e( 'Profile photo preview', 'buddypress' ); ?>" />
+					</div>
+
+					<input type="submit" name="avatar-crop-submit" id="avatar-crop-submit" value="<?php esc_attr_e( 'Crop Image', 'buddypress' ); ?>" />
+
+					<input type="hidden" name="image_src" id="image_src" value="<?php bp_avatar_to_crop_src(); ?>" />
+					<input type="hidden" name="upload" id="upload" />
+					<input type="hidden" id="x" name="x" />
+					<input type="hidden" id="y" name="y" />
+					<input type="hidden" id="w" name="w" />
+					<input type="hidden" id="h" name="h" />
+
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of the group avatar creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_group_avatar_creation_step' ); ?>
+
+				<?php wp_nonce_field( 'groups_create_save_group-avatar' ); ?>
+
+			<?php endif; ?>
+
+			<?php /* Group creation step 4: Cover image */ ?>
+			<?php if ( bp_is_group_creation_step( 'group-cover-image' ) ) : ?>
+
+				<h2 class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'Cover Image', 'buddypress' );
+				?></h2>
+
+				<?php
+
+				/**
+				 * Fires before the display of the group cover image creation step.
+				 *
+				 * @since 2.4.0
+				 */
+				do_action( 'bp_before_group_cover_image_creation_step' ); ?>
+
+				<div id="header-cover-image"></div>
+
+				<p><?php _e( 'The Cover Image will be used to customize the header of your group.', 'buddypress' ); ?></p>
+
+				<?php bp_attachments_get_template_part( 'cover-images/index' ); ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of the group cover image creation step.
+				 *
+				 * @since 2.4.0
+				 */
+				do_action( 'bp_after_group_cover_image_creation_step' ); ?>
+
+				<?php wp_nonce_field( 'groups_create_save_group-cover-image' ); ?>
+
+			<?php endif; ?>
+
+			<?php /* Group creation step 5: Invite friends to group */ ?>
+			<?php if ( bp_is_group_creation_step( 'group-invites' ) ) : ?>
+
+				<h2 class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'Group Invites', 'buddypress' );
+				?></h2>
+
+				<?php
+
+				/**
+				 * Fires before the display of the group invites creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_before_group_invites_creation_step' ); ?>
+
+				<?php if ( bp_is_active( 'friends' ) && bp_get_total_friend_count( bp_loggedin_user_id() ) ) : ?>
+
+					<div class="left-menu">
+
+						<div id="invite-list">
+							<ul>
+								<?php bp_new_group_invite_friend_list(); ?>
+							</ul>
+
+							<?php wp_nonce_field( 'groups_invite_uninvite_user', '_wpnonce_invite_uninvite_user' ); ?>
+						</div>
+
+					</div><!-- .left-menu -->
+
+					<div class="main-column">
+
+						<div id="message" class="info">
+							<p><?php _e('Select people to invite from your friends list.', 'buddypress' ); ?></p>
+						</div>
+
+						<?php /* The ID 'friend-list' is important for AJAX support. */ ?>
+						<ul id="friend-list" class="item-list">
+
+						<?php if ( bp_group_has_invites() ) : ?>
+
+							<?php while ( bp_group_invites() ) : bp_group_the_invite(); ?>
+
+								<li id="<?php bp_group_invite_item_id(); ?>">
+
+									<?php bp_group_invite_user_avatar(); ?>
+
+									<h4><?php bp_group_invite_user_link(); ?></h4>
+									<span class="activity"><?php bp_group_invite_user_last_active(); ?></span>
+
+									<div class="action">
+										<a class="remove" href="<?php bp_group_invite_user_remove_invite_url(); ?>" id="<?php bp_group_invite_item_id(); ?>"><?php _e( 'Remove Invite', 'buddypress' ); ?></a>
+									</div>
+								</li>
+
+							<?php endwhile; ?>
+
+							<?php wp_nonce_field( 'groups_send_invites', '_wpnonce_send_invites' ); ?>
+
+						<?php endif; ?>
+
+						</ul>
+
+					</div><!-- .main-column -->
+
+				<?php else : ?>
+
+					<div id="message" class="info">
+						<p><?php _e( 'Once you have built up friend connections you will be able to invite others to your group.', 'buddypress' ); ?></p>
+					</div>
+
+				<?php endif; ?>
+
+				<?php wp_nonce_field( 'groups_create_save_group-invites' ); ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of the group invites creation step.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_group_invites_creation_step' ); ?>
+
+			<?php endif; ?>
+
+			<?php
+
+			/**
+			 * Fires inside the group admin template.
+			 *
+			 * Allows plugins to add custom group creation steps.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'groups_custom_create_steps' ); ?>
+
+			<?php
+
+			/**
+			 * Fires before the display of the group creation step buttons.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_before_group_creation_step_buttons' ); ?>
+
+			<?php if ( 'crop-image' != bp_get_avatar_admin_step() ) : ?>
+
+				<div class="submit" id="previous-next">
+
+					<?php /* Previous Button */ ?>
+					<?php if ( !bp_is_first_group_creation_step() ) : ?>
+
+						<input type="button" value="<?php esc_attr_e( 'Back to Previous Step', 'buddypress' ); ?>" id="group-creation-previous" name="previous" onclick="location.href='<?php bp_group_creation_previous_link(); ?>'" />
+
+					<?php endif; ?>
+
+					<?php /* Next Button */ ?>
+					<?php if ( !bp_is_last_group_creation_step() && !bp_is_first_group_creation_step() ) : ?>
+
+						<input type="submit" value="<?php esc_attr_e( 'Next Step', 'buddypress' ); ?>" id="group-creation-next" name="save" />
+
+					<?php endif;?>
+
+					<?php /* Create Button */ ?>
+					<?php if ( bp_is_first_group_creation_step() ) : ?>
+
+						<input type="submit" value="<?php esc_attr_e( 'Create Group and Continue', 'buddypress' ); ?>" id="group-creation-create" name="save" />
+
+					<?php endif; ?>
+
+					<?php /* Finish Button */ ?>
+					<?php if ( bp_is_last_group_creation_step() ) : ?>
+
+						<input type="submit" value="<?php esc_attr_e( 'Finish', 'buddypress' ); ?>" id="group-creation-finish" name="save" />
+
+					<?php endif; ?>
+				</div>
+
+			<?php endif;?>
+
+			<?php
+
+			/**
+			 * Fires after the display of the group creation step buttons.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_after_group_creation_step_buttons' ); ?>
+
+			<?php /* Don't leave out this hidden field */ ?>
+			<input type="hidden" name="group_id" id="group_id" value="<?php bp_new_group_id(); ?>" />
+
+			<?php
+
+			/**
+			 * Fires and displays the groups directory content.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_directory_groups_content' ); ?>
+
+		</div><!-- .item-body -->
+
+		<?php
+
+		/**
+		 * Fires after the display of group creation.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_create_group' ); ?>
+
+	</form>
+
+	<?php
+
+	/**
+	 * Fires after the display of group creation content.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_after_create_group_content_template' ); ?>
+
+</div>
+
+<?php
+
+/**
+ * Fires at the bottom of the groups creation template file.
+ *
+ * @since 1.7.0
+ */
+do_action( 'bp_after_create_group_page' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/groups-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/groups-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/groups-loop.php	(revision 12265)
@@ -0,0 +1,148 @@
+<?php
+/**
+ * BuddyPress - Groups Loop
+ *
+ * Querystring is set via AJAX in _inc/ajax.php - bp_legacy_theme_object_filter().
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php
+
+/**
+ * Fires before the display of groups from the groups loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_groups_loop' ); ?>
+
+<?php if ( bp_get_current_group_directory_type() ) : ?>
+	<p class="current-group-type"><?php bp_current_group_directory_type_message() ?></p>
+<?php endif; ?>
+
+<?php if ( bp_has_groups( bp_ajax_querystring( 'groups' ) ) ) : ?>
+
+	<div id="pag-top" class="pagination">
+
+		<div class="pag-count" id="group-dir-count-top">
+
+			<?php bp_groups_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="group-dir-pag-top">
+
+			<?php bp_groups_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires before the listing of the groups list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_groups_list' ); ?>
+
+	<ul id="groups-list" class="item-list" aria-live="assertive" aria-atomic="true" aria-relevant="all">
+
+	<?php while ( bp_groups() ) : bp_the_group(); ?>
+
+		<li <?php bp_group_class(); ?>>
+			<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+				<div class="item-avatar">
+					<a href="<?php bp_group_permalink(); ?>"><?php bp_group_avatar( 'type=thumb&width=50&height=50' ); ?></a>
+				</div>
+			<?php endif; ?>
+
+			<div class="item">
+				<div class="item-title"><?php bp_group_link(); ?></div>
+				<div class="item-meta"><span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_last_active( 0, array( 'relative' => false ) ) ); ?>"><?php printf( __( 'active %s', 'buddypress' ), bp_get_group_last_active() ); ?></span></div>
+
+				<div class="item-desc"><?php bp_group_description_excerpt(); ?></div>
+
+				<?php
+
+				/**
+				 * Fires inside the listing of an individual group listing item.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_directory_groups_item' ); ?>
+
+			</div>
+
+			<div class="action">
+
+				<?php
+
+				/**
+				 * Fires inside the action section of an individual group listing item.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_directory_groups_actions' ); ?>
+
+				<div class="meta">
+
+					<?php bp_group_type(); ?> / <?php bp_group_member_count(); ?>
+
+				</div>
+
+			</div>
+
+			<div class="clear"></div>
+		</li>
+
+	<?php endwhile; ?>
+
+	</ul>
+
+	<?php
+
+	/**
+	 * Fires after the listing of the groups list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_groups_list' ); ?>
+
+	<div id="pag-bottom" class="pagination">
+
+		<div class="pag-count" id="group-dir-count-bottom">
+
+			<?php bp_groups_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="group-dir-pag-bottom">
+
+			<?php bp_groups_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'There were no groups found.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of groups from the groups loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_groups_loop' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/index.php	(revision 12265)
@@ -0,0 +1,163 @@
+<?php
+/**
+ * BuddyPress - Groups
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires at the top of the groups directory template file.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_before_directory_groups_page' ); ?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the display of the groups.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_groups' ); ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the groups content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_groups_content' ); ?>
+
+	<?php /* Backward compatibility for inline search form. Use template part instead. */ ?>
+	<?php if ( has_filter( 'bp_directory_groups_search_form' ) ) : ?>
+
+		<div id="group-dir-search" class="dir-search" role="search">
+			<?php bp_directory_groups_search_form(); ?>
+		</div><!-- #group-dir-search -->
+
+	<?php else: ?>
+
+		<?php bp_get_template_part( 'common/search/dir-search-form' ); ?>
+
+	<?php endif; ?>
+
+	<form action="" method="post" id="groups-directory-form" class="dir-form">
+
+		<div id="template-notices" role="alert" aria-atomic="true">
+			<?php
+
+			/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+			do_action( 'template_notices' ); ?>
+
+		</div>
+
+		<div class="item-list-tabs" aria-label="<?php esc_attr_e( 'Groups directory main navigation', 'buddypress' ); ?>">
+			<ul>
+				<li class="selected" id="groups-all"><a href="<?php bp_groups_directory_permalink(); ?>"><?php printf( __( 'All Groups %s', 'buddypress' ), '<span>' . bp_get_total_group_count() . '</span>' ); ?></a></li>
+
+				<?php if ( is_user_logged_in() && bp_get_total_group_count_for_user( bp_loggedin_user_id() ) ) : ?>
+					<li id="groups-personal"><a href="<?php echo bp_loggedin_user_domain() . bp_get_groups_slug() . '/my-groups/'; ?>"><?php printf( __( 'My Groups %s', 'buddypress' ), '<span>' . bp_get_total_group_count_for_user( bp_loggedin_user_id() ) . '</span>' ); ?></a></li>
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires inside the groups directory group filter input.
+				 *
+				 * @since 1.5.0
+				 */
+				do_action( 'bp_groups_directory_group_filter' ); ?>
+
+			</ul>
+		</div><!-- .item-list-tabs -->
+
+		<div class="item-list-tabs" id="subnav" aria-label="<?php esc_attr_e( 'Groups directory secondary navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+				<?php
+
+				/**
+				 * Fires inside the groups directory group types.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_groups_directory_group_types' ); ?>
+
+				<li id="groups-order-select" class="last filter">
+
+					<label for="groups-order-by"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+
+					<select id="groups-order-by">
+						<option value="active"><?php _e( 'Last Active', 'buddypress' ); ?></option>
+						<option value="popular"><?php _e( 'Most Members', 'buddypress' ); ?></option>
+						<option value="newest"><?php _e( 'Newly Created', 'buddypress' ); ?></option>
+						<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+
+						<?php
+
+						/**
+						 * Fires inside the groups directory group order options.
+						 *
+						 * @since 1.2.0
+						 */
+						do_action( 'bp_groups_directory_order_options' ); ?>
+					</select>
+				</li>
+			</ul>
+		</div>
+
+		<h2 class="bp-screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Groups directory', 'buddypress' );
+		?></h2>
+
+		<div id="groups-dir-list" class="groups dir-list">
+			<?php bp_get_template_part( 'groups/groups-loop' ); ?>
+		</div><!-- #groups-dir-list -->
+
+		<?php
+
+		/**
+		 * Fires and displays the group content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_directory_groups_content' ); ?>
+
+		<?php wp_nonce_field( 'directory_groups', '_wpnonce-groups-filter' ); ?>
+
+		<?php
+
+		/**
+		 * Fires after the display of the groups content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_directory_groups_content' ); ?>
+
+	</form><!-- #groups-directory-form -->
+
+	<?php
+
+	/**
+	 * Fires after the display of the groups.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_groups' ); ?>
+
+</div><!-- #buddypress -->
+
+<?php
+
+/**
+ * Fires at the bottom of the groups directory template file.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_after_directory_groups_page' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/activity.php	(revision 12265)
@@ -0,0 +1,89 @@
+<?php
+/**
+ * BuddyPress - Groups Activity
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Group secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<li class="feed"><a href="<?php bp_group_activity_feed_link(); ?>" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( 'RSS Feed', 'buddypress' ); ?>" aria-label="<?php esc_attr_e( 'RSS Feed', 'buddypress' ); ?>"><?php _e( 'RSS', 'buddypress' ); ?></a></li>
+
+		<?php
+
+		/**
+		 * Fires inside the syndication options list, after the RSS option.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_group_activity_syndication_options' ); ?>
+
+		<li id="activity-filter-select" class="last">
+			<label for="activity-filter-by"><?php _e( 'Show:', 'buddypress' ); ?></label>
+			<select id="activity-filter-by">
+				<option value="-1"><?php _e( '&mdash; Everything &mdash;', 'buddypress' ); ?></option>
+
+				<?php bp_activity_show_filters( 'group' ); ?>
+
+				<?php
+
+				/**
+				 * Fires inside the select input for group activity filter options.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_group_activity_filter_options' ); ?>
+			</select>
+		</li>
+	</ul>
+</div><!-- .item-list-tabs -->
+
+<?php
+
+/**
+ * Fires before the display of the group activity post form.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_group_activity_post_form' ); ?>
+
+<?php if ( is_user_logged_in() && bp_group_is_member() ) : ?>
+
+	<?php bp_get_template_part( 'activity/post-form' ); ?>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of the group activity post form.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_group_activity_post_form' ); ?>
+<?php
+
+/**
+ * Fires before the display of the group activities list.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_group_activity_content' ); ?>
+
+<div class="activity single-group" aria-live="polite" aria-atomic="true" aria-relevant="all">
+
+	<?php bp_get_template_part( 'activity/activity-loop' ); ?>
+
+</div><!-- .activity.single-group -->
+
+<?php
+
+/**
+ * Fires after the display of the group activities list.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_group_activity_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin.php	(revision 12265)
@@ -0,0 +1,71 @@
+<?php
+/**
+ * BuddyPress - Groups Admin
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Group secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<?php bp_group_admin_tabs(); ?>
+	</ul>
+</div><!-- .item-list-tabs -->
+
+<?php
+/**
+ * Fires before the group admin form and content.
+ *
+ * @since 2.7.0
+ */
+do_action( 'bp_before_group_admin_form' ); ?>
+
+<form action="<?php bp_group_admin_form_action(); ?>" name="group-settings-form" id="group-settings-form" class="standard-form" method="post" enctype="multipart/form-data">
+
+	<?php
+	/**
+	 * Fires inside the group admin form and before the content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_group_admin_content' ); ?>
+
+	<?php /* Fetch the template for the current admin screen being viewed */ ?>
+
+	<?php if ( bp_is_group_admin_screen( bp_action_variable() ) ) : ?>
+
+		<?php bp_get_template_part( 'groups/single/admin/' . bp_action_variable() ); ?>
+
+	<?php endif; ?>
+
+	<?php
+
+	/**
+	 * Fires inside the group admin template.
+	 *
+	 * Allows plugins to add custom group edit screens.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'groups_custom_edit_steps' ); ?>
+
+	<?php
+
+	/**
+	 * Fires inside the group admin form and after the content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_group_admin_content' ); ?>
+
+</form><!-- #group-settings-form -->
+
+<?php
+/**
+ * Fires after the group admin form and content.
+ *
+ * @since 2.7.0
+ */
+do_action( 'bp_after_group_admin_form' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/delete-group.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/delete-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/delete-group.php	(revision 12265)
@@ -0,0 +1,42 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Delete Group
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2 class="bp-screen-reader-text"><?php _e( 'Delete Group', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the display of group delete admin.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_delete_admin' ); ?>
+
+<div id="message" class="info">
+	<p><?php _e( 'WARNING: Deleting this group will completely remove ALL content associated with it. There is no way back, please be careful with this option.', 'buddypress' ); ?></p>
+</div>
+
+<label for="delete-group-understand"><input type="checkbox" name="delete-group-understand" id="delete-group-understand" value="1" onclick="if(this.checked) { document.getElementById('delete-group-button').disabled = ''; } else { document.getElementById('delete-group-button').disabled = 'disabled'; }" /> <?php _e( 'I understand the consequences of deleting this group.', 'buddypress' ); ?></label>
+
+<?php
+
+/**
+ * Fires after the display of group delete admin.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_delete_admin' ); ?>
+
+<div class="submit">
+	<input type="submit" disabled="disabled" value="<?php esc_attr_e( 'Delete Group', 'buddypress' ); ?>" id="delete-group-button" name="delete-group-button" />
+</div>
+
+<?php wp_nonce_field( 'groups_delete_group' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/edit-details.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/edit-details.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/edit-details.php	(revision 12265)
@@ -0,0 +1,54 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Edit Details
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2 class="bp-screen-reader-text"><?php _e( 'Manage Group Details', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the display of group admin details.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_details_admin' ); ?>
+
+<label for="group-name"><?php _e( 'Group Name (required)', 'buddypress' ); ?></label>
+<input type="text" name="group-name" id="group-name" value="<?php bp_group_name(); ?>" aria-required="true" />
+
+<label for="group-desc"><?php _e( 'Group Description (required)', 'buddypress' ); ?></label>
+<textarea name="group-desc" id="group-desc" aria-required="true"><?php bp_group_description_editable(); ?></textarea>
+
+<?php
+
+/**
+ * Fires after the group description admin details.
+ *
+ * @since 1.0.0
+ */
+do_action( 'groups_custom_group_fields_editable' ); ?>
+
+<p>
+	<label for="group-notify-members">
+		<input type="checkbox" name="group-notify-members" id="group-notify-members" value="1" /> <?php _e( 'Notify group members of these changes via email', 'buddypress' ); ?>
+	</label>
+</p>
+
+<?php
+
+/**
+ * Fires after the display of group admin details.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_details_admin' ); ?>
+
+<p><input type="submit" value="<?php esc_attr_e( 'Save Changes', 'buddypress' ); ?>" id="save" name="save" /></p>
+<?php wp_nonce_field( 'groups_edit_group_details' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-avatar.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Group Avatar
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2 class="bp-screen-reader-text"><?php _e( 'Group Avatar', 'buddypress' ); ?></h2>
+
+<?php if ( 'upload-image' == bp_get_avatar_admin_step() ) : ?>
+
+	<p><?php _e("Upload an image to use as a profile photo for this group. The image will be shown on the main group page, and in search results.", 'buddypress' ); ?></p>
+
+	<p>
+		<label for="file" class="bp-screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Select an image', 'buddypress' );
+		?></label>
+		<input type="file" name="file" id="file" />
+		<input type="submit" name="upload" id="upload" value="<?php esc_attr_e( 'Upload Image', 'buddypress' ); ?>" />
+		<input type="hidden" name="action" id="action" value="bp_avatar_upload" />
+	</p>
+
+	<?php if ( bp_get_group_has_avatar() ) : ?>
+
+		<p><?php _e( "If you'd like to remove the existing group profile photo but not upload a new one, please use the delete group profile photo button.", 'buddypress' ); ?></p>
+
+		<?php bp_button( array( 'id' => 'delete_group_avatar', 'component' => 'groups', 'wrapper_id' => 'delete-group-avatar-button', 'link_class' => 'edit', 'link_href' => bp_get_group_avatar_delete_link(), 'link_text' => __( 'Delete Group Profile Photo', 'buddypress' ) ) ); ?>
+
+	<?php endif; ?>
+
+	<?php
+	/**
+	 * Load the Avatar UI templates
+	 *
+	 * @since  2.3.0
+	 */
+	bp_avatar_get_templates(); ?>
+
+	<?php wp_nonce_field( 'bp_avatar_upload' ); ?>
+
+<?php endif; ?>
+
+<?php if ( 'crop-image' == bp_get_avatar_admin_step() ) : ?>
+
+	<h4><?php _e( 'Crop Profile Photo', 'buddypress' ); ?></h4>
+
+	<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-to-crop" class="avatar" alt="<?php esc_attr_e( 'Profile photo to crop', 'buddypress' ); ?>" />
+
+	<div id="avatar-crop-pane">
+		<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-crop-preview" class="avatar" alt="<?php esc_attr_e( 'Profile photo preview', 'buddypress' ); ?>" />
+	</div>
+
+	<input type="submit" name="avatar-crop-submit" id="avatar-crop-submit" value="<?php esc_attr_e( 'Crop Image', 'buddypress' ); ?>" />
+
+	<input type="hidden" name="image_src" id="image_src" value="<?php bp_avatar_to_crop_src(); ?>" />
+	<input type="hidden" id="x" name="x" />
+	<input type="hidden" id="y" name="y" />
+	<input type="hidden" id="w" name="w" />
+	<input type="hidden" id="h" name="h" />
+
+	<?php wp_nonce_field( 'bp_avatar_cropstore' ); ?>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-cover-image.php	(revision 12265)
@@ -0,0 +1,34 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Group Cover Image Settings
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2><?php _e( 'Cover Image', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the display of profile cover image upload content.
+ *
+ * @since 2.4.0
+ */
+do_action( 'bp_before_group_settings_cover_image' ); ?>
+
+<p><?php _e( 'The Cover Image will be used to customize the header of your group.', 'buddypress' ); ?></p>
+
+<?php bp_attachments_get_template_part( 'cover-images/index' ); ?>
+
+<?php
+
+/**
+ * Fires after the display of group cover image upload content.
+ *
+ * @since 2.4.0
+ */
+do_action( 'bp_after_group_settings_cover_image' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/group-settings.php	(revision 12265)
@@ -0,0 +1,111 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Group Settings
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2 class="bp-screen-reader-text"><?php _e( 'Manage Group Settings', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the group settings admin display.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_settings_admin' ); ?>
+
+<fieldset class="group-create-privacy">
+
+	<legend><?php _e( 'Privacy Options', 'buddypress' ); ?></legend>
+
+	<div class="radio">
+
+		<label for="group-status-public"><input type="radio" name="group-status" id="group-status-public" value="public"<?php if ( 'public' == bp_get_new_group_status() || !bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="public-group-description" /> <?php _e( 'This is a public group', 'buddypress' ); ?></label>
+
+		<ul id="public-group-description">
+			<li><?php _e( 'Any site member can join this group.', 'buddypress' ); ?></li>
+			<li><?php _e( 'This group will be listed in the groups directory and in search results.', 'buddypress' ); ?></li>
+			<li><?php _e( 'Group content and activity will be visible to any site member.', 'buddypress' ); ?></li>
+		</ul>
+
+		<label for="group-status-private"><input type="radio" name="group-status" id="group-status-private" value="private"<?php if ( 'private' == bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="private-group-description" /> <?php _e( 'This is a private group', 'buddypress' ); ?></label>
+
+		<ul id="private-group-description">
+			<li><?php _e( 'Only users who request membership and are accepted can join the group.', 'buddypress' ); ?></li>
+			<li><?php _e( 'This group will be listed in the groups directory and in search results.', 'buddypress' ); ?></li>
+			<li><?php _e( 'Group content and activity will only be visible to members of the group.', 'buddypress' ); ?></li>
+		</ul>
+
+		<label for="group-status-hidden"><input type="radio" name="group-status" id="group-status-hidden" value="hidden"<?php if ( 'hidden' == bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="hidden-group-description" /> <?php _e('This is a hidden group', 'buddypress' ); ?></label>
+
+		<ul id="hidden-group-description">
+			<li><?php _e( 'Only users who are invited can join the group.', 'buddypress' ); ?></li>
+			<li><?php _e( 'This group will not be listed in the groups directory or search results.', 'buddypress' ); ?></li>
+			<li><?php _e( 'Group content and activity will only be visible to members of the group.', 'buddypress' ); ?></li>
+		</ul>
+
+	</div>
+
+</fieldset>
+
+<?php // Group type selection ?>
+<?php if ( $group_types = bp_groups_get_group_types( array( 'show_in_create_screen' => true ), 'objects' ) ): ?>
+
+	<fieldset class="group-create-types">
+		<legend><?php _e( 'Group Types', 'buddypress' ); ?></legend>
+
+		<p><?php _e( 'Select the types this group should be a part of.', 'buddypress' ); ?></p>
+
+		<?php foreach ( $group_types as $type ) : ?>
+			<div class="checkbox">
+				<label for="<?php printf( 'group-type-%s', $type->name ); ?>">
+					<input type="checkbox" name="group-types[]" id="<?php printf( 'group-type-%s', $type->name ); ?>" value="<?php echo esc_attr( $type->name ); ?>" <?php checked( bp_groups_has_group_type( bp_get_current_group_id(), $type->name ) ); ?>/> <?php echo esc_html( $type->labels['name'] ); ?>
+					<?php
+						if ( ! empty( $type->description ) ) {
+							printf( __( '&ndash; %s', 'buddypress' ), '<span class="bp-group-type-desc">' . esc_html( $type->description ) . '</span>' );
+						}
+					?>
+				</label>
+			</div>
+
+		<?php endforeach; ?>
+
+	</fieldset>
+
+<?php endif; ?>
+
+<fieldset class="group-create-invitations">
+
+	<legend><?php _e( 'Group Invitations', 'buddypress' ); ?></legend>
+
+	<p><?php _e( 'Which members of this group are allowed to invite others?', 'buddypress' ); ?></p>
+
+	<div class="radio">
+
+		<label for="group-invite-status-members"><input type="radio" name="group-invite-status" id="group-invite-status-members" value="members"<?php bp_group_show_invite_status_setting( 'members' ); ?> /> <?php _e( 'All group members', 'buddypress' ); ?></label>
+
+		<label for="group-invite-status-mods"><input type="radio" name="group-invite-status" id="group-invite-status-mods" value="mods"<?php bp_group_show_invite_status_setting( 'mods' ); ?> /> <?php _e( 'Group admins and mods only', 'buddypress' ); ?></label>
+
+		<label for="group-invite-status-admins"><input type="radio" name="group-invite-status" id="group-invite-status-admins" value="admins"<?php bp_group_show_invite_status_setting( 'admins' ); ?> /> <?php _e( 'Group admins only', 'buddypress' ); ?></label>
+
+	</div>
+
+</fieldset>
+
+<?php
+
+/**
+ * Fires after the group settings admin display.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_settings_admin' ); ?>
+
+<p><input type="submit" value="<?php esc_attr_e( 'Save Changes', 'buddypress' ); ?>" id="save" name="save" /></p>
+<?php wp_nonce_field( 'groups_edit_group_settings' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/manage-members.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/manage-members.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/manage-members.php	(revision 12265)
@@ -0,0 +1,333 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Manage Members
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2 class="bp-screen-reader-text"><?php _e( 'Manage Members', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the group manage members admin display.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_manage_members_admin' ); ?>
+
+<div aria-live="polite" aria-relevant="all" aria-atomic="true">
+
+	<div class="bp-widget group-members-list group-admins-list">
+		<h3 class="section-header"><?php _e( 'Administrators', 'buddypress' ); ?></h3>
+
+		<?php if ( bp_group_has_members( array( 'per_page' => 15, 'group_role' => array( 'admin' ), 'page_arg' => 'mlpage-admin' ) ) ) : ?>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<div class="pagination no-ajax">
+
+					<div id="member-count" class="pag-count">
+						<?php bp_group_member_pagination_count(); ?>
+					</div>
+
+					<div id="member-admin-pagination" class="pagination-links">
+						<?php bp_group_member_admin_pagination(); ?>
+					</div>
+
+				</div>
+
+			<?php endif; ?>
+
+			<ul id="admins-list" class="item-list">
+				<?php while ( bp_group_members() ) : bp_group_the_member(); ?>
+					<li>
+						<div class="item-avatar">
+							<?php bp_group_member_avatar_thumb(); ?>
+						</div>
+
+						<div class="item">
+							<div class="item-title">
+								<?php bp_group_member_link(); ?>
+							</div>
+							<p class="joined item-meta">
+								<?php bp_group_member_joined_since(); ?>
+							</p>
+							<?php
+
+							/**
+							 * Fires inside the item section of a member admin item in group management area.
+							 *
+							 * @since 1.1.0
+							 * @since 2.7.0 Added $section parameter.
+							 *
+							 * @param $section Which list contains this item.
+							 */
+							do_action( 'bp_group_manage_members_admin_item', 'admins-list' ); ?>
+						</div>
+
+						<div class="action">
+							<?php if ( count( bp_group_admin_ids( false, 'array' ) ) > 1 ) : ?>
+								<a class="button confirm admin-demote-to-member" href="<?php bp_group_member_demote_link(); ?>"><?php _e( 'Demote to Member', 'buddypress' ); ?></a>
+							<?php endif; ?>
+
+							<?php
+
+							/**
+							 * Fires inside the action section of a member admin item in group management area.
+							 *
+							 * @since 2.7.0
+							 *
+							 * @param $section Which list contains this item.
+							 */
+							do_action( 'bp_group_manage_members_admin_actions', 'admins-list' ); ?>
+						</div>
+					</li>
+				<?php endwhile; ?>
+			</ul>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<div class="pagination no-ajax">
+
+					<div id="member-count" class="pag-count">
+						<?php bp_group_member_pagination_count(); ?>
+					</div>
+
+					<div id="member-admin-pagination" class="pagination-links">
+						<?php bp_group_member_admin_pagination(); ?>
+					</div>
+
+				</div>
+
+			<?php endif; ?>
+
+		<?php else: ?>
+
+		<div id="message" class="info">
+			<p><?php _e( 'No group administrators were found.', 'buddypress' ); ?></p>
+		</div>
+
+		<?php endif; ?>
+	</div>
+
+	<div class="bp-widget group-members-list group-mods-list">
+		<h3 class="section-header"><?php _e( 'Moderators', 'buddypress' ); ?></h3>
+
+		<?php if ( bp_group_has_members( array( 'per_page' => 15, 'group_role' => array( 'mod' ), 'page_arg' => 'mlpage-mod' ) ) ) : ?>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<div class="pagination no-ajax">
+
+					<div id="member-count" class="pag-count">
+						<?php bp_group_member_pagination_count(); ?>
+					</div>
+
+					<div id="member-admin-pagination" class="pagination-links">
+						<?php bp_group_member_admin_pagination(); ?>
+					</div>
+
+				</div>
+
+			<?php endif; ?>
+
+			<ul id="mods-list" class="item-list">
+
+				<?php while ( bp_group_members() ) : bp_group_the_member(); ?>
+					<li>
+						<div class="item-avatar">
+							<?php bp_group_member_avatar_thumb(); ?>
+						</div>
+
+						<div class="item">
+							<div class="item-title">
+								<?php bp_group_member_link(); ?>
+							</div>
+							<p class="joined item-meta">
+								<?php bp_group_member_joined_since(); ?>
+							</p>
+							<?php
+
+							/**
+							 * Fires inside the item section of a member admin item in group management area.
+							 *
+							 * @since 1.1.0
+							 * @since 2.7.0 Added $section parameter.
+							 *
+							 * @param $section Which list contains this item.
+							 */
+							do_action( 'bp_group_manage_members_admin_item', 'admins-list' ); ?>
+						</div>
+
+						<div class="action">
+							<a href="<?php bp_group_member_promote_admin_link(); ?>" class="button confirm mod-promote-to-admin"><?php _e( 'Promote to Admin', 'buddypress' ); ?></a>
+							<a class="button confirm mod-demote-to-member" href="<?php bp_group_member_demote_link(); ?>"><?php _e( 'Demote to Member', 'buddypress' ); ?></a>
+
+							<?php
+
+							/**
+							 * Fires inside the action section of a member admin item in group management area.
+							 *
+							 * @since 2.7.0
+							 *
+							 * @param $section Which list contains this item.
+							 */
+							do_action( 'bp_group_manage_members_admin_actions', 'mods-list' ); ?>
+
+						</div>
+					</li>
+				<?php endwhile; ?>
+
+			</ul>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<div class="pagination no-ajax">
+
+					<div id="member-count" class="pag-count">
+						<?php bp_group_member_pagination_count(); ?>
+					</div>
+
+					<div id="member-admin-pagination" class="pagination-links">
+						<?php bp_group_member_admin_pagination(); ?>
+					</div>
+
+				</div>
+
+			<?php endif; ?>
+
+		<?php else: ?>
+
+			<div id="message" class="info">
+				<p><?php _e( 'No group moderators were found.', 'buddypress' ); ?></p>
+			</div>
+
+		<?php endif; ?>
+	</div>
+
+	<div class="bp-widget group-members-list">
+		<h3 class="section-header"><?php _e( "Members", 'buddypress' ); ?></h3>
+
+		<?php if ( bp_group_has_members( array( 'per_page' => 15, 'exclude_banned' => 0 ) ) ) : ?>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<div class="pagination no-ajax">
+
+					<div id="member-count" class="pag-count">
+						<?php bp_group_member_pagination_count(); ?>
+					</div>
+
+					<div id="member-admin-pagination" class="pagination-links">
+						<?php bp_group_member_admin_pagination(); ?>
+					</div>
+
+				</div>
+
+			<?php endif; ?>
+
+			<ul id="members-list" class="item-list" aria-live="assertive" aria-relevant="all">
+				<?php while ( bp_group_members() ) : bp_group_the_member(); ?>
+
+					<li class="<?php bp_group_member_css_class(); ?>">
+						<div class="item-avatar">
+							<?php bp_group_member_avatar_thumb(); ?>
+						</div>
+
+						<div class="item">
+							<div class="item-title">
+								<?php bp_group_member_link(); ?>
+								<?php
+								if ( bp_get_group_member_is_banned() ) {
+									echo ' <span class="banned">';
+									_e( '(banned)', 'buddypress' );
+									echo '</span>';
+								} ?>
+							</div>
+							<p class="joined item-meta">
+								<?php bp_group_member_joined_since(); ?>
+							</p>
+							<?php
+
+							/**
+							 * Fires inside the item section of a member admin item in group management area.
+							 *
+							 * @since 1.1.0
+							 * @since 2.7.0 Added $section parameter.
+							 *
+							 * @param $section Which list contains this item.
+							 */
+							do_action( 'bp_group_manage_members_admin_item', 'admins-list' ); ?>
+						</div>
+
+						<div class="action">
+							<?php if ( bp_get_group_member_is_banned() ) : ?>
+
+								<a href="<?php bp_group_member_unban_link(); ?>" class="button confirm member-unban"><?php _e( 'Remove Ban', 'buddypress' ); ?></a>
+
+							<?php else : ?>
+
+								<a href="<?php bp_group_member_ban_link(); ?>" class="button confirm member-ban"><?php _e( 'Kick &amp; Ban', 'buddypress' ); ?></a>
+								<a href="<?php bp_group_member_promote_mod_link(); ?>" class="button confirm member-promote-to-mod"><?php _e( 'Promote to Mod', 'buddypress' ); ?></a>
+								<a href="<?php bp_group_member_promote_admin_link(); ?>" class="button confirm member-promote-to-admin"><?php _e( 'Promote to Admin', 'buddypress' ); ?></a>
+
+							<?php endif; ?>
+
+							<a href="<?php bp_group_member_remove_link(); ?>" class="button confirm"><?php _e( 'Remove from group', 'buddypress' ); ?></a>
+
+							<?php
+
+							/**
+							 * Fires inside the action section of a member admin item in group management area.
+							 *
+							 * @since 2.7.0
+							 *
+							 * @param $section Which list contains this item.
+							 */
+							do_action( 'bp_group_manage_members_admin_actions', 'members-list' ); ?>
+						</div>
+					</li>
+
+				<?php endwhile; ?>
+			</ul>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<div class="pagination no-ajax">
+
+					<div id="member-count" class="pag-count">
+						<?php bp_group_member_pagination_count(); ?>
+					</div>
+
+					<div id="member-admin-pagination" class="pagination-links">
+						<?php bp_group_member_admin_pagination(); ?>
+					</div>
+
+				</div>
+
+			<?php endif; ?>
+
+		<?php else: ?>
+
+			<div id="message" class="info">
+				<p><?php _e( 'No group members were found.', 'buddypress' ); ?></p>
+			</div>
+
+		<?php endif; ?>
+	</div>
+
+</div>
+
+<?php
+
+/**
+ * Fires after the group manage members admin display.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_manage_members_admin' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/membership-requests.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/membership-requests.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/admin/membership-requests.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * BuddyPress - Groups Admin - Membership Requests
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2 class="bp-screen-reader-text"><?php _e( 'Manage Membership Requests', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the display of group membership requests admin.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_membership_requests_admin' ); ?>
+
+	<div class="requests">
+
+		<?php bp_get_template_part( 'groups/single/requests-loop' ); ?>
+
+	</div>
+
+<?php
+
+/**
+ * Fires after the display of group membership requests admin.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_membership_requests_admin' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/cover-image-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/cover-image-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/cover-image-header.php	(revision 12265)
@@ -0,0 +1,130 @@
+<?php
+/**
+ * BuddyPress - Groups Cover Image Header.
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of a group's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_group_header' ); ?>
+
+<div id="cover-image-container">
+	<a id="header-cover-image" href="<?php echo esc_url( bp_get_group_permalink() ); ?>"></a>
+
+	<div id="item-header-cover-image">
+		<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+			<div id="item-header-avatar">
+				<a href="<?php echo esc_url( bp_get_group_permalink() ); ?>">
+
+					<?php bp_group_avatar(); ?>
+
+				</a>
+			</div><!-- #item-header-avatar -->
+		<?php endif; ?>
+
+		<div id="item-header-content">
+
+			<div id="item-buttons"><?php
+
+				/**
+				 * Fires in the group header actions section.
+				 *
+				 * @since 1.2.6
+				 */
+				do_action( 'bp_group_header_actions' ); ?></div><!-- #item-buttons -->
+
+			<?php
+
+			/**
+			 * Fires before the display of the group's header meta.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_before_group_header_meta' ); ?>
+
+			<div id="item-meta">
+
+				<?php
+
+				/**
+				 * Fires after the group header actions section.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_group_header_meta' ); ?>
+
+				<span class="highlight"><?php bp_group_type(); ?></span>
+				<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_last_active( 0, array( 'relative' => false ) ) ); ?>"><?php printf( __( 'active %s', 'buddypress' ), bp_get_group_last_active() ); ?></span>
+
+				<?php bp_group_description(); ?>
+
+				<?php bp_group_type_list(); ?>
+			</div>
+		</div><!-- #item-header-content -->
+
+		<div id="item-actions">
+
+			<?php if ( bp_group_is_visible() ) : ?>
+
+				<h2><?php _e( 'Group Admins', 'buddypress' ); ?></h2>
+
+				<?php bp_group_list_admins();
+
+				/**
+				 * Fires after the display of the group's administrators.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_group_menu_admins' );
+
+				if ( bp_group_has_moderators() ) :
+
+					/**
+					 * Fires before the display of the group's moderators, if there are any.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_before_group_menu_mods' ); ?>
+
+					<h2><?php _e( 'Group Mods' , 'buddypress' ); ?></h2>
+
+					<?php bp_group_list_mods();
+
+					/**
+					 * Fires after the display of the group's moderators, if there are any.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_after_group_menu_mods' );
+
+				endif;
+
+			endif; ?>
+
+		</div><!-- #item-actions -->
+
+	</div><!-- #item-header-cover-image -->
+</div><!-- #cover-image-container -->
+
+<?php
+
+/**
+ * Fires after the display of a group's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_group_header' ); ?>
+
+<div id="template-notices" role="alert" aria-atomic="true">
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+	do_action( 'template_notices' ); ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/group-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/group-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/group-header.php	(revision 12265)
@@ -0,0 +1,129 @@
+<?php
+/**
+ * BuddyPress - Groups Header
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of a group's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_group_header' );
+
+?>
+
+<div id="item-actions">
+
+	<?php if ( bp_group_is_visible() ) : ?>
+
+		<h2><?php _e( 'Group Admins', 'buddypress' ); ?></h2>
+
+		<?php bp_group_list_admins();
+
+		/**
+		 * Fires after the display of the group's administrators.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_group_menu_admins' );
+
+		if ( bp_group_has_moderators() ) :
+
+			/**
+			 * Fires before the display of the group's moderators, if there are any.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_before_group_menu_mods' ); ?>
+
+			<h2><?php _e( 'Group Mods' , 'buddypress' ); ?></h2>
+
+			<?php bp_group_list_mods();
+
+			/**
+			 * Fires after the display of the group's moderators, if there are any.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_after_group_menu_mods' );
+
+		endif;
+
+	endif; ?>
+
+</div><!-- #item-actions -->
+
+<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+	<div id="item-header-avatar">
+		<a href="<?php echo esc_url( bp_get_group_permalink() ); ?>" class="bp-tooltip" data-bp-tooltip="<?php echo esc_attr( bp_get_group_name() ); ?>">
+
+			<?php bp_group_avatar(); ?>
+
+		</a>
+	</div><!-- #item-header-avatar -->
+<?php endif; ?>
+
+<div id="item-header-content">
+	<span class="highlight"><?php bp_group_type(); ?></span>
+	<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_last_active( 0, array( 'relative' => false ) ) ); ?>"><?php printf( __( 'active %s', 'buddypress' ), bp_get_group_last_active() ); ?></span>
+
+	<?php
+
+	/**
+	 * Fires before the display of the group's header meta.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_group_header_meta' ); ?>
+
+	<div id="item-meta">
+
+		<?php bp_group_description(); ?>
+
+		<?php bp_group_type_list(); ?>
+
+		<div id="item-buttons">
+
+			<?php
+
+			/**
+			 * Fires in the group header actions section.
+			 *
+			 * @since 1.2.6
+			 */
+			do_action( 'bp_group_header_actions' ); ?>
+
+		</div><!-- #item-buttons -->
+
+		<?php
+
+		/**
+		 * Fires after the group header actions section.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_group_header_meta' ); ?>
+
+	</div>
+</div><!-- #item-header-content -->
+
+<?php
+
+/**
+ * Fires after the display of a group's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_group_header' );  ?>
+
+<div id="template-notices" role="alert" aria-atomic="true">
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+	do_action( 'template_notices' ); ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/home.php	(revision 12265)
@@ -0,0 +1,153 @@
+<?php
+/**
+ * BuddyPress - Groups Home
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div id="buddypress">
+
+	<?php if ( bp_has_groups() ) : while ( bp_groups() ) : bp_the_group(); ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the group home content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_group_home_content' ); ?>
+
+	<div id="item-header" role="complementary">
+
+		<?php
+		/**
+		 * If the cover image feature is enabled, use a specific header
+		 */
+		if ( bp_group_use_cover_image_header() ) :
+			bp_get_template_part( 'groups/single/cover-image-header' );
+		else :
+			bp_get_template_part( 'groups/single/group-header' );
+		endif;
+		?>
+
+	</div><!-- #item-header -->
+
+	<div id="item-nav">
+		<div class="item-list-tabs no-ajax" id="object-nav" aria-label="<?php esc_attr_e( 'Group primary navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+
+				<?php bp_get_options_nav(); ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of group options navigation.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_group_options_nav' ); ?>
+
+			</ul>
+		</div>
+	</div><!-- #item-nav -->
+
+	<div id="item-body">
+
+		<?php
+
+		/**
+		 * Fires before the display of the group home body.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_group_body' );
+
+		/**
+		 * Does this next bit look familiar? If not, go check out WordPress's
+		 * /wp-includes/template-loader.php file.
+		 *
+		 * @todo A real template hierarchy? Gasp!
+		 */
+
+			// Looking at home location
+			if ( bp_is_group_home() ) :
+
+				if ( bp_group_is_visible() ) {
+
+					// Load appropriate front template
+					bp_groups_front_template_part();
+
+				} else {
+
+					/**
+					 * Fires before the display of the group status message.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_before_group_status_message' ); ?>
+
+					<div id="message" class="info">
+						<p><?php bp_group_status_message(); ?></p>
+					</div>
+
+					<?php
+
+					/**
+					 * Fires after the display of the group status message.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_after_group_status_message' );
+
+				}
+
+			// Not looking at home
+			else :
+
+				// Group Admin
+				if     ( bp_is_group_admin_page() ) : bp_get_template_part( 'groups/single/admin'        );
+
+				// Group Activity
+				elseif ( bp_is_group_activity()   ) : bp_get_template_part( 'groups/single/activity'     );
+
+				// Group Members
+				elseif ( bp_is_group_members()    ) : bp_groups_members_template_part();
+
+				// Group Invitations
+				elseif ( bp_is_group_invites()    ) : bp_get_template_part( 'groups/single/send-invites' );
+
+				// Membership request
+				elseif ( bp_is_group_membership_request() ) : bp_get_template_part( 'groups/single/request-membership' );
+
+				// Anything else (plugins mostly)
+				else                                : bp_get_template_part( 'groups/single/plugins'      );
+
+				endif;
+
+			endif;
+
+		/**
+		 * Fires after the display of the group home body.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_group_body' ); ?>
+
+	</div><!-- #item-body -->
+
+	<?php
+
+	/**
+	 * Fires after the display of the group home content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_group_home_content' ); ?>
+
+	<?php endwhile; endif; ?>
+
+</div><!-- #buddypress -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/invites-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/invites-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/invites-loop.php	(revision 12265)
@@ -0,0 +1,125 @@
+<?php
+/**
+ * BuddyPress - Group Invites Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div class="left-menu">
+
+	<div id="invite-list">
+
+		<ul>
+			<?php bp_new_group_invite_friend_list(); ?>
+		</ul>
+
+		<?php wp_nonce_field( 'groups_invite_uninvite_user', '_wpnonce_invite_uninvite_user' ); ?>
+
+	</div>
+
+</div><!-- .left-menu -->
+
+<div class="main-column">
+
+	<?php
+
+	/**
+	 * Fires before the display of the group send invites list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_group_send_invites_list' ); ?>
+
+	<?php if ( bp_group_has_invites( bp_ajax_querystring( 'invite' ) . '&per_page=10' ) ) : ?>
+
+		<div id="pag-top" class="pagination">
+
+			<div class="pag-count" id="group-invite-count-top">
+
+				<?php bp_group_invite_pagination_count(); ?>
+
+			</div>
+
+			<div class="pagination-links" id="group-invite-pag-top">
+
+				<?php bp_group_invite_pagination_links(); ?>
+
+			</div>
+
+		</div>
+
+		<?php /* The ID 'friend-list' is important for AJAX support. */ ?>
+		<ul id="friend-list" class="item-list">
+
+		<?php while ( bp_group_invites() ) : bp_group_the_invite(); ?>
+
+			<li id="<?php bp_group_invite_item_id(); ?>">
+				<?php bp_group_invite_user_avatar(); ?>
+
+				<h3><?php bp_group_invite_user_link(); ?></h3>
+				<span class="activity"><?php bp_group_invite_user_last_active(); ?></span>
+
+				<?php
+
+				/**
+				 * Fires inside the invite item listing.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_group_send_invites_item' ); ?>
+
+				<div class="action">
+					<a class="button remove" href="<?php bp_group_invite_user_remove_invite_url(); ?>" id="<?php bp_group_invite_item_id(); ?>"><?php _e( 'Remove Invite', 'buddypress' ); ?></a>
+
+					<?php
+
+					/**
+					 * Fires inside the action area for a send invites item.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_group_send_invites_item_action' ); ?>
+				</div>
+			</li>
+
+		<?php endwhile; ?>
+
+		</ul><!-- #friend-list -->
+
+		<div id="pag-bottom" class="pagination">
+
+			<div class="pag-count" id="group-invite-count-bottom">
+
+				<?php bp_group_invite_pagination_count(); ?>
+
+			</div>
+
+			<div class="pagination-links" id="group-invite-pag-bottom">
+
+				<?php bp_group_invite_pagination_links(); ?>
+
+			</div>
+
+		</div>
+
+	<?php else : ?>
+
+		<div id="message" class="info">
+			<p><?php _e( 'Select friends to invite.', 'buddypress' ); ?></p>
+		</div>
+
+	<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of the group send invites list.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_send_invites_list' ); ?>
+
+</div><!-- .main-column -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/members.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/members.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/members.php	(revision 12265)
@@ -0,0 +1,135 @@
+<?php
+/**
+ * BuddyPress - Groups Members
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php if ( bp_group_has_members( bp_ajax_querystring( 'group_members' ) ) ) : ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the group members content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_group_members_content' ); ?>
+
+	<div id="pag-top" class="pagination">
+
+		<div class="pag-count" id="member-count-top">
+
+			<?php bp_members_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="member-pag-top">
+
+			<?php bp_members_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires before the display of the group members list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_group_members_list' ); ?>
+
+	<ul id="member-list" class="item-list">
+
+		<?php while ( bp_group_members() ) : bp_group_the_member(); ?>
+
+			<li>
+				<a href="<?php bp_group_member_domain(); ?>">
+
+					<?php bp_group_member_avatar_thumb(); ?>
+
+				</a>
+
+				<h5><?php bp_group_member_link(); ?></h5>
+				<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_member_joined_since( array( 'relative' => false ) ) ); ?>"><?php bp_group_member_joined_since(); ?></span>
+
+				<?php
+
+				/**
+				 * Fires inside the listing of an individual group member listing item.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_group_members_list_item' ); ?>
+
+				<?php if ( bp_is_active( 'friends' ) ) : ?>
+
+					<div class="action">
+
+						<?php bp_add_friend_button( bp_get_group_member_id(), bp_get_group_member_is_friend() ); ?>
+
+						<?php
+
+						/**
+						 * Fires inside the action section of an individual group member listing item.
+						 *
+						 * @since 1.1.0
+						 */
+						do_action( 'bp_group_members_list_item_action' ); ?>
+
+					</div>
+
+				<?php endif; ?>
+			</li>
+
+		<?php endwhile; ?>
+
+	</ul>
+
+	<?php
+
+	/**
+	 * Fires after the display of the group members list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_group_members_list' ); ?>
+
+	<div id="pag-bottom" class="pagination">
+
+		<div class="pag-count" id="member-count-bottom">
+
+			<?php bp_members_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="member-pag-bottom">
+
+			<?php bp_members_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires after the display of the group members content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_group_members_content' ); ?>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'No members were found.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/plugins.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/plugins.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/plugins.php	(revision 12265)
@@ -0,0 +1,33 @@
+<?php
+/**
+ * BuddyPress - Groups plugins
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of content for plugins using the BP_Group_Extension.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_group_plugin_template' ); ?>
+
+<?php
+
+/**
+ * Fires and displays content for plugins using the BP_Group_Extension.
+ *
+ * @since 1.0.0
+ */
+do_action( 'bp_template_content' ); ?>
+
+<?php
+
+/**
+ * Fires after the display of content for plugins using the BP_Group_Extension.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_group_plugin_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/request-membership.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/request-membership.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/request-membership.php	(revision 12265)
@@ -0,0 +1,58 @@
+<?php
+/**
+ * BuddyPress - Groups Request Membership
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.1.0
+ */
+
+/**
+ * Fires before the display of the group membership request form.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_request_membership_content' ); ?>
+
+<?php if ( !bp_group_has_requested_membership() ) : ?>
+	<h2 class="bp-screen-reader-text"><?php esc_html_e( 'Group membership request form', 'buddypress' ); ?></h2>
+
+	<p>
+		<?php
+		echo esc_html(
+			sprintf(
+				/* translators:  %s =group name */
+				__( 'You are requesting to become a member of the group "%s".', 'buddypress' ),
+				bp_get_group_name()
+			)
+		);
+		?>
+	</p>
+
+	<form action="<?php bp_group_form_action('request-membership' ); ?>" method="post" name="request-membership-form" id="request-membership-form" class="standard-form">
+		<label for="group-request-membership-comments"><?php esc_html_e( 'Comments (optional)', 'buddypress' ); ?></label>
+		<textarea name="group-request-membership-comments" id="group-request-membership-comments"></textarea>
+
+		<?php
+
+		/**
+		 * Fires after the textarea for the group membership request form.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_group_request_membership_content' ); ?>
+
+		<p><input type="submit" name="group-request-send" id="group-request-send" value="<?php echo esc_attr_x( 'Send Request', 'button', 'buddypress' ); ?>" />
+
+		<?php wp_nonce_field( 'groups_request_membership' ); ?>
+	</form><!-- #request-membership-form -->
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of the group membership request form.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_request_membership_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/requests-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/requests-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/requests-loop.php	(revision 12265)
@@ -0,0 +1,99 @@
+<?php
+/**
+ * BuddyPress - Groups Requests Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php if ( bp_group_has_membership_requests( bp_ajax_querystring( 'membership_requests' ) ) ) : ?>
+
+	<div id="pag-top" class="pagination">
+
+		<div class="pag-count" id="group-mem-requests-count-top">
+
+			<?php bp_group_requests_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="group-mem-requests-pag-top">
+
+			<?php bp_group_requests_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<ul id="request-list" class="item-list">
+		<?php while ( bp_group_membership_requests() ) : bp_group_the_membership_request(); ?>
+
+			<li class="item-list group-request-list">
+
+				<div class="item-avatar"><?php bp_group_request_user_avatar_thumb(); ?></div>
+
+				<div class="item">
+
+					<div class="item-title"><?php bp_group_request_user_link(); ?> </div>
+
+					<span class="activity"><?php bp_group_request_time_since_requested(); ?></span>
+
+					<p class="comments"><?php bp_group_request_comment(); ?></p>
+
+					<?php
+
+					/**
+					 * Fires inside the groups membership request list loop.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_group_membership_requests_admin_item' ); ?>
+
+				</div>
+
+				<div class="action">
+
+					<?php bp_button( array( 'id' => 'group_membership_accept', 'component' => 'groups', 'wrapper_class' => 'accept', 'link_href' => bp_get_group_request_accept_link(), 'link_text' => __( 'Accept', 'buddypress' ) ) ); ?>
+
+					<?php bp_button( array( 'id' => 'group_membership_reject', 'component' => 'groups', 'wrapper_class' => 'reject', 'link_href' => bp_get_group_request_reject_link(), 'link_text' => __( 'Reject', 'buddypress' ) ) ); ?>
+
+					<?php
+
+					/**
+					 * Fires inside the list of membership request actions.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_group_membership_requests_admin_item_action' ); ?>
+
+				</div>
+			</li>
+
+		<?php endwhile; ?>
+	</ul>
+
+	<div id="pag-bottom" class="pagination">
+
+		<div class="pag-count" id="group-mem-requests-count-bottom">
+
+			<?php bp_group_requests_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="group-mem-requests-pag-bottom">
+
+			<?php bp_group_requests_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<?php else: ?>
+
+		<div id="message" class="info">
+			<p><?php _e( 'There are no pending membership requests.', 'buddypress' ); ?></p>
+		</div>
+
+	<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/send-invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/send-invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/groups/single/send-invites.php	(revision 12265)
@@ -0,0 +1,67 @@
+<?php
+/**
+ * BuddyPress - Groups Send Invites
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the send invites content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_send_invites_content' ); ?>
+
+<?php
+/* Does the user have friends that could be invited to the group? */
+if ( bp_get_new_group_invite_friend_list() ) : ?>
+
+	<h2 class="bp-screen-reader-text"><?php _e( 'Send invites', 'buddypress' ); ?></h2>
+
+	<?php /* 'send-invite-form' is important for AJAX support */ ?>
+	<form action="<?php bp_group_send_invite_form_action(); ?>" method="post" id="send-invite-form" class="standard-form">
+
+		<div class="invite" aria-live="polite" aria-atomic="false" aria-relevant="all">
+			<?php bp_get_template_part( 'groups/single/invites-loop' ); ?>
+		</div>
+
+		<div class="submit">
+			<input type="submit" name="submit" id="submit" value="<?php esc_attr_e( 'Send Invites', 'buddypress' ); ?>" />
+		</div>
+
+		<?php wp_nonce_field( 'groups_send_invites', '_wpnonce_send_invites' ); ?>
+
+		<?php /* This is important, don't forget it */ ?>
+		<input type="hidden" name="group_id" id="group_id" value="<?php bp_group_id(); ?>" />
+
+	</form><!-- #send-invite-form -->
+
+<?php
+/* No eligible friends? Maybe the user doesn't have any friends yet. */
+elseif ( 0 == bp_get_total_friend_count( bp_loggedin_user_id() ) ) : ?>
+
+	<div id="message" class="info">
+		<p class="notice"><?php _e( 'Group invitations can only be extended to friends.', 'buddypress' ); ?></p>
+		<p class="message-body"><?php _e( "Once you've made some friendships, you'll be able to invite those members to this group.", 'buddypress' ); ?></p>
+	</div>
+
+<?php
+/* The user does have friends, but none are eligible to be invited to this group. */
+else : ?>
+
+	<div id="message" class="info">
+		<p class="notice"><?php _e( 'All of your friends already belong to this group.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the send invites content.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_group_send_invites_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/activate.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/activate.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/activate.php	(revision 12265)
@@ -0,0 +1,87 @@
+<?php
+/**
+ * BuddyPress - Members Activate
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the display of the member activation page.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_activation_page' ); ?>
+
+	<div class="page" id="activate-page">
+
+		<div id="template-notices" role="alert" aria-atomic="true">
+			<?php
+
+			/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+			do_action( 'template_notices' ); ?>
+
+		</div>
+
+		<?php
+
+		/**
+		 * Fires before the display of the member activation page content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_activate_content' ); ?>
+
+		<?php if ( bp_account_was_activated() ) : ?>
+
+			<?php if ( isset( $_GET['e'] ) ) : ?>
+				<p><?php _e( 'Your account was activated successfully! Your account details have been sent to you in a separate email.', 'buddypress' ); ?></p>
+			<?php else : ?>
+				<p><?php printf( __( 'Your account was activated successfully! You can now <a href="%s">log in</a> with the username and password you provided when you signed up.', 'buddypress' ), wp_login_url( bp_get_root_domain() ) ); ?></p>
+			<?php endif; ?>
+
+		<?php else : ?>
+
+			<p><?php _e( 'Please provide a valid activation key.', 'buddypress' ); ?></p>
+
+			<form action="" method="post" class="standard-form" id="activation-form">
+
+				<label for="key"><?php _e( 'Activation Key:', 'buddypress' ); ?></label>
+				<input type="text" name="key" id="key" value="<?php echo esc_attr( bp_get_current_activation_key() ); ?>" />
+
+				<p class="submit">
+					<input type="submit" name="submit" value="<?php esc_attr_e( 'Activate', 'buddypress' ); ?>" />
+				</p>
+
+			</form>
+
+		<?php endif; ?>
+
+		<?php
+
+		/**
+		 * Fires after the display of the member activation page content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_activate_content' ); ?>
+
+	</div><!-- .page -->
+
+	<?php
+
+	/**
+	 * Fires after the display of the member activation page.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_activation_page' ); ?>
+
+</div><!-- #buddypress -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/index.php	(revision 12265)
@@ -0,0 +1,163 @@
+<?php
+/**
+ * BuddyPress - Members
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires at the top of the members directory template file.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_before_directory_members_page' ); ?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the display of the members.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_members' ); ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the members content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_members_content' ); ?>
+
+	<?php /* Backward compatibility for inline search form. Use template part instead. */ ?>
+	<?php if ( has_filter( 'bp_directory_members_search_form' ) ) : ?>
+
+		<div id="members-dir-search" class="dir-search" role="search">
+			<?php bp_directory_members_search_form(); ?>
+		</div><!-- #members-dir-search -->
+
+	<?php else: ?>
+
+		<?php bp_get_template_part( 'common/search/dir-search-form' ); ?>
+
+	<?php endif; ?>
+
+	<?php
+	/**
+	 * Fires before the display of the members list tabs.
+	 *
+	 * @since 1.8.0
+	 */
+	do_action( 'bp_before_directory_members_tabs' ); ?>
+
+	<form action="" method="post" id="members-directory-form" class="dir-form">
+
+		<div class="item-list-tabs" aria-label="<?php esc_attr_e( 'Members directory main navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+				<li class="selected" id="members-all"><a href="<?php bp_members_directory_permalink(); ?>"><?php printf( __( 'All Members %s', 'buddypress' ), '<span>' . bp_get_total_member_count() . '</span>' ); ?></a></li>
+
+				<?php if ( is_user_logged_in() && bp_is_active( 'friends' ) && bp_get_total_friend_count( bp_loggedin_user_id() ) ) : ?>
+					<li id="members-personal"><a href="<?php echo esc_url( bp_loggedin_user_domain() . bp_get_friends_slug() . '/my-friends/' ); ?>"><?php printf( __( 'My Friends %s', 'buddypress' ), '<span>' . bp_get_total_friend_count( bp_loggedin_user_id() ) . '</span>' ); ?></a></li>
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires inside the members directory member types.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_members_directory_member_types' ); ?>
+
+			</ul>
+		</div><!-- .item-list-tabs -->
+
+		<div class="item-list-tabs" id="subnav" aria-label="<?php esc_attr_e( 'Members directory secondary navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+				<?php
+
+				/**
+				 * Fires inside the members directory member sub-types.
+				 *
+				 * @since 1.5.0
+				 */
+				do_action( 'bp_members_directory_member_sub_types' ); ?>
+
+				<li id="members-order-select" class="last filter">
+					<label for="members-order-by"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+					<select id="members-order-by">
+						<option value="active"><?php _e( 'Last Active', 'buddypress' ); ?></option>
+						<option value="newest"><?php _e( 'Newest Registered', 'buddypress' ); ?></option>
+
+						<?php if ( bp_is_active( 'xprofile' ) ) : ?>
+							<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+						<?php endif; ?>
+
+						<?php
+
+						/**
+						 * Fires inside the members directory member order options.
+						 *
+						 * @since 1.2.0
+						 */
+						do_action( 'bp_members_directory_order_options' ); ?>
+					</select>
+				</li>
+			</ul>
+		</div>
+
+		<h2 class="bp-screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Members directory', 'buddypress' );
+		?></h2>
+
+		<div id="members-dir-list" class="members dir-list">
+			<?php bp_get_template_part( 'members/members-loop' ); ?>
+		</div><!-- #members-dir-list -->
+
+		<?php
+
+		/**
+		 * Fires and displays the members content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_directory_members_content' ); ?>
+
+		<?php wp_nonce_field( 'directory_members', '_wpnonce-member-filter' ); ?>
+
+		<?php
+
+		/**
+		 * Fires after the display of the members content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_directory_members_content' ); ?>
+
+	</form><!-- #members-directory-form -->
+
+	<?php
+
+	/**
+	 * Fires after the display of the members.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_members' ); ?>
+
+</div><!-- #buddypress -->
+
+<?php
+
+/**
+ * Fires at the bottom of the members directory template file.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_after_directory_members_page' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/members-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/members-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/members-loop.php	(revision 12265)
@@ -0,0 +1,155 @@
+<?php
+/**
+ * BuddyPress - Members Loop
+ *
+ * Querystring is set via AJAX in _inc/ajax.php - bp_legacy_theme_object_filter()
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of the members loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_members_loop' ); ?>
+
+<?php if ( bp_get_current_member_type() ) : ?>
+	<p class="current-member-type"><?php bp_current_member_type_message() ?></p>
+<?php endif; ?>
+
+<?php if ( bp_has_members( bp_ajax_querystring( 'members' ) ) ) : ?>
+
+	<div id="pag-top" class="pagination">
+
+		<div class="pag-count" id="member-dir-count-top">
+
+			<?php bp_members_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="member-dir-pag-top">
+
+			<?php bp_members_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires before the display of the members list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_members_list' ); ?>
+
+	<ul id="members-list" class="item-list" aria-live="assertive" aria-relevant="all">
+
+	<?php while ( bp_members() ) : bp_the_member(); ?>
+
+		<li <?php bp_member_class(); ?>>
+			<div class="item-avatar">
+				<a href="<?php bp_member_permalink(); ?>"><?php bp_member_avatar(); ?></a>
+			</div>
+
+			<div class="item">
+				<div class="item-title">
+					<a href="<?php bp_member_permalink(); ?>"><?php bp_member_name(); ?></a>
+
+					<?php if ( bp_get_member_latest_update() ) : ?>
+
+						<span class="update"> <?php bp_member_latest_update(); ?></span>
+
+					<?php endif; ?>
+
+				</div>
+
+				<div class="item-meta"><span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_member_last_active( array( 'relative' => false ) ) ); ?>"><?php bp_member_last_active(); ?></span></div>
+
+				<?php
+
+				/**
+				 * Fires inside the display of a directory member item.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_directory_members_item' ); ?>
+
+				<?php
+				 /***
+				  * If you want to show specific profile fields here you can,
+				  * but it'll add an extra query for each member in the loop
+				  * (only one regardless of the number of fields you show):
+				  *
+				  * bp_member_profile_data( 'field=the field name' );
+				  */
+				?>
+			</div>
+
+			<div class="action">
+
+				<?php
+
+				/**
+				 * Fires inside the members action HTML markup to display actions.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_directory_members_actions' ); ?>
+
+			</div>
+
+			<div class="clear"></div>
+		</li>
+
+	<?php endwhile; ?>
+
+	</ul>
+
+	<?php
+
+	/**
+	 * Fires after the display of the members list.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_members_list' ); ?>
+
+	<?php bp_member_hidden_fields(); ?>
+
+	<div id="pag-bottom" class="pagination">
+
+		<div class="pag-count" id="member-dir-count-bottom">
+
+			<?php bp_members_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="member-dir-pag-bottom">
+
+			<?php bp_members_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( "Sorry, no members were found.", 'buddypress' ); ?></p>
+	</div>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of the members loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_members_loop' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/register.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/register.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/register.php	(revision 12265)
@@ -0,0 +1,432 @@
+<?php
+/**
+ * BuddyPress - Members Register
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires at the top of the BuddyPress member registration page template.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_register_page' ); ?>
+
+	<div class="page" id="register-page">
+
+		<form action="" name="signup_form" id="signup_form" class="standard-form" method="post" enctype="multipart/form-data">
+
+		<?php if ( 'registration-disabled' == bp_get_current_signup_step() ) : ?>
+
+			<div id="template-notices" role="alert" aria-atomic="true">
+				<?php
+
+				/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+				do_action( 'template_notices' ); ?>
+
+			</div>
+
+			<?php
+
+			/**
+			 * Fires before the display of the registration disabled message.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_before_registration_disabled' ); ?>
+
+				<p><?php _e( 'User registration is currently not allowed.', 'buddypress' ); ?></p>
+
+			<?php
+
+			/**
+			 * Fires after the display of the registration disabled message.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_after_registration_disabled' ); ?>
+		<?php endif; // registration-disabled signup step ?>
+
+		<?php if ( 'request-details' == bp_get_current_signup_step() ) : ?>
+
+			<div id="template-notices" role="alert" aria-atomic="true">
+				<?php
+
+				/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+				do_action( 'template_notices' ); ?>
+
+			</div>
+
+			<p><?php _e( 'Registering for this site is easy. Just fill in the fields below, and we\'ll get a new account set up for you in no time.', 'buddypress' ); ?></p>
+
+			<?php
+
+			/**
+			 * Fires before the display of member registration account details fields.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_before_account_details_fields' ); ?>
+
+			<div class="register-section" id="basic-details-section">
+
+				<?php /***** Basic Account Details ******/ ?>
+
+				<h2><?php _e( 'Account Details', 'buddypress' ); ?></h2>
+
+				<label for="signup_username"><?php _e( 'Username', 'buddypress' ); ?> <?php _e( '(required)', 'buddypress' ); ?></label>
+				<?php
+
+				/**
+				 * Fires and displays any member registration username errors.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_signup_username_errors' ); ?>
+				<input type="text" name="signup_username" id="signup_username" value="<?php bp_signup_username_value(); ?>" <?php bp_form_field_attributes( 'username' ); ?>/>
+
+				<label for="signup_email"><?php _e( 'Email Address', 'buddypress' ); ?> <?php _e( '(required)', 'buddypress' ); ?></label>
+				<?php
+
+				/**
+				 * Fires and displays any member registration email errors.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_signup_email_errors' ); ?>
+				<input type="email" name="signup_email" id="signup_email" value="<?php bp_signup_email_value(); ?>" <?php bp_form_field_attributes( 'email' ); ?>/>
+
+				<label for="signup_password"><?php _e( 'Choose a Password', 'buddypress' ); ?> <?php _e( '(required)', 'buddypress' ); ?></label>
+				<?php
+
+				/**
+				 * Fires and displays any member registration password errors.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_signup_password_errors' ); ?>
+				<input type="password" name="signup_password" id="signup_password" value="" class="password-entry" <?php bp_form_field_attributes( 'password' ); ?>/>
+				<div id="pass-strength-result"></div>
+
+				<label for="signup_password_confirm"><?php _e( 'Confirm Password', 'buddypress' ); ?> <?php _e( '(required)', 'buddypress' ); ?></label>
+				<?php
+
+				/**
+				 * Fires and displays any member registration password confirmation errors.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_signup_password_confirm_errors' ); ?>
+				<input type="password" name="signup_password_confirm" id="signup_password_confirm" value="" class="password-entry-confirm" <?php bp_form_field_attributes( 'password' ); ?>/>
+
+				<?php
+
+				/**
+				 * Fires and displays any extra member registration details fields.
+				 *
+				 * @since 1.9.0
+				 */
+				do_action( 'bp_account_details_fields' ); ?>
+
+			</div><!-- #basic-details-section -->
+
+			<?php
+
+			/**
+			 * Fires after the display of member registration account details fields.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_after_account_details_fields' ); ?>
+
+			<?php /***** Extra Profile Details ******/ ?>
+
+			<?php if ( bp_is_active( 'xprofile' ) ) : ?>
+
+				<?php
+
+				/**
+				 * Fires before the display of member registration xprofile fields.
+				 *
+				 * @since 1.2.4
+				 */
+				do_action( 'bp_before_signup_profile_fields' ); ?>
+
+				<div class="register-section" id="profile-details-section">
+
+					<h2><?php _e( 'Profile Details', 'buddypress' ); ?></h2>
+
+					<?php /* Use the profile field loop to render input fields for the 'base' profile field group */ ?>
+					<?php if ( bp_is_active( 'xprofile' ) ) : if ( bp_has_profile( array( 'profile_group_id' => 1, 'fetch_field_data' => false ) ) ) : while ( bp_profile_groups() ) : bp_the_profile_group(); ?>
+
+					<?php while ( bp_profile_fields() ) : bp_the_profile_field(); ?>
+
+						<div<?php bp_field_css_class( 'editfield' ); ?>>
+							<fieldset>
+
+							<?php
+							$field_type = bp_xprofile_create_field_type( bp_get_the_profile_field_type() );
+							$field_type->edit_field_html();
+
+							/**
+							 * Fires before the display of the visibility options for xprofile fields.
+							 *
+							 * @since 1.7.0
+							 */
+							do_action( 'bp_custom_profile_edit_fields_pre_visibility' );
+
+							if ( bp_current_user_can( 'bp_xprofile_change_field_visibility' ) ) : ?>
+								<p class="field-visibility-settings-toggle" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id() ?>"><span id="<?php bp_the_profile_field_input_name(); ?>-2">
+									<?php
+									printf(
+										__( 'This field can be seen by: %s', 'buddypress' ),
+										'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+									);
+									?>
+									</span>
+									<button type="button" class="visibility-toggle-link" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-2" aria-expanded="false"><?php _ex( 'Change', 'Change profile field visibility level', 'buddypress' ); ?></button>
+								</p>
+
+								<div class="field-visibility-settings" id="field-visibility-settings-<?php bp_the_profile_field_id() ?>">
+									<fieldset>
+										<legend><?php _e( 'Who can see this field?', 'buddypress' ) ?></legend>
+
+										<?php bp_profile_visibility_radio_buttons() ?>
+
+									</fieldset>
+									<button type="button" class="field-visibility-settings-close"><?php _e( 'Close', 'buddypress' ) ?></button>
+
+								</div>
+							<?php else : ?>
+								<p class="field-visibility-settings-notoggle" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id() ?>">
+									<?php
+									printf(
+										__( 'This field can be seen by: %s', 'buddypress' ),
+										'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+									);
+									?>
+								</p>
+							<?php endif ?>
+
+							<?php
+
+							/**
+							 * Fires after the display of the visibility options for xprofile fields.
+							 *
+							 * @since 1.1.0
+							 */
+							do_action( 'bp_custom_profile_edit_fields' ); ?>
+
+							</fieldset>
+						</div>
+
+					<?php endwhile; ?>
+
+					<input type="hidden" name="signup_profile_field_ids" id="signup_profile_field_ids" value="<?php bp_the_profile_field_ids(); ?>" />
+
+					<?php endwhile; endif; endif; ?>
+
+					<?php
+
+					/**
+					 * Fires and displays any extra member registration xprofile fields.
+					 *
+					 * @since 1.9.0
+					 */
+					do_action( 'bp_signup_profile_fields' ); ?>
+
+				</div><!-- #profile-details-section -->
+
+				<?php
+
+				/**
+				 * Fires after the display of member registration xprofile fields.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_signup_profile_fields' ); ?>
+
+			<?php endif; ?>
+
+			<?php if ( bp_get_blog_signup_allowed() ) : ?>
+
+				<?php
+
+				/**
+				 * Fires before the display of member registration blog details fields.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_before_blog_details_fields' ); ?>
+
+				<?php /***** Blog Creation Details ******/ ?>
+
+				<div class="register-section" id="blog-details-section">
+
+					<h2><?php _e( 'Blog Details', 'buddypress' ); ?></h2>
+
+					<p><label for="signup_with_blog"><input type="checkbox" name="signup_with_blog" id="signup_with_blog" value="1"<?php if ( (int) bp_get_signup_with_blog_value() ) : ?> checked="checked"<?php endif; ?> /> <?php _e( 'Yes, I\'d like to create a new site', 'buddypress' ); ?></label></p>
+
+					<div id="blog-details"<?php if ( (int) bp_get_signup_with_blog_value() ) : ?>class="show"<?php endif; ?>>
+
+						<label for="signup_blog_url"><?php _e( 'Blog URL', 'buddypress' ); ?> <?php _e( '(required)', 'buddypress' ); ?></label>
+						<?php
+
+						/**
+						 * Fires and displays any member registration blog URL errors.
+						 *
+						 * @since 1.1.0
+						 */
+						do_action( 'bp_signup_blog_url_errors' ); ?>
+
+						<?php if ( is_subdomain_install() ) : ?>
+							http:// <input type="text" name="signup_blog_url" id="signup_blog_url" value="<?php bp_signup_blog_url_value(); ?>" /> .<?php bp_signup_subdomain_base(); ?>
+						<?php else : ?>
+							<?php echo home_url( '/' ); ?> <input type="text" name="signup_blog_url" id="signup_blog_url" value="<?php bp_signup_blog_url_value(); ?>" />
+						<?php endif; ?>
+
+						<label for="signup_blog_title"><?php _e( 'Site Title', 'buddypress' ); ?> <?php _e( '(required)', 'buddypress' ); ?></label>
+						<?php
+
+						/**
+						 * Fires and displays any member registration blog title errors.
+						 *
+						 * @since 1.1.0
+						 */
+						do_action( 'bp_signup_blog_title_errors' ); ?>
+						<input type="text" name="signup_blog_title" id="signup_blog_title" value="<?php bp_signup_blog_title_value(); ?>" />
+
+						<fieldset class="register-site">
+							<legend class="label"><?php _e( 'Privacy: I would like my site to appear in search engines, and in public listings around this network.', 'buddypress' ); ?></legend>
+							<?php
+
+							/**
+							 * Fires and displays any member registration blog privacy errors.
+							 *
+							 * @since 1.1.0
+							 */
+							do_action( 'bp_signup_blog_privacy_errors' ); ?>
+
+							<label for="signup_blog_privacy_public"><input type="radio" name="signup_blog_privacy" id="signup_blog_privacy_public" value="public"<?php if ( 'public' == bp_get_signup_blog_privacy_value() || !bp_get_signup_blog_privacy_value() ) : ?> checked="checked"<?php endif; ?> /> <?php _e( 'Yes', 'buddypress' ); ?></label>
+							<label for="signup_blog_privacy_private"><input type="radio" name="signup_blog_privacy" id="signup_blog_privacy_private" value="private"<?php if ( 'private' == bp_get_signup_blog_privacy_value() ) : ?> checked="checked"<?php endif; ?> /> <?php _e( 'No', 'buddypress' ); ?></label>
+						</fieldset>
+
+						<?php
+
+						/**
+						 * Fires and displays any extra member registration blog details fields.
+						 *
+						 * @since 1.9.0
+						 */
+						do_action( 'bp_blog_details_fields' ); ?>
+
+					</div>
+
+				</div><!-- #blog-details-section -->
+
+				<?php
+
+				/**
+				 * Fires after the display of member registration blog details fields.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_blog_details_fields' ); ?>
+
+			<?php endif; ?>
+
+			<?php
+
+			/**
+			 * Fires before the display of the registration submit buttons.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_before_registration_submit_buttons' ); ?>
+
+			<div class="submit">
+				<input type="submit" name="signup_submit" id="signup_submit" value="<?php esc_attr_e( 'Complete Sign Up', 'buddypress' ); ?>" />
+			</div>
+
+			<?php
+
+			/**
+			 * Fires after the display of the registration submit buttons.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'bp_after_registration_submit_buttons' ); ?>
+
+			<?php wp_nonce_field( 'bp_new_signup' ); ?>
+
+		<?php endif; // request-details signup step ?>
+
+		<?php if ( 'completed-confirmation' == bp_get_current_signup_step() ) : ?>
+
+			<div id="template-notices" role="alert" aria-atomic="true">
+				<?php
+
+				/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+				do_action( 'template_notices' ); ?>
+
+			</div>
+
+			<?php
+
+			/**
+			 * Fires before the display of the registration confirmed messages.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_before_registration_confirmed' ); ?>
+
+			<div id="template-notices" role="alert" aria-atomic="true">
+				<?php if ( bp_registration_needs_activation() ) : ?>
+					<p><?php _e( 'You have successfully created your account! To begin using this site you will need to activate your account via the email we have just sent to your address.', 'buddypress' ); ?></p>
+				<?php else : ?>
+					<p><?php _e( 'You have successfully created your account! Please log in using the username and password you have just created.', 'buddypress' ); ?></p>
+				<?php endif; ?>
+			</div>
+
+			<?php
+
+			/**
+			 * Fires after the display of the registration confirmed messages.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_after_registration_confirmed' ); ?>
+
+		<?php endif; // completed-confirmation signup step ?>
+
+		<?php
+
+		/**
+		 * Fires and displays any custom signup steps.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_custom_signup_steps' ); ?>
+
+		</form>
+
+	</div>
+
+	<?php
+
+	/**
+	 * Fires at the bottom of the BuddyPress member registration page template.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_register_page' ); ?>
+
+</div><!-- #buddypress -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/activity.php	(revision 12265)
@@ -0,0 +1,78 @@
+<?php
+/**
+ * BuddyPress - Users Activity
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+
+		<?php bp_get_options_nav(); ?>
+
+		<li id="activity-filter-select" class="last">
+			<label for="activity-filter-by"><?php _e( 'Show:', 'buddypress' ); ?></label>
+			<select id="activity-filter-by">
+				<option value="-1"><?php _e( '&mdash; Everything &mdash;', 'buddypress' ); ?></option>
+
+				<?php bp_activity_show_filters(); ?>
+
+				<?php
+
+				/**
+				 * Fires inside the select input for member activity filter options.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_member_activity_filter_options' ); ?>
+
+			</select>
+		</li>
+	</ul>
+</div><!-- .item-list-tabs -->
+
+<?php
+
+/**
+ * Fires before the display of the member activity post form.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_member_activity_post_form' ); ?>
+
+<?php
+if ( is_user_logged_in() && bp_is_my_profile() && ( !bp_current_action() || bp_is_current_action( 'just-me' ) ) )
+	bp_get_template_part( 'activity/post-form' );
+
+/**
+ * Fires after the display of the member activity post form.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_member_activity_post_form' );
+
+/**
+ * Fires before the display of the member activities list.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_member_activity_content' ); ?>
+
+<div class="activity" aria-live="polite" aria-atomic="true" aria-relevant="all">
+
+	<?php bp_get_template_part( 'activity/activity-loop' ) ?>
+
+</div><!-- .activity -->
+
+<?php
+
+/**
+ * Fires after the display of the member activities list.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_member_activity_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/blogs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/blogs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/blogs.php	(revision 12265)
@@ -0,0 +1,72 @@
+<?php
+/**
+ * BuddyPress - Users Blogs
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+
+		<?php bp_get_options_nav(); ?>
+
+		<li id="blogs-order-select" class="last filter">
+
+			<label for="blogs-order-by"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+			<select id="blogs-order-by">
+				<option value="active"><?php _e( 'Last Active', 'buddypress' ); ?></option>
+				<option value="newest"><?php _e( 'Newest', 'buddypress' ); ?></option>
+				<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+
+				<?php
+
+				/**
+				 * Fires inside the members blogs order options select input.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_member_blog_order_options' ); ?>
+
+			</select>
+		</li>
+	</ul>
+</div><!-- .item-list-tabs -->
+
+<?php
+switch ( bp_current_action() ) :
+
+	// Home/My Blogs
+	case 'my-sites' :
+
+		/**
+		 * Fires before the display of member blogs content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_blogs_content' ); ?>
+
+		<div class="blogs myblogs">
+
+			<?php bp_get_template_part( 'blogs/blogs-loop' ) ?>
+
+		</div><!-- .blogs.myblogs -->
+
+		<?php
+
+		/**
+		 * Fires after the display of member blogs content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_blogs_content' );
+		break;
+
+	// Any other
+	default :
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/cover-image-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/cover-image-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/cover-image-header.php	(revision 12265)
@@ -0,0 +1,107 @@
+<?php
+/**
+ * BuddyPress - Users Cover Image Header
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php
+
+/**
+ * Fires before the display of a member's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_member_header' ); ?>
+
+<div id="cover-image-container">
+	<a id="header-cover-image" href="<?php bp_displayed_user_link(); ?>"></a>
+
+	<div id="item-header-cover-image">
+		<div id="item-header-avatar">
+			<a href="<?php bp_displayed_user_link(); ?>">
+
+				<?php bp_displayed_user_avatar( 'type=full' ); ?>
+
+			</a>
+		</div><!-- #item-header-avatar -->
+
+		<div id="item-header-content">
+
+			<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+				<h2 class="user-nicename">@<?php bp_displayed_user_mentionname(); ?></h2>
+			<?php endif; ?>
+
+			<div id="item-buttons"><?php
+
+				/**
+				 * Fires in the member header actions section.
+				 *
+				 * @since 1.2.6
+				 */
+				do_action( 'bp_member_header_actions' ); ?></div><!-- #item-buttons -->
+
+			<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_user_last_activity( bp_displayed_user_id() ) ); ?>"><?php bp_last_activity( bp_displayed_user_id() ); ?></span>
+
+			<?php
+
+			/**
+			 * Fires before the display of the member's header meta.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_before_member_header_meta' ); ?>
+
+			<div id="item-meta">
+
+				<?php if ( bp_is_active( 'activity' ) ) : ?>
+
+					<div id="latest-update">
+
+						<?php bp_activity_latest_update( bp_displayed_user_id() ); ?>
+
+					</div>
+
+				<?php endif; ?>
+
+				<?php
+
+				 /**
+				  * Fires after the group header actions section.
+				  *
+				  * If you'd like to show specific profile fields here use:
+				  * bp_member_profile_data( 'field=About Me' ); -- Pass the name of the field
+				  *
+				  * @since 1.2.0
+				  */
+				 do_action( 'bp_profile_header_meta' );
+
+				 ?>
+
+			</div><!-- #item-meta -->
+
+		</div><!-- #item-header-content -->
+
+	</div><!-- #item-header-cover-image -->
+</div><!-- #cover-image-container -->
+
+<?php
+
+/**
+ * Fires after the display of a member's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_member_header' ); ?>
+
+<div id="template-notices" role="alert" aria-atomic="true">
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+	do_action( 'template_notices' ); ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/friends.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/friends.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/friends.php	(revision 12265)
@@ -0,0 +1,92 @@
+<?php
+/**
+ * BuddyPress - Users Friends
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<?php if ( bp_is_my_profile() ) bp_get_options_nav(); ?>
+
+		<?php if ( !bp_is_current_action( 'requests' ) ) : ?>
+
+			<li id="members-order-select" class="last filter">
+
+				<label for="members-friends"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+				<select id="members-friends">
+					<option value="active"><?php _e( 'Last Active', 'buddypress' ); ?></option>
+					<option value="newest"><?php _e( 'Newest Registered', 'buddypress' ); ?></option>
+					<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+
+					<?php
+
+					/**
+					 * Fires inside the members friends order options select input.
+					 *
+					 * @since 2.0.0
+					 */
+					do_action( 'bp_member_friends_order_options' ); ?>
+
+				</select>
+			</li>
+
+		<?php endif; ?>
+
+	</ul>
+</div>
+
+<?php
+switch ( bp_current_action() ) :
+
+	// Home/My Friends
+	case 'my-friends' :
+
+		/**
+		 * Fires before the display of member friends content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_friends_content' ); ?>
+
+		<?php if (is_user_logged_in() ) : ?>
+			<h2 class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'My friends', 'buddypress' );
+			?></h2>
+		<?php else : ?>
+			<h2 class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'Friends', 'buddypress' );
+			?></h2>
+		<?php endif ?>
+
+		<div class="members friends">
+
+			<?php bp_get_template_part( 'members/members-loop' ) ?>
+
+		</div><!-- .members.friends -->
+
+		<?php
+
+		/**
+		 * Fires after the display of member friends content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_friends_content' );
+		break;
+
+	case 'requests' :
+		bp_get_template_part( 'members/single/friends/requests' );
+		break;
+
+	// Any other
+	default :
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/friends/requests.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/friends/requests.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/friends/requests.php	(revision 12265)
@@ -0,0 +1,120 @@
+<?php
+/**
+ * BuddyPress - Members Friends Requests
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of member friend requests content.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_member_friend_requests_content' ); ?>
+
+<?php if ( bp_has_members( 'type=alphabetical&include=' . bp_get_friendship_requests() ) ) : ?>
+
+	<h2 class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Friendship requests', 'buddypress' );
+	?></h2>
+
+	<div id="pag-top" class="pagination no-ajax">
+
+		<div class="pag-count" id="member-dir-count-top">
+
+			<?php bp_members_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="member-dir-pag-top">
+
+			<?php bp_members_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+	<ul id="friend-list" class="item-list">
+		<?php while ( bp_members() ) : bp_the_member(); ?>
+
+			<li id="friendship-<?php bp_friend_friendship_id(); ?>">
+				<div class="item-avatar">
+					<a href="<?php bp_member_link(); ?>"><?php bp_member_avatar(); ?></a>
+				</div>
+
+				<div class="item">
+					<div class="item-title"><a href="<?php bp_member_link(); ?>"><?php bp_member_name(); ?></a></div>
+					<div class="item-meta"><span class="activity"><?php bp_member_last_active(); ?></span></div>
+
+					<?php
+					/**
+					 * Fires inside the display of a member friend request item.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_friend_requests_item' );
+					?>
+				</div>
+
+				<div class="action">
+					<a class="button accept" href="<?php bp_friend_accept_request_link(); ?>"><?php _e( 'Accept', 'buddypress' ); ?></a> &nbsp;
+					<a class="button reject" href="<?php bp_friend_reject_request_link(); ?>"><?php _e( 'Reject', 'buddypress' ); ?></a>
+
+					<?php
+
+					/**
+					 * Fires inside the member friend request actions markup.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_friend_requests_item_action' ); ?>
+				</div>
+			</li>
+
+		<?php endwhile; ?>
+	</ul>
+
+	<?php
+
+	/**
+	 * Fires and displays the member friend requests content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_friend_requests_content' ); ?>
+
+	<div id="pag-bottom" class="pagination no-ajax">
+
+		<div class="pag-count" id="member-dir-count-bottom">
+
+			<?php bp_members_pagination_count(); ?>
+
+		</div>
+
+		<div class="pagination-links" id="member-dir-pag-bottom">
+
+			<?php bp_members_pagination_links(); ?>
+
+		</div>
+
+	</div>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'You have no pending friendship requests.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif;?>
+
+<?php
+
+/**
+ * Fires after the display of member friend requests content.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_member_friend_requests_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/groups.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/groups.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/groups.php	(revision 12265)
@@ -0,0 +1,95 @@
+<?php
+/**
+ * BuddyPress - Users Groups
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<?php if ( bp_is_my_profile() ) bp_get_options_nav(); ?>
+
+		<?php if ( !bp_is_current_action( 'invites' ) ) : ?>
+
+			<li id="groups-order-select" class="last filter">
+
+				<label for="groups-order-by"><?php _e( 'Order By:', 'buddypress' ); ?></label>
+				<select id="groups-order-by">
+					<option value="active"><?php _e( 'Last Active', 'buddypress' ); ?></option>
+					<option value="popular"><?php _e( 'Most Members', 'buddypress' ); ?></option>
+					<option value="newest"><?php _e( 'Newly Created', 'buddypress' ); ?></option>
+					<option value="alphabetical"><?php _e( 'Alphabetical', 'buddypress' ); ?></option>
+
+					<?php
+
+					/**
+					 * Fires inside the members group order options select input.
+					 *
+					 * @since 1.2.0
+					 */
+					do_action( 'bp_member_group_order_options' ); ?>
+
+				</select>
+			</li>
+
+		<?php endif; ?>
+
+	</ul>
+</div><!-- .item-list-tabs -->
+
+<?php
+
+switch ( bp_current_action() ) :
+
+	// Home/My Groups
+	case 'my-groups' :
+
+		/**
+		 * Fires before the display of member groups content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_groups_content' ); ?>
+
+		<?php if ( is_user_logged_in() ) : ?>
+			<h2 class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'My groups', 'buddypress' );
+			?></h2>
+		<?php else : ?>
+			<h2 class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'Member\'s groups', 'buddypress' );
+			?></h2>
+		<?php endif; ?>
+
+		<div class="groups mygroups">
+
+			<?php bp_get_template_part( 'groups/groups-loop' ); ?>
+
+		</div>
+
+		<?php
+
+		/**
+		 * Fires after the display of member groups content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_groups_content' );
+		break;
+
+	// Group Invitations
+	case 'invites' :
+		bp_get_template_part( 'members/single/groups/invites' );
+		break;
+
+	// Any other
+	default :
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/groups/invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/groups/invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/groups/invites.php	(revision 12265)
@@ -0,0 +1,84 @@
+<?php
+/**
+ * BuddyPress - Members Single Group Invites
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of member group invites content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_group_invites_content' ); ?>
+
+<?php if ( bp_has_groups( 'type=invites&user_id=' . bp_loggedin_user_id() ) ) : ?>
+
+	<h2 class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Group invitations', 'buddypress' );
+	?></h2>
+
+	<ul id="group-list" class="invites item-list">
+
+		<?php while ( bp_groups() ) : bp_the_group(); ?>
+
+			<li>
+				<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+					<div class="item-avatar">
+						<a href="<?php bp_group_permalink(); ?>"><?php bp_group_avatar( 'type=thumb&width=50&height=50' ); ?></a>
+					</div>
+				<?php endif; ?>
+
+				<h4><?php bp_group_link(); ?><span class="small"> - <?php printf( _nx( '%d member', '%d members', bp_get_group_total_members( false ),'Group member count', 'buddypress' ), bp_get_group_total_members( false )  ); ?></span></h4>
+
+				<p class="desc">
+					<?php bp_group_description_excerpt(); ?>
+				</p>
+
+				<?php
+
+				/**
+				 * Fires inside the display of a member group invite item.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_group_invites_item' ); ?>
+
+				<div class="action">
+					<a class="button accept" href="<?php bp_group_accept_invite_link(); ?>"><?php _e( 'Accept', 'buddypress' ); ?></a> &nbsp;
+					<a class="button reject confirm" href="<?php bp_group_reject_invite_link(); ?>"><?php _e( 'Reject', 'buddypress' ); ?></a>
+
+					<?php
+
+					/**
+					 * Fires inside the member group item action markup.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_group_invites_item_action' ); ?>
+
+				</div>
+			</li>
+
+		<?php endwhile; ?>
+	</ul>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'You have no outstanding group invites.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif;?>
+
+<?php
+
+/**
+ * Fires after the display of member group invites content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_group_invites_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/home.php	(revision 12265)
@@ -0,0 +1,119 @@
+<?php
+/**
+ * BuddyPress - Members Home
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div id="buddypress">
+
+	<?php
+
+	/**
+	 * Fires before the display of member home content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_home_content' ); ?>
+
+	<div id="item-header" role="complementary">
+
+		<?php
+		/**
+		 * If the cover image feature is enabled, use a specific header
+		 */
+		if ( bp_displayed_user_use_cover_image_header() ) :
+			bp_get_template_part( 'members/single/cover-image-header' );
+		else :
+			bp_get_template_part( 'members/single/member-header' );
+		endif;
+		?>
+
+	</div><!-- #item-header -->
+
+	<div id="item-nav">
+		<div class="item-list-tabs no-ajax" id="object-nav" aria-label="<?php esc_attr_e( 'Member primary navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+
+				<?php bp_get_displayed_user_nav(); ?>
+
+				<?php
+
+				/**
+				 * Fires after the display of member options navigation.
+				 *
+				 * @since 1.2.4
+				 */
+				do_action( 'bp_member_options_nav' ); ?>
+
+			</ul>
+		</div>
+	</div><!-- #item-nav -->
+
+	<div id="item-body">
+
+		<?php
+
+		/**
+		 * Fires before the display of member body content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_body' );
+
+		if ( bp_is_user_front() ) :
+			bp_displayed_user_front_template_part();
+
+		elseif ( bp_is_user_activity() ) :
+			bp_get_template_part( 'members/single/activity' );
+
+		elseif ( bp_is_user_blogs() ) :
+			bp_get_template_part( 'members/single/blogs'    );
+
+		elseif ( bp_is_user_friends() ) :
+			bp_get_template_part( 'members/single/friends'  );
+
+		elseif ( bp_is_user_groups() ) :
+			bp_get_template_part( 'members/single/groups'   );
+
+		elseif ( bp_is_user_messages() ) :
+			bp_get_template_part( 'members/single/messages' );
+
+		elseif ( bp_is_user_profile() ) :
+			bp_get_template_part( 'members/single/profile'  );
+
+		elseif ( bp_is_user_notifications() ) :
+			bp_get_template_part( 'members/single/notifications' );
+
+		elseif ( bp_is_user_settings() ) :
+			bp_get_template_part( 'members/single/settings' );
+
+		// If nothing sticks, load a generic template
+		else :
+			bp_get_template_part( 'members/single/plugins'  );
+
+		endif;
+
+		/**
+		 * Fires after the display of member body content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_body' ); ?>
+
+	</div><!-- #item-body -->
+
+	<?php
+
+	/**
+	 * Fires after the display of member home content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_home_content' ); ?>
+
+</div><!-- #buddypress -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/member-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/member-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/member-header.php	(revision 12265)
@@ -0,0 +1,104 @@
+<?php
+/**
+ * BuddyPress - Users Header
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php
+
+/**
+ * Fires before the display of a member's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_member_header' ); ?>
+
+<div id="item-header-avatar">
+	<a href="<?php bp_displayed_user_link(); ?>">
+
+		<?php bp_displayed_user_avatar( 'type=full' ); ?>
+
+	</a>
+</div><!-- #item-header-avatar -->
+
+<div id="item-header-content">
+
+	<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+		<h2 class="user-nicename">@<?php bp_displayed_user_mentionname(); ?></h2>
+	<?php endif; ?>
+
+	<span class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_user_last_activity( bp_displayed_user_id() ) ); ?>"><?php bp_last_activity( bp_displayed_user_id() ); ?></span>
+
+	<?php
+
+	/**
+	 * Fires before the display of the member's header meta.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_header_meta' ); ?>
+
+	<div id="item-meta">
+
+		<?php if ( bp_is_active( 'activity' ) ) : ?>
+
+			<div id="latest-update">
+
+				<?php bp_activity_latest_update( bp_displayed_user_id() ); ?>
+
+			</div>
+
+		<?php endif; ?>
+
+		<div id="item-buttons">
+
+			<?php
+
+			/**
+			 * Fires in the member header actions section.
+			 *
+			 * @since 1.2.6
+			 */
+			do_action( 'bp_member_header_actions' ); ?>
+
+		</div><!-- #item-buttons -->
+
+		<?php
+
+		 /**
+		  * Fires after the group header actions section.
+		  *
+		  * If you'd like to show specific profile fields here use:
+		  * bp_member_profile_data( 'field=About Me' ); -- Pass the name of the field
+		  *
+		  * @since 1.2.0
+		  */
+		 do_action( 'bp_profile_header_meta' );
+
+		 ?>
+
+	</div><!-- #item-meta -->
+
+</div><!-- #item-header-content -->
+
+<?php
+
+/**
+ * Fires after the display of a member's header.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_member_header' ); ?>
+
+<div id="template-notices" role="alert" aria-atomic="true">
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/activity/index.php */
+	do_action( 'template_notices' ); ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages.php	(revision 12265)
@@ -0,0 +1,110 @@
+<?php
+/**
+ * BuddyPress - Users Messages
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+
+		<?php bp_get_options_nav(); ?>
+
+	</ul>
+
+	<?php if ( bp_is_messages_inbox() || bp_is_messages_sentbox() ) : ?>
+
+		<div class="message-search"><?php bp_message_search_form(); ?></div>
+
+	<?php endif; ?>
+
+</div><!-- .item-list-tabs -->
+
+<?php
+switch ( bp_current_action() ) :
+
+	// Inbox/Sentbox
+	case 'inbox'   :
+	case 'sentbox' :
+
+		/**
+		 * Fires before the member messages content for inbox and sentbox.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_messages_content' ); ?>
+
+		<?php if ( bp_is_messages_inbox() ) : ?>
+			<h2 class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'Messages inbox', 'buddypress' );
+			?></h2>
+		<?php elseif ( bp_is_messages_sentbox() ) : ?>
+			<h2 class="bp-screen-reader-text"><?php
+				/* translators: accessibility text */
+				_e( 'Sent Messages', 'buddypress' );
+			?></h2>
+		<?php endif; ?>
+
+		<div class="messages">
+			<?php bp_get_template_part( 'members/single/messages/messages-loop' ); ?>
+		</div><!-- .messages -->
+
+		<?php
+
+		/**
+		 * Fires after the member messages content for inbox and sentbox.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_messages_content' );
+		break;
+
+	// Single Message View
+	case 'view' :
+		bp_get_template_part( 'members/single/messages/single' );
+		break;
+
+	// Compose
+	case 'compose' :
+		bp_get_template_part( 'members/single/messages/compose' );
+		break;
+
+	// Sitewide Notices
+	case 'notices' :
+
+		/**
+		 * Fires before the member messages content for notices.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_messages_content' ); ?>
+
+		<h2 class="bp-screen-reader-text"><?php
+			/* translators: accessibility text */
+			_e( 'Sitewide Notices', 'buddypress' );
+		?></h2>
+
+		<div class="messages">
+			<?php bp_get_template_part( 'members/single/messages/notices-loop' ); ?>
+		</div><!-- .messages -->
+
+		<?php
+
+		/**
+		 * Fires after the member messages content for inbox and sentbox.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_messages_content' );
+		break;
+
+	// Any other
+	default :
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/compose.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/compose.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/compose.php	(revision 12265)
@@ -0,0 +1,66 @@
+<?php
+/**
+ * BuddyPress - Members Single Messages Compose
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<h2 class="bp-screen-reader-text"><?php
+	/* translators: accessibility text */
+	_e( 'Compose Message', 'buddypress' );
+?></h2>
+
+<form action="<?php bp_messages_form_action('compose' ); ?>" method="post" id="send_message_form" class="standard-form" enctype="multipart/form-data">
+
+	<?php
+
+	/**
+	 * Fires before the display of message compose content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_messages_compose_content' ); ?>
+
+	<label for="send-to-input"><?php _e("Send To (Username or Friend's Name)", 'buddypress' ); ?></label>
+	<ul class="first acfb-holder">
+		<li>
+			<?php bp_message_get_recipient_tabs(); ?>
+			<input type="text" name="send-to-input" class="send-to-input" id="send-to-input" />
+		</li>
+	</ul>
+
+	<?php if ( bp_current_user_can( 'bp_moderate' ) ) : ?>
+		<p><label for="send-notice"><input type="checkbox" id="send-notice" name="send-notice" value="1" /> <?php _e( "This is a notice to all users.", 'buddypress' ); ?></label></p>
+	<?php endif; ?>
+
+	<label for="subject"><?php _e( 'Subject', 'buddypress' ); ?></label>
+	<input type="text" name="subject" id="subject" value="<?php bp_messages_subject_value(); ?>" />
+
+	<label for="message_content"><?php _e( 'Message', 'buddypress' ); ?></label>
+	<textarea name="content" id="message_content" rows="15" cols="40"><?php bp_messages_content_value(); ?></textarea>
+
+	<input type="hidden" name="send_to_usernames" id="send-to-usernames" value="<?php bp_message_get_recipient_usernames(); ?>" class="<?php bp_message_get_recipient_usernames(); ?>" />
+
+	<?php
+
+	/**
+	 * Fires after the display of message compose content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_messages_compose_content' ); ?>
+
+	<div class="submit">
+		<input type="submit" value="<?php esc_attr_e( "Send Message", 'buddypress' ); ?>" name="send" id="send" />
+	</div>
+
+	<?php wp_nonce_field( 'messages_send_message' ); ?>
+</form>
+
+<script type="text/javascript">
+	document.getElementById("send-to-input").focus();
+</script>
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/message.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/message.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/message.php	(revision 12265)
@@ -0,0 +1,87 @@
+<?php
+/**
+ * BuddyPress - Private Message Content.
+ *
+ * This template is used in /messages/single.php during the message loop to
+ * display each message and when a new message is created via AJAX.
+ *
+ * @since 2.4.0
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+			<div class="message-box <?php bp_the_thread_message_css_class(); ?>">
+
+				<div class="message-metadata">
+
+					<?php
+
+					/**
+					 * Fires before the single message header is displayed.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_before_message_meta' ); ?>
+
+					<?php bp_the_thread_message_sender_avatar( 'type=thumb&width=30&height=30' ); ?>
+
+					<?php if ( bp_get_the_thread_message_sender_link() ) : ?>
+
+						<strong><a href="<?php bp_the_thread_message_sender_link(); ?>"><?php bp_the_thread_message_sender_name(); ?></a></strong>
+
+					<?php else : ?>
+
+						<strong><?php bp_the_thread_message_sender_name(); ?></strong>
+
+					<?php endif; ?>
+
+					<span class="activity"><?php bp_the_thread_message_time_since(); ?></span>
+
+					<?php if ( bp_is_active( 'messages', 'star' ) ) : ?>
+						<div class="message-star-actions">
+							<?php bp_the_message_star_action_link(); ?>
+						</div>
+					<?php endif; ?>
+
+					<?php
+
+					/**
+					 * Fires after the single message header is displayed.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_after_message_meta' ); ?>
+
+				</div><!-- .message-metadata -->
+
+				<?php
+
+				/**
+				 * Fires before the message content for a private message.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_before_message_content' ); ?>
+
+				<div class="message-content">
+
+					<?php bp_the_thread_message_content(); ?>
+
+				</div><!-- .message-content -->
+
+				<?php
+
+				/**
+				 * Fires after the message content for a private message.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_after_message_content' ); ?>
+
+				<div class="clear"></div>
+
+			</div><!-- .message-box -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/messages-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/messages-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/messages-loop.php	(revision 12265)
@@ -0,0 +1,207 @@
+<?php
+/**
+ * BuddyPress - Members Messages Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the members messages loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_member_messages_loop' ); ?>
+
+<?php if ( bp_has_message_threads( bp_ajax_querystring( 'messages' ) ) ) : ?>
+
+	<h2 class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Starred messages', 'buddypress' );
+	?></h2>
+
+	<div class="pagination no-ajax" id="user-pag">
+
+		<div class="pag-count" id="messages-dir-count">
+			<?php bp_messages_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="messages-dir-pag">
+			<?php bp_messages_pagination(); ?>
+		</div>
+
+	</div><!-- .pagination -->
+
+	<?php
+
+	/**
+	 * Fires after the members messages pagination display.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_messages_pagination' ); ?>
+
+	<?php
+
+	/**
+	 * Fires before the members messages threads.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_messages_threads' ); ?>
+
+	<form action="<?php echo bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() ?>/bulk-manage/" method="post" id="messages-bulk-management">
+
+		<table id="message-threads" class="messages-notices">
+
+			<thead>
+				<tr>
+					<th scope="col" class="thread-checkbox bulk-select-all"><input id="select-all-messages" type="checkbox"><label class="bp-screen-reader-text" for="select-all-messages"><?php
+						/* translators: accessibility text */
+						_e( 'Select all', 'buddypress' );
+					?></label></th>
+					<th scope="col" class="thread-from"><?php _e( 'From', 'buddypress' ); ?></th>
+					<th scope="col" class="thread-info"><?php _e( 'Subject', 'buddypress' ); ?></th>
+
+					<?php
+
+					/**
+					 * Fires inside the messages box table header to add a new column.
+					 *
+					 * This is to primarily add a <th> cell to the messages box table header. Use
+					 * the related 'bp_messages_inbox_list_item' hook to add a <td> cell.
+					 *
+					 * @since 2.3.0
+					 */
+					do_action( 'bp_messages_inbox_list_header' ); ?>
+
+					<?php if ( bp_is_active( 'messages', 'star' ) ) : ?>
+						<th scope="col" class="thread-star"><span class="message-action-star"><span class="icon"></span> <span class="screen-reader-text"><?php
+							/* translators: accessibility text */
+							_e( 'Star', 'buddypress' );
+						?></span></span></th>
+					<?php endif; ?>
+
+					<th scope="col" class="thread-options"><?php _e( 'Actions', 'buddypress' ); ?></th>
+				</tr>
+			</thead>
+
+			<tbody>
+
+				<?php while ( bp_message_threads() ) : bp_message_thread(); ?>
+
+					<tr id="m-<?php bp_message_thread_id(); ?>" class="<?php bp_message_css_class(); ?><?php if ( bp_message_thread_has_unread() ) : ?> unread<?php else: ?> read<?php endif; ?>">
+						<td class="bulk-select-check">
+							<label for="bp-message-thread-<?php bp_message_thread_id(); ?>"><input type="checkbox" name="message_ids[]" id="bp-message-thread-<?php bp_message_thread_id(); ?>" class="message-check" value="<?php bp_message_thread_id(); ?>" /><span class="bp-screen-reader-text"><?php
+								/* translators: accessibility text */
+								_e( 'Select this message', 'buddypress' );
+							?></span></label>
+						</td>
+
+						<?php if ( 'sentbox' != bp_current_action() ) : ?>
+							<td class="thread-from">
+								<?php bp_message_thread_avatar( array( 'width' => 25, 'height' => 25 ) ); ?>
+								<span class="from"><?php _e( 'From:', 'buddypress' ); ?></span> <?php bp_message_thread_from(); ?>
+								<?php bp_message_thread_total_and_unread_count(); ?>
+								<span class="activity"><?php bp_message_thread_last_post_date(); ?></span>
+							</td>
+						<?php else: ?>
+							<td class="thread-from">
+								<?php bp_message_thread_avatar( array( 'width' => 25, 'height' => 25 ) ); ?>
+								<span class="to"><?php _e( 'To:', 'buddypress' ); ?></span> <?php bp_message_thread_to(); ?>
+								<?php bp_message_thread_total_and_unread_count(); ?>
+								<span class="activity"><?php bp_message_thread_last_post_date(); ?></span>
+							</td>
+						<?php endif; ?>
+
+						<td class="thread-info">
+							<p><a href="<?php bp_message_thread_view_link( bp_get_message_thread_id(), bp_displayed_user_id() ); ?>" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( "View Message", 'buddypress' ); ?>" aria-label="<?php esc_attr_e( "View Message", 'buddypress' ); ?>"><?php bp_message_thread_subject(); ?></a></p>
+							<p class="thread-excerpt"><?php bp_message_thread_excerpt(); ?></p>
+						</td>
+
+						<?php
+
+						/**
+						 * Fires inside the messages box table row to add a new column.
+						 *
+						 * This is to primarily add a <td> cell to the message box table. Use the
+						 * related 'bp_messages_inbox_list_header' hook to add a <th> header cell.
+						 *
+						 * @since 1.1.0
+						 */
+						do_action( 'bp_messages_inbox_list_item' ); ?>
+
+						<?php if ( bp_is_active( 'messages', 'star' ) ) : ?>
+							<td class="thread-star">
+								<?php bp_the_message_star_action_link( array( 'thread_id' => bp_get_message_thread_id() ) ); ?>
+							</td>
+						<?php endif; ?>
+
+						<td class="thread-options">
+							<?php if ( bp_message_thread_has_unread() ) : ?>
+								<a class="read" href="<?php bp_the_message_thread_mark_read_url( bp_displayed_user_id() );?>"><?php _e( 'Read', 'buddypress' ); ?></a>
+							<?php else : ?>
+								<a class="unread" href="<?php bp_the_message_thread_mark_unread_url( bp_displayed_user_id() );?>"><?php _e( 'Unread', 'buddypress' ); ?></a>
+							<?php endif; ?>
+							 |
+							<a class="delete" href="<?php bp_message_thread_delete_link( bp_displayed_user_id() ); ?>"><?php _e( 'Delete', 'buddypress' ); ?></a>
+
+							<?php
+
+							/**
+							 * Fires after the thread options links for each message in the messages loop list.
+							 *
+							 * @since 2.5.0
+							 */
+							do_action( 'bp_messages_thread_options' ); ?>
+						</td>
+					</tr>
+
+				<?php endwhile; ?>
+
+			</tbody>
+
+		</table><!-- #message-threads -->
+
+		<div class="messages-options-nav">
+			<?php bp_messages_bulk_management_dropdown(); ?>
+		</div><!-- .messages-options-nav -->
+
+		<?php wp_nonce_field( 'messages_bulk_nonce', 'messages_bulk_nonce' ); ?>
+	</form>
+
+	<?php
+
+	/**
+	 * Fires after the members messages threads.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_messages_threads' ); ?>
+
+	<?php
+
+	/**
+	 * Fires and displays member messages options.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_messages_options' ); ?>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'Sorry, no messages were found.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif;?>
+
+<?php
+
+/**
+ * Fires after the members messages loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_member_messages_loop' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/notices-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/notices-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/notices-loop.php	(revision 12265)
@@ -0,0 +1,108 @@
+<?php
+/**
+ * BuddyPress - Members Single Messages Notice Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the members notices loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_notices_loop' ); ?>
+
+<?php if ( bp_has_message_threads() ) : ?>
+
+	<div class="pagination no-ajax" id="user-pag">
+
+		<div class="pag-count" id="messages-dir-count">
+			<?php bp_messages_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="messages-dir-pag">
+			<?php bp_messages_pagination(); ?>
+		</div>
+
+	</div><!-- .pagination -->
+
+	<?php
+
+	/**
+	 * Fires after the members notices pagination display.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_notices_pagination' ); ?>
+	<?php
+
+	/**
+	 * Fires before the members notice items.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_notices' ); ?>
+
+	<table id="message-threads" class="messages-notices sitewide-notices">
+		<?php while ( bp_message_threads() ) : bp_message_thread(); ?>
+			<tr id="notice-<?php bp_message_notice_id(); ?>" class="<?php bp_message_css_class(); ?>">
+				<td width="1%"></td>
+				<td width="38%">
+					<strong><?php bp_message_notice_subject(); ?></strong>
+					<?php bp_message_notice_text(); ?>
+				</td>
+				<td width="21%">
+
+					<?php if ( bp_messages_is_active_notice() ) : ?>
+
+						<strong><?php bp_messages_is_active_notice(); ?></strong>
+
+					<?php endif; ?>
+
+					<span class="activity"><?php _e( 'Sent:', 'buddypress' ); ?> <?php bp_message_notice_post_date(); ?></span>
+				</td>
+
+				<?php
+
+				/**
+				 * Fires inside the display of a member notice list item.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_notices_list_item' ); ?>
+
+				<td width="10%">
+					<a class="button" href="<?php bp_message_activate_deactivate_link(); ?>" class="confirm"><?php bp_message_activate_deactivate_text(); ?></a>
+					<a class="button" href="<?php bp_message_notice_delete_link(); ?>" class="confirm"><?php esc_html_e( "Delete Message", 'buddypress' ); ?></a>
+				</td>
+			</tr>
+		<?php endwhile; ?>
+	</table><!-- #message-threads -->
+
+	<?php
+
+	/**
+	 * Fires after the members notice items.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_notices' ); ?>
+
+<?php else: ?>
+
+	<div id="message" class="info">
+		<p><?php _e( 'Sorry, no notices were found.', 'buddypress' ); ?></p>
+	</div>
+
+<?php endif;?>
+
+<?php
+
+/**
+ * Fires after the members notices loop.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_notices_loop' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/single.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/single.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/messages/single.php	(revision 12265)
@@ -0,0 +1,172 @@
+<?php
+/**
+ * BuddyPress - Members Single Message
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div id="message-thread">
+
+	<?php
+
+	/**
+	 * Fires before the display of a single member message thread content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_message_thread_content' ); ?>
+
+	<?php if ( bp_thread_has_messages() ) : ?>
+
+		<h2 id="message-subject"><?php bp_the_thread_subject(); ?></h2>
+
+		<p id="message-recipients">
+			<span class="highlight">
+
+				<?php if ( bp_get_thread_recipients_count() <= 1 ) : ?>
+
+					<?php _e( 'You are alone in this conversation.', 'buddypress' ); ?>
+
+				<?php elseif ( bp_get_max_thread_recipients_to_list() <= bp_get_thread_recipients_count() ) : ?>
+
+					<?php printf( __( 'Conversation between %s recipients.', 'buddypress' ), number_format_i18n( bp_get_thread_recipients_count() ) ); ?>
+
+				<?php else : ?>
+
+					<?php printf( __( 'Conversation between %s.', 'buddypress' ), bp_get_thread_recipients_list() ); ?>
+
+				<?php endif; ?>
+
+			</span>
+
+			<a class="button confirm" href="<?php bp_the_thread_delete_link(); ?>"><?php _e( 'Delete', 'buddypress' ); ?></a>
+
+			<?php
+
+			/**
+			 * Fires after the action links in the header of a single message thread.
+			 *
+			 * @since 2.5.0
+			 */
+			do_action( 'bp_after_message_thread_recipients' ); ?>
+		</p>
+
+		<?php
+
+		/**
+		 * Fires before the display of the message thread list.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_message_thread_list' ); ?>
+
+		<?php while ( bp_thread_messages() ) : bp_thread_the_message(); ?>
+			<?php bp_get_template_part( 'members/single/messages/message' ); ?>
+		<?php endwhile; ?>
+
+		<?php
+
+		/**
+		 * Fires after the display of the message thread list.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_message_thread_list' ); ?>
+
+		<?php
+
+		/**
+		 * Fires before the display of the message thread reply form.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_message_thread_reply' ); ?>
+
+		<form id="send-reply" action="<?php bp_messages_form_action(); ?>" method="post" class="standard-form">
+
+			<div class="message-box">
+
+				<div class="message-metadata">
+
+					<?php
+
+					/** This action is documented in bp-templates/bp-legacy/buddypress-functions.php */
+					do_action( 'bp_before_message_meta' ); ?>
+
+					<div class="avatar-box">
+						<?php bp_loggedin_user_avatar( 'type=thumb&height=30&width=30' ); ?>
+
+						<strong><?php _e( 'Send a Reply', 'buddypress' ); ?></strong>
+					</div>
+
+					<?php
+
+					/** This action is documented in bp-templates/bp-legacy/buddypress-functions.php */
+					do_action( 'bp_after_message_meta' ); ?>
+
+				</div><!-- .message-metadata -->
+
+				<div class="message-content">
+
+					<?php
+
+					/**
+					 * Fires before the display of the message reply box.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_before_message_reply_box' ); ?>
+
+					<label for="message_content" class="bp-screen-reader-text"><?php
+						/* translators: accessibility text */
+						_e( 'Reply to Message', 'buddypress' );
+					?></label>
+					<textarea name="content" id="message_content" rows="15" cols="40"></textarea>
+
+					<?php
+
+					/**
+					 * Fires after the display of the message reply box.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_after_message_reply_box' ); ?>
+
+					<div class="submit">
+						<input type="submit" name="send" value="<?php esc_attr_e( 'Send Reply', 'buddypress' ); ?>" id="send_reply_button"/>
+					</div>
+
+					<input type="hidden" id="thread_id" name="thread_id" value="<?php bp_the_thread_id(); ?>" />
+					<input type="hidden" id="messages_order" name="messages_order" value="<?php bp_thread_messages_order(); ?>" />
+					<?php wp_nonce_field( 'messages_send_message', 'send_message_nonce' ); ?>
+
+				</div><!-- .message-content -->
+
+			</div><!-- .message-box -->
+
+		</form><!-- #send-reply -->
+
+		<?php
+
+		/**
+		 * Fires after the display of the message thread reply form.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_message_thread_reply' ); ?>
+
+	<?php endif; ?>
+
+	<?php
+
+	/**
+	 * Fires after the display of a single member message thread content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_message_thread_content' ); ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress - Users Notifications
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<?php bp_get_options_nav(); ?>
+
+		<li id="members-order-select" class="last filter">
+			<?php bp_notifications_sort_order_form(); ?>
+		</li>
+	</ul>
+</div>
+
+<?php
+switch ( bp_current_action() ) :
+
+	case 'unread' :
+		bp_get_template_part( 'members/single/notifications/unread' );
+		break;
+
+	case 'read' :
+		bp_get_template_part( 'members/single/notifications/read' );
+		break;
+
+	// Any other actions.
+	default :
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/feedback-no-notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/feedback-no-notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/feedback-no-notifications.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * BuddyPress - Members Feedback No Notifications
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<div id="message" class="info">
+
+	<?php if ( bp_is_current_action( 'unread' ) ) : ?>
+
+		<?php if ( bp_is_my_profile() ) : ?>
+
+			<p><?php _e( 'You have no unread notifications.', 'buddypress' ); ?></p>
+
+		<?php else : ?>
+
+			<p><?php _e( 'This member has no unread notifications.', 'buddypress' ); ?></p>
+
+		<?php endif; ?>
+
+	<?php else : ?>
+
+		<?php if ( bp_is_my_profile() ) : ?>
+
+			<p><?php _e( 'You have no notifications.', 'buddypress' ); ?></p>
+
+		<?php else : ?>
+
+			<p><?php _e( 'This member has no notifications.', 'buddypress' ); ?></p>
+
+		<?php endif; ?>
+
+	<?php endif; ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/notifications-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/notifications-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/notifications-loop.php	(revision 12265)
@@ -0,0 +1,51 @@
+<?php
+/**
+ * BuddyPress - Members Notifications Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+<form action="" method="post" id="notifications-bulk-management">
+	<table class="notifications">
+		<thead>
+			<tr>
+				<th class="icon"></th>
+				<th class="bulk-select-all"><input id="select-all-notifications" type="checkbox"><label class="bp-screen-reader-text" for="select-all-notifications"><?php
+					/* translators: accessibility text */
+					_e( 'Select all', 'buddypress' );
+				?></label></th>
+				<th class="title"><?php _e( 'Notification', 'buddypress' ); ?></th>
+				<th class="date"><?php _e( 'Date Received', 'buddypress' ); ?></th>
+				<th class="actions"><?php _e( 'Actions',    'buddypress' ); ?></th>
+			</tr>
+		</thead>
+
+		<tbody>
+
+			<?php while ( bp_the_notifications() ) : bp_the_notification(); ?>
+
+				<tr>
+					<td></td>
+					<td class="bulk-select-check"><label for="<?php bp_the_notification_id(); ?>"><input id="<?php bp_the_notification_id(); ?>" type="checkbox" name="notifications[]" value="<?php bp_the_notification_id(); ?>" class="notification-check"><span class="bp-screen-reader-text"><?php
+						/* translators: accessibility text */
+						_e( 'Select this notification', 'buddypress' );
+					?></span></label></td>
+					<td class="notification-description"><?php bp_the_notification_description();  ?></td>
+					<td class="notification-since"><?php bp_the_notification_time_since();   ?></td>
+					<td class="notification-actions"><?php bp_the_notification_action_links(); ?></td>
+				</tr>
+
+			<?php endwhile; ?>
+
+		</tbody>
+	</table>
+
+	<div class="notifications-options-nav">
+		<?php bp_notifications_bulk_management_dropdown(); ?>
+	</div><!-- .notifications-options-nav -->
+
+	<?php wp_nonce_field( 'notifications_bulk_nonce', 'notifications_bulk_nonce' ); ?>
+</form>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/read.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/read.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/read.php	(revision 12265)
@@ -0,0 +1,45 @@
+<?php
+/**
+ * BuddyPress - Members Read Notifications
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php if ( bp_has_notifications() ) : ?>
+
+	<h2 class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Notifications', 'buddypress' );
+	?></h2>
+
+	<div id="pag-top" class="pagination no-ajax">
+		<div class="pag-count" id="notifications-count-top">
+			<?php bp_notifications_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="notifications-pag-top">
+			<?php bp_notifications_pagination_links(); ?>
+		</div>
+	</div>
+
+	<?php bp_get_template_part( 'members/single/notifications/notifications-loop' ); ?>
+
+	<div id="pag-bottom" class="pagination no-ajax">
+		<div class="pag-count" id="notifications-count-bottom">
+			<?php bp_notifications_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="notifications-pag-bottom">
+			<?php bp_notifications_pagination_links(); ?>
+		</div>
+	</div>
+
+<?php else : ?>
+
+	<?php bp_get_template_part( 'members/single/notifications/feedback-no-notifications' ); ?>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/unread.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/unread.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/notifications/unread.php	(revision 12265)
@@ -0,0 +1,45 @@
+<?php
+/**
+ * BuddyPress - Members Unread Notifications
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<?php if ( bp_has_notifications() ) : ?>
+
+	<h2 class="bp-screen-reader-text"><?php
+		/* translators: accessibility text */
+		_e( 'Unread notifications', 'buddypress' );
+	?></h2>
+
+	<div id="pag-top" class="pagination no-ajax">
+		<div class="pag-count" id="notifications-count-top">
+			<?php bp_notifications_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="notifications-pag-top">
+			<?php bp_notifications_pagination_links(); ?>
+		</div>
+	</div>
+
+	<?php bp_get_template_part( 'members/single/notifications/notifications-loop' ); ?>
+
+	<div id="pag-bottom" class="pagination no-ajax">
+		<div class="pag-count" id="notifications-count-bottom">
+			<?php bp_notifications_pagination_count(); ?>
+		</div>
+
+		<div class="pagination-links" id="notifications-pag-bottom">
+			<?php bp_notifications_pagination_links(); ?>
+		</div>
+	</div>
+
+<?php else : ?>
+
+	<?php bp_get_template_part( 'members/single/notifications/feedback-no-notifications' ); ?>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/plugins.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/plugins.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/plugins.php	(revision 12265)
@@ -0,0 +1,67 @@
+<?php
+/**
+ * BuddyPress - Users Plugins Template
+ *
+ * 3rd-party plugins should use this template to easily add template
+ * support to their plugins for the members component.
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+		/**
+		 * Fires at the start of the member plugin template.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_member_plugin_template' ); ?>
+
+		<?php if ( ! bp_is_current_component_core() ) : ?>
+
+		<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+			<ul>
+				<?php bp_get_options_nav(); ?>
+
+				<?php
+
+				/**
+				 * Fires inside the member plugin template nav <ul> tag.
+				 *
+				 * @since 1.2.2
+				 */
+				do_action( 'bp_member_plugin_options_nav' ); ?>
+			</ul>
+		</div><!-- .item-list-tabs -->
+
+		<?php endif; ?>
+
+		<?php if ( has_action( 'bp_template_title' ) ) : ?>
+			<h3><?php
+
+			/**
+			 * Fires inside the member plugin template <h3> tag.
+			 *
+			 * @since 1.0.0
+			 */
+			do_action( 'bp_template_title' ); ?></h3>
+
+		<?php endif; ?>
+
+		<?php
+
+		/**
+		 * Fires and displays the member plugin template content.
+		 *
+		 * @since 1.0.0
+		 */
+		do_action( 'bp_template_content' ); ?>
+
+		<?php
+
+		/**
+		 * Fires at the end of the member plugin template.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_member_plugin_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile.php	(revision 12265)
@@ -0,0 +1,73 @@
+<?php
+/**
+ * BuddyPress - Users Profile
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<?php bp_get_options_nav(); ?>
+	</ul>
+</div><!-- .item-list-tabs -->
+
+<?php
+
+/**
+ * Fires before the display of member profile content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_profile_content' ); ?>
+
+<div class="profile">
+
+<?php switch ( bp_current_action() ) :
+
+	// Edit
+	case 'edit'   :
+		bp_get_template_part( 'members/single/profile/edit' );
+		break;
+
+	// Change Avatar
+	case 'change-avatar' :
+		bp_get_template_part( 'members/single/profile/change-avatar' );
+		break;
+
+	// Change Cover Image
+	case 'change-cover-image' :
+		bp_get_template_part( 'members/single/profile/change-cover-image' );
+		break;
+
+	// Compose
+	case 'public' :
+
+		// Display XProfile
+		if ( bp_is_active( 'xprofile' ) )
+			bp_get_template_part( 'members/single/profile/profile-loop' );
+
+		// Display WordPress profile (fallback)
+		else
+			bp_get_template_part( 'members/single/profile/profile-wp' );
+
+		break;
+
+	// Any other
+	default :
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch; ?>
+</div><!-- .profile -->
+
+<?php
+
+/**
+ * Fires after the display of member profile content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_profile_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/change-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/change-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/change-avatar.php	(revision 12265)
@@ -0,0 +1,96 @@
+<?php
+/**
+ * BuddyPress - Members Profile Change Avatar
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2><?php _e( 'Change Profile Photo', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the display of profile avatar upload content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_profile_avatar_upload_content' ); ?>
+
+<?php if ( !(int)bp_get_option( 'bp-disable-avatar-uploads' ) ) : ?>
+
+	<p><?php _e( 'Your profile photo will be used on your profile and throughout the site. If there is a <a href="http://gravatar.com">Gravatar</a> associated with your account email we will use that, or you can upload an image from your computer.', 'buddypress' ); ?></p>
+
+	<form action="" method="post" id="avatar-upload-form" class="standard-form" enctype="multipart/form-data">
+
+		<?php if ( 'upload-image' == bp_get_avatar_admin_step() ) : ?>
+
+			<?php wp_nonce_field( 'bp_avatar_upload' ); ?>
+			<p><?php _e( 'Click below to select a JPG, GIF or PNG format photo from your computer and then click \'Upload Image\' to proceed.', 'buddypress' ); ?></p>
+
+			<p id="avatar-upload">
+				<label for="file" class="bp-screen-reader-text"><?php
+					/* translators: accessibility text */
+					_e( 'Select an image', 'buddypress' );
+				?></label>
+				<input type="file" name="file" id="file" />
+				<input type="submit" name="upload" id="upload" value="<?php esc_attr_e( 'Upload Image', 'buddypress' ); ?>" />
+				<input type="hidden" name="action" id="action" value="bp_avatar_upload" />
+			</p>
+
+			<?php if ( bp_get_user_has_avatar() ) : ?>
+				<p><?php _e( "If you'd like to delete your current profile photo but not upload a new one, please use the delete profile photo button.", 'buddypress' ); ?></p>
+				<p><a class="button edit" href="<?php bp_avatar_delete_link(); ?>"><?php _e( 'Delete My Profile Photo', 'buddypress' ); ?></a></p>
+			<?php endif; ?>
+
+		<?php endif; ?>
+
+		<?php if ( 'crop-image' == bp_get_avatar_admin_step() ) : ?>
+
+			<h5><?php _e( 'Crop Your New Profile Photo', 'buddypress' ); ?></h5>
+
+			<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-to-crop" class="avatar" alt="<?php esc_attr_e( 'Profile photo to crop', 'buddypress' ); ?>" />
+
+			<div id="avatar-crop-pane">
+				<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-crop-preview" class="avatar" alt="<?php esc_attr_e( 'Profile photo preview', 'buddypress' ); ?>" />
+			</div>
+
+			<input type="submit" name="avatar-crop-submit" id="avatar-crop-submit" value="<?php esc_attr_e( 'Crop Image', 'buddypress' ); ?>" />
+
+			<input type="hidden" name="image_src" id="image_src" value="<?php bp_avatar_to_crop_src(); ?>" />
+			<input type="hidden" id="x" name="x" />
+			<input type="hidden" id="y" name="y" />
+			<input type="hidden" id="w" name="w" />
+			<input type="hidden" id="h" name="h" />
+
+			<?php wp_nonce_field( 'bp_avatar_cropstore' ); ?>
+
+		<?php endif; ?>
+
+	</form>
+
+	<?php
+	/**
+	 * Load the Avatar UI templates
+	 *
+	 * @since  2.3.0
+	 */
+	bp_avatar_get_templates(); ?>
+
+<?php else : ?>
+
+	<p><?php _e( 'Your profile photo will be used on your profile and throughout the site. To change your profile photo, please create an account with <a href="https://gravatar.com">Gravatar</a> using the same email address as you used to register with this site.', 'buddypress' ); ?></p>
+
+<?php endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of profile avatar upload content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_profile_avatar_upload_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/change-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/change-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/change-cover-image.php	(revision 12265)
@@ -0,0 +1,34 @@
+<?php
+/**
+ * BuddyPress - Members Profile Change Cover Image
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<h2><?php _e( 'Change Cover Image', 'buddypress' ); ?></h2>
+
+<?php
+
+/**
+ * Fires before the display of profile cover image upload content.
+ *
+ * @since 2.4.0
+ */
+do_action( 'bp_before_profile_edit_cover_image' ); ?>
+
+<p><?php _e( 'Your Cover Image will be used to customize the header of your profile.', 'buddypress' ); ?></p>
+
+<?php bp_attachments_get_template_part( 'cover-images/index' ); ?>
+
+<?php
+
+/**
+ * Fires after the display of profile cover image upload content.
+ *
+ * @since 2.4.0
+ */
+do_action( 'bp_after_profile_edit_cover_image' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/edit.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/edit.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/edit.php	(revision 12265)
@@ -0,0 +1,126 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile Edit
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires after the display of member profile edit content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_before_profile_edit_content' );
+
+if ( bp_has_profile( 'profile_group_id=' . bp_get_current_profile_group_id() ) ) :
+	while ( bp_profile_groups() ) : bp_the_profile_group(); ?>
+
+<form action="<?php bp_the_profile_group_edit_form_action(); ?>" method="post" id="profile-edit-form" class="standard-form <?php bp_the_profile_group_slug(); ?>">
+
+	<?php
+
+		/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+		do_action( 'bp_before_profile_field_content' ); ?>
+
+		<h2><?php printf( __( "Editing '%s' Profile Group", 'buddypress' ), bp_get_the_profile_group_name() ); ?></h2>
+
+		<?php if ( bp_profile_has_multiple_groups() ) : ?>
+			<ul class="button-nav" aria-label="<?php esc_attr_e( 'Profile field groups', 'buddypress' ); ?>" role="navigation">
+
+				<?php bp_profile_group_tabs(); ?>
+
+			</ul>
+		<?php endif ;?>
+
+		<div class="clear"></div>
+
+		<?php while ( bp_profile_fields() ) : bp_the_profile_field(); ?>
+
+			<div<?php bp_field_css_class( 'editfield' ); ?>>
+				<fieldset>
+
+				<?php
+				$field_type = bp_xprofile_create_field_type( bp_get_the_profile_field_type() );
+				$field_type->edit_field_html();
+
+				/**
+				 * Fires before the display of visibility options for the field.
+				 *
+				 * @since 1.7.0
+				 */
+				do_action( 'bp_custom_profile_edit_fields_pre_visibility' );
+				?>
+
+				<?php if ( bp_current_user_can( 'bp_xprofile_change_field_visibility' ) ) : ?>
+					<p class="field-visibility-settings-toggle" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id() ?>"><span id="<?php bp_the_profile_field_input_name(); ?>-2">
+						<?php
+						printf(
+							__( 'This field can be seen by: %s', 'buddypress' ),
+							'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+						);
+						?>
+						</span>
+						<button type="button" class="visibility-toggle-link" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-2" aria-expanded="false"><?php _ex( 'Change', 'Change profile field visibility level', 'buddypress' ); ?></button>
+					</p>
+
+					<div class="field-visibility-settings" id="field-visibility-settings-<?php bp_the_profile_field_id() ?>">
+						<fieldset>
+							<legend><?php _e( 'Who can see this field?', 'buddypress' ) ?></legend>
+
+							<?php bp_profile_visibility_radio_buttons() ?>
+
+						</fieldset>
+						<button type="button" class="field-visibility-settings-close"><?php _e( 'Close', 'buddypress' ) ?></button>
+					</div>
+				<?php else : ?>
+					<div class="field-visibility-settings-notoggle" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id() ?>">
+						<?php
+						printf(
+							__( 'This field can be seen by: %s', 'buddypress' ),
+							'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+						);
+						?>
+					</div>
+				<?php endif ?>
+
+				<?php
+
+				/**
+				 * Fires after the visibility options for a field.
+				 *
+				 * @since 1.1.0
+				 */
+				do_action( 'bp_custom_profile_edit_fields' ); ?>
+
+				</fieldset>
+			</div>
+
+		<?php endwhile; ?>
+
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+	do_action( 'bp_after_profile_field_content' ); ?>
+
+	<div class="submit">
+		<input type="submit" name="profile-group-edit-submit" id="profile-group-edit-submit" value="<?php esc_attr_e( 'Save Changes', 'buddypress' ); ?> " />
+	</div>
+
+	<input type="hidden" name="field_ids" id="field_ids" value="<?php bp_the_profile_field_ids(); ?>" />
+
+	<?php wp_nonce_field( 'bp_xprofile_edit' ); ?>
+
+</form>
+
+<?php endwhile; endif; ?>
+
+<?php
+
+/**
+ * Fires after the display of member profile edit content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_profile_edit_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/profile-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/profile-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/profile-loop.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+/**
+ * BuddyPress - Members Profile Loop
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+do_action( 'bp_before_profile_loop_content' ); ?>
+
+<?php if ( bp_has_profile() ) : ?>
+
+	<?php while ( bp_profile_groups() ) : bp_the_profile_group(); ?>
+
+		<?php if ( bp_profile_group_has_fields() ) : ?>
+
+			<?php
+
+			/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+			do_action( 'bp_before_profile_field_content' ); ?>
+
+			<div class="bp-widget <?php bp_the_profile_group_slug(); ?>">
+
+				<h2><?php bp_the_profile_group_name(); ?></h2>
+
+				<table class="profile-fields">
+
+					<?php while ( bp_profile_fields() ) : bp_the_profile_field(); ?>
+
+						<?php if ( bp_field_has_data() ) : ?>
+
+							<tr<?php bp_field_css_class(); ?>>
+
+								<td class="label"><?php bp_the_profile_field_name(); ?></td>
+
+								<td class="data"><?php bp_the_profile_field_value(); ?></td>
+
+							</tr>
+
+						<?php endif; ?>
+
+						<?php
+
+						/**
+						 * Fires after the display of a field table row for profile data.
+						 *
+						 * @since 1.1.0
+						 */
+						do_action( 'bp_profile_field_item' ); ?>
+
+					<?php endwhile; ?>
+
+				</table>
+			</div>
+
+			<?php
+
+			/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+			do_action( 'bp_after_profile_field_content' ); ?>
+
+		<?php endif; ?>
+
+	<?php endwhile; ?>
+
+	<?php
+
+	/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+	do_action( 'bp_profile_field_buttons' ); ?>
+
+<?php endif; ?>
+
+<?php
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php */
+do_action( 'bp_after_profile_loop_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/profile/profile-wp.php	(revision 12265)
@@ -0,0 +1,115 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile WP
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of member profile loop content.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_before_profile_loop_content' ); ?>
+
+<?php $ud = get_userdata( bp_displayed_user_id() ); ?>
+
+<?php
+
+	/**
+	 * Fires before the display of member profile field content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_profile_field_content' ); ?>
+
+	<div class="bp-widget wp-profile">
+		<h2><?php bp_is_my_profile() ? _e( 'My Profile', 'buddypress' ) : printf( __( "%s's Profile", 'buddypress' ), bp_get_displayed_user_fullname() ); ?></h2>
+
+		<table class="wp-profile-fields">
+
+			<?php if ( $ud->display_name ) : ?>
+
+				<tr id="wp_displayname">
+					<td class="label"><?php _e( 'Name', 'buddypress' ); ?></td>
+					<td class="data"><?php echo $ud->display_name; ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+			<?php if ( $ud->user_description ) : ?>
+
+				<tr id="wp_desc">
+					<td class="label"><?php _e( 'About Me', 'buddypress' ); ?></td>
+					<td class="data"><?php echo $ud->user_description; ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+			<?php if ( $ud->user_url ) : ?>
+
+				<tr id="wp_website">
+					<td class="label"><?php _e( 'Website', 'buddypress' ); ?></td>
+					<td class="data"><?php echo make_clickable( $ud->user_url ); ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+			<?php if ( $ud->jabber ) : ?>
+
+				<tr id="wp_jabber">
+					<td class="label"><?php _e( 'Jabber', 'buddypress' ); ?></td>
+					<td class="data"><?php echo $ud->jabber; ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+			<?php if ( $ud->aim ) : ?>
+
+				<tr id="wp_aim">
+					<td class="label"><?php _e( 'AOL Messenger', 'buddypress' ); ?></td>
+					<td class="data"><?php echo $ud->aim; ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+			<?php if ( $ud->yim ) : ?>
+
+				<tr id="wp_yim">
+					<td class="label"><?php _e( 'Yahoo Messenger', 'buddypress' ); ?></td>
+					<td class="data"><?php echo $ud->yim; ?></td>
+				</tr>
+
+			<?php endif; ?>
+
+		</table>
+	</div>
+
+<?php
+
+/**
+ * Fires after the display of member profile field content.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_after_profile_field_content' ); ?>
+
+<?php
+
+/**
+ * Fires and displays the profile field buttons.
+ *
+ * @since 1.1.0
+ */
+do_action( 'bp_profile_field_buttons' ); ?>
+
+<?php
+
+/**
+ * Fires after the display of member profile loop content.
+ *
+ * @since 1.2.0
+ */
+do_action( 'bp_after_profile_loop_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings.php	(revision 12265)
@@ -0,0 +1,43 @@
+<?php
+/**
+ * BuddyPress - Users Settings
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+?>
+
+<div class="item-list-tabs no-ajax" id="subnav" aria-label="<?php esc_attr_e( 'Member secondary navigation', 'buddypress' ); ?>" role="navigation">
+	<ul>
+		<?php if ( bp_core_can_edit_settings() ) : ?>
+
+			<?php bp_get_options_nav(); ?>
+
+		<?php endif; ?>
+	</ul>
+</div>
+
+<?php
+
+switch ( bp_current_action() ) :
+	case 'notifications'  :
+		bp_get_template_part( 'members/single/settings/notifications'  );
+		break;
+	case 'capabilities'   :
+		bp_get_template_part( 'members/single/settings/capabilities'   );
+		break;
+	case 'delete-account' :
+		bp_get_template_part( 'members/single/settings/delete-account' );
+		break;
+	case 'general'        :
+		bp_get_template_part( 'members/single/settings/general'        );
+		break;
+	case 'profile'        :
+		bp_get_template_part( 'members/single/settings/profile'        );
+		break;
+	default:
+		bp_get_template_part( 'members/single/plugins'                 );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/capabilities.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/capabilities.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/capabilities.php	(revision 12265)
@@ -0,0 +1,49 @@
+<?php
+/**
+ * BuddyPress - Members Settings Capabilities
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_before_member_settings_template' ); ?>
+
+<form action="<?php echo bp_displayed_user_domain() . bp_get_settings_slug() . '/capabilities/'; ?>" name="account-capabilities-form" id="account-capabilities-form" class="standard-form" method="post">
+
+	<?php
+
+	/**
+	 * Fires before the display of the submit button for user capabilities saving.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_members_capabilities_account_before_submit' ); ?>
+
+	<label for="user-spammer">
+		<input type="checkbox" name="user-spammer" id="user-spammer" value="1" <?php checked( bp_is_user_spammer( bp_displayed_user_id() ) ); ?> />
+		 <?php _e( 'This user is a spammer.', 'buddypress' ); ?>
+	</label>
+
+	<div class="submit">
+		<input type="submit" value="<?php esc_attr_e( 'Save', 'buddypress' ); ?>" id="capabilities-submit" name="capabilities-submit" />
+	</div>
+
+	<?php
+
+	/**
+	 * Fires after the display of the submit button for user capabilities saving.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_members_capabilities_account_after_submit' ); ?>
+
+	<?php wp_nonce_field( 'capabilities' ); ?>
+
+</form>
+
+<?php
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_after_member_settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/delete-account.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/delete-account.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/delete-account.php	(revision 12265)
@@ -0,0 +1,63 @@
+<?php
+/**
+ * BuddyPress - Members Settings Delete Account
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_before_member_settings_template' ); ?>
+
+<div id="message" class="info">
+
+	<?php if ( bp_is_my_profile() ) : ?>
+
+		<p><?php _e( 'Deleting your account will delete all of the content you have created. It will be completely irrecoverable.', 'buddypress' ); ?></p>
+
+	<?php else : ?>
+
+		<p><?php _e( 'Deleting this account will delete all of the content it has created. It will be completely irrecoverable.', 'buddypress' ); ?></p>
+
+	<?php endif; ?>
+
+</div>
+
+<form action="<?php echo bp_displayed_user_domain() . bp_get_settings_slug() . '/delete-account'; ?>" name="account-delete-form" id="account-delete-form" class="standard-form" method="post">
+
+	<?php
+
+	/**
+	 * Fires before the display of the submit button for user delete account submitting.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_members_delete_account_before_submit' ); ?>
+
+	<label for="delete-account-understand">
+		<input type="checkbox" name="delete-account-understand" id="delete-account-understand" value="1" onclick="if(this.checked) { document.getElementById('delete-account-button').disabled = ''; } else { document.getElementById('delete-account-button').disabled = 'disabled'; }" />
+		 <?php _e( 'I understand the consequences.', 'buddypress' ); ?>
+	</label>
+
+	<div class="submit">
+		<input type="submit" disabled="disabled" value="<?php esc_attr_e( 'Delete Account', 'buddypress' ); ?>" id="delete-account-button" name="delete-account-button" />
+	</div>
+
+	<?php
+
+	/**
+	 * Fires after the display of the submit button for user delete account submitting.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_members_delete_account_after_submit' ); ?>
+
+	<?php wp_nonce_field( 'delete-account' ); ?>
+
+</form>
+
+<?php
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_after_member_settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/general.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/general.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/general.php	(revision 12265)
@@ -0,0 +1,66 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_before_member_settings_template' ); ?>
+
+<h2 class="bp-screen-reader-text"><?php
+	/* translators: accessibility text */
+	_e( 'Account settings', 'buddypress' );
+?></h2>
+
+<form action="<?php echo bp_displayed_user_domain() . bp_get_settings_slug() . '/general'; ?>" method="post" class="standard-form" id="settings-form">
+
+	<?php if ( !is_super_admin() ) : ?>
+
+		<label for="pwd"><?php _e( 'Current Password <span>(required to update email or change current password)</span>', 'buddypress' ); ?></label>
+		<input type="password" name="pwd" id="pwd" size="16" value="" class="settings-input small" <?php bp_form_field_attributes( 'password' ); ?>/> &nbsp;<a href="<?php echo wp_lostpassword_url(); ?>"><?php _e( 'Lost your password?', 'buddypress' ); ?></a>
+
+	<?php endif; ?>
+
+	<label for="email"><?php _e( 'Account Email', 'buddypress' ); ?></label>
+	<input type="email" name="email" id="email" value="<?php echo bp_get_displayed_user_email(); ?>" class="settings-input" <?php bp_form_field_attributes( 'email' ); ?>/>
+
+	<label for="pass1"><?php _e( 'Change Password <span>(leave blank for no change)</span>', 'buddypress' ); ?></label>
+	<input type="password" name="pass1" id="pass1" size="16" value="" class="settings-input small password-entry" <?php bp_form_field_attributes( 'password' ); ?>/>
+	<div id="pass-strength-result"></div>
+	<label for="pass2"><?php _e( 'Repeat New Password', 'buddypress' );
+	?></label>
+	<input type="password" name="pass2" id="pass2" size="16" value="" class="settings-input small password-entry-confirm" <?php bp_form_field_attributes( 'password' ); ?>/>
+
+	<?php
+
+	/**
+	 * Fires before the display of the submit button for user general settings saving.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_core_general_settings_before_submit' ); ?>
+
+	<div class="submit">
+		<input type="submit" name="submit" value="<?php esc_attr_e( 'Save Changes', 'buddypress' ); ?>" id="submit" class="auto" />
+	</div>
+
+	<?php
+
+	/**
+	 * Fires after the display of the submit button for user general settings saving.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_core_general_settings_after_submit' ); ?>
+
+	<?php wp_nonce_field( 'bp_settings_general' ); ?>
+
+</form>
+
+<?php
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_after_member_settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/notifications.php	(revision 12265)
@@ -0,0 +1,59 @@
+<?php
+/**
+ * BuddyPress - Members Settings Notifications
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_before_member_settings_template' ); ?>
+
+<h2 class="bp-screen-reader-text"><?php
+	/* translators: accessibility text */
+	_e( 'Notification settings', 'buddypress' );
+?></h2>
+
+<form action="<?php echo bp_displayed_user_domain() . bp_get_settings_slug() . '/notifications'; ?>" method="post" class="standard-form" id="settings-form">
+	<p><?php _e( 'Send an email notice when:', 'buddypress' ); ?></p>
+
+	<?php
+
+	/**
+	 * Fires at the top of the member template notification settings form.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_notification_settings' ); ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the submit button for user notification saving.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_members_notification_settings_before_submit' ); ?>
+
+	<div class="submit">
+		<input type="submit" name="submit" value="<?php esc_attr_e( 'Save Changes', 'buddypress' ); ?>" id="submit" class="auto" />
+	</div>
+
+	<?php
+
+	/**
+	 * Fires after the display of the submit button for user notification saving.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_members_notification_settings_after_submit' ); ?>
+
+	<?php wp_nonce_field('bp_settings_notifications' ); ?>
+
+</form>
+
+<?php
+
+/** This action is documented in bp-templates/bp-legacy/buddypress/members/single/settings/profile.php */
+do_action( 'bp_after_member_settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/profile.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/profile.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/buddypress/members/single/settings/profile.php	(revision 12265)
@@ -0,0 +1,93 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile
+ *
+ * @package BuddyPress
+ * @subpackage bp-legacy
+ * @version 3.0.0
+ */
+
+/**
+ * Fires before the display of member settings template.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_before_member_settings_template' ); ?>
+
+<h2 class="bp-screen-reader-text"><?php
+	/* translators: accessibility text */
+	_e( 'Profile visibility settings', 'buddypress' );
+?></h2>
+
+<form action="<?php echo trailingslashit( bp_displayed_user_domain() . bp_get_settings_slug() . '/profile' ); ?>" method="post" class="standard-form" id="settings-form">
+
+	<?php if ( bp_xprofile_get_settings_fields() ) : ?>
+
+		<?php while ( bp_profile_groups() ) : bp_the_profile_group(); ?>
+
+			<?php if ( bp_profile_fields() ) : ?>
+
+				<table class="profile-settings" id="xprofile-settings-<?php bp_the_profile_group_slug(); ?>">
+					<thead>
+						<tr>
+							<th class="title field-group-name"><?php bp_the_profile_group_name(); ?></th>
+							<th class="title"><?php _e( 'Visibility', 'buddypress' ); ?></th>
+						</tr>
+					</thead>
+
+					<tbody>
+
+						<?php while ( bp_profile_fields() ) : bp_the_profile_field(); ?>
+
+							<tr <?php bp_field_css_class(); ?>>
+								<td class="field-name"><?php bp_the_profile_field_name(); ?></td>
+								<td class="field-visibility"><?php bp_profile_settings_visibility_select(); ?></td>
+							</tr>
+
+						<?php endwhile; ?>
+
+					</tbody>
+				</table>
+
+			<?php endif; ?>
+
+		<?php endwhile; ?>
+
+	<?php endif; ?>
+
+	<?php
+
+	/**
+	 * Fires before the display of the submit button for user profile saving.
+	 *
+	 * @since 2.0.0
+	 */
+	do_action( 'bp_core_xprofile_settings_before_submit' ); ?>
+
+	<div class="submit">
+		<input id="submit" type="submit" name="xprofile-settings-submit" value="<?php esc_attr_e( 'Save Settings', 'buddypress' ); ?>" class="auto" />
+	</div>
+
+	<?php
+
+	/**
+	 * Fires after the display of the submit button for user profile saving.
+	 *
+	 * @since 2.0.0
+	 */
+	do_action( 'bp_core_xprofile_settings_after_submit' ); ?>
+
+	<?php wp_nonce_field( 'bp_xprofile_settings' ); ?>
+
+	<input type="hidden" name="field_ids" id="field_ids" value="<?php bp_the_profile_field_ids(); ?>" />
+
+</form>
+
+<?php
+
+/**
+ * Fires after the display of member settings template.
+ *
+ * @since 1.5.0
+ */
+do_action( 'bp_after_member_settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/buddypress-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/buddypress-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/buddypress-rtl.css	(revision 12265)
@@ -0,0 +1,2261 @@
+/*--------------------------------------------------------------
+Hello, this is the BuddyPress Legacy stylesheet.
+
+@version 3.0.0
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 - Navigation
+	1.1 - Pagination
+2.0 - WordPress
+	2.1 - Images
+3.0 - BuddyPress
+	3.1 - Activity
+		3.1.1 - Activity Listing
+		3.1.2 - Activity Comments
+	3.2 - Toolbar
+	3.3 - Directories - Members, Groups, Blogs, Forums
+	3.4 - Error / Success Messages
+	3.5 - Forms
+	3.6 - Ajax Loading
+	3.7 - Topics and Tables - Forums and General
+	3.8 - Headers, Lists and Tabs - Activity, Groups, Blogs, Forums
+		3.8.1 - Cover Image
+	3.9 - Private Messaging Threads
+	3.10 - Extended Profiles
+	3.11 - Widgets
+	3.12 - Tooltips
+4.0 - Media Queries
+	4.1 - Smartphones Landscape
+	4.2 - Smartphones Portrait
+	4.3 - Smartphones - smaller screen sizes
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+1 - Navigation
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+1.1 - Pagination
+--------------------------------------------------------------*/
+#buddypress div.pagination {
+	background: transparent;
+	border: none;
+	color: #767676;
+	font-size: small;
+	margin: 0;
+	position: relative;
+	display: block;
+	float: right;
+	width: 100%;
+	padding: 10px 0;
+}
+
+#buddypress div.pagination .pag-count {
+	float: right;
+	margin-right: 10px;
+}
+
+#buddypress div.pagination .pagination-links {
+	float: left;
+	margin-left: 10px;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	font-size: 90%;
+	padding: 0 5px;
+}
+
+#buddypress div.pagination .pagination-links a:hover {
+	font-weight: 700;
+}
+
+#buddypress noscript div.pagination {
+	margin-bottom: 15px;
+}
+
+#buddypress #nav-above {
+	display: none;
+}
+
+#buddypress .paged #nav-above {
+	display: block;
+}
+
+/*--------------------------------------------------------------
+2 - WordPress
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+2.1 - Images
+--------------------------------------------------------------*/
+#buddypress img.wp-smiley {
+	border: none !important;
+	clear: none !important;
+	float: none !important;
+	margin: 0 !important;
+	padding: 0 !important;
+}
+
+#buddypress .clear {
+	clear: right;
+}
+
+/*--------------------------------------------------------------
+3.0 - BuddyPress
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+3.1 - Activity
+--------------------------------------------------------------*/
+#buddypress #activity-stream {
+	margin-top: -5px;
+}
+
+#buddypress #activity-stream p {
+	margin: 5px 0;
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .home-page form#whats-new-form {
+	border-bottom: none;
+	padding-bottom: 0;
+}
+
+#buddypress form#whats-new-form #whats-new-avatar {
+	float: right;
+}
+
+#buddypress form#whats-new-form #whats-new-content {
+	margin-right: 55px;
+	padding: 0 20px 20px 0;
+}
+
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 0.5;
+	margin-bottom: 15px;
+	margin-right: 75px;
+}
+
+#buddypress form#whats-new-form textarea {
+	background: #fff;
+	box-sizing: border-box;
+	color: #555;
+	font-family: inherit;
+	font-size: medium;
+	height: 2.2em;
+	line-height: 1.4;
+	padding: 6px;
+	width: 100%;
+}
+
+body.no-js #buddypress form#whats-new-form textarea {
+	height: 50px;
+}
+
+#buddypress form#whats-new-form #whats-new-options select {
+	max-width: 200px;
+	margin-top: 12px;
+}
+
+#buddypress form#whats-new-form #whats-new-submit {
+	float: left;
+	margin-top: 12px;
+}
+
+#buddypress #whats-new-options:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+body.no-js #buddypress #whats-new-options {
+	height: auto;
+}
+
+#buddypress #whats-new:focus {
+	border-color: rgba(31, 179, 221, 0.9) !important;
+	outline-color: rgba(31, 179, 221, 0.9);
+}
+
+/*--------------------------------------------------------------
+3.1.1 - Activity Listing
+--------------------------------------------------------------*/
+#buddypress ul.activity-list li {
+	overflow: hidden;
+	padding: 15px 0 0;
+	list-style: none;
+}
+
+#buddypress .activity-list .activity-avatar {
+	float: right;
+}
+
+#buddypress ul.item-list.activity-list li.has-comments {
+	padding-bottom: 15px;
+}
+
+body.activity-permalink #buddypress ul.activity-list li.has-comments {
+	padding-bottom: 0;
+}
+
+#buddypress .activity-list li.mini {
+	font-size: 80%;
+	position: relative;
+}
+
+#buddypress .activity-list li.mini .activity-avatar img.avatar,
+#buddypress .activity-list li.mini .activity-avatar img.FB_profile_pic { /* stylelint-disable-line selector-class-pattern */
+	height: 20px;
+	margin-right: 30px;
+	width: 20px;
+}
+
+#buddypress .activity-permalink .activity-list li.mini .activity-avatar img.avatar,
+#buddypress .activity-permalink .activity-list li.mini .activity-avatar img.FB_profile_pic { /* stylelint-disable-line selector-class-pattern */
+	height: auto;
+	margin-right: 0;
+	width: auto;
+}
+
+body.activity-permalink #buddypress .activity-list > li:first-child {
+	padding-top: 0;
+}
+
+#buddypress .activity-list li .activity-content {
+	position: relative;
+}
+
+#buddypress .activity-list li.mini .activity-content p {
+	margin: 0;
+}
+
+#buddypress .activity-list li.mini .activity-comments {
+	clear: both;
+	font-size: 120%;
+}
+
+body.activity-permalink #buddypress li.mini .activity-meta {
+	margin-top: 4px;
+}
+
+#buddypress .activity-list li .activity-inreplyto {
+	color: #767676;
+	font-size: 80%;
+}
+
+#buddypress .activity-list li .activity-inreplyto > p {
+	margin: 0;
+	display: inline;
+}
+
+#buddypress .activity-list li .activity-inreplyto blockquote,
+#buddypress .activity-list li .activity-inreplyto div.activity-inner {
+	background: none;
+	border: none;
+	display: inline;
+	margin: 0;
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress .activity-list .activity-content {
+	margin: 0 70px 0 0;
+}
+
+body.activity-permalink #buddypress .activity-list li .activity-content {
+	border: none;
+	font-size: 100%;
+	line-height: 1.5;
+	margin-right: 170px;
+	margin-left: 0;
+	padding: 0;
+}
+
+body.activity-permalink #buddypress .activity-list li .activity-header > p {
+	margin: 0;
+	padding: 5px 0 0;
+}
+
+#buddypress .activity-list .activity-content .activity-header,
+#buddypress .activity-list .activity-content .comment-header {
+	color: #767676;
+	line-height: 2.2;
+}
+
+#buddypress .activity-header {
+	margin-left: 20px;
+}
+
+#buddypress .activity-header a,
+#buddypress .comment-meta a,
+#buddypress .acomment-meta a {
+	text-decoration: none;
+}
+
+#buddypress .activity-list .activity-content .activity-header img.avatar {
+	float: none !important;
+	margin: 0 0 -8px 5px !important;
+}
+
+#buddypress a.bp-secondary-action,
+#buddypress span.highlight {
+	font-size: 80%;
+	padding: 0;
+	margin-left: 5px;
+	text-decoration: none;
+}
+
+#buddypress .activity-list .activity-content .activity-inner,
+#buddypress .activity-list .activity-content blockquote {
+	margin: 10px 0 5px 10px;
+	overflow: hidden;
+}
+
+#buddypress .activity-list li.new_forum_post .activity-content .activity-inner,
+#buddypress .activity-list li.new_forum_topic .activity-content .activity-inner {
+	border-right: 2px solid #eaeaea;
+	margin-right: 5px;
+	padding-right: 10px;
+}
+
+body.activity-permalink #buddypress .activity-content .activity-inner,
+body.activity-permalink #buddypress .activity-content blockquote {
+	margin-right: 0;
+	margin-top: 5px;
+}
+
+#buddypress .activity-inner > p {
+	word-wrap: break-word;
+}
+
+#buddypress .activity-inner > .activity-inner {
+	margin: 0;
+}
+
+#buddypress .activity-inner > blockquote {
+	margin: 0;
+}
+
+#buddypress .activity-list .activity-content img.thumbnail {
+	border: 2px solid #eee;
+	float: right;
+	margin: 0 0 5px 10px;
+}
+
+#buddypress .activity-read-more {
+	margin-right: 1em;
+	white-space: nowrap;
+}
+
+#buddypress .activity-list li.load-more,
+#buddypress .activity-list li.load-newest {
+	background: #f0f0f0;
+	font-size: 110%;
+	margin: 15px 0;
+	padding: 10px 15px;
+	text-align: center;
+}
+
+#buddypress .activity-list li.load-more a,
+#buddypress .activity-list li.load-newest a {
+	color: #4d4d4d;
+}
+
+
+/*--------------------------------------------------------------
+3.1.2 - Activity Comments
+--------------------------------------------------------------*/
+#buddypress div.activity-meta {
+	margin: 18px 0 0;
+}
+
+body.activity-permalink #buddypress div.activity-meta {
+	margin-bottom: 6px;
+}
+
+#buddypress div.activity-meta a {
+	padding: 4px 8px;
+}
+
+#buddypress a.activity-time-since {
+	color: #767676;
+	text-decoration: none;
+}
+
+#buddypress a.activity-time-since:hover {
+	color: #767676;
+	text-decoration: underline;
+}
+
+#buddypress a.bp-primary-action,
+#buddypress #reply-title small a {
+	font-size: 80%;
+	margin-left: 5px;
+	text-decoration: none;
+}
+
+#buddypress a.bp-primary-action span,
+#buddypress #reply-title small a span {
+	background: #767676;
+	color: #fff;
+	font-size: 90%;
+	margin-right: 2px;
+	padding: 0 5px;
+}
+
+#buddypress a.bp-primary-action:hover span,
+#buddypress #reply-title small a:hover span {
+	background: #555;
+	color: #fff;
+}
+
+#buddypress div.activity-comments {
+	margin: 0 70px 0 0;
+	overflow: hidden; /* IE fix */
+	position: relative;
+	width: auto;
+	clear: both;
+}
+
+body.activity-permalink #buddypress div.activity-comments {
+	background: none;
+	margin-right: 170px;
+	width: auto;
+}
+
+#buddypress div.activity-comments > ul {
+	padding: 0 10px 0 0;
+}
+
+#buddypress div.activity-comments ul,
+#buddypress div.activity-comments ul li {
+	border: none;
+	list-style: none;
+}
+
+#buddypress div.activity-comments ul {
+	clear: both;
+	margin: 0;
+}
+
+#buddypress div.activity-comments ul li {
+	border-top: 1px solid #eee;
+	padding: 10px 0 0;
+}
+
+body.activity-permalink #buddypress .activity-list li.mini .activity-comments {
+	clear: none;
+	margin-top: 0;
+}
+
+body.activity-permalink #buddypress div.activity-comments ul li {
+	border-width: 1px;
+	padding: 10px 0 0;
+}
+
+#buddypress div.activity-comments > ul > li:first-child {
+	border-top: none;
+}
+
+#buddypress div.activity-comments ul li:last-child {
+	margin-bottom: 0;
+}
+
+#buddypress div.activity-comments ul li > ul {
+	margin-right: 30px;
+	margin-top: 0;
+	padding-right: 10px;
+}
+
+body.activity-permalink #buddypress div.activity-comments ul li > ul {
+	margin-top: 10px;
+}
+
+body.activity-permalink #buddypress div.activity-comments > ul {
+	padding: 0 15px 0 10px;
+}
+
+#buddypress div.activity-comments div.acomment-avatar img {
+	border-width: 1px;
+	float: right;
+	height: 25px;
+	margin-left: 10px;
+	width: 25px;
+}
+
+#buddypress div.activity-comments div.acomment-content {
+	font-size: 80%;
+	margin: 5px 40px 0 0;
+}
+
+#buddypress div.acomment-content .time-since,
+#buddypress div.acomment-content .activity-delete-link,
+#buddypress div.acomment-content .comment-header {
+	display: none;
+}
+
+body.activity-permalink #buddypress div.activity-comments div.acomment-content {
+	font-size: 90%;
+}
+
+#buddypress div.activity-comments div.acomment-meta {
+	color: #767676;
+	font-size: 80%;
+}
+
+#buddypress div.activity-comments form.ac-form {
+	display: none;
+	padding: 10px;
+}
+
+#buddypress div.activity-comments li form.ac-form {
+	margin-left: 15px;
+	clear: both;
+}
+
+#buddypress div.activity-comments form.root {
+	margin-right: 0;
+}
+
+#buddypress div.activity-comments div#message {
+	margin-top: 15px;
+	margin-bottom: 0;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #fff;
+	border: 1px inset #ccc;
+	margin-bottom: 10px;
+	padding: 8px;
+}
+
+#buddypress div.activity-comments form textarea {
+	border: none;
+	background: transparent;
+	box-shadow: none;
+	outline: none;
+	color: #555;
+	font-family: inherit;
+	font-size: 100%;
+	height: 60px;
+	padding: 0;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.activity-comments form input {
+	margin-top: 5px;
+}
+
+#buddypress div.activity-comments form div.ac-reply-avatar {
+	float: right;
+}
+
+#buddypress div.ac-reply-avatar img {
+	border: 1px solid #eee;
+}
+
+#buddypress div.activity-comments form div.ac-reply-content {
+	color: #767676;
+	margin-right: 50px;
+	padding-right: 15px;
+}
+
+#buddypress div.activity-comments form div.ac-reply-content a {
+	text-decoration: none;
+}
+
+#buddypress .acomment-options {
+	float: right;
+	margin: 5px 40px 5px 0;
+}
+
+#buddypress .acomment-options a {
+	color: #767676;
+}
+
+#buddypress .acomment-options a:hover {
+	color: inherit;
+}
+
+/*--------------------------------------------------------------
+3.3 - Directories - Members, Groups, Blogs, Forums
+--------------------------------------------------------------*/
+#buddypress div.dir-search {
+	float: left;
+	margin: -39px 0 0 0;
+}
+
+#buddypress div.dir-search input[type="text"],
+#buddypress li.groups-members-search input[type="text"] {
+	font-size: 90%;
+	padding: 1px 3px;
+}
+
+#buddypress .current-member-type {
+	font-style: italic;
+}
+
+#buddypress .dir-form {
+	clear: both;
+}
+
+/*--------------------------------------------------------------
+3.4 - Errors / Success Messages
+--------------------------------------------------------------*/
+#buddypress div#message {
+	margin: 0 0 15px;
+}
+
+#buddypress #message.info {
+	margin-bottom: 0;
+}
+
+#buddypress div#message.updated {
+	clear: both;
+	display: block;
+}
+
+#buddypress div#message p,
+#sitewide-notice p {
+	font-size: 90%;
+	display: block;
+	padding: 10px 15px;
+}
+
+#buddypress div#message.error p {
+	background-color: #fdc;
+	border: 1px solid #a00;
+	clear: right;
+	color: #800;
+}
+
+#buddypress div#message.warning p {
+	background-color: #ffe0af;
+	border: 1px solid #ffd087;
+	clear: right;
+	color: #800;
+}
+
+#buddypress div#message.updated p {
+	background-color: #efc;
+	border: 1px solid #591;
+	color: #250;
+}
+
+#buddypress #pass-strength-result {
+	background-color: #eee;
+	border-color: #ddd;
+	border-style: solid;
+	border-width: 1px;
+	display: none;
+	margin: 5px 0 5px 5px;
+	padding: 5px;
+	text-align: center;
+	width: 150px;
+}
+
+#buddypress .standard-form #basic-details-section #pass-strength-result {
+	width: 35%;
+}
+
+#buddypress #pass-strength-result.error,
+#buddypress #pass-strength-result.bad {
+	background-color: #ffb78c;
+	border-color: #ff853c !important;
+	display: block;
+}
+
+#buddypress #pass-strength-result.good {
+	background-color: #ffec8b;
+	border-color: #fc0 !important;
+	display: block;
+}
+
+#buddypress #pass-strength-result.short {
+	background-color: #ffa0a0;
+	border-color: #f04040 !important;
+	display: block;
+}
+
+#buddypress #pass-strength-result.strong {
+	background-color: #c3ff88;
+	border-color: #8dff1c !important;
+	display: block;
+}
+
+#buddypress .standard-form#signup_form div div.error {
+	background: #faa;
+	color: #a00;
+	margin: 0 0 10px;
+	padding: 6px;
+	width: 90%;
+}
+
+#buddypress div.accept,
+#buddypress div.reject {
+	float: right;
+	margin-right: 10px;
+}
+
+#buddypress ul.button-nav li {
+	float: right;
+	margin: 0 0 10px 10px;
+	list-style: none;
+}
+
+#buddypress ul.button-nav li.current a {
+	font-weight: 700;
+}
+
+#sitewide-notice #message {
+	right: 2%;
+	position: fixed;
+	top: 1em;
+	width: 96%;
+	z-index: 9999;
+}
+
+#sitewide-notice.admin-bar-on #message {
+	top: 3.3em;
+}
+
+#sitewide-notice strong {
+	display: block;
+	margin-bottom: -1em;
+}
+
+/*--------------------------------------------------------------
+3.5 - Forms
+--------------------------------------------------------------*/
+
+#buddypress form fieldset {
+	border: 0;
+	padding: 0;
+}
+
+#buddypress .standard-form textarea,
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="color"],
+#buddypress .standard-form input[type="date"],
+#buddypress .standard-form input[type="datetime"],
+#buddypress .standard-form input[type="datetime-local"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="month"],
+#buddypress .standard-form input[type="number"],
+#buddypress .standard-form input[type="range"],
+#buddypress .standard-form input[type="search"],
+#buddypress .standard-form input[type="tel"],
+#buddypress .standard-form input[type="time"],
+#buddypress .standard-form input[type="url"],
+#buddypress .standard-form input[type="week"],
+#buddypress .standard-form select,
+#buddypress .standard-form input[type="password"],
+#buddypress .dir-search input[type="search"],
+#buddypress .dir-search input[type="text"],
+#buddypress .groups-members-search input[type="search"],
+#buddypress .groups-members-search input[type="text"] {
+	border: 1px solid #ccc;
+	background: #fafafa;
+	border-radius: 0;
+	color: #737373;
+	font: inherit;
+	font-size: 100%;
+	padding: 6px;
+}
+
+#buddypress .standard-form select {
+	padding: 3px;
+}
+
+#buddypress .standard-form input[type="password"] {
+	margin-bottom: 5px;
+}
+
+#buddypress .standard-form label,
+#buddypress .standard-form span.label,
+#buddypress .standard-form legend {
+	display: block;
+	font-weight: 700;
+	margin: 15px 0 5px;
+	width: auto;
+}
+
+#buddypress label.xprofile-field-label {
+	display: inline;
+}
+
+#buddypress .standard-form p label,
+#buddypress .standard-form #invite-list label {
+	font-weight: 400;
+	margin: auto;
+}
+
+#buddypress .standard-form .checkbox label,
+#buddypress .standard-form .radio label {
+	color: #767676;
+	font-size: 100%;
+	font-weight: 400;
+	margin: 5px 0 0;
+}
+
+#buddypress .standard-form .checkbox label input,
+#buddypress .standard-form .radio label input {
+	margin-left: 3px;
+}
+
+#buddypress .standard-form#sidebar-login-form label {
+	margin-top: 5px;
+}
+
+#buddypress .standard-form input[type="text"] {
+	width: 75%;
+}
+
+#buddypress .standard-form#sidebar-login-form input[type="text"],
+#buddypress .standard-form#sidebar-login-form input[type="password"] {
+	padding: 4px;
+	width: 95%;
+}
+
+#buddypress .standard-form #basic-details-section input[type="password"],
+#buddypress .standard-form #blog-details-section input#signup_blog_url {
+	width: 35%;
+}
+
+#buddypress .standard-form#signup_form input[type="text"],
+#buddypress .standard-form#signup_form textarea,
+#buddypress .form-allowed-tags,
+#buddypress #commentform input[type="text"],
+#buddypress #commentform textarea {
+	width: 90%;
+}
+
+#buddypress .standard-form#signup_form div.submit {
+	float: left;
+}
+
+#buddypress div#signup-avatar img {
+	margin: 0 0 10px 15px;
+}
+
+#buddypress .standard-form textarea {
+	width: 75%;
+	height: 120px;
+}
+
+#buddypress .standard-form textarea#message_content {
+	height: 200px;
+}
+
+#buddypress .standard-form#send-reply textarea {
+	width: 97.5%;
+}
+
+#buddypress .standard-form p.description {
+	color: #767676;
+	font-size: 80%;
+	margin: 5px 0;
+}
+
+#buddypress .standard-form div.submit {
+	clear: both;
+	padding: 15px 0 0;
+}
+
+#buddypress .standard-form p.submit {
+	margin-bottom: 0;
+	padding: 15px 0 0;
+}
+
+#buddypress .standard-form div.submit input {
+	margin-left: 15px;
+}
+
+#buddypress .standard-form div.radio ul {
+	margin: 10px 38px 15px 0;
+	list-style: disc;
+}
+
+#buddypress .standard-form div.radio ul li {
+	margin-bottom: 5px;
+}
+
+#buddypress .standard-form a.clear-value {
+	display: block;
+	margin-top: 5px;
+	outline: none;
+}
+
+#buddypress .standard-form #basic-details-section,
+#buddypress .standard-form #blog-details-section,
+#buddypress .standard-form #profile-details-section {
+	float: right;
+	width: 48%;
+}
+
+#buddypress .standard-form #profile-details-section {
+	float: left;
+}
+
+#buddypress .standard-form #blog-details-section,
+#buddypress #notifications-bulk-management {
+	clear: right;
+}
+
+body.no-js #buddypress #notifications-bulk-management #select-all-notifications,
+body.no-js #buddypress label[for="message-type-select"],
+body.no-js #buddypress #message-type-select,
+body.no-js #buddypress #delete_inbox_messages,
+body.no-js #buddypress #delete_sentbox_messages,
+body.no-js #buddypress #messages-bulk-management #select-all-messages {
+	display: none;
+}
+
+#buddypress .standard-form input:focus,
+#buddypress .standard-form textarea:focus,
+#buddypress .standard-form select:focus {
+	background: #fafafa;
+	color: #555;
+}
+
+#buddypress form#send-invite-form {
+	margin-top: 20px;
+}
+
+#buddypress div#invite-list {
+	background: #f5f5f5;
+	height: 400px;
+	margin: 0 0 10px;
+	overflow: auto;
+	padding: 5px;
+	width: 160px;
+}
+
+#buddypress .standard-form button,
+#buddypress a.button,
+#buddypress input[type="submit"],
+#buddypress input[type="button"],
+#buddypress input[type="reset"],
+#buddypress ul.button-nav li a,
+#buddypress .generic-button a,
+#buddypress .comment-reply-link,
+a.bp-title-button {
+	background: #fff; /* Old browsers */
+	border: 1px solid #ccc;
+	color: #767676;
+	font-size: small;
+	cursor: pointer;
+	outline: none;
+	padding: 4px 10px;
+	text-align: center;
+	text-decoration: none;
+}
+
+#buddypress .standard-form button:hover,
+#buddypress a.button:hover,
+#buddypress a.button:focus,
+#buddypress input[type="submit"]:hover,
+#buddypress input[type="button"]:hover,
+#buddypress input[type="reset"]:hover,
+#buddypress ul.button-nav li a:hover,
+#buddypress ul.button-nav li.current a,
+#buddypress div.generic-button a:hover,
+#buddypress .comment-reply-link:hover {
+	background: #ededed;
+	border: 1px solid #bbb;
+	color: #555;
+	outline: none;
+	text-decoration: none;
+}
+
+#buddypress form.standard-form .left-menu {
+	float: right;
+}
+
+#buddypress form.standard-form .left-menu #invite-list ul {
+	margin: 1%;
+	list-style: none;
+}
+
+#buddypress form.standard-form .left-menu #invite-list ul li {
+	margin: 0 1% 0 0;
+}
+
+#buddypress form.standard-form .main-column {
+	margin-right: 190px;
+}
+
+#buddypress form.standard-form .main-column ul#friend-list {
+	clear: none;
+	float: right;
+}
+
+#buddypress form.standard-form .main-column ul#friend-list h3,
+#buddypress form.standard-form .main-column ul#friend-list h4 {
+	clear: none;
+}
+
+/* Overrides for embedded WP editors */
+#buddypress .wp-editor-wrap a.button,
+#buddypress .wp-editor-wrap button,
+#buddypress .wp-editor-wrap input[type="submit"],
+#buddypress .wp-editor-wrap input[type="button"],
+#buddypress .wp-editor-wrap input[type="reset"] {
+	padding: 0 8px 1px;
+}
+
+/* Form classes & generic attr styling */
+#buddypress form *[disabled="disabled"] {
+	cursor: default;
+	opacity: 0.4;
+}
+
+fieldset.register-site {
+	margin-top: 1em;
+}
+
+fieldset.create-site {
+	margin-bottom: 2em;
+}
+
+fieldset.create-site legend {
+	margin-bottom: 1em;
+}
+
+fieldset.create-site label {
+	margin-left: 3em;
+}
+
+.bp-screen-reader-text {
+	clip: rect(1px, 1px, 1px, 1px);
+	height: 1px;
+	overflow: hidden;
+	position: absolute !important;
+	width: 1px;
+	word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */
+}
+
+.bp-screen-reader-text:focus {
+	background-color: #f1f1f1;
+	border-radius: 3px;
+	box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
+	clip: auto !important;
+	color: #21759b;
+	display: block;
+	font-size: 14px;
+	font-size: 0.875rem;
+	font-weight: 700;
+	height: auto;
+	right: 5px;
+	line-height: normal;
+	padding: 15px 23px 14px;
+	text-decoration: none;
+	top: 5px;
+	width: auto;
+	z-index: 100000; /* Above WP toolbar. */
+}
+
+/*--------------------------------------------------------------
+3.6 - Ajax Loading
+--------------------------------------------------------------*/
+#buddypress a.loading,
+#buddypress input.loading {
+	-webkit-animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+	-moz-animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+	border-color: #aaa;
+}
+
+#buddypress a.loading:hover,
+#buddypress input.loading:hover {
+	color: #767676;
+}
+
+#buddypress input[type="submit"].pending,
+#buddypress input[type="button"].pending,
+#buddypress input[type="reset"].pending,
+#buddypress input[type="submit"].disabled,
+#buddypress input[type="button"].disabled,
+#buddypress input[type="reset"].disabled,
+#buddypress input[type="submit"][disabled="disabled"],
+#buddypress button.pending,
+#buddypress button.disabled,
+#buddypress div.pending a,
+#buddypress a.disabled {
+	border-color: #eee;
+	color: #bbb;
+	cursor: default;
+}
+
+#buddypress input[type="submit"]:hover.pending,
+#buddypress input[type="button"]:hover.pending,
+#buddypress input[type="reset"]:hover.pending,
+#buddypress input[type="submit"]:hover.disabled,
+#buddypress input[type="button"]:hover.disabled,
+#buddypress input[type="reset"]:hover.disabled,
+#buddypress button.pending:hover,
+#buddypress button.disabled:hover,
+#buddypress div.pending a:hover,
+#buddypress a.disabled:hover {
+	border-color: #eee;
+	color: #bbb;
+}
+
+/*--------------------------------------------------------------
+3.7 - Forums, Tables and Topics
+--------------------------------------------------------------*/
+#buddypress ul#topic-post-list {
+	margin: 0;
+	width: auto;
+}
+
+#buddypress ul#topic-post-list li {
+	padding: 15px;
+	position: relative;
+}
+
+#buddypress ul#topic-post-list li.alt {
+	background: #f5f5f5;
+}
+
+#buddypress ul#topic-post-list li div.poster-meta {
+	color: #767676;
+	margin-bottom: 10px;
+}
+
+#buddypress ul#topic-post-list li div.post-content {
+	margin-right: 54px;
+}
+
+#buddypress div.topic-tags {
+	font-size: 80%;
+}
+
+#buddypress div.admin-links {
+	color: #767676;
+	font-size: 80%;
+	position: absolute;
+	top: 15px;
+	left: 25px;
+}
+
+#buddypress div#topic-meta {
+	margin: 0;
+	padding: 5px 19px 30px;
+	position: relative;
+}
+
+#buddypress div#topic-meta div.admin-links {
+	left: 19px;
+	top: -36px;
+}
+
+#buddypress div#topic-meta h3 {
+	margin: 5px 0;
+}
+
+#buddypress div#new-topic-post {
+	display: none;
+	margin: 20px 0 0;
+	padding: 1px 0 0;
+}
+
+#buddypress table.notifications,
+#buddypress table.notifications-settings,
+#buddypress table.profile-settings,
+#buddypress table.profile-fields,
+#buddypress table.wp-profile-fields,
+#buddypress table.messages-notices,
+#buddypress table.forum {
+	width: 100%;
+}
+
+#buddypress table.notifications thead tr,
+#buddypress table.notifications-settings thead tr,
+#buddypress table.profile-settings thead tr,
+#buddypress table.profile-fields thead tr,
+#buddypress table.wp-profile-fields thead tr,
+#buddypress table.messages-notices thead tr,
+#buddypress table.forum thead tr {
+	background: #eaeaea;
+}
+
+#buddypress table#message-threads {
+	clear: both;
+}
+
+#buddypress table.profile-fields {
+	margin-bottom: 20px;
+}
+
+#buddypress table.profile-fields:last-child {
+	margin-bottom: 0;
+}
+
+#buddypress table.profile-fields p {
+	margin: 0;
+}
+
+#buddypress table.profile-fields p:last-child {
+	margin-top: 0;
+}
+
+#buddypress table.notifications tr td,
+#buddypress table.notifications-settings tr td,
+#buddypress table.profile-settings tr td,
+#buddypress table.profile-fields tr td,
+#buddypress table.wp-profile-fields tr td,
+#buddypress table.messages-notices tr td,
+#buddypress table.forum tr td,
+#buddypress table.notifications tr th,
+#buddypress table.notifications-settings tr th,
+#buddypress table.profile-fields tr th,
+#buddypress table.wp-profile-fields tr th,
+#buddypress table.messages-notices tr th,
+#buddypress table.forum tr th {
+	padding: 8px;
+	vertical-align: middle;
+}
+
+#buddypress table.notifications tr td.label,
+#buddypress table.notifications-settings tr td.label,
+#buddypress table.profile-fields tr td.label,
+#buddypress table.wp-profile-fields tr td.label,
+#buddypress table.messages-notices tr td.label,
+#buddypress table.forum tr td.label {
+	border-left: 1px solid #eaeaea;
+	font-weight: 700;
+	width: 25%;
+}
+
+#buddypress #message-threads .thread-info {
+	min-width: 40%;
+}
+
+#buddypress table tr td.thread-info p {
+	margin: 0;
+}
+
+#buddypress table tr td.thread-info p.thread-excerpt {
+	color: #767676;
+	font-size: 80%;
+	margin-top: 3px;
+}
+
+#buddypress table.forum td {
+	text-align: center;
+}
+
+#buddypress table.notifications tr.alt td,
+#buddypress table.notifications-settings tr.alt td,
+#buddypress table.profile-settings tr.alt td,
+#buddypress table.profile-fields tr.alt td,
+#buddypress table.wp-profile-fields tr.alt td,
+#buddypress table.messages-notices tr.alt td,
+#buddypress table.forum tr.alt td {
+	background: #f5f5f5;
+	color: #707070;
+}
+
+#buddypress table.notification-settings {
+	margin-bottom: 20px;
+	text-align: right;
+}
+
+#buddypress #groups-notification-settings {
+	margin-bottom: 0;
+}
+
+#buddypress table.notifications th.icon,
+#buddypress table.notifications td:first-child,
+#buddypress table.notification-settings th.icon,
+#buddypress table.notification-settings td:first-child {
+	display: none;
+}
+
+#buddypress table.notification-settings th.title,
+#buddypress table.profile-settings th.title {
+	width: 80%;
+}
+
+#buddypress table.notification-settings .yes,
+#buddypress table.notification-settings .no {
+	text-align: center;
+	width: 40px;
+}
+
+#buddypress table.forum {
+	margin: 0;
+	width: auto;
+	clear: both;
+}
+
+#buddypress table.forum tr.sticky td {
+	font-size: 110%;
+	background: #fff9db;
+	border-top: 1px solid #ffe8c4;
+	border-bottom: 1px solid #ffe8c4;
+}
+
+#buddypress table.forum tr.closed td.td-title {
+	padding-right: 35px;
+}
+
+#buddypress table.forum td p.topic-text {
+	color: #767676;
+	font-size: 100%;
+}
+
+#buddypress table.forum tr > td:first-child,
+#buddypress table.forum tr > th:first-child {
+	padding-right: 15px;
+}
+
+#buddypress table.forum tr > td:last-child,
+#buddypress table.forum tr > th:last-child {
+	padding-left: 15px;
+}
+
+#buddypress table.forum tr th#th-title,
+#buddypress table.forum tr th#th-poster,
+#buddypress table.forum tr th#th-group,
+#buddypress table.forum td.td-poster,
+#buddypress table.forum td.td-group,
+#buddypress table.forum td.td-title {
+	text-align: right;
+}
+
+#buddypress table.forum tr td.td-title a.topic-title {
+	font-size: 110%;
+}
+
+#buddypress table.forum td.td-freshness {
+	white-space: nowrap;
+}
+
+#buddypress table.forum td.td-freshness span.time-since {
+	font-size: 80%;
+	color: #767676;
+}
+
+#buddypress table.forum td img.avatar {
+	float: none;
+	margin: 0 0 -8px 5px;
+}
+
+#buddypress table.forum td.td-poster,
+#buddypress table.forum td.td-group {
+	min-width: 140px;
+}
+
+#buddypress table.forum th#th-title {
+	width: 80%;
+}
+
+#buddypress table.forum th#th-freshness {
+	width: 25%;
+}
+
+#buddypress table.forum th#th-postcount {
+	width: 15%;
+}
+
+#buddypress table.forum p.topic-meta {
+	font-size: 80%;
+	margin: 5px 0 0;
+}
+
+/*-------------------------------------------------------------------------
+3.8 - Headers, Lists and Tabs - Activity, Groups, Blogs, Forums, Profiles
+-------------------------------------------------------------------------*/
+#buddypress .item-body {
+	margin: 20px 0;
+}
+
+#buddypress span.activity {
+	display: inline-block;
+	font-size: small;
+	padding: 0;
+}
+
+#buddypress span.user-nicename {
+	color: #767676;
+	display: inline-block;
+	font-size: 120%;
+	font-weight: 700;
+}
+
+#buddypress div#message p,
+#sitewide-notice p {
+	background-color: #ffd;
+	border: 1px solid #cb2;
+	color: #440;
+	font-weight: 400;
+	margin-top: 3px;
+	text-decoration: none;
+}
+
+.bp-site-wide-message #message p,
+.admin-bar-on #message p {
+	padding-left: 25px;
+	position: relative;
+}
+
+.bp-site-wide-message #message button,
+.admin-bar-on #message button {
+	font-size: 0.8em;
+	padding: 2px 4px;
+	position: absolute;
+	left: 0;
+	top: 0;
+}
+
+.admin-bar-on #message button {
+	left: 10px;
+	top: 7px;
+}
+
+#buddypress #item-header:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+#buddypress div#item-header div#item-header-content {
+	float: right;
+	margin-right: 0;
+}
+
+#buddypress div#item-header h2 {
+	line-height: 1.2;
+	margin: 0 0 15px;
+}
+
+#buddypress div#item-header h2 a {
+	color: #767676;
+	text-decoration: none;
+}
+
+#buddypress div#item-header img.avatar {
+	float: right;
+	margin: 0 0 19px 15px;
+}
+
+#buddypress div#item-header h2 {
+	margin-bottom: 5px;
+}
+
+#buddypress div#item-header h2 span.highlight {
+	font-size: 60%;
+	font-weight: 400;
+	line-height: 1.7;
+	vertical-align: middle;
+	display: inline-block;
+}
+
+#buddypress div#item-header h2 span.highlight span {
+	background: #a1dcfa;
+	color: #fff;
+	cursor: pointer;
+	font-weight: 700;
+	font-size: 80%;
+	margin-bottom: 2px;
+	padding: 1px 4px;
+	position: relative;
+	left: -2px;
+	top: -2px;
+	vertical-align: middle;
+}
+
+#buddypress div#item-header div#item-meta {
+	font-size: 80%;
+	color: #767676;
+	overflow: hidden;
+	margin: 15px 0 5px;
+	padding-bottom: 10px;
+}
+
+#buddypress div#item-header div#item-actions {
+	float: left;
+	margin: 0 15px 15px 0;
+	text-align: left;
+	width: 20%;
+}
+
+#buddypress div#item-header div#item-actions h2,
+#buddypress div#item-header div#item-actions h3 {
+	margin: 0 0 5px;
+}
+
+#buddypress div#item-header div#item-actions a {
+	display: inline-block;
+}
+
+#buddypress div#item-header ul {
+	margin-bottom: 15px;
+}
+
+#buddypress div#item-header ul:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+#buddypress div#item-header ul h5,
+#buddypress div#item-header ul span,
+#buddypress div#item-header ul hr {
+	display: none;
+}
+
+#buddypress div#item-header ul li {
+	float: left;
+	list-style: none;
+}
+
+#buddypress div#item-header ul img.avatar,
+#buddypress div#item-header ul.avatars img.avatar {
+	height: 30px;
+	margin: 2px;
+	width: 30px;
+}
+
+#buddypress div#item-header div.generic-button,
+#buddypress div#item-header a.button {
+	float: right;
+	margin: 10px 0 0 10px;
+}
+
+body.no-js #buddypress div#item-header .js-self-profile-button {
+	display: none;
+}
+
+#buddypress div#item-header div#message.info {
+	line-height: 0.8;
+}
+
+#buddypress ul.item-list {
+	border-top: 1px solid #eaeaea;
+	width: 100%;
+	list-style: none;
+	clear: both;
+	margin: 0;
+	padding: 0;
+}
+
+body.activity-permalink #buddypress ul.item-list,
+body.activity-permalink #buddypress ul.item-list li.activity-item {
+	border: none;
+}
+
+#buddypress ul.item-list li {
+	border-bottom: 1px solid #eaeaea;
+	padding: 15px 0;
+	margin: 0;
+	position: relative;
+	list-style: none;
+}
+
+#buddypress ul.single-line li {
+	border: none;
+}
+
+#buddypress ul.item-list li img.avatar {
+	float: right;
+	margin: 0 0 0 10px;
+}
+
+#buddypress ul.item-list li div.item-title,
+#buddypress ul.item-list li h3,
+#buddypress ul.item-list li h4 {
+	font-weight: 400;
+	font-size: 90%;
+	margin: 0;
+	width: 75%;
+}
+
+#buddypress ul.item-list li div.item-title span {
+	color: #767676;
+	font-size: 80%;
+}
+
+#buddypress ul.item-list li div.item-desc {
+	color: #767676;
+	font-size: 80%;
+	margin: 10px 60px 0 0;
+	width: 50%;
+}
+
+#buddypress ul.item-list li.group-no-avatar div.item-desc {
+	margin-right: 0;
+}
+
+#buddypress ul.item-list li div.action {
+	position: absolute;
+	top: 15px;
+	left: 0;
+	text-align: left;
+}
+
+#buddypress ul.item-list li div.meta {
+	color: #767676;
+	font-size: 80%;
+	margin-top: 10px;
+}
+
+#buddypress ul.item-list li h5 span.small {
+	float: left;
+	font-size: 80%;
+	font-weight: 400;
+}
+
+#buddypress div.item-list-tabs {
+	background: transparent;
+	clear: right;
+	overflow: hidden;
+}
+
+#buddypress div.item-list-tabs ul {
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li {
+	float: right;
+	margin: 0;
+	list-style: none;
+}
+
+#buddypress div.item-list-tabs#subnav ul li {
+	margin-top: 0;
+}
+
+#buddypress div.item-list-tabs ul li.last {
+	float: left;
+	margin: 7px 0 0;
+}
+
+#buddypress div.item-list-tabs#subnav ul li.last {
+	margin-top: 4px;
+}
+
+#buddypress div.item-list-tabs ul li.last select {
+	max-width: 185px;
+}
+
+#buddypress div.item-list-tabs ul li a,
+#buddypress div.item-list-tabs ul li span {
+	display: block;
+	padding: 5px 10px;
+	text-decoration: none;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	background: #eee;
+	border-radius: 50%;
+	border: 1px solid #ccc;
+	color: #6c6c6c;
+	display: inline;
+	font-size: 70%;
+	margin-right: 2px;
+	padding: 3px 6px;
+	text-align: center;
+	vertical-align: middle;
+}
+
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background-color: #eee;
+	color: #555;
+	opacity: 0.9;
+	font-weight: 700;
+}
+
+#buddypress div.item-list-tabs ul li.selected a span,
+#buddypress div.item-list-tabs ul li.current a span,
+#buddypress div.item-list-tabs ul li a:hover span {
+	background-color: #eee;
+}
+
+#buddypress div.item-list-tabs ul li.selected a span,
+#buddypress div.item-list-tabs ul li.current a span {
+	background-color: #fff;
+}
+
+#buddypress div#item-nav ul li.loading a {
+	background-position: 12% 50%;
+}
+
+#buddypress div.item-list-tabs#object-nav {
+	margin-top: 0;
+}
+
+#buddypress div.item-list-tabs#subnav {
+	background: transparent;
+	margin: 10px 0;
+	overflow: hidden;
+}
+
+#buddypress #admins-list li,
+#buddypress #mods-list li,
+#buddypress #members-list li {
+	overflow: auto;
+	list-style: none;
+}
+
+#buddypress .group-members-list {
+	width: 100%;
+	margin-top: 1em;
+	clear: both;
+	overflow: auto;
+}
+
+#buddypress #item-buttons:empty {
+	display: none;
+}
+
+/*--------------------------------------------------------------
+3.8.1 - Cover Image
+--------------------------------------------------------------*/
+
+#buddypress #cover-image-container {
+	position: relative;
+	z-index: 0;
+}
+
+#buddypress #header-cover-image {
+	background-color: #c5c5c5;
+	background-position: center top;
+	background-repeat: no-repeat;
+	background-size: cover;
+	border: 0;
+	display: block;
+	right: 0;
+	margin: 0;
+	padding: 0;
+	position: absolute;
+	top: 0;
+	width: 100%;
+	z-index: 1;
+}
+
+#buddypress #item-header-cover-image {
+	padding: 0 1em;
+	position: relative;
+	z-index: 2;
+}
+
+/*--------------------------------------------------------------
+3.9 - Private Messaging Threads
+--------------------------------------------------------------*/
+#buddypress table#message-threads tr.unread td {
+	background: #fff9db;
+	border-top: 1px solid #ffe8c4;
+	border-bottom: 1px solid #ffe8c4;
+	font-weight: 700;
+}
+
+#buddypress table#message-threads tr.unread td .thread-excerpt,
+#buddypress table#message-threads tr.unread td .activity,
+#buddypress table#message-threads tr.unread td.thread-options {
+	font-weight: 400;
+}
+
+#buddypress li span.unread-count,
+#buddypress tr.unread span.unread-count {
+	background: #d00;
+	color: #fff;
+	font-weight: 700;
+	padding: 2px 8px;
+}
+
+#buddypress div.item-list-tabs ul li a span.unread-count {
+	padding: 1px 6px;
+	color: #fff;
+}
+
+#buddypress div#message-thread div.message-box {
+	margin: 0;
+	padding: 15px;
+}
+
+#buddypress div#message-thread div.alt {
+	background: #f4f4f4;
+}
+
+#buddypress div#message-thread p#message-recipients {
+	margin: 10px 0 20px;
+}
+
+#buddypress div#message-thread img.avatar {
+	float: right;
+	margin: 0 0 0 10px;
+	vertical-align: middle;
+}
+
+#buddypress div#message-thread strong {
+	font-size: 100%;
+	margin: 0;
+}
+
+#buddypress div#message-thread strong a {
+	text-decoration: none;
+}
+
+#buddypress div#message-thread strong span.activity {
+	margin-top: 4px;
+}
+
+#buddypress div#message-thread div.message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+#buddypress div#message-thread div.message-content {
+	margin-right: 45px;
+}
+
+#buddypress div#message-thread div.message-options {
+	text-align: left;
+}
+
+#buddypress #message-threads img.avatar {
+	max-width: none;
+}
+
+#buddypress div.message-search {
+	float: left;
+	margin: 0 20px;
+}
+
+.message-metadata {
+	position: relative;
+}
+
+.message-star-actions {
+	position: absolute;
+	left: 0;
+	top: 0;
+}
+
+#buddypress a.message-action-star,
+#buddypress a.message-action-unstar {
+	border-bottom: 0;
+	text-decoration: none;
+	outline: none;
+}
+
+a.message-action-star {
+	opacity: 0.7;
+}
+
+a.message-action-star:hover {
+	opacity: 1;
+}
+
+.message-action-star span.icon:before,
+.message-action-unstar span.icon:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.message-action-star span.icon:before {
+	color: #767676;
+	content: "\f154";
+}
+
+.message-action-unstar span.icon:before {
+	color: #fcdd77;
+	content: "\f155";
+}
+
+/*--------------------------------------------------------------
+3.10 - Extended Profiles
+--------------------------------------------------------------*/
+
+#buddypress div.profile h2 {
+	margin-bottom: auto;
+	margin-top: 15px;
+}
+
+#buddypress #profile-edit-form ul.button-nav {
+	margin-top: 15px;
+}
+
+body.no-js #buddypress .field-visibility-settings-toggle,
+body.no-js #buddypress .field-visibility-settings-close {
+	display: none;
+}
+
+#buddypress .field-visibility-settings {
+	display: none;
+	margin-top: 10px;
+}
+
+body.no-js #buddypress .field-visibility-settings {
+	display: block;
+}
+
+#buddypress .current-visibility-level {
+	font-weight: 700;
+	font-style: normal;
+}
+
+#buddypress .field-visibility-settings,
+#buddypress .field-visibility-settings-toggle,
+#buddypress .field-visibility-settings-notoggle {
+	color: #707070;
+}
+
+#buddypress .field-visibility-settings-toggle a,
+#buddypress .field-visibility-settings a {
+	font-size: 80%;
+}
+
+body.register #buddypress div.page ul {
+	list-style: none;
+}
+
+#buddypress .standard-form .field-visibility-settings label {
+	margin: 0;
+	font-weight: 400;
+}
+
+#buddypress .field-visibility-settings legend,
+#buddypress .field-visibility-settings-toggle {
+	font-style: italic;
+}
+
+#buddypress .field-visibility-settings .radio {
+	list-style: none;
+	margin-bottom: 0;
+}
+
+#buddypress .field-visibility select {
+	margin: 0;
+}
+
+#buddypress .wp-editor-container {
+	border: 1px solid #dedede;
+}
+
+#buddypress .html-active button.switch-html {
+	border-bottom-color: transparent;
+	border-bottom-right-radius: 0;
+	border-bottom-left-radius: 0;
+	background: #f5f5f5;
+	color: #707070;
+}
+
+#buddypress .tmce-active button.switch-tmce {
+	border-bottom-color: transparent;
+	border-bottom-right-radius: 0;
+	border-bottom-left-radius: 0;
+	background: #f5f5f5;
+	color: #707070;
+}
+
+#buddypress .standard-form .wp-editor-container textarea {
+	width: 100%;
+	padding-top: 0;
+	padding-bottom: 0;
+}
+
+/*--------------------------------------------------------------
+3.11 - Widgets
+--------------------------------------------------------------*/
+
+.widget.buddypress span.activity {
+	display: inline-block;
+	font-size: small;
+	padding: 0;
+}
+
+.widget.buddypress div.item-options {
+	font-size: 90%;
+	margin: 0 0 1em;
+	padding: 1em 0;
+}
+
+.widget.buddypress div.item {
+	margin: 0 0 1em;
+}
+
+.widget.buddypress div.item-meta,
+.widget.buddypress div.item-content {
+	font-size: 11px;
+	margin-right: 50px;
+}
+
+.widget.buddypress div.avatar-block:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.widget.buddypress .item-avatar a {
+	float: right;
+	margin-bottom: 15px;
+	margin-left: 10px;
+}
+
+.widget.buddypress div.item-avatar img {
+	display: inline-block;
+	height: 40px;
+	margin: 1px;
+	width: 40px;
+}
+
+.widget.buddypress .item-avatar a,
+.widget.buddypress .item-avatar a:hover,
+.widget.buddypress .item-avatar a:focus,
+.widget.buddypress .item-avatar a:active,
+.widget.buddypress .item-avatar a img {
+	box-shadow: none;
+}
+
+.widget.buddypress #bp-login-widget-form label {
+	display: block;
+	margin: 1rem 0 0.5rem;
+}
+
+.widget.buddypress #bp-login-widget-form #bp-login-widget-submit {
+	margin-left: 10px;
+}
+
+.widget.buddypress .bp-login-widget-user-avatar {
+	float: right;
+}
+
+.bp-login-widget-user-avatar img.avatar {
+	height: 40px;
+	width: 40px;
+}
+
+.widget.buddypress .bp-login-widget-user-links > div {
+	padding-right: 60px;
+}
+
+.widget.buddypress .bp-login-widget-user-links > div {
+	margin-bottom: 0.5rem;
+}
+
+.widget.buddypress .bp-login-widget-user-links > div.bp-login-widget-user-link a {
+	font-weight: 700;
+}
+
+.widget.buddypress #groups-list,
+.widget.buddypress #members-list,
+.widget.buddypress #friends-list {
+	margin-right: 0;
+	padding-right: 0;
+}
+
+.widget.buddypress #groups-list li,
+.widget.buddypress #members-list li,
+.widget.buddypress #friends-list li {
+	clear: both;
+	list-style-type: none;
+}
+
+/*--------------------------------------------------------------
+3.12 - Tooltips
+--------------------------------------------------------------*/
+
+.buddypress .bp-tooltip {
+	position: relative;
+}
+
+.bp-tooltip:after {
+	background: #fff;
+	border: 1px solid #aaa;
+	border-collapse: separate;
+	border-radius: 1px;
+	box-shadow: -1px 1px 0 1px rgba(132, 132, 132, 0.3);
+	color: #000;
+	content: attr(data-bp-tooltip);
+	display: none;
+	font-family: sans-serif;
+	font-size: 11px;
+	font-weight: 400;
+	letter-spacing: normal;
+	line-height: 1.5;
+	margin-top: 10px;
+	max-width: 240px;
+	opacity: 0;
+	padding: 3px 6px;
+	position: absolute;
+	left: 50%;
+	text-align: center;
+	text-decoration: none;
+	text-shadow: none;
+	text-transform: none;
+	top: 100%;
+	transform: translateX(-50%);
+	transition: opacity 2s ease-out;
+	white-space: pre;
+	word-wrap: break-word;
+	z-index: 998;
+}
+
+.bp-tooltip:hover:after,
+.bp-tooltip:active:after,
+.bp-tooltip:focus:after {
+	display: inline-block;
+	opacity: 1;
+	overflow: visible;
+	text-decoration: none;
+	z-index: 999;
+}
+
+#group-admins .bp-tooltip:after,
+#group-mods .bp-tooltip:after,
+.message-metadata .bp-tooltip:after {
+	left: 0;
+	text-align: left;
+	transform: translateX(0);
+}
+
+.feed .bp-tooltip:after,
+.item-list .bp-tooltip:after,
+.messages-notices .bp-tooltip:after {
+	right: 0;
+	left: auto;
+	text-align: right;
+	transform: translateX(0);
+}
+
+.bp-site-wide-message .bp-tooltip:after,
+.admin-bar-on .bp-tooltip:after {
+	left: 50px;
+}
+
+/*--------------------------------------------------------------
+4.0 - Media Queries
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+4.1 - Smartphones - landscape
+--------------------------------------------------------------*/
+@media only screen and (max-width: 480px ) {
+
+	#buddypress div.dir-search {
+		float: left;
+		margin-top: -50px;
+		text-align: left;
+	}
+
+	#buddypress div.dir-search input[type="text"] {
+		margin-bottom: 1em;
+		width: 50%;
+	}
+
+	a.bp-title-button {
+		margin-right: 10px;
+	}
+
+	#buddypress form.standard-form .main-column div.action {
+		position: relative;
+		margin-bottom: 1em;
+	}
+
+	#buddypress form.standard-form .main-column ul#friend-list h3,
+	#buddypress form.standard-form .main-column ul#friend-list h4 {
+		width: 100%;
+	}
+}
+
+/*--------------------------------------------------------------
+4.2 - Smartphones - portrait
+--------------------------------------------------------------*/
+@media only screen and (max-width: 320px) {
+
+	#buddypress div.dir-search {
+		clear: right;
+		float: right;
+		margin-top: 0;
+		text-align: right;
+	}
+
+	#buddypress li#groups-order-select {
+		clear: right;
+		float: right;
+	}
+
+	#buddypress ul.item-list li div.action {
+		clear: right;
+		float: right;
+		margin-top: 0;
+		margin-right: 70px;
+		position: relative;
+		top: 0;
+		left: 0;
+		text-align: right;
+	}
+
+	#buddypress ul.item-list li div.item-desc {
+		clear: right;
+		float: right;
+		margin: 10px 0 0;
+		width: auto;
+	}
+
+	#buddypress li div.item {
+		margin-right: 70px;
+		width: auto;
+	}
+
+	#buddypress ul.item-list li div.meta {
+		margin-top: 0;
+	}
+
+	#buddypress .item-desc p {
+		margin: 0 0 10px;
+	}
+
+	#buddypress div.pagination .pag-count {
+		margin-right: 0;
+	}
+}
+
+/*--------------------------------------------------------------
+4.2 - Smartphones - smaller screen sizes
+--------------------------------------------------------------*/
+@media only screen and (max-width: 240px) {
+
+	#buddypress div.dir-search {
+		float: right;
+		margin: 0;
+	}
+
+	#buddypress div.dir-search input[type="text"] {
+		width: 50%;
+	}
+
+	#buddypress li#groups-order-select {
+		float: right;
+	}
+
+	#buddypress ul.item-list li img.avatar {
+		width: 30px;
+		height: auto;
+	}
+
+	#buddypress ul.item-list li div.action,
+	#buddypress li div.item {
+		margin-right: 45px;
+	}
+
+	h1 a.bp-title-button {
+		clear: right;
+		float: right;
+		margin: 10px 0 20px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/buddypress.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/buddypress.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/buddypress.css	(revision 12265)
@@ -0,0 +1,2261 @@
+/*--------------------------------------------------------------
+Hello, this is the BuddyPress Legacy stylesheet.
+
+@version 3.0.0
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 - Navigation
+	1.1 - Pagination
+2.0 - WordPress
+	2.1 - Images
+3.0 - BuddyPress
+	3.1 - Activity
+		3.1.1 - Activity Listing
+		3.1.2 - Activity Comments
+	3.2 - Toolbar
+	3.3 - Directories - Members, Groups, Blogs, Forums
+	3.4 - Error / Success Messages
+	3.5 - Forms
+	3.6 - Ajax Loading
+	3.7 - Topics and Tables - Forums and General
+	3.8 - Headers, Lists and Tabs - Activity, Groups, Blogs, Forums
+		3.8.1 - Cover Image
+	3.9 - Private Messaging Threads
+	3.10 - Extended Profiles
+	3.11 - Widgets
+	3.12 - Tooltips
+4.0 - Media Queries
+	4.1 - Smartphones Landscape
+	4.2 - Smartphones Portrait
+	4.3 - Smartphones - smaller screen sizes
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+1 - Navigation
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+1.1 - Pagination
+--------------------------------------------------------------*/
+#buddypress div.pagination {
+	background: transparent;
+	border: none;
+	color: #767676;
+	font-size: small;
+	margin: 0;
+	position: relative;
+	display: block;
+	float: left;
+	width: 100%;
+	padding: 10px 0;
+}
+
+#buddypress div.pagination .pag-count {
+	float: left;
+	margin-left: 10px;
+}
+
+#buddypress div.pagination .pagination-links {
+	float: right;
+	margin-right: 10px;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	font-size: 90%;
+	padding: 0 5px;
+}
+
+#buddypress div.pagination .pagination-links a:hover {
+	font-weight: 700;
+}
+
+#buddypress noscript div.pagination {
+	margin-bottom: 15px;
+}
+
+#buddypress #nav-above {
+	display: none;
+}
+
+#buddypress .paged #nav-above {
+	display: block;
+}
+
+/*--------------------------------------------------------------
+2 - WordPress
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+2.1 - Images
+--------------------------------------------------------------*/
+#buddypress img.wp-smiley {
+	border: none !important;
+	clear: none !important;
+	float: none !important;
+	margin: 0 !important;
+	padding: 0 !important;
+}
+
+#buddypress .clear {
+	clear: left;
+}
+
+/*--------------------------------------------------------------
+3.0 - BuddyPress
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+3.1 - Activity
+--------------------------------------------------------------*/
+#buddypress #activity-stream {
+	margin-top: -5px;
+}
+
+#buddypress #activity-stream p {
+	margin: 5px 0;
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .home-page form#whats-new-form {
+	border-bottom: none;
+	padding-bottom: 0;
+}
+
+#buddypress form#whats-new-form #whats-new-avatar {
+	float: left;
+}
+
+#buddypress form#whats-new-form #whats-new-content {
+	margin-left: 55px;
+	padding: 0 0 20px 20px;
+}
+
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 0.5;
+	margin-bottom: 15px;
+	margin-left: 75px;
+}
+
+#buddypress form#whats-new-form textarea {
+	background: #fff;
+	box-sizing: border-box;
+	color: #555;
+	font-family: inherit;
+	font-size: medium;
+	height: 2.2em;
+	line-height: 1.4;
+	padding: 6px;
+	width: 100%;
+}
+
+body.no-js #buddypress form#whats-new-form textarea {
+	height: 50px;
+}
+
+#buddypress form#whats-new-form #whats-new-options select {
+	max-width: 200px;
+	margin-top: 12px;
+}
+
+#buddypress form#whats-new-form #whats-new-submit {
+	float: right;
+	margin-top: 12px;
+}
+
+#buddypress #whats-new-options:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+body.no-js #buddypress #whats-new-options {
+	height: auto;
+}
+
+#buddypress #whats-new:focus {
+	border-color: rgba(31, 179, 221, 0.9) !important;
+	outline-color: rgba(31, 179, 221, 0.9);
+}
+
+/*--------------------------------------------------------------
+3.1.1 - Activity Listing
+--------------------------------------------------------------*/
+#buddypress ul.activity-list li {
+	overflow: hidden;
+	padding: 15px 0 0;
+	list-style: none;
+}
+
+#buddypress .activity-list .activity-avatar {
+	float: left;
+}
+
+#buddypress ul.item-list.activity-list li.has-comments {
+	padding-bottom: 15px;
+}
+
+body.activity-permalink #buddypress ul.activity-list li.has-comments {
+	padding-bottom: 0;
+}
+
+#buddypress .activity-list li.mini {
+	font-size: 80%;
+	position: relative;
+}
+
+#buddypress .activity-list li.mini .activity-avatar img.avatar,
+#buddypress .activity-list li.mini .activity-avatar img.FB_profile_pic { /* stylelint-disable-line selector-class-pattern */
+	height: 20px;
+	margin-left: 30px;
+	width: 20px;
+}
+
+#buddypress .activity-permalink .activity-list li.mini .activity-avatar img.avatar,
+#buddypress .activity-permalink .activity-list li.mini .activity-avatar img.FB_profile_pic { /* stylelint-disable-line selector-class-pattern */
+	height: auto;
+	margin-left: 0;
+	width: auto;
+}
+
+body.activity-permalink #buddypress .activity-list > li:first-child {
+	padding-top: 0;
+}
+
+#buddypress .activity-list li .activity-content {
+	position: relative;
+}
+
+#buddypress .activity-list li.mini .activity-content p {
+	margin: 0;
+}
+
+#buddypress .activity-list li.mini .activity-comments {
+	clear: both;
+	font-size: 120%;
+}
+
+body.activity-permalink #buddypress li.mini .activity-meta {
+	margin-top: 4px;
+}
+
+#buddypress .activity-list li .activity-inreplyto {
+	color: #767676;
+	font-size: 80%;
+}
+
+#buddypress .activity-list li .activity-inreplyto > p {
+	margin: 0;
+	display: inline;
+}
+
+#buddypress .activity-list li .activity-inreplyto blockquote,
+#buddypress .activity-list li .activity-inreplyto div.activity-inner {
+	background: none;
+	border: none;
+	display: inline;
+	margin: 0;
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress .activity-list .activity-content {
+	margin: 0 0 0 70px;
+}
+
+body.activity-permalink #buddypress .activity-list li .activity-content {
+	border: none;
+	font-size: 100%;
+	line-height: 1.5;
+	margin-left: 170px;
+	margin-right: 0;
+	padding: 0;
+}
+
+body.activity-permalink #buddypress .activity-list li .activity-header > p {
+	margin: 0;
+	padding: 5px 0 0;
+}
+
+#buddypress .activity-list .activity-content .activity-header,
+#buddypress .activity-list .activity-content .comment-header {
+	color: #767676;
+	line-height: 2.2;
+}
+
+#buddypress .activity-header {
+	margin-right: 20px;
+}
+
+#buddypress .activity-header a,
+#buddypress .comment-meta a,
+#buddypress .acomment-meta a {
+	text-decoration: none;
+}
+
+#buddypress .activity-list .activity-content .activity-header img.avatar {
+	float: none !important;
+	margin: 0 5px -8px 0 !important;
+}
+
+#buddypress a.bp-secondary-action,
+#buddypress span.highlight {
+	font-size: 80%;
+	padding: 0;
+	margin-right: 5px;
+	text-decoration: none;
+}
+
+#buddypress .activity-list .activity-content .activity-inner,
+#buddypress .activity-list .activity-content blockquote {
+	margin: 10px 10px 5px 0;
+	overflow: hidden;
+}
+
+#buddypress .activity-list li.new_forum_post .activity-content .activity-inner,
+#buddypress .activity-list li.new_forum_topic .activity-content .activity-inner {
+	border-left: 2px solid #eaeaea;
+	margin-left: 5px;
+	padding-left: 10px;
+}
+
+body.activity-permalink #buddypress .activity-content .activity-inner,
+body.activity-permalink #buddypress .activity-content blockquote {
+	margin-left: 0;
+	margin-top: 5px;
+}
+
+#buddypress .activity-inner > p {
+	word-wrap: break-word;
+}
+
+#buddypress .activity-inner > .activity-inner {
+	margin: 0;
+}
+
+#buddypress .activity-inner > blockquote {
+	margin: 0;
+}
+
+#buddypress .activity-list .activity-content img.thumbnail {
+	border: 2px solid #eee;
+	float: left;
+	margin: 0 10px 5px 0;
+}
+
+#buddypress .activity-read-more {
+	margin-left: 1em;
+	white-space: nowrap;
+}
+
+#buddypress .activity-list li.load-more,
+#buddypress .activity-list li.load-newest {
+	background: #f0f0f0;
+	font-size: 110%;
+	margin: 15px 0;
+	padding: 10px 15px;
+	text-align: center;
+}
+
+#buddypress .activity-list li.load-more a,
+#buddypress .activity-list li.load-newest a {
+	color: #4d4d4d;
+}
+
+
+/*--------------------------------------------------------------
+3.1.2 - Activity Comments
+--------------------------------------------------------------*/
+#buddypress div.activity-meta {
+	margin: 18px 0 0;
+}
+
+body.activity-permalink #buddypress div.activity-meta {
+	margin-bottom: 6px;
+}
+
+#buddypress div.activity-meta a {
+	padding: 4px 8px;
+}
+
+#buddypress a.activity-time-since {
+	color: #767676;
+	text-decoration: none;
+}
+
+#buddypress a.activity-time-since:hover {
+	color: #767676;
+	text-decoration: underline;
+}
+
+#buddypress a.bp-primary-action,
+#buddypress #reply-title small a {
+	font-size: 80%;
+	margin-right: 5px;
+	text-decoration: none;
+}
+
+#buddypress a.bp-primary-action span,
+#buddypress #reply-title small a span {
+	background: #767676;
+	color: #fff;
+	font-size: 90%;
+	margin-left: 2px;
+	padding: 0 5px;
+}
+
+#buddypress a.bp-primary-action:hover span,
+#buddypress #reply-title small a:hover span {
+	background: #555;
+	color: #fff;
+}
+
+#buddypress div.activity-comments {
+	margin: 0 0 0 70px;
+	overflow: hidden; /* IE fix */
+	position: relative;
+	width: auto;
+	clear: both;
+}
+
+body.activity-permalink #buddypress div.activity-comments {
+	background: none;
+	margin-left: 170px;
+	width: auto;
+}
+
+#buddypress div.activity-comments > ul {
+	padding: 0 0 0 10px;
+}
+
+#buddypress div.activity-comments ul,
+#buddypress div.activity-comments ul li {
+	border: none;
+	list-style: none;
+}
+
+#buddypress div.activity-comments ul {
+	clear: both;
+	margin: 0;
+}
+
+#buddypress div.activity-comments ul li {
+	border-top: 1px solid #eee;
+	padding: 10px 0 0;
+}
+
+body.activity-permalink #buddypress .activity-list li.mini .activity-comments {
+	clear: none;
+	margin-top: 0;
+}
+
+body.activity-permalink #buddypress div.activity-comments ul li {
+	border-width: 1px;
+	padding: 10px 0 0;
+}
+
+#buddypress div.activity-comments > ul > li:first-child {
+	border-top: none;
+}
+
+#buddypress div.activity-comments ul li:last-child {
+	margin-bottom: 0;
+}
+
+#buddypress div.activity-comments ul li > ul {
+	margin-left: 30px;
+	margin-top: 0;
+	padding-left: 10px;
+}
+
+body.activity-permalink #buddypress div.activity-comments ul li > ul {
+	margin-top: 10px;
+}
+
+body.activity-permalink #buddypress div.activity-comments > ul {
+	padding: 0 10px 0 15px;
+}
+
+#buddypress div.activity-comments div.acomment-avatar img {
+	border-width: 1px;
+	float: left;
+	height: 25px;
+	margin-right: 10px;
+	width: 25px;
+}
+
+#buddypress div.activity-comments div.acomment-content {
+	font-size: 80%;
+	margin: 5px 0 0 40px;
+}
+
+#buddypress div.acomment-content .time-since,
+#buddypress div.acomment-content .activity-delete-link,
+#buddypress div.acomment-content .comment-header {
+	display: none;
+}
+
+body.activity-permalink #buddypress div.activity-comments div.acomment-content {
+	font-size: 90%;
+}
+
+#buddypress div.activity-comments div.acomment-meta {
+	color: #767676;
+	font-size: 80%;
+}
+
+#buddypress div.activity-comments form.ac-form {
+	display: none;
+	padding: 10px;
+}
+
+#buddypress div.activity-comments li form.ac-form {
+	margin-right: 15px;
+	clear: both;
+}
+
+#buddypress div.activity-comments form.root {
+	margin-left: 0;
+}
+
+#buddypress div.activity-comments div#message {
+	margin-top: 15px;
+	margin-bottom: 0;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #fff;
+	border: 1px inset #ccc;
+	margin-bottom: 10px;
+	padding: 8px;
+}
+
+#buddypress div.activity-comments form textarea {
+	border: none;
+	background: transparent;
+	box-shadow: none;
+	outline: none;
+	color: #555;
+	font-family: inherit;
+	font-size: 100%;
+	height: 60px;
+	padding: 0;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.activity-comments form input {
+	margin-top: 5px;
+}
+
+#buddypress div.activity-comments form div.ac-reply-avatar {
+	float: left;
+}
+
+#buddypress div.ac-reply-avatar img {
+	border: 1px solid #eee;
+}
+
+#buddypress div.activity-comments form div.ac-reply-content {
+	color: #767676;
+	margin-left: 50px;
+	padding-left: 15px;
+}
+
+#buddypress div.activity-comments form div.ac-reply-content a {
+	text-decoration: none;
+}
+
+#buddypress .acomment-options {
+	float: left;
+	margin: 5px 0 5px 40px;
+}
+
+#buddypress .acomment-options a {
+	color: #767676;
+}
+
+#buddypress .acomment-options a:hover {
+	color: inherit;
+}
+
+/*--------------------------------------------------------------
+3.3 - Directories - Members, Groups, Blogs, Forums
+--------------------------------------------------------------*/
+#buddypress div.dir-search {
+	float: right;
+	margin: -39px 0 0 0;
+}
+
+#buddypress div.dir-search input[type="text"],
+#buddypress li.groups-members-search input[type="text"] {
+	font-size: 90%;
+	padding: 1px 3px;
+}
+
+#buddypress .current-member-type {
+	font-style: italic;
+}
+
+#buddypress .dir-form {
+	clear: both;
+}
+
+/*--------------------------------------------------------------
+3.4 - Errors / Success Messages
+--------------------------------------------------------------*/
+#buddypress div#message {
+	margin: 0 0 15px;
+}
+
+#buddypress #message.info {
+	margin-bottom: 0;
+}
+
+#buddypress div#message.updated {
+	clear: both;
+	display: block;
+}
+
+#buddypress div#message p,
+#sitewide-notice p {
+	font-size: 90%;
+	display: block;
+	padding: 10px 15px;
+}
+
+#buddypress div#message.error p {
+	background-color: #fdc;
+	border: 1px solid #a00;
+	clear: left;
+	color: #800;
+}
+
+#buddypress div#message.warning p {
+	background-color: #ffe0af;
+	border: 1px solid #ffd087;
+	clear: left;
+	color: #800;
+}
+
+#buddypress div#message.updated p {
+	background-color: #efc;
+	border: 1px solid #591;
+	color: #250;
+}
+
+#buddypress #pass-strength-result {
+	background-color: #eee;
+	border-color: #ddd;
+	border-style: solid;
+	border-width: 1px;
+	display: none;
+	margin: 5px 5px 5px 0;
+	padding: 5px;
+	text-align: center;
+	width: 150px;
+}
+
+#buddypress .standard-form #basic-details-section #pass-strength-result {
+	width: 35%;
+}
+
+#buddypress #pass-strength-result.error,
+#buddypress #pass-strength-result.bad {
+	background-color: #ffb78c;
+	border-color: #ff853c !important;
+	display: block;
+}
+
+#buddypress #pass-strength-result.good {
+	background-color: #ffec8b;
+	border-color: #fc0 !important;
+	display: block;
+}
+
+#buddypress #pass-strength-result.short {
+	background-color: #ffa0a0;
+	border-color: #f04040 !important;
+	display: block;
+}
+
+#buddypress #pass-strength-result.strong {
+	background-color: #c3ff88;
+	border-color: #8dff1c !important;
+	display: block;
+}
+
+#buddypress .standard-form#signup_form div div.error {
+	background: #faa;
+	color: #a00;
+	margin: 0 0 10px;
+	padding: 6px;
+	width: 90%;
+}
+
+#buddypress div.accept,
+#buddypress div.reject {
+	float: left;
+	margin-left: 10px;
+}
+
+#buddypress ul.button-nav li {
+	float: left;
+	margin: 0 10px 10px 0;
+	list-style: none;
+}
+
+#buddypress ul.button-nav li.current a {
+	font-weight: 700;
+}
+
+#sitewide-notice #message {
+	left: 2%;
+	position: fixed;
+	top: 1em;
+	width: 96%;
+	z-index: 9999;
+}
+
+#sitewide-notice.admin-bar-on #message {
+	top: 3.3em;
+}
+
+#sitewide-notice strong {
+	display: block;
+	margin-bottom: -1em;
+}
+
+/*--------------------------------------------------------------
+3.5 - Forms
+--------------------------------------------------------------*/
+
+#buddypress form fieldset {
+	border: 0;
+	padding: 0;
+}
+
+#buddypress .standard-form textarea,
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="color"],
+#buddypress .standard-form input[type="date"],
+#buddypress .standard-form input[type="datetime"],
+#buddypress .standard-form input[type="datetime-local"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="month"],
+#buddypress .standard-form input[type="number"],
+#buddypress .standard-form input[type="range"],
+#buddypress .standard-form input[type="search"],
+#buddypress .standard-form input[type="tel"],
+#buddypress .standard-form input[type="time"],
+#buddypress .standard-form input[type="url"],
+#buddypress .standard-form input[type="week"],
+#buddypress .standard-form select,
+#buddypress .standard-form input[type="password"],
+#buddypress .dir-search input[type="search"],
+#buddypress .dir-search input[type="text"],
+#buddypress .groups-members-search input[type="search"],
+#buddypress .groups-members-search input[type="text"] {
+	border: 1px solid #ccc;
+	background: #fafafa;
+	border-radius: 0;
+	color: #737373;
+	font: inherit;
+	font-size: 100%;
+	padding: 6px;
+}
+
+#buddypress .standard-form select {
+	padding: 3px;
+}
+
+#buddypress .standard-form input[type="password"] {
+	margin-bottom: 5px;
+}
+
+#buddypress .standard-form label,
+#buddypress .standard-form span.label,
+#buddypress .standard-form legend {
+	display: block;
+	font-weight: 700;
+	margin: 15px 0 5px;
+	width: auto;
+}
+
+#buddypress label.xprofile-field-label {
+	display: inline;
+}
+
+#buddypress .standard-form p label,
+#buddypress .standard-form #invite-list label {
+	font-weight: 400;
+	margin: auto;
+}
+
+#buddypress .standard-form .checkbox label,
+#buddypress .standard-form .radio label {
+	color: #767676;
+	font-size: 100%;
+	font-weight: 400;
+	margin: 5px 0 0;
+}
+
+#buddypress .standard-form .checkbox label input,
+#buddypress .standard-form .radio label input {
+	margin-right: 3px;
+}
+
+#buddypress .standard-form#sidebar-login-form label {
+	margin-top: 5px;
+}
+
+#buddypress .standard-form input[type="text"] {
+	width: 75%;
+}
+
+#buddypress .standard-form#sidebar-login-form input[type="text"],
+#buddypress .standard-form#sidebar-login-form input[type="password"] {
+	padding: 4px;
+	width: 95%;
+}
+
+#buddypress .standard-form #basic-details-section input[type="password"],
+#buddypress .standard-form #blog-details-section input#signup_blog_url {
+	width: 35%;
+}
+
+#buddypress .standard-form#signup_form input[type="text"],
+#buddypress .standard-form#signup_form textarea,
+#buddypress .form-allowed-tags,
+#buddypress #commentform input[type="text"],
+#buddypress #commentform textarea {
+	width: 90%;
+}
+
+#buddypress .standard-form#signup_form div.submit {
+	float: right;
+}
+
+#buddypress div#signup-avatar img {
+	margin: 0 15px 10px 0;
+}
+
+#buddypress .standard-form textarea {
+	width: 75%;
+	height: 120px;
+}
+
+#buddypress .standard-form textarea#message_content {
+	height: 200px;
+}
+
+#buddypress .standard-form#send-reply textarea {
+	width: 97.5%;
+}
+
+#buddypress .standard-form p.description {
+	color: #767676;
+	font-size: 80%;
+	margin: 5px 0;
+}
+
+#buddypress .standard-form div.submit {
+	clear: both;
+	padding: 15px 0 0;
+}
+
+#buddypress .standard-form p.submit {
+	margin-bottom: 0;
+	padding: 15px 0 0;
+}
+
+#buddypress .standard-form div.submit input {
+	margin-right: 15px;
+}
+
+#buddypress .standard-form div.radio ul {
+	margin: 10px 0 15px 38px;
+	list-style: disc;
+}
+
+#buddypress .standard-form div.radio ul li {
+	margin-bottom: 5px;
+}
+
+#buddypress .standard-form a.clear-value {
+	display: block;
+	margin-top: 5px;
+	outline: none;
+}
+
+#buddypress .standard-form #basic-details-section,
+#buddypress .standard-form #blog-details-section,
+#buddypress .standard-form #profile-details-section {
+	float: left;
+	width: 48%;
+}
+
+#buddypress .standard-form #profile-details-section {
+	float: right;
+}
+
+#buddypress .standard-form #blog-details-section,
+#buddypress #notifications-bulk-management {
+	clear: left;
+}
+
+body.no-js #buddypress #notifications-bulk-management #select-all-notifications,
+body.no-js #buddypress label[for="message-type-select"],
+body.no-js #buddypress #message-type-select,
+body.no-js #buddypress #delete_inbox_messages,
+body.no-js #buddypress #delete_sentbox_messages,
+body.no-js #buddypress #messages-bulk-management #select-all-messages {
+	display: none;
+}
+
+#buddypress .standard-form input:focus,
+#buddypress .standard-form textarea:focus,
+#buddypress .standard-form select:focus {
+	background: #fafafa;
+	color: #555;
+}
+
+#buddypress form#send-invite-form {
+	margin-top: 20px;
+}
+
+#buddypress div#invite-list {
+	background: #f5f5f5;
+	height: 400px;
+	margin: 0 0 10px;
+	overflow: auto;
+	padding: 5px;
+	width: 160px;
+}
+
+#buddypress .standard-form button,
+#buddypress a.button,
+#buddypress input[type="submit"],
+#buddypress input[type="button"],
+#buddypress input[type="reset"],
+#buddypress ul.button-nav li a,
+#buddypress .generic-button a,
+#buddypress .comment-reply-link,
+a.bp-title-button {
+	background: #fff; /* Old browsers */
+	border: 1px solid #ccc;
+	color: #767676;
+	font-size: small;
+	cursor: pointer;
+	outline: none;
+	padding: 4px 10px;
+	text-align: center;
+	text-decoration: none;
+}
+
+#buddypress .standard-form button:hover,
+#buddypress a.button:hover,
+#buddypress a.button:focus,
+#buddypress input[type="submit"]:hover,
+#buddypress input[type="button"]:hover,
+#buddypress input[type="reset"]:hover,
+#buddypress ul.button-nav li a:hover,
+#buddypress ul.button-nav li.current a,
+#buddypress div.generic-button a:hover,
+#buddypress .comment-reply-link:hover {
+	background: #ededed;
+	border: 1px solid #bbb;
+	color: #555;
+	outline: none;
+	text-decoration: none;
+}
+
+#buddypress form.standard-form .left-menu {
+	float: left;
+}
+
+#buddypress form.standard-form .left-menu #invite-list ul {
+	margin: 1%;
+	list-style: none;
+}
+
+#buddypress form.standard-form .left-menu #invite-list ul li {
+	margin: 0 0 0 1%;
+}
+
+#buddypress form.standard-form .main-column {
+	margin-left: 190px;
+}
+
+#buddypress form.standard-form .main-column ul#friend-list {
+	clear: none;
+	float: left;
+}
+
+#buddypress form.standard-form .main-column ul#friend-list h3,
+#buddypress form.standard-form .main-column ul#friend-list h4 {
+	clear: none;
+}
+
+/* Overrides for embedded WP editors */
+#buddypress .wp-editor-wrap a.button,
+#buddypress .wp-editor-wrap button,
+#buddypress .wp-editor-wrap input[type="submit"],
+#buddypress .wp-editor-wrap input[type="button"],
+#buddypress .wp-editor-wrap input[type="reset"] {
+	padding: 0 8px 1px;
+}
+
+/* Form classes & generic attr styling */
+#buddypress form *[disabled="disabled"] {
+	cursor: default;
+	opacity: 0.4;
+}
+
+fieldset.register-site {
+	margin-top: 1em;
+}
+
+fieldset.create-site {
+	margin-bottom: 2em;
+}
+
+fieldset.create-site legend {
+	margin-bottom: 1em;
+}
+
+fieldset.create-site label {
+	margin-right: 3em;
+}
+
+.bp-screen-reader-text {
+	clip: rect(1px, 1px, 1px, 1px);
+	height: 1px;
+	overflow: hidden;
+	position: absolute !important;
+	width: 1px;
+	word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */
+}
+
+.bp-screen-reader-text:focus {
+	background-color: #f1f1f1;
+	border-radius: 3px;
+	box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
+	clip: auto !important;
+	color: #21759b;
+	display: block;
+	font-size: 14px;
+	font-size: 0.875rem;
+	font-weight: 700;
+	height: auto;
+	left: 5px;
+	line-height: normal;
+	padding: 15px 23px 14px;
+	text-decoration: none;
+	top: 5px;
+	width: auto;
+	z-index: 100000; /* Above WP toolbar. */
+}
+
+/*--------------------------------------------------------------
+3.6 - Ajax Loading
+--------------------------------------------------------------*/
+#buddypress a.loading,
+#buddypress input.loading {
+	-webkit-animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+	-moz-animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+	border-color: #aaa;
+}
+
+#buddypress a.loading:hover,
+#buddypress input.loading:hover {
+	color: #767676;
+}
+
+#buddypress input[type="submit"].pending,
+#buddypress input[type="button"].pending,
+#buddypress input[type="reset"].pending,
+#buddypress input[type="submit"].disabled,
+#buddypress input[type="button"].disabled,
+#buddypress input[type="reset"].disabled,
+#buddypress input[type="submit"][disabled="disabled"],
+#buddypress button.pending,
+#buddypress button.disabled,
+#buddypress div.pending a,
+#buddypress a.disabled {
+	border-color: #eee;
+	color: #bbb;
+	cursor: default;
+}
+
+#buddypress input[type="submit"]:hover.pending,
+#buddypress input[type="button"]:hover.pending,
+#buddypress input[type="reset"]:hover.pending,
+#buddypress input[type="submit"]:hover.disabled,
+#buddypress input[type="button"]:hover.disabled,
+#buddypress input[type="reset"]:hover.disabled,
+#buddypress button.pending:hover,
+#buddypress button.disabled:hover,
+#buddypress div.pending a:hover,
+#buddypress a.disabled:hover {
+	border-color: #eee;
+	color: #bbb;
+}
+
+/*--------------------------------------------------------------
+3.7 - Forums, Tables and Topics
+--------------------------------------------------------------*/
+#buddypress ul#topic-post-list {
+	margin: 0;
+	width: auto;
+}
+
+#buddypress ul#topic-post-list li {
+	padding: 15px;
+	position: relative;
+}
+
+#buddypress ul#topic-post-list li.alt {
+	background: #f5f5f5;
+}
+
+#buddypress ul#topic-post-list li div.poster-meta {
+	color: #767676;
+	margin-bottom: 10px;
+}
+
+#buddypress ul#topic-post-list li div.post-content {
+	margin-left: 54px;
+}
+
+#buddypress div.topic-tags {
+	font-size: 80%;
+}
+
+#buddypress div.admin-links {
+	color: #767676;
+	font-size: 80%;
+	position: absolute;
+	top: 15px;
+	right: 25px;
+}
+
+#buddypress div#topic-meta {
+	margin: 0;
+	padding: 5px 19px 30px;
+	position: relative;
+}
+
+#buddypress div#topic-meta div.admin-links {
+	right: 19px;
+	top: -36px;
+}
+
+#buddypress div#topic-meta h3 {
+	margin: 5px 0;
+}
+
+#buddypress div#new-topic-post {
+	display: none;
+	margin: 20px 0 0;
+	padding: 1px 0 0;
+}
+
+#buddypress table.notifications,
+#buddypress table.notifications-settings,
+#buddypress table.profile-settings,
+#buddypress table.profile-fields,
+#buddypress table.wp-profile-fields,
+#buddypress table.messages-notices,
+#buddypress table.forum {
+	width: 100%;
+}
+
+#buddypress table.notifications thead tr,
+#buddypress table.notifications-settings thead tr,
+#buddypress table.profile-settings thead tr,
+#buddypress table.profile-fields thead tr,
+#buddypress table.wp-profile-fields thead tr,
+#buddypress table.messages-notices thead tr,
+#buddypress table.forum thead tr {
+	background: #eaeaea;
+}
+
+#buddypress table#message-threads {
+	clear: both;
+}
+
+#buddypress table.profile-fields {
+	margin-bottom: 20px;
+}
+
+#buddypress table.profile-fields:last-child {
+	margin-bottom: 0;
+}
+
+#buddypress table.profile-fields p {
+	margin: 0;
+}
+
+#buddypress table.profile-fields p:last-child {
+	margin-top: 0;
+}
+
+#buddypress table.notifications tr td,
+#buddypress table.notifications-settings tr td,
+#buddypress table.profile-settings tr td,
+#buddypress table.profile-fields tr td,
+#buddypress table.wp-profile-fields tr td,
+#buddypress table.messages-notices tr td,
+#buddypress table.forum tr td,
+#buddypress table.notifications tr th,
+#buddypress table.notifications-settings tr th,
+#buddypress table.profile-fields tr th,
+#buddypress table.wp-profile-fields tr th,
+#buddypress table.messages-notices tr th,
+#buddypress table.forum tr th {
+	padding: 8px;
+	vertical-align: middle;
+}
+
+#buddypress table.notifications tr td.label,
+#buddypress table.notifications-settings tr td.label,
+#buddypress table.profile-fields tr td.label,
+#buddypress table.wp-profile-fields tr td.label,
+#buddypress table.messages-notices tr td.label,
+#buddypress table.forum tr td.label {
+	border-right: 1px solid #eaeaea;
+	font-weight: 700;
+	width: 25%;
+}
+
+#buddypress #message-threads .thread-info {
+	min-width: 40%;
+}
+
+#buddypress table tr td.thread-info p {
+	margin: 0;
+}
+
+#buddypress table tr td.thread-info p.thread-excerpt {
+	color: #767676;
+	font-size: 80%;
+	margin-top: 3px;
+}
+
+#buddypress table.forum td {
+	text-align: center;
+}
+
+#buddypress table.notifications tr.alt td,
+#buddypress table.notifications-settings tr.alt td,
+#buddypress table.profile-settings tr.alt td,
+#buddypress table.profile-fields tr.alt td,
+#buddypress table.wp-profile-fields tr.alt td,
+#buddypress table.messages-notices tr.alt td,
+#buddypress table.forum tr.alt td {
+	background: #f5f5f5;
+	color: #707070;
+}
+
+#buddypress table.notification-settings {
+	margin-bottom: 20px;
+	text-align: left;
+}
+
+#buddypress #groups-notification-settings {
+	margin-bottom: 0;
+}
+
+#buddypress table.notifications th.icon,
+#buddypress table.notifications td:first-child,
+#buddypress table.notification-settings th.icon,
+#buddypress table.notification-settings td:first-child {
+	display: none;
+}
+
+#buddypress table.notification-settings th.title,
+#buddypress table.profile-settings th.title {
+	width: 80%;
+}
+
+#buddypress table.notification-settings .yes,
+#buddypress table.notification-settings .no {
+	text-align: center;
+	width: 40px;
+}
+
+#buddypress table.forum {
+	margin: 0;
+	width: auto;
+	clear: both;
+}
+
+#buddypress table.forum tr.sticky td {
+	font-size: 110%;
+	background: #fff9db;
+	border-top: 1px solid #ffe8c4;
+	border-bottom: 1px solid #ffe8c4;
+}
+
+#buddypress table.forum tr.closed td.td-title {
+	padding-left: 35px;
+}
+
+#buddypress table.forum td p.topic-text {
+	color: #767676;
+	font-size: 100%;
+}
+
+#buddypress table.forum tr > td:first-child,
+#buddypress table.forum tr > th:first-child {
+	padding-left: 15px;
+}
+
+#buddypress table.forum tr > td:last-child,
+#buddypress table.forum tr > th:last-child {
+	padding-right: 15px;
+}
+
+#buddypress table.forum tr th#th-title,
+#buddypress table.forum tr th#th-poster,
+#buddypress table.forum tr th#th-group,
+#buddypress table.forum td.td-poster,
+#buddypress table.forum td.td-group,
+#buddypress table.forum td.td-title {
+	text-align: left;
+}
+
+#buddypress table.forum tr td.td-title a.topic-title {
+	font-size: 110%;
+}
+
+#buddypress table.forum td.td-freshness {
+	white-space: nowrap;
+}
+
+#buddypress table.forum td.td-freshness span.time-since {
+	font-size: 80%;
+	color: #767676;
+}
+
+#buddypress table.forum td img.avatar {
+	float: none;
+	margin: 0 5px -8px 0;
+}
+
+#buddypress table.forum td.td-poster,
+#buddypress table.forum td.td-group {
+	min-width: 140px;
+}
+
+#buddypress table.forum th#th-title {
+	width: 80%;
+}
+
+#buddypress table.forum th#th-freshness {
+	width: 25%;
+}
+
+#buddypress table.forum th#th-postcount {
+	width: 15%;
+}
+
+#buddypress table.forum p.topic-meta {
+	font-size: 80%;
+	margin: 5px 0 0;
+}
+
+/*-------------------------------------------------------------------------
+3.8 - Headers, Lists and Tabs - Activity, Groups, Blogs, Forums, Profiles
+-------------------------------------------------------------------------*/
+#buddypress .item-body {
+	margin: 20px 0;
+}
+
+#buddypress span.activity {
+	display: inline-block;
+	font-size: small;
+	padding: 0;
+}
+
+#buddypress span.user-nicename {
+	color: #767676;
+	display: inline-block;
+	font-size: 120%;
+	font-weight: 700;
+}
+
+#buddypress div#message p,
+#sitewide-notice p {
+	background-color: #ffd;
+	border: 1px solid #cb2;
+	color: #440;
+	font-weight: 400;
+	margin-top: 3px;
+	text-decoration: none;
+}
+
+.bp-site-wide-message #message p,
+.admin-bar-on #message p {
+	padding-right: 25px;
+	position: relative;
+}
+
+.bp-site-wide-message #message button,
+.admin-bar-on #message button {
+	font-size: 0.8em;
+	padding: 2px 4px;
+	position: absolute;
+	right: 0;
+	top: 0;
+}
+
+.admin-bar-on #message button {
+	right: 10px;
+	top: 7px;
+}
+
+#buddypress #item-header:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+#buddypress div#item-header div#item-header-content {
+	float: left;
+	margin-left: 0;
+}
+
+#buddypress div#item-header h2 {
+	line-height: 1.2;
+	margin: 0 0 15px;
+}
+
+#buddypress div#item-header h2 a {
+	color: #767676;
+	text-decoration: none;
+}
+
+#buddypress div#item-header img.avatar {
+	float: left;
+	margin: 0 15px 19px 0;
+}
+
+#buddypress div#item-header h2 {
+	margin-bottom: 5px;
+}
+
+#buddypress div#item-header h2 span.highlight {
+	font-size: 60%;
+	font-weight: 400;
+	line-height: 1.7;
+	vertical-align: middle;
+	display: inline-block;
+}
+
+#buddypress div#item-header h2 span.highlight span {
+	background: #a1dcfa;
+	color: #fff;
+	cursor: pointer;
+	font-weight: 700;
+	font-size: 80%;
+	margin-bottom: 2px;
+	padding: 1px 4px;
+	position: relative;
+	right: -2px;
+	top: -2px;
+	vertical-align: middle;
+}
+
+#buddypress div#item-header div#item-meta {
+	font-size: 80%;
+	color: #767676;
+	overflow: hidden;
+	margin: 15px 0 5px;
+	padding-bottom: 10px;
+}
+
+#buddypress div#item-header div#item-actions {
+	float: right;
+	margin: 0 0 15px 15px;
+	text-align: right;
+	width: 20%;
+}
+
+#buddypress div#item-header div#item-actions h2,
+#buddypress div#item-header div#item-actions h3 {
+	margin: 0 0 5px;
+}
+
+#buddypress div#item-header div#item-actions a {
+	display: inline-block;
+}
+
+#buddypress div#item-header ul {
+	margin-bottom: 15px;
+}
+
+#buddypress div#item-header ul:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+#buddypress div#item-header ul h5,
+#buddypress div#item-header ul span,
+#buddypress div#item-header ul hr {
+	display: none;
+}
+
+#buddypress div#item-header ul li {
+	float: right;
+	list-style: none;
+}
+
+#buddypress div#item-header ul img.avatar,
+#buddypress div#item-header ul.avatars img.avatar {
+	height: 30px;
+	margin: 2px;
+	width: 30px;
+}
+
+#buddypress div#item-header div.generic-button,
+#buddypress div#item-header a.button {
+	float: left;
+	margin: 10px 10px 0 0;
+}
+
+body.no-js #buddypress div#item-header .js-self-profile-button {
+	display: none;
+}
+
+#buddypress div#item-header div#message.info {
+	line-height: 0.8;
+}
+
+#buddypress ul.item-list {
+	border-top: 1px solid #eaeaea;
+	width: 100%;
+	list-style: none;
+	clear: both;
+	margin: 0;
+	padding: 0;
+}
+
+body.activity-permalink #buddypress ul.item-list,
+body.activity-permalink #buddypress ul.item-list li.activity-item {
+	border: none;
+}
+
+#buddypress ul.item-list li {
+	border-bottom: 1px solid #eaeaea;
+	padding: 15px 0;
+	margin: 0;
+	position: relative;
+	list-style: none;
+}
+
+#buddypress ul.single-line li {
+	border: none;
+}
+
+#buddypress ul.item-list li img.avatar {
+	float: left;
+	margin: 0 10px 0 0;
+}
+
+#buddypress ul.item-list li div.item-title,
+#buddypress ul.item-list li h3,
+#buddypress ul.item-list li h4 {
+	font-weight: 400;
+	font-size: 90%;
+	margin: 0;
+	width: 75%;
+}
+
+#buddypress ul.item-list li div.item-title span {
+	color: #767676;
+	font-size: 80%;
+}
+
+#buddypress ul.item-list li div.item-desc {
+	color: #767676;
+	font-size: 80%;
+	margin: 10px 0 0 60px;
+	width: 50%;
+}
+
+#buddypress ul.item-list li.group-no-avatar div.item-desc {
+	margin-left: 0;
+}
+
+#buddypress ul.item-list li div.action {
+	position: absolute;
+	top: 15px;
+	right: 0;
+	text-align: right;
+}
+
+#buddypress ul.item-list li div.meta {
+	color: #767676;
+	font-size: 80%;
+	margin-top: 10px;
+}
+
+#buddypress ul.item-list li h5 span.small {
+	float: right;
+	font-size: 80%;
+	font-weight: 400;
+}
+
+#buddypress div.item-list-tabs {
+	background: transparent;
+	clear: left;
+	overflow: hidden;
+}
+
+#buddypress div.item-list-tabs ul {
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li {
+	float: left;
+	margin: 0;
+	list-style: none;
+}
+
+#buddypress div.item-list-tabs#subnav ul li {
+	margin-top: 0;
+}
+
+#buddypress div.item-list-tabs ul li.last {
+	float: right;
+	margin: 7px 0 0;
+}
+
+#buddypress div.item-list-tabs#subnav ul li.last {
+	margin-top: 4px;
+}
+
+#buddypress div.item-list-tabs ul li.last select {
+	max-width: 185px;
+}
+
+#buddypress div.item-list-tabs ul li a,
+#buddypress div.item-list-tabs ul li span {
+	display: block;
+	padding: 5px 10px;
+	text-decoration: none;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	background: #eee;
+	border-radius: 50%;
+	border: 1px solid #ccc;
+	color: #6c6c6c;
+	display: inline;
+	font-size: 70%;
+	margin-left: 2px;
+	padding: 3px 6px;
+	text-align: center;
+	vertical-align: middle;
+}
+
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background-color: #eee;
+	color: #555;
+	opacity: 0.9;
+	font-weight: 700;
+}
+
+#buddypress div.item-list-tabs ul li.selected a span,
+#buddypress div.item-list-tabs ul li.current a span,
+#buddypress div.item-list-tabs ul li a:hover span {
+	background-color: #eee;
+}
+
+#buddypress div.item-list-tabs ul li.selected a span,
+#buddypress div.item-list-tabs ul li.current a span {
+	background-color: #fff;
+}
+
+#buddypress div#item-nav ul li.loading a {
+	background-position: 88% 50%;
+}
+
+#buddypress div.item-list-tabs#object-nav {
+	margin-top: 0;
+}
+
+#buddypress div.item-list-tabs#subnav {
+	background: transparent;
+	margin: 10px 0;
+	overflow: hidden;
+}
+
+#buddypress #admins-list li,
+#buddypress #mods-list li,
+#buddypress #members-list li {
+	overflow: auto;
+	list-style: none;
+}
+
+#buddypress .group-members-list {
+	width: 100%;
+	margin-top: 1em;
+	clear: both;
+	overflow: auto;
+}
+
+#buddypress #item-buttons:empty {
+	display: none;
+}
+
+/*--------------------------------------------------------------
+3.8.1 - Cover Image
+--------------------------------------------------------------*/
+
+#buddypress #cover-image-container {
+	position: relative;
+	z-index: 0;
+}
+
+#buddypress #header-cover-image {
+	background-color: #c5c5c5;
+	background-position: center top;
+	background-repeat: no-repeat;
+	background-size: cover;
+	border: 0;
+	display: block;
+	left: 0;
+	margin: 0;
+	padding: 0;
+	position: absolute;
+	top: 0;
+	width: 100%;
+	z-index: 1;
+}
+
+#buddypress #item-header-cover-image {
+	padding: 0 1em;
+	position: relative;
+	z-index: 2;
+}
+
+/*--------------------------------------------------------------
+3.9 - Private Messaging Threads
+--------------------------------------------------------------*/
+#buddypress table#message-threads tr.unread td {
+	background: #fff9db;
+	border-top: 1px solid #ffe8c4;
+	border-bottom: 1px solid #ffe8c4;
+	font-weight: 700;
+}
+
+#buddypress table#message-threads tr.unread td .thread-excerpt,
+#buddypress table#message-threads tr.unread td .activity,
+#buddypress table#message-threads tr.unread td.thread-options {
+	font-weight: 400;
+}
+
+#buddypress li span.unread-count,
+#buddypress tr.unread span.unread-count {
+	background: #d00;
+	color: #fff;
+	font-weight: 700;
+	padding: 2px 8px;
+}
+
+#buddypress div.item-list-tabs ul li a span.unread-count {
+	padding: 1px 6px;
+	color: #fff;
+}
+
+#buddypress div#message-thread div.message-box {
+	margin: 0;
+	padding: 15px;
+}
+
+#buddypress div#message-thread div.alt {
+	background: #f4f4f4;
+}
+
+#buddypress div#message-thread p#message-recipients {
+	margin: 10px 0 20px;
+}
+
+#buddypress div#message-thread img.avatar {
+	float: left;
+	margin: 0 10px 0 0;
+	vertical-align: middle;
+}
+
+#buddypress div#message-thread strong {
+	font-size: 100%;
+	margin: 0;
+}
+
+#buddypress div#message-thread strong a {
+	text-decoration: none;
+}
+
+#buddypress div#message-thread strong span.activity {
+	margin-top: 4px;
+}
+
+#buddypress div#message-thread div.message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+#buddypress div#message-thread div.message-content {
+	margin-left: 45px;
+}
+
+#buddypress div#message-thread div.message-options {
+	text-align: right;
+}
+
+#buddypress #message-threads img.avatar {
+	max-width: none;
+}
+
+#buddypress div.message-search {
+	float: right;
+	margin: 0 20px;
+}
+
+.message-metadata {
+	position: relative;
+}
+
+.message-star-actions {
+	position: absolute;
+	right: 0;
+	top: 0;
+}
+
+#buddypress a.message-action-star,
+#buddypress a.message-action-unstar {
+	border-bottom: 0;
+	text-decoration: none;
+	outline: none;
+}
+
+a.message-action-star {
+	opacity: 0.7;
+}
+
+a.message-action-star:hover {
+	opacity: 1;
+}
+
+.message-action-star span.icon:before,
+.message-action-unstar span.icon:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.message-action-star span.icon:before {
+	color: #767676;
+	content: "\f154";
+}
+
+.message-action-unstar span.icon:before {
+	color: #fcdd77;
+	content: "\f155";
+}
+
+/*--------------------------------------------------------------
+3.10 - Extended Profiles
+--------------------------------------------------------------*/
+
+#buddypress div.profile h2 {
+	margin-bottom: auto;
+	margin-top: 15px;
+}
+
+#buddypress #profile-edit-form ul.button-nav {
+	margin-top: 15px;
+}
+
+body.no-js #buddypress .field-visibility-settings-toggle,
+body.no-js #buddypress .field-visibility-settings-close {
+	display: none;
+}
+
+#buddypress .field-visibility-settings {
+	display: none;
+	margin-top: 10px;
+}
+
+body.no-js #buddypress .field-visibility-settings {
+	display: block;
+}
+
+#buddypress .current-visibility-level {
+	font-weight: 700;
+	font-style: normal;
+}
+
+#buddypress .field-visibility-settings,
+#buddypress .field-visibility-settings-toggle,
+#buddypress .field-visibility-settings-notoggle {
+	color: #707070;
+}
+
+#buddypress .field-visibility-settings-toggle a,
+#buddypress .field-visibility-settings a {
+	font-size: 80%;
+}
+
+body.register #buddypress div.page ul {
+	list-style: none;
+}
+
+#buddypress .standard-form .field-visibility-settings label {
+	margin: 0;
+	font-weight: 400;
+}
+
+#buddypress .field-visibility-settings legend,
+#buddypress .field-visibility-settings-toggle {
+	font-style: italic;
+}
+
+#buddypress .field-visibility-settings .radio {
+	list-style: none;
+	margin-bottom: 0;
+}
+
+#buddypress .field-visibility select {
+	margin: 0;
+}
+
+#buddypress .wp-editor-container {
+	border: 1px solid #dedede;
+}
+
+#buddypress .html-active button.switch-html {
+	border-bottom-color: transparent;
+	border-bottom-left-radius: 0;
+	border-bottom-right-radius: 0;
+	background: #f5f5f5;
+	color: #707070;
+}
+
+#buddypress .tmce-active button.switch-tmce {
+	border-bottom-color: transparent;
+	border-bottom-left-radius: 0;
+	border-bottom-right-radius: 0;
+	background: #f5f5f5;
+	color: #707070;
+}
+
+#buddypress .standard-form .wp-editor-container textarea {
+	width: 100%;
+	padding-top: 0;
+	padding-bottom: 0;
+}
+
+/*--------------------------------------------------------------
+3.11 - Widgets
+--------------------------------------------------------------*/
+
+.widget.buddypress span.activity {
+	display: inline-block;
+	font-size: small;
+	padding: 0;
+}
+
+.widget.buddypress div.item-options {
+	font-size: 90%;
+	margin: 0 0 1em;
+	padding: 1em 0;
+}
+
+.widget.buddypress div.item {
+	margin: 0 0 1em;
+}
+
+.widget.buddypress div.item-meta,
+.widget.buddypress div.item-content {
+	font-size: 11px;
+	margin-left: 50px;
+}
+
+.widget.buddypress div.avatar-block:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.widget.buddypress .item-avatar a {
+	float: left;
+	margin-bottom: 15px;
+	margin-right: 10px;
+}
+
+.widget.buddypress div.item-avatar img {
+	display: inline-block;
+	height: 40px;
+	margin: 1px;
+	width: 40px;
+}
+
+.widget.buddypress .item-avatar a,
+.widget.buddypress .item-avatar a:hover,
+.widget.buddypress .item-avatar a:focus,
+.widget.buddypress .item-avatar a:active,
+.widget.buddypress .item-avatar a img {
+	box-shadow: none;
+}
+
+.widget.buddypress #bp-login-widget-form label {
+	display: block;
+	margin: 1rem 0 0.5rem;
+}
+
+.widget.buddypress #bp-login-widget-form #bp-login-widget-submit {
+	margin-right: 10px;
+}
+
+.widget.buddypress .bp-login-widget-user-avatar {
+	float: left;
+}
+
+.bp-login-widget-user-avatar img.avatar {
+	height: 40px;
+	width: 40px;
+}
+
+.widget.buddypress .bp-login-widget-user-links > div {
+	padding-left: 60px;
+}
+
+.widget.buddypress .bp-login-widget-user-links > div {
+	margin-bottom: 0.5rem;
+}
+
+.widget.buddypress .bp-login-widget-user-links > div.bp-login-widget-user-link a {
+	font-weight: 700;
+}
+
+.widget.buddypress #groups-list,
+.widget.buddypress #members-list,
+.widget.buddypress #friends-list {
+	margin-left: 0;
+	padding-left: 0;
+}
+
+.widget.buddypress #groups-list li,
+.widget.buddypress #members-list li,
+.widget.buddypress #friends-list li {
+	clear: both;
+	list-style-type: none;
+}
+
+/*--------------------------------------------------------------
+3.12 - Tooltips
+--------------------------------------------------------------*/
+
+.buddypress .bp-tooltip {
+	position: relative;
+}
+
+.bp-tooltip:after {
+	background: #fff;
+	border: 1px solid #aaa;
+	border-collapse: separate;
+	border-radius: 1px;
+	box-shadow: 1px 1px 0 1px rgba(132, 132, 132, 0.3);
+	color: #000;
+	content: attr(data-bp-tooltip);
+	display: none;
+	font-family: sans-serif;
+	font-size: 11px;
+	font-weight: 400;
+	letter-spacing: normal;
+	line-height: 1.5;
+	margin-top: 10px;
+	max-width: 240px;
+	opacity: 0;
+	padding: 3px 6px;
+	position: absolute;
+	right: 50%;
+	text-align: center;
+	text-decoration: none;
+	text-shadow: none;
+	text-transform: none;
+	top: 100%;
+	transform: translateX(50%);
+	transition: opacity 2s ease-out;
+	white-space: pre;
+	word-wrap: break-word;
+	z-index: 998;
+}
+
+.bp-tooltip:hover:after,
+.bp-tooltip:active:after,
+.bp-tooltip:focus:after {
+	display: inline-block;
+	opacity: 1;
+	overflow: visible;
+	text-decoration: none;
+	z-index: 999;
+}
+
+#group-admins .bp-tooltip:after,
+#group-mods .bp-tooltip:after,
+.message-metadata .bp-tooltip:after {
+	right: 0;
+	text-align: right;
+	transform: translateX(0);
+}
+
+.feed .bp-tooltip:after,
+.item-list .bp-tooltip:after,
+.messages-notices .bp-tooltip:after {
+	left: 0;
+	right: auto;
+	text-align: left;
+	transform: translateX(0);
+}
+
+.bp-site-wide-message .bp-tooltip:after,
+.admin-bar-on .bp-tooltip:after {
+	right: 50px;
+}
+
+/*--------------------------------------------------------------
+4.0 - Media Queries
+--------------------------------------------------------------*/
+
+/*--------------------------------------------------------------
+4.1 - Smartphones - landscape
+--------------------------------------------------------------*/
+@media only screen and (max-width: 480px ) {
+
+	#buddypress div.dir-search {
+		float: right;
+		margin-top: -50px;
+		text-align: right;
+	}
+
+	#buddypress div.dir-search input[type="text"] {
+		margin-bottom: 1em;
+		width: 50%;
+	}
+
+	a.bp-title-button {
+		margin-left: 10px;
+	}
+
+	#buddypress form.standard-form .main-column div.action {
+		position: relative;
+		margin-bottom: 1em;
+	}
+
+	#buddypress form.standard-form .main-column ul#friend-list h3,
+	#buddypress form.standard-form .main-column ul#friend-list h4 {
+		width: 100%;
+	}
+}
+
+/*--------------------------------------------------------------
+4.2 - Smartphones - portrait
+--------------------------------------------------------------*/
+@media only screen and (max-width: 320px) {
+
+	#buddypress div.dir-search {
+		clear: left;
+		float: left;
+		margin-top: 0;
+		text-align: left;
+	}
+
+	#buddypress li#groups-order-select {
+		clear: left;
+		float: left;
+	}
+
+	#buddypress ul.item-list li div.action {
+		clear: left;
+		float: left;
+		margin-top: 0;
+		margin-left: 70px;
+		position: relative;
+		top: 0;
+		right: 0;
+		text-align: left;
+	}
+
+	#buddypress ul.item-list li div.item-desc {
+		clear: left;
+		float: left;
+		margin: 10px 0 0;
+		width: auto;
+	}
+
+	#buddypress li div.item {
+		margin-left: 70px;
+		width: auto;
+	}
+
+	#buddypress ul.item-list li div.meta {
+		margin-top: 0;
+	}
+
+	#buddypress .item-desc p {
+		margin: 0 0 10px;
+	}
+
+	#buddypress div.pagination .pag-count {
+		margin-left: 0;
+	}
+}
+
+/*--------------------------------------------------------------
+4.2 - Smartphones - smaller screen sizes
+--------------------------------------------------------------*/
+@media only screen and (max-width: 240px) {
+
+	#buddypress div.dir-search {
+		float: left;
+		margin: 0;
+	}
+
+	#buddypress div.dir-search input[type="text"] {
+		width: 50%;
+	}
+
+	#buddypress li#groups-order-select {
+		float: left;
+	}
+
+	#buddypress ul.item-list li img.avatar {
+		width: 30px;
+		height: auto;
+	}
+
+	#buddypress ul.item-list li div.action,
+	#buddypress li div.item {
+		margin-left: 45px;
+	}
+
+	h1 a.bp-title-button {
+		clear: left;
+		float: left;
+		margin: 10px 0 20px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/embeds-activity-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/embeds-activity-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/embeds-activity-rtl.css	(revision 12265)
@@ -0,0 +1,156 @@
+/*
+ * @version 3.0.0
+ */
+#bp-embed-header:after {
+	clear: both;
+	content: "";
+	display: table;
+	margin-bottom: 1em;
+}
+
+.bp-embed-avatar {
+	float: right;
+	margin: 0 0 0 0.75em;
+}
+
+p.bp-embed-activity-action {
+	font-size: 15px;
+	margin-bottom: 0;
+}
+
+p.bp-embed-activity-action a:first-child {
+	color: #32373c;
+	font-weight: 700;
+}
+
+p.bp-embed-activity-action img.avatar {
+	padding: 0 3px 0 4px;
+	vertical-align: text-bottom;
+}
+
+.bp-embed-excerpt {
+	margin-bottom: 1em;
+}
+
+.bp-embed-excerpt a {
+	color: #21759b;
+	display: inline-block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	vertical-align: top;
+	white-space: nowrap;
+	max-width: 250px;
+}
+
+.activity-read-more {
+	margin-right: 0.5em;
+}
+
+.activity-read-more a {
+	color: #b4b9be;
+}
+
+.wp-embed-footer {
+	margin-top: 20px;
+}
+
+span.bp-embed-timestamp {
+	font-size: 0.9em;
+}
+
+video {
+	width: 100%;
+	height: auto;
+}
+
+.bp-activity-embed-display-media {
+	border: 1px solid #ccc;
+	border-radius: 6px;
+}
+
+.bp-activity-embed-display-media.one-col,
+.bp-activity-embed-display-media.one-col .thumb,
+.bp-activity-embed-display-media.one-col .thumb img {
+	width: 100%;
+}
+
+.bp-activity-embed-display-media.two-col .thumb,
+.bp-activity-embed-display-media.two-col .caption {
+	display: table-cell;
+}
+
+.bp-activity-embed-display-media.two-col .thumb {
+	background: #000;
+	vertical-align: middle;
+}
+
+.bp-activity-embed-display-media.two-col .caption {
+	vertical-align: top;
+}
+
+.bp-activity-embed-display-media.two-col .thumb img {
+	border-left: 1px solid #ccc;
+	display: block;
+	width: 100%;
+}
+
+.bp-activity-embed-display-media .thumb {
+	position: relative;
+}
+
+.bp-activity-embed-display-media .caption {
+	padding: 0.2em 0.5em 0.5em;
+}
+
+a.play-btn {
+	background: rgba(0, 0, 0, 0.75);
+	border-radius: 50%;
+	height: 50px;
+	right: 50%;
+	margin: 0;
+	padding: 1em;
+	position: absolute;
+	text-indent: 0.25em;
+	top: 50%;
+	transform: translateY(-50%) translateX(50%);
+	-webkit-transform: translateY(-50%) translateX(50%);
+	transition: all 0.2s ease-out;
+	width: 50px;
+}
+
+.bp-activity-embed-display-media.two-col a.play-btn {
+	height: 35px;
+	width: 35px;
+}
+
+a.play-btn:hover {
+	background: rgba(0, 0, 0, 0.95);
+	transform: translateY(-50%) translateX(50%) scale(1.05);
+	-webkit-transform: translateY(-50%) translateX(50%) scale(1.05);
+	transition: all 0.2s ease-out;
+}
+
+.bp-activity-embed-display-media .thumb svg {
+	fill: #fff;
+	overflow: hidden;
+}
+
+.bp-activity-embed-display-media .caption-description {
+	font-size: 90%;
+	margin: 0.4em 0;
+}
+
+@media only screen and (max-width: 480px) {
+
+	.bp-activity-embed-display-media.two-col .thumb {
+		border-bottom: 1px solid #ccc;
+		border-left: 0;
+		display: block;
+		max-width: none !important;
+	}
+
+	a.play-btn {
+		height: 35px;
+		width: 35px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/embeds-activity.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/embeds-activity.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/embeds-activity.css	(revision 12265)
@@ -0,0 +1,156 @@
+/*
+ * @version 3.0.0
+ */
+#bp-embed-header:after {
+	clear: both;
+	content: "";
+	display: table;
+	margin-bottom: 1em;
+}
+
+.bp-embed-avatar {
+	float: left;
+	margin: 0 0.75em 0 0;
+}
+
+p.bp-embed-activity-action {
+	font-size: 15px;
+	margin-bottom: 0;
+}
+
+p.bp-embed-activity-action a:first-child {
+	color: #32373c;
+	font-weight: 700;
+}
+
+p.bp-embed-activity-action img.avatar {
+	padding: 0 4px 0 3px;
+	vertical-align: text-bottom;
+}
+
+.bp-embed-excerpt {
+	margin-bottom: 1em;
+}
+
+.bp-embed-excerpt a {
+	color: #21759b;
+	display: inline-block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	vertical-align: top;
+	white-space: nowrap;
+	max-width: 250px;
+}
+
+.activity-read-more {
+	margin-left: 0.5em;
+}
+
+.activity-read-more a {
+	color: #b4b9be;
+}
+
+.wp-embed-footer {
+	margin-top: 20px;
+}
+
+span.bp-embed-timestamp {
+	font-size: 0.9em;
+}
+
+video {
+	width: 100%;
+	height: auto;
+}
+
+.bp-activity-embed-display-media {
+	border: 1px solid #ccc;
+	border-radius: 6px;
+}
+
+.bp-activity-embed-display-media.one-col,
+.bp-activity-embed-display-media.one-col .thumb,
+.bp-activity-embed-display-media.one-col .thumb img {
+	width: 100%;
+}
+
+.bp-activity-embed-display-media.two-col .thumb,
+.bp-activity-embed-display-media.two-col .caption {
+	display: table-cell;
+}
+
+.bp-activity-embed-display-media.two-col .thumb {
+	background: #000;
+	vertical-align: middle;
+}
+
+.bp-activity-embed-display-media.two-col .caption {
+	vertical-align: top;
+}
+
+.bp-activity-embed-display-media.two-col .thumb img {
+	border-right: 1px solid #ccc;
+	display: block;
+	width: 100%;
+}
+
+.bp-activity-embed-display-media .thumb {
+	position: relative;
+}
+
+.bp-activity-embed-display-media .caption {
+	padding: 0.2em 0.5em 0.5em;
+}
+
+a.play-btn {
+	background: rgba(0, 0, 0, 0.75);
+	border-radius: 50%;
+	height: 50px;
+	left: 50%;
+	margin: 0;
+	padding: 1em;
+	position: absolute;
+	text-indent: 0.25em;
+	top: 50%;
+	transform: translateY(-50%) translateX(-50%);
+	-webkit-transform: translateY(-50%) translateX(-50%);
+	transition: all 0.2s ease-out;
+	width: 50px;
+}
+
+.bp-activity-embed-display-media.two-col a.play-btn {
+	height: 35px;
+	width: 35px;
+}
+
+a.play-btn:hover {
+	background: rgba(0, 0, 0, 0.95);
+	transform: translateY(-50%) translateX(-50%) scale(1.05);
+	-webkit-transform: translateY(-50%) translateX(-50%) scale(1.05);
+	transition: all 0.2s ease-out;
+}
+
+.bp-activity-embed-display-media .thumb svg {
+	fill: #fff;
+	overflow: hidden;
+}
+
+.bp-activity-embed-display-media .caption-description {
+	font-size: 90%;
+	margin: 0.4em 0;
+}
+
+@media only screen and (max-width: 480px) {
+
+	.bp-activity-embed-display-media.two-col .thumb {
+		border-bottom: 1px solid #ccc;
+		border-right: 0;
+		display: block;
+		max-width: none !important;
+	}
+
+	a.play-btn {
+		height: 35px;
+		width: 35px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven-rtl.css	(revision 12265)
@@ -0,0 +1,2008 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyeleven theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyeleven this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+body.buddypress:not(.page-template-sidebar-page) #content {
+	margin: 0 10%;
+	width: auto;
+}
+
+body.buddypress:not(.page-template-sidebar-page) #content .entry-header,
+body.buddypress:not(.page-template-sidebar-page) #content .entry-content {
+	width: auto;
+}
+
+.buddypress.singular.page .hentry {
+	padding-top: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+/* Here we start things off */
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a:focus, #buddypress div.item-list-tabs ul li a:hover {
+	color: #555;
+}
+
+#buddypress div.item-list-tabs ul li a:visited {
+	color: #21759b;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 450px) {
+	.bp-user #buddypress #object-nav {
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 5px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:focus:before, .bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:active:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:focus ul, .bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:active ul {
+		min-height: 320px;
+		opacity: 1;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 650px) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f2f2f2;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 1px solid #d4d0ba;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	float: left;
+	margin-top: 0;
+	padding: 5px 0 5px;
+	text-align: left;
+	width: 230px;
+}
+
+@media screen and (max-width: 450px) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		float: none;
+		width: auto;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select option {
+	font-style: normal;
+}
+
+.bp-user #buddypress div#subnav.item-list-tabs li.last {
+	margin-right: 0;
+	text-align: left;
+}
+
+@media screen and (max-width: 450px) {
+	.bp-user #buddypress div#subnav.item-list-tabs li.last {
+		clear: right;
+		float: none;
+		margin-right: 10px;
+		text-align: right;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 800px) {
+	body.bp-user #buddypress #item-header,
+	body.bp-user #buddypress #item-body {
+		background: #fff;
+	}
+	body.bp-user #buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	body.bp-user #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.bp-user #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body.bp-user #buddypress #object-nav ul li.selected {
+		background: #ddd;
+	}
+	body.bp-user #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	body.bp-user #buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 25px 0 0;
+		width: auto;
+	}
+	body.bp-user #buddypress #item-body #subnav {
+		margin: 0 -25px 0 0;
+	}
+	body.bp-user #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: right;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #373737;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress ul.item-list {
+	border-top: 0;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 15px;
+	text-align: center;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 15px;
+}
+
+@media screen and (min-width: 450px) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-right: 0;
+	overflow: hidden;
+}
+
+#buddypress ul.item-list li .item .item-meta {
+	text-align: center;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item .item-meta {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-right: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 450px) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title > a {
+	text-decoration: none;
+}
+
+#buddypress ul.item-list li .item .item-title > a:visited {
+	color: #21759b;
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.8rem;
+	padding: 15px 0;
+	text-align: right;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 5%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 18px;
+		font-size: 1.2rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin: 0;
+	margin-right: 0;
+	padding: 15px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+}
+
+@media screen and (min-width: 37em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 5px 15px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 15px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 650px) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: right;
+		margin: 15px 0 25px;
+		padding: 15px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.6;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	max-width: 220px;
+	min-height: 1.5em;
+	padding: 0 0.4em 0 0;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		font-size: 12px;
+		font-size: 0.8rem;
+		float: left;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit input {
+		padding: 0 6px;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 50px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li.load-newest a {
+	display: block;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 25px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content,
+#buddypress #activity-stream li .activity-comments {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li .activity-header a:visited {
+	color: #21759b;
+}
+
+#buddypress #activity-stream li .activity-inner img {
+	height: auto;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 15px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress #activity-stream li .activity-comments {
+		margin-right: 70px;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-meta a {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin-bottom: 5px;
+	padding: 0.2em 0.5em;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		float: right;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 15px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	margin-bottom: 50px;
+}
+
+@media screen and (min-width: 650px) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.3333333333rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream .activity-comments {
+	margin-right: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	position: relative;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 2px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li:hover * {
+	color: #555;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li .acomment-meta,
+#buddypress #activity-stream .activity-comments > ul > li .acomment-content {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	border: 1px solid #d4d0ba;
+	box-sizing: border-box;
+	margin: 15px 0;
+	width: 100%;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(55, 55, 55, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.8rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 650px) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: left;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress a:visited {
+	color: #21759b;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	text-align: right;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+	.bp-user #buddypress #item-header-content .user-nicename,
+	.single-item.groups #buddypress #item-header-content .user-nicename {
+		color: #555;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 800px) {
+	#buddypress #item-header-cover-image #item-header-content .user-nicename {
+		color: #555;
+		text-shadow: none;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 25px;
+	}
+}
+
+@media screen and (max-width: 50em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 50px;
+}
+
+.single-item.groups #buddypress div#item-header #item-header-content {
+	box-sizing: border-box;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0 !important;
+	width: 100%;
+}
+
+@media screen and (min-width: 50em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		padding-right: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-right: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	font-weight: 700;
+	line-height: 1.4;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions #group-admins li,
+.single-item.groups #buddypress div#item-header div#item-actions #group-mods li {
+	margin: 0;
+}
+
+@media screen and (min-width: 650px) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 2%;
+		padding: 0 0.5em;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: left;
+		width: 28%;
+	}
+}
+
+@media screen and (min-width: 64em) {
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+	margin-top: 225px !important;
+}
+
+@media screen and (min-width: 50em) and (max-width: 60em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		max-width: 60% !important;
+		width: 60% !important;
+	}
+}
+
+@media screen and (max-width: 64em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+		border: 0;
+		clear: both;
+		margin-top: 0 !important;
+		max-width: 100% !important;
+		padding-top: 0 !important;
+		width: auto;
+	}
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-content #item-buttons .generic-button {
+	margin-left: 5px;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		margin-left: 0%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	font-weight: 700;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(55, 55, 55, 0.6);
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 25px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li img,
+	.groups.manage-members #group-settings-form .item-list li h5 {
+		float: right;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 15px;
+	width: 100%;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: block;
+	margin: 5px 0;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		display: inline-block;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 450px) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 25px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0 0;
+}
+
+.groups.group-members #subnav li #search-members-form label input[type="text"] {
+	box-shadow: none;
+}
+
+@media screen and (max-width: 450px) {
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 13px;
+	font-size: 0.8666666667rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 40%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: right;
+	margin-left: 0;
+	width: 49%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: right;
+	font-family: inherit;
+	line-height: 20px;
+	margin-right: 1%;
+	width: 50%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus, .bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.2rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle {
+	margin: 5px 0 25px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #6f6e6e;
+	border-radius: 2px;
+	background-clip: padding-box;
+	color: #eee;
+	font-weight: 700;
+	padding: 0.1em 0.5em;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:focus, .bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:hover,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:focus,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:hover {
+	color: #fff;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread #message-recipients .highlight {
+	font-size: 100%;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 15px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	box-sizing: border-box;
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom: 1px solid #ccc;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star,
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-unstar {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star span.icon:before {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.7333333333rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	border-color: #eaeaea;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress #send_message_form input,
+.bp-user #buddypress #send_message_form textarea {
+	box-sizing: border-box;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 15px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices tr {
+	margin: 3em 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-right: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a.button {
+	border: 1px solid #d4d0ba;
+	line-height: initial;
+	padding: 0.4em 0.3em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	line-height: 1;
+	padding-bottom: 1em;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 15px;
+}
+
+.bp-user .ac_results li {
+	margin: 15px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+.bp-user .ac_results li img {
+	vertical-align: bottom;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.3333333333rem;
+	margin: 25px 0 15px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-left: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 15px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	border-radius: 0;
+	box-shadow: none;
+	box-sizing: border-box;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	line-height: 1.8;
+	min-height: 2rem;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(212, 208, 186, 0.6);
+	float: right;
+	margin: 0;
+	padding: 0 0.2em;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 800px) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress table {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin: 25px 0;
+}
+
+.bp-user #buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	padding: 0 8px;
+	text-transform: capitalize;
+}
+
+.bp-user #buddypress table tr td {
+	padding: 8px;
+}
+
+.bp-user #buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+.bp-user #buddypress .notifications th,
+.bp-user #buddypress .messages-notices th {
+	width: 30%;
+}
+
+.bp-user #buddypress .notifications th.bulk-select-all,
+.bp-user #buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+.bp-user #buddypress .notifications th.actions,
+.bp-user #buddypress .messages-notices th.actions {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .bulk-select-check,
+.bp-user #buddypress .notifications .thread-star,
+.bp-user #buddypress .messages-notices .bulk-select-check,
+.bp-user #buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions,
+.bp-user #buddypress .notifications td.thread-options,
+.bp-user #buddypress .messages-notices .notification-actions,
+.bp-user #buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions a,
+.bp-user #buddypress .notifications td.thread-options a,
+.bp-user #buddypress .messages-notices .notification-actions a,
+.bp-user #buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications td .button,
+.bp-user #buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.2rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven.css	(revision 12265)
@@ -0,0 +1,2008 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyeleven theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyeleven this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+body.buddypress:not(.page-template-sidebar-page) #content {
+	margin: 0 10%;
+	width: auto;
+}
+
+body.buddypress:not(.page-template-sidebar-page) #content .entry-header,
+body.buddypress:not(.page-template-sidebar-page) #content .entry-content {
+	width: auto;
+}
+
+.buddypress.singular.page .hentry {
+	padding-top: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+/* Here we start things off */
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a:focus, #buddypress div.item-list-tabs ul li a:hover {
+	color: #555;
+}
+
+#buddypress div.item-list-tabs ul li a:visited {
+	color: #21759b;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 450px) {
+	.bp-user #buddypress #object-nav {
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 5px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:focus:before, .bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:active:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:focus ul, .bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:active ul {
+		min-height: 320px;
+		opacity: 1;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 650px) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f2f2f2;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 1px solid #d4d0ba;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	float: right;
+	margin-top: 0;
+	padding: 5px 0 5px;
+	text-align: right;
+	width: 230px;
+}
+
+@media screen and (max-width: 450px) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		float: none;
+		width: auto;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select option {
+	font-style: normal;
+}
+
+.bp-user #buddypress div#subnav.item-list-tabs li.last {
+	margin-left: 0;
+	text-align: right;
+}
+
+@media screen and (max-width: 450px) {
+	.bp-user #buddypress div#subnav.item-list-tabs li.last {
+		clear: left;
+		float: none;
+		margin-left: 10px;
+		text-align: left;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 800px) {
+	body.bp-user #buddypress #item-header,
+	body.bp-user #buddypress #item-body {
+		background: #fff;
+	}
+	body.bp-user #buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	body.bp-user #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.bp-user #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body.bp-user #buddypress #object-nav ul li.selected {
+		background: #ddd;
+	}
+	body.bp-user #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	body.bp-user #buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 25px;
+		width: auto;
+	}
+	body.bp-user #buddypress #item-body #subnav {
+		margin: 0 0 0 -25px;
+	}
+	body.bp-user #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: left;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #373737;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress ul.item-list {
+	border-top: 0;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 15px;
+	text-align: center;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 15px;
+}
+
+@media screen and (min-width: 450px) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-left: 0;
+	overflow: hidden;
+}
+
+#buddypress ul.item-list li .item .item-meta {
+	text-align: center;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item .item-meta {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-left: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 450px) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title > a {
+	text-decoration: none;
+}
+
+#buddypress ul.item-list li .item .item-title > a:visited {
+	color: #21759b;
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.8rem;
+	padding: 15px 0;
+	text-align: left;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 5%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 18px;
+		font-size: 1.2rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin: 0;
+	margin-left: 0;
+	padding: 15px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+}
+
+@media screen and (min-width: 37em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 15px 5px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 15px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 650px) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: left;
+		margin: 15px 0 25px;
+		padding: 15px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.6;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	max-width: 220px;
+	min-height: 1.5em;
+	padding: 0 0 0 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		font-size: 12px;
+		font-size: 0.8rem;
+		float: right;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit input {
+		padding: 0 6px;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 50px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li.load-newest a {
+	display: block;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 25px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content,
+#buddypress #activity-stream li .activity-comments {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li .activity-header a:visited {
+	color: #21759b;
+}
+
+#buddypress #activity-stream li .activity-inner img {
+	height: auto;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 15px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress #activity-stream li .activity-comments {
+		margin-left: 70px;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-meta a {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin-bottom: 5px;
+	padding: 0.2em 0.5em;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		float: left;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 15px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	margin-bottom: 50px;
+}
+
+@media screen and (min-width: 650px) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.3333333333rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream .activity-comments {
+	margin-left: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	position: relative;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 0 0 2px;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li:hover * {
+	color: #555;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li .acomment-meta,
+#buddypress #activity-stream .activity-comments > ul > li .acomment-content {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	border: 1px solid #d4d0ba;
+	box-sizing: border-box;
+	margin: 15px 0;
+	width: 100%;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(55, 55, 55, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.8rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 650px) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: right;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress a:visited {
+	color: #21759b;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	text-align: left;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+	.bp-user #buddypress #item-header-content .user-nicename,
+	.single-item.groups #buddypress #item-header-content .user-nicename {
+		color: #555;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 800px) {
+	#buddypress #item-header-cover-image #item-header-content .user-nicename {
+		color: #555;
+		text-shadow: none;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 25px;
+	}
+}
+
+@media screen and (max-width: 50em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 50px;
+}
+
+.single-item.groups #buddypress div#item-header #item-header-content {
+	box-sizing: border-box;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0 !important;
+	width: 100%;
+}
+
+@media screen and (min-width: 50em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		padding-left: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-left: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	font-weight: 700;
+	line-height: 1.4;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions #group-admins li,
+.single-item.groups #buddypress div#item-header div#item-actions #group-mods li {
+	margin: 0;
+}
+
+@media screen and (min-width: 650px) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 2%;
+		padding: 0 0.5em;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: right;
+		width: 28%;
+	}
+}
+
+@media screen and (min-width: 64em) {
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+	margin-top: 225px !important;
+}
+
+@media screen and (min-width: 50em) and (max-width: 60em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		max-width: 60% !important;
+		width: 60% !important;
+	}
+}
+
+@media screen and (max-width: 64em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+		border: 0;
+		clear: both;
+		margin-top: 0 !important;
+		max-width: 100% !important;
+		padding-top: 0 !important;
+		width: auto;
+	}
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-content #item-buttons .generic-button {
+	margin-right: 5px;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		margin-right: 0%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	font-weight: 700;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(55, 55, 55, 0.6);
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 25px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li img,
+	.groups.manage-members #group-settings-form .item-list li h5 {
+		float: left;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 15px;
+	width: 100%;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: block;
+	margin: 5px 0;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		display: inline-block;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 450px) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 25px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 0 0 0;
+}
+
+.groups.group-members #subnav li #search-members-form label input[type="text"] {
+	box-shadow: none;
+}
+
+@media screen and (max-width: 450px) {
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 13px;
+	font-size: 0.8666666667rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 40%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: left;
+	margin-right: 0;
+	width: 49%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: left;
+	font-family: inherit;
+	line-height: 20px;
+	margin-left: 1%;
+	width: 50%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus, .bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.2rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle {
+	margin: 5px 0 25px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #6f6e6e;
+	border-radius: 2px;
+	background-clip: padding-box;
+	color: #eee;
+	font-weight: 700;
+	padding: 0.1em 0.5em;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:focus, .bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:hover,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:focus,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:hover {
+	color: #fff;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread #message-recipients .highlight {
+	font-size: 100%;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 15px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	box-sizing: border-box;
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom: 1px solid #ccc;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star,
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-unstar {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star span.icon:before {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.7333333333rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	border-color: #eaeaea;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress #send_message_form input,
+.bp-user #buddypress #send_message_form textarea {
+	box-sizing: border-box;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 15px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices tr {
+	margin: 3em 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-left: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a.button {
+	border: 1px solid #d4d0ba;
+	line-height: initial;
+	padding: 0.4em 0.3em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	line-height: 1;
+	padding-bottom: 1em;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 15px;
+}
+
+.bp-user .ac_results li {
+	margin: 15px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+.bp-user .ac_results li img {
+	vertical-align: bottom;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.3333333333rem;
+	margin: 25px 0 15px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-right: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 15px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	border-radius: 0;
+	box-shadow: none;
+	box-sizing: border-box;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	line-height: 1.8;
+	min-height: 2rem;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(212, 208, 186, 0.6);
+	float: left;
+	margin: 0;
+	padding: 0 0.2em;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 800px) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress table {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin: 25px 0;
+}
+
+.bp-user #buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	padding: 0 8px;
+	text-transform: capitalize;
+}
+
+.bp-user #buddypress table tr td {
+	padding: 8px;
+}
+
+.bp-user #buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+.bp-user #buddypress .notifications th,
+.bp-user #buddypress .messages-notices th {
+	width: 30%;
+}
+
+.bp-user #buddypress .notifications th.bulk-select-all,
+.bp-user #buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+.bp-user #buddypress .notifications th.actions,
+.bp-user #buddypress .messages-notices th.actions {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .bulk-select-check,
+.bp-user #buddypress .notifications .thread-star,
+.bp-user #buddypress .messages-notices .bulk-select-check,
+.bp-user #buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions,
+.bp-user #buddypress .notifications td.thread-options,
+.bp-user #buddypress .messages-notices .notification-actions,
+.bp-user #buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions a,
+.bp-user #buddypress .notifications td.thread-options a,
+.bp-user #buddypress .messages-notices .notification-actions a,
+.bp-user #buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications td .button,
+.bp-user #buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.2rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyeleven.scss	(revision 12265)
@@ -0,0 +1,2820 @@
+
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility .
+
+// 2011 Media Queries / Breakpoints.
+// 2011 works breakpoints on a max-width approach
+
+// @media screen and (max-width: 800px)
+// @media screen and (max-width: 650px)
+// @media screen and (max-width: 450px)
+
+// Mixins and Variables
+
+// simplify remembering/writing media queries
+// written as:
+// @include medium-up {
+//  body {property: value;}
+// }
+
+// BP  min width mixins
+
+// max-width 320px
+@mixin small-only {
+
+	@media screen and (max-width: 320px) {
+
+		@content;
+	}
+}
+
+// min-width 320px
+@mixin xsmall-up {
+
+	@media screen and (min-width: 320px) {
+
+		@content;
+	}
+}
+
+// min-width 450px
+@mixin small-up {
+
+	@media screen and (min-width: 450px) {
+
+		@content;
+	}
+}
+
+// min-width 650px (match to 2011 max widths)
+@mixin medium-up {
+
+	@media screen and (min-width: 650px) {
+
+		@content;
+	}
+}
+
+// min-width 800px (match to 2011 max widths)
+@mixin large-up {
+
+	@media screen and (min-width: 800px) {
+
+		@content;
+	}
+}
+
+// 2011 max-width mixins
+
+// max-width 800px
+@mixin max800 {
+
+	@media screen and (max-width: 800px) {
+
+		@content;
+	}
+}
+// max-width 650px
+@mixin max650 {
+
+	@media screen and (max-width: 650px) {
+
+		@content;
+	}
+}
+// max-width 450px
+@mixin max450 {
+
+	@media screen and (max-width: 450px) {
+
+		@content;
+	}
+}
+
+// Common spacing values
+$spacing-val-lg: 50px;
+$spacing-val-md: 25px;
+$spacing-val-sm: 15px;
+$spacing-val-xs: 5px;
+
+// Twentyeleven sizes fonts using pixel values;
+// we'll continue to set BP sizes via our  rem/pixel mixin.
+
+// A simple mixin to handle font-sizing
+// 2011 sets a html/body size @ 15px
+
+@mixin font-size($font-size: 15) {
+	$rem-font-value: ($font-size / 15);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($box-model) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2011 font family
+@mixin default-font-sans() {
+	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+// Variables: color definitions
+$body-text:          #373737;
+$content-background: #fff;
+$off-white:          lighten(#f9f8ee, 0.7);
+$cream-background:   #f7f5e7;
+$light-background:   #f7f7f7;
+$medium-background:  #ccc;
+$dark-background:    #555;
+$border-color:       #d4d0ba;    // border color is varied using rgba
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$background-hover:   #db572f; // 2011 link background hover
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #9fd1e2;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             #9dc3c8;
+$link-action:        #c82b2b;
+
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyeleven theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyeleven this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+
+
+.buddypress {
+	// Remove any empty clearing elements, CSS provides clearing.
+	div.clear {
+		display: none;
+	}
+
+} // .buddypress (body class)
+
+// 2011 page templates do not support sidebars, page entry element kept to a
+// narrow width - we override this to allow bp-user screens to
+// maximise screen real estate.
+
+// Main dir pages: sidebar or not to sidebar
+// If no sidebar lets fatten the contents girth
+// User account single screens never have a sidebar
+body.buddypress:not(.page-template-sidebar-page) {
+
+	#content {
+		margin: 0 10%;
+		width: auto;
+
+		.entry-header,
+		.entry-content {
+			width: auto;
+		}
+	}
+}
+
+// Reduce the top padding for single pages, too extreme for BP screens.
+.buddypress.singular.page {
+
+	.hentry {
+		padding-top: 0;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets will appear over weighted in selectors
+
+/* Here we start things off */
+#buddypress {
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected,
+			li.current {
+
+				a {
+					background: inherit;
+					opacity: 1;
+				}
+			}
+		}
+	}
+	// global nav styles
+	div.item-list-tabs {
+
+		ul {
+			overflow: hidden;
+			padding: 0;
+
+			li {
+
+				a {
+
+					&:focus,
+					&:hover {
+						color: #555;
+					}
+					// Keep links from inheriting :visited states
+					&:visited {
+						color: #21759b;
+					}
+
+					span {
+						border-radius: 25%;
+					}
+				}
+
+			}
+		}
+	} // close item-list-tabs
+
+} // close #buddypress
+
+// .bp-user #object-nav @ small screen as a dropdown
+
+.bp-user {
+
+	#buddypress {
+
+		@include max450 {
+
+			#object-nav {
+				border: 1px solid $border-light;
+				overflow: visible;
+				padding: $spacing-val-xs;
+
+				ul {
+					border: 0;
+					height: 0;
+					// all latest vendor releases handle transitions, so no prefixes used.
+					transition: height 0.3s ease-in-out 0.7s;
+					visibility: hidden;
+				}
+
+				&:before {
+					content: "Menu \021E9";
+					display: inline;
+					opacity: 0.7;
+				}
+
+				&:focus,
+				&:hover,
+				&:active {
+
+					&:before {
+						content: "Menu \021E7";
+					}
+
+					ul {
+						min-height: 320px;
+						opacity: 1;
+						visibility: visible;
+					}
+				}
+			}
+
+			#subnav {
+				clear: both;
+			}
+
+		} // close @media
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress {
+
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+				float: none;
+			}
+
+			@include max650 {
+
+				li:not(:last-child) {
+					border-bottom: 1px solid $border-light;
+				}
+
+				li:not(.selected) {
+
+					&:focus,
+					&:hover {
+						background: darken($light-background, 2%);
+					}
+				}
+
+			} // close @media
+
+			@include medium-up {
+
+				li {
+					float: left;
+				}
+			}
+
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			border-bottom: 0;
+			border-top: 1px solid $border-color;
+			margin-top: $spacing-val-xs;
+			padding: 0;
+
+			li.last {
+				background: $content-background;
+				float: right;
+				margin-top: 0;
+				padding: $spacing-val-xs 0 $spacing-val-xs;
+				text-align: right;
+				width: 230px;
+
+				@include max450 {
+					float: none;
+					width: auto;
+				}
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+					padding: 0;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+
+					option {
+						font-style: normal;
+					}
+				}
+			}
+
+		} // close ul
+	} // close #subnav
+
+} // close #buddypress
+
+.bp-user {
+
+	#buddypress {
+
+		div#subnav.item-list-tabs {
+
+			li.last {
+				margin-left: 0;
+				text-align: right;
+
+				@include max450 {
+					clear: left;
+					float: none;
+					margin-left: 10px;
+					text-align: left;
+				}
+			}
+		}
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+
+// The pages templates in 2011 do not support sidebars
+
+@include large-up {
+
+	body.bp-user {
+
+		#buddypress {
+
+			#item-header,
+			#item-body {
+				background: $content-background;
+			}
+
+			#object-nav {
+				border-right: 1px solid $border-med;
+				float: left;
+				margin-right: -1px;
+				width: 200px;
+
+				ul {
+					background: none;
+					border-bottom: 0;
+					padding: 0;
+
+					li {
+						float: none;
+						overflow: hidden;
+
+						&.selected {
+							background: #ddd;
+						}
+
+						span {
+							background: $content-background;
+							border-radius: 10%;
+							float: right;
+							margin-right: 2px;
+						}
+					}
+				}
+			}
+
+			#item-body {
+				border-left: 1px solid $border-med;
+				overflow: hidden;
+				padding: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				#subnav {
+					margin: 0 0 0 (-$spacing-val-md);
+
+					ul {
+						margin-top: 0;
+					}
+				}
+			}
+
+		} // close  #buddypress
+	} // close body.bp-user
+} // close @media
+
+
+// Tabbed Nav styling
+// Primarilly used for group create screens & ? directory nav.
+
+@include medium-up {
+
+	#main {
+
+		#buddypress {
+
+			#group-create-tabs.item-list-tabs {
+
+				ul:before,
+				ul:after {
+					content: " ";
+					display: table;
+				}
+
+				ul:after {
+					clear: both;
+				}
+
+				ul {
+					background: none;
+					border: 0;
+					border-bottom: 1px solid $border-med;
+					overflow: visible;
+					padding-bottom: 0;
+
+					li {
+						float: left;
+						width: auto;
+					}
+
+					li.selected,
+					li.current {
+						border: 1px solid $border-med;
+						border-bottom-color: $content-background;
+
+						@include border-top-radius(4px);
+						margin-bottom: -1px;
+
+						a {
+							background: none;
+							color: $body-text;
+							outline: none;
+						}
+					}
+				}
+
+			} // close .item-list-tabs
+
+			#subnav {
+
+				ul {
+					border-bottom: 0;
+				}
+
+			} // close #subnav
+		} // close #buddypress
+	}
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+		min-height: 0;
+
+		&:before,
+		&:after {
+			height: 0;
+			width: 0;
+		}
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2011 font-family
+
+#buddypress {
+
+	ul.item-list {
+		border-top: 0;
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@include medium-up {
+					margin-bottom: 0;
+				}
+
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+					margin-bottom: $spacing-val-sm;
+
+					// small device: move avatars to the left(right)
+					@include small-up {
+						display: block;
+						float: left;
+					}
+				}
+			} //close .item-avatar
+
+			.item {
+
+				// counter the BP rule
+				margin-left: 0;
+				overflow: hidden;
+
+				.item-meta {
+					text-align: center;
+
+					@media screen and (min-width: 61.5625em) {
+						text-align: left;
+					}
+				}
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin-left: 0;
+					width: 94%;
+				}
+
+				.item-title {
+
+					@include font-size(16);
+					line-height: 1.2;
+					text-align: center;
+
+					@include small-up {
+						text-align: left;
+					}
+
+					width: 100%;
+
+					> a {
+						text-decoration: none;
+
+						&:visited {
+							color: #21759b;
+						}
+					}
+
+					.update {
+						display: block;
+
+						@include font-size(12);
+
+						@include medium-up {
+
+							@include font-size(14);
+						}
+
+						padding: $spacing-val-sm 0;
+						text-align: left;
+					}
+				}  // close .item-title
+
+			} // close .item
+
+			@include medium-up {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 5%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+
+					.item-title {
+
+						@include font-size(18);
+					}
+				}
+
+			}// close medium-up
+
+			div.action {
+				clear: left;
+				float: none;
+				margin: 0;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-sm 0 $spacing-val-xs;
+
+				// push the actions to the right
+				@include medium-up {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-xs 0;
+					width: 100%;
+
+					a {
+						display: block;
+					}
+					// Provisionally lets keep the 'action' div
+					//position but inline-block the buttons
+
+					@media screen and (min-width: 37em) {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm 5px 0;
+						width: auto;
+					} // close @media
+
+					@include medium-up {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		@include max650 {
+
+			#whats-new-content {
+				clear: left;
+				margin: $spacing-val-sm 0 $spacing-val-md;
+				padding: $spacing-val-sm 0 0;
+			}
+		}
+
+		#whats-new-content.active {
+
+			#whats-new-options[style] {
+
+				#whats-new-post-in-box {
+					border: 1px solid rgba($border-color, 0.5);
+					float: left;
+
+					@include font-size(12);
+					line-height: 1.6;
+					margin-top: 12px;
+					padding-left: 0.2em;
+					width: 100%;
+
+					select {
+						background: none;
+						border: 0;
+						float: right;
+						margin: 0;
+						max-width: 220px;
+						min-height: 1.5em;
+						padding: 0 0 0 0.4em;
+					}
+				}
+
+				@media screen and (min-width: 30em) {
+
+					#whats-new-post-in-box {
+						width: auto;
+					}
+
+					#whats-new-submit {
+
+						@include font-size(12);
+						float: right;
+
+						input {
+							padding: 0 6px;
+						}
+					}
+				}
+
+			} // close #whats-new-options
+		} // close #whats-new-content.active
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			&.load-newest {
+
+				a {
+					display: block;
+				}
+			}
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content,
+			.activity-comments {
+				margin-left: 0;
+			}
+
+			.activity-header {
+
+				@include font-size(14);
+
+				a:visited {
+					color: #21759b;
+				}
+			}
+
+			.activity-inner {
+
+				img {
+					height: auto;
+				}
+			}
+
+			@include medium-up {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+				.activity-comments {
+					margin-left: 70px;
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+
+				.activity-meta {
+
+					a {
+
+						@include font-size(12);
+					}
+				}
+			}
+		} // close li.mini
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				line-height: inherit;
+				margin-right: 0;
+
+				p {
+					border-bottom: 1px solid rgba($border-light, 0.6);
+					margin-top: 0;
+					padding: 0 0.2em;
+				}
+
+				img.avatar {
+					display: inline-block;
+					margin-bottom: 0 !important; // have to match to BP
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+
+					@include font-size(14);
+					margin-bottom: $spacing-val-xs;
+
+					@include medium-up {
+						float: left;
+						margin-bottom: 0;
+					}
+
+					padding: 0.2em 0.5em;
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@include medium-up {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			} // close li.mini
+
+			.activity-comments {
+				margin-left: 0;
+			}
+
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			position: relative;
+
+			> ul {
+				background: rgba($light-background, 0.6);
+				margin: $spacing-val-sm 0 0 2px;
+
+				> li {
+
+					&:hover * {
+						color: #555;
+					}
+
+					.acomment-meta,
+					.acomment-content {
+
+						@include font-size(12);
+					}
+
+				}
+
+			}
+
+			.ac-form {
+				border: 1px solid $border-color;
+				box-sizing: border-box;
+				margin: $spacing-val-sm 0;
+				width: 100%;
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+
+	} // close #activity-steam
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@include medium-up {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#signup_form.standard-form {
+
+		#profile-details-section,
+		#basic-details-section,
+		#blog-details-section {
+			float: none;
+			width: 100%;
+
+			@media screen and (min-width: 37.5em) {
+				width: 48%;
+			}
+		}
+
+		@media screen and (min-width: 37.5em) {
+
+			#profile-details-section {
+				float: right;
+			}
+
+			#basic-details-section {
+				float: left;
+			}
+		}
+
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+		// On object nav links, table links (notifications etc) we don't really
+		// need :visited inheritance, so set pseudo class to :link color.
+		a {
+
+			&:visited {
+				color: #21759b;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+.bp-user,
+.single-item.groups {
+
+	#buddypress {
+
+		#item-header-content {
+
+			#item-meta {
+
+				@include font-size(14);
+				text-align: left;
+
+				p {
+					margin-bottom: 0.5em;
+				}
+			}
+		}
+	}
+
+	@media screen and (max-width: 37.5em) {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	@media screen and (max-width: 46.25em) {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	#buddypress {
+
+		@media screen and (max-width: 37.5em) {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+
+				.user-nicename {
+					color: #555;
+				}
+			}
+		} // close @media
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+	} // close #buddypress
+
+}
+
+// Manage cover images
+@include max800 {
+
+	#buddypress {
+
+		#item-header-cover-image {
+
+			#item-header-content {
+
+				.user-nicename {
+					color: #555;
+					text-shadow: none;
+				}
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@media screen and (max-width: 50em) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+
+					h2 {
+						border-bottom: 1px solid $border-light;
+						text-align: center;
+					}
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			#item-header-content {
+
+				@include box-model(border-box);
+			}
+
+			div#item-actions {
+				margin: 0 !important;
+				width: 100%;
+
+				@media screen and (min-width: 50em) {
+					border-left: 1px solid $border-light;
+					clear: none;
+					float: right;
+					padding-left: 0.2em;
+					width: 30%;
+				} // close @media
+
+				@media screen and (min-width: 46.25em) {
+					width: 40%;
+				}
+
+				ul {
+					margin-top: 0;
+					padding-left: 0;
+				}
+
+				h2 {
+
+					@include font-size(14);
+					font-weight: 700;
+					line-height: 1.4;
+				}
+
+				#group-admins,
+				#group-mods {
+
+					li {
+						margin: 0;
+					}
+				}
+
+			} // close item-actions
+
+			@include medium-up {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-content {
+					margin-left: 2%;
+					padding: 0 0.5em;
+				}
+
+				div#item-actions {
+					float: right;
+					width: 28%;
+				}
+
+			} // close @media
+
+			@media screen and (min-width: 64em) {
+
+				#item-header-content {
+					width: 40%;
+				}
+			}
+		} // close #item-header
+	} // close #buddypress
+
+	// These rules handle the specific instance of Cover Images on single
+	// groups activated & attempt to address positioning concerns arising
+	// from themes breakpoints. Due to cover image styles being applied via
+	// embedded styles & thus carrying heavier weight than linked styles
+	// the use of !important to override may be required.
+	#buddypress {
+
+		#cover-image-container {
+
+			#item-header-cover-image {
+
+				#item-actions {
+					margin-top: 225px !important;
+				}
+
+				@media screen and (min-width: 50em) and (max-width: 60em) {
+
+					#item-header-content {
+						max-width: 60% !important;
+						width: 60% !important;
+					}
+				}
+
+				@media screen and (max-width: 64em) {
+
+					#item-actions {
+						border: 0;
+						clear: both;
+						margin-top: 0 !important;
+						max-width: 100% !important;
+						padding-top: 0 !important;
+						width: auto;
+
+						h2 {
+							border-bottom: 1px solid $border-light;
+						}
+					}
+				} // @media
+			}
+		}
+	}
+
+} // close .single-item.groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+
+			#item-header-avatar {
+				text-align: center;
+
+				img.avatar,
+				a {
+					border-bottom: 0;
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			#item-header-content {
+
+				#item-buttons {
+
+					.generic-button {
+						margin-right: 5px;
+					}
+				}
+			}
+
+			@include medium-up {
+
+				#item-header-avatar {
+					float: left;
+
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: right;
+					margin-right: 0%;
+					width: 69%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+		textarea {
+			height: auto;
+			min-height: 100px;
+			overflow: auto;
+		}
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form {
+
+		div.radio label {
+			font-weight: 700;
+			padding: 0.2em;
+
+			ul {
+				color: rgba($body-text, 0.6);
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+
+					@include medium-up {
+						float: left;
+					}
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+					width: 100%;
+
+					a {
+						display: block;
+						margin: $spacing-val-xs 0;
+
+						@include medium-up {
+							display: inline-block;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+
+			@include max450 {
+				background: $content-background;
+				padding: $spacing-val-md 0;
+			}
+
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+
+				label {
+
+					input[type="text"] {
+						box-shadow: none;
+
+						@include max450 {
+							width: 100%;
+						}
+					}
+				}
+
+				margin: $spacing-val-xs 0 0 0;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(13);
+			}
+
+			td {
+
+				@include font-size(12);
+			}
+
+			@include medium-up {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav,
+		.messages-options-nav {
+			float: left;
+			width: 100%;
+
+			@media screen and (min-width: 37.5em) {
+				width: 40%;
+			}
+
+			select,
+			input {
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 0;
+				width: 49%;
+
+				@media screen and (min-width: 37.5em) {
+					width: auto;
+				}
+			}
+
+			input {
+				float: left;
+				font-family: inherit;
+				line-height: 20px;
+				margin-left: 1%;
+				width: 50%;
+
+				@media screen and (min-width: 37.5em) {
+					width: auto;
+				}
+
+				&[disabled="disabled"]:focus,
+				&[disabled="disabled"]:hover {
+					background: none;
+				}
+			}
+
+		} // close .notifications-options-nav/messages-options-nav
+
+	} //close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			.bp-widget {
+
+				h2 {
+					background: lighten($dark-background, 10%);
+					color: #fff;
+					margin-bottom: 0;
+					padding: 0.4em;
+				}
+
+				table {
+					margin-top: 0;
+				}
+			}
+
+			// Edit profile
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle {
+					margin: $spacing-val-xs 0 $spacing-val-md;
+				}
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: lighten($dark-background, 10%);
+
+					@include border-radius(2px);
+					color: #eee;
+					font-weight: 700;
+					padding: 0.1em 0.5em;
+					text-decoration: none;
+
+					&:focus,
+					&:hover {
+						color: #fff;
+					}
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+
+	} // close #buddypress
+} // close .bp-user
+
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+
+				.highlight {
+					font-size: 100%;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: lighten($dark-background, 10%);
+				}
+			}
+
+			tr {
+
+				td {
+					background: $content-background;
+					box-sizing: border-box;
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom: 1px solid $medium-background;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+					}
+				}
+
+				td.thread-star {
+					vertical-align: middle;
+
+					.message-action-star,
+					.message-action-unstar {
+						line-height: 1.2;
+					}
+
+					span.icon:before {
+
+						@include font-size(14);
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 3em;
+
+					@media screen and (max-width: 37.5em) {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 1.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@media screen and (max-width: 37.5em) {
+						clear: both;
+
+						@include font-size(11);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					border-color: $border-light;
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		#send_message_form {
+
+			input,
+			textarea {
+				box-sizing: border-box;
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		#message-threads.sitewide-notices {
+
+			tr {
+				margin: 3em 0;
+			}
+
+			td {
+				width: 100%;
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					margin-bottom: 0.4em;
+					padding-left: 0.2em;
+				}
+
+				a {
+					display: inline-block;
+				}
+
+				a.button {
+					border: 1px solid $border-color;
+					line-height: initial;
+					padding: 0.4em 0.3em;
+				}
+			}
+
+			td:first-child {
+				display: none;
+			}
+
+			td:nth-child(2) {
+
+				strong {
+					margin: -8px -8px 8px;
+				}
+			}
+
+			td:first-child + td + td {
+				// notice date
+				border-bottom: 0;
+
+				span {
+					line-height: 1;
+				}
+			}
+
+			td:last-child {
+				// notice actions
+				line-height: 1;
+				padding-bottom: 1em;
+				text-align: right;
+
+				a:last-child:after {
+					content: attr(title);
+					display: block;
+					line-height: initial;
+					text-indent: 0;
+				}
+			}
+		}
+
+	} // #buddypress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+
+			img {
+				vertical-align: bottom;
+			}
+
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+
+		// Profile table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #buddyPress
+
+} // close .bp-user
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Attempt to reset form control widths
+#main {
+
+	#buddypress {
+
+		.standard-form {
+
+			li {
+				float: none;
+			}
+
+			input[type="text"],
+			input[type="email"],
+			input[type="password"],
+			textarea {
+				width: 100%;
+			}
+		}
+	}
+}
+
+
+#buddypress {
+
+	// Match BP form elements to Themes(2012) styling where styles
+	// are not inherited or being overriden.
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.5);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	.standard-form {
+
+		// Keep BP form element borders to 1px rather than 2013 2px
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		button,
+		select,
+		textarea {
+			border-color: rgba($border-color, 0.5);
+			border-width: 1px;
+		}
+
+	} // close .standard-form
+
+	#signup_form.standard-form {
+
+		div.submit {
+			float: none;
+
+			input {
+				margin-right: 0;
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.6);
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 70%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+				border: 0;
+				border-radius: 0;
+				box-shadow: none;
+				box-sizing: border-box;
+
+				@include font-size(14);
+				line-height: 1.8;
+				min-height: 2rem;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.6);
+				float: left;
+				margin: 0;
+				padding: 0 0.2em;
+				width: 70%;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0 1em;
+				text-align: center;
+				text-transform: none;
+				width: 30%;
+			}
+		} // close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+
+	@media screen and (min-width: 37.5em) {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+			float: right;
+			margin-bottom: 5px !important;
+
+			form {
+
+				label,
+				input[type="text"],
+				input[type="submit"] {
+					width: auto;
+				}
+			}
+		}
+
+	} // close @media
+
+
+	@include large-up {
+
+		.dir-search,
+		.message-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16);
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16);
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			@include font-size(14);
+			margin: $spacing-val-md 0;
+
+			tr th {
+				background: lighten($dark-background, 10%);
+				border-color: darken($border-light, 20%);
+				color: $content-background;
+
+				@include font-size(16);
+				padding: 0 8px;
+				text-transform: capitalize;
+			}
+
+			tr td {
+				padding: 8px;
+			}
+
+			// Reduce the themes inherited paragraph margins in tables
+			p {
+				margin-bottom: 0.5em;
+			}
+		}
+
+		@media screen and (min-width: 55em) {
+
+			table {
+
+				@include font-size(16);
+			}
+		}
+
+		// Manage some table cells widths that are disproportionate to their content
+		.notifications,
+		.messages-notices {
+
+			th {
+				width: 30%;
+
+				&.bulk-select-all {
+					text-align: center;
+					width: 10%;
+				}
+			}
+
+			th.actions {
+				text-align: center;
+			}
+
+			.bulk-select-check,
+			.thread-star {
+				text-align: center;
+			}
+
+			.notification-actions,
+			td.thread-options {
+				text-align: center;
+
+				a {
+					display: inline-block;
+					margin: 0;
+					padding: 0;
+				}
+			}
+
+			// Notices action buttons, this maybe better moved, temp for now to address
+			// styling issues - this will need styling ideas
+			td {
+
+				.button {
+					border: 0;
+					display: block;
+					padding: 0;
+					text-align: center;
+				}
+			}
+
+		} // .notifications, .message-notices
+
+	} // #buddypress
+} // .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning' )
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen-rtl.css	(revision 12265)
@@ -0,0 +1,1956 @@
+/*------------------------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyfifteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfifteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+--------------------------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Forums
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group Send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+------------------------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements & overrides
+*-------------------------------------------------------------------------------
+*/
+#buddypress h1,
+#buddypress h2,
+#buddypress h3,
+#buddypress h4,
+#buddypress h5,
+#buddypress h6 {
+	clear: none;
+}
+
+.buddypress div.clear {
+	display: none;
+}
+
+.buddypress main {
+	padding-top: 4%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress main {
+		padding-top: 0;
+	}
+}
+
+.buddypress main article {
+	margin: 0 4%;
+	padding-top: 8.3333%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress main article {
+		margin: 0 1px 0 0;
+	}
+}
+
+@media screen and (min-width: 87.6875em) {
+	.buddypress main article {
+		margin: 0 8.3333% 0 4.3333%;
+	}
+}
+
+.buddypress main article .entry-header,
+.buddypress main article .entry-content {
+	padding: 0 3rem 3rem;
+}
+
+.buddypress main article #buddypress {
+	margin-bottom: 40px;
+}
+
+.buddypress .site-footer {
+	margin: 0 4%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress .site-footer {
+		margin: 0 35.2941% 0 0;
+		width: 61.8235%;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+.buddypress #buddypress .item-list-tabs a,
+.buddypress #buddypress .activity-header a,
+.buddypress #buddypress .activity-inner a,
+.buddypress #buddypress .activity-comments a,
+.buddypress #buddypress .item-title a,
+.buddypress #buddypress .load-more a,
+.buddypress #buddypress .field-visibility-settings-toggle a,
+.buddypress #buddypress #latest-update a,
+.buddypress #buddypress table a,
+.buddypress #buddypress .avatar-nav-items a {
+	border-bottom: 0;
+}
+
+.buddypress #buddypress .pagination-links a,
+.buddypress #buddypress .pagination-links span {
+	border-bottom: 0;
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress {
+	/*__ Horizontal menus __*/
+}
+
+#buddypress div.item-list-tabs ul li.selected a {
+	background: #555;
+	color: #fff;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 5px 0;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+#buddypress #object-nav ul li:not(.selected) a {
+	opacity: 0.7;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	margin-top: 0;
+	padding: 5px 0 5px 5px;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: left;
+	}
+}
+
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: #555;
+	color: #fff;
+	opacity: 1;
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress,
+	.single-item.groups #buddypress {
+		background: #f7f7f7;
+	}
+	#buddypress #item-header,
+	#buddypress #item-body {
+		background: #fff;
+	}
+	#buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	#buddypress #object-nav ul {
+		border-bottom: 0;
+		padding: 0;
+	}
+	#buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	#buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	#buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 20px 0 0;
+		width: auto;
+	}
+	#buddypress #item-body #subnav {
+		margin: 0 -20px 0 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Noto Sans", sans-serif;
+}
+
+#buddypress .item-list a.activity-time-since {
+	color: #717171;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-right: 25%;
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-right: 0;
+	width: 70%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 10%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin-bottom: -20px;
+	margin-right: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 10px 10px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-post-in-box select {
+		font-size: 14px;
+		font-size: 1.4rem;
+		max-width: 120px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: right;
+		margin: 10px 0 40px;
+		padding: 10px 0 0;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-submit {
+		float: none;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-submit input {
+		width: 100%;
+	}
+	#buddypress form#whats-new-form #whats-new-options {
+		display: flex;
+		flex-direction: column;
+	}
+	#buddypress form#whats-new-form #whats-new-options #whats-new-submit {
+		order: 2;
+	}
+	#buddypress form#whats-new-form #whats-new-options #whats-new-post-in-box {
+		order: 1;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(153, 153, 153, 0.5);
+	float: right;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	min-height: 1.5em;
+	outline: 0;
+	padding-right: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-options[style] #whats-new-submit {
+		float: left;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 10px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 13px;
+	font-size: 1.3rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	background-color: #f7f7f7;
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	color: #717171;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin: 0 0 5px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress {
+	background: none;
+}
+
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px 0;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.6rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 2rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	margin: 10px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments a {
+	color: #717171;
+}
+
+#buddypress #activity-stream .activity-comments.has-comments {
+	border-right: 1px solid #eaeaea;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	padding: 20px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 2px 0 0;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-comments > ul {
+		margin-right: 70px;
+	}
+}
+
+#buddypress #activity-stream .activity-comments > ul ul {
+	margin-right: 1%;
+	padding-right: 0;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #activity-stream .activity-comments > ul ul {
+		margin-right: 1%;
+		padding-right: 1em;
+	}
+}
+
+#buddypress #activity-stream .activity-comments > ul ul li {
+	border-right: 1px solid #eaeaea;
+	border-top: 0;
+	padding-right: 0.2em;
+}
+
+#buddypress #activity-stream .activity-comments > ul .acomment-meta {
+	border-bottom: 1px solid #eaeaea;
+	color: #737373;
+	font-style: italic;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #activity-stream .activity-comments > ul .acomment-avatar {
+		display: block;
+		text-align: center;
+	}
+	#buddypress #activity-stream .activity-comments > ul .acomment-avatar a,
+	#buddypress #activity-stream .activity-comments > ul .acomment-avatar img.avatar {
+		display: inline;
+		float: none;
+	}
+	#buddypress #activity-stream .activity-comments > ul .acomment-content,
+	#buddypress #activity-stream .activity-comments > ul .acomment-meta,
+	#buddypress #activity-stream .activity-comments > ul .acomment-options {
+		margin: 5px;
+	}
+	#buddypress #activity-stream .activity-comments > ul .acomment-content {
+		padding: 0 10px;
+	}
+}
+
+#buddypress #activity-stream .activity-comments .ac-reply-avatar {
+	display: none;
+}
+
+#buddypress #activity-stream .activity-comments .ac-reply-content {
+	margin-right: 0;
+	padding-right: 0;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-comments .ac-reply-avatar {
+		display: block;
+	}
+	#buddypress #activity-stream .activity-comments .ac-reply-content {
+		overflow: hidden;
+	}
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(51, 51, 51, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 1.2rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(153, 153, 153, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 59.6875em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		clear: none;
+		float: left;
+		width: 50%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	background: #555;
+	color: #fff;
+	font-size: 14px;
+	font-size: 1.4rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: left;
+		width: 28%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		margin-left: 5%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+#item-body h1,
+#item-body h2,
+#item-body h3,
+#item-body h4,
+#item-body h5,
+#item-body h6 {
+	margin: 1em 0;
+}
+
+#item-body h1 a,
+#item-body h2 a,
+#item-body h3 a,
+#item-body h4 a,
+#item-body h5 a,
+#item-body h6 a {
+	border-bottom: 0;
+}
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups #group-settings-form #request-list h4 {
+	background: none;
+	color: inherit;
+}
+
+.groups.edit-details #group-settings-form label {
+	background: #555;
+	border-top-left-radius: 4px;
+	border-top-right-radius: 4px;
+	background-clip: padding-box;
+	color: #fff;
+	display: inline-block;
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.edit-details #group-settings-form label {
+		width: 60%;
+	}
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 1.4rem;
+	width: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label,
+.groups.group-settings #create-group-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio ul,
+.groups.group-settings #create-group-form div.radio ul {
+	color: #767676;
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: right;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+/* Members loop */
+@media screen and (max-width: 30em) {
+	.groups.group-members #buddypress #members-group-list li > a {
+		border-bottom: 0;
+		display: block;
+		margin-bottom: 10px;
+		text-align: center;
+	}
+	.groups.group-members #buddypress #members-group-list li > a img.avatar {
+		display: inline;
+		float: none;
+	}
+}
+
+.groups.group-members #buddypress #members-group-list li h5 {
+	display: inline-block;
+	font-size: 14px;
+	font-size: 1.4rem;
+	margin: 0;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.groups.group-members #buddypress #members-group-list li h5 {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+.groups.group-members #buddypress #members-group-list li h5 a {
+	border-bottom: 0;
+}
+
+.groups.group-members #buddypress #members-group-list li span.activity {
+	font-size: 12px;
+	font-size: 1.2rem;
+}
+
+.groups.group-members #buddypress #members-group-list li .action {
+	top: 0;
+}
+
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0 5px;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.3 - Group Send Invites
+*-----------------------------------------
+*/
+.groups.group-invites #item-body .item-list .action {
+	margin-bottom: 0;
+}
+
+@media screen and (min-width: 46.25em) {
+	.groups.group-invites #buddypress #item-body #message {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	.groups.group-invites #buddypress #send-invite-form {
+		margin-top: 0;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.groups.group-invites #item-body .left-menu {
+		float: none;
+		margin: 10px 0;
+	}
+	.groups.group-invites #item-body .left-menu #invite-list {
+		height: auto;
+		width: auto;
+	}
+	.groups.group-invites #item-body .main-column {
+		margin-right: 0;
+	}
+	.groups.group-invites #item-body .submit input {
+		display: inline-block;
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 1.2rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 1.4rem;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.8rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 50%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 1.4rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: right;
+	margin-left: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: 60%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: left;
+	font-family: "Noto Serif", serif;
+	line-height: 1.5;
+	margin-top: 10px;
+	width: 100%;
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled]:hover {
+	background: none;
+}
+
+@media screen and (min-width: 30em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		margin-top: 0;
+		width: 38%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+/* Edit Profile */
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.8rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		right: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #b7b7b7;
+	border-bottom-width: 2px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 2.6em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 1.2rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 1.1rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: #dce5ff;
+	border-color: #d1d0d0;
+	border-bottom-color: #b7b7b7;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-right: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: #b7b7b7;
+	line-height: 1;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 2rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings th.title {
+	width: 75%;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(153, 153, 153, 0.3);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress select {
+	border: 1px solid rgba(153, 153, 153, 0.5);
+}
+
+#buddypress .wp-editor-wrap a.button,
+#buddypress .wp-editor-wrap button,
+#buddypress .wp-editor-wrap input[type="submit"],
+#buddypress .wp-editor-wrap input[type="button"],
+#buddypress .wp-editor-wrap input[type="reset"] {
+	padding: 0 5px 1px;
+}
+
+#buddypress .standard-form li,
+#buddypress .group-create-form li {
+	float: none;
+	list-style: none;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form textarea,
+#buddypress .group-create-form input[type="text"],
+#buddypress .group-create-form textarea {
+	width: 100%;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(153, 153, 153, 0.4);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: right;
+	margin: 0;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 1.4rem;
+	border: 0;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(153, 153, 153, 0.4);
+	padding: 0.2em 0.2em 0.2em 0;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 20%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 1.4rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress #item-body div#message {
+	margin-top: 20px;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress #item-body div#message {
+		margin-left: 20px;
+	}
+}
+
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.8rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen.css	(revision 12265)
@@ -0,0 +1,1956 @@
+/*------------------------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyfifteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfifteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+--------------------------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Forums
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group Send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+------------------------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements & overrides
+*-------------------------------------------------------------------------------
+*/
+#buddypress h1,
+#buddypress h2,
+#buddypress h3,
+#buddypress h4,
+#buddypress h5,
+#buddypress h6 {
+	clear: none;
+}
+
+.buddypress div.clear {
+	display: none;
+}
+
+.buddypress main {
+	padding-top: 4%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress main {
+		padding-top: 0;
+	}
+}
+
+.buddypress main article {
+	margin: 0 4%;
+	padding-top: 8.3333%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress main article {
+		margin: 0 0 0 1px;
+	}
+}
+
+@media screen and (min-width: 87.6875em) {
+	.buddypress main article {
+		margin: 0 4.3333% 0 8.3333%;
+	}
+}
+
+.buddypress main article .entry-header,
+.buddypress main article .entry-content {
+	padding: 0 3rem 3rem;
+}
+
+.buddypress main article #buddypress {
+	margin-bottom: 40px;
+}
+
+.buddypress .site-footer {
+	margin: 0 4%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress .site-footer {
+		margin: 0 0 0 35.2941%;
+		width: 61.8235%;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+.buddypress #buddypress .item-list-tabs a,
+.buddypress #buddypress .activity-header a,
+.buddypress #buddypress .activity-inner a,
+.buddypress #buddypress .activity-comments a,
+.buddypress #buddypress .item-title a,
+.buddypress #buddypress .load-more a,
+.buddypress #buddypress .field-visibility-settings-toggle a,
+.buddypress #buddypress #latest-update a,
+.buddypress #buddypress table a,
+.buddypress #buddypress .avatar-nav-items a {
+	border-bottom: 0;
+}
+
+.buddypress #buddypress .pagination-links a,
+.buddypress #buddypress .pagination-links span {
+	border-bottom: 0;
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress {
+	/*__ Horizontal menus __*/
+}
+
+#buddypress div.item-list-tabs ul li.selected a {
+	background: #555;
+	color: #fff;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 5px 0;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+#buddypress #object-nav ul li:not(.selected) a {
+	opacity: 0.7;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	margin-top: 0;
+	padding: 5px 5px 5px 0;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: right;
+	}
+}
+
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: #555;
+	color: #fff;
+	opacity: 1;
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress,
+	.single-item.groups #buddypress {
+		background: #f7f7f7;
+	}
+	#buddypress #item-header,
+	#buddypress #item-body {
+		background: #fff;
+	}
+	#buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	#buddypress #object-nav ul {
+		border-bottom: 0;
+		padding: 0;
+	}
+	#buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	#buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	#buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 20px;
+		width: auto;
+	}
+	#buddypress #item-body #subnav {
+		margin: 0 0 0 -20px;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Noto Sans", sans-serif;
+}
+
+#buddypress .item-list a.activity-time-since {
+	color: #717171;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-left: 25%;
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-left: 0;
+	width: 70%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 10%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin-bottom: -20px;
+	margin-left: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 10px 10px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-post-in-box select {
+		font-size: 14px;
+		font-size: 1.4rem;
+		max-width: 120px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: left;
+		margin: 10px 0 40px;
+		padding: 10px 0 0;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-submit {
+		float: none;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-submit input {
+		width: 100%;
+	}
+	#buddypress form#whats-new-form #whats-new-options {
+		display: flex;
+		flex-direction: column;
+	}
+	#buddypress form#whats-new-form #whats-new-options #whats-new-submit {
+		order: 2;
+	}
+	#buddypress form#whats-new-form #whats-new-options #whats-new-post-in-box {
+		order: 1;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(153, 153, 153, 0.5);
+	float: left;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	min-height: 1.5em;
+	outline: 0;
+	padding-left: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-options[style] #whats-new-submit {
+		float: right;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 10px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 13px;
+	font-size: 1.3rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	background-color: #f7f7f7;
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	color: #717171;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin: 0 0 5px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress {
+	background: none;
+}
+
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px 0;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.6rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 2rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	margin: 10px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments a {
+	color: #717171;
+}
+
+#buddypress #activity-stream .activity-comments.has-comments {
+	border-left: 1px solid #eaeaea;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	padding: 20px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 0 0 2px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-comments > ul {
+		margin-left: 70px;
+	}
+}
+
+#buddypress #activity-stream .activity-comments > ul ul {
+	margin-left: 1%;
+	padding-left: 0;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #activity-stream .activity-comments > ul ul {
+		margin-left: 1%;
+		padding-left: 1em;
+	}
+}
+
+#buddypress #activity-stream .activity-comments > ul ul li {
+	border-left: 1px solid #eaeaea;
+	border-top: 0;
+	padding-left: 0.2em;
+}
+
+#buddypress #activity-stream .activity-comments > ul .acomment-meta {
+	border-bottom: 1px solid #eaeaea;
+	color: #737373;
+	font-style: italic;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #activity-stream .activity-comments > ul .acomment-avatar {
+		display: block;
+		text-align: center;
+	}
+	#buddypress #activity-stream .activity-comments > ul .acomment-avatar a,
+	#buddypress #activity-stream .activity-comments > ul .acomment-avatar img.avatar {
+		display: inline;
+		float: none;
+	}
+	#buddypress #activity-stream .activity-comments > ul .acomment-content,
+	#buddypress #activity-stream .activity-comments > ul .acomment-meta,
+	#buddypress #activity-stream .activity-comments > ul .acomment-options {
+		margin: 5px;
+	}
+	#buddypress #activity-stream .activity-comments > ul .acomment-content {
+		padding: 0 10px;
+	}
+}
+
+#buddypress #activity-stream .activity-comments .ac-reply-avatar {
+	display: none;
+}
+
+#buddypress #activity-stream .activity-comments .ac-reply-content {
+	margin-left: 0;
+	padding-left: 0;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-comments .ac-reply-avatar {
+		display: block;
+	}
+	#buddypress #activity-stream .activity-comments .ac-reply-content {
+		overflow: hidden;
+	}
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(51, 51, 51, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 1.2rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(153, 153, 153, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 59.6875em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		clear: none;
+		float: right;
+		width: 50%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	background: #555;
+	color: #fff;
+	font-size: 14px;
+	font-size: 1.4rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: right;
+		width: 28%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		margin-right: 5%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+#item-body h1,
+#item-body h2,
+#item-body h3,
+#item-body h4,
+#item-body h5,
+#item-body h6 {
+	margin: 1em 0;
+}
+
+#item-body h1 a,
+#item-body h2 a,
+#item-body h3 a,
+#item-body h4 a,
+#item-body h5 a,
+#item-body h6 a {
+	border-bottom: 0;
+}
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups #group-settings-form #request-list h4 {
+	background: none;
+	color: inherit;
+}
+
+.groups.edit-details #group-settings-form label {
+	background: #555;
+	border-top-right-radius: 4px;
+	border-top-left-radius: 4px;
+	background-clip: padding-box;
+	color: #fff;
+	display: inline-block;
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.edit-details #group-settings-form label {
+		width: 60%;
+	}
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 1.4rem;
+	width: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label,
+.groups.group-settings #create-group-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio ul,
+.groups.group-settings #create-group-form div.radio ul {
+	color: #767676;
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: left;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+/* Members loop */
+@media screen and (max-width: 30em) {
+	.groups.group-members #buddypress #members-group-list li > a {
+		border-bottom: 0;
+		display: block;
+		margin-bottom: 10px;
+		text-align: center;
+	}
+	.groups.group-members #buddypress #members-group-list li > a img.avatar {
+		display: inline;
+		float: none;
+	}
+}
+
+.groups.group-members #buddypress #members-group-list li h5 {
+	display: inline-block;
+	font-size: 14px;
+	font-size: 1.4rem;
+	margin: 0;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.groups.group-members #buddypress #members-group-list li h5 {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+.groups.group-members #buddypress #members-group-list li h5 a {
+	border-bottom: 0;
+}
+
+.groups.group-members #buddypress #members-group-list li span.activity {
+	font-size: 12px;
+	font-size: 1.2rem;
+}
+
+.groups.group-members #buddypress #members-group-list li .action {
+	top: 0;
+}
+
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 5px 0 0;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.3 - Group Send Invites
+*-----------------------------------------
+*/
+.groups.group-invites #item-body .item-list .action {
+	margin-bottom: 0;
+}
+
+@media screen and (min-width: 46.25em) {
+	.groups.group-invites #buddypress #item-body #message {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	.groups.group-invites #buddypress #send-invite-form {
+		margin-top: 0;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.groups.group-invites #item-body .left-menu {
+		float: none;
+		margin: 10px 0;
+	}
+	.groups.group-invites #item-body .left-menu #invite-list {
+		height: auto;
+		width: auto;
+	}
+	.groups.group-invites #item-body .main-column {
+		margin-left: 0;
+	}
+	.groups.group-invites #item-body .submit input {
+		display: inline-block;
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 1.2rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 1.4rem;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.8rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 50%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 1.4rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: left;
+	margin-right: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: 60%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: right;
+	font-family: "Noto Serif", serif;
+	line-height: 1.5;
+	margin-top: 10px;
+	width: 100%;
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled]:hover {
+	background: none;
+}
+
+@media screen and (min-width: 30em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		margin-top: 0;
+		width: 38%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+/* Edit Profile */
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.8rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 1.4rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		left: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #b7b7b7;
+	border-bottom-width: 2px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 2.6em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 1.2rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 1.1rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: #dce5ff;
+	border-color: #d1d0d0;
+	border-bottom-color: #b7b7b7;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-left: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: #b7b7b7;
+	line-height: 1;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 2rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings th.title {
+	width: 75%;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(153, 153, 153, 0.3);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress select {
+	border: 1px solid rgba(153, 153, 153, 0.5);
+}
+
+#buddypress .wp-editor-wrap a.button,
+#buddypress .wp-editor-wrap button,
+#buddypress .wp-editor-wrap input[type="submit"],
+#buddypress .wp-editor-wrap input[type="button"],
+#buddypress .wp-editor-wrap input[type="reset"] {
+	padding: 0 5px 1px;
+}
+
+#buddypress .standard-form li,
+#buddypress .group-create-form li {
+	float: none;
+	list-style: none;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form textarea,
+#buddypress .group-create-form input[type="text"],
+#buddypress .group-create-form textarea {
+	width: 100%;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(153, 153, 153, 0.4);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: left;
+	margin: 0;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 1.4rem;
+	border: 0;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(153, 153, 153, 0.4);
+	padding: 0.2em 0 0.2em 0.2em;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 20%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 1.4rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1.6rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress #item-body div#message {
+	margin-top: 20px;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress #item-body div#message {
+		margin-right: 20px;
+	}
+}
+
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.8rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfifteen.scss	(revision 12265)
@@ -0,0 +1,2627 @@
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility .
+
+// Twentyfifteen Media Queries / Breakpoints
+// @media screen and (max-width: 22em) // Additional for xtra small mobile only
+// @media screen and (max-width: 30em) // Additional small only
+// @media screen and (min-width: 38.75em)
+// @media screen and (min-width: 46.25em)
+// @media screen and (min-width: 55em)
+// @media screen and (min-width: 59.6875em)
+// @media screen and (min-width: 77.5em)
+// @media screen and (min-width: 87.6875em)
+
+// Mixins and Variables
+
+// Common margin values
+$spacing-val-lg: 40px;
+$spacing-val-md: 20px;
+$spacing-val-sm: 10px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing to match 2015 62.5% approach
+@mixin font-size($font-size: 16) {
+	$rem-font-value: ($font-size / 10);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($box-model) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2015 font family
+@mixin default-font-sans() {
+	font-family: "Noto Sans", sans-serif;
+}
+
+@mixin default-font-serif() {
+	font-family: "Noto Serif", serif;
+}
+
+// Variables: color definitions
+$content-background: #fff;
+$light-background:   #f7f7f7;
+$medium-background:  #ccc;
+$dark-background:    #555;
+$border-color:       #999;    // border color is varied using rgba
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$body-text:          #333;    // 2015 body text color
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #9fd1e2;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             #dce5ff;
+$link-action:        #c82b2b;
+
+/*------------------------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyfifteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfifteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+--------------------------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Forums
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group Send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+------------------------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements & overrides
+*-------------------------------------------------------------------------------
+*/
+
+// Twentyfifteen  adds a generic grouped ruleset of clear:both;
+// for all heading h# elements this is problematic where floats are concerned
+// We reset this here under our #buddypress namespace
+#buddypress {
+
+	h1,
+	h2,
+	h3,
+	h4,
+	h5,
+	h6 {
+		clear: none;
+	}
+}
+
+// As BP screens have specific content lets adjust the themes primary elements
+// to maximise the screen real estate for BP pages.
+
+.buddypress {
+
+	div.clear {
+		display: none;
+	}
+
+	.site-content {
+
+		@media screen and (min-width: 77.5em) {
+		}
+	}
+
+	main {
+		padding-top: 4%;
+
+		@media screen and (min-width: 59.6875em) {
+			padding-top: 0;
+		}
+
+		article {
+			margin: 0 4%;
+			padding-top: 8.3333%;
+
+			@media screen and (min-width: 59.6875em) {
+				margin: 0 0 0 1px;
+			}
+
+			@media screen and (min-width: 87.6875em) {
+				margin: 0 4.3333% 0 8.3333%;
+			}
+
+			.entry-header,
+			.entry-content {
+				padding: 0 3rem 3rem;
+			}
+
+			#buddypress {
+				margin-bottom: $spacing-val-lg;
+			}
+		} // .article
+	} // .main
+
+	.site-footer {
+		margin: 0 4%;
+
+		@media screen and (min-width: 59.6875em) {
+			margin: 0 0 0 35.2941%;
+			width: 61.8235%;
+		}
+	}
+} // .buddypress (body class)
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+// Remove 2015 border bottom on BP links/spans
+
+.buddypress #buddypress {
+
+	.item-list-tabs,
+	.activity-header,
+	.activity-inner,
+	.activity-comments,
+	.item-title,
+	.load-more,
+	.field-visibility-settings-toggle,
+	#latest-update,
+	table,
+	.avatar-nav-items {
+
+		a {
+			border-bottom: 0;
+		}
+	}
+
+	.pagination-links {
+
+		a,
+		span {
+			border-bottom: 0;
+		}
+	}
+} // close .buddypress #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets are seemingly over weighted in selectors
+
+#buddypress {
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected {
+
+				a {
+					background: $dark-background;
+					color: #fff;
+					opacity: 1;
+				}
+			}
+		}
+	}
+	// Global Nav Styles
+	div.item-list-tabs {
+
+		ul {
+			background-color: $light-background;
+			border-bottom: 1px solid rgba($border-light, 0.9);
+			border-top: 1px solid rgba($border-light, 0.9);
+			overflow: hidden;
+			padding: $spacing-val-xs 0;
+
+			li {
+
+				a {
+
+					span {
+						border-radius: 25%;
+					}
+				}
+			}
+		}
+	}
+
+	/*__ Horizontal menus __*/
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+				float: none;
+			}
+
+			li:not(.selected) {
+
+				a {
+					opacity: 0.7;
+				}
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				li {
+					float: left;
+				}
+			}
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			background-color: $light-background;
+			border-bottom: 0;
+			padding: 0;
+
+			li.last {
+				background: $content-background;
+				margin-top: 0;
+				padding: $spacing-val-xs $spacing-val-xs $spacing-val-xs 0;
+				width: 100%;
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+				}
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				li.last {
+					text-align: right;
+				}
+			}
+		} // close ul
+	} // close #subnav
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected,
+			li.current {
+
+				a {
+					background: $dark-background;
+					color: #fff;
+					opacity: 1;
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+
+@media screen and (min-width: 55em) {
+
+	.bp-user #buddypress,
+	.single-item.groups #buddypress {
+		background: $light-background;
+	}
+
+	#buddypress {
+
+		#item-header,
+		#item-body {
+			background: $content-background;
+		}
+
+		#object-nav {
+			border-right: 1px solid $border-med;
+			float: left;
+			margin-right: -1px;
+			width: 200px;
+
+			ul {
+				border-bottom: 0;
+				padding: 0;
+
+				li {
+					float: none;
+					overflow: hidden;
+
+					span {
+						background: $content-background;
+						border-radius: 10%;
+						float: right;
+						margin-right: 2px;
+					}
+				}
+			}
+		}
+
+		#item-body {
+			border-left: 1px solid $border-med;
+			overflow: hidden;
+			padding: 0 0 0 $spacing-val-md;
+			width: auto;
+
+			#subnav {
+				margin: 0 0 0 (-$spacing-val-md);
+			}
+		}
+
+	} // close  #buddypress
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Adjusts Meta items and various elements to match 2015 font-family
+#buddypress {
+
+	.item-list {
+
+		.activity-header,
+		.activity-meta {
+
+			@include default-font-sans();
+		}
+
+		a.activity-time-since {
+			color: #717171;
+		}
+	}
+}
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2015 font-family
+
+#buddypress {
+
+	ul.item-list {
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@media screen and (min-width: 38.75em) {
+					margin-bottom: 0;
+				}
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+					// Quite early on we need to move avatars to the left(right)
+					@media screen and (min-width: 38.75em) {
+						display: block;
+						float: left;
+					}
+				}
+			}
+
+			.item {
+				margin-left: 25%;
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin-left: 0;
+					width: 70%;
+				}
+			}
+
+			@media screen and (min-width: 59.6875em) {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 10%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+				}
+			}// close @media
+
+			div.action {
+				clear: left;
+				float: none;
+				margin-bottom: -$spacing-val-md;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-md 0 $spacing-val-xs;
+				// push the actions to the right
+				@media screen and (min-width: 59.6875em) {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-sm 0;
+					width: 100%;
+
+					a {
+						display: block;
+						width: 100%;
+					}
+					// Provisionally lets keep the 'action' div
+					//position but inline-block the buttons
+
+					@media screen and (min-width: 38.75em) {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm $spacing-val-sm 0;
+						width: auto;
+					} // close @media
+
+					@media screen and (min-width: 59.6875em) {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		@media screen and (max-width: 30em) {
+
+			#whats-new-post-in-box {
+
+				select {
+
+					@include font-size(14);
+					max-width: 120px;
+				}
+			}
+		}
+
+		@media screen and (max-width: 38.75em) {
+
+			#whats-new-content {
+				clear: left;
+				margin: $spacing-val-sm 0 $spacing-val-lg;
+				padding: $spacing-val-sm 0 0;
+
+				#whats-new-submit {
+					float: none;
+
+					input {
+						width: 100%;
+					}
+				}
+			}
+
+			#whats-new-options {
+				display: flex;
+				flex-direction: column;
+
+				#whats-new-submit {
+					order: 2;
+				}
+
+				#whats-new-post-in-box {
+					order: 1;
+				}
+			}
+		} // close @media
+
+		#whats-new-options[style] {
+
+			#whats-new-post-in-box {
+				border: 1px solid rgba($border-color, 0.5);
+				float: left;
+				line-height: 1.5;
+				margin-top: 12px;
+				padding-left: 0.2em;
+				width: 100%;
+
+				select {
+					background: none;
+					border: 0;
+					float: right;
+					margin: 0;
+					min-height: 1.5em;
+					outline: 0;
+					padding-left: 0.4em;
+				}
+			}
+
+			@media screen and (min-width: 30em) {
+
+				#whats-new-post-in-box {
+					width: auto;
+				}
+
+				#whats-new-submit {
+					float: right;
+				}
+			}
+
+		} // close #whats-new-options
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content {
+				margin-left: 0;
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(13);
+				}
+			}
+		}
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				line-height: inherit;
+				margin-right: 0;
+
+				p {
+					background-color: $light-background;
+					border: 1px solid rgba($border-light, 0.6);
+					color: #717171;
+					padding: 0 0.2em;
+				}
+
+				img.avatar {
+					display: inline-block;
+					margin-bottom: 0 !important; // have to match to BP
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+					margin: 0 0 $spacing-val-xs;
+
+					@media screen and (min-width: 38.75em) {
+						display: inline-block;
+						margin: 0;
+					}
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+
+				a {
+					font-style: italic;
+				}
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+		background: none; // counter the .bp-user faux column background
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md 0;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			margin: $spacing-val-sm 0 0;
+
+			a {
+				color: #717171;
+			}
+
+			&.has-comments {
+				border-left: 1px solid $border-light;
+			}
+
+			.ac-form {
+				padding: $spacing-val-md 0 0;
+			}
+
+			> ul {
+				background: rgba($light-background, 0.6);
+				margin: 15px 0 0 2px;
+
+				@media screen and (min-width: 38.75em) {
+					margin-left: 70px;
+				}
+
+				ul {
+					// all nested ul margins & padding flattened initially
+					margin-left: 1%;
+					padding-left: 0;
+
+					@media screen and (min-width: 30em) {
+						margin-left: 1%;
+						padding-left: 1em;
+					}
+				}
+
+				ul li {
+					border-left: 1px solid $border-light;
+					border-top: 0;
+					padding-left: 0.2em;
+				}
+
+				.acomment-meta {
+					border-bottom: 1px solid $border-light;
+					color: #737373;
+					font-style: italic;
+				}
+
+				// Display the user avatar stacked up to 30em
+				// reduce/widen the content margins
+				@media screen and (max-width: 38.75em) {
+
+					.acomment-avatar {
+						display: block;
+						text-align: center;
+
+						a,
+						img.avatar {
+							display: inline;
+							float: none;
+						}
+					}
+
+					.acomment-content,
+					.acomment-meta,
+					.acomment-options {
+						margin: $spacing-val-xs;
+					}
+
+					.acomment-content {
+						padding: 0 $spacing-val-sm;
+					}
+				} // close @media
+
+			} // close the ul li comments construct
+
+			.ac-reply-avatar {
+				display: none;
+			}
+
+			.ac-reply-content {
+				margin-left: 0;
+				padding-left: 0;
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				.ac-reply-avatar {
+					display: block;
+				}
+
+				.ac-reply-content {
+					overflow: hidden;
+				}
+			}
+
+			.ac-form {
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+	} // close .activity
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@media screen and (min-width: 59.6875em) {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+.bp-user,
+.single-item.groups {
+
+	@media screen and (max-width: 46.25em) {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	@media screen and (max-width: 38.75em) {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+
+		@media screen and (max-width: 38.75em) {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@media screen and (max-width: 38.75em) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			div#item-actions {
+				margin: 0;
+				width: 100%;
+
+				@media screen and (min-width: 38.75em) {
+					clear: none;
+					float: right;
+					width: 50%;
+				}
+
+				h2 {
+					background: $dark-background;
+					color: $content-background;
+
+					@include font-size(14);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(16);
+					}
+
+					padding: 0.2em;
+				}
+			} // close item-actions
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-avatar {
+					width: 21%;
+				}
+
+				#item-header-content {
+					margin-left: 4%;
+					width: 40%;
+				}
+
+				div#item-actions {
+					float: right;
+					width: 28%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+	} // close #buddypress
+} // close .groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+			padding: $spacing-val-md 0;
+
+			#item-header-avatar {
+				text-align: center;
+				width: 100%;
+
+				a {
+					border-bottom: 0;
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar {
+					float: left;
+					width: 20%;
+
+					img.avatar,
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: right;
+					margin-right: 5%;
+					width: 69%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+// Manage the margins inherited from 2015 inside the item-body sections
+// generally far too large a top margin for headings in our lists
+#item-body {
+
+	h1,
+	h2,
+	h3,
+	h4,
+	h5,
+	h6 {
+		margin: 1em 0;
+
+		a {
+			border-bottom: 0;
+		}
+	}
+}
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+		// remove backgrounds from request list member names.
+		#request-list {
+
+			h4 {
+				background: none;
+				color: inherit;
+			}
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			background: $dark-background;
+
+			@include border-top-radius(4px);
+			color: $content-background;
+			display: inline-block;
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+
+			@media screen and (min-width: 38.75em) {
+				width: 60%;
+			}
+
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form,
+	#create-group-form {
+
+		div.radio {
+
+			label {
+				border: 1px solid $border-light;
+				padding: 0.2em;
+			}
+
+			ul {
+				color: #767676;
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+					float: left;
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+
+					a {
+						display: inline-block;
+						margin: $spacing-val-xs 0;
+						width: 100%;
+
+						@media screen and (min-width: 38.75em) {
+							width: auto;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+/* Members loop */
+
+.groups.group-members {
+
+	#buddypress {
+
+		#members-group-list {
+
+			li {
+
+				@media screen and (max-width: 30em) {
+
+					> a {
+						border-bottom: 0;
+						display: block;
+						margin-bottom: $spacing-val-sm;
+						text-align: center;
+
+						img.avatar {
+							display: inline;
+							float: none;
+						}
+					}
+				}
+
+				h5 {
+					display: inline-block;
+
+					@include font-size(14);
+
+					@media screen and (min-width: 59.6875em) {
+
+						@include font-size(16);
+					}
+
+					margin: 0;
+
+					a {
+						border-bottom: 0;
+					}
+				} //  close h5
+
+				span.activity {
+
+					@include font-size(12);
+				}
+
+				.action {
+					top: 0;
+				}
+			} // close li
+		}
+	}
+}
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+
+			@media screen and (max-width: 38.75em) {
+				background: $content-background;
+				padding: $spacing-val-md 0;
+			}
+
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+
+				@media screen and (max-width: 38.75em) {
+					margin: 0;
+					width: 100%;
+
+					label {
+
+						input[type="text"] {
+							width: 100%;
+						}
+					}
+				}
+
+				margin: $spacing-val-xs $spacing-val-xs 0 0;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.3 - Group Send Invites
+*-----------------------------------------
+*/
+
+.groups.group-invites {
+
+	#item-body {
+
+		.item-list {
+
+			.action {
+				margin-bottom: 0;
+			}
+		}
+	}
+
+	@media screen and (min-width: 46.25em) {
+
+		#buddypress {
+
+			#item-body {
+
+				#message {
+					margin-top: 0;
+				}
+			}
+		}
+	}
+
+	@media screen and (min-width: 55em) {
+
+		#buddypress {
+
+			#send-invite-form {
+				margin-top: 0;
+			}
+		}
+	}
+
+	#item-body {
+
+		@media screen and (max-width: 46.25em) {
+
+			.left-menu {
+				float: none;
+				margin: $spacing-val-sm 0;
+
+				#invite-list {
+					height: auto;
+					width: auto;
+				}
+			}
+
+			.main-column {
+				margin-left: 0;
+			}
+
+			.submit {
+
+				input {
+					display: inline-block;
+					width: 100%;
+				}
+			}
+
+		} // close @media
+	} // close .item-body
+
+} // close .groups.group-invites
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	.entry-title {
+		margin-bottom: 0.5em;
+	}
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(14);
+			}
+
+			td {
+
+				@include font-size(12);
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+
+			@media screen and (min-width: 77.5em) {
+
+				th {
+
+					@include font-size(18);
+				}
+
+				td {
+
+					@include font-size(16);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav,
+		.messages-options-nav {
+			float: left;
+			width: 100%;
+
+			@media screen and (min-width: 30em) {
+				width: 50%;
+			}
+
+			select,
+			input {
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 0;
+				width: 100%;
+
+				@media screen and (min-width: 30em) {
+					width: 60%;
+				}
+			}
+
+			input {
+				float: right;
+
+				@include default-font-serif();
+				line-height: 1.5;
+				margin-top: $spacing-val-sm;
+				width: 100%;
+
+				&[disabled]:hover {
+					background: none;
+				}
+
+				@media screen and (min-width: 30em) {
+					margin-top: 0;
+					width: 38%;
+				}
+			}
+
+		} // close .notifications-options-nav
+
+	} // close #BuddyPress
+
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			.bp-widget {
+
+				h2 {
+					background: lighten($dark-background, 10%);
+					color: #fff;
+					margin-bottom: 0;
+					padding: 0.4em;
+				}
+
+				table {
+					margin-top: 0;
+				}
+			}
+		}
+	}
+}
+
+/* Edit Profile */
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: $dark-background;
+					color: #fff;
+					padding: 0.2em 0.5em;
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+
+	} // close #BuddyPress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+.bp-user {
+
+	#buddypress {
+
+		#groups-list {
+
+			li {
+
+				.item {
+
+					@media screen and (min-width: 77.5em) {
+						left: 5%;
+						width: 50%;
+					}
+				}
+			}
+		}
+
+	} // close #BuddyPress
+
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: lighten($dark-background, 10%);
+				}
+			}
+
+			tr {
+
+				td {
+					background: $content-background;
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom-color: darken($border-light, 20%);
+					border-bottom-width: 2px;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 2.6em;
+
+					@media screen and (max-width: 38.75em) {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 2.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@media screen and (max-width: 38.75em) {
+						clear: both;
+
+						@include font-size(11);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					background: $unread;
+					border-color: darken($border-light, 10%);
+					border-bottom-color: darken($border-light, 20%);
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		#message-threads.sitewide-notices {
+
+			td {
+				width: 100%;
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					margin-bottom: 0.4em;
+					padding-left: 0.2em;
+				}
+
+				a {
+					display: inline-block;
+				}
+			}
+
+			td:first-child {
+				display: none;
+			}
+
+			td:first-child + td + td {
+				// notice date
+				border-bottom: 0;
+
+				span {
+					line-height: 1;
+				}
+			}
+
+			td:last-child {
+				// notice actions
+				border-bottom-color: darken($border-light, 20%);
+				line-height: 1;
+				text-align: right;
+
+				a:last-child:after {
+					content: attr(title);
+					display: block;
+					line-height: initial;
+					text-indent: 0;
+				}
+			}
+		}
+
+	} // close #BuddyPress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		ul {
+			margin: 0;
+		}
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			th.title {
+				width: 75%;
+			}
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+		// Profile visibility table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #BuddyPress
+
+} // close .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+#buddypress {
+
+	// Match BP form elements to Themes(2015) styling where styles
+	// are not inherited or being overriden.
+
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.3);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	select {
+		border: 1px solid rgba($border-color, 0.5);
+	}
+
+	// Overrides for embedded WP editors.
+	.wp-editor-wrap {
+
+		a.button,
+		button,
+		input[type="submit"],
+		input[type="button"],
+		input[type="reset"] {
+			padding: 0 5px 1px;
+		}
+	}
+
+} // close #buddypress
+
+
+#buddypress {
+
+	// Manage form control widths override BP 75% width.
+
+	.standard-form,
+	.group-create-form {
+
+		li {
+			float: none;
+			list-style: none;
+		}
+
+		input[type="text"],
+		textarea {
+			width: 100%;
+		}
+	}
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		// Stylise the seach form elements, in part this deals with padding
+		// issues on the submit & sizing issues between bp styles & twentyfifteen
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.4);
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 80%;
+			}
+
+			input[type="text"] {
+				float: left;
+				margin: 0;
+				width: 80%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+
+				@include font-size(14);
+				border: 0;
+				line-height: inherit;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.4);
+				padding: 0.2em 0 0.2em 0.2em;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0.2em 1em;
+				text-align: center;
+				text-transform: none;
+				width: 20%;
+			}
+		}// close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+	@media screen and (min-width: 38.75em) {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+			float: right;
+			margin-bottom: 5px !important;
+
+			form {
+
+				label,
+				input[type="text"],
+				input[type="submit"] {
+					width: auto;
+				}
+			}
+		}
+	} // close @media
+
+	@media screen and (min-width: 77.5em) {
+
+		.dir-search,
+		.message-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16);
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16);
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+
+// Adjust table font sizes, default too large proportionally
+// This approach will require refinement and perhaps re-location to
+// a more general typography section to manage BP elements grouped under
+// breakpoints. Provide top/bottom margins for tables, lacking in BP styles
+#buddypress {
+
+	table {
+
+		@include font-size(14);
+		margin: $spacing-val-md 0;
+
+		tr th {
+			background: lighten($dark-background, 10%);
+			border-color: darken($border-light, 20%);
+			color: $content-background;
+		}
+
+		// Reduce the themes inherited paragraph margins in tables
+		p {
+			margin-bottom: 0.5em;
+		}
+	}
+
+	@media screen and (min-width: 55em) {
+
+		table {
+
+			@include font-size(16);
+		}
+	}
+}
+
+/*__ User Account tables __*/
+
+#buddypress {
+
+	// Manage some table cells widths that are disproportionate to their content
+	.notifications,
+	.messages-notices {
+
+		th {
+			width: 30%;
+
+			&.bulk-select-all {
+				text-align: center;
+				width: 10%;
+			}
+		}
+
+		.bulk-select-check,
+		.thread-star {
+			text-align: center;
+		}
+
+		.notification-actions,
+		td.thread-options {
+			text-align: center;
+
+			a {
+				display: inline-block;
+				margin: 0;
+				padding: 0;
+			}
+		}
+
+		// Notices action buttons, this maybe better moved, temp for now to address
+		// styling issues - this will need styling ideas
+		td {
+
+			.button {
+				border: 0;
+				display: block;
+				padding: 0;
+				text-align: center;
+			}
+		}
+
+	} // .notifications, .message-notices
+
+} // #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	#item-body {
+
+		div#message {
+			margin-top: $spacing-val-md;
+
+			@media screen and (min-width: 55em) {
+				margin-right: $spacing-val-md;
+			}
+		}
+	}
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning' )
+// we need to use the body classes
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen-rtl.css	(revision 12265)
@@ -0,0 +1,1501 @@
+/*------------------------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyfourteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfourteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the ruelesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+--------------------------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Theme Structural Elements
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 - Activity Whats new
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Forums
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - Item-header: Groups;
+		6.1.2 -  item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1- Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group Send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading
+11.0 - Widgets
+------------------------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section - 1.0 - Theme - Structural Elements
+*-------------------------------------------------------------------------------
+*/
+#buddypress h1,
+#buddypress h2,
+#buddypress h3,
+#buddypress h4,
+#buddypress h5,
+#buddypress h6 {
+	clear: none;
+}
+
+.buddypress .site-content {
+	padding-bottom: 20px;
+	padding-top: 4%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress .site-content {
+		padding-top: 0;
+	}
+}
+
+.buddypress .site-content article {
+	margin: 0 4%;
+	padding-top: 0;
+}
+
+.buddypress .site-content article .entry-header,
+.buddypress .site-content article .entry-content {
+	max-width: 100%;
+	padding: 0;
+}
+
+.buddypress .site-content article #buddypress {
+	margin-bottom: 40px;
+}
+
+/**
+* ------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @subsection 2.1 - Object Nav / Subnav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress .item-list-tabs ul li {
+	width: 100%;
+}
+
+@media screen and (min-width: 400px) {
+	#buddypress .item-list-tabs ul li {
+		width: 50%;
+	}
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress .item-list-tabs ul li {
+		width: auto;
+	}
+}
+
+#buddypress .item-list-tabs ul a {
+	color: #1b6e08;
+}
+
+.directory #buddypress .item-list-tabs ul:before,
+.directory #buddypress .item-list-tabs ul:after {
+	content: " ";
+	display: table;
+}
+
+.directory #buddypress .item-list-tabs ul:after {
+	clear: both;
+}
+
+.directory #buddypress .item-list-tabs ul {
+	border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+	padding-bottom: 10px;
+}
+
+@media screen and (min-width: 783px) {
+	.directory #buddypress .item-list-tabs ul {
+		padding-bottom: 0;
+	}
+	.directory #buddypress .item-list-tabs ul li.selected {
+		border: 1px solid rgba(0, 0, 0, 0.2);
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	.directory #buddypress .item-list-tabs ul li.selected a {
+		background: none;
+		outline: none;
+	}
+}
+
+.directory #buddypress #subnav ul {
+	border-bottom: 0;
+}
+
+.directory #buddypress #subnav a {
+	color: #1b6e08;
+}
+
+/* List last filters global */
+#buddypress #subnav li#activity-filter-select.last {
+	line-height: 2.1;
+	margin-top: 0;
+}
+
+@media screen and (max-width: 783px) {
+	#buddypress #object-nav ul {
+		border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+		overflow: hidden;
+		padding-bottom: 10px;
+	}
+	#buddypress div#subnav.item-list-tabs {
+		margin-bottom: 40px;
+	}
+	#buddypress div#subnav.item-list-tabs ul li {
+		width: auto;
+	}
+}
+
+/*__ Vertical Menu User Account Screens __*/
+@media screen and (min-width: 783px) {
+	.bp-user #buddypress,
+	.single-item.groups #buddypress {
+		background: #d8d7d7;
+	}
+	#buddypress #item-header,
+	#buddypress #item-body {
+		background: #fff;
+	}
+	#buddypress #item-body {
+		border-right: 1px solid #bebebe;
+	}
+	#buddypress #object-nav {
+		background: #d8d7d7;
+		border-left: 1px solid #bebebe;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	#buddypress #object-nav ul {
+		border-bottom: 0;
+		border-top: 1px solid #bebebe;
+		padding: 0;
+	}
+	#buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	#buddypress #object-nav ul li:focus, #buddypress #object-nav ul li:hover {
+		background: #f1f1f1;
+	}
+	#buddypress #object-nav ul li span {
+		border-radius: 10%;
+		float: left;
+		margin-left: 15px;
+	}
+	#buddypress #object-nav ul li.current {
+		background: #f1f1f1;
+	}
+	#buddypress #item-body {
+		overflow: hidden;
+		padding: 0 20px;
+		width: auto;
+	}
+	#buddypress #item-body #subnav {
+		border: 1px solid #bebebe;
+		border-right: 0;
+		border-left: 0;
+		margin: 0 -20px 20px;
+	}
+	#buddypress #item-body #subnav li:focus,
+	#buddypress #item-body #subnav li:hover {
+		background: #f1f1f1;
+	}
+	#buddypress #item-body #subnav li.current {
+		background: #f1f1f1;
+	}
+	#buddypress #item-body #subnav li#activity-filter-select:focus, #buddypress #item-body #subnav li#activity-filter-select:hover {
+		background: none;
+	}
+	#buddypress #item-body #subnav li#activity-filter-select select {
+		max-width: 130px;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @subsection 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .item-title a {
+	display: block;
+	font-size: 22px;
+	font-size: 1.375rem;
+	text-decoration: none;
+}
+
+#buddypress .item-list .item-title span.update,
+#buddypress .item-list .item-title span a {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	float: none;
+	margin-bottom: 20px;
+	text-align: center;
+}
+
+#buddypress ul.item-list li .item-avatar a img.avatar {
+	float: none;
+}
+
+#buddypress ul.item-list li div.item-title {
+	float: none;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.item-title > a {
+	text-align: center;
+}
+
+#buddypress ul.item-list li .item {
+	margin-right: 0;
+}
+
+#buddypress ul.item-list li div.item-desc {
+	margin-right: 0;
+	width: auto;
+}
+
+#buddypress ul.item-list li div.action {
+	clear: both;
+	float: none;
+	margin: 0;
+	position: relative;
+	text-align: right;
+}
+
+#buddypress ul.item-list li div.action div {
+	margin: 0 0 10px;
+}
+
+#buddypress ul.item-list li .activity-meta,
+#buddypress ul.item-list li .action {
+	float: none;
+}
+
+#buddypress ul.item-list li .activity-meta a,
+#buddypress ul.item-list li .activity-meta div,
+#buddypress ul.item-list li .action a,
+#buddypress ul.item-list li .action div {
+	display: block;
+	margin: 5px 0;
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress ul.item-list li .item-avatar {
+		float: right;
+		margin-bottom: 0;
+	}
+	#buddypress ul.item-list li div.item-title > a {
+		text-align: right;
+	}
+	#buddypress ul.item-list li .item {
+		float: left;
+		margin-right: 5%;
+		width: 80%;
+	}
+	#buddypress ul.item-list li .activity-meta {
+		clear: both;
+	}
+	#buddypress ul.item-list li .activity-meta a,
+	#buddypress ul.item-list li .activity-meta div {
+		display: inline-block;
+		margin: 5px 0 0 5px;
+	}
+}
+
+@media screen and (min-width: 783px) {
+	#buddypress ul.item-list li .item {
+		float: right;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item div.item-desc {
+		margin: 0;
+		width: auto;
+	}
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		top: 0;
+		width: 25%;
+	}
+	#buddypress ul.item-list li div.action div {
+		display: block;
+	}
+	#buddypress ul.item-list li div.action div a {
+		box-sizing: border-box;
+		display: block;
+		width: 100%;
+	}
+	#buddypress ul.item-list li div.action .meta {
+		text-align: center;
+	}
+}
+
+#buddypress ul.item-list li .meta,
+#buddypress ul.item-list li .item-meta .activity {
+	font-style: italic;
+}
+
+/**
+*------------------------------------------------------------
+* @section 4.1 - Activity
+*------------------------------------------------------------
+*/
+/**
+*-------------------------------------------------
+* @subsection 4.1.1 - Activity What's New
+*-------------------------------------------------
+*/
+#buddypress form#whats-new-form {
+	margin: 40px 0;
+}
+
+#buddypress form#whats-new-form #whats-new-avatar {
+	float: none;
+	margin-bottom: 20px;
+	text-align: center;
+}
+
+#buddypress form#whats-new-form #whats-new-content,
+#buddypress form#whats-new-form p.activity-greeting {
+	margin-right: 0;
+	padding-right: 0;
+}
+
+#buddypress form#whats-new-form p.activity-greeting {
+	clear: right;
+	line-height: inherit;
+	margin-bottom: 5px;
+}
+
+@media screen and (max-width: 594px) {
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options {
+		display: flex;
+		flex-direction: column;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-submit {
+		order: 2;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-submit input {
+		width: 100%;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-post-in-box {
+		margin-top: 10px;
+		order: 1;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-post-in-box select {
+		margin-top: 5px;
+		max-width: 100%;
+		width: 100%;
+	}
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress form#whats-new-form #whats-new-avatar {
+		float: right;
+	}
+	#buddypress form#whats-new-form #whats-new-content,
+	#buddypress form#whats-new-form p.activity-greeting {
+		clear: none;
+		overflow: hidden;
+		padding-right: 20px;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.2 - Activity Listing
+*-------------------------------------------------
+*/
+#buddypress #activity-stream {
+	margin-top: 40px;
+}
+
+#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar {
+	float: none;
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar img.avatar {
+	display: inline;
+	float: none;
+}
+
+#buddypress .activity-list li.mini .activity-avatar img.avatar {
+	margin-right: 0;
+}
+
+#buddypress .activity-list li.mini .activity-meta a {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+#buddypress .activity-list .activity-content {
+	margin: 0;
+}
+
+#buddypress .activity-list .activity-content .activity-header {
+	margin: -0.5em 0 0;
+}
+
+#buddypress .activity-list .activity-content .activity-header > p {
+	line-height: 1.4;
+	margin-top: -10px;
+}
+
+#buddypress .activity-list .activity-content .activity-header img.avatar {
+	margin-bottom: 0 !important;
+	vertical-align: middle;
+}
+
+#buddypress .activity-list .activity-content .activity-inner {
+	margin: 10px 0;
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar {
+		float: right;
+		margin: 0 0 0 20px;
+	}
+	#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar img.avatar {
+		display: inline;
+		float: none;
+	}
+	#buddypress .activity-list li.mini .activity-avatar {
+		margin: 0 5% 0 10px;
+	}
+	#buddypress .activity-list .activity-content {
+		margin-right: 0;
+		overflow: hidden;
+	}
+}
+
+body.activity-permalink #buddypress {
+	background: transparent;
+}
+
+@media screen and (max-width: 760px) {
+	body.activity-permalink #buddypress .activity-list li .activity-content,
+	body.activity-permalink #buddypress .activity-list li .activity-comments {
+		margin-right: 0;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.3 - Activity Comments
+*-------------------------------------------------
+*/
+#buddypress .activity .activity-comments {
+	margin-right: 0;
+	margin-top: 5px;
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress .activity .activity-comments {
+		margin-right: 70px;
+	}
+}
+
+#buddypress .activity .activity-comments a {
+	color: #1b6e08;
+}
+
+#buddypress .activity .activity-comments ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 30px 2px 0 0;
+}
+
+#buddypress .activity .activity-comments ul ul {
+	border-right: 1px solid rgba(0, 0, 0, 0.1);
+	padding-right: 2%;
+}
+
+#buddypress .activity .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+@media screen and (max-width: 400px) {
+	#buddypress .activity .activity-comments .acomment-avatar {
+		display: block;
+		text-align: center;
+	}
+	#buddypress .activity .activity-comments .acomment-avatar a,
+	#buddypress .activity .activity-comments .acomment-avatar img.avatar {
+		display: inline;
+		float: none;
+	}
+	#buddypress .activity .activity-comments .acomment-content,
+	#buddypress .activity .activity-comments .acomment-meta,
+	#buddypress .activity .activity-comments .acomment-options {
+		margin: 5px;
+	}
+	#buddypress .activity .activity-comments .acomment-content {
+		padding: 0 10px;
+	}
+}
+
+#buddypress .activity .activity-comments .ac-form input[type="submit"],
+#buddypress .activity .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(43, 43, 43, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress .activity .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(0, 0, 0, 0.2);
+	text-align: center;
+}
+
+#buddypress .activity .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress .activity .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*------------------------------------------------------------
+* @section 4.2 - Members Loop
+*------------------------------------------------------------
+*/
+/*
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens
+*-------------------------------------------------------------------------------
+*/
+/* groups & user account page entry title */
+.single-item.groups .entry-title,
+.bp-user .entry-title {
+	background: #4d4c4c;
+	color: #fff;
+	font-size: 22px;
+	font-size: 1.375rem;
+	margin-bottom: 10px;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 673px) {
+	.single-item.groups .entry-title,
+	.bp-user .entry-title {
+		font-size: 28px;
+		font-size: 1.75rem;
+	}
+}
+
+/**
+*------------------------------------------------------------
+* @subsection 6.1 - Item Header: Global
+*------------------------------------------------------------
+*/
+#buddypress div#item-header {
+	padding-bottom: 10px;
+}
+
+#buddypress div#item-header img.avatar {
+	margin: 0 0 10px 20px;
+}
+
+#buddypress div#item-header #message {
+	clear: both;
+	margin-bottom: 5px;
+	padding-top: 10px;
+}
+
+#buddypress div#item-header #message p {
+	margin-bottom: 0;
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.1.1 - item-header: Groups
+*-------------------------------------------------
+*/
+@media screen and (max-width: 594px) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		margin-right: 0;
+		order: 3;
+		text-align: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions ul {
+		margin-right: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions ul li {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #message {
+		order: 4;
+	}
+}
+
+.single-item.groups #buddypress #item-header #item-actions {
+	float: none;
+	margin: 0 20px 0 0;
+	width: auto;
+}
+
+.single-item.groups #buddypress #item-header #item-actions h2 {
+	font-size: 20px;
+	font-size: 1.25rem;
+}
+
+.single-item.groups #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.single-item.groups #buddypress #item-header #item-header-content {
+	float: none;
+	overflow: hidden;
+}
+
+@media screen and (min-width: 594px) {
+	.single-item.groups #buddypress #item-header #item-actions {
+		float: left;
+	}
+	.single-item.groups #buddypress #item-header #item-header-avatar {
+		float: right;
+	}
+	.single-item.groups #buddypress #item-header #item-header-content {
+		float: none;
+		overflow: hidden;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.1.2 - item-header: User Accounts
+*-------------------------------------------------
+*/
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img {
+	float: none;
+}
+
+.bp-user #buddypress #item-header #latest-update {
+	margin-bottom: 10px;
+}
+
+@media screen and (max-width: 594px) {
+	.bp-user #buddypress #item-header #item-buttons div.generic-button {
+		display: inline-block;
+		float: none;
+		margin: 0 0 5px 0;
+		width: 100%;
+	}
+	.bp-user #buddypress #item-header #item-buttons div.generic-button a {
+		box-sizing: border-box;
+		display: block;
+		width: 100%;
+	}
+}
+
+@media screen and (min-width: 673px) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+		overflow: hidden;
+		text-align: right;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		width: 78%;
+	}
+	.bp-user #buddypress #item-header #item-header-content #latest-update {
+		margin-bottom: 20px;
+	}
+}
+
+/**
+*------------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*------------------------------------------------------------
+*/
+/**
+*-------------------------------------------------
+* @subsection 6.2.1 - item-body: Groups
+*-------------------------------------------------
+*/
+/**
+	*------------------------------
+	* 6.2.1.1 - Groups Management
+	*------------------------------
+	*/
+/* Settings */
+#group-settings-form .radio strong,
+#create-group-form .radio strong {
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+#group-settings-form .radio ul,
+#create-group-form .radio ul {
+	color: #6b6b6b;
+}
+
+#group-settings-form #group-creation-previous,
+#create-group-form #group-creation-previous {
+	margin: 10px 0;
+}
+
+/* Manage Members */
+.groups.manage-members #group-settings-form h3 {
+	margin-top: 40px;
+}
+
+.groups.manage-members #group-settings-form .item-list {
+	background: #f7f7f7;
+	border-top: 0;
+	padding: 0 0.5em;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.groups.manage-members #group-settings-form .item-list li:last-child {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	clear: none;
+	float: right;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+	color: #22830c;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	display: block;
+	float: none;
+	margin-top: 10px;
+	width: 100%;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+	*------------------------------
+	* 6.2.1.2 - Group Members List
+	*------------------------------
+	*/
+#buddypress #members-group-list .item-list h5 {
+	display: inline-block;
+	margin: 0;
+}
+
+#buddypress #members-group-list .item-list span.activity {
+	padding-right: 10px;
+}
+
+#buddypress #item-body #subnav li.groups-members-search:focus,
+#buddypress #item-body #subnav li.groups-members-search:hover,
+#buddypress #item-body #subnav li.last:focus,
+#buddypress #item-body #subnav li.last:hover {
+	background: none;
+}
+
+.groups.group-members #subnav ul:before,
+.groups.group-members #subnav ul:after {
+	content: " ";
+	display: table;
+}
+
+.groups.group-members #subnav ul:after {
+	clear: both;
+}
+
+.groups.group-members #subnav ul {
+	padding: 5px 0;
+}
+
+@media screen and (max-width: 400px) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 10px 0;
+		width: 100% !important;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	box-sizing: border-box;
+	float: right;
+}
+
+@media screen and (max-width: 400px) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+@media screen and (min-width: 783px) {
+	.groups.group-members #subnav li #search-members-form {
+		margin-right: 3px;
+	}
+}
+
+/**
+	*------------------------------
+	* 6.2.1.3 - Groups Send Invites
+	*------------------------------
+	*/
+@media screen and (min-width: 783px) {
+	.send-invites #buddypress #item-body {
+		border-top: 1px solid #bebebe;
+	}
+}
+
+@media screen and (max-width: 400px) {
+	.send-invites #buddypress #item-body .left-menu {
+		float: none;
+	}
+	.send-invites #buddypress #item-body .left-menu #invite-list {
+		height: auto;
+		width: auto;
+	}
+	.send-invites #buddypress #item-body .main-column {
+		margin-right: 0;
+	}
+}
+
+.send-invites #buddypress #item-body li img.avatar {
+	display: block;
+	float: none;
+	margin: 10px auto;
+}
+
+.send-invites #buddypress #item-body li .action {
+	margin-top: 10px;
+	text-align: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.send-invites #buddypress #item-body li .action {
+		text-align: left;
+	}
+}
+
+@media screen and (min-width: 594px) {
+	.send-invites #buddypress #item-body #invite-list {
+		width: 160px;
+	}
+	.send-invites #buddypress #item-body .main-column {
+		margin-right: 190px;
+	}
+	.send-invites #buddypress #item-body li img.avatar {
+		float: right;
+		margin: 0 0 0 10px;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.2.2 - item-body: User Accounts
+*-------------------------------------------------
+*/
+/**
+	*------------------------------
+	* 6.2.2.2 - Extended Profiles
+	*------------------------------
+	*/
+#buddypress .profile .bp-widget h2 {
+	background: #949494;
+	color: #fff;
+	padding: 0.4em 0.2em;
+}
+
+#buddypress .profile #profile-edit-form .button-nav:before,
+#buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+#buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+	margin-right: 0;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid rgba(0, 0, 0, 0.1);
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+#buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+#buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress .profile #profile-edit-form .field-visibility-settings-close,
+#buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #000;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.5 - Messaging
+	*------------------------------
+	*/
+#buddypress table#message-threads {
+	border-right: 1px solid rgba(0, 0, 0, 0.2);
+	border-left: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress table#message-threads tbody > tr:first-child td.bulk-select-check,
+#buddypress table#message-threads tbody > tr:first-child td.thread-from {
+	border-top: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress table#message-threads tr td {
+	background: #fff;
+	border-left: 0 !important;
+	box-sizing: border-box;
+	display: inline-block;
+	float: right;
+}
+
+#buddypress table#message-threads tr td.thread-star,
+#buddypress table#message-threads tr td.thread-options {
+	border-bottom-color: rgba(0, 0, 0, 0.2);
+	border-bottom-width: 2px;
+	border-right: 0;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 549px) {
+	#buddypress table#message-threads tr td.thread-star,
+	#buddypress table#message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+#buddypress table#message-threads tr td.bulk-select-check,
+#buddypress table#message-threads tr td.thread-from {
+	border-right: 0;
+	height: 2.6em;
+}
+
+@media screen and (max-width: 320px) {
+	#buddypress table#message-threads tr td.bulk-select-check,
+	#buddypress table#message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+#buddypress table#message-threads tr td.thread-from,
+#buddypress table#message-threads tr td.thread-options {
+	border-right: 0;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+#buddypress table#message-threads tr td.thread-info {
+	border-right: 0;
+	padding-right: 41px;
+	width: 100%;
+}
+
+#buddypress table#message-threads tr td.thread-options {
+	text-align: left;
+}
+
+#buddypress table#message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+#buddypress table#message-threads tr span.from {
+	display: none;
+}
+
+#buddypress table#message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 320px) {
+	#buddypress table#message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.6875rem;
+		width: 100%;
+	}
+}
+
+#buddypress table#message-threads tr.unread td {
+	background: #f1f4ff;
+	border-color: rgba(0, 0, 0, 0.2);
+	border-top: 0;
+	border-width: 1px;
+}
+
+#buddypress table#message-threads th {
+	display: none;
+}
+
+#buddypress table#message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+#buddypress table#message-threads th.bulk-select-all,
+#buddypress table#message-threads td.bulk-select-check,
+#buddypress table#message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+#buddypress #message-thread #message-subject {
+	background: #4d4c4c;
+	color: #fff;
+	padding: 0.5em;
+}
+
+#buddypress #message-thread .message-box .message-metadata {
+	margin-bottom: 10px;
+}
+
+#buddypress #message-thread .message-box .message-content {
+	clear: right;
+	margin-right: 0;
+}
+
+#buddypress .acfb-holder {
+	list-style: none;
+	margin: 0;
+}
+
+#buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+#buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+#buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 10px;
+}
+
+#buddypress .acfb-holder li.friend-tab span.p:focus, #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+#buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+#buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+#buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+#buddypress #message-threads.sitewide-notices td strong {
+	background: #4d4c4c;
+	color: #fff;
+	font-size: 16px;
+	font-size: 1rem;
+	display: block;
+	margin-bottom: 0.4em;
+	padding: 0.2em;
+}
+
+#buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+	line-height: initial;
+}
+
+#buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+#buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+#buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+#buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: rgba(0, 0, 0, 0.2);
+	line-height: 1;
+	text-align: left;
+}
+
+#buddypress #message-threads.sitewide-notices td:last-child a:last-child {
+	line-height: 0;
+	margin-right: 0.7em;
+	text-indent: -999em;
+}
+
+#buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+#buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+}
+
+#buddypress table.notification-settings td.yes,
+#buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+#buddypress table.profile-settings {
+	width: 100%;
+}
+
+#buddypress table.profile-settings th.field-group-name,
+#buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 401px) {
+	#buddypress table.profile-settings th.field-group-name,
+	#buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+#buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/*------------------------------------------------------------------------------
+* @section - 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress input[type="text"],
+#buddypress input[type="email"],
+#buddypress input[type="password"],
+#buddypress input[type="submit"],
+#buddypress select,
+#buddypress textarea {
+	border: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form input[type="submit"],
+#buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress #send-reply.standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	clear: both;
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid #ccc;
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: right;
+	margin: 0;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid #ccc;
+	padding: 0.2em 0.2em 0.2em 0;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 20%;
+}
+
+@media screen and (min-width: 401px) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search {
+		clear: none;
+		float: left;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress div.message-search form input[type="text"] {
+		width: auto;
+	}
+	#buddypress div.message-search {
+		margin: 3px 0 0;
+	}
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="submit"],
+	#buddypress li.groups-members-search form input[type="text"] {
+		width: auto;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	border-color: rgba(0, 0, 0, 0.2);
+}
+
+#buddypress table .alt a {
+	color: #22820c;
+}
+
+/*__ User Account tables __*/
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen.css	(revision 12265)
@@ -0,0 +1,1501 @@
+/*------------------------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyfourteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfourteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the ruelesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+--------------------------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Theme Structural Elements
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 - Activity Whats new
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Forums
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - Item-header: Groups;
+		6.1.2 -  item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1- Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group Send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading
+11.0 - Widgets
+------------------------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section - 1.0 - Theme - Structural Elements
+*-------------------------------------------------------------------------------
+*/
+#buddypress h1,
+#buddypress h2,
+#buddypress h3,
+#buddypress h4,
+#buddypress h5,
+#buddypress h6 {
+	clear: none;
+}
+
+.buddypress .site-content {
+	padding-bottom: 20px;
+	padding-top: 4%;
+}
+
+@media screen and (min-width: 59.6875em) {
+	.buddypress .site-content {
+		padding-top: 0;
+	}
+}
+
+.buddypress .site-content article {
+	margin: 0 4%;
+	padding-top: 0;
+}
+
+.buddypress .site-content article .entry-header,
+.buddypress .site-content article .entry-content {
+	max-width: 100%;
+	padding: 0;
+}
+
+.buddypress .site-content article #buddypress {
+	margin-bottom: 40px;
+}
+
+/**
+* ------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @subsection 2.1 - Object Nav / Subnav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress .item-list-tabs ul li {
+	width: 100%;
+}
+
+@media screen and (min-width: 400px) {
+	#buddypress .item-list-tabs ul li {
+		width: 50%;
+	}
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress .item-list-tabs ul li {
+		width: auto;
+	}
+}
+
+#buddypress .item-list-tabs ul a {
+	color: #1b6e08;
+}
+
+.directory #buddypress .item-list-tabs ul:before,
+.directory #buddypress .item-list-tabs ul:after {
+	content: " ";
+	display: table;
+}
+
+.directory #buddypress .item-list-tabs ul:after {
+	clear: both;
+}
+
+.directory #buddypress .item-list-tabs ul {
+	border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+	padding-bottom: 10px;
+}
+
+@media screen and (min-width: 783px) {
+	.directory #buddypress .item-list-tabs ul {
+		padding-bottom: 0;
+	}
+	.directory #buddypress .item-list-tabs ul li.selected {
+		border: 1px solid rgba(0, 0, 0, 0.2);
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	.directory #buddypress .item-list-tabs ul li.selected a {
+		background: none;
+		outline: none;
+	}
+}
+
+.directory #buddypress #subnav ul {
+	border-bottom: 0;
+}
+
+.directory #buddypress #subnav a {
+	color: #1b6e08;
+}
+
+/* List last filters global */
+#buddypress #subnav li#activity-filter-select.last {
+	line-height: 2.1;
+	margin-top: 0;
+}
+
+@media screen and (max-width: 783px) {
+	#buddypress #object-nav ul {
+		border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+		overflow: hidden;
+		padding-bottom: 10px;
+	}
+	#buddypress div#subnav.item-list-tabs {
+		margin-bottom: 40px;
+	}
+	#buddypress div#subnav.item-list-tabs ul li {
+		width: auto;
+	}
+}
+
+/*__ Vertical Menu User Account Screens __*/
+@media screen and (min-width: 783px) {
+	.bp-user #buddypress,
+	.single-item.groups #buddypress {
+		background: #d8d7d7;
+	}
+	#buddypress #item-header,
+	#buddypress #item-body {
+		background: #fff;
+	}
+	#buddypress #item-body {
+		border-left: 1px solid #bebebe;
+	}
+	#buddypress #object-nav {
+		background: #d8d7d7;
+		border-right: 1px solid #bebebe;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	#buddypress #object-nav ul {
+		border-bottom: 0;
+		border-top: 1px solid #bebebe;
+		padding: 0;
+	}
+	#buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	#buddypress #object-nav ul li:focus, #buddypress #object-nav ul li:hover {
+		background: #f1f1f1;
+	}
+	#buddypress #object-nav ul li span {
+		border-radius: 10%;
+		float: right;
+		margin-right: 15px;
+	}
+	#buddypress #object-nav ul li.current {
+		background: #f1f1f1;
+	}
+	#buddypress #item-body {
+		overflow: hidden;
+		padding: 0 20px;
+		width: auto;
+	}
+	#buddypress #item-body #subnav {
+		border: 1px solid #bebebe;
+		border-left: 0;
+		border-right: 0;
+		margin: 0 -20px 20px;
+	}
+	#buddypress #item-body #subnav li:focus,
+	#buddypress #item-body #subnav li:hover {
+		background: #f1f1f1;
+	}
+	#buddypress #item-body #subnav li.current {
+		background: #f1f1f1;
+	}
+	#buddypress #item-body #subnav li#activity-filter-select:focus, #buddypress #item-body #subnav li#activity-filter-select:hover {
+		background: none;
+	}
+	#buddypress #item-body #subnav li#activity-filter-select select {
+		max-width: 130px;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @subsection 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .item-title a {
+	display: block;
+	font-size: 22px;
+	font-size: 1.375rem;
+	text-decoration: none;
+}
+
+#buddypress .item-list .item-title span.update,
+#buddypress .item-list .item-title span a {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	float: none;
+	margin-bottom: 20px;
+	text-align: center;
+}
+
+#buddypress ul.item-list li .item-avatar a img.avatar {
+	float: none;
+}
+
+#buddypress ul.item-list li div.item-title {
+	float: none;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.item-title > a {
+	text-align: center;
+}
+
+#buddypress ul.item-list li .item {
+	margin-left: 0;
+}
+
+#buddypress ul.item-list li div.item-desc {
+	margin-left: 0;
+	width: auto;
+}
+
+#buddypress ul.item-list li div.action {
+	clear: both;
+	float: none;
+	margin: 0;
+	position: relative;
+	text-align: left;
+}
+
+#buddypress ul.item-list li div.action div {
+	margin: 0 0 10px;
+}
+
+#buddypress ul.item-list li .activity-meta,
+#buddypress ul.item-list li .action {
+	float: none;
+}
+
+#buddypress ul.item-list li .activity-meta a,
+#buddypress ul.item-list li .activity-meta div,
+#buddypress ul.item-list li .action a,
+#buddypress ul.item-list li .action div {
+	display: block;
+	margin: 5px 0;
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress ul.item-list li .item-avatar {
+		float: left;
+		margin-bottom: 0;
+	}
+	#buddypress ul.item-list li div.item-title > a {
+		text-align: left;
+	}
+	#buddypress ul.item-list li .item {
+		float: right;
+		margin-left: 5%;
+		width: 80%;
+	}
+	#buddypress ul.item-list li .activity-meta {
+		clear: both;
+	}
+	#buddypress ul.item-list li .activity-meta a,
+	#buddypress ul.item-list li .activity-meta div {
+		display: inline-block;
+		margin: 5px 5px 0 0;
+	}
+}
+
+@media screen and (min-width: 783px) {
+	#buddypress ul.item-list li .item {
+		float: left;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item div.item-desc {
+		margin: 0;
+		width: auto;
+	}
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		top: 0;
+		width: 25%;
+	}
+	#buddypress ul.item-list li div.action div {
+		display: block;
+	}
+	#buddypress ul.item-list li div.action div a {
+		box-sizing: border-box;
+		display: block;
+		width: 100%;
+	}
+	#buddypress ul.item-list li div.action .meta {
+		text-align: center;
+	}
+}
+
+#buddypress ul.item-list li .meta,
+#buddypress ul.item-list li .item-meta .activity {
+	font-style: italic;
+}
+
+/**
+*------------------------------------------------------------
+* @section 4.1 - Activity
+*------------------------------------------------------------
+*/
+/**
+*-------------------------------------------------
+* @subsection 4.1.1 - Activity What's New
+*-------------------------------------------------
+*/
+#buddypress form#whats-new-form {
+	margin: 40px 0;
+}
+
+#buddypress form#whats-new-form #whats-new-avatar {
+	float: none;
+	margin-bottom: 20px;
+	text-align: center;
+}
+
+#buddypress form#whats-new-form #whats-new-content,
+#buddypress form#whats-new-form p.activity-greeting {
+	margin-left: 0;
+	padding-left: 0;
+}
+
+#buddypress form#whats-new-form p.activity-greeting {
+	clear: left;
+	line-height: inherit;
+	margin-bottom: 5px;
+}
+
+@media screen and (max-width: 594px) {
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options {
+		display: flex;
+		flex-direction: column;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-submit {
+		order: 2;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-submit input {
+		width: 100%;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-post-in-box {
+		margin-top: 10px;
+		order: 1;
+	}
+	#buddypress form#whats-new-form #whats-new-content #whats-new-options #whats-new-post-in-box select {
+		margin-top: 5px;
+		max-width: 100%;
+		width: 100%;
+	}
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress form#whats-new-form #whats-new-avatar {
+		float: left;
+	}
+	#buddypress form#whats-new-form #whats-new-content,
+	#buddypress form#whats-new-form p.activity-greeting {
+		clear: none;
+		overflow: hidden;
+		padding-left: 20px;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.2 - Activity Listing
+*-------------------------------------------------
+*/
+#buddypress #activity-stream {
+	margin-top: 40px;
+}
+
+#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar {
+	float: none;
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar img.avatar {
+	display: inline;
+	float: none;
+}
+
+#buddypress .activity-list li.mini .activity-avatar img.avatar {
+	margin-left: 0;
+}
+
+#buddypress .activity-list li.mini .activity-meta a {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+#buddypress .activity-list .activity-content {
+	margin: 0;
+}
+
+#buddypress .activity-list .activity-content .activity-header {
+	margin: -0.5em 0 0;
+}
+
+#buddypress .activity-list .activity-content .activity-header > p {
+	line-height: 1.4;
+	margin-top: -10px;
+}
+
+#buddypress .activity-list .activity-content .activity-header img.avatar {
+	margin-bottom: 0 !important;
+	vertical-align: middle;
+}
+
+#buddypress .activity-list .activity-content .activity-inner {
+	margin: 10px 0;
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar {
+		float: left;
+		margin: 0 20px 0 0;
+	}
+	#buddypress .activity-list li.activity-item:not(.mini) .activity-avatar img.avatar {
+		display: inline;
+		float: none;
+	}
+	#buddypress .activity-list li.mini .activity-avatar {
+		margin: 0 10px 0 5%;
+	}
+	#buddypress .activity-list .activity-content {
+		margin-left: 0;
+		overflow: hidden;
+	}
+}
+
+body.activity-permalink #buddypress {
+	background: transparent;
+}
+
+@media screen and (max-width: 760px) {
+	body.activity-permalink #buddypress .activity-list li .activity-content,
+	body.activity-permalink #buddypress .activity-list li .activity-comments {
+		margin-left: 0;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.3 - Activity Comments
+*-------------------------------------------------
+*/
+#buddypress .activity .activity-comments {
+	margin-left: 0;
+	margin-top: 5px;
+}
+
+@media screen and (min-width: 594px) {
+	#buddypress .activity .activity-comments {
+		margin-left: 70px;
+	}
+}
+
+#buddypress .activity .activity-comments a {
+	color: #1b6e08;
+}
+
+#buddypress .activity .activity-comments ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 30px 0 0 2px;
+}
+
+#buddypress .activity .activity-comments ul ul {
+	border-left: 1px solid rgba(0, 0, 0, 0.1);
+	padding-left: 2%;
+}
+
+#buddypress .activity .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+@media screen and (max-width: 400px) {
+	#buddypress .activity .activity-comments .acomment-avatar {
+		display: block;
+		text-align: center;
+	}
+	#buddypress .activity .activity-comments .acomment-avatar a,
+	#buddypress .activity .activity-comments .acomment-avatar img.avatar {
+		display: inline;
+		float: none;
+	}
+	#buddypress .activity .activity-comments .acomment-content,
+	#buddypress .activity .activity-comments .acomment-meta,
+	#buddypress .activity .activity-comments .acomment-options {
+		margin: 5px;
+	}
+	#buddypress .activity .activity-comments .acomment-content {
+		padding: 0 10px;
+	}
+}
+
+#buddypress .activity .activity-comments .ac-form input[type="submit"],
+#buddypress .activity .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(43, 43, 43, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress .activity .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(0, 0, 0, 0.2);
+	text-align: center;
+}
+
+#buddypress .activity .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress .activity .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*------------------------------------------------------------
+* @section 4.2 - Members Loop
+*------------------------------------------------------------
+*/
+/*
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens
+*-------------------------------------------------------------------------------
+*/
+/* groups & user account page entry title */
+.single-item.groups .entry-title,
+.bp-user .entry-title {
+	background: #4d4c4c;
+	color: #fff;
+	font-size: 22px;
+	font-size: 1.375rem;
+	margin-bottom: 10px;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 673px) {
+	.single-item.groups .entry-title,
+	.bp-user .entry-title {
+		font-size: 28px;
+		font-size: 1.75rem;
+	}
+}
+
+/**
+*------------------------------------------------------------
+* @subsection 6.1 - Item Header: Global
+*------------------------------------------------------------
+*/
+#buddypress div#item-header {
+	padding-bottom: 10px;
+}
+
+#buddypress div#item-header img.avatar {
+	margin: 0 20px 10px 0;
+}
+
+#buddypress div#item-header #message {
+	clear: both;
+	margin-bottom: 5px;
+	padding-top: 10px;
+}
+
+#buddypress div#item-header #message p {
+	margin-bottom: 0;
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.1.1 - item-header: Groups
+*-------------------------------------------------
+*/
+@media screen and (max-width: 594px) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		margin-left: 0;
+		order: 3;
+		text-align: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions ul {
+		margin-left: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions ul li {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #message {
+		order: 4;
+	}
+}
+
+.single-item.groups #buddypress #item-header #item-actions {
+	float: none;
+	margin: 0 0 0 20px;
+	width: auto;
+}
+
+.single-item.groups #buddypress #item-header #item-actions h2 {
+	font-size: 20px;
+	font-size: 1.25rem;
+}
+
+.single-item.groups #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.single-item.groups #buddypress #item-header #item-header-content {
+	float: none;
+	overflow: hidden;
+}
+
+@media screen and (min-width: 594px) {
+	.single-item.groups #buddypress #item-header #item-actions {
+		float: right;
+	}
+	.single-item.groups #buddypress #item-header #item-header-avatar {
+		float: left;
+	}
+	.single-item.groups #buddypress #item-header #item-header-content {
+		float: none;
+		overflow: hidden;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.1.2 - item-header: User Accounts
+*-------------------------------------------------
+*/
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img {
+	float: none;
+}
+
+.bp-user #buddypress #item-header #latest-update {
+	margin-bottom: 10px;
+}
+
+@media screen and (max-width: 594px) {
+	.bp-user #buddypress #item-header #item-buttons div.generic-button {
+		display: inline-block;
+		float: none;
+		margin: 0 0 5px 0;
+		width: 100%;
+	}
+	.bp-user #buddypress #item-header #item-buttons div.generic-button a {
+		box-sizing: border-box;
+		display: block;
+		width: 100%;
+	}
+}
+
+@media screen and (min-width: 673px) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+		overflow: hidden;
+		text-align: left;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		width: 78%;
+	}
+	.bp-user #buddypress #item-header #item-header-content #latest-update {
+		margin-bottom: 20px;
+	}
+}
+
+/**
+*------------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*------------------------------------------------------------
+*/
+/**
+*-------------------------------------------------
+* @subsection 6.2.1 - item-body: Groups
+*-------------------------------------------------
+*/
+/**
+	*------------------------------
+	* 6.2.1.1 - Groups Management
+	*------------------------------
+	*/
+/* Settings */
+#group-settings-form .radio strong,
+#create-group-form .radio strong {
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+#group-settings-form .radio ul,
+#create-group-form .radio ul {
+	color: #6b6b6b;
+}
+
+#group-settings-form #group-creation-previous,
+#create-group-form #group-creation-previous {
+	margin: 10px 0;
+}
+
+/* Manage Members */
+.groups.manage-members #group-settings-form h3 {
+	margin-top: 40px;
+}
+
+.groups.manage-members #group-settings-form .item-list {
+	background: #f7f7f7;
+	border-top: 0;
+	padding: 0 0.5em;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.groups.manage-members #group-settings-form .item-list li:last-child {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	clear: none;
+	float: left;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+	color: #22830c;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	display: block;
+	float: none;
+	margin-top: 10px;
+	width: 100%;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+	*------------------------------
+	* 6.2.1.2 - Group Members List
+	*------------------------------
+	*/
+#buddypress #members-group-list .item-list h5 {
+	display: inline-block;
+	margin: 0;
+}
+
+#buddypress #members-group-list .item-list span.activity {
+	padding-left: 10px;
+}
+
+#buddypress #item-body #subnav li.groups-members-search:focus,
+#buddypress #item-body #subnav li.groups-members-search:hover,
+#buddypress #item-body #subnav li.last:focus,
+#buddypress #item-body #subnav li.last:hover {
+	background: none;
+}
+
+.groups.group-members #subnav ul:before,
+.groups.group-members #subnav ul:after {
+	content: " ";
+	display: table;
+}
+
+.groups.group-members #subnav ul:after {
+	clear: both;
+}
+
+.groups.group-members #subnav ul {
+	padding: 5px 0;
+}
+
+@media screen and (max-width: 400px) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 10px 0;
+		width: 100% !important;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	box-sizing: border-box;
+	float: left;
+}
+
+@media screen and (max-width: 400px) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+@media screen and (min-width: 783px) {
+	.groups.group-members #subnav li #search-members-form {
+		margin-left: 3px;
+	}
+}
+
+/**
+	*------------------------------
+	* 6.2.1.3 - Groups Send Invites
+	*------------------------------
+	*/
+@media screen and (min-width: 783px) {
+	.send-invites #buddypress #item-body {
+		border-top: 1px solid #bebebe;
+	}
+}
+
+@media screen and (max-width: 400px) {
+	.send-invites #buddypress #item-body .left-menu {
+		float: none;
+	}
+	.send-invites #buddypress #item-body .left-menu #invite-list {
+		height: auto;
+		width: auto;
+	}
+	.send-invites #buddypress #item-body .main-column {
+		margin-left: 0;
+	}
+}
+
+.send-invites #buddypress #item-body li img.avatar {
+	display: block;
+	float: none;
+	margin: 10px auto;
+}
+
+.send-invites #buddypress #item-body li .action {
+	margin-top: 10px;
+	text-align: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.send-invites #buddypress #item-body li .action {
+		text-align: right;
+	}
+}
+
+@media screen and (min-width: 594px) {
+	.send-invites #buddypress #item-body #invite-list {
+		width: 160px;
+	}
+	.send-invites #buddypress #item-body .main-column {
+		margin-left: 190px;
+	}
+	.send-invites #buddypress #item-body li img.avatar {
+		float: left;
+		margin: 0 10px 0 0;
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.2.2 - item-body: User Accounts
+*-------------------------------------------------
+*/
+/**
+	*------------------------------
+	* 6.2.2.2 - Extended Profiles
+	*------------------------------
+	*/
+#buddypress .profile .bp-widget h2 {
+	background: #949494;
+	color: #fff;
+	padding: 0.4em 0.2em;
+}
+
+#buddypress .profile #profile-edit-form .button-nav:before,
+#buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+#buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+	margin-left: 0;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid rgba(0, 0, 0, 0.1);
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+#buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+#buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+#buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress .profile #profile-edit-form .field-visibility-settings-close,
+#buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #000;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.5 - Messaging
+	*------------------------------
+	*/
+#buddypress table#message-threads {
+	border-left: 1px solid rgba(0, 0, 0, 0.2);
+	border-right: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress table#message-threads tbody > tr:first-child td.bulk-select-check,
+#buddypress table#message-threads tbody > tr:first-child td.thread-from {
+	border-top: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress table#message-threads tr td {
+	background: #fff;
+	border-right: 0 !important;
+	box-sizing: border-box;
+	display: inline-block;
+	float: left;
+}
+
+#buddypress table#message-threads tr td.thread-star,
+#buddypress table#message-threads tr td.thread-options {
+	border-bottom-color: rgba(0, 0, 0, 0.2);
+	border-bottom-width: 2px;
+	border-left: 0;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 549px) {
+	#buddypress table#message-threads tr td.thread-star,
+	#buddypress table#message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+#buddypress table#message-threads tr td.bulk-select-check,
+#buddypress table#message-threads tr td.thread-from {
+	border-left: 0;
+	height: 2.6em;
+}
+
+@media screen and (max-width: 320px) {
+	#buddypress table#message-threads tr td.bulk-select-check,
+	#buddypress table#message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+#buddypress table#message-threads tr td.thread-from,
+#buddypress table#message-threads tr td.thread-options {
+	border-left: 0;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+#buddypress table#message-threads tr td.thread-info {
+	border-left: 0;
+	padding-left: 41px;
+	width: 100%;
+}
+
+#buddypress table#message-threads tr td.thread-options {
+	text-align: right;
+}
+
+#buddypress table#message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+#buddypress table#message-threads tr span.from {
+	display: none;
+}
+
+#buddypress table#message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 320px) {
+	#buddypress table#message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.6875rem;
+		width: 100%;
+	}
+}
+
+#buddypress table#message-threads tr.unread td {
+	background: #f1f4ff;
+	border-color: rgba(0, 0, 0, 0.2);
+	border-top: 0;
+	border-width: 1px;
+}
+
+#buddypress table#message-threads th {
+	display: none;
+}
+
+#buddypress table#message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+#buddypress table#message-threads th.bulk-select-all,
+#buddypress table#message-threads td.bulk-select-check,
+#buddypress table#message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+#buddypress #message-thread #message-subject {
+	background: #4d4c4c;
+	color: #fff;
+	padding: 0.5em;
+}
+
+#buddypress #message-thread .message-box .message-metadata {
+	margin-bottom: 10px;
+}
+
+#buddypress #message-thread .message-box .message-content {
+	clear: left;
+	margin-left: 0;
+}
+
+#buddypress .acfb-holder {
+	list-style: none;
+	margin: 0;
+}
+
+#buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+#buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+#buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 10px;
+}
+
+#buddypress .acfb-holder li.friend-tab span.p:focus, #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+#buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+#buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+#buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+#buddypress #message-threads.sitewide-notices td strong {
+	background: #4d4c4c;
+	color: #fff;
+	font-size: 16px;
+	font-size: 1rem;
+	display: block;
+	margin-bottom: 0.4em;
+	padding: 0.2em;
+}
+
+#buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+	line-height: initial;
+}
+
+#buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+#buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+#buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+#buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: rgba(0, 0, 0, 0.2);
+	line-height: 1;
+	text-align: right;
+}
+
+#buddypress #message-threads.sitewide-notices td:last-child a:last-child {
+	line-height: 0;
+	margin-left: 0.7em;
+	text-indent: -999em;
+}
+
+#buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+#buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+}
+
+#buddypress table.notification-settings td.yes,
+#buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+#buddypress table.profile-settings {
+	width: 100%;
+}
+
+#buddypress table.profile-settings th.field-group-name,
+#buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 401px) {
+	#buddypress table.profile-settings th.field-group-name,
+	#buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+#buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/*------------------------------------------------------------------------------
+* @section - 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress input[type="text"],
+#buddypress input[type="email"],
+#buddypress input[type="password"],
+#buddypress input[type="submit"],
+#buddypress select,
+#buddypress textarea {
+	border: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form input[type="submit"],
+#buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress #send-reply.standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	clear: both;
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid #ccc;
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: left;
+	margin: 0;
+	width: 80%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid #ccc;
+	padding: 0.2em 0 0.2em 0.2em;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 20%;
+}
+
+@media screen and (min-width: 401px) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search {
+		clear: none;
+		float: right;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress div.message-search form input[type="text"] {
+		width: auto;
+	}
+	#buddypress div.message-search {
+		margin: 3px 0 0;
+	}
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="submit"],
+	#buddypress li.groups-members-search form input[type="text"] {
+		width: auto;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	border-color: rgba(0, 0, 0, 0.2);
+}
+
+#buddypress table .alt a {
+	color: #22820c;
+}
+
+/*__ User Account tables __*/
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyfourteen.scss	(revision 12265)
@@ -0,0 +1,2087 @@
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility.
+
+// Twentyfourteen Media Queries / Breakpoints
+//@media screen and (max-width: 400px)
+//@media screen and (min-width: 401px)
+//@media screen and (min-width: 594px)
+//@media screen and (min-width: 673px)
+//@media screen and (min-width: 783px)
+//@media screen and (min-width: 810px)
+//@media screen and (min-width: 846px)
+//@media screen and (min-width: 1008px)
+//@media screen and (min-width: 1040px)
+//@media screen and (min-width: 1110px)
+
+// Mixins and Variables
+
+// Common margin values
+$spacing-val-lg: 40px;
+$spacing-val-md: 20px;
+$spacing-val-sm: 10px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing to match 2014 body 100% approach
+@mixin font-size($font-size: 16) {
+	$rem-font-value: ($font-size / 16);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($box-model: border-box) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// titles dark backgrounds
+@mixin title-background-dark() {
+	background: lighten($dark-background, 30%);
+	color: $dark-back-text-color;
+}
+
+// Variabals: color definitions
+$content-background:     #fff;
+$dark-back-text-color:   #fff;
+$light-background:       #f7f7f7;
+$medium-background:      #949494;
+$dark-background:        #000;
+$border-color:           #000; // border color can be varied using rgba
+$border-med:             rgba(#000, 0.2);
+$border-light:           rgba(#000, 0.1); // BP dividers
+$body-text:              #2b2b2b; // 2015 body text color
+$bp-button-hover:        #ededed; // this is the default BP button hover background
+$notice-warning:         #d05656;
+$notice-info:            #9fd1e2;
+$notice-update:          #ced98c;
+$stripe-odd:             #ebf0ff;
+$stripe-even:            #dbe5ff;
+$unread:                 #cddaff;
+$link-action:            #c82b2b;
+
+// 2014 font family
+@mixin default-font() {
+	font-family: Lato, sans-serif;
+}
+
+/*------------------------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyfourteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfourteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the ruelesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+--------------------------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+--------------------------------------------------------------------------------
+1.0 Theme Structural Elements
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 - Activity Whats new
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Forums
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - Item-header: Groups;
+		6.1.2 -  item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1- Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group Send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading
+11.0 - Widgets
+------------------------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section - 1.0 - Theme - Structural Elements
+*-------------------------------------------------------------------------------
+*/
+
+// Twentyfourteen  adds a generic grouped ruleset of clear:both;
+// for all heading h# elements this is problematic where floats are concerned
+// We reset this here under our #buddypress namespace
+#buddypress {
+
+	h1,
+	h2,
+	h3,
+	h4,
+	h5,
+	h6 {
+		clear: none;
+	}
+}
+
+// As BP screens have specific content lets adjust the themes primary elements
+// to maximise the screen real estate for BP pages.
+
+.buddypress {
+
+	.site-content {
+		padding-bottom: $spacing-val-md;
+		padding-top: 4%;
+
+		@media screen and (min-width: 59.6875em) {
+			padding-top: 0;
+		}
+
+		article {
+			margin: 0 4%;
+			padding-top: 0;
+
+			.entry-header,
+			.entry-content {
+				max-width: 100%;
+				padding: 0;
+			}
+
+			#buddypress {
+				margin-bottom: $spacing-val-lg;
+			}
+
+		} // .article
+	} // .content
+} // .buddypress ( body class )
+
+/**
+* ------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------------
+* @subsection 2.1 - Object Nav / Subnav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress {
+
+	.item-list-tabs {
+
+		ul {
+
+			li {
+				width: 100%;
+			}
+
+			@media screen and (min-width: 400px) {
+
+				li {
+					width: 50%;
+				}
+			}
+
+			@media screen and (min-width: 594px) {
+
+				li {
+					width: auto;
+				}
+			}
+
+			a {
+				color: #1b6e08;
+			}
+		}
+	}
+}
+
+.directory #buddypress {
+
+	.item-list-tabs {
+
+		ul:before,
+		ul:after {
+			content: " ";
+			display: table;
+		}
+
+		ul:after {
+			clear: both;
+		}
+
+		ul {
+			border-bottom: 1px solid $border-med;
+			padding-bottom: $spacing-val-sm;
+		}
+
+		@media screen and (min-width: 783px) {
+
+			ul {
+				padding-bottom: 0;
+
+				li.selected {
+					border: 1px solid $border-med;
+					border-bottom-color: $content-background;
+
+					@include border-top-radius(4px);
+					margin-bottom: -1px;
+
+					a {
+						background: none;
+						outline: none;
+					}
+				}
+			}
+		} // close @media
+	} // close .item-list-tabs
+
+	#subnav {
+
+		ul {
+			border-bottom: 0;
+		}
+
+		a {
+			color: #1b6e08;
+		}
+
+	} // close #subnav
+} // close .directory #buddypress
+
+/* List last filters global */
+#buddypress {
+
+	#subnav {
+
+		li#activity-filter-select.last {
+			line-height: 2.1;
+			margin-top: 0;
+		}
+	}
+}
+
+// Add a little bottom margin & border seperation to object/subnav
+// & keep subnav to horizontal up to vert nav display for user/group screens
+
+@media screen and (max-width: 783px) {
+
+	#buddypress {
+
+		#object-nav {
+
+			ul {
+				border-bottom: 1px solid $border-med;
+				overflow: hidden;
+				padding-bottom: $spacing-val-sm;
+			}
+		}
+
+		div#subnav.item-list-tabs {
+			margin-bottom: $spacing-val-lg;
+
+			ul {
+
+				li {
+					width: auto;
+				}
+			}
+		}
+	}
+
+} // close @media
+
+/*__ Vertical Menu User Account Screens __*/
+
+// This block contains the rules to re-factor the item-body structural element
+// to sit alongside the vert menu & menu item styles.
+
+// Boders & backgrounds for user/group navs rely on one color
+// set this here for convenience of changing and clarity
+$nav-background-border: #bebebe;
+
+@media screen and (min-width: 783px) {
+
+	.bp-user,
+	.single-item.groups {
+
+		#buddypress {
+			background: lighten($nav-background-border, 10%);
+		}
+	}
+
+	#buddypress {
+
+		#item-header,
+		#item-body {
+			background: #fff;
+		}
+
+		#item-body {
+			border-left: 1px solid $nav-background-border;
+		}
+
+		#object-nav {
+			background: lighten($nav-background-border, 10%);
+			border-right: 1px solid $nav-background-border;
+			float: left;
+			margin-right: -1px;
+			width: 200px;
+
+			ul {
+				border-bottom: 0;
+				border-top: 1px solid $nav-background-border;
+				padding: 0;
+
+				li {
+					float: none;
+					overflow: hidden;
+
+					&:focus,
+					&:hover {
+						background: lighten($nav-background-border, 20%);
+					}
+
+					span {
+						border-radius: 10%;
+						float: right;
+						margin-right: 15px;
+					}
+				}
+
+				li.current {
+					background: lighten($nav-background-border, 20%);
+				}
+			}
+		}
+
+		#item-body {
+			overflow: hidden;
+			padding: 0 $spacing-val-md;
+			width: auto;
+
+			#subnav {
+				border: 1px solid $nav-background-border;
+				border-left: 0;
+				border-right: 0;
+				margin: 0 (-$spacing-val-md) $spacing-val-md;
+
+				li:focus,
+				li:hover {
+					background: lighten($nav-background-border, 20%);
+				}
+
+				li.current {
+					background: lighten($nav-background-border, 20%);
+				}
+
+				li#activity-filter-select {
+
+					&:focus,
+					&:hover {
+						background: none;
+					}
+
+					select {
+						max-width: 130px;
+					}
+				}
+
+			}
+		}
+	} // close  #buddypress
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @subsection 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+
+		.pag-count {
+			margin-left: 0;
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress {
+
+	.item-list {
+
+		.item-title {
+
+			a {
+				display: block;
+
+				@include font-size(22);
+				text-decoration: none;
+			}
+
+			span.update,
+			span a {
+
+				@include font-size(12);
+			}
+		}
+	}
+}
+
+// This block:
+// - Manages li items, Stacks them small screen, floated left at wider screens.
+// - Adjusts display of action buttons at small screen
+
+#buddypress {
+
+	ul.item-list {
+
+		li {
+			overflow: hidden !important; // buddypress styles over specified
+
+			.item-avatar {
+				float: none;
+				margin-bottom: $spacing-val-md;
+				text-align: center;
+
+				a {
+
+					img.avatar {
+						float: none;
+					}
+				}
+			}
+
+			div.item-title {
+				float: none;
+				width: 100%;
+
+				> a {
+					text-align: center;
+				}
+			}
+
+			.item {
+				margin-left: 0;
+			}
+
+			// groups group description
+			div.item-desc {
+				margin-left: 0;
+				width: auto;
+			}
+
+			div.action {
+				clear: both;
+				float: none;
+				margin: 0;
+				position: relative;
+				text-align: left;
+
+				div {
+					margin: 0 0 $spacing-val-sm;
+				}
+
+			} // close .action
+
+			// item-list buttons blocked/full width for mobile/small screens
+
+			.activity-meta,
+			.action {
+				float: none;
+
+				a,
+				div {
+					display: block;
+					margin: $spacing-val-xs 0;
+				}
+			}
+
+			@media screen and (min-width: 594px) {
+
+				.item-avatar {
+					float: left;
+					margin-bottom: 0;
+				}
+
+				div.item-title > a {
+					text-align: left;
+				}
+
+				.item {
+					float: right;
+					margin-left: 5%;
+					width: 80%;
+				}
+
+				.activity-meta {
+					clear: both;
+
+					a,
+					div {
+						display: inline-block;
+						margin: $spacing-val-xs $spacing-val-xs 0 0;
+					}
+				}
+
+			} // close @media
+
+			@media screen and (min-width: 783px) {
+
+				.item {
+					float: left;
+					width: 55%;
+
+					div.item-desc {
+						margin: 0;
+						width: auto;
+					}
+				}
+
+				div.action {
+					clear: none;
+					float: right;
+					top: 0;
+					width: 25%;
+
+					div {
+						display: block;
+
+						a {
+
+							@include box-model(border-box);
+							display: block;
+							width: 100%;
+						}
+					}
+
+					.meta {
+						text-align: center;
+					}
+				} // close .action
+
+			} // close @media
+
+			.meta,
+			.item-meta .activity {
+				font-style: italic;
+			}
+
+		} // close li
+	} // ul.item-list
+} // close #budypress
+
+/**
+*------------------------------------------------------------
+* @section 4.1 - Activity
+*------------------------------------------------------------
+*/
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.1 - Activity What's New
+*-------------------------------------------------
+*/
+#buddypress {
+
+	form#whats-new-form {
+		margin: $spacing-val-lg 0;
+
+		#whats-new-avatar {
+			float: none;
+			margin-bottom: $spacing-val-md;
+			text-align: center;
+		}
+
+		#whats-new-content,
+		p.activity-greeting {
+			margin-left: 0;
+			padding-left: 0;
+		}
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			clear: left;
+			line-height: inherit;
+			margin-bottom: $spacing-val-xs;
+		}
+
+		#whats-new-content {
+
+			@media screen and (max-width: 594px) {
+
+				#whats-new-options {
+					display: flex;
+					flex-direction: column;
+
+					#whats-new-submit {
+						order: 2;
+
+						input {
+							width: 100%;
+						}
+					}
+
+					#whats-new-post-in-box {
+						margin-top: $spacing-val-sm;
+						order: 1;
+
+						select {
+							margin-top: $spacing-val-xs;
+							max-width: 100%;
+							width: 100%;
+						}
+					}
+				}
+			}
+		} // close whats-new-content.active
+
+		@media screen and (min-width: 594px) {
+
+			#whats-new-avatar {
+				float: left;
+			}
+
+			#whats-new-content,
+			p.activity-greeting {
+				clear: none;
+				overflow: hidden;
+				padding-left: $spacing-val-md;
+			}
+		}
+	} // close form#whats-new-form
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.2 - Activity Listing
+*-------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+		margin-top: $spacing-val-lg;
+	}
+
+	.activity-list {
+
+		li.activity-item:not(.mini) {
+
+			.activity-avatar {
+				float: none;
+				margin-bottom: $spacing-val-sm;
+				text-align: center;
+
+				img.avatar {
+					display: inline;
+					float: none;
+				}
+			}
+		}
+
+		li.mini {
+
+			.activity-avatar {
+
+				img.avatar {
+					margin-left: 0;
+				}
+			}
+
+			.activity-meta {
+
+				a {
+
+					@include font-size(12);
+				}
+			}
+		}
+
+		.activity-content {
+			margin: 0;
+
+			.activity-header {
+				margin: -0.5em 0 0;
+
+				> p {
+					line-height: 1.4;
+					margin-top: -$spacing-val-sm;
+				}
+
+				// BP adds !important. need to override prop val
+				img.avatar {
+					margin-bottom: 0 !important;
+					vertical-align: middle;
+				}
+			}
+
+			.activity-inner {
+				margin: $spacing-val-sm 0;
+			}
+		}
+
+		@media screen and (min-width: 594px) {
+
+			li.activity-item:not(.mini) {
+
+				.activity-avatar {
+					float: left;
+					margin: 0 $spacing-val-md 0 0;
+
+					img.avatar {
+						display: inline;
+						float: none;
+					}
+				}
+			}
+
+			li.mini {
+
+				.activity-avatar {
+					margin: 0 $spacing-val-sm 0 5%;
+				}
+			}
+
+			.activity-content {
+				margin-left: 0;
+				overflow: hidden;
+			}
+		}
+
+	} // close .activity-list
+} // close #buddypress
+
+// Activity Single Item 'Permalink' Page
+body.activity-permalink {
+
+	#buddypress {
+		// remove user account faux column background
+		background: transparent;
+
+		.activity-list {
+
+			li {
+
+				@media screen and (max-width: 760px) {
+
+					.activity-content,
+					.activity-comments {
+						margin-left: 0;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 4.1.3 - Activity Comments
+*-------------------------------------------------
+*/
+
+#buddypress {
+
+	.activity {
+
+		.activity-comments {
+			margin-left: 0;
+			margin-top: 5px;
+
+			@media screen and (min-width: 594px) {
+				margin-left: 70px;
+			}
+
+			a {
+				color: #1b6e08;
+			}
+
+			ul {
+				background: rgba($light-background, 0.6);
+				margin: 30px 0 0 2px;
+			}
+
+			ul ul {
+				border-left: 1px solid $border-light;
+				padding-left: 2%;
+			}
+
+			.acomment-meta {
+				color: #737373;
+			}
+
+			// Display the user avatar stacked up to 594px
+			// reduce/widen the content margins
+			@media screen and (max-width: 400px) {
+
+				.acomment-avatar {
+					display: block;
+					text-align: center;
+
+					a,
+					img.avatar {
+						display: inline;
+						float: none;
+					}
+				}
+
+				.acomment-content,
+				.acomment-meta,
+				.acomment-options {
+					margin: $spacing-val-xs;
+				}
+
+				.acomment-content {
+					padding: 0 $spacing-val-sm;
+				}
+			} // close @media
+
+			.ac-form {
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid $border-med;
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+	} // close .activity
+
+} // close #buddypress
+
+
+/**
+*------------------------------------------------------------
+* @section 4.2 - Members Loop
+*------------------------------------------------------------
+*/
+
+
+/*
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens
+*-------------------------------------------------------------------------------
+*/
+
+/* groups & user account page entry title */
+.single-item.groups,
+.bp-user {
+
+	.entry-title {
+
+		@include title-background-dark();
+		@include font-size(22);
+		margin-bottom: $spacing-val-sm;
+		padding: 0.2em;
+	}
+
+	@media screen and (min-width: 673px) {
+
+		.entry-title {
+
+			@include font-size(28);
+		}
+	}
+
+} // close #buddypress
+
+/**
+*------------------------------------------------------------
+* @subsection 6.1 - Item Header: Global
+*------------------------------------------------------------
+*/
+// override BP margins - users/groups manage their own styles later on
+#buddypress {
+
+	div#item-header {
+		padding-bottom: $spacing-val-sm;
+
+		img.avatar {
+			margin: 0 $spacing-val-md $spacing-val-sm 0;
+		}
+
+		// Prevent messages block abutting bottom of floats, with padding-top
+		#message {
+			clear: both;
+			margin-bottom: $spacing-val-xs;
+			padding-top: $spacing-val-sm;
+
+			p {
+				margin-bottom: 0;
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.1.1 - item-header: Groups
+*-------------------------------------------------
+*/
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 594px) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					margin-left: 0;
+					order: 3;
+					text-align: left;
+
+					ul {
+						margin-left: 0;
+
+						li {
+							float: left;
+						}
+					}
+				} // close #item-actions
+
+				#message {
+					order: 4;
+				}
+
+			}
+		} // close @media
+
+		#item-header {
+
+			#item-actions {
+				float: none;
+				margin: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				h2 {
+
+					@include font-size(20);
+				}
+			}
+
+			#item-header-avatar {
+				text-align: center;
+			}
+
+			#item-header-content {
+				float: none;
+				overflow: hidden;
+			}
+
+			@media screen and (min-width: 594px) {
+
+				#item-actions {
+					float: right;
+				}
+
+				#item-header-avatar {
+					float: left;
+				}
+
+				#item-header-content {
+					float: none;
+					overflow: hidden;
+				}
+			}// close @media
+
+		}
+	}
+} // close .single-item.groups
+
+/**
+*-------------------------------------------------
+* @subsection 6.1.2 - item-header: User Accounts
+*-------------------------------------------------
+*/
+
+.bp-user #buddypress {
+
+	#item-header {
+
+		#item-header-avatar {
+			text-align: center;
+			width: 100%;
+
+			img {
+				float: none;
+			}
+		}
+
+		#latest-update {
+			margin-bottom: $spacing-val-sm;
+		}
+
+		@media screen and (max-width: 594px) {
+
+			#item-buttons {
+
+				div.generic-button {
+					display: inline-block;
+					float: none;
+					margin: 0 0 $spacing-val-xs 0;
+					width: 100%;
+
+					a {
+
+						@include box-model();
+						display: block;
+						width: 100%;
+					}
+				}
+			}
+		}
+
+		@media screen and (min-width: 673px) {
+
+			#item-header-avatar {
+				float: left;
+				overflow: hidden;
+				text-align: left;
+				width: 20%;
+			}
+
+			#item-header-content {
+				float: right;
+				width: 78%;
+
+				#latest-update {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+	} // close #item-header
+
+} // close .bp-user #buddypress
+
+/**
+*------------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*------------------------------------------------------------
+*/
+
+/**
+*-------------------------------------------------
+* @subsection 6.2.1 - item-body: Groups
+*-------------------------------------------------
+*/
+
+/**
+	*------------------------------
+	* 6.2.1.1 - Groups Management
+	*------------------------------
+	*/
+
+/* Settings */
+
+#group-settings-form,
+#create-group-form {
+
+	.radio {
+
+		strong {
+
+			@include font-size(18);
+		}
+
+		ul {
+			color: lighten($body-text, 25%);
+		}
+	}
+
+	#group-creation-previous {
+		margin: $spacing-val-sm 0;
+	}
+}
+
+/* Manage Members */
+.groups.manage-members {
+
+	#group-settings-form {
+
+		h3 {
+			margin-top: $spacing-val-lg;
+		}
+
+		.item-list {
+			background: $light-background;
+			border-top: 0;
+			padding: 0 0.5em;
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				&:last-child {
+					border-bottom: 0;
+				}
+
+				img,
+				h5 {
+					clear: none;
+					float: left;
+
+					> a {
+						border-bottom: 0;
+						color: #22830c;
+					}
+				}
+
+				span.small {
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+					width: 100%;
+
+					a {
+						display: inline-block;
+						margin: $spacing-val-xs 0;
+						width: 100%;
+
+						@media screen and (min-width: 38.75em) {
+							width: auto;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+	*------------------------------
+	* 6.2.1.2 - Group Members List
+	*------------------------------
+	*/
+
+#buddypress {
+
+	#members-group-list {
+
+		.item-list {
+
+			h5 {
+				display: inline-block;
+				margin: 0;
+			}
+
+			span.activity {
+				padding-left: $spacing-val-sm;
+			}
+		}
+	}
+} // close #buddypress
+
+#buddypress {
+
+	#item-body {
+
+		#subnav {
+
+			li.groups-members-search:focus,
+			li.groups-members-search:hover,
+			li.last:focus,
+			li.last:hover {
+				background: none;
+			}
+		}
+	}
+}
+
+// Need to target groups members via body classes
+.groups.group-members {
+
+	#subnav {
+
+		ul:before,
+		ul:after {
+			content: " ";
+			display: table;
+		}
+
+		ul:after {
+			clear: both;
+		}
+
+		ul {
+			padding: $spacing-val-xs 0;
+		}
+
+
+		li {
+
+			@media screen and (max-width: 400px) {
+				background: $content-background;
+				padding: $spacing-val-sm 0;
+				width: 100% !important;
+			}
+
+			#search-members-form {
+
+				@include box-model(border-box);
+				float: left;
+
+				@media screen and (max-width: 400px) {
+					margin: 0;
+					width: 100%;
+
+					label {
+
+						input[type="text"] {
+							width: 100%;
+						}
+					}
+				}
+
+				@media screen and (min-width: 783px) {
+					margin-left: 3px;
+				}
+			}
+
+		}
+
+	} // close #subnav
+} // close .groups.group-members
+
+/**
+	*------------------------------
+	* 6.2.1.3 - Groups Send Invites
+	*------------------------------
+	*/
+
+.send-invites {
+
+	#buddypress {
+
+		#item-body {
+
+			@media screen and (min-width: 783px) {
+				border-top: 1px solid $nav-background-border;
+			}
+
+			@media screen and (max-width: 400px) {
+
+				.left-menu {
+					float: none;
+
+					#invite-list {
+						height: auto;
+						width: auto;
+					}
+				}
+
+				.main-column {
+					margin-left: 0;
+				}
+			}
+
+			li {
+
+				img.avatar {
+					display: block;
+					float: none;
+					margin: $spacing-val-sm auto;
+				}
+
+				.action {
+					margin-top: $spacing-val-sm;
+					text-align: left;
+					width: 100%;
+
+					@media screen and (min-width: 38.75em) {
+						text-align: right;
+					}
+				}
+			}
+
+			@media screen and (min-width: 594px) {
+
+				#invite-list {
+					width: 160px;
+				}
+
+				.main-column {
+					margin-left: 190px;
+				}
+
+				li {
+
+					img.avatar {
+						float: left;
+						margin: 0 $spacing-val-sm 0 0;
+					}
+				}
+			}
+
+		}
+	}
+}
+
+/**
+*-------------------------------------------------
+* @subsection 6.2.2 - item-body: User Accounts
+*-------------------------------------------------
+*/
+
+
+/**
+	*------------------------------
+	* 6.2.2.2 - Extended Profiles
+	*------------------------------
+	*/
+
+
+#buddypress {
+
+	.profile {
+
+		.bp-widget {
+
+			h2 {
+				background: $medium-background;
+				color: #fff;
+				padding: 0.4em 0.2em;
+			}
+		}
+
+		// Edit profile
+
+		#profile-edit-form {
+
+			.button-nav:before,
+			.button-nav:after {
+				content: " ";
+				display: table;
+			}
+
+			.button-nav:after {
+				clear: both;
+			}
+
+			ul.button-nav {
+				border-bottom: 1px solid $border-light;
+				margin-left: 0;
+
+				li {
+					float: left;
+					margin-bottom: 0;
+
+					&.current {
+						border: 1px solid $border-light;
+						border-bottom-color: #fff;
+						margin-bottom: -1px;
+					}
+				} // li
+
+				a {
+					background: none;
+					border: 0;
+
+					@include font-size(18);
+				}
+			} //.button-nav
+
+			.field-visibility-settings-toggle,
+			.field-visibility-settings {
+
+				@include font-size(14);
+			}
+
+			.field-visibility-settings-close,
+			.visibility-toggle-link {
+				background: $dark-background;
+				color: #fff;
+				padding: 0.2em 0.5em;
+			}
+		} // close profile form
+
+	} // close .profile
+} // close #buddypress - user acounts item-body
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.5 - Messaging
+	*------------------------------
+	*/
+
+#buddypress {
+
+	table#message-threads {
+		border-left: 1px solid $border-med;
+		border-right: 1px solid $border-med;
+
+		tbody > tr:first-child {
+
+			td.bulk-select-check,
+			td.thread-from {
+				border-top: 1px solid $border-med;
+			}
+		}
+
+		tr {
+
+			td {
+				background: $content-background;
+				border-right: 0 !important;
+
+				@include box-model(border-box);
+				display: inline-block;
+				float: left;
+			}
+
+			td.thread-star,
+			td.thread-options {
+				border-bottom-color: darken($border-med, 5%);
+				border-bottom-width: 2px;
+				border-left: 0;
+				height: 2.4em;
+				padding-bottom: 0.2em;
+				padding-top: 0.2em;
+
+				@media screen and (max-width: 549px) {
+					padding-top: 0;
+				}
+			}
+
+			td.bulk-select-check,
+			td.thread-from {
+				border-left: 0;
+				height: 2.6em;
+
+				@media screen and (max-width: 320px) {
+					height: 5.2em;
+				}
+			}
+
+			td.thread-from,
+			td.thread-options {
+				border-left: 0;
+
+				@include calc(width, "100% - 30px");
+				margin-left: 0;
+			}
+
+			td.thread-info {
+				border-left: 0;
+				padding-left: 41px;
+				width: 100%;
+			}
+
+			td.thread-options {
+				text-align: right;
+
+				a {
+
+					@include font-size(12);
+					line-height: 2.2;
+				}
+			}
+
+			span.from {
+				display: none;
+			}
+
+			span.activity {
+				display: block;
+				float: right;
+				line-height: 2;
+
+				@media screen and (max-width: 320px) {
+					clear: both;
+
+					@include font-size(11);
+					width: 100%;
+				}
+			}
+		}
+
+		tr.unread {
+
+			td {
+				background: lighten($unread, 7%);
+				border-color: $border-med;
+				border-top: 0;
+				border-width: 1px;
+			}
+		}
+
+		th {
+			display: none;
+		}
+
+		th.bulk-select-all {
+			border-bottom: 0;
+			display: inline-block;
+			text-align: left;
+		}
+
+		th.bulk-select-all,
+		td.bulk-select-check,
+		td.thread-star {
+			border-right: 0;
+			width: 30px;
+		}
+
+	} // close #messages-threads
+
+	#message-thread {
+
+		#message-subject {
+
+			@include title-background-dark();
+			padding: 0.5em;
+		}
+
+		.message-box {
+
+			.message-metadata {
+				margin-bottom: $spacing-val-sm;
+			}
+
+			.message-content {
+				clear: left;
+				margin-left: 0;
+			}
+		}
+	}
+
+	.acfb-holder {
+		list-style: none;
+		margin: 0;
+
+		li {
+			margin-left: 0;
+		}
+
+		li.friend-tab {
+			background: lighten($notice-info, 20%);
+			border: inherit;
+			margin-right: 0;
+			padding: 0.5em;
+
+			span.p {
+				// the 'X' close
+				padding-left: $spacing-val-sm;
+
+				&:focus,
+				&:hover {
+					color: $link-action;
+					cursor: pointer;
+				}
+			}
+
+			a {
+				border-bottom: 0;
+				text-decoration: none;
+
+				img {
+					// this is the tiny user avatar
+					display: inline;
+					height: 20px;
+					vertical-align: middle;
+					width: 20px !important; // override inline style
+				}
+			}
+		}
+	}
+
+	#message-threads.sitewide-notices {
+
+		td {
+			width: 100%;
+
+			strong {
+
+				@include title-background-dark();
+				@include font-size(16);
+				display: block;
+				margin-bottom: 0.4em;
+				padding: 0.2em;
+			}
+
+			a {
+				display: inline-block;
+				line-height: initial;
+			}
+		}
+
+		td:first-child {
+			display: none;
+		}
+
+		td:first-child + td + td {
+			// notice date
+			border-bottom: 0;
+
+			span {
+				line-height: 1;
+			}
+		}
+
+		td:last-child {
+			// notice actions
+			border-bottom-color: $border-med;
+			line-height: 1;
+			text-align: right;
+
+			a:last-child {
+				// hide the 'x' text
+				line-height: 0;
+				margin-left: 0.7em;
+				text-indent: -999em;
+			}
+
+			a:last-child:after {
+				content: attr(title);
+				display: block;
+				line-height: initial;
+				text-indent: 0;
+			}
+		}
+	}
+
+} // close #buddypress - user acounts item-body
+
+.bp-user {
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		ul {
+			margin: 0;
+		}
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+		}
+	}
+
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+#buddypress {
+
+	#settings-form {
+		// 'p' = email notification screen sub heading
+		> p {
+
+			@include font-size(20);
+		}
+	}
+
+	table.notification-settings {
+
+		td.yes,
+		td.no {
+			vertical-align: middle;
+		}
+	}
+	// Profile table td widths
+	table.profile-settings {
+		width: 100%;
+
+		th.field-group-name,
+		td.field-name {
+			width: 50%;
+		}
+
+		@media screen and (min-width: 401px) {
+
+			th.field-group-name,
+			td.field-name {
+				width: 70%;
+			}
+		}
+
+		td.field-visibility {
+
+			select {
+				width: 100%;
+			}
+		}
+	}
+} // close #buddypress - user acounts item-body
+
+/*------------------------------------------------------------------------------
+* @section - 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Match BP form elements to Themes(2014) styling where styles
+// are not inherited or being overriden.
+#buddypress {
+
+	input[type="text"],
+	input[type="email"],
+	input[type="password"],
+	input[type="submit"],
+	select,
+	textarea {
+		border: 1px solid $border-med;
+	}
+
+	.standard-form {
+
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		input[type="submit"],
+		textarea {
+			width: 100%;
+		}
+	}
+
+	// BP :(
+	#send-reply.standard-form textarea {
+		width: 100%;
+	}
+
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid $border-med;
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		clear: both;
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		// Stylise the seach form elements, in part this deals with padding
+		// issues on the submit & sizing issues between bp styles & twentyfourteen
+		form {
+			border: 1px solid #ccc;
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 80%;
+			}
+
+			input[type="text"] {
+				float: left;
+				margin: 0;
+				width: 80%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+				border: 0;
+
+				@include font-size(14);
+				line-height: inherit;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid #ccc;
+				padding: 0.2em 0 0.2em 0.2em;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0.2em 1em;
+				text-align: center;
+				text-transform: none;
+				width: 20%;
+			}
+
+		}// close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+	@media screen and (min-width: 401px) {
+
+		div.dir-search,
+		div.message-search {
+			clear: none;
+			float: right;
+
+			form {
+
+				label,
+				input[type="submit"],
+				input[type="text"] {
+					width: auto;
+				}
+			}
+		}
+
+		div.message-search {
+			margin: 3px 0 0;
+		}
+
+		li.groups-members-search {
+
+			form {
+
+				label,
+				input[type="submit"],
+				input[type="text"] {
+					width: auto;
+				}
+			}
+		}
+
+	} // close @media
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables General
+*-------------------------------------------------------------------------------
+*/
+#buddypress {
+
+	table {
+		border-color: $border-med;
+
+		.alt a {
+			color: #22820c;
+		}
+	}
+}
+
+/*__ User Account tables __*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our messages ( 'warning' )
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen-rtl.css	(revision 12265)
@@ -0,0 +1,2172 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress twentyseventeen theme.
+
+@version 3.2.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyseventeen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+.buddypress ul.item-list h1,
+.buddypress ul.item-list h2,
+.buddypress ul.item-list h3,
+.buddypress ul.item-list h4,
+.buddypress ul.item-list h5,
+.buddypress ul.item-list h6 {
+	clear: none;
+	padding: 0;
+}
+
+.buddypress #page a {
+	box-shadow: none;
+	text-decoration: none !important;
+}
+
+.buddypress .entry-title {
+	text-align: center;
+}
+
+@media screen and (min-width: 55em) {
+	.buddypress .entry-title {
+		text-align: right;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	.buddypress.bp-user.page .site-content,
+	.buddypress.single-item.groups .site-content,
+	.buddypress.groups.group-create .site-content,
+	.directory.buddypress.page-one-column .site-content {
+		padding-top: 40px;
+	}
+	.buddypress.bp-user.page .entry-header,
+	.buddypress.single-item.groups .entry-header,
+	.buddypress.groups.group-create .entry-header,
+	.directory.buddypress.page-one-column .entry-header {
+		margin: 10px 0;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	body.buddypress.page.page-two-column #primary .entry-header {
+		width: 30%;
+	}
+	body.buddypress.page.page-two-column #primary .entry-content {
+		width: 68%;
+	}
+	body.buddypress:not(.has-sidebar) #primary.content-area,
+	body.buddypress:not(.page-two-column) #primary.content-area {
+		max-width: 100%;
+		width: 100%;
+	}
+	body.buddypress:not(.has-sidebar) #primary.content-area .entry-content,
+	body.buddypress:not(.has-sidebar) #primary.content-area .content-bottom-widgets,
+	body.buddypress:not(.page-two-column) #primary.content-area .entry-content,
+	body.buddypress:not(.page-two-column) #primary.content-area .content-bottom-widgets {
+		margin-right: 0;
+		margin-left: 0;
+	}
+	body.buddypress:not(.has-sidebar) .sidebar,
+	body.buddypress:not(.page-two-column) .sidebar {
+		float: right;
+		margin-right: 75%;
+		padding: 0;
+		width: 25%;
+	}
+}
+
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.selected,
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.current {
+	border-bottom-color: #222;
+}
+
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.selected a,
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.current a {
+	color: inherit;
+}
+
+body.colors-dark #buddypress div.item-list-tabs ul li.selected a,
+body.colors-dark #buddypress div.item-list-tabs ul li.current a {
+	background: none;
+	color: inherit;
+}
+
+body.colors-dark #buddypress #object-nav li:not(.current):focus a,
+body.colors-dark #buddypress #object-nav li:not(.current):hover a {
+	color: #555;
+}
+
+body.colors-dark #buddypress #subnav.item-list-tabs ul {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	margin-bottom: 20px;
+}
+
+body.colors-dark #buddypress #subnav.item-list-tabs ul li.last {
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+}
+
+body.colors-dark #buddypress #subnav.item-list-tabs select option {
+	background-color: #333;
+}
+
+body.colors-dark #buddypress .item-list div.meta {
+	color: #ddd;
+}
+
+body.colors-dark #buddypress .item-list div.item-desc,
+body.colors-dark #buddypress .item-list .activity-header p,
+body.colors-dark #buddypress .item-list .activity-comments ul,
+body.colors-dark #buddypress .item-list .acomment-meta {
+	color: #eee;
+}
+
+body.colors-dark #buddypress .item-list .activity-meta a,
+body.colors-dark #buddypress .item-list .action a {
+	background: #fafafa;
+}
+
+body.colors-dark #buddypress .item-list .activity-meta a:focus, body.colors-dark #buddypress .item-list .activity-meta a:hover,
+body.colors-dark #buddypress .item-list .action a:focus,
+body.colors-dark #buddypress .item-list .action a:hover {
+	background: #fff;
+}
+
+body.colors-dark #buddypress #latest-update {
+	color: #eee;
+}
+
+body.colors-dark #buddypress div.pagination * {
+	color: #ddd;
+}
+
+body.colors-dark #buddypress #item-header .user-nicename {
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body table thead tr,
+body.colors-dark #buddypress #item-body table thead th {
+	background: none;
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body table tr.alt td {
+	background: none;
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body table .field-visibility-settings,
+body.colors-dark #buddypress #item-body table .field-visibility-settings-notoggle {
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body fieldset {
+	background: none;
+}
+
+body.colors-dark #buddypress #item-body .checkbox label,
+body.colors-dark #buddypress #item-body .radio label {
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body div#invite-list {
+	background: none;
+}
+
+body.colors-dark.group-members #buddypress #subnav li {
+	background: none;
+}
+
+body.colors-dark.group-members #buddypress #subnav .groups-members-search form {
+	margin-bottom: 20px;
+	margin-top: 0;
+}
+
+.directory.colors-dark #buddypress #subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 0;
+}
+
+.directory.colors-dark #buddypress #subnav.item-list-tabs ul li.last.filter {
+	border-top: 0;
+}
+
+.directory.colors-dark #buddypress div.activity ul.item-list {
+	border-top: 0;
+}
+
+body.colors-light #buddypress div.item-list-tabs ul {
+	background-color: #fafafa;
+}
+
+body.colors-light #buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+}
+
+body.colors-light #buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+}
+
+body.colors-light #buddypress .item-list .activity-header p {
+	background-color: #f7f7f7;
+	color: #878787;
+}
+
+body.colors-light #buddypress .item-list .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+body.colors-light #buddypress #item-body .profile h2 {
+	background: #878787;
+	color: #fff;
+}
+
+body.colors-light #buddypress table tr.alt td {
+	background: #f5f5f5;
+	color: #333;
+}
+
+body.colors-light #buddypress div#invite-list {
+	background: #fafafa;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul li.selected a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f4f2df;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	margin-top: 0;
+	padding: 5px 0 5px 5px;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last label {
+	display: inline;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+	height: auto;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: left;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	body:not(.page-two-column) #buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	body:not(.page-two-column) #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body:not(.page-two-column) #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body:not(.page-two-column) #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	body:not(.page-two-column) #buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 20px 0 0;
+		width: auto;
+	}
+	body:not(.page-two-column) #buddypress #item-body #subnav {
+		margin: 0 -20px 0 0;
+	}
+	body:not(.page-two-column) #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	#buddypress #group-create-tabs.item-list-tabs ul:before,
+	#buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul li {
+		float: right;
+		width: auto;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #333;
+		outline: none;
+	}
+	#buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	font-weight: 400;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+}
+
+#buddypress .activity-meta .button:focus, #buddypress .activity-meta .button:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress .action .generic-button a:focus, #buddypress .action .generic-button a:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	overflow: hidden;
+}
+
+@media screen and (min-width: 48em) {
+	#buddypress ul.item-list li .item {
+		margin-right: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-right: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 18px;
+	font-size: 1.125rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 13px;
+	font-size: 0.8125rem;
+	padding: 10px 0;
+	text-align: right;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 5%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 22px;
+		font-size: 1.375rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin-bottom: -20px;
+	margin-right: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 10px 10px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: right;
+		margin: 10px 0 20px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(190, 190, 190, 0.5);
+	float: right;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	min-height: 1.5em;
+	padding-right: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: left;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header a {
+	color: #0075c4;
+}
+
+@media screen and (min-width: 48em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 10px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	box-shadow: none;
+	display: inline-block;
+	margin: 0 5px !important;
+	vertical-align: middle;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin-bottom: 5px;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin-bottom: 0;
+		width: auto;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 48em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.25rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	border-right: 1px solid #eaeaea;
+	margin: 20px 0 20px;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #activity-stream .activity-comments {
+		margin-right: 20px;
+	}
+}
+
+#buddypress #activity-stream .activity-comments ul {
+	margin: 15px 2px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments ul li {
+	border-top: 1px solid #bebebe;
+}
+
+#buddypress #activity-stream .activity-comments ul a {
+	color: #0077c7;
+}
+
+#buddypress #activity-stream .activity-comments .acomment-meta {
+	border-bottom: 1px solid #eaeaea;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(51, 51, 51, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(190, 190, 190, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 55em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+	#buddypress #members-list li.is-current-user .item {
+		float: none;
+		right: 0;
+		padding-right: 5%;
+		width: auto;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: left;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+@media screen and (min-width: 48em) {
+	.bp-user.page-two-column #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		margin-right: 140px;
+		margin-top: -100px;
+	}
+	.single-item.groups.page-two-column #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		margin-right: 10px;
+	}
+}
+
+.bp-user #buddypress #item-header-cover-image .user-nicename,
+.single-item.groups #buddypress #item-header-cover-image .user-nicename {
+	color: #333;
+	text-shadow: none;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress #item-header-cover-image .user-nicename,
+	.single-item.groups #buddypress #item-header-cover-image .user-nicename {
+		color: #fff;
+		text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
+	}
+}
+
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.875rem;
+	text-align: right;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		padding-right: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-right: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.875rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 48em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: left;
+		width: 28%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		margin-left: 5%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+.groups.single-item.members #buddypress #subnav.item-list-tabs ul {
+	background: none;
+	border-top: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.875rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(51, 51, 51, 0.6);
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: right;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+.bp-user #buddypress table a {
+	color: #0074c2;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 67em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.125rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 300px;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	height: auto;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 0.875rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: right;
+	margin-left: 1%;
+	width: 59%;
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: left;
+	font-family: inherit;
+	line-height: 20px;
+	width: 40%;
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile {
+	/* Edit profile */
+}
+
+.bp-user #buddypress .profile h2 {
+	margin: 40px 0 10px;
+	padding: 0.1em 0.4em 0.1em 0;
+}
+
+.bp-user #buddypress .profile table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile .profile-fields tr.alt td {
+	color: #333;
+}
+
+.bp-user #buddypress .profile .profile-fields tr:last-child {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-notoggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin-top: 10px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	border-radius: 3px;
+	background-clip: padding-box;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		right: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: none;
+	border-bottom: 1px solid #bebebe;
+}
+
+.bp-user #buddypress #message-threads thead tr th {
+	background: #555;
+}
+
+.bp-user #buddypress #message-threads tr {
+	border-bottom: 5px solid #878787;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #bebebe;
+	border-bottom-width: 1px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-info,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-top: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.6875rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: none;
+	border-color: #bebebe;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #e3f6ff;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-right: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: #b7b7b7;
+	line-height: 1;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(190, 190, 190, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress select {
+	height: auto;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(190, 190, 190, 0.5);
+	border-width: 1px;
+}
+
+#buddypress .standard-form select {
+	color: #737373;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-left: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(190, 190, 190, 0.6);
+	position: relative;
+	border-radius: 2px;
+	background-clip: padding-box;
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	margin: 0;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: right;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(190, 190, 190, 0.6);
+	font-weight: 400;
+	padding: 0.2em 0.2em 0.2em 0;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border-radius: none;
+	float: left;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+#buddypress div.dir-search {
+	margin-top: 0;
+}
+
+#buddypress .dir-search #search-members-form input[type="text"],
+#buddypress #search-groups-form input[type="text"],
+#buddypress #search-message-form input[type="text"] {
+	float: right;
+	margin: 0;
+	width: 70%;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+}
+
+@media screen and (min-width: 67em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+body.colors-dark #page #buddypress .dir-search form,
+body.colors-dark #page #buddypress .groups-members-search form,
+body.colors-dark #page #buddypress .message-search form {
+	background: #333;
+	border-color: #555;
+	border-radius: 2px;
+	background-clip: padding-box;
+	padding: 1px;
+}
+
+body.colors-dark #page #buddypress .dir-search input[type="text"],
+body.colors-dark #page #buddypress .groups-members-search input[type="text"],
+body.colors-dark #page #buddypress .message-search input[type="text"] {
+	background: none;
+}
+
+body.colors-dark #page #buddypress .dir-search input[type="submit"],
+body.colors-dark #page #buddypress .groups-members-search input[type="submit"],
+body.colors-dark #page #buddypress .message-search input[type="submit"] {
+	border-radius: 2px;
+	background-clip: padding-box;
+}
+
+body.colors-dark #page .message-search {
+	margin-top: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #f7f7f7;
+	border-color: #eaeaea;
+	color: #333;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+#buddypress .notifications .actions {
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #b0e5ff;
+	border: 1px solid #4ac3ff;
+	color: #00547d;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+#buddypress .bp-avatar-status .warning,
+#buddypress .bp-cover-image-status .warning {
+	background: #7dd4ff;
+	border: 1px solid #000;
+	color: #333;
+	font-size: 16px;
+	font-size: 1rem;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
+#buddypress .acfb-holder li.friend-tab {
+	background: #7dd4ff;
+	border: inherit;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen.css	(revision 12265)
@@ -0,0 +1,2172 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress twentyseventeen theme.
+
+@version 3.2.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyseventeen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+.buddypress ul.item-list h1,
+.buddypress ul.item-list h2,
+.buddypress ul.item-list h3,
+.buddypress ul.item-list h4,
+.buddypress ul.item-list h5,
+.buddypress ul.item-list h6 {
+	clear: none;
+	padding: 0;
+}
+
+.buddypress #page a {
+	box-shadow: none;
+	text-decoration: none !important;
+}
+
+.buddypress .entry-title {
+	text-align: center;
+}
+
+@media screen and (min-width: 55em) {
+	.buddypress .entry-title {
+		text-align: left;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	.buddypress.bp-user.page .site-content,
+	.buddypress.single-item.groups .site-content,
+	.buddypress.groups.group-create .site-content,
+	.directory.buddypress.page-one-column .site-content {
+		padding-top: 40px;
+	}
+	.buddypress.bp-user.page .entry-header,
+	.buddypress.single-item.groups .entry-header,
+	.buddypress.groups.group-create .entry-header,
+	.directory.buddypress.page-one-column .entry-header {
+		margin: 10px 0;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	body.buddypress.page.page-two-column #primary .entry-header {
+		width: 30%;
+	}
+	body.buddypress.page.page-two-column #primary .entry-content {
+		width: 68%;
+	}
+	body.buddypress:not(.has-sidebar) #primary.content-area,
+	body.buddypress:not(.page-two-column) #primary.content-area {
+		max-width: 100%;
+		width: 100%;
+	}
+	body.buddypress:not(.has-sidebar) #primary.content-area .entry-content,
+	body.buddypress:not(.has-sidebar) #primary.content-area .content-bottom-widgets,
+	body.buddypress:not(.page-two-column) #primary.content-area .entry-content,
+	body.buddypress:not(.page-two-column) #primary.content-area .content-bottom-widgets {
+		margin-left: 0;
+		margin-right: 0;
+	}
+	body.buddypress:not(.has-sidebar) .sidebar,
+	body.buddypress:not(.page-two-column) .sidebar {
+		float: left;
+		margin-left: 75%;
+		padding: 0;
+		width: 25%;
+	}
+}
+
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.selected,
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.current {
+	border-bottom-color: #222;
+}
+
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.selected a,
+body.colors-dark #buddypress #group-create-tabs.item-list-tabs li.current a {
+	color: inherit;
+}
+
+body.colors-dark #buddypress div.item-list-tabs ul li.selected a,
+body.colors-dark #buddypress div.item-list-tabs ul li.current a {
+	background: none;
+	color: inherit;
+}
+
+body.colors-dark #buddypress #object-nav li:not(.current):focus a,
+body.colors-dark #buddypress #object-nav li:not(.current):hover a {
+	color: #555;
+}
+
+body.colors-dark #buddypress #subnav.item-list-tabs ul {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	margin-bottom: 20px;
+}
+
+body.colors-dark #buddypress #subnav.item-list-tabs ul li.last {
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+}
+
+body.colors-dark #buddypress #subnav.item-list-tabs select option {
+	background-color: #333;
+}
+
+body.colors-dark #buddypress .item-list div.meta {
+	color: #ddd;
+}
+
+body.colors-dark #buddypress .item-list div.item-desc,
+body.colors-dark #buddypress .item-list .activity-header p,
+body.colors-dark #buddypress .item-list .activity-comments ul,
+body.colors-dark #buddypress .item-list .acomment-meta {
+	color: #eee;
+}
+
+body.colors-dark #buddypress .item-list .activity-meta a,
+body.colors-dark #buddypress .item-list .action a {
+	background: #fafafa;
+}
+
+body.colors-dark #buddypress .item-list .activity-meta a:focus, body.colors-dark #buddypress .item-list .activity-meta a:hover,
+body.colors-dark #buddypress .item-list .action a:focus,
+body.colors-dark #buddypress .item-list .action a:hover {
+	background: #fff;
+}
+
+body.colors-dark #buddypress #latest-update {
+	color: #eee;
+}
+
+body.colors-dark #buddypress div.pagination * {
+	color: #ddd;
+}
+
+body.colors-dark #buddypress #item-header .user-nicename {
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body table thead tr,
+body.colors-dark #buddypress #item-body table thead th {
+	background: none;
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body table tr.alt td {
+	background: none;
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body table .field-visibility-settings,
+body.colors-dark #buddypress #item-body table .field-visibility-settings-notoggle {
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body fieldset {
+	background: none;
+}
+
+body.colors-dark #buddypress #item-body .checkbox label,
+body.colors-dark #buddypress #item-body .radio label {
+	color: #eee;
+}
+
+body.colors-dark #buddypress #item-body div#invite-list {
+	background: none;
+}
+
+body.colors-dark.group-members #buddypress #subnav li {
+	background: none;
+}
+
+body.colors-dark.group-members #buddypress #subnav .groups-members-search form {
+	margin-bottom: 20px;
+	margin-top: 0;
+}
+
+.directory.colors-dark #buddypress #subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 0;
+}
+
+.directory.colors-dark #buddypress #subnav.item-list-tabs ul li.last.filter {
+	border-top: 0;
+}
+
+.directory.colors-dark #buddypress div.activity ul.item-list {
+	border-top: 0;
+}
+
+body.colors-light #buddypress div.item-list-tabs ul {
+	background-color: #fafafa;
+}
+
+body.colors-light #buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+}
+
+body.colors-light #buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+}
+
+body.colors-light #buddypress .item-list .activity-header p {
+	background-color: #f7f7f7;
+	color: #878787;
+}
+
+body.colors-light #buddypress .item-list .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+body.colors-light #buddypress #item-body .profile h2 {
+	background: #878787;
+	color: #fff;
+}
+
+body.colors-light #buddypress table tr.alt td {
+	background: #f5f5f5;
+	color: #333;
+}
+
+body.colors-light #buddypress div#invite-list {
+	background: #fafafa;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul li.selected a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f4f2df;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	margin-top: 0;
+	padding: 5px 5px 5px 0;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last label {
+	display: inline;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+	height: auto;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: right;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	body:not(.page-two-column) #buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	body:not(.page-two-column) #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body:not(.page-two-column) #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body:not(.page-two-column) #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	body:not(.page-two-column) #buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 20px;
+		width: auto;
+	}
+	body:not(.page-two-column) #buddypress #item-body #subnav {
+		margin: 0 0 0 -20px;
+	}
+	body:not(.page-two-column) #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	#buddypress #group-create-tabs.item-list-tabs ul:before,
+	#buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul li {
+		float: left;
+		width: auto;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #333;
+		outline: none;
+	}
+	#buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	font-weight: 400;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+}
+
+#buddypress .activity-meta .button:focus, #buddypress .activity-meta .button:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress .action .generic-button a:focus, #buddypress .action .generic-button a:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	overflow: hidden;
+}
+
+@media screen and (min-width: 48em) {
+	#buddypress ul.item-list li .item {
+		margin-left: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-left: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 18px;
+	font-size: 1.125rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 13px;
+	font-size: 0.8125rem;
+	padding: 10px 0;
+	text-align: left;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 5%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 22px;
+		font-size: 1.375rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin-bottom: -20px;
+	margin-left: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 10px 10px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: left;
+		margin: 10px 0 20px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(190, 190, 190, 0.5);
+	float: left;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	min-height: 1.5em;
+	padding-left: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: right;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header a {
+	color: #0075c4;
+}
+
+@media screen and (min-width: 48em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 10px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	box-shadow: none;
+	display: inline-block;
+	margin: 0 5px !important;
+	vertical-align: middle;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin-bottom: 5px;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin-bottom: 0;
+		width: auto;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 48em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.25rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	border-left: 1px solid #eaeaea;
+	margin: 20px 0 20px;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #activity-stream .activity-comments {
+		margin-left: 20px;
+	}
+}
+
+#buddypress #activity-stream .activity-comments ul {
+	margin: 15px 0 0 2px;
+}
+
+#buddypress #activity-stream .activity-comments ul li {
+	border-top: 1px solid #bebebe;
+}
+
+#buddypress #activity-stream .activity-comments ul a {
+	color: #0077c7;
+}
+
+#buddypress #activity-stream .activity-comments .acomment-meta {
+	border-bottom: 1px solid #eaeaea;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(51, 51, 51, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(190, 190, 190, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 55em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+	#buddypress #members-list li.is-current-user .item {
+		float: none;
+		left: 0;
+		padding-left: 5%;
+		width: auto;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: right;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+@media screen and (min-width: 48em) {
+	.bp-user.page-two-column #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		margin-left: 140px;
+		margin-top: -100px;
+	}
+	.single-item.groups.page-two-column #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		margin-left: 10px;
+	}
+}
+
+.bp-user #buddypress #item-header-cover-image .user-nicename,
+.single-item.groups #buddypress #item-header-cover-image .user-nicename {
+	color: #333;
+	text-shadow: none;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress #item-header-cover-image .user-nicename,
+	.single-item.groups #buddypress #item-header-cover-image .user-nicename {
+		color: #fff;
+		text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
+	}
+}
+
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.875rem;
+	text-align: left;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		padding-left: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-left: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.875rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 48em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 48em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: right;
+		width: 28%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		margin-right: 5%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+.groups.single-item.members #buddypress #subnav.item-list-tabs ul {
+	background: none;
+	border-top: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.875rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(51, 51, 51, 0.6);
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: left;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 0 0;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+.bp-user #buddypress table a {
+	color: #0074c2;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 67em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.125rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 300px;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	height: auto;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 0.875rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: left;
+	margin-right: 1%;
+	width: 59%;
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: right;
+	font-family: inherit;
+	line-height: 20px;
+	width: 40%;
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile {
+	/* Edit profile */
+}
+
+.bp-user #buddypress .profile h2 {
+	margin: 40px 0 10px;
+	padding: 0.1em 0 0.1em 0.4em;
+}
+
+.bp-user #buddypress .profile table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile .profile-fields tr.alt td {
+	color: #333;
+}
+
+.bp-user #buddypress .profile .profile-fields tr:last-child {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-notoggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin-top: 10px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	border-radius: 3px;
+	background-clip: padding-box;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		left: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: none;
+	border-bottom: 1px solid #bebebe;
+}
+
+.bp-user #buddypress #message-threads thead tr th {
+	background: #555;
+}
+
+.bp-user #buddypress #message-threads tr {
+	border-bottom: 5px solid #878787;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #bebebe;
+	border-bottom-width: 1px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-info,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-top: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.6875rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: none;
+	border-color: #bebebe;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #e3f6ff;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-left: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: #b7b7b7;
+	line-height: 1;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 48em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(190, 190, 190, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress select {
+	height: auto;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(190, 190, 190, 0.5);
+	border-width: 1px;
+}
+
+#buddypress .standard-form select {
+	color: #737373;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-right: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(190, 190, 190, 0.6);
+	position: relative;
+	border-radius: 2px;
+	background-clip: padding-box;
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	margin: 0;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: left;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(190, 190, 190, 0.6);
+	font-weight: 400;
+	padding: 0.2em 0 0.2em 0.2em;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border-radius: none;
+	float: right;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+#buddypress div.dir-search {
+	margin-top: 0;
+}
+
+#buddypress .dir-search #search-members-form input[type="text"],
+#buddypress #search-groups-form input[type="text"],
+#buddypress #search-message-form input[type="text"] {
+	float: left;
+	margin: 0;
+	width: 70%;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+}
+
+@media screen and (min-width: 67em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+body.colors-dark #page #buddypress .dir-search form,
+body.colors-dark #page #buddypress .groups-members-search form,
+body.colors-dark #page #buddypress .message-search form {
+	background: #333;
+	border-color: #555;
+	border-radius: 2px;
+	background-clip: padding-box;
+	padding: 1px;
+}
+
+body.colors-dark #page #buddypress .dir-search input[type="text"],
+body.colors-dark #page #buddypress .groups-members-search input[type="text"],
+body.colors-dark #page #buddypress .message-search input[type="text"] {
+	background: none;
+}
+
+body.colors-dark #page #buddypress .dir-search input[type="submit"],
+body.colors-dark #page #buddypress .groups-members-search input[type="submit"],
+body.colors-dark #page #buddypress .message-search input[type="submit"] {
+	border-radius: 2px;
+	background-clip: padding-box;
+}
+
+body.colors-dark #page .message-search {
+	margin-top: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #f7f7f7;
+	border-color: #eaeaea;
+	color: #333;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+#buddypress .notifications .actions {
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #b0e5ff;
+	border: 1px solid #4ac3ff;
+	color: #00547d;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+#buddypress .bp-avatar-status .warning,
+#buddypress .bp-cover-image-status .warning {
+	background: #7dd4ff;
+	border: 1px solid #000;
+	color: #333;
+	font-size: 16px;
+	font-size: 1rem;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
+#buddypress .acfb-holder li.friend-tab {
+	background: #7dd4ff;
+	border: inherit;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyseventeen.scss	(revision 12265)
@@ -0,0 +1,3180 @@
+
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility.
+
+// 2017 Media Queries / Breakpoints.
+$small-up-2017:      "min-width: 30em";
+$med-small-up-2017:  "min-width: 48em";
+$medium-up-2017:     "min-width: 55em";
+$large-up-2017:      "min-width: 67em";
+$x-large-up-2017:    "min-width: 79em";
+
+
+// In addition BP will use the em breakpoints as  referenced below to provide
+// a series of finer breakpoints for max widths, to allow fine adjustments;
+// For main structural elements we use the themes values, only using bp
+// breakpoints to manage granular elements in loops etc.
+
+$x-small-only-bp: "max-width: 22em"; // for xtra small mobile only
+$small-only-bp:   "max-width: 38.75em"; //  small only
+$small-up-bp:     "min-width: 38.75em"; //  small only
+$med-only-bp:     "max-width: 46.25em"; // med small only
+
+// Mixins and Variables
+
+// simplify remembering/writing media queries
+// written as:
+// @include medium-up {
+//  body {property: value;}
+// }
+//
+
+@mixin x-small-only {
+
+	@media screen and ($x-small-only-bp) {
+
+		@content;
+	}
+}
+
+@mixin small-only {
+
+	@media screen and ($small-only-bp) {
+
+		@content;
+	}
+}
+
+@mixin medium-only-bp {
+
+	@media screen and ($med-only-bp) {
+
+		@content;
+	}
+}
+
+@mixin small-up-bp {
+
+	@media screen and ($small-up-bp) {
+
+		@content;
+	}
+}
+
+@mixin small-up {
+
+	@media screen and ($small-up-2017) {
+
+		@content;
+	}
+}
+
+@mixin med-small-up {
+
+	@media screen and ($med-small-up-2017) {
+
+		@content;
+	}
+}
+
+@mixin medium-up {
+
+	@media screen and ($medium-up-2017) {
+
+		@content;
+	}
+}
+
+@mixin large-up {
+
+	@media screen and ($large-up-2017) {
+
+		@content;
+	}
+}
+
+@mixin x-large-up {
+
+	@media screen and ($x-large-up-2017) {
+
+		@content;
+	}
+}
+
+// Common spacing values
+$spacing-val-lg: 40px;
+$spacing-val-md: 20px;
+$spacing-val-sm: 10px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing
+// 2017 uses a basic pixel font size approach, we'll stick with rem/px
+@mixin font-size($font-size: 16) {
+	$rem-font-value: ($font-size / 16);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-radius-none() {
+	-webkit-border-radius: none;
+	-moz-border-radius: none;
+	-ms-border-radius: none;
+	border-radius: none;
+}
+
+// Box sizing
+@mixin box-model($box-model) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2017 font family
+@mixin default-font-sans() {
+	font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+}
+
+// Variables: color definitions
+
+$dark-grey:     #555;
+$medium-grey:   #878787;
+$light-grey:    #dcdcdc;
+$x-light-grey:  #f7f7f7;
+$white:         #fff;
+
+$body-text:          #333;
+$bp-light-text:      $medium-grey;
+$bp-xlight-text:     #eee;
+$content-background: #fff;
+$off-white:          #fafafa;
+$cream-background:   #f7f5e7;
+$light-background:   $x-light-grey;
+$medium-background:  $medium-grey;
+$dark-background:    $dark-grey;
+$border-color:       #bebebe;
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$background-hover:   #db572f; // 2017 link background hover
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #7dd4ff;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             $x-light-grey;
+$link-action:        #c82b2b;
+
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress twentyseventeen theme.
+
+@version 3.2.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyseventeen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+
+.buddypress {
+	// Remove any empty clearing elements, CSS provides clearing.
+	div.clear {
+		display: none;
+	}
+
+	// deal with global clearing property set on heading elements
+	// - for the moment we'll just specify this on  lists ul only
+
+	ul.item-list {
+
+		h1,
+		h2,
+		h3,
+		h4,
+		h5,
+		h6 {
+			clear: none;
+			padding: 0;
+		}
+	}
+
+	// Ensure no text decoration or box shadow causing underlined effect.
+	#page a {
+		box-shadow: none;
+		text-decoration: none !important;
+	}
+
+	.entry-title {
+		text-align: center;
+
+		@include medium-up {
+			text-align: left;
+		}
+	}
+
+} // close .buddypress
+
+// reduce the overlarge entry title margins
+@include medium-up {
+
+	.buddypress.bp-user.page,
+	.buddypress.single-item.groups,
+	.buddypress.groups.group-create,
+	.directory.buddypress.page-one-column {
+
+		.site-content {
+			padding-top: $spacing-val-lg;
+		}
+
+		.entry-header {
+			margin: $spacing-val-sm 0;
+		}
+	}
+}
+
+// Open up the content width areas for BP screens to allow max widths.
+// This takes into account the ability to columnize pages in 2017.
+// Only effect changes based on breakpoints.
+
+@include med-small-up {
+
+	body.buddypress.page.page-two-column {
+
+		#primary {
+
+			.entry-header {
+				width: 30%;
+			}
+
+			.entry-content {
+				width: 68%;
+			}
+		}
+	}
+
+	body.buddypress:not(.has-sidebar),
+	body.buddypress:not(.page-two-column) {
+
+		#primary.content-area {
+			max-width: 100%;
+			width: 100%;
+
+			.entry-content,
+			.content-bottom-widgets {
+				margin-left: 0;
+				margin-right: 0;
+			}
+		}
+
+		.sidebar {
+			float: left;
+			margin-left: 75%;
+			padding: 0;
+			width: 25%;
+		}
+
+	} // body.buddypress
+}
+
+// 2017 allows for selecting two distinct color schemes.
+// Our companion styles respect themes colors & inherit or avoid
+// setting backgrounds/colors so selecting the dark scheme generally
+// allows the theme to reset the BP text/headings etc to white
+// In places we do set backgrounds, Navs, search forms etc these
+// are added here under the themes body class to enable changing
+
+// Some element colors are inherited from BP original sheet so need to
+// be reset under the dark scheme, where set in this sheet we simply state them
+// only for the light scheme, additionally elements such as BP form styles
+// need styles removing & are handled later in 'Forms' section
+body.colors-dark {
+
+	#buddypress {
+
+		//tabs border bottom color needs to match dark background
+		#group-create-tabs.item-list-tabs {
+
+			li.selected,
+			li.current {
+				border-bottom-color: #222;
+
+				a {
+					color: inherit;
+				}
+			}
+		}
+
+		div.item-list-tabs {
+
+			ul {
+
+				li.selected,
+				li.current {
+
+					a {
+						background: none;
+						color: inherit;
+					}
+				}
+			}
+		}
+
+		#object-nav {
+
+			li:not(.current):focus,
+			li:not(.current):hover {
+
+				a {
+					color: #555;
+				}
+			}
+		}
+
+		// With the dark scheme the subnav links lose definition as nav menu links,
+		// add a border top to clarify the li.last element seperated from links.
+		#subnav.item-list-tabs {
+
+			ul {
+				border-bottom: 1px solid rgba($border-light, 0.9);
+				margin-bottom: $spacing-val-md;
+
+				li.last {
+					border-top: 1px solid rgba($border-light, 0.9);
+				}
+			}
+
+			select {
+
+				// we're forced to set a back color as Chrome won't
+				// accept inherit/none whereas FF does.
+				option {
+					background-color: #333;
+				}
+			}
+		}
+
+		// lists text: descr, meta, etc
+		.item-list {
+
+			div.meta {
+				color: #ddd;
+			}
+
+			div.item-desc,
+			.activity-header p,
+			.activity-comments ul,
+			.acomment-meta {
+				color: $bp-xlight-text;
+			}
+
+			.activity-meta,
+			.action {
+
+				a {
+					background: $off-white;
+
+					&:focus,
+					&:hover {
+						background: $white;
+					}
+				}
+			}
+		}
+
+		#latest-update {
+			color: $bp-xlight-text;
+		}
+
+		div.pagination * {
+			color: #ddd;
+		}
+
+		#item-header {
+
+			.user-nicename {
+				color: $bp-xlight-text;
+			}
+		}
+
+		#item-body {
+
+			table {
+
+				thead {
+
+					tr,
+					th {
+						background: none;
+						color: $bp-xlight-text;
+					}
+				}
+
+				tr.alt {
+
+					td {
+						background: none;
+						color: $bp-xlight-text;
+					}
+				}
+
+				.field-visibility-settings,
+				.field-visibility-settings-notoggle {
+					color: #eee;
+				}
+
+			}
+
+			fieldset {
+				background: none;
+			}
+
+			.checkbox,
+			.radio {
+
+				label {
+					color: #eee;
+				}
+			}
+
+			div#invite-list {
+				background: none;
+			}
+		}
+
+	} // close #buddypress
+}
+
+
+body.colors-dark.group-members {
+
+	#buddypress {
+
+		#subnav {
+
+			li {
+				background: none;
+			}
+
+			.groups-members-search {
+
+				form {
+					margin-bottom: $spacing-val-md;
+					margin-top: 0;
+				}
+			}
+		}
+	}
+}
+
+// remove borders for dir component screens.
+.directory.colors-dark {
+
+	#buddypress {
+
+		#subnav.item-list-tabs {
+
+			ul {
+				border-bottom: 0;
+				border-top: 0;
+
+				li.last.filter {
+					border-top: 0;
+				}
+			}
+		}
+
+		div.activity {
+
+			ul.item-list {
+				border-top: 0;
+			}
+		}
+	}
+}
+
+body.colors-light {
+
+	#buddypress {
+
+		div.item-list-tabs {
+
+			ul {
+				background-color: $off-white;
+			}
+		}
+
+		div#subnav.item-list-tabs {
+
+			ul {
+				background-color: $light-background;
+
+				li.last {
+					background: $content-background;
+				}
+			}
+		}
+
+		.item-list {
+
+			.activity-header {
+
+				p {
+					background-color: $light-background;
+					color: $bp-light-text;
+				}
+			}
+
+			.activity-comments {
+
+				.acomment-meta {
+					color: #737373;
+				}
+			}
+		}
+
+		#item-body {
+
+			.profile {
+
+				h2 {
+					background: $medium-grey;
+					color: #fff;
+				}
+			}
+		}
+
+		table {
+
+			tr.alt {
+
+				td {
+					background: #f5f5f5;
+					color: $body-text;
+				}
+			}
+		}
+
+		div#invite-list {
+			background: $off-white;
+		}
+	} // close #buddypress
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets will appear over weighted in selectors
+
+#buddypress {
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected {
+
+				a {
+					background: inherit;
+					opacity: 1;
+				}
+			}
+		}
+	}
+
+	// global nav styles
+	div.item-list-tabs {
+
+		ul {
+			border-bottom: 1px solid rgba($border-light, 0.9);
+			overflow: hidden;
+			padding: 0;
+
+			li {
+
+				a {
+
+					span {
+						border-radius: 25%;
+					}
+				}
+			}
+		}
+	} // close item-list-tabs
+
+} // close #buddypress
+
+
+/*__ Horizontal menus __*/
+#buddypress {
+
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+				float: none;
+			}
+
+			@include small-only {
+
+				li:not(:last-child) {
+					border-bottom: 1px solid $border-light;
+				}
+			}
+
+			li:not(.selected) {
+
+				@include small-only {
+
+					&:focus,
+					&:hover {
+						background: darken($cream-background, 2%);
+					}
+				}
+			}
+
+			@include small-up-bp {
+
+				li {
+					float: left;
+				}
+			}
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			border-bottom: 0;
+			margin-top: $spacing-val-xs;
+			padding: 0;
+
+			li.last {
+				margin-top: 0;
+				padding: $spacing-val-xs $spacing-val-xs $spacing-val-xs 0;
+				width: 100%;
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+				}
+
+				label {
+					display: inline;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+					height: auto;
+				}
+			}
+
+			@include small-up-bp {
+
+				li.last {
+					text-align: right;
+				}
+			}
+		} // close ul
+	} // close #subnav
+
+} // close #buddypress
+
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+// 2017 right sidebar is displayed if the widget sidebar has widgets;
+// we'll only display object nav as a vert menu if not .two-column-page.
+
+@include medium-up {
+
+	body:not(.page-two-column) {
+
+		#buddypress {
+
+			#object-nav {
+				border-right: 1px solid $border-med;
+				float: left;
+				margin-right: -1px;
+				width: 200px;
+
+				ul {
+					background: none;
+					border-bottom: 0;
+					padding: 0;
+
+					li {
+						float: none;
+						overflow: hidden;
+
+						span {
+							background: $content-background;
+							border-radius: 10%;
+							float: right;
+							margin-right: 2px;
+						}
+					}
+				}
+			}
+
+			#item-body {
+				border-left: 1px solid $border-med;
+				overflow: hidden;
+				padding: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				#subnav {
+					margin: 0 0 0 (-$spacing-val-md);
+
+					ul {
+						margin-top: 0;
+					}
+				}
+			}
+
+		} // close  #buddypress
+	} // close body:not(.sidebar)
+} // close @media
+
+
+// Tabbed Nav styling
+// Primarilly used for group create screens & ? directory nav.
+
+@include med-small-up {
+
+	#buddypress {
+
+		#group-create-tabs.item-list-tabs {
+
+			ul:before,
+			ul:after {
+				content: " ";
+				display: table;
+			}
+
+			ul:after {
+				clear: both;
+			}
+
+			ul {
+				background: none;
+				border: 0;
+				border-bottom: 1px solid $border-med;
+				overflow: visible;
+				padding-bottom: 0;
+
+				li {
+					float: left;
+					width: auto;
+				}
+
+				li.selected,
+				li.current {
+					border: 1px solid $border-med;
+					border-bottom-color: $content-background;
+
+					@include border-top-radius(4px);
+					margin-bottom: -1px;
+
+					a {
+						background: none;
+						color: $body-text;
+						outline: none;
+					}
+				}
+			}
+
+		} // close .item-list-tabs
+
+		#subnav {
+
+			ul {
+				border-bottom: 0;
+			}
+
+		} // close #subnav
+	} // close #buddypress
+
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+		font-weight: 400;
+		min-height: 0;
+
+		&:before,
+		&:after {
+			height: 0;
+			width: 0;
+		}
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Adjusts Meta items and various elements to match 2017 font-family
+#buddypress {
+
+	.item-list {
+
+		.activity-header,
+		.activity-meta {
+
+			@include default-font-sans();
+		}
+	}
+}
+
+#buddypress {
+
+	.activity-meta {
+
+		.button {
+
+			&:focus,
+			&:hover {
+				background: inherit;
+				color: #000;
+			}
+		}
+	}
+
+	.action {
+
+		.generic-button {
+
+			a {
+
+				&:focus,
+				&:hover {
+					background: inherit;
+					color: #000;
+				}
+			}
+		}
+	}
+} // close #buddypress
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2017 font-family
+
+#buddypress {
+
+	ul.item-list {
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@include small-up-bp {
+					margin-bottom: 0;
+				}
+
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+					margin-bottom: $spacing-val-sm;
+
+					// Quite early on we need to move avatars to the left(right)
+					@include small-up {
+						display: block;
+						float: left;
+					}
+				}
+			}
+
+			.item {
+				overflow: hidden;
+
+				@include med-small-up {
+					margin-left: 15%;
+				}
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin-left: 0;
+					width: 94%;
+				}
+
+				.item-title {
+
+					@include font-size(18);
+					line-height: 1.2;
+					text-align: center;
+
+					@include small-up {
+						text-align: left;
+					}
+
+					width: 100%;
+
+					.update {
+						display: block;
+
+						@include font-size(13);
+
+						@include medium-up {
+
+							@include font-size(14);
+						}
+
+						padding: $spacing-val-sm 0;
+						text-align: left;
+					}
+				}
+
+			}
+
+			@include medium-up {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 5%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+
+					.item-title {
+
+						@include font-size(22);
+					}
+				}
+
+			}// close medium-up
+
+			div.action {
+				clear: left;
+				float: none;
+				margin-bottom: -$spacing-val-md;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-md 0 $spacing-val-xs;
+
+				// push the actions to the right
+				@include medium-up {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-sm 0;
+					width: 100%;
+
+					a {
+						display: block;
+						width: 100%;
+					}
+					// Provisionally lets keep the 'action' div
+					// position but inline-block the buttons
+
+					@include small-up {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm $spacing-val-sm 0;
+						width: auto;
+					} // close @media
+
+					@include medium-up {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		@include medium-only-bp {
+
+			#whats-new-content {
+				clear: left;
+				margin: $spacing-val-sm 0 $spacing-val-md;
+				padding: $spacing-val-sm 0 0;
+			}
+		}
+
+		#whats-new-content.active {
+
+			#whats-new-options[style] {
+
+				#whats-new-post-in-box {
+					border: 1px solid rgba($border-color, 0.5);
+					float: left;
+					line-height: 1.5;
+					margin-top: 12px;
+					padding-left: 0.2em;
+					width: 100%;
+
+					select {
+						background: none;
+						border: 0;
+						float: right;
+						margin: 0;
+						min-height: 1.5em;
+						padding-left: 0.4em;
+					}
+				}
+
+				@include small-up {
+
+					#whats-new-post-in-box {
+						width: auto;
+					}
+
+					#whats-new-submit {
+						float: right;
+					}
+				}
+
+			} // close #whats-new-options
+		} // close #whats-new-content.active
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content {
+				margin-left: 0;
+
+				.activity-header {
+
+					@include font-size(14);
+
+					a {
+						color: #0075c4;
+					}
+				}
+			}
+
+			@include med-small-up {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+			}
+		}
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				margin-right: 0;
+
+				p {
+					border: 1px solid rgba($border-light, 0.6);
+					margin-top: 0;
+					padding: 0 0.2em;
+				}
+
+				img.avatar {
+					box-shadow: none;
+					display: inline-block;
+					margin: 0 5px !important; // have to match to BP
+					vertical-align: middle;
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+					margin-bottom: $spacing-val-xs;
+
+					@include small-up {
+						display: inline-block;
+						margin-bottom: 0;
+						width: auto;
+					}
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+
+				a {
+					font-style: italic;
+				}
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@include med-small-up {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			border-left: 1px solid $border-light;
+			margin: $spacing-val-md 0 $spacing-val-md;
+
+			@include small-up {
+				margin-left: $spacing-val-md;
+			}
+
+			ul {
+				margin: 15px 0 0 2px;
+
+				li {
+					border-top: 1px solid $border-color;
+				}
+
+				a {
+					color: #0077c7;
+				}
+			}
+
+			.acomment-meta {
+				border-bottom: 1px solid $border-light;
+			}
+
+			.ac-form {
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+	} // close .activity
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@include medium-up {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+
+			li.is-current-user {
+
+				.item {
+					float: none;
+					left: 0;
+					padding-left: 5%;
+					width: auto;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#signup_form.standard-form {
+
+		#profile-details-section,
+		#basic-details-section,
+		#blog-details-section {
+			float: none;
+			width: 100%;
+
+			@include small-up-bp {
+				width: 48%;
+			}
+		}
+
+		@include small-up {
+
+			#profile-details-section {
+				float: right;
+			}
+
+			#basic-details-section {
+				float: left;
+			}
+		}
+
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+@include med-small-up {
+
+
+	.bp-user.page-two-column {
+
+		#buddypress {
+
+			#cover-image-container {
+
+				#item-header-cover-image {
+
+					#item-header-content {
+						margin-left: 140px;
+						margin-top: -100px;
+					}
+				}
+			}
+		}
+	}
+
+	.single-item.groups.page-two-column {
+
+		#buddypress {
+
+			#cover-image-container {
+
+				#item-header-cover-image {
+
+					#item-header-content {
+						margin-left: 10px;
+					}
+				}
+			}
+		}
+	}
+
+} // close media
+
+.bp-user,
+.single-item.groups {
+
+	#buddypress {
+
+		// we need to remove the cover image dec styles
+		// & reset them for med up  only.
+		#item-header-cover-image {
+
+			.user-nicename {
+				color: $body-text;
+				text-shadow: none;
+			}
+		}
+
+		@include med-small-up {
+
+			#item-header-cover-image {
+
+				.user-nicename {
+					color: $white;
+					text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
+				}
+			}
+		}
+
+		#item-header-content {
+
+			#item-meta {
+
+				@include font-size(14);
+				text-align: left;
+
+				p {
+					margin-bottom: 0.5em;
+				}
+			}
+		}
+	}
+
+	@include medium-only-bp {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	@include small-only {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	#buddypress {
+
+		@include medium-only-bp {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+
+		@include small-only {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@include medium-only-bp {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@include small-only {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+
+					h2 {
+						border-bottom: 1px solid $border-light;
+						text-align: center;
+					}
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			div#item-actions {
+				margin: 0;
+				width: 100%;
+
+				@include small-up-bp {
+					border-left: 1px solid $border-light;
+					clear: none;
+					float: right;
+					padding-left: 0.2em;
+					width: 30%;
+				} // close @media
+
+				@include med-small-up {
+					width: 40%;
+				}
+
+				ul {
+					margin-top: 0;
+					padding-left: 0;
+				}
+
+				h2 {
+
+					@include font-size(14);
+
+					@include med-small-up {
+
+						@include font-size(16);
+					}
+
+					padding: 0.2em;
+				}
+			} // close item-actions
+
+			@include med-small-up {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-avatar {
+					width: 21%;
+				}
+
+				#item-header-content {
+					margin-left: 4%;
+					width: 40%;
+				}
+
+				div#item-actions {
+					float: right;
+					width: 28%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+	} // close #buddypress
+} // close .groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+			padding: $spacing-val-md 0;
+
+			#item-header-avatar {
+				text-align: center;
+				width: 100%;
+
+				img.avatar,
+				a {
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			@include med-small-up {
+
+				#item-header-avatar {
+					float: left;
+					width: 20%;
+
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: right;
+					margin-right: 5%;
+					width: 69%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+.groups.single-item.members {
+
+	#buddypress {
+
+		#subnav.item-list-tabs {
+
+			ul {
+				background: none;
+				border-top: 0;
+			}
+
+		}
+	}
+
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+		textarea {
+			height: auto;
+			min-height: 100px;
+			overflow: auto;
+		}
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form {
+
+		div.radio label {
+			border: 1px solid $border-light;
+			padding: 0.2em;
+
+			ul {
+				color: rgba($body-text, 0.6);
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+					float: left;
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+
+					a {
+						display: inline-block;
+						margin: $spacing-val-xs 0;
+						width: 100%;
+
+						@media screen and (min-width: 38.75em) {
+							width: auto;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+
+			@include small-only {
+				background: $content-background;
+				padding: $spacing-val-md 0;
+			}
+
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+
+				@include small-only {
+					margin: 0;
+					width: 100%;
+
+					label {
+
+						input[type="text"] {
+							width: 100%;
+						}
+					}
+				}
+
+				margin: $spacing-val-xs 0 0;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	.entry-title {
+		margin-bottom: 0.5em;
+	}
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(14);
+			}
+
+			td {
+
+				@include font-size(12);
+			}
+
+			a {
+				color: #0074c2;
+			}
+
+			@include medium-up {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+
+			@include large-up {
+
+				th {
+
+					@include font-size(18);
+				}
+
+				td {
+
+					@include font-size(16);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav,
+		.messages-options-nav {
+
+			float: left;
+			width: 100%;
+
+			@include small-up-bp {
+				width: 300px;
+			}
+
+			select {
+				height: auto;
+			}
+
+			select,
+			input {
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 1%;
+				width: 59%;
+			}
+
+			input {
+				float: right;
+				font-family: inherit;
+				line-height: 20px;
+				width: 40%;
+			}
+
+			input[disabled="disabled"]:focus,
+			input[disabled="disabled"]:hover {
+				background: none;
+			}
+
+
+		} // close .notifications-options-nav/.messages-options-nav
+
+	} //close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			h2 {
+				margin: $spacing-val-lg 0 $spacing-val-sm;
+				padding: 0.1em 0 0.1em 0.4em;
+			}
+
+			table {
+				margin-top: 0;
+			}
+
+			.profile-fields {
+
+				tr.alt {
+
+					td {
+						color: $body-text;
+					}
+				}
+
+				tr:last-child {
+					border-bottom: 0;
+				}
+			}
+
+			/* Edit profile */
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings-notoggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+					margin-top: $spacing-val-sm;
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: $dark-background;
+
+					@include border-radius(3px);
+					color: #fff;
+					padding: 0.2em 0.5em;
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#groups-list {
+
+			li {
+
+				.item {
+
+					@media screen and (min-width: 77.5em) {
+						left: 5%;
+						width: 50%;
+					}
+				}
+			}
+		}
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@include med-small-up {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: none;
+					border-bottom: 1px solid $border-color;
+
+
+					th {
+						background: #555;
+					}
+				}
+			}
+
+			tr {
+				border-bottom: 5px solid $medium-grey;
+
+				td {
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom-color: $border-color;
+					border-bottom-width: 1px;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-star,
+				td.thread-from,
+				td.thread-info,
+				td.thread-options {
+					border-top: 0;
+				}
+
+				td.thread-star {
+					vertical-align: middle;
+
+					.message-action-star {
+						line-height: 1.2;
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 3em;
+
+					@include small-only {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 2.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@include small-only {
+						clear: both;
+
+						@include font-size(11);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					background: none;
+					border-color: $border-color;
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		#message-threads.sitewide-notices {
+
+			td {
+				width: 100%;
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					margin-bottom: 0.4em;
+					padding-left: 0.2em;
+				}
+
+				a {
+					display: inline-block;
+				}
+			}
+
+			td:first-child {
+				display: none;
+			}
+
+			td:nth-child(2) {
+
+				strong {
+					margin: -8px -8px 8px;
+				}
+			}
+
+			td:first-child + td + td {
+				// notice date
+				border-bottom: 0;
+
+				span {
+					line-height: 1;
+				}
+			}
+
+			td:last-child {
+				// notice actions
+				border-bottom-color: darken($border-light, 20%);
+				line-height: 1;
+				text-align: right;
+
+				a:last-child:after {
+					content: attr(title);
+					display: block;
+					line-height: initial;
+					text-indent: 0;
+				}
+			}
+		}
+
+	} // #buddypress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		ul {
+			margin: 0;
+		}
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+
+		// Profile table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@include med-small-up {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #buddyPress
+
+} // close .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Attempt to reset form control widths
+#main {
+
+	#buddypress {
+
+		.standard-form {
+
+			li {
+				float: none;
+			}
+
+			input[type="text"],
+			input[type="email"],
+			input[type="password"],
+			textarea {
+				width: 100%;
+			}
+		}
+	}
+}
+
+#buddypress {
+
+	// Match BP form elements to Themes(2017) styling where styles
+	// are not inherited or being overriden.
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.5);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	// 2017 sets large height on selects lets try to manage that
+	select {
+		height: auto;
+	}
+
+	.standard-form {
+
+		// Keep BP form element borders to 1px rather than 2013 2px
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		button,
+		select,
+		textarea {
+			border-color: rgba($border-color, 0.5);
+			border-width: 1px;
+		}
+
+		select {
+			color: #737373;
+		}
+
+	} // close .standard-form
+
+	#signup_form.standard-form {
+
+		div.submit {
+			float: none;
+
+			input {
+				margin-right: 0;
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		// Stylise the seach form elements, in part this deals with padding
+		// issues on the submit & sizing issues between bp styles & 2017
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.6);
+			position: relative;
+
+			@include border-radius(2px);
+			overflow: hidden;
+
+			label {
+				float: left;
+				margin: 0;
+				width: 70%;
+			}
+
+			input[type="text"] {
+				float: left;
+				margin: 0;
+				width: 100%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+				border: 0;
+
+				@include font-size(14);
+				line-height: inherit;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.6);
+				font-weight: 400;
+				padding: 0.2em 0 0.2em 0.2em;
+			}
+
+			input[type="submit"] {
+
+				@include border-radius-none();
+				float: right;
+				font-weight: 400;
+				padding: 0.2em 1em;
+				text-align: center;
+				text-transform: none;
+				width: 30%;
+			}
+		} // close form
+
+	} // close .dir-search, .message-search
+
+	// Dirs & certain components hide search hides labels so can't width the
+	// label element adjust so that the input takes reduced width.
+
+	div.dir-search {
+		margin-top: 0;
+	}
+
+	.dir-search #search-members-form,
+	#search-groups-form,
+	#search-message-form {
+
+		input[type="text"] {
+			float: left;
+			margin: 0;
+			width: 70%;
+		}
+	}
+
+	// Shift the search parent to the right and allow to shrinkwrap
+
+	@include small-up {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+			float: right;
+			margin-bottom: 5px !important;
+		}
+
+	} // close @media
+
+
+	@include large-up {
+
+		.dir-search,
+		.message-search,
+		li.groups-members-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16) ;
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16) ;
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+// Adjustments for form elements colors, backgrounds, borders under
+// the dark scheme.
+body.colors-dark {
+
+	#page { // for weight
+
+		#buddypress {
+
+			.dir-search,
+			.groups-members-search,
+			.message-search {
+
+				form {
+					background: #333;
+					border-color: #555;
+
+					@include border-radius(2px);
+					padding: 1px;
+				}
+
+				input[type="text"] {
+					background: none;
+				}
+
+				input[type="submit"] {
+
+					@include border-radius(2px);
+				}
+			}
+		}
+
+		.message-search {
+			margin-top: 0;
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+
+// Adjust table font sizes, default too large proportionally
+// This approach will require refinement and perhaps re-location to
+// a more general typography section to manage BP elements grouped under
+// breakpoints. Provide top/bottom margins for tables, lacking in BP styles
+
+#buddypress {
+
+	table {
+
+		@include font-size(14);
+		margin: $spacing-val-md 0;
+
+		tr th {
+			background: $light-background;
+			border-color: $border-light;
+			color: $body-text;
+		}
+
+		// Reduce the themes inherited paragraph margins in tables
+		p {
+			margin-bottom: 0.5em;
+		}
+	}
+
+	@include medium-up {
+
+		table {
+
+			@include font-size(16);
+		}
+	}
+}
+
+/*__ User Account tables __*/
+
+#buddypress {
+
+	// Manage some table cells widths that are disproportionate to their content
+	.notifications,
+	.messages-notices {
+
+		th {
+			width: 30%;
+
+			&.bulk-select-all {
+				text-align: center;
+				width: 10%;
+			}
+		}
+
+		.bulk-select-check,
+		.thread-star {
+			text-align: center;
+		}
+
+		.notification-actions,
+		td.thread-options {
+			text-align: center;
+
+			a {
+				display: inline-block;
+				margin: 0;
+				padding: 0;
+			}
+		}
+
+		// Notices action buttons, this maybe better moved, temp for now to address
+		// styling issues - this will need styling ideas
+		td {
+
+			.button {
+				border: 0;
+				display: block;
+				padding: 0;
+				text-align: center;
+			}
+		}
+
+	} // .notifications, .message-notices
+
+	.notifications { // table element
+
+		.actions { // th element
+			text-align: center;
+		}
+	}
+
+} // #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+	.bp-avatar-status,
+	.bp-cover-image-status {
+
+		.warning {
+			background: #7dd4ff;
+			border: 1px solid #000;
+			color: $body-text;
+
+			@include font-size(16);
+		}
+	}
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning')
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
+
+#buddypress {
+
+	.acfb-holder {
+
+		li.friend-tab {
+			background: $notice-info;
+			border: inherit;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen-rtl.css	(revision 12265)
@@ -0,0 +1,1926 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentysixteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentysixteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+/**
+* Corrective measure to deal with 2016 / BP handling of no-js
+* classes - hide menu if BP still showing 'no-js'.
+*/
+@media screen and (max-width: 905px) {
+	html.js body.no-js .site-header-menu {
+		display: none;
+	}
+}
+
+.buddypress div.clear {
+	display: none;
+}
+
+.buddypress #page a {
+	box-shadow: none;
+	text-decoration: none !important;
+}
+
+.buddypress .entry-title {
+	text-align: center;
+}
+
+@media screen and (min-width: 61.5625em) {
+	.buddypress .entry-title {
+		text-align: right;
+	}
+}
+
+@media screen and (min-width: 44.375em) {
+	.buddypress #primary {
+		float: none;
+		margin: 0;
+		width: auto;
+	}
+	.buddypress #primary .entry-header {
+		margin: 0;
+	}
+	.buddypress #primary .entry-content,
+	.buddypress #primary .content-bottom-widgets {
+		margin-right: 7.6923%;
+		margin-left: 7.6923%;
+	}
+	.buddypress .sidebar {
+		float: none;
+		margin-right: 0;
+		padding: 0 7.6923%;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 61.5625em) {
+	.buddypress #primary {
+		float: right;
+		margin-left: -100%;
+		width: 70%;
+	}
+	.buddypress #primary .entry-content,
+	.buddypress #primary .content-bottom-widgets {
+		margin-right: 0;
+		margin-left: 0;
+	}
+	.buddypress .sidebar {
+		float: right;
+		margin-right: 75%;
+		padding: 0;
+		width: 25%;
+	}
+}
+
+.buddypress.no-sidebar #primary {
+	float: none;
+	margin: 0;
+	width: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul li.selected a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	background-color: #f7f5e7;
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a {
+	color: #0073c1;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #object-nav {
+		background: #f7f5e7;
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 10px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:focus:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:focus ul {
+		height: 320px;
+		opacity: 1;
+		overflow-y: auto;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f4f2df;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 0;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	margin-top: 0;
+	padding: 5px 0 5px 5px;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: left;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	body.no-sidebar #buddypress #item-header,
+	body.no-sidebar #buddypress #item-body {
+		background: #fff;
+	}
+	body.no-sidebar #buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	body.no-sidebar #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.no-sidebar #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body.no-sidebar #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	body.no-sidebar #buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 20px 0 0;
+		width: auto;
+	}
+	body.no-sidebar #buddypress #item-body #subnav {
+		margin: 0 -20px 0 0;
+	}
+	body.no-sidebar #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: right;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #141412;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+#buddypress .activity-meta .button:focus, #buddypress .activity-meta .button:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress .action .generic-button a:focus, #buddypress .action .generic-button a:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 44.375em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	overflow: hidden;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress ul.item-list li .item {
+		margin-right: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-right: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 18px;
+	font-size: 1.125rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 44.375em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.75rem;
+	padding: 10px 0;
+	text-align: right;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 5%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 22px;
+		font-size: 1.375rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin-bottom: -20px;
+	margin-right: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 10px 10px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: right;
+		margin: 10px 0 20px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	min-height: 1.5em;
+	padding-right: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: left;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header a {
+	color: #0075c4;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 10px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	background-color: #f7f7f7;
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	color: #727272;
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin-bottom: 5px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.25rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	border-right: 1px solid #eaeaea;
+	margin-top: 5px;
+}
+
+#buddypress #activity-stream .activity-comments ul {
+	background: rgba(247, 247, 247, 0.6);
+	color: #737373;
+	margin: 15px 2px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments ul a {
+	color: #0077c7;
+}
+
+#buddypress #activity-stream .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(20, 20, 18, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 61.5625em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: left;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.875rem;
+	text-align: right;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		padding-right: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-right: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.875rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: left;
+		width: 28%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		margin-left: 5%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.875rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(20, 20, 18, 0.6);
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: right;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0 5px;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+.bp-user #buddypress table a {
+	color: #0074c2;
+}
+
+@media screen and (min-width: 61.5625em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 75em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.125rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.bp-user #buddypress .notifications-options-nav {
+		width: 300px;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select {
+	float: right;
+	margin-left: 0;
+	width: 60%;
+}
+
+.bp-user #buddypress .notifications-options-nav input {
+	float: left;
+	font-family: inherit;
+	line-height: 20px;
+	width: 40%;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile {
+	/* Edit profile */
+}
+
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		right: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #ccc;
+	border-bottom-width: 2px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.6875rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: #dce5ff;
+	border-color: #d4d0ba;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-right: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: #b7b7b7;
+	line-height: 1;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress .standard-form select {
+	color: #737373;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-left: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: right;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 0.875rem;
+	border: 0;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(212, 208, 186, 0.6);
+	padding: 0.2em 0.2em 0.2em 0;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 75em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
+#buddypress .acfb-holder li.friend-tab {
+	background: #9fd1e2;
+	border: inherit;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen.css	(revision 12265)
@@ -0,0 +1,1926 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentysixteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentysixteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+/**
+* Corrective measure to deal with 2016 / BP handling of no-js
+* classes - hide menu if BP still showing 'no-js'.
+*/
+@media screen and (max-width: 905px) {
+	html.js body.no-js .site-header-menu {
+		display: none;
+	}
+}
+
+.buddypress div.clear {
+	display: none;
+}
+
+.buddypress #page a {
+	box-shadow: none;
+	text-decoration: none !important;
+}
+
+.buddypress .entry-title {
+	text-align: center;
+}
+
+@media screen and (min-width: 61.5625em) {
+	.buddypress .entry-title {
+		text-align: left;
+	}
+}
+
+@media screen and (min-width: 44.375em) {
+	.buddypress #primary {
+		float: none;
+		margin: 0;
+		width: auto;
+	}
+	.buddypress #primary .entry-header {
+		margin: 0;
+	}
+	.buddypress #primary .entry-content,
+	.buddypress #primary .content-bottom-widgets {
+		margin-left: 7.6923%;
+		margin-right: 7.6923%;
+	}
+	.buddypress .sidebar {
+		float: none;
+		margin-left: 0;
+		padding: 0 7.6923%;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 61.5625em) {
+	.buddypress #primary {
+		float: left;
+		margin-right: -100%;
+		width: 70%;
+	}
+	.buddypress #primary .entry-content,
+	.buddypress #primary .content-bottom-widgets {
+		margin-left: 0;
+		margin-right: 0;
+	}
+	.buddypress .sidebar {
+		float: left;
+		margin-left: 75%;
+		padding: 0;
+		width: 25%;
+	}
+}
+
+.buddypress.no-sidebar #primary {
+	float: none;
+	margin: 0;
+	width: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul li.selected a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	background-color: #f7f5e7;
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a {
+	color: #0073c1;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #object-nav {
+		background: #f7f5e7;
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 10px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:focus:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:focus ul {
+		height: 320px;
+		opacity: 1;
+		overflow-y: auto;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f4f2df;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 0;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	margin-top: 0;
+	padding: 5px 5px 5px 0;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: right;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	body.no-sidebar #buddypress #item-header,
+	body.no-sidebar #buddypress #item-body {
+		background: #fff;
+	}
+	body.no-sidebar #buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	body.no-sidebar #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.no-sidebar #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body.no-sidebar #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	body.no-sidebar #buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 20px;
+		width: auto;
+	}
+	body.no-sidebar #buddypress #item-body #subnav {
+		margin: 0 0 0 -20px;
+	}
+	body.no-sidebar #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: left;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #141412;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+#buddypress .activity-meta .button:focus, #buddypress .activity-meta .button:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress .action .generic-button a:focus, #buddypress .action .generic-button a:hover {
+	background: inherit;
+	color: #000;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 44.375em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	overflow: hidden;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress ul.item-list li .item {
+		margin-left: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-left: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 18px;
+	font-size: 1.125rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 44.375em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.75rem;
+	padding: 10px 0;
+	text-align: left;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 5%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 22px;
+		font-size: 1.375rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin-bottom: -20px;
+	margin-left: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 10px 10px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: left;
+		margin: 10px 0 20px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	min-height: 1.5em;
+	padding-left: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: right;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header a {
+	color: #0075c4;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 10px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	background-color: #f7f7f7;
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	color: #727272;
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin-bottom: 5px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.25rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	border-left: 1px solid #eaeaea;
+	margin-top: 5px;
+}
+
+#buddypress #activity-stream .activity-comments ul {
+	background: rgba(247, 247, 247, 0.6);
+	color: #737373;
+	margin: 15px 0 0 2px;
+}
+
+#buddypress #activity-stream .activity-comments ul a {
+	color: #0077c7;
+}
+
+#buddypress #activity-stream .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(20, 20, 18, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 61.5625em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: right;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.875rem;
+	text-align: left;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		padding-left: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-left: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.875rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: right;
+		width: 28%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+		width: 20%;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		margin-right: 5%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.875rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(20, 20, 18, 0.6);
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: left;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 5px 0 0;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.75rem;
+}
+
+.bp-user #buddypress table a {
+	color: #0074c2;
+}
+
+@media screen and (min-width: 61.5625em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 75em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.125rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.bp-user #buddypress .notifications-options-nav {
+		width: 300px;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select {
+	float: left;
+	margin-right: 0;
+	width: 60%;
+}
+
+.bp-user #buddypress .notifications-options-nav input {
+	float: right;
+	font-family: inherit;
+	line-height: 20px;
+	width: 40%;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile {
+	/* Edit profile */
+}
+
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		left: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #ccc;
+	border-bottom-width: 2px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.6875rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: #dce5ff;
+	border-color: #d4d0ba;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-left: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	border-bottom-color: #b7b7b7;
+	line-height: 1;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 10px;
+}
+
+.bp-user .ac_results ul {
+	margin: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress .standard-form select {
+	color: #737373;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-right: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: left;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 0.875rem;
+	border: 0;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(212, 208, 186, 0.6);
+	padding: 0.2em 0 0.2em 0.2em;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 75em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
+#buddypress .acfb-holder li.friend-tab {
+	background: #9fd1e2;
+	border: inherit;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentysixteen.scss	(revision 12265)
@@ -0,0 +1,2695 @@
+
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility .
+
+// 2016 Media Queries / Breakpoints.
+// @media screen and (min-width: 44.375em)
+// @media screen and (min-width: 61.5625em)
+// @media screen and (min-width: 75em)
+
+
+// In addition BP will use the em breakpoints as  referenced below to provide
+// a series of finer breakpoints, these are be used sparingly though.
+// @media screen and (max-width: 22em) // Additional for xtra small mobile only
+// @media screen and (max-width: 30em) // Additional small only
+// @media screen and (min-width: 38.75em)
+// @media screen and (min-width: 46.25em)
+// @media screen and (min-width: 55em)
+// @media screen and (min-width: 87.6875em)
+
+// Mixins and Variables
+
+// simplify remembering/writing media queries
+// written as:
+// @include medium-up {
+//  body {property: value;}
+// }
+//
+
+@mixin small-up {
+
+	@media screen and (min-width: 44.375em) {
+
+		@content;
+	}
+}
+
+@mixin medium-up {
+
+	@media screen and (min-width: 61.5625em) {
+
+		@content;
+	}
+}
+
+@mixin large-up {
+
+	@media screen and (min-width: 75em) {
+
+		@content;
+	}
+}
+
+
+// Common spacing values
+$spacing-val-lg: 40px;
+$spacing-val-md: 20px;
+$spacing-val-sm: 10px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing
+// 2016 uses a basic pixel font size approach, we'll stick with rem/px
+@mixin font-size($font-size: 16) {
+	$rem-font-value: ($font-size / 16);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($box-model) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2016 font family
+@mixin default-font-sans() {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+// Variables: color definitions
+$body-text:          #141412;
+$content-background: #fff;
+$off-white:          lighten(#f9f8ee, 0.7);
+$cream-background:   #f7f5e7;
+$light-background:   #f7f7f7;
+$medium-background:  #ccc;
+$dark-background:    #555;
+$border-color:       #d4d0ba;    // border color is varied using rgba
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$background-hover:   #db572f; // 2016 link background hover
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #9fd1e2;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             #dce5ff;
+$link-action:        #c82b2b;
+
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentysixteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentysixteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+
+// A conflict between BP's body.no-js being removed only
+// when the DOM is fully loaded & Twentysixteens hardcoded
+// class on the html el & scripting in head at start of DOM
+// causes mobile menu to stay opened too long.
+// This ruleset hides menu if html.js (2016 JS has kicked in) but BP still
+// rendering body.no-js before it's own JS removal kicks in.
+// See trac ticket https://buddypress.trac.wordpress.org/ticket/7314
+
+/**
+* Corrective measure to deal with 2016 / BP handling of no-js
+* classes - hide menu if BP still showing 'no-js'.
+*/
+
+@media screen and (max-width: 905px) {
+
+	html.js {
+
+		body.no-js {
+
+			.site-header-menu {
+				display: none;
+			}
+		}
+	}
+}
+
+.buddypress {
+	// Remove any empty clearing elements, CSS provides clearing.
+	div.clear {
+		display: none;
+	}
+
+	// Ensure no text decoration or box shadow causing underlined effect.
+	#page a {
+		box-shadow: none;
+		text-decoration: none !important;
+	}
+
+	.entry-title {
+		text-align: center;
+
+		@include medium-up {
+			text-align: left;
+		}
+	}
+
+	// The breakpoint for main & sidebar could cut in earlier, elements
+	// wrap to blocks/columns but margins & padding still in effect
+	// for sidebar style.css L:2781.
+
+	@include small-up {
+
+		#primary {
+			float: none;
+			margin: 0;
+			width: auto;
+
+			.entry-header {
+				margin: 0;
+			}
+
+			.entry-content,
+			.content-bottom-widgets {
+				margin-left: 7.6923%;
+				margin-right: 7.6923%;
+			}
+		}
+
+		.sidebar {
+			float: none;
+			margin-left: 0;
+			padding: 0 7.6923%;
+			width: auto;
+		}
+	}
+
+	@include medium-up {
+
+		#primary {
+			float: left;
+			margin-right: -100%;
+			width: 70%;
+
+			.entry-content,
+			.content-bottom-widgets {
+				margin-left: 0;
+				margin-right: 0;
+			}
+		}
+
+		.sidebar {
+			float: left;
+			margin-left: 75%;
+			padding: 0;
+			width: 25%;
+		}
+
+	}
+
+} // .buddypress (body class)
+
+// 2016 defines the class 'no-sidebar' if the primary sidebar is empty/inactive
+// in terms of widgets; if this is in effect on a site then adjust the BP
+// screens for full width.
+
+.buddypress.no-sidebar {
+
+	#primary {
+		float: none;
+		margin: 0;
+		width: auto;
+	}
+
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets will appear over weighted in selectors
+
+#buddypress {
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected {
+
+				a {
+					background: inherit;
+					opacity: 1;
+				}
+			}
+		}
+	}
+	// global nav styles
+	div.item-list-tabs {
+
+		ul {
+			background-color: $cream-background;
+			border-bottom: 1px solid rgba($border-light, 0.9);
+			border-top: 1px solid rgba($border-light, 0.9);
+			overflow: hidden;
+			padding: 0;
+
+			li {
+
+				a {
+					color: #0073c1;
+
+					span {
+						border-radius: 25%;
+					}
+				}
+			}
+		}
+	} // close item-list-tabs
+
+} // close #buddypress
+
+// .bp-user #object-nav @ small screen as a dropdown
+
+.bp-user {
+
+	#buddypress {
+
+		@media screen and (max-width: 38.75em) {
+
+			#object-nav {
+				background: $cream-background;
+				border: 1px solid $border-light;
+				overflow: visible;
+				padding: $spacing-val-sm;
+
+				ul {
+					border: 0;
+					height: 0;
+					// all latest vendor releases handle transitions, so no prefixes used.
+					transition: height 0.3s ease-in-out 0.7s;
+					visibility: hidden;
+				}
+
+				&:before {
+					content: "Menu \021E9";
+					display: inline;
+					opacity: 0.7;
+				}
+
+				&:hover,
+				&:focus {
+
+					&:before {
+						content: "Menu \021E7";
+					}
+
+					ul {
+						height: 320px;
+						opacity: 1;
+						overflow-y: auto;
+						visibility: visible;
+					}
+				}
+			}
+
+			#subnav {
+				clear: both;
+			}
+
+		} // close @media
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress {
+
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+				float: none;
+			}
+
+			@media screen and (max-width: 38.75em) {
+
+				li:not(:last-child) {
+					border-bottom: 1px solid $border-light;
+				}
+			}
+
+			li:not(.selected) {
+
+				@media screen and (max-width: 38.75em) {
+
+					&:focus,
+					&:hover {
+						background: darken($cream-background, 2%);
+					}
+				}
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				li {
+					float: left;
+				}
+			}
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			background-color: $light-background;
+			border-bottom: 0;
+			margin-top: $spacing-val-xs;
+			padding: 0;
+
+			li.last {
+				background: $content-background;
+				margin-top: 0;
+				padding: $spacing-val-xs $spacing-val-xs $spacing-val-xs 0;
+				width: 100%;
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+				}
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				li.last {
+					text-align: right;
+				}
+			}
+		} // close ul
+	} // close #subnav
+
+} // close #buddypress
+
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+// 2016 right sidebar is displayed if the widget sidebar has widgets;
+// we'll only display object nav as a vert menu if sidebar not present.
+
+@media screen and (min-width: 55em) {
+
+	body.no-sidebar {
+
+		#buddypress {
+
+			#item-header,
+			#item-body {
+				background: $content-background;
+			}
+
+			#object-nav {
+				border-right: 1px solid $border-med;
+				float: left;
+				margin-right: -1px;
+				width: 200px;
+
+				ul {
+					background: none;
+					border-bottom: 0;
+					padding: 0;
+
+					li {
+						float: none;
+						overflow: hidden;
+
+						span {
+							background: $content-background;
+							border-radius: 10%;
+							float: right;
+							margin-right: 2px;
+						}
+					}
+				}
+			}
+
+			#item-body {
+				border-left: 1px solid $border-med;
+				overflow: hidden;
+				padding: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				#subnav {
+					margin: 0 0 0 (-$spacing-val-md);
+
+					ul {
+						margin-top: 0;
+					}
+				}
+			}
+
+		} // close  #buddypress
+	} // close body:not(.sidebar)
+} // close @media
+
+
+// Tabbed Nav styling
+// Primarilly used for group create screens & ? directory nav.
+
+@media screen and (min-width: 46.25em) {
+
+	#main {
+
+		#buddypress {
+
+			#group-create-tabs.item-list-tabs {
+
+				ul:before,
+				ul:after {
+					content: " ";
+					display: table;
+				}
+
+				ul:after {
+					clear: both;
+				}
+
+				ul {
+					background: none;
+					border: 0;
+					border-bottom: 1px solid $border-med;
+					overflow: visible;
+					padding-bottom: 0;
+
+					li {
+						float: left;
+						width: auto;
+					}
+
+					li.selected,
+					li.current {
+						border: 1px solid $border-med;
+						border-bottom-color: $content-background;
+
+						@include border-top-radius(4px);
+						margin-bottom: -1px;
+
+						a {
+							background: none;
+							color: $body-text;
+							outline: none;
+						}
+					}
+				}
+
+			} // close .item-list-tabs
+
+			#subnav {
+
+				ul {
+					border-bottom: 0;
+				}
+
+			} // close #subnav
+		} // close #buddypress
+	}
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+		min-height: 0;
+
+		&:before,
+		&:after {
+			height: 0;
+			width: 0;
+		}
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Adjusts Meta items and various elements to match 2016 font-family
+#buddypress {
+
+	.item-list {
+
+		.activity-header,
+		.activity-meta {
+
+			@include default-font-sans();
+		}
+	}
+}
+
+#buddypress {
+
+	.activity-meta {
+
+		.button {
+
+			&:focus,
+			&:hover {
+				background: inherit;
+				color: #000;
+			}
+		}
+	}
+
+	.action {
+
+		.generic-button {
+
+			a {
+
+				&:focus,
+				&:hover {
+					background: inherit;
+					color: #000;
+				}
+			}
+		}
+	}
+} // close #buddypress
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2016 font-family
+
+#buddypress {
+
+	ul.item-list {
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@media screen and (min-width: 38.75em) {
+					margin-bottom: 0;
+				}
+
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+					margin-bottom: $spacing-val-sm;
+
+					// Quite early on we need to move avatars to the left(right)
+					@include small-up {
+						display: block;
+						float: left;
+					}
+				}
+			}
+
+			.item {
+				overflow: hidden;
+
+				@media screen and (min-width: 46.25em) {
+					margin-left: 15%;
+				}
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin-left: 0;
+					width: 94%;
+				}
+
+				.item-title {
+
+					@include font-size(18);
+					line-height: 1.2;
+					text-align: center;
+
+					@include small-up {
+						text-align: left;
+					}
+
+					width: 100%;
+
+					.update {
+						display: block;
+
+						@include font-size(12);
+
+						@include medium-up {
+
+							@include font-size(14);
+						}
+
+						padding: $spacing-val-sm 0;
+						text-align: left;
+					}
+				}
+
+			}
+
+			@include medium-up {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 5%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+
+					.item-title {
+
+						@include font-size(22);
+					}
+				}
+
+			}// close medium-up
+
+			div.action {
+				clear: left;
+				float: none;
+				margin-bottom: -$spacing-val-md;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-md 0 $spacing-val-xs;
+				// push the actions to the right
+
+				@include medium-up {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-sm 0;
+					width: 100%;
+
+					a {
+						display: block;
+						width: 100%;
+					}
+					// Provisionally lets keep the 'action' div
+					//position but inline-block the buttons
+
+					@media screen and (min-width: 38.75em) {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm $spacing-val-sm 0;
+						width: auto;
+					} // close @media
+
+					@include medium-up {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		@media screen and (max-width: 46.25em) {
+
+			#whats-new-content {
+				clear: left;
+				margin: $spacing-val-sm 0 $spacing-val-md;
+				padding: $spacing-val-sm 0 0;
+			}
+		}
+
+		#whats-new-content.active {
+
+			#whats-new-options[style] {
+
+				#whats-new-post-in-box {
+					border: 1px solid rgba($border-color, 0.5);
+					float: left;
+					line-height: 1.5;
+					margin-top: 12px;
+					padding-left: 0.2em;
+					width: 100%;
+
+					select {
+						background: none;
+						border: 0;
+						float: right;
+						margin: 0;
+						min-height: 1.5em;
+						padding-left: 0.4em;
+					}
+				}
+
+				@media screen and (min-width: 30em) {
+
+					#whats-new-post-in-box {
+						width: auto;
+					}
+
+					#whats-new-submit {
+						float: right;
+					}
+				}
+
+			} // close #whats-new-options
+		} // close #whats-new-content.active
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content {
+				margin-left: 0;
+
+				.activity-header {
+
+					@include font-size(14);
+
+					a {
+						color: #0075c4;
+					}
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+			}
+		}
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				line-height: inherit;
+				margin-right: 0;
+
+				p {
+					background-color: $light-background;
+					border: 1px solid rgba($border-light, 0.6);
+					color: #727272;
+					margin-top: 0;
+					padding: 0 0.2em;
+				}
+
+				img.avatar {
+					display: inline-block;
+					margin-bottom: 0 !important; // have to match to BP
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+					margin-bottom: $spacing-val-xs;
+
+					@media screen and (min-width: 38.75em) {
+						display: inline-block;
+						margin-bottom: 0;
+					}
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+
+				a {
+					font-style: italic;
+				}
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			border-left: 1px solid $border-light;
+			margin-top: 5px;
+
+			ul {
+				background: rgba($light-background, 0.6);
+				color: #737373;
+				margin: 15px 0 0 2px;
+
+				a {
+					color: #0077c7;
+				}
+			}
+
+			.acomment-meta {
+				color: #737373;
+			}
+
+			.ac-form {
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+	} // close .activity
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@include medium-up {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#signup_form.standard-form {
+
+		#profile-details-section,
+		#basic-details-section,
+		#blog-details-section {
+			float: none;
+			width: 100%;
+
+			@media screen and (min-width: 38.75em) {
+				width: 48%;
+			}
+		}
+
+		@media screen and (min-width: 38.75em) {
+
+			#profile-details-section {
+				float: right;
+			}
+
+			#basic-details-section {
+				float: left;
+			}
+		}
+
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+.bp-user,
+.single-item.groups {
+
+	#buddypress {
+
+		#item-header-content {
+
+			#item-meta {
+
+				@include font-size(14);
+				text-align: left;
+
+				p {
+					margin-bottom: 0.5em;
+				}
+			}
+		}
+	}
+
+	@media screen and (max-width: 46.25em) {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	@media screen and (max-width: 38.75em) {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+
+		@media screen and (max-width: 38.75em) {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@media screen and (max-width: 38.75em) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+
+					h2 {
+						border-bottom: 1px solid $border-light;
+						text-align: center;
+					}
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			div#item-actions {
+				margin: 0;
+				width: 100%;
+
+				@media screen and (min-width: 38.75em) {
+					border-left: 1px solid $border-light;
+					clear: none;
+					float: right;
+					padding-left: 0.2em;
+					width: 30%;
+				} // close @media
+
+				@media screen and (min-width: 46.25em) {
+					width: 40%;
+				}
+
+				ul {
+					margin-top: 0;
+					padding-left: 0;
+				}
+
+				h2 {
+
+					@include font-size(14);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(16);
+					}
+
+					padding: 0.2em;
+				}
+			} // close item-actions
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-avatar {
+					width: 21%;
+				}
+
+				#item-header-content {
+					margin-left: 4%;
+					width: 40%;
+				}
+
+				div#item-actions {
+					float: right;
+					width: 28%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+	} // close #buddypress
+} // close .groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+			padding: $spacing-val-md 0;
+
+			#item-header-avatar {
+				text-align: center;
+				width: 100%;
+
+				img.avatar,
+				a {
+					border-bottom: 0;
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar {
+					float: left;
+					width: 20%;
+
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: right;
+					margin-right: 5%;
+					width: 69%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+		textarea {
+			height: auto;
+			min-height: 100px;
+			overflow: auto;
+		}
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form {
+
+		div.radio label {
+			border: 1px solid $border-light;
+			padding: 0.2em;
+
+			ul {
+				color: rgba($body-text, 0.6);
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+					float: left;
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+
+					a {
+						display: inline-block;
+						margin: $spacing-val-xs 0;
+						width: 100%;
+
+						@media screen and (min-width: 38.75em) {
+							width: auto;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+
+			@media screen and (max-width: 38.75em) {
+				background: $content-background;
+				padding: $spacing-val-md 0;
+			}
+
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+
+				@media screen and (max-width: 38.75em) {
+					margin: 0;
+					width: 100%;
+
+					label {
+
+						input[type="text"] {
+							width: 100%;
+						}
+					}
+				}
+
+				margin: $spacing-val-xs $spacing-val-xs 0 0;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	.entry-title {
+		margin-bottom: 0.5em;
+	}
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(14);
+			}
+
+			td {
+
+				@include font-size(12);
+			}
+
+			a {
+				color: #0074c2;
+			}
+
+			@include medium-up {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+
+			@include large-up {
+
+				th {
+
+					@include font-size(18);
+				}
+
+				td {
+
+					@include font-size(16);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav {
+			border: 1px solid rgba($border-color, 0.5);
+			float: left;
+			width: 100%;
+
+			@media screen and (min-width: 38.75em) {
+				width: 300px;
+			}
+
+			select,
+			input {
+				border: 0;
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 0;
+				width: 60%;
+			}
+
+			input {
+				float: right;
+				font-family: inherit;
+				line-height: 20px;
+				width: 40%;
+			}
+
+		} // close .notifications-options-nav
+
+	} //close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			.bp-widget {
+
+				h2 {
+					background: lighten($dark-background, 10%);
+					color: #fff;
+					margin-bottom: 0;
+					padding: 0.4em;
+				}
+
+				table {
+					margin-top: 0;
+				}
+			}
+
+			/* Edit profile */
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: $dark-background;
+					color: #fff;
+					padding: 0.2em 0.5em;
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#groups-list {
+
+			li {
+
+				.item {
+
+					@media screen and (min-width: 77.5em) {
+						left: 5%;
+						width: 50%;
+					}
+				}
+			}
+		}
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: lighten($dark-background, 10%);
+				}
+			}
+
+			tr {
+
+				td {
+					background: $content-background;
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom-color: $medium-background;
+					border-bottom-width: 2px;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+					}
+				}
+
+				td.thread-star {
+					vertical-align: middle;
+
+					.message-action-star {
+						line-height: 1.2;
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 3em;
+
+					@media screen and (max-width: 38.75em) {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 2.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@media screen and (max-width: 38.75em) {
+						clear: both;
+
+						@include font-size(11);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					background: $unread;
+					border-color: $border-color;
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		#message-threads.sitewide-notices {
+
+			td {
+				width: 100%;
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					margin-bottom: 0.4em;
+					padding-left: 0.2em;
+				}
+
+				a {
+					display: inline-block;
+				}
+			}
+
+			td:first-child {
+				display: none;
+			}
+
+			td:nth-child(2) {
+
+				strong {
+					margin: -8px -8px 8px;
+				}
+			}
+
+			td:first-child + td + td {
+				// notice date
+				border-bottom: 0;
+
+				span {
+					line-height: 1;
+				}
+			}
+
+			td:last-child {
+				// notice actions
+				border-bottom-color: darken($border-light, 20%);
+				line-height: 1;
+				text-align: right;
+
+				a:last-child:after {
+					content: attr(title);
+					display: block;
+					line-height: initial;
+					text-indent: 0;
+				}
+			}
+		}
+
+	} // #buddypress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		ul {
+			margin: 0;
+		}
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+		// Profile table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #buddyPress
+
+} // close .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Attempt to reset form control widths
+#main {
+
+	#buddypress {
+
+		.standard-form {
+
+			li {
+				float: none;
+			}
+
+			input[type="text"],
+			input[type="email"],
+			input[type="password"],
+			textarea {
+				width: 100%;
+			}
+		}
+	}
+}
+
+
+#buddypress {
+
+	// Match BP form elements to Themes(2016) styling where styles
+	// are not inherited or being overriden.
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.5);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	.standard-form {
+
+		// Keep BP form element borders to 1px rather than 2013 2px
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		button,
+		select,
+		textarea {
+			border-color: rgba($border-color, 0.5);
+			border-width: 1px;
+		}
+
+		select {
+			color: #737373;
+		}
+
+	} // close .standard-form
+
+	#signup_form.standard-form {
+
+		div.submit {
+			float: none;
+
+			input {
+				margin-right: 0;
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		// Stylise the seach form elements, in part this deals with padding
+		// issues on the submit & sizing issues between bp styles & 2016
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.6);
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 70%;
+			}
+
+			input[type="text"] {
+				float: left;
+				margin: 0;
+				width: 100%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+
+				@include font-size(14);
+				border: 0;
+				line-height: inherit;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.6);
+				padding: 0.2em 0 0.2em 0.2em;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0.2em 1em;
+				text-align: center;
+				text-transform: none;
+				width: 30%;
+			}
+		} // close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+
+	@media screen and (min-width: 38.75em) {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+			float: right;
+			margin-bottom: 5px !important;
+
+			form {
+
+				label,
+				input[type="text"],
+				input[type="submit"] {
+					width: auto;
+				}
+			}
+		}
+
+	} // close @media
+
+
+	@include large-up {
+
+		.dir-search,
+		.message-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16);
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16);
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+
+// Adjust table font sizes, default too large proportionally
+// This approach will require refinement and perhaps re-location to
+// a more general typography section to manage BP elements grouped under
+// breakpoints. Provide top/bottom margins for tables, lacking in BP styles
+#buddypress {
+
+	table {
+
+		@include font-size(14);
+		margin: $spacing-val-md 0;
+
+		tr th {
+			background: lighten($dark-background, 10%);
+			border-color: darken($border-light, 20%);
+			color: $content-background;
+		}
+
+		// Reduce the themes inherited paragraph margins in tables
+		p {
+			margin-bottom: 0.5em;
+		}
+	}
+
+	@media screen and (min-width: 55em) {
+
+		table {
+
+			@include font-size(16);
+		}
+	}
+}
+
+/*__ User Account tables __*/
+
+#buddypress {
+
+	// Manage some table cells widths that are disproportionate to their content
+	.notifications,
+	.messages-notices {
+
+		th {
+			width: 30%;
+
+			&.bulk-select-all {
+				text-align: center;
+				width: 10%;
+			}
+		}
+
+		.bulk-select-check,
+		.thread-star {
+			text-align: center;
+		}
+
+		.notification-actions,
+		td.thread-options {
+			text-align: center;
+
+			a {
+				display: inline-block;
+				margin: 0;
+				padding: 0;
+			}
+		}
+
+		// Notices action buttons, this maybe better moved, temp for now to address
+		// styling issues - this will need styling ideas
+		td {
+
+			.button {
+				border: 0;
+				display: block;
+				padding: 0;
+				text-align: center;
+			}
+		}
+
+	} // .notifications, .message-notices
+
+} // #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning' )
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
+
+#buddypress {
+
+	.acfb-holder {
+
+		li.friend-tab {
+			background: $notice-info;
+			border: inherit;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten-rtl.css	(revision 12265)
@@ -0,0 +1,1949 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyten theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyten this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+body.page-template-onecolumn-page #main #container {
+	margin: 0;
+}
+
+body.page-template-onecolumn-page #content {
+	margin: 0 10%;
+	width: auto;
+}
+
+body.page-template-onecolumn-page #content .entry-header,
+body.page-template-onecolumn-page #content .entry-content {
+	width: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+/* Here we start things off */
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a:focus, #buddypress div.item-list-tabs ul li a:hover {
+	color: #555;
+}
+
+#buddypress div.item-list-tabs ul li a:visited {
+	color: #21759b;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	padding: 0 4px !important;
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 1px solid #d4d0ba;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	float: left;
+	margin-top: 0;
+	padding: 5px 0 5px;
+	text-align: left;
+	width: 230px;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select option {
+	font-style: normal;
+}
+
+.bp-user #buddypress div#subnav.item-list-tabs li.last {
+	margin-right: 0;
+	text-align: left;
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 800px) {
+	body.bp-user #buddypress #item-header,
+	body.bp-user #buddypress #item-body {
+		background: #fff;
+	}
+	body.bp-user #buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	body.bp-user #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.bp-user #buddypress #object-nav ul li {
+		float: none;
+		margin-bottom: 5px;
+	}
+	body.bp-user #buddypress #object-nav ul li a {
+		overflow: hidden;
+	}
+	body.bp-user #buddypress #object-nav ul li.selected {
+		background: #ddd;
+	}
+	body.bp-user #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	body.bp-user #buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 25px 0 0;
+		width: auto;
+	}
+	body.bp-user #buddypress #item-body #subnav {
+		margin: 0 -25px 0 0;
+	}
+	body.bp-user #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media only screen and (min-device-width: 375px) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: right;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #373737;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress ul.item-list {
+	border-top: 0;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 15px;
+	text-align: center;
+}
+
+@media only screen and (min-device-width: 375px) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 15px;
+}
+
+@media only screen and (min-device-width: 450px) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+		opacity: 1;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-right: 0;
+	overflow: hidden;
+}
+
+#buddypress ul.item-list li .item .item-meta {
+	text-align: center;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item .item-meta {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-right: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-device-width: 450px) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title > a {
+	text-decoration: none;
+}
+
+#buddypress ul.item-list li .item .item-title > a:visited {
+	color: #21759b;
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.8rem;
+	padding: 15px 0;
+	text-align: right;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 5%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 18px;
+		font-size: 1.2rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin: 0;
+	margin-right: 0;
+	padding: 15px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+}
+
+@media only screen and (min-device-width: 375px) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 5px 15px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 15px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.6;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	max-width: 220px;
+	min-height: 1.5em;
+	padding: 0 0.4em 0 0;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		font-size: 12px;
+		font-size: 0.8rem;
+		float: left;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit input {
+		padding: 0 6px;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 50px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li.load-newest a {
+	display: block;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 25px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content,
+#buddypress #activity-stream li .activity-comments {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li .activity-header a:visited {
+	color: #21759b;
+}
+
+#buddypress #activity-stream li .activity-inner img {
+	height: auto;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 15px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress #activity-stream li .activity-comments {
+		margin-right: 70px;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-meta a {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin-bottom: 5px;
+	padding: 0.2em 0.5em;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		float: right;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 15px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	margin-bottom: 50px;
+}
+
+@media screen and (min-width: 650px) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.3333333333rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream .activity-comments {
+	margin-right: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	position: relative;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 2px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li:hover * {
+	color: #555;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li .acomment-meta,
+#buddypress #activity-stream .activity-comments > ul > li .acomment-content {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	border: 1px solid #d4d0ba;
+	box-sizing: border-box;
+	margin: 15px 0;
+	width: 100%;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(55, 55, 55, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.8rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 650px) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: left;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress a:visited {
+	color: #21759b;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	text-align: right;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+	.bp-user #buddypress #item-header-content .user-nicename,
+	.single-item.groups #buddypress #item-header-content .user-nicename {
+		color: #555;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 25px;
+	}
+}
+
+@media screen and (max-width: 50em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 50px;
+}
+
+.single-item.groups #buddypress div#item-header #item-header-content {
+	box-sizing: border-box;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0 !important;
+	width: 100%;
+}
+
+@media screen and (min-width: 50em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		padding-right: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-right: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	font-weight: 700;
+	line-height: 1.4;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions #group-admins li,
+.single-item.groups #buddypress div#item-header div#item-actions #group-mods li {
+	margin: 0;
+}
+
+@media screen and (min-width: 650px) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 2%;
+		padding: 0 0.5em;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: left;
+		width: 28%;
+	}
+}
+
+@media screen and (min-width: 64em) {
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+	margin-top: 225px !important;
+}
+
+@media screen and (min-width: 50em) and (max-width: 60em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		max-width: 60% !important;
+		width: 60% !important;
+	}
+}
+
+@media screen and (max-width: 64em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+		border: 0;
+		clear: both;
+		margin-top: 0 !important;
+		max-width: 100% !important;
+		padding-top: 0 !important;
+		width: auto;
+	}
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-cover-image .user-nicename {
+	color: inherit;
+	text-shadow: none;
+}
+
+@media screen and (min-width: 800px) {
+	.bp-user #buddypress #item-header #item-header-cover-image .user-nicename {
+		color: #fff;
+		text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
+	}
+}
+
+.bp-user #buddypress #item-header #item-header-content #item-buttons .generic-button {
+	margin-left: 5px;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		margin-left: 0%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	font-weight: 700;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(55, 55, 55, 0.6);
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 25px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li img,
+	.groups.manage-members #group-settings-form .item-list li h5 {
+		float: right;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 15px;
+	width: 100%;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: block;
+	margin: 5px 0;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		display: inline-block;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0 0;
+}
+
+.groups.group-members #subnav li #search-members-form label input[type="text"] {
+	box-shadow: none;
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 13px;
+	font-size: 0.8666666667rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 60%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: right;
+	margin-left: 0;
+	width: 49%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: right;
+	font-family: inherit;
+	line-height: 20px;
+	margin-right: 1%;
+	width: 50%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus, .bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.2rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle {
+	margin: 5px 0 25px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #6f6e6e;
+	border-radius: 2px;
+	background-clip: padding-box;
+	color: #eee;
+	font-weight: 700;
+	padding: 0.1em 0.5em;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:focus, .bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:hover,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:focus,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:hover {
+	color: #fff;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread #message-recipients .highlight {
+	font-size: 100%;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 15px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	box-sizing: border-box;
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom: 1px solid #ccc;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star,
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-unstar {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star span.icon:before {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.7333333333rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	border-color: #eaeaea;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress #send_message_form input,
+.bp-user #buddypress #send_message_form textarea {
+	box-sizing: border-box;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 15px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices tr {
+	margin: 3em 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-right: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a.button {
+	border: 1px solid #d4d0ba;
+	line-height: initial;
+	padding: 0.4em 0.3em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	line-height: 1;
+	padding-bottom: 1em;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 15px;
+}
+
+.bp-user .ac_results li {
+	margin: 15px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+.bp-user .ac_results li img {
+	vertical-align: bottom;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.3333333333rem;
+	margin: 25px 0 15px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	border-width: 1px;
+	box-shadow: none;
+	box-sizing: border-box;
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-left: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 15px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	border-radius: 0;
+	box-shadow: none;
+	box-sizing: border-box;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	line-height: 1.8;
+	margin: 0;
+	min-height: 2rem;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(212, 208, 186, 0.6);
+	float: right;
+	margin: 0;
+	padding: 0 0.2em;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-device-width: 450px) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 800px) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress table {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin: 25px 0;
+}
+
+.bp-user #buddypress table input {
+	margin: 0;
+}
+
+.bp-user #buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	padding: 4px 8px;
+	text-transform: capitalize;
+}
+
+.bp-user #buddypress table tr td {
+	padding: 8px;
+}
+
+.bp-user #buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+.bp-user #buddypress .notifications th,
+.bp-user #buddypress .messages-notices th {
+	width: 30%;
+}
+
+.bp-user #buddypress .notifications th.bulk-select-all,
+.bp-user #buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+.bp-user #buddypress .notifications th.actions,
+.bp-user #buddypress .messages-notices th.actions {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .bulk-select-check,
+.bp-user #buddypress .notifications .thread-star,
+.bp-user #buddypress .messages-notices .bulk-select-check,
+.bp-user #buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions,
+.bp-user #buddypress .notifications td.thread-options,
+.bp-user #buddypress .messages-notices .notification-actions,
+.bp-user #buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions a,
+.bp-user #buddypress .notifications td.thread-options a,
+.bp-user #buddypress .messages-notices .notification-actions a,
+.bp-user #buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications td .button,
+.bp-user #buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.2rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten.css	(revision 12265)
@@ -0,0 +1,1949 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyten theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyten this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+body.page-template-onecolumn-page #main #container {
+	margin: 0;
+}
+
+body.page-template-onecolumn-page #content {
+	margin: 0 10%;
+	width: auto;
+}
+
+body.page-template-onecolumn-page #content .entry-header,
+body.page-template-onecolumn-page #content .entry-content {
+	width: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+/* Here we start things off */
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a:focus, #buddypress div.item-list-tabs ul li a:hover {
+	color: #555;
+}
+
+#buddypress div.item-list-tabs ul li a:visited {
+	color: #21759b;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	padding: 0 4px !important;
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 1px solid #d4d0ba;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	float: right;
+	margin-top: 0;
+	padding: 5px 0 5px;
+	text-align: right;
+	width: 230px;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select option {
+	font-style: normal;
+}
+
+.bp-user #buddypress div#subnav.item-list-tabs li.last {
+	margin-left: 0;
+	text-align: right;
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 800px) {
+	body.bp-user #buddypress #item-header,
+	body.bp-user #buddypress #item-body {
+		background: #fff;
+	}
+	body.bp-user #buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	body.bp-user #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.bp-user #buddypress #object-nav ul li {
+		float: none;
+		margin-bottom: 5px;
+	}
+	body.bp-user #buddypress #object-nav ul li a {
+		overflow: hidden;
+	}
+	body.bp-user #buddypress #object-nav ul li.selected {
+		background: #ddd;
+	}
+	body.bp-user #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	body.bp-user #buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 25px;
+		width: auto;
+	}
+	body.bp-user #buddypress #item-body #subnav {
+		margin: 0 0 0 -25px;
+	}
+	body.bp-user #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media only screen and (min-device-width: 375px) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: left;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #373737;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress ul.item-list {
+	border-top: 0;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 15px;
+	text-align: center;
+}
+
+@media only screen and (min-device-width: 375px) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 15px;
+}
+
+@media only screen and (min-device-width: 450px) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+		opacity: 1;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-left: 0;
+	overflow: hidden;
+}
+
+#buddypress ul.item-list li .item .item-meta {
+	text-align: center;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item .item-meta {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-left: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-device-width: 450px) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title > a {
+	text-decoration: none;
+}
+
+#buddypress ul.item-list li .item .item-title > a:visited {
+	color: #21759b;
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.8rem;
+	padding: 15px 0;
+	text-align: left;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 5%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 18px;
+		font-size: 1.2rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin: 0;
+	margin-left: 0;
+	padding: 15px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+}
+
+@media only screen and (min-device-width: 375px) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 15px 5px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 15px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.6;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	max-width: 220px;
+	min-height: 1.5em;
+	padding: 0 0 0 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		font-size: 12px;
+		font-size: 0.8rem;
+		float: right;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit input {
+		padding: 0 6px;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 50px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li.load-newest a {
+	display: block;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 25px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content,
+#buddypress #activity-stream li .activity-comments {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li .activity-header a:visited {
+	color: #21759b;
+}
+
+#buddypress #activity-stream li .activity-inner img {
+	height: auto;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 15px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress #activity-stream li .activity-comments {
+		margin-left: 70px;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-meta a {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin-bottom: 5px;
+	padding: 0.2em 0.5em;
+}
+
+@media screen and (min-width: 650px) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		float: left;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 15px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	margin-bottom: 50px;
+}
+
+@media screen and (min-width: 650px) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.3333333333rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 25px;
+}
+
+.activity-permalink #buddypress #activity-stream .activity-comments {
+	margin-left: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	position: relative;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 0 0 2px;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li:hover * {
+	color: #555;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li .acomment-meta,
+#buddypress #activity-stream .activity-comments > ul > li .acomment-content {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	border: 1px solid #d4d0ba;
+	box-sizing: border-box;
+	margin: 15px 0;
+	width: 100%;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(55, 55, 55, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.8rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 650px) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: right;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress a:visited {
+	color: #21759b;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	text-align: left;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+	.bp-user #buddypress #item-header-content .user-nicename,
+	.single-item.groups #buddypress #item-header-content .user-nicename {
+		color: #555;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 25px;
+	}
+}
+
+@media screen and (max-width: 50em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 50px;
+}
+
+.single-item.groups #buddypress div#item-header #item-header-content {
+	box-sizing: border-box;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0 !important;
+	width: 100%;
+}
+
+@media screen and (min-width: 50em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		padding-left: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-left: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	font-weight: 700;
+	line-height: 1.4;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions #group-admins li,
+.single-item.groups #buddypress div#item-header div#item-actions #group-mods li {
+	margin: 0;
+}
+
+@media screen and (min-width: 650px) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 2%;
+		padding: 0 0.5em;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: right;
+		width: 28%;
+	}
+}
+
+@media screen and (min-width: 64em) {
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+	margin-top: 225px !important;
+}
+
+@media screen and (min-width: 50em) and (max-width: 60em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		max-width: 60% !important;
+		width: 60% !important;
+	}
+}
+
+@media screen and (max-width: 64em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+		border: 0;
+		clear: both;
+		margin-top: 0 !important;
+		max-width: 100% !important;
+		padding-top: 0 !important;
+		width: auto;
+	}
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-cover-image .user-nicename {
+	color: inherit;
+	text-shadow: none;
+}
+
+@media screen and (min-width: 800px) {
+	.bp-user #buddypress #item-header #item-header-cover-image .user-nicename {
+		color: #fff;
+		text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
+	}
+}
+
+.bp-user #buddypress #item-header #item-header-content #item-buttons .generic-button {
+	margin-right: 5px;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		margin-right: 0%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	font-weight: 700;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(55, 55, 55, 0.6);
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 25px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li img,
+	.groups.manage-members #group-settings-form .item-list li h5 {
+		float: left;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 15px;
+	width: 100%;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: block;
+	margin: 5px 0;
+}
+
+@media screen and (min-width: 650px) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		display: inline-block;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 0 0 0;
+}
+
+.groups.group-members #subnav li #search-members-form label input[type="text"] {
+	box-shadow: none;
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 13px;
+	font-size: 0.8666666667rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.8rem;
+}
+
+@media screen and (min-width: 650px) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.9333333333rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 60%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: left;
+	margin-right: 0;
+	width: 49%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: left;
+	font-family: inherit;
+	line-height: 20px;
+	margin-left: 1%;
+	width: 50%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus, .bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.2rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle {
+	margin: 5px 0 25px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #6f6e6e;
+	border-radius: 2px;
+	background-clip: padding-box;
+	color: #eee;
+	font-weight: 700;
+	padding: 0.1em 0.5em;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:focus, .bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:hover,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:focus,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:hover {
+	color: #fff;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread #message-recipients .highlight {
+	font-size: 100%;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 15px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	box-sizing: border-box;
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom: 1px solid #ccc;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star,
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-unstar {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star span.icon:before {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.8rem;
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.7333333333rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	border-color: #eaeaea;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress #send_message_form input,
+.bp-user #buddypress #send_message_form textarea {
+	box-sizing: border-box;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 15px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices tr {
+	margin: 3em 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-left: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a.button {
+	border: 1px solid #d4d0ba;
+	line-height: initial;
+	padding: 0.4em 0.3em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	line-height: 1;
+	padding-bottom: 1em;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 15px;
+}
+
+.bp-user .ac_results li {
+	margin: 15px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+.bp-user .ac_results li img {
+	vertical-align: bottom;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.3333333333rem;
+	margin: 25px 0 15px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	border-width: 1px;
+	box-shadow: none;
+	box-sizing: border-box;
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-right: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 15px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	border: 0;
+	border-radius: 0;
+	box-shadow: none;
+	box-sizing: border-box;
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	line-height: 1.8;
+	margin: 0;
+	min-height: 2rem;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(212, 208, 186, 0.6);
+	float: left;
+	margin: 0;
+	padding: 0 0.2em;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-device-width: 450px) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 800px) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress table {
+	font-size: 14px;
+	font-size: 0.9333333333rem;
+	margin: 25px 0;
+}
+
+.bp-user #buddypress table input {
+	margin: 0;
+}
+
+.bp-user #buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+	font-size: 16px;
+	font-size: 1.0666666667rem;
+	padding: 4px 8px;
+	text-transform: capitalize;
+}
+
+.bp-user #buddypress table tr td {
+	padding: 8px;
+}
+
+.bp-user #buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table {
+		font-size: 16px;
+		font-size: 1.0666666667rem;
+	}
+}
+
+.bp-user #buddypress .notifications th,
+.bp-user #buddypress .messages-notices th {
+	width: 30%;
+}
+
+.bp-user #buddypress .notifications th.bulk-select-all,
+.bp-user #buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+.bp-user #buddypress .notifications th.actions,
+.bp-user #buddypress .messages-notices th.actions {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .bulk-select-check,
+.bp-user #buddypress .notifications .thread-star,
+.bp-user #buddypress .messages-notices .bulk-select-check,
+.bp-user #buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions,
+.bp-user #buddypress .notifications td.thread-options,
+.bp-user #buddypress .messages-notices .notification-actions,
+.bp-user #buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions a,
+.bp-user #buddypress .notifications td.thread-options a,
+.bp-user #buddypress .messages-notices .notification-actions a,
+.bp-user #buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications td .button,
+.bp-user #buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.2rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentyten.scss	(revision 12265)
@@ -0,0 +1,2710 @@
+
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility .
+
+// 2010 Media Queries / Breakpoints.
+// 2010 is a fixed layout  with no fluid elements.
+// We will provide some simple breakpoints to manage BP elements
+// on a limited basis.
+
+
+// Mixins and Variables
+
+// simplify remembering/writing media queries
+// written as:
+// @include medium-up {
+//  body {property: value;}
+// }
+
+// BP  min/max width mixins
+
+// As layout is fixed width 'device' is needed to tell phones to work with
+// breakpoints provided is a 'up to' max-width and min-width as device widths
+// for mobiles This is not a perfect solution but provides a degree of layout
+// contro for BP elements. But not used to any great degree.
+
+@mixin small-device-up {
+
+	@media only screen and (min-device-width: 375px) {
+
+		@content;
+	}
+}
+
+@mixin small-device-max {
+
+	@media only screen and (max-device-width: 375px) {
+
+		@content;
+	}
+}
+
+@mixin medium-device-up {
+
+	@media only screen and (min-device-width: 450px) {
+
+		@content;
+	}
+}
+
+// min-width 450px
+@mixin small-up {
+
+	@media screen and (min-device-width: 450px) {
+
+		@content;
+	}
+}
+
+// min-width 650px
+@mixin medium-up {
+
+	@media screen and (min-width: 650px) {
+
+		@content;
+	}
+}
+
+// min-width 800px
+@mixin large-up {
+
+	@media screen and (min-width: 800px) {
+
+		@content;
+	}
+}
+
+// Common spacing values
+$spacing-val-lg: 50px;
+$spacing-val-md: 25px;
+$spacing-val-sm: 15px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing
+// 2010 sets a html/body size @ 15px
+
+@mixin font-size($font-size: 15) {
+	$rem-font-value: ($font-size / 15);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($model-type) {
+	-webkit-box-sizing: $model-type;
+	-moz-box-sizing: $model-type;
+	box-sizing: $model-type;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2010 font family
+@mixin default-font-sans() {
+	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+// Variables: color definitions
+$body-text:          #373737;
+$content-background: #fff;
+$off-white:          lighten(#f9f8ee, 0.7);
+$cream-background:   #f7f5e7;
+$light-background:   #f7f7f7;
+$medium-background:  #ccc;
+$dark-background:    #555;
+$border-color:       #d4d0ba;    // border color is varied using rgba
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$background-hover:   #db572f; // 2010 link background hover
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #9fd1e2;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             #9dc3c8;
+$link-action:        #c82b2b;
+
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentyten theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyten this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+
+.buddypress {
+	// Remove any empty clearing elements, CSS provides clearing.
+	div.clear {
+		display: none;
+	}
+
+} // .buddypress (body class)
+
+
+// Main dir pages: sidebar or not to sidebar
+// If no sidebar lets correct some odd layout rules
+
+body.page-template-onecolumn-page {
+
+	#main {
+
+		#container {
+			margin: 0;
+		}
+	}
+
+	#content {
+		margin: 0 10%;
+		width: auto;
+
+		.entry-header,
+		.entry-content {
+			width: auto;
+		}
+	}
+}
+
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets will appear over weighted in selectors
+
+/* Here we start things off */
+#buddypress {
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected,
+			li.current {
+
+				a {
+					background: inherit;
+					opacity: 1;
+				}
+			}
+		}
+	}
+	// global nav styles
+	div.item-list-tabs {
+
+		ul {
+			overflow: hidden;
+			padding: 0;
+
+			li {
+
+				a {
+
+					&:focus,
+					&:hover {
+						color: #555;
+					}
+					// Keep links from inheriting :visited states
+					&:visited {
+						color: #21759b;
+					}
+
+					span {
+						padding: 0 4px !important;
+					}
+				}
+
+			}
+		}
+	} // close item-list-tabs
+
+} // close #buddypress
+
+
+/*__ Horizontal menus __*/
+#buddypress {
+
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+				float: none;
+			}
+
+			@include medium-up {
+
+				li {
+					float: left;
+				}
+			}
+
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			border-bottom: 0;
+			border-top: 1px solid $border-color;
+			margin-top: $spacing-val-xs;
+			padding: 0;
+
+			li.last {
+				background: $content-background;
+				float: right;
+				margin-top: 0;
+				padding: $spacing-val-xs 0 $spacing-val-xs;
+				text-align: right;
+				width: 230px;
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+					padding: 0;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+
+					option {
+						font-style: normal;
+					}
+				}
+			}
+
+		} // close ul
+	} // close #subnav
+
+} // close #buddypress
+
+.bp-user {
+
+	#buddypress {
+
+		div#subnav.item-list-tabs {
+
+			li.last {
+				margin-left: 0;
+				text-align: right;
+			}
+		}
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+
+@include large-up {
+
+	body.bp-user {
+
+		#buddypress {
+
+			#item-header,
+			#item-body {
+				background: $content-background;
+			}
+
+			#object-nav {
+				border-right: 1px solid $border-med;
+				float: left;
+				margin-right: -1px;
+				width: 200px;
+
+				ul {
+					background: none;
+					border-bottom: 0;
+					padding: 0;
+
+					li {
+						float: none;
+						margin-bottom: 5px;
+
+						a {
+							overflow: hidden;
+						}
+
+						&.selected {
+							background: #ddd;
+						}
+
+						span {
+							background: $content-background;
+							border-radius: 10%;
+							float: right;
+							margin-right: 2px;
+						}
+					}
+				}
+			}
+
+			#item-body {
+				border-left: 1px solid $border-med;
+				overflow: hidden;
+				padding: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				#subnav {
+					margin: 0 0 0 (-$spacing-val-md);
+
+					ul {
+						margin-top: 0;
+					}
+				}
+			}
+
+		} // close  #buddypress
+	} // close body.bp-user
+} // close @media
+
+
+// Tabbed Nav styling
+// Primarilly used for group create screens & ? directory nav.
+
+@include small-device-up {
+
+	#main {
+
+		#buddypress {
+
+			#group-create-tabs.item-list-tabs {
+
+				ul:before,
+				ul:after {
+					content: " ";
+					display: table;
+				}
+
+				ul:after {
+					clear: both;
+				}
+
+				ul {
+					background: none;
+					border: 0;
+					border-bottom: 1px solid $border-med;
+					overflow: visible;
+					padding-bottom: 0;
+
+					li {
+						float: left;
+						width: auto;
+					}
+
+					li.selected,
+					li.current {
+						border: 1px solid $border-med;
+						border-bottom-color: $content-background;
+
+						@include border-top-radius(4px);
+						margin-bottom: -1px;
+
+						a {
+							background: none;
+							color: $body-text;
+							outline: none;
+						}
+					}
+				}
+
+			} // close .item-list-tabs
+
+			#subnav {
+
+				ul {
+					border-bottom: 0;
+				}
+
+			} // close #subnav
+		} // close #buddypress
+	}
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+		min-height: 0;
+
+		&:before,
+		&:after {
+			height: 0;
+			width: 0;
+		}
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2010 font-family
+
+#buddypress {
+
+	ul.item-list {
+		border-top: 0;
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@include small-device-up {
+					margin-bottom: 0;
+				}
+
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+					margin-bottom: $spacing-val-sm;
+
+					// small device: move avatars to the left(right)
+					@include medium-device-up {
+						display: block;
+						float: left;
+						opacity: 1;
+					}
+				}
+			} //close .item-avatar
+
+			.item {
+
+				// counter the BP rule
+				margin-left: 0;
+				overflow: hidden;
+
+				.item-meta {
+					text-align: center;
+
+					@include medium-up {
+						text-align: left;
+					}
+				}
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin-left: 0;
+					width: 94%;
+				}
+
+				.item-title {
+
+					@include font-size(16);
+					line-height: 1.2;
+					text-align: center;
+
+					@include small-up {
+						text-align: left;
+					}
+
+					width: 100%;
+
+					> a {
+						text-decoration: none;
+
+						&:visited {
+							color: #21759b;
+						}
+					}
+
+					.update {
+						display: block;
+
+						@include font-size(12);
+
+						@include medium-up {
+
+							@include font-size(14);
+						}
+
+						padding: $spacing-val-sm 0;
+						text-align: left;
+					}
+				}  // close .item-title
+
+			} // close .item
+
+			@include medium-up {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 5%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+
+					.item-title {
+
+						@include font-size(18);
+					}
+				}
+
+			}// close medium-up
+
+			div.action {
+				clear: left;
+				float: none;
+				margin: 0;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-sm 0 $spacing-val-xs;
+
+				// push the actions to the right
+				@include medium-up {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-xs 0;
+					width: 100%;
+
+					a {
+						display: block;
+					}
+					// Provisionally lets keep the 'action' div
+					//position but inline-block the buttons
+
+					@include small-device-up {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm 5px 0;
+						width: auto;
+					} // close @media
+
+					@include medium-up {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		#whats-new-content.active {
+
+			#whats-new-options[style] {
+
+				#whats-new-post-in-box {
+					border: 1px solid rgba($border-color, 0.5);
+					float: left;
+
+					@include font-size(12);
+					line-height: 1.6;
+					margin-top: 12px;
+					padding-left: 0.2em;
+					width: 100%;
+
+					select {
+						background: none;
+						border: 0;
+						float: right;
+						margin: 0;
+						max-width: 220px;
+						min-height: 1.5em;
+						padding: 0 0 0 0.4em;
+					}
+				}
+
+				@media screen and (min-width: 30em) {
+
+					#whats-new-post-in-box {
+						width: auto;
+					}
+
+					#whats-new-submit {
+
+						@include font-size(12);
+						float: right;
+
+						input {
+							padding: 0 6px;
+						}
+					}
+				}
+
+			} // close #whats-new-options
+		} // close #whats-new-content.active
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			&.load-newest {
+
+				a {
+					display: block;
+				}
+			}
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content,
+			.activity-comments {
+				margin-left: 0;
+			}
+
+			.activity-header {
+
+				@include font-size(14);
+
+				a:visited {
+					color: #21759b;
+				}
+			}
+
+			.activity-inner {
+
+				img {
+					height: auto;
+				}
+			}
+
+			@include medium-up {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+				.activity-comments {
+					margin-left: 70px;
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+
+				.activity-meta {
+
+					a {
+
+						@include font-size(12);
+					}
+				}
+			}
+		} // close li.mini
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				line-height: inherit;
+				margin-right: 0;
+
+				p {
+					border-bottom: 1px solid rgba($border-light, 0.6);
+					margin-top: 0;
+					padding: 0 0.2em;
+				}
+
+				img.avatar {
+					display: inline-block;
+					margin-bottom: 0 !important; // have to match to BP
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+
+					@include font-size(14);
+					margin-bottom: $spacing-val-xs;
+
+					@include medium-up {
+						float: left;
+						margin-bottom: 0;
+					}
+
+					padding: 0.2em 0.5em;
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@include medium-up {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			} // close li.mini
+
+			.activity-comments {
+				margin-left: 0;
+			}
+
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			position: relative;
+
+			> ul {
+				background: rgba($light-background, 0.6);
+				margin: $spacing-val-sm 0 0 2px;
+
+				> li {
+
+					&:hover * {
+						color: #555;
+					}
+
+					.acomment-meta,
+					.acomment-content {
+
+						@include font-size(12);
+					}
+
+				}
+
+			}
+
+			.ac-form {
+				border: 1px solid $border-color;
+
+				@include box-model(border-box);
+				margin: $spacing-val-sm 0;
+				width: 100%;
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+
+	} // close #activity-steam
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@include medium-up {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#signup_form.standard-form {
+
+		#profile-details-section,
+		#basic-details-section,
+		#blog-details-section {
+			float: none;
+			width: 100%;
+
+			@media screen and (min-width: 37.5em) {
+				width: 48%;
+			}
+		}
+
+		@media screen and (min-width: 37.5em) {
+
+			#profile-details-section {
+				float: right;
+			}
+
+			#basic-details-section {
+				float: left;
+			}
+		}
+
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+		// On object nav links, table links (notifications etc) we don't
+		// really need :visited inheritance, so set pseudo class to :link color.
+		a {
+
+			&:visited {
+				color: #21759b;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+.bp-user,
+.single-item.groups {
+
+	#buddypress {
+
+		#item-header-content {
+
+			#item-meta {
+
+				@include font-size(14);
+				text-align: left;
+
+				p {
+					margin-bottom: 0.5em;
+				}
+			}
+		}
+	}
+
+	@media screen and (max-width: 37.5em) {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	@media screen and (max-width: 46.25em) {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	#buddypress {
+
+		@media screen and (max-width: 37.5em) {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+
+				.user-nicename {
+					color: #555;
+				}
+			}
+		} // close @media
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+	} // close #buddypress
+
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@media screen and (max-width: 50em) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+
+					h2 {
+						border-bottom: 1px solid $border-light;
+						text-align: center;
+					}
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			#item-header-content {
+
+				@include box-model(border-box);
+			}
+
+			div#item-actions {
+				margin: 0 !important;
+				width: 100%;
+
+				@media screen and (min-width: 50em) {
+					border-left: 1px solid $border-light;
+					clear: none;
+					float: right;
+					padding-left: 0.2em;
+					width: 30%;
+				} // close @media
+
+				@media screen and (min-width: 46.25em) {
+					width: 40%;
+				}
+
+				ul {
+					margin-top: 0;
+					padding-left: 0;
+				}
+
+				h2 {
+
+					@include font-size(14);
+					font-weight: 700;
+					line-height: 1.4;
+				}
+
+				#group-admins,
+				#group-mods {
+
+					li {
+						margin: 0;
+					}
+				}
+
+			} // close item-actions
+
+			@include medium-up {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-content {
+					margin-left: 2%;
+					padding: 0 0.5em;
+				}
+
+				div#item-actions {
+					float: right;
+					width: 28%;
+				}
+
+			} // close @media
+
+			@media screen and (min-width: 64em) {
+
+				#item-header-content {
+					width: 40%;
+				}
+			}
+		} // close #item-header
+	} // close #buddypress
+
+	// These rules handle the specific instance of Cover Images on single groups
+	// activated & attempt to address positioning concerns arising from themes
+	// breakpoints.
+	// Due to cover image styles being applied via embedded styles & thus carrying
+	// heavier weight than linked styles the use of !important to
+	// override may be required.
+	#buddypress {
+
+		#cover-image-container {
+
+			#item-header-cover-image {
+
+				#item-actions {
+					margin-top: 225px !important;
+				}
+
+				@media screen and (min-width: 50em) and (max-width: 60em) {
+
+					#item-header-content {
+						max-width: 60% !important;
+						width: 60% !important;
+					}
+				}
+
+				@media screen and (max-width: 64em) {
+
+					#item-actions {
+						border: 0;
+						clear: both;
+						margin-top: 0 !important;
+						max-width: 100% !important;
+						padding-top: 0 !important;
+						width: auto;
+
+						h2 {
+							border-bottom: 1px solid $border-light;
+						}
+					}
+				} // @media
+			}
+		}
+	}
+
+} // close .single-item.groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+
+			#item-header-avatar {
+				text-align: center;
+
+				img.avatar,
+				a {
+					border-bottom: 0;
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			#item-header-cover-image {
+
+				.user-nicename {
+					color: inherit;
+					text-shadow: none;
+
+					@include large-up {
+						color: #fff;
+						text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
+					}
+				}
+			}
+
+			#item-header-content {
+
+				#item-buttons {
+
+					.generic-button {
+						margin-right: 5px;
+					}
+				}
+			}
+
+			@include medium-up {
+
+				#item-header-avatar {
+					float: left;
+
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: right;
+					margin-right: 0%;
+					width: 69%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+		textarea {
+			height: auto;
+			min-height: 100px;
+			overflow: auto;
+		}
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form {
+
+		div.radio label {
+			font-weight: 700;
+			padding: 0.2em;
+
+			ul {
+				color: rgba($body-text, 0.6);
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+
+					@include medium-up {
+						float: left;
+					}
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+					width: 100%;
+
+					a {
+						display: block;
+						margin: $spacing-val-xs 0;
+
+						@include medium-up {
+							display: inline-block;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+				margin: $spacing-val-xs 0 0 0;
+
+				label {
+
+					input[type="text"] {
+						box-shadow: none;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(13);
+			}
+
+			td {
+
+				@include font-size(12);
+			}
+
+			@include medium-up {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav,
+		.messages-options-nav {
+			float: left;
+			width: 100%;
+
+			@media screen and (min-width: 37.5em) {
+				width: 60%;
+			}
+
+			select,
+			input {
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 0;
+				width: 49%;
+
+				@media screen and (min-width: 37.5em) {
+					width: auto;
+				}
+			}
+
+			input {
+				float: left;
+				font-family: inherit;
+				line-height: 20px;
+				margin-left: 1%;
+				width: 50%;
+
+				@media screen and (min-width: 37.5em) {
+					width: auto;
+				}
+
+				&[disabled="disabled"]:focus,
+				&[disabled="disabled"]:hover {
+					background: none;
+				}
+			}
+
+		} // close .notifications-options-nav/messages-options-nav
+
+	} //close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			.bp-widget {
+
+				h2 {
+					background: lighten($dark-background, 10%);
+					color: #fff;
+					margin-bottom: 0;
+					padding: 0.4em;
+				}
+
+				table {
+					margin-top: 0;
+				}
+			}
+
+			// Edit profile
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle {
+					margin: $spacing-val-xs 0 $spacing-val-md;
+				}
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: lighten($dark-background, 10%);
+
+					@include border-radius(2px);
+					color: #eee;
+					font-weight: 700;
+					padding: 0.1em 0.5em;
+					text-decoration: none;
+
+					&:focus,
+					&:hover {
+						color: #fff;
+					}
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+
+	} // close #buddypress
+} // close .bp-user
+
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+
+				.highlight {
+					font-size: 100%;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: lighten($dark-background, 10%);
+				}
+			}
+
+			tr {
+
+				td {
+					background: $content-background;
+
+					@include box-model(border-box);
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom: 1px solid $medium-background;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+					}
+				}
+
+				td.thread-star {
+					vertical-align: middle;
+
+					.message-action-star,
+					.message-action-unstar {
+						line-height: 1.2;
+					}
+
+					span.icon:before {
+
+						@include font-size(14);
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 3em;
+
+					@media screen and (max-width: 37.5em) {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 1.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@media screen and (max-width: 37.5em) {
+						clear: both;
+
+						@include font-size(11);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					border-color: $border-light;
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		#send_message_form {
+
+			input,
+			textarea {
+
+				@include box-model(border-box);
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		#message-threads.sitewide-notices {
+
+			tr {
+				margin: 3em 0;
+			}
+
+			td {
+				width: 100%;
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					margin-bottom: 0.4em;
+					padding-left: 0.2em;
+				}
+
+				a {
+					display: inline-block;
+				}
+
+				a.button {
+					border: 1px solid $border-color;
+					line-height: initial;
+					padding: 0.4em 0.3em;
+				}
+			}
+
+			td:first-child {
+				display: none;
+			}
+
+			td:nth-child(2) {
+
+				strong {
+					margin: -8px -8px 8px;
+				}
+			}
+
+			td:first-child + td + td {
+				// notice date
+				border-bottom: 0;
+
+				span {
+					line-height: 1;
+				}
+			}
+
+			td:last-child {
+				// notice actions
+				line-height: 1;
+				padding-bottom: 1em;
+				text-align: right;
+
+				a:last-child:after {
+					content: attr(title);
+					display: block;
+					line-height: initial;
+					text-indent: 0;
+				}
+			}
+		}
+
+	} // #buddypress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+
+			img {
+				vertical-align: bottom;
+			}
+
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+
+		// Profile table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #buddyPress
+
+} // close .bp-user
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Attempt to reset form control widths
+#main {
+
+	#buddypress {
+
+		.standard-form {
+
+			li {
+				float: none;
+			}
+
+			input[type="text"],
+			input[type="email"],
+			input[type="password"],
+			textarea {
+				border-width: 1px;
+				box-shadow: none;
+
+				@include box-model(border-box);
+				width: 100%;
+			}
+		}
+	}
+}
+
+
+#buddypress {
+
+	// Match BP form elements to Themes(2012) styling where styles
+	// are not inherited or being overriden.
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.5);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	.standard-form {
+
+		// Keep BP form element borders to 1px rather than 2013 2px
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		button,
+		select,
+		textarea {
+			border-color: rgba($border-color, 0.5);
+			border-width: 1px;
+		}
+
+	} // close .standard-form
+
+	#signup_form.standard-form {
+
+		div.submit {
+			float: none;
+
+			input {
+				margin-right: 0;
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.6);
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 70%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+				border: 0;
+				border-radius: 0;
+				box-shadow: none;
+
+				@include box-model(border-box);
+				@include font-size(14);
+				line-height: 1.8;
+				margin: 0;
+				min-height: 2rem;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.6);
+				float: left;
+				margin: 0;
+				padding: 0 0.2em;
+				width: 70%;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0 1em;
+				text-align: center;
+				text-transform: none;
+				width: 30%;
+			}
+		} // close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+
+	@include small-up {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+			float: right;
+			margin-bottom: 5px !important;
+
+			form {
+
+				label,
+				input[type="text"],
+				input[type="submit"] {
+					width: auto;
+				}
+			}
+		}
+
+	} // close @media
+
+	@include large-up {
+
+		.dir-search,
+		.message-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16);
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16);
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			@include font-size(14);
+			margin: $spacing-val-md 0;
+
+			// 2010 adds huge bottom margin to input controls,
+			//  unwanted in the table paradigm
+			input {
+				margin: 0;
+			}
+
+			tr th {
+				background: lighten($dark-background, 10%);
+				border-color: darken($border-light, 20%);
+				color: $content-background;
+
+				@include font-size(16);
+				padding: 4px 8px;
+				text-transform: capitalize;
+			}
+
+			tr td {
+				padding: 8px;
+			}
+
+			// Reduce the themes inherited paragraph margins in tables
+			p {
+				margin-bottom: 0.5em;
+			}
+		}
+
+		@media screen and (min-width: 55em) {
+
+			table {
+
+				@include font-size(16);
+			}
+		}
+
+		// Manage some table cells widths that are disproportionate to their content
+		.notifications,
+		.messages-notices {
+
+			th {
+				width: 30%;
+
+				&.bulk-select-all {
+					text-align: center;
+					width: 10%;
+				}
+			}
+
+			th.actions {
+				text-align: center;
+			}
+
+			.bulk-select-check,
+			.thread-star {
+				text-align: center;
+			}
+
+			.notification-actions,
+			td.thread-options {
+				text-align: center;
+
+				a {
+					display: inline-block;
+					margin: 0;
+					padding: 0;
+				}
+			}
+
+			// Notices action buttons, this maybe better moved, temp for now to address
+			// styling issues - this will need styling ideas
+			td {
+
+				.button {
+					border: 0;
+					display: block;
+					padding: 0;
+					text-align: center;
+				}
+			}
+
+		} // .notifications, .message-notices
+
+	} // #buddypress
+} // .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning' )
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen-rtl.css	(revision 12265)
@@ -0,0 +1,1968 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentythirteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfifteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements & overrides
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+@media (max-width: 999px) {
+	.buddypress #content {
+		padding: 0 20px;
+	}
+}
+
+@media (max-width: 643px) {
+	.buddypress #content {
+		padding: 0;
+	}
+}
+
+.buddypress .entry-header,
+.buddypress .entry-content,
+.buddypress .entry-summary,
+.buddypress .entry-meta {
+	max-width: 890px;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul {
+	background-color: #f7f5e7;
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #object-nav {
+		background: #f7f5e7;
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 10px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:focus:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:focus ul {
+		height: 320px;
+		opacity: 1;
+		overflow-y: auto;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus & small screen __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:focus, #buddypress #object-nav ul li:hover {
+		background: #f4f2df;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 0;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	margin-top: 0;
+	padding: 5px 0 5px 5px;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: left;
+	}
+}
+
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: none;
+	opacity: 1;
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	body:not(.sidebar) #buddypress #item-header,
+	body:not(.sidebar) #buddypress #item-body {
+		background: #fff;
+	}
+	body:not(.sidebar) #buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	body:not(.sidebar) #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body:not(.sidebar) #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body:not(.sidebar) #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	body:not(.sidebar) #buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 20px 0 0;
+		width: auto;
+	}
+	body:not(.sidebar) #buddypress #item-body #subnav {
+		margin: 0 -20px 0 0;
+	}
+	body:not(.sidebar) #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: right;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #141412;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+#buddypress .activity-meta .button:focus, #buddypress .activity-meta .button:hover {
+	background: #bd4b28;
+	color: #f7f5e7;
+}
+
+#buddypress .action .generic-button a:focus, #buddypress .action .generic-button a:hover {
+	background: #bd4b28;
+	color: #f7f5e7;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	overflow: hidden;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress ul.item-list li .item {
+		margin-right: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin: 0 auto;
+	width: 100%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 18px;
+	font-size: 1.125rem;
+	line-height: 1.2;
+	margin: 0 auto;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	font-size: 12px;
+	font-size: 0.75rem;
+	display: block;
+	padding: 10px 0;
+	text-align: right;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 5%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 22px;
+		font-size: 1.375rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin-bottom: -20px;
+	margin-right: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 10px 10px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: right;
+		margin: 10px 0 20px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	min-height: 1.5em;
+	padding-right: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: left;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 10px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	background-color: #f7f7f7;
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	color: #727272;
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p a.activity-time-since {
+	color: #727272;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin-bottom: 5px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.25rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	border-right: 1px solid #eaeaea;
+	margin-top: 5px;
+}
+
+#buddypress #activity-stream .activity-comments ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 2px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments a {
+	color: #ac0404;
+}
+
+#buddypress #activity-stream .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(20, 20, 18, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 59.6875em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: left;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user .entry-header,
+.single-item.groups .entry-header {
+	margin-bottom: 0;
+}
+
+.bp-user .entry-header .entry-title,
+.single-item.groups .entry-header .entry-title {
+	color: #8c8c7f;
+	font-size: 32px;
+	font-size: 2rem;
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user .entry-header .entry-title,
+	.single-item.groups .entry-header .entry-title {
+		text-align: right;
+	}
+}
+
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.875rem;
+	text-align: right;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		padding-right: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-right: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.875rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		clear: both;
+		float: left;
+		width: 28%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+	border: 0;
+	float: none;
+	width: auto;
+}
+
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+		clear: both !important;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content {
+		padding-right: 10px;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content #item-buttons {
+		margin-top: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		margin-top: 230px !important;
+		padding-top: 0 !important;
+		width: 20%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-content .user-nicename,
+.bp-user #buddypress #item-header #item-header-content span.activity {
+	display: block;
+	text-align: center;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+		width: auto;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: none;
+		overflow: hidden;
+	}
+	.bp-user #buddypress #item-header #item-header-content .user-nicename,
+	.bp-user #buddypress #item-header #item-header-content span.activity {
+		text-align: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content #item-meta {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header-cover-image #item-header-content .user-nicename {
+		margin-bottom: 20px;
+	}
+	.bp-user #buddypress #item-header-cover-image #item-header-content #item-buttons {
+		margin: 0;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.875rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(20, 20, 18, 0.6);
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: right;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0 5px;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.3 - Group send Invites
+*-----------------------------------------
+*/
+#buddypress div.invite .left-menu {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.invite .left-menu {
+		float: right;
+	}
+}
+
+#buddypress div.invite #invite-list {
+	border: 1px solid #eaeaea;
+	height: auto;
+	min-height: 3.125em;
+	padding: 0 0.3em 0.5em;
+	width: auto;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.invite #invite-list {
+		width: 220px;
+	}
+}
+
+#buddypress div.invite #invite-list ul {
+	padding-right: 0;
+}
+
+#buddypress div.invite .main-column {
+	margin-right: 0;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.invite .main-column {
+		margin-right: 235px;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 13px;
+	font-size: 0.8125rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.125rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.bp-user #buddypress .notifications-options-nav {
+		width: 300px;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select {
+	float: right;
+	margin-left: 0;
+	width: 60%;
+}
+
+.bp-user #buddypress .notifications-options-nav input {
+	float: left;
+	font-family: inherit;
+	line-height: 20px;
+	width: 40%;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		right: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #ccc;
+	border-bottom-width: 2px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+	.bp-user #buddypress #message-threads tr td.thread-star span.icon:before,
+	.bp-user #buddypress #message-threads tr td.thread-options span.icon:before {
+		font-size: 14px;
+	}
+}
+
+.bp-user #buddypress #message-threads tr .thread-checkbox {
+	padding: 0 0.3em 0 0;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info .thread-excerpt {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 12px;
+		font-size: 0.75rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: #dce5ff;
+	border-color: #d4d0ba;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress .sitewide-notices tr td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress .sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	padding-right: 0.2em;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 10px;
+}
+
+.bp-user .ac_results ul {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form {
+	padding: 0 10px;
+}
+
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-left: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 60%;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.dir-search form label,
+	#buddypress div.message-search form label,
+	#buddypress li.groups-members-search form label {
+		width: 80%;
+	}
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: right;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 0.875rem;
+	border: 0;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(212, 208, 186, 0.6);
+	padding: 0.2em 0.2em 0.2em 0;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 40%;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: 20%;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+}
+
+#buddypress table th,
+#buddypress table td {
+	padding: 0.3em;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen.css	(revision 12265)
@@ -0,0 +1,1968 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentythirteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfifteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements & overrides
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+@media (max-width: 999px) {
+	.buddypress #content {
+		padding: 0 20px;
+	}
+}
+
+@media (max-width: 643px) {
+	.buddypress #content {
+		padding: 0;
+	}
+}
+
+.buddypress .entry-header,
+.buddypress .entry-content,
+.buddypress .entry-summary,
+.buddypress .entry-meta {
+	max-width: 890px;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul {
+	background-color: #f7f5e7;
+	border-bottom: 1px solid rgba(234, 234, 234, 0.9);
+	border-top: 1px solid rgba(234, 234, 234, 0.9);
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #object-nav {
+		background: #f7f5e7;
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 10px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:focus:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:focus ul {
+		height: 320px;
+		opacity: 1;
+		overflow-y: auto;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus & small screen __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:focus, #buddypress #object-nav ul li:hover {
+		background: #f4f2df;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	background-color: #f7f7f7;
+	border-bottom: 0;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	margin-top: 0;
+	padding: 5px 5px 5px 0;
+	width: 100%;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		text-align: right;
+	}
+}
+
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: none;
+	opacity: 1;
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 55em) {
+	body:not(.sidebar) #buddypress #item-header,
+	body:not(.sidebar) #buddypress #item-body {
+		background: #fff;
+	}
+	body:not(.sidebar) #buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	body:not(.sidebar) #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body:not(.sidebar) #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body:not(.sidebar) #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	body:not(.sidebar) #buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 20px;
+		width: auto;
+	}
+	body:not(.sidebar) #buddypress #item-body #subnav {
+		margin: 0 0 0 -20px;
+	}
+	body:not(.sidebar) #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: left;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #141412;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress .item-list .activity-header,
+#buddypress .item-list .activity-meta {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+#buddypress .activity-meta .button:focus, #buddypress .activity-meta .button:hover {
+	background: #bd4b28;
+	color: #f7f5e7;
+}
+
+#buddypress .action .generic-button a:focus, #buddypress .action .generic-button a:hover {
+	background: #bd4b28;
+	color: #f7f5e7;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	overflow: hidden;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress ul.item-list li .item {
+		margin-left: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin: 0 auto;
+	width: 100%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 18px;
+	font-size: 1.125rem;
+	line-height: 1.2;
+	margin: 0 auto;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	font-size: 12px;
+	font-size: 0.75rem;
+	display: block;
+	padding: 10px 0;
+	text-align: left;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 5%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 22px;
+		font-size: 1.375rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin-bottom: -20px;
+	margin-left: 0;
+	padding: 20px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 10px 10px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 59.6875em) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: left;
+		margin: 10px 0 20px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	min-height: 1.5em;
+	padding-left: 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: right;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 40px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 20px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 10px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	background-color: #f7f7f7;
+	border: 1px solid rgba(234, 234, 234, 0.6);
+	color: #727272;
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p a.activity-time-since {
+	color: #727272;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	margin-bottom: 5px;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		display: inline-block;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more:focus a, #buddypress #activity-stream .load-more:hover a {
+	font-style: italic;
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 20px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1rem;
+	margin-bottom: 40px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.25rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 20px;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	border-left: 1px solid #eaeaea;
+	margin-top: 5px;
+}
+
+#buddypress #activity-stream .activity-comments ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 15px 0 0 2px;
+}
+
+#buddypress #activity-stream .activity-comments a {
+	color: #ac0404;
+}
+
+#buddypress #activity-stream .activity-comments .acomment-meta {
+	color: #737373;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(20, 20, 18, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.75rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 59.6875em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: right;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user .entry-header,
+.single-item.groups .entry-header {
+	margin-bottom: 0;
+}
+
+.bp-user .entry-header .entry-title,
+.single-item.groups .entry-header .entry-title {
+	color: #8c8c7f;
+	font-size: 32px;
+	font-size: 2rem;
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user .entry-header .entry-title,
+	.single-item.groups .entry-header .entry-title {
+		text-align: left;
+	}
+}
+
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 0.875rem;
+	text-align: left;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 20px;
+	}
+}
+
+@media screen and (max-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 40px;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		padding-left: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-left: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 0.875rem;
+	padding: 0.2em;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		width: 21%;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 4%;
+		width: 40%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		clear: both;
+		float: right;
+		width: 28%;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions h2 {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+	border: 0;
+	float: none;
+	width: auto;
+}
+
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+		clear: both !important;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content {
+		padding-left: 10px;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content #item-buttons {
+		margin-top: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		margin-top: 230px !important;
+		padding-top: 0 !important;
+		width: 20%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 20px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+	width: 100%;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-content .user-nicename,
+.bp-user #buddypress #item-header #item-header-content span.activity {
+	display: block;
+	text-align: center;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+		width: auto;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: none;
+		overflow: hidden;
+	}
+	.bp-user #buddypress #item-header #item-header-content .user-nicename,
+	.bp-user #buddypress #item-header #item-header-content span.activity {
+		text-align: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content #item-meta {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header-cover-image #item-header-content .user-nicename {
+		margin-bottom: 20px;
+	}
+	.bp-user #buddypress #item-header-cover-image #item-header-content #item-buttons {
+		margin: 0;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 0.875rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(20, 20, 18, 0.6);
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 20px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: left;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 20px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 5px 0 0;
+}
+
+@media screen and (max-width: 38.75em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.3 - Group send Invites
+*-----------------------------------------
+*/
+#buddypress div.invite .left-menu {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.invite .left-menu {
+		float: left;
+	}
+}
+
+#buddypress div.invite #invite-list {
+	border: 1px solid #eaeaea;
+	height: auto;
+	min-height: 3.125em;
+	padding: 0 0.3em 0.5em;
+	width: auto;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.invite #invite-list {
+		width: 220px;
+	}
+}
+
+#buddypress div.invite #invite-list ul {
+	padding-left: 0;
+}
+
+#buddypress div.invite .main-column {
+	margin-left: 0;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.invite .main-column {
+		margin-left: 235px;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 13px;
+	font-size: 0.8125rem;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 0.875rem;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress table th {
+		font-size: 18px;
+		font-size: 1.125rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 38.75em) {
+	.bp-user #buddypress .notifications-options-nav {
+		width: 300px;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input {
+	border: 0;
+	font-size: 14px;
+	font-size: 0.875rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select {
+	float: left;
+	margin-right: 0;
+	width: 60%;
+}
+
+.bp-user #buddypress .notifications-options-nav input {
+	float: right;
+	font-family: inherit;
+	line-height: 20px;
+	width: 40%;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.125rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #555;
+	color: #fff;
+	padding: 0.2em 0.5em;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		left: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom-color: #ccc;
+	border-bottom-width: 2px;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+	.bp-user #buddypress #message-threads tr td.thread-star span.icon:before,
+	.bp-user #buddypress #message-threads tr td.thread-options span.icon:before {
+		font-size: 14px;
+	}
+}
+
+.bp-user #buddypress #message-threads tr .thread-checkbox {
+	padding: 0 0 0 0.3em;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info .thread-excerpt {
+	font-size: 14px;
+	font-size: 0.875rem;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.75rem;
+	line-height: 2.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 38.75em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 12px;
+		font-size: 0.75rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	background: #dce5ff;
+	border-color: #d4d0ba;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress .sitewide-notices tr td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress .sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	padding-left: 0.2em;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 10px;
+}
+
+.bp-user .ac_results ul {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form {
+	padding: 0 10px;
+}
+
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.25rem;
+	margin: 20px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-right: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 60%;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.dir-search form label,
+	#buddypress div.message-search form label,
+	#buddypress li.groups-members-search form label {
+		width: 80%;
+	}
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	float: left;
+	margin: 0;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 0.875rem;
+	border: 0;
+	line-height: inherit;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(212, 208, 186, 0.6);
+	padding: 0.2em 0 0.2em 0.2em;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0.2em 1em;
+	text-align: center;
+	text-transform: none;
+	width: 40%;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: 20%;
+	}
+}
+
+@media screen and (min-width: 38.75em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 77.5em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+#buddypress table {
+	font-size: 14px;
+	font-size: 0.875rem;
+	margin: 20px 0;
+}
+
+#buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+}
+
+#buddypress table th,
+#buddypress table td {
+	padding: 0.3em;
+}
+
+#buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	#buddypress table {
+		font-size: 16px;
+		font-size: 1rem;
+	}
+}
+
+/*__ User Account tables __*/
+#buddypress .notifications th,
+#buddypress .messages-notices th {
+	width: 30%;
+}
+
+#buddypress .notifications th.bulk-select-all,
+#buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+#buddypress .notifications .bulk-select-check,
+#buddypress .notifications .thread-star,
+#buddypress .messages-notices .bulk-select-check,
+#buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions,
+#buddypress .notifications td.thread-options,
+#buddypress .messages-notices .notification-actions,
+#buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+#buddypress .notifications .notification-actions a,
+#buddypress .notifications td.thread-options a,
+#buddypress .messages-notices .notification-actions a,
+#buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+#buddypress .notifications td .button,
+#buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.125rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentythirteen.scss	(revision 12265)
@@ -0,0 +1,2680 @@
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility .
+
+// Twentythirteen Media Queries / Breakpoints.
+// @media (max-width: 1599px)
+// @media (max-width: 1069px)
+// @media (max-width: 999px)
+// @media (max-width: 767px)
+// @media (max-width: 643px)
+// @media (max-width: 359px)
+
+// These differ from other WP themes in using fewer breakpoints
+// and in using max-width rather than min-width
+// In addition BP will use the em breakpoints as used for twentyfifteen
+// referenced below.
+// @media screen and (max-width: 22em) // Additional for xtra small mobile only
+// @media screen and (max-width: 30em) // Additional small only
+// @media screen and (min-width: 38.75em)
+// @media screen and (min-width: 46.25em)
+// @media screen and (min-width: 55em)
+// @media screen and (min-width: 59.6875em)
+// @media screen and (min-width: 77.5em)
+// @media screen and (min-width: 87.6875em)
+
+// Mixins and Variables
+
+// Common margin values
+$spacing-val-lg: 40px;
+$spacing-val-md: 20px;
+$spacing-val-sm: 10px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing
+// 2013 uses a basic pixel font size approach, we'll stick with rem/px
+@mixin font-size($font-size: 16) {
+	$rem-font-value: ($font-size / 16);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($box-model) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2013 font family
+@mixin default-font-sans() {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+// Variables: color definitions
+$body-text:          #141412;
+$light-body:         lighten(#141412, 45%);
+$content-background: #fff;
+$off-white:          lighten(#f9f8ee, 10%);
+$cream-background:   #f7f5e7;
+$light-background:   #f7f7f7;
+$medium-background:  #ccc;
+$dark-background:    #555;
+$border-color:       #d4d0ba;    // border color is varied using rgba
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$background-hover:   #bd4b28; // 2013 link background hover
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #9fd1e2;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             #dce5ff;
+$link-action:        #c82b2b;
+
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentythirteen theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentyfifteen this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+			6.2.1.3 - Group send Invites
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements & overrides
+*-------------------------------------------------------------------------------
+*/
+
+// As BP screens have specific content lets adjust the themes primary elements
+// to maximise the screen real estate for BP pages.
+
+.buddypress {
+
+	div.clear {
+		display: none;
+	}
+
+	// re-factor main themes primary elements widths for BP screens
+	@media (max-width: 999px) {
+
+		#content {
+			padding: 0 $spacing-val-md;
+		}
+	}
+
+	@media (max-width: 643px) {
+
+		#content {
+			padding: 0;
+		}
+	}
+
+	.entry-header,
+	.entry-content,
+	.entry-summary,
+	.entry-meta {
+		max-width: 890px;
+	}
+
+} // .buddypress (body class)
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets will appear over weighted in selectors
+
+#buddypress {
+
+	// Global Nav Styles
+	div.item-list-tabs {
+
+		ul {
+			background-color: $cream-background;
+			border-bottom: 1px solid rgba($border-light, 0.9);
+			border-top: 1px solid rgba($border-light, 0.9);
+			overflow: hidden;
+			padding: 0;
+
+			li {
+
+				a {
+
+					span {
+						border-radius: 25%;
+					}
+				}
+			}
+		}
+	}
+}
+// .bp-user #object-nav @ small screen as a dropdown
+
+.bp-user {
+
+	#buddypress {
+
+		@media screen and (max-width: 38.75em) {
+
+			#object-nav {
+				background: $cream-background;
+				border: 1px solid $border-light;
+				overflow: visible;
+				padding: $spacing-val-sm;
+
+				ul {
+					border: 0;
+					height: 0;
+					// all latest vendor releases handle transitions, so no prefixes used.
+					transition: height 0.3s ease-in-out 0.7s;
+					visibility: hidden;
+				}
+
+				&:before {
+					content: "Menu \021E9";
+					display: inline;
+					opacity: 0.7;
+				}
+
+				&:hover,
+				&:focus {
+
+					&:before {
+						content: "Menu \021E7";
+					}
+
+					ul {
+						height: 320px;
+						opacity: 1;
+						overflow-y: auto;
+						visibility: visible;
+					}
+				}
+			}
+
+			#subnav {
+				clear: both;
+			}
+
+		} // close @media
+	}
+}
+
+/*__ Horizontal menus & small screen __*/
+
+#buddypress {
+
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+
+				@media screen and (max-width: 38.75em) {
+
+					&:focus,
+					&:hover {
+						background: darken($cream-background, 2%);
+					}
+				}
+
+				float: none;
+			}
+
+			@media screen and (max-width: 38.75em) {
+
+				li:not(:last-child) {
+					border-bottom: 1px solid $border-light;
+				}
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				li {
+					float: left;
+				}
+			}
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			background-color: $light-background;
+			border-bottom: 0;
+			margin-top: $spacing-val-xs;
+			padding: 0;
+
+			li.last {
+				background: $content-background;
+				margin-top: 0;
+				padding: $spacing-val-xs $spacing-val-xs $spacing-val-xs 0;
+				width: 100%;
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+				}
+			}
+
+			@media screen and (min-width: 38.75em) {
+
+				li.last {
+					text-align: right;
+				}
+			}
+		} // close ul
+	} // close #subnav
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected,
+			li.current {
+
+				a {
+					background: none;
+					opacity: 1;
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+// As 2013 has an inner sidebar which if active can narrow screen too far for
+// a columner nav we'll only use these rules if no sidebar 'body:not(.sidebar)'.
+
+@media screen and (min-width: 55em) {
+
+	body:not(.sidebar) {
+
+		#buddypress {
+
+			#item-header,
+			#item-body {
+				background: $content-background;
+			}
+
+			#object-nav {
+				border-right: 1px solid $border-med;
+				float: left;
+				margin-right: -1px;
+				width: 200px;
+
+				ul {
+					background: none;
+					border-bottom: 0;
+					padding: 0;
+
+					li {
+						float: none;
+						overflow: hidden;
+
+						span {
+							background: $content-background;
+							border-radius: 10%;
+							float: right;
+							margin-right: 2px;
+						}
+					}
+				}
+			}
+
+			#item-body {
+				border-left: 1px solid $border-med;
+				overflow: hidden;
+				padding: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				#subnav {
+					margin: 0 0 0 (-$spacing-val-md);
+
+					ul {
+						margin-top: 0;
+					}
+				}
+			}
+
+		} // close  #buddypress
+	} // close body:not(.sidebar)
+} // close @media
+
+
+// Tabbed Nav styling
+// Primarilly used for group create screens & ? directory nav.
+
+@media screen and (min-width: 46.25em) {
+
+	#main {
+
+		#buddypress {
+
+			#group-create-tabs.item-list-tabs {
+
+				ul:before,
+				ul:after {
+					content: " ";
+					display: table;
+				}
+
+				ul:after {
+					clear: both;
+				}
+
+				ul {
+					background: none;
+					border: 0;
+					border-bottom: 1px solid $border-med;
+					overflow: visible;
+					padding: 0;
+
+					li {
+						float: left;
+						width: auto;
+					}
+
+					li.selected,
+					li.current {
+						border: 1px solid $border-med;
+						border-bottom-color: $content-background;
+
+						@include border-top-radius(4px);
+						margin-bottom: -1px;
+
+						a {
+							background: none;
+							color: $body-text;
+							outline: none;
+						}
+					}
+				}
+
+			} // close .item-list-tabs
+
+			#subnav {
+
+				ul {
+					border-bottom: 0;
+				}
+
+			} // close #subnav
+		} // close #buddypress
+	}
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Adjusts Meta items and various elements to match 2013 font-family
+#buddypress {
+
+	.item-list {
+
+		.activity-header,
+		.activity-meta {
+
+			@include default-font-sans();
+		}
+	}
+}
+
+#buddypress {
+
+	.activity-meta {
+
+		.button {
+
+			&:focus,
+			&:hover {
+				background: $background-hover;
+				color: $cream-background;
+			}
+		}
+	}
+
+	.action {
+
+		.generic-button {
+
+			a {
+
+				&:focus,
+				&:hover {
+					background: $background-hover;
+					color: $cream-background;
+				}
+			}
+		}
+	}
+} // close #buddypress
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2013 font-family
+
+#buddypress {
+
+	ul.item-list {
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@media screen and (min-width: 38.75em) {
+					margin-bottom: 0;
+				}
+
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+
+					// Quite early on we need to move avatars to the left(right)
+					@media screen and (min-width: 38.75em) {
+						display: block;
+						float: left;
+					}
+				}
+			}
+
+			.item {
+				overflow: hidden;
+
+				@media screen and (min-width: 46.25em) {
+					margin-left: 15%;
+				}
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin: 0 auto;
+					width: 100%;
+				}
+
+				.item-title {
+
+					@include font-size(18);
+					line-height: 1.2;
+					margin: 0 auto;
+					text-align: center;
+
+					@media screen and (min-width: 38.75em) {
+						text-align: left;
+					}
+
+					width: 100%;
+
+					.update {
+
+						@include font-size(12);
+						display: block;
+
+						@media screen and (min-width: 59.6875em) {
+
+							@include font-size(14);
+						}
+
+						padding: $spacing-val-sm 0;
+						text-align: left;
+					}
+
+				} // .item-title
+
+			}
+
+			@media screen and (min-width: 59.6875em) {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 5%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+
+					.item-title {
+
+						@include font-size(22);
+					}
+				}
+
+			}// close @media
+
+			div.action {
+				clear: left;
+				float: none;
+				margin-bottom: -$spacing-val-md;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-md 0 $spacing-val-xs;
+				// push the actions to the right
+
+				@media screen and (min-width: 59.6875em) {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-sm 0;
+					width: 100%;
+
+					a {
+						display: block;
+						width: 100%;
+					}
+					// Provisionally lets keep the 'action' div
+					//position but inline-block the buttons
+
+					@media screen and (min-width: 38.75em) {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm $spacing-val-sm 0;
+						width: auto;
+					} // close @media
+
+					@media screen and (min-width: 59.6875em) {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		@media screen and (max-width: 46.25em) {
+
+			#whats-new-content {
+				clear: left;
+				margin: $spacing-val-sm 0 $spacing-val-md;
+				padding: $spacing-val-sm 0 0;
+			}
+		}
+
+		#whats-new-content.active {
+
+			#whats-new-options[style] {
+
+				#whats-new-post-in-box {
+					border: 1px solid rgba($border-color, 0.5);
+					float: left;
+					line-height: 1.5;
+					margin-top: 12px;
+					padding-left: 0.2em;
+					width: 100%;
+
+					select {
+						background: none;
+						border: 0;
+						float: right;
+						margin: 0;
+						min-height: 1.5em;
+						padding-left: 0.4em;
+					}
+				}
+
+				@media screen and (min-width: 30em) {
+
+					#whats-new-post-in-box {
+						width: auto;
+					}
+
+					#whats-new-submit {
+						float: right;
+					}
+				}
+
+			} // close #whats-new-options
+		} // close #whats-new-content.active
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content {
+				margin-left: 0;
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+			}
+		}
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				line-height: inherit;
+				margin-right: 0;
+
+				p {
+					background-color: $light-background;
+					border: 1px solid rgba($border-light, 0.6);
+					color: #727272;
+					margin-top: 0;
+					padding: 0 0.2em;
+
+					a.activity-time-since {
+						color: #727272;
+					}
+				}
+
+				img.avatar {
+					display: inline-block;
+					margin-bottom: 0 !important; // have to match to BP
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+					margin-bottom: $spacing-val-xs;
+
+					@media screen and (min-width: 38.75em) {
+						display: inline-block;
+						margin-bottom: 0;
+					}
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+
+				a {
+					font-style: italic;
+				}
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			border-left: 1px solid $border-light;
+			margin-top: 5px;
+
+			ul {
+				background: rgba($light-background, 0.6);
+				margin: 15px 0 0 2px;
+			}
+
+			a {
+				color: #ac0404;
+			}
+
+			.acomment-meta {
+				color: #737373;
+			}
+
+			.ac-form {
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+	} // close .activity
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@media screen and (min-width: 59.6875em) {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#signup_form.standard-form {
+
+		#profile-details-section,
+		#basic-details-section,
+		#blog-details-section {
+			float: none;
+			width: 100%;
+
+			@media screen and (min-width: 38.75em) {
+				width: 48%;
+			}
+		}
+
+		@media screen and (min-width: 38.75em) {
+
+			#profile-details-section {
+				float: right;
+			}
+
+			#basic-details-section {
+				float: left;
+			}
+		}
+
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+.bp-user,
+.single-item.groups {
+
+	.entry-header {
+		margin-bottom: 0;
+
+		.entry-title {
+			color: $light-body;
+
+			@include font-size(32);
+			margin-bottom: $spacing-val-sm;
+			text-align: center;
+
+			@media screen and (min-width: 46.25em) {
+				text-align: left;
+			}
+		}
+	}
+
+	#buddypress {
+
+		#item-header-content {
+
+			#item-meta {
+
+				@include font-size(14);
+				text-align: left;
+
+				p {
+					margin-bottom: 0.5em;
+				}
+			}
+		}
+	}
+
+	@media screen and (max-width: 46.25em) {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	@media screen and (max-width: 38.75em) {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+
+		@media screen and (max-width: 38.75em) {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@media screen and (max-width: 38.75em) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+
+					h2 {
+						border-bottom: 1px solid $border-light;
+						text-align: center;
+					}
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			div#item-actions {
+				margin: 0;
+				width: 100%;
+
+				@media screen and (min-width: 38.75em) {
+					border-left: 1px solid $border-light;
+					clear: none;
+					float: right;
+					padding-left: 0.2em;
+					width: 30%;
+				} // close @media
+
+				@media screen and (min-width: 46.25em) {
+					width: 40%;
+				}
+
+				ul {
+					margin-top: 0;
+					padding-left: 0;
+				}
+
+				h2 {
+
+					@include font-size(14);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(16);
+					}
+
+					padding: 0.2em;
+				}
+			} // close item-actions
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-avatar {
+					width: 21%;
+				}
+
+				#item-header-content {
+					margin-left: 4%;
+					width: 40%;
+				}
+
+				div#item-actions {
+					clear: both;
+					float: right;
+					width: 28%;
+
+					h2 {
+
+						@include font-size(14);
+					}
+				}
+
+			} // close @media
+
+			#item-header-cover-image {
+
+				#item-actions {
+					border: 0;
+
+					@media screen and (max-width: 46.25em) {
+						clear: both !important;
+					}
+
+					float: none;
+					width: auto;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					#item-header-content {
+						padding-left: $spacing-val-sm;
+
+						#item-buttons {
+							margin-top: 0;
+						}
+					}
+
+					#item-actions {
+						border-left: 1px solid $border-light;
+						clear: none;
+						float: right;
+						margin-top: 230px !important;
+						padding-top: 0 !important;
+						width: 20%;
+					}
+				}
+			}
+
+		} // close #item-header
+	} // close #buddypress
+} // close .groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+			padding: $spacing-val-md 0;
+
+			#item-header-avatar {
+				text-align: center;
+				width: 100%;
+
+				img.avatar,
+				a {
+					border-bottom: 0;
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			#item-header-content {
+
+				.user-nicename,
+				span.activity {
+					display: block;
+					text-align: center;
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar {
+					float: left;
+					width: auto;
+
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: none;
+					overflow: hidden;
+
+					.user-nicename,
+					span.activity {
+						text-align: left;
+					}
+
+					#item-meta {
+						margin-top: 0;
+					}
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+		#item-header-cover-image {
+
+			#item-header-content {
+
+				@media screen and (min-width: 46.25em) {
+
+					.user-nicename {
+						margin-bottom: $spacing-val-md;
+					}
+
+					#item-buttons {
+						margin: 0;
+					}
+				}
+			}
+		}
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+		textarea {
+			height: auto;
+			min-height: 100px;
+			overflow: auto;
+		}
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form {
+
+		div.radio label {
+			border: 1px solid $border-light;
+			padding: 0.2em;
+
+			ul {
+				color: rgba($body-text, 0.6);
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+					float: left;
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+
+					a {
+						display: inline-block;
+						margin: $spacing-val-xs 0;
+						width: 100%;
+
+						@media screen and (min-width: 38.75em) {
+							width: auto;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+
+			@media screen and (max-width: 38.75em) {
+				background: $content-background;
+				padding: $spacing-val-md 0;
+			}
+
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+
+				@media screen and (max-width: 38.75em) {
+					margin: 0;
+					width: 100%;
+
+					label {
+
+						input[type="text"] {
+							width: 100%;
+						}
+					}
+				}
+
+				margin: $spacing-val-xs $spacing-val-xs 0 0;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.3 - Group send Invites
+*-----------------------------------------
+*/
+#buddypress {
+
+	div.invite {
+
+		.left-menu {
+			float: none;
+
+			@media screen and (min-width: 46.25em) {
+				float: left;
+			}
+		}
+
+		#invite-list {
+			border: 1px solid $border-light;
+			height: auto;
+			min-height: 3.125em;
+			padding: 0 0.3em 0.5em;
+			width: auto;
+
+			@media screen and (min-width: 46.25em) {
+				width: 220px;
+			}
+
+			ul {
+				padding-left: 0;
+			}
+		}
+
+		.main-column {
+			margin-left: 0;
+
+			@media screen and (min-width: 46.25em) {
+				margin-left: 235px;
+			}
+		}
+
+	} // close div.invite
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	.entry-title {
+		margin-bottom: 0.5em;
+	}
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(14);
+			}
+
+			td {
+
+				@include font-size(13);
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+
+			@media screen and (min-width: 77.5em) {
+
+				th {
+
+					@include font-size(18);
+				}
+
+				td {
+
+					@include font-size(16);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav {
+			border: 1px solid rgba($border-color, 0.5);
+			float: left;
+			width: 100%;
+
+			@media screen and (min-width: 38.75em) {
+				width: 300px;
+			}
+
+			select,
+			input {
+				border: 0;
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 0;
+				width: 60%;
+			}
+
+			input {
+				float: right;
+				font-family: inherit;
+				line-height: 20px;
+				width: 40%;
+			}
+
+		} // close .notifications-options-nav
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			.bp-widget {
+
+				h2 {
+					background: lighten($dark-background, 10%);
+					color: #fff;
+					margin-bottom: 0;
+					padding: 0.4em;
+				}
+
+				table {
+					margin-top: 0;
+				}
+			}
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+					padding: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: $dark-background;
+					color: #fff;
+					padding: 0.2em 0.5em;
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+	} // close #buddyPress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#groups-list {
+
+			li {
+
+				.item {
+
+					@media screen and (min-width: 77.5em) {
+						left: 5%;
+						width: 50%;
+					}
+				}
+			}
+		}
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: lighten($dark-background, 10%);
+				}
+			}
+
+			tr {
+
+				td {
+					background: $content-background;
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom-color: $medium-background;
+					border-bottom-width: 2px;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+
+						span.icon:before {
+							font-size: 14px;
+						}
+					}
+				}
+
+				.thread-checkbox {
+					padding: 0 0 0 0.3em;
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 3em;
+
+					@media screen and (max-width: 38.75em) {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+
+					.thread-excerpt {
+
+						@include font-size(14);
+					}
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 2.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@media screen and (max-width: 38.75em) {
+						clear: both;
+
+						@include font-size(12);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					background: $unread;
+					border-color: $border-color;
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+			margin: 0;
+			padding: 0;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		.sitewide-notices {
+
+			tr {
+
+				td:first-child {
+					display: none;
+				}
+			}
+
+			td {
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					padding-left: 0.2em;
+				}
+			}
+		}
+	} // close #buddypress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		ul {
+			list-style: none;
+			margin: 0;
+			padding: 0;
+		}
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+			padding: 0 $spacing-val-sm;
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+		// Profile table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #buddyPress
+
+} // close .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+// Attempt to reset form control widths
+#main {
+
+	#buddypress {
+
+		.standard-form {
+
+			li {
+				float: none;
+			}
+
+			input[type="text"],
+			input[type="email"],
+			input[type="password"],
+			textarea {
+				width: 100%;
+			}
+		}
+	}
+}
+
+
+#buddypress {
+
+	// Match BP form elements to Themes(2013) styling where styles
+	// are not inherited or being overriden.
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.5);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	.standard-form {
+
+		// Keep BP form element borders to 1px rather than 2013 2px
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		button,
+		select,
+		textarea {
+			border-color: rgba($border-color, 0.5);
+			border-width: 1px;
+		}
+
+	} // close .standard-form
+
+	#signup_form.standard-form {
+
+		div.submit {
+			float: none;
+
+			input {
+				margin-right: 0;
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		// Stylise the seach form elements, in part this deals with padding
+		// issues on the submit & sizing issues between bp styles & twentythirteen
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.6);
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 60%;
+
+				@media screen and (min-width: 46.25em) {
+					width: 80%;
+				}
+			}
+
+			input[type="text"] {
+				float: left;
+				margin: 0;
+				width: 100%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+
+				@include font-size(14);
+				border: 0;
+				line-height: inherit;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.6);
+				padding: 0.2em 0 0.2em 0.2em;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0.2em 1em;
+				text-align: center;
+				text-transform: none;
+				width: 40%;
+
+				@media screen and (min-width: 46.25em) {
+					width: 20%;
+				}
+
+			}
+		} // close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+	@media screen and (min-width: 38.75em) {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+			float: right;
+			margin-bottom: 5px !important;
+
+			form {
+
+				label,
+				input[type="text"],
+				input[type="submit"] {
+					width: auto;
+				}
+			}
+		}
+
+	} // close @media
+
+
+	@media screen and (min-width: 77.5em) {
+
+		.dir-search,
+		.message-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16);
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16);
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+
+// Adjust table font sizes, default too large proportionally
+// This approach will require refinement and perhaps re-location to
+// a more general typography section to manage BP elements grouped under
+// breakpoints. Provide top/bottom margins for tables, lacking in BP styles
+
+#buddypress {
+
+	table {
+
+		@include font-size(14);
+		margin: $spacing-val-md 0;
+
+		tr th {
+			background: lighten($dark-background, 10%);
+			border-color: darken($border-light, 20%);
+			color: $content-background;
+		}
+
+		// twentythirteen removes left padding, replace!
+		th,
+		td {
+			padding: 0.3em;
+		}
+
+		// Reduce the themes inherited paragraph margins in tables
+		p {
+			margin-bottom: 0.5em;
+		}
+	}
+
+	@media screen and (min-width: 55em) {
+
+		table {
+
+			@include font-size(16);
+		}
+	}
+}
+
+/*__ User Account tables __*/
+
+#buddypress {
+
+	// Manage some table cells widths that are disproportionate to their content
+	.notifications,
+	.messages-notices {
+
+		th {
+			width: 30%;
+
+			&.bulk-select-all {
+				text-align: center;
+				width: 10%;
+			}
+		}
+
+		.bulk-select-check,
+		.thread-star {
+			text-align: center;
+		}
+
+		.notification-actions,
+		td.thread-options {
+			text-align: center;
+
+			a {
+				display: inline-block;
+				margin: 0;
+				padding: 0;
+			}
+		}
+
+		// Notices action buttons, this maybe better moved, temp for now to address
+		// styling issues - this will need styling ideas
+		td {
+
+			.button {
+				border: 0;
+				display: block;
+				padding: 0;
+				text-align: center;
+			}
+		}
+
+	} // .notifications, .message-notices
+
+} // #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning' )
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve-rtl.css	(revision 12265)
@@ -0,0 +1,2000 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentytwelve theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentytwelve this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a:focus, #buddypress div.item-list-tabs ul li a:hover {
+	color: #555;
+}
+
+#buddypress div.item-list-tabs ul li a:visited {
+	color: #21759b;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 30em) {
+	.bp-user #buddypress #object-nav {
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 10px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:focus:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:focus ul {
+		height: 320px;
+		opacity: 1;
+		overflow-y: auto;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 37.5em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f2f2f2;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #object-nav ul li {
+		float: right;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 1px solid #d4d0ba;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	float: left;
+	margin-top: 0;
+	padding: 5px 0 5px;
+	text-align: left;
+	width: 230px;
+}
+
+@media screen and (max-width: 30em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		float: none;
+		width: auto;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select option {
+	font-style: normal;
+}
+
+.bp-user #buddypress div#subnav.item-list-tabs li.last {
+	margin-right: 0;
+	text-align: left;
+}
+
+@media screen and (max-width: 30em) {
+	.bp-user #buddypress div#subnav.item-list-tabs li.last {
+		clear: right;
+		float: none;
+		margin-right: 10px;
+		text-align: right;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 60em) {
+	body.full-width #buddypress #item-header,
+	body.full-width #buddypress #item-body {
+		background: #fff;
+	}
+	body.full-width #buddypress #object-nav {
+		border-left: 1px solid #ddd;
+		float: right;
+		margin-left: -1px;
+		width: 200px;
+	}
+	body.full-width #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.full-width #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body.full-width #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	body.full-width #buddypress #item-body {
+		border-right: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 24px 0 0;
+		width: auto;
+	}
+	body.full-width #buddypress #item-body #subnav {
+		margin: 0 -24px 0 0;
+	}
+	body.full-width #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: right;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #141412;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	right: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress ul.item-list {
+	border-top: 0;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: right;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-right: 0;
+	overflow: hidden;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item {
+		margin-right: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item .item-meta {
+	text-align: center;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item .item-meta {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-right: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 16px;
+	font-size: 1.1428571429rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: right;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title > a {
+	text-decoration: none;
+}
+
+#buddypress ul.item-list li .item .item-title > a:visited {
+	color: #21759b;
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+	padding: 10px 0;
+	text-align: right;
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: right;
+	}
+	#buddypress ul.item-list li .item {
+		right: 5%;
+		margin-right: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 18px;
+		font-size: 1.2857142857rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: right;
+	float: none;
+	margin-bottom: -24px;
+	margin-right: 0;
+	padding: 24px 0 5px;
+	position: relative;
+	text-align: right;
+	top: 0;
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: left;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 0 10px 10px;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li div.action div {
+		clear: left;
+		float: left;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: right;
+		margin: 10px 0 24px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: right;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-right: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: left;
+	margin: 0;
+	min-height: 1.5em;
+	padding: 0 0.4em 0 0;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: left;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit input {
+		padding: 2px 6px;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 48px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 24px;
+	margin-right: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content,
+#buddypress #activity-stream li .activity-comments {
+	margin-right: 0;
+}
+
+#buddypress #activity-stream li .activity-header {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+#buddypress #activity-stream li .activity-header a:visited {
+	color: #21759b;
+}
+
+#buddypress #activity-stream li .activity-inner img {
+	height: auto;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: right;
+		margin-left: 10px;
+		text-align: right;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+	#buddypress #activity-stream li .activity-comments {
+		margin-right: 70px;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-right: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-meta a {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-left: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	font-size: 14px;
+	font-size: 1rem;
+	margin-bottom: 5px;
+	padding: 0.2em 0.5em;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		float: right;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 24px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.1428571429rem;
+	margin-bottom: 48px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.4285714286rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 24px;
+}
+
+.activity-permalink #buddypress #activity-stream .activity-comments {
+	margin-right: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	position: relative;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 10px 2px 0 0;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li:hover * {
+	color: #555;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li .acomment-meta,
+#buddypress #activity-stream .activity-comments > ul > li .acomment-content {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+}
+
+#buddypress #activity-stream .activity-comments a {
+	color: #737373;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	border: 1px solid #d4d0ba;
+	box-sizing: border-box;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(20, 20, 18, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 60em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: right;
+	}
+	#buddypress #members-list li .action {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: left;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress a:visited {
+	color: #21759b;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 1rem;
+	text-align: right;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-left: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 24px;
+	}
+}
+
+@media screen and (max-width: 50em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 48px;
+}
+
+.single-item.groups #buddypress div#item-header #item-header-content {
+	box-sizing: border-box;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0 !important;
+	width: 100%;
+}
+
+@media screen and (min-width: 50em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-right: 1px solid #eaeaea;
+		clear: none;
+		float: left;
+		padding-right: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-right: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 1rem;
+	padding: 0.2em;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions #group-admins li,
+.single-item.groups #buddypress div#item-header div#item-actions #group-mods li {
+	margin: 0;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: right;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-right: 2%;
+		padding: 0 0.5em;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: left;
+		width: 28%;
+	}
+}
+
+@media screen and (min-width: 64em) {
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+	margin-top: 225px !important;
+}
+
+@media screen and (min-width: 50em) and (max-width: 60em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		max-width: 60% !important;
+		width: 60% !important;
+	}
+}
+
+@media screen and (max-width: 64em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+		border: 0;
+		clear: both;
+		margin-top: 0 !important;
+		max-width: 100% !important;
+		padding-top: 0 !important;
+		width: auto;
+	}
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 24px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-content #item-buttons .generic-button {
+	margin-left: 5px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: right;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: left;
+		margin-left: 0%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 1rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(20, 20, 18, 0.6);
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 24px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: right;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: right;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 37.5em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 24px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: left;
+	margin: 5px 0 0 5px;
+}
+
+@media screen and (max-width: 37.5em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 13px;
+	font-size: 0.9285714286rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+}
+
+@media screen and (min-width: 60em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: right;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 40%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 1rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: right;
+	margin-left: 0;
+	width: 49%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: right;
+	font-family: inherit;
+	line-height: 20px;
+	margin-right: 1%;
+	width: 50%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus, .bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile {
+	/* Edit profile */
+}
+
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-right: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: right;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.2857142857rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle {
+	margin: 5px 0 24px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #6f6e6e;
+	border-radius: 2px;
+	background-clip: padding-box;
+	color: #f1f1f1;
+	font-weight: 700;
+	padding: 0.1em 0.5em;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:focus, .bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:hover,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:focus,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:hover {
+	color: #fff;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		right: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0.2em 0.3em 0;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread #message-recipients .highlight {
+	font-size: 100%;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: right;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: left;
+	margin-left: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	box-sizing: border-box;
+	display: inline-block;
+	float: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom: 1px solid #ccc;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star,
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-unstar {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star span.icon:before {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-right: 0 !important;
+	width: calc(100% - 30px);
+	margin-right: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-right: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: left;
+	line-height: 2;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.7857142857rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	border-color: #eaeaea;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-left: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress #send_message_form input,
+.bp-user #buddypress #send_message_form textarea {
+	box-sizing: border-box;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-right: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-left: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-right: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices tr {
+	margin: 3em 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-right: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a.button {
+	border: 1px solid #d4d0ba;
+	line-height: initial;
+	padding: 0.4em 0.3em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	line-height: 1;
+	padding-bottom: 1em;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-right: 10px;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+.bp-user .ac_results li img {
+	vertical-align: bottom;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.4285714286rem;
+	margin: 24px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-left: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: right;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 1rem;
+	border: 0;
+	border-radius: 0;
+	line-height: 1.8;
+	min-height: 2rem;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-left: 1px solid rgba(212, 208, 186, 0.6);
+	float: right;
+	margin: 0;
+	padding: 0 0.2em;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: left;
+	font-weight: 400;
+	padding: 0 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: left;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 75em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress table {
+	font-size: 14px;
+	font-size: 1rem;
+	margin: 24px 0;
+}
+
+.bp-user #buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+	padding: 0 8px;
+}
+
+.bp-user #buddypress table tr td {
+	padding: 8px;
+}
+
+.bp-user #buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+}
+
+.bp-user #buddypress .notifications th,
+.bp-user #buddypress .messages-notices th {
+	width: 30%;
+}
+
+.bp-user #buddypress .notifications th.bulk-select-all,
+.bp-user #buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+.bp-user #buddypress .notifications th.actions,
+.bp-user #buddypress .messages-notices th.actions {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .bulk-select-check,
+.bp-user #buddypress .notifications .thread-star,
+.bp-user #buddypress .messages-notices .bulk-select-check,
+.bp-user #buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions,
+.bp-user #buddypress .notifications td.thread-options,
+.bp-user #buddypress .messages-notices .notification-actions,
+.bp-user #buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions a,
+.bp-user #buddypress .notifications td.thread-options a,
+.bp-user #buddypress .messages-notices .notification-actions a,
+.bp-user #buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications td .button,
+.bp-user #buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.2857142857rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve.css	(revision 12265)
@@ -0,0 +1,2000 @@
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentytwelve theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentytwelve this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+.buddypress div.clear {
+	display: none;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+#buddypress div.item-list-tabs ul li.selected a,
+#buddypress div.item-list-tabs ul li.current a {
+	background: inherit;
+	opacity: 1;
+}
+
+#buddypress div.item-list-tabs ul {
+	overflow: hidden;
+	padding: 0;
+}
+
+#buddypress div.item-list-tabs ul li a:focus, #buddypress div.item-list-tabs ul li a:hover {
+	color: #555;
+}
+
+#buddypress div.item-list-tabs ul li a:visited {
+	color: #21759b;
+}
+
+#buddypress div.item-list-tabs ul li a span {
+	border-radius: 25%;
+}
+
+@media screen and (max-width: 30em) {
+	.bp-user #buddypress #object-nav {
+		border: 1px solid #eaeaea;
+		overflow: visible;
+		padding: 10px;
+	}
+	.bp-user #buddypress #object-nav ul {
+		border: 0;
+		height: 0;
+		transition: height 0.3s ease-in-out 0.7s;
+		visibility: hidden;
+	}
+	.bp-user #buddypress #object-nav:before {
+		content: "Menu \021E9";
+		display: inline;
+		opacity: 0.7;
+	}
+	.bp-user #buddypress #object-nav:hover:before, .bp-user #buddypress #object-nav:focus:before {
+		content: "Menu \021E7";
+	}
+	.bp-user #buddypress #object-nav:hover ul, .bp-user #buddypress #object-nav:focus ul {
+		height: 320px;
+		opacity: 1;
+		overflow-y: auto;
+		visibility: visible;
+	}
+	.bp-user #buddypress #subnav {
+		clear: both;
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress #object-nav ul {
+	overflow: hidden;
+}
+
+#buddypress #object-nav ul li {
+	float: none;
+}
+
+@media screen and (max-width: 37.5em) {
+	#buddypress #object-nav ul li:not(:last-child) {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	#buddypress #object-nav ul li:not(.selected):focus, #buddypress #object-nav ul li:not(.selected):hover {
+		background: #f2f2f2;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #object-nav ul li {
+		float: left;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs {
+	margin-top: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul {
+	border-bottom: 0;
+	border-top: 1px solid #d4d0ba;
+	margin-top: 5px;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last {
+	background: #fff;
+	float: right;
+	margin-top: 0;
+	padding: 5px 0 5px;
+	text-align: right;
+	width: 230px;
+}
+
+@media screen and (max-width: 30em) {
+	#buddypress div#subnav.item-list-tabs ul li.last {
+		float: none;
+		width: auto;
+	}
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last select:focus {
+	background: transparent;
+	border: 0;
+	outline: 0;
+	padding: 0;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select,
+#buddypress div#subnav.item-list-tabs ul li.last label,
+#buddypress div#subnav.item-list-tabs ul li.last option {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select {
+	font-style: italic;
+}
+
+#buddypress div#subnav.item-list-tabs ul li.last select option {
+	font-style: normal;
+}
+
+.bp-user #buddypress div#subnav.item-list-tabs li.last {
+	margin-left: 0;
+	text-align: right;
+}
+
+@media screen and (max-width: 30em) {
+	.bp-user #buddypress div#subnav.item-list-tabs li.last {
+		clear: left;
+		float: none;
+		margin-left: 10px;
+		text-align: left;
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+@media screen and (min-width: 60em) {
+	body.full-width #buddypress #item-header,
+	body.full-width #buddypress #item-body {
+		background: #fff;
+	}
+	body.full-width #buddypress #object-nav {
+		border-right: 1px solid #ddd;
+		float: left;
+		margin-right: -1px;
+		width: 200px;
+	}
+	body.full-width #buddypress #object-nav ul {
+		background: none;
+		border-bottom: 0;
+		padding: 0;
+	}
+	body.full-width #buddypress #object-nav ul li {
+		float: none;
+		overflow: hidden;
+	}
+	body.full-width #buddypress #object-nav ul li span {
+		background: #fff;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	body.full-width #buddypress #item-body {
+		border-left: 1px solid #ddd;
+		overflow: hidden;
+		padding: 0 0 0 24px;
+		width: auto;
+	}
+	body.full-width #buddypress #item-body #subnav {
+		margin: 0 0 0 -24px;
+	}
+	body.full-width #buddypress #item-body #subnav ul {
+		margin-top: 0;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	#main #buddypress #group-create-tabs.item-list-tabs ul:before,
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		content: " ";
+		display: table;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul:after {
+		clear: both;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul {
+		background: none;
+		border: 0;
+		border-bottom: 1px solid #ddd;
+		overflow: visible;
+		padding-bottom: 0;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li {
+		float: left;
+		width: auto;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current {
+		border: 1px solid #ddd;
+		border-bottom-color: #fff;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		background-clip: padding-box;
+		margin-bottom: -1px;
+	}
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.selected a,
+	#main #buddypress #group-create-tabs.item-list-tabs ul li.current a {
+		background: none;
+		color: #141412;
+		outline: none;
+	}
+	#main #buddypress #subnav ul {
+		border-bottom: 0;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+#buddypress div.pagination {
+	box-shadow: none;
+	min-height: 0;
+}
+
+#buddypress div.pagination:before, #buddypress div.pagination:after {
+	height: 0;
+	width: 0;
+}
+
+#buddypress div.pagination .pag-count {
+	margin-left: 0;
+}
+
+#buddypress div.pagination .pagination-links {
+	margin-right: 0;
+}
+
+#buddypress div.pagination .pagination-links span,
+#buddypress div.pagination .pagination-links a {
+	height: auto;
+	line-height: 1;
+	padding: 5px;
+}
+
+#buddypress div.pagination .pagination-links .prev,
+#buddypress div.pagination .pagination-links .next {
+	background-color: transparent;
+	color: inherit;
+	overflow: visible;
+	width: auto;
+}
+
+#buddypress div.pagination .pagination-links .prev:before,
+#buddypress div.pagination .pagination-links .next:before {
+	display: none;
+}
+
+#buddypress div.pagination .pagination-links .prev {
+	left: auto;
+	position: static;
+}
+
+#buddypress div.pagination .pagination-links .next {
+	position: static;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+#buddypress ul.item-list {
+	border-top: 0;
+}
+
+#buddypress ul.item-list li {
+	overflow: hidden !important;
+}
+
+#buddypress ul.item-list li .item-avatar {
+	margin-bottom: 10px;
+	text-align: center;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li .item-avatar {
+		margin-bottom: 0;
+	}
+}
+
+#buddypress ul.item-list li .item-avatar a {
+	border-bottom: 0;
+}
+
+#buddypress ul.item-list li .item-avatar img.avatar {
+	display: inline-block;
+	float: none;
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li .item-avatar img.avatar {
+		display: block;
+		float: left;
+	}
+}
+
+#buddypress ul.item-list li .item {
+	margin-left: 0;
+	overflow: hidden;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item {
+		margin-left: 15%;
+	}
+}
+
+#buddypress ul.item-list li .item .item-meta {
+	text-align: center;
+}
+
+@media screen and (min-width: 61.5625em) {
+	#buddypress ul.item-list li .item .item-meta {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item span.activity {
+	font-style: italic;
+}
+
+#buddypress ul.item-list li .item .item-desc {
+	margin-left: 0;
+	width: 94%;
+}
+
+#buddypress ul.item-list li .item .item-title {
+	font-size: 16px;
+	font-size: 1.1428571429rem;
+	line-height: 1.2;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li .item .item-title {
+		text-align: left;
+	}
+}
+
+#buddypress ul.item-list li .item .item-title > a {
+	text-decoration: none;
+}
+
+#buddypress ul.item-list li .item .item-title > a:visited {
+	color: #21759b;
+}
+
+#buddypress ul.item-list li .item .item-title .update {
+	display: block;
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+	padding: 10px 0;
+	text-align: left;
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li .item .item-title .update {
+		font-size: 14px;
+		font-size: 1rem;
+	}
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li .item-avatar,
+	#buddypress ul.item-list li .item,
+	#buddypress ul.item-list li .action {
+		float: left;
+	}
+	#buddypress ul.item-list li .item {
+		left: 5%;
+		margin-left: 0;
+		position: relative;
+		width: 55%;
+	}
+	#buddypress ul.item-list li .item .item-title {
+		font-size: 18px;
+		font-size: 1.2857142857rem;
+	}
+}
+
+#buddypress ul.item-list li div.action {
+	clear: left;
+	float: none;
+	margin-bottom: -24px;
+	margin-left: 0;
+	padding: 24px 0 5px;
+	position: relative;
+	text-align: left;
+	top: 0;
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li div.action {
+		clear: none;
+		float: right;
+		margin-bottom: 0;
+		padding: 0;
+	}
+}
+
+#buddypress ul.item-list li div.action div {
+	display: inline-block;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress ul.item-list li div.action div a {
+	display: block;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress ul.item-list li div.action div {
+		margin: 0 10px 10px 0;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 60em) {
+	#buddypress ul.item-list li div.action div {
+		clear: right;
+		float: right;
+		margin: 0 0 10px 0;
+	}
+}
+
+#buddypress ul.item-list li div.action .meta {
+	font-style: italic;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+#buddypress form#whats-new-form p.activity-greeting {
+	line-height: 1.4;
+}
+
+@media screen and (max-width: 46.25em) {
+	#buddypress form#whats-new-form #whats-new-content {
+		clear: left;
+		margin: 10px 0 24px;
+		padding: 10px 0 0;
+	}
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+	border: 1px solid rgba(212, 208, 186, 0.5);
+	float: left;
+	line-height: 1.5;
+	margin-top: 12px;
+	padding-left: 0.2em;
+	width: 100%;
+}
+
+#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box select {
+	background: none;
+	border: 0;
+	float: right;
+	margin: 0;
+	min-height: 1.5em;
+	padding: 0 0 0 0.4em;
+}
+
+@media screen and (min-width: 30em) {
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-post-in-box {
+		width: auto;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit {
+		float: right;
+	}
+	#buddypress form#whats-new-form #whats-new-content.active #whats-new-options[style] #whats-new-submit input {
+		padding: 2px 6px;
+	}
+}
+
+#buddypress #item-body form#whats-new-form {
+	margin: 48px 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream li {
+	padding: 25px 0 15px;
+}
+
+#buddypress #activity-stream li .activity-avatar {
+	float: none;
+	text-align: center;
+}
+
+#buddypress #activity-stream li .activity-avatar a {
+	display: inline-block;
+}
+
+#buddypress #activity-stream li .activity-avatar a img.avatar {
+	display: inline;
+	float: none;
+	height: 60px;
+	margin-bottom: 24px;
+	margin-left: 0;
+	width: 60px;
+}
+
+#buddypress #activity-stream li .activity-content,
+#buddypress #activity-stream li .activity-comments {
+	margin-left: 0;
+}
+
+#buddypress #activity-stream li .activity-header {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+#buddypress #activity-stream li .activity-header a:visited {
+	color: #21759b;
+}
+
+#buddypress #activity-stream li .activity-inner img {
+	height: auto;
+}
+
+@media screen and (min-width: 46.25em) {
+	#buddypress #activity-stream li .activity-avatar {
+		float: left;
+		margin-right: 10px;
+		text-align: left;
+	}
+	#buddypress #activity-stream li .activity-avatar a {
+		border-bottom: 0;
+	}
+	#buddypress #activity-stream li .activity-content {
+		margin: 0;
+		overflow: hidden;
+	}
+	#buddypress #activity-stream li .activity-content .activity-header {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+	#buddypress #activity-stream li .activity-comments {
+		margin-left: 70px;
+	}
+}
+
+#buddypress #activity-stream li.mini .activity-avatar a img.avatar {
+	height: 30px;
+	margin-left: 15px;
+	width: 30px;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-header {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+#buddypress #activity-stream li.mini .activity-content .activity-meta a {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+}
+
+#buddypress #activity-stream .activity-content {
+	margin-top: -12px;
+}
+
+#buddypress #activity-stream .activity-content .activity-header {
+	line-height: inherit;
+	margin-right: 0;
+}
+
+#buddypress #activity-stream .activity-content .activity-header p {
+	border-bottom: 1px solid rgba(234, 234, 234, 0.6);
+	margin-top: 0;
+	padding: 0 0.2em;
+}
+
+#buddypress #activity-stream .activity-content .activity-header img.avatar {
+	display: inline-block;
+	margin-bottom: 0 !important;
+}
+
+#buddypress #activity-stream .activity-content .activity-meta a {
+	display: block;
+	font-size: 14px;
+	font-size: 1rem;
+	margin-bottom: 5px;
+	padding: 0.2em 0.5em;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #activity-stream .activity-content .activity-meta a {
+		float: left;
+		margin-bottom: 0;
+	}
+}
+
+#buddypress #activity-stream .load-more {
+	background: #f7f7f7;
+	border: 1px solid transparent;
+	padding: 10px;
+}
+
+#buddypress #activity-stream .load-more:focus, #buddypress #activity-stream .load-more:hover {
+	background: #f4f4f4;
+	border: 1px solid rgba(159, 209, 226, 0.3);
+}
+
+#buddypress #activity-stream .load-more a {
+	display: block;
+}
+
+/* Single activity view  - activity permalink */
+.activity-permalink #buddypress #activity-stream li.activity-item {
+	padding: 24px;
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+	font-size: 16px;
+	font-size: 1.1428571429rem;
+	margin-bottom: 48px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.activity-permalink #buddypress #activity-stream li.mini .activity-header {
+		font-size: 20px;
+		font-size: 1.4285714286rem;
+	}
+}
+
+.activity-permalink #buddypress #activity-stream li.mini .activity-header p {
+	padding: 24px;
+}
+
+.activity-permalink #buddypress #activity-stream .activity-comments {
+	margin-left: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress #activity-stream .activity-comments {
+	position: relative;
+}
+
+#buddypress #activity-stream .activity-comments > ul {
+	background: rgba(247, 247, 247, 0.6);
+	margin: 10px 0 0 2px;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li:hover * {
+	color: #555;
+}
+
+#buddypress #activity-stream .activity-comments > ul > li .acomment-meta,
+#buddypress #activity-stream .activity-comments > ul > li .acomment-content {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+}
+
+#buddypress #activity-stream .activity-comments a {
+	color: #737373;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form {
+	border: 1px solid #d4d0ba;
+	box-sizing: border-box;
+	margin: 10px 0;
+	width: 100%;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form input[type="submit"],
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	color: rgba(20, 20, 18, 0.8);
+	display: inline-block;
+	font-family: inherit;
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+	font-weight: 400;
+	line-height: 1.2;
+	padding: 4px 10px;
+	text-transform: lowercase;
+	width: 100px;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel {
+	border: 1px solid rgba(212, 208, 186, 0.7);
+	text-align: center;
+}
+
+#buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:focus, #buddypress #activity-stream .activity-comments .ac-form .ac-reply-cancel:hover {
+	background: #ededed;
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 60em) {
+	#buddypress #members-list li .item-avatar,
+	#buddypress #members-list li .item {
+		float: left;
+	}
+	#buddypress #members-list li .action {
+		float: right;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+#buddypress #signup_form.standard-form #profile-details-section,
+#buddypress #signup_form.standard-form #basic-details-section,
+#buddypress #signup_form.standard-form #blog-details-section {
+	float: none;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section,
+	#buddypress #signup_form.standard-form #basic-details-section,
+	#buddypress #signup_form.standard-form #blog-details-section {
+		width: 48%;
+	}
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress #signup_form.standard-form #profile-details-section {
+		float: right;
+	}
+	#buddypress #signup_form.standard-form #basic-details-section {
+		float: left;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress a:visited {
+	color: #21759b;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.bp-user #buddypress #item-header-content #item-meta,
+.single-item.groups #buddypress #item-header-content #item-meta {
+	font-size: 14px;
+	font-size: 1rem;
+	text-align: left;
+}
+
+.bp-user #buddypress #item-header-content #item-meta p,
+.single-item.groups #buddypress #item-header-content #item-meta p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user h1,
+	.bp-user #item-header-content,
+	.single-item.groups h1,
+	.single-item.groups #item-header-content {
+		text-align: center;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user main header.entry-header,
+	.single-item.groups main header.entry-header {
+		padding-bottom: 1rem;
+	}
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress h1,
+	.single-item.groups #buddypress h1 {
+		margin-bottom: 0;
+	}
+	.bp-user #buddypress #item-header-avatar img.avatar,
+	.single-item.groups #buddypress #item-header-avatar img.avatar {
+		margin-right: 0;
+	}
+	.bp-user #buddypress #item-header-content,
+	.single-item.groups #buddypress #item-header-content {
+		width: 100%;
+	}
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #item-header .generic-button,
+	.single-item.groups #buddypress #item-header .generic-button {
+		float: none;
+		margin: 1.5em 0 0;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+@media screen and (max-width: 46.25em) {
+	.single-item.groups #buddypress #item-header #item-meta {
+		margin-bottom: 24px;
+	}
+}
+
+@media screen and (max-width: 50em) {
+	.single-item.groups #buddypress div#item-header {
+		display: flex;
+		flex-direction: column;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar {
+		order: 1;
+		text-align: center;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a {
+		border-bottom: 0;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-avatar a img {
+		display: inline-block;
+		float: none;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		order: 2;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions {
+		order: 3;
+	}
+	.single-item.groups #buddypress div#item-header #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+		text-align: center;
+	}
+}
+
+.single-item.groups #buddypress div#item-header {
+	padding-bottom: 48px;
+}
+
+.single-item.groups #buddypress div#item-header #item-header-content {
+	box-sizing: border-box;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions {
+	margin: 0 !important;
+	width: 100%;
+}
+
+@media screen and (min-width: 50em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		border-left: 1px solid #eaeaea;
+		clear: none;
+		float: right;
+		padding-left: 0.2em;
+		width: 30%;
+	}
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions ul {
+	margin-top: 0;
+	padding-left: 0;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions h2 {
+	font-size: 14px;
+	font-size: 1rem;
+	padding: 0.2em;
+}
+
+.single-item.groups #buddypress div#item-header div#item-actions #group-admins li,
+.single-item.groups #buddypress div#item-header div#item-actions #group-mods li {
+	margin: 0;
+}
+
+@media screen and (min-width: 46.25em) {
+	.single-item.groups #buddypress div#item-header #item-header-avatar,
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		float: left;
+	}
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		margin-left: 2%;
+		padding: 0 0.5em;
+	}
+	.single-item.groups #buddypress div#item-header div#item-actions {
+		float: right;
+		width: 28%;
+	}
+}
+
+@media screen and (min-width: 64em) {
+	.single-item.groups #buddypress div#item-header #item-header-content {
+		width: 40%;
+	}
+}
+
+.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+	margin-top: 225px !important;
+}
+
+@media screen and (min-width: 50em) and (max-width: 60em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-header-content {
+		max-width: 60% !important;
+		width: 60% !important;
+	}
+}
+
+@media screen and (max-width: 64em) {
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions {
+		border: 0;
+		clear: both;
+		margin-top: 0 !important;
+		max-width: 100% !important;
+		padding-top: 0 !important;
+		width: auto;
+	}
+	.single-item.groups #buddypress #cover-image-container #item-header-cover-image #item-actions h2 {
+		border-bottom: 1px solid #eaeaea;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user #buddypress #item-header {
+	padding: 24px 0;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar {
+	text-align: center;
+}
+
+.bp-user #buddypress #item-header #item-header-avatar img.avatar,
+.bp-user #buddypress #item-header #item-header-avatar a {
+	border-bottom: 0;
+	display: inline-block;
+	float: none;
+}
+
+.bp-user #buddypress #item-header #item-header-content #item-buttons .generic-button {
+	margin-right: 5px;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #item-header #item-header-avatar {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-avatar a {
+		float: left;
+	}
+	.bp-user #buddypress #item-header #item-header-content {
+		float: right;
+		margin-right: 0%;
+		width: 69%;
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+.groups #group-settings-form h3 {
+	background: #555;
+	color: #fff;
+	padding: 0.2em;
+}
+
+.groups.edit-details #group-settings-form label {
+	margin-bottom: 0;
+	padding: 0.2em;
+	width: 80%;
+}
+
+.groups.edit-details #group-settings-form textarea + p label {
+	background: none;
+	color: inherit;
+	font-size: 14px;
+	font-size: 1rem;
+	width: auto;
+}
+
+.groups.edit-details #group-settings-form textarea {
+	height: auto;
+	min-height: 100px;
+	overflow: auto;
+}
+
+.groups.group-settings #group-settings-form div.radio label {
+	border: 1px solid #eaeaea;
+	padding: 0.2em;
+}
+
+.groups.group-settings #group-settings-form div.radio label ul {
+	color: rgba(20, 20, 18, 0.6);
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+.groups.group-avatar form > p {
+	margin-top: 24px;
+}
+
+.groups.manage-members #group-settings-form .item-list li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.groups.manage-members #group-settings-form .item-list li img,
+.groups.manage-members #group-settings-form .item-list li h5 {
+	float: left;
+}
+
+.groups.manage-members #group-settings-form .item-list li img > a,
+.groups.manage-members #group-settings-form .item-list li h5 > a {
+	border-bottom: 0;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small {
+	clear: left;
+	display: block;
+	float: none;
+	margin-top: 10px;
+}
+
+.groups.manage-members #group-settings-form .item-list li span.small a {
+	display: inline-block;
+	margin: 5px 0;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.groups.manage-members #group-settings-form .item-list li span.small a {
+		width: auto;
+	}
+}
+
+.groups.manage-members #group-settings-form .item-list li h5 {
+	margin: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+.groups.group-members #subnav li {
+	width: 100%;
+}
+
+@media screen and (max-width: 37.5em) {
+	.groups.group-members #subnav li {
+		background: #fff;
+		padding: 24px 0;
+	}
+}
+
+.groups.group-members #subnav li #search-members-form {
+	float: right;
+	margin: 5px 5px 0 0;
+}
+
+@media screen and (max-width: 37.5em) {
+	.groups.group-members #subnav li #search-members-form {
+		margin: 0;
+		width: 100%;
+	}
+	.groups.group-members #subnav li #search-members-form label input[type="text"] {
+		width: 100%;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+.bp-user .entry-title {
+	margin-bottom: 0.5em;
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user #buddypress table th {
+	font-size: 13px;
+	font-size: 0.9285714286rem;
+}
+
+.bp-user #buddypress table td {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+}
+
+@media screen and (min-width: 60em) {
+	.bp-user #buddypress table th {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+	.bp-user #buddypress table td {
+		font-size: 14px;
+		font-size: 1rem;
+	}
+}
+
+.bp-user #buddypress .pag-count {
+	font-style: italic;
+}
+
+.bp-user #buddypress .notifications-options-nav,
+.bp-user #buddypress .messages-options-nav {
+	float: left;
+	width: 100%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav,
+	.bp-user #buddypress .messages-options-nav {
+		width: 40%;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav select,
+.bp-user #buddypress .messages-options-nav input {
+	font-size: 14px;
+	font-size: 1rem;
+	outline: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications-options-nav select,
+.bp-user #buddypress .messages-options-nav select {
+	float: left;
+	margin-right: 0;
+	width: 49%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav select,
+	.bp-user #buddypress .messages-options-nav select {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input,
+.bp-user #buddypress .messages-options-nav input {
+	float: left;
+	font-family: inherit;
+	line-height: 20px;
+	margin-left: 1%;
+	width: 50%;
+}
+
+@media screen and (min-width: 37.5em) {
+	.bp-user #buddypress .notifications-options-nav input,
+	.bp-user #buddypress .messages-options-nav input {
+		width: auto;
+	}
+}
+
+.bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:focus, .bp-user #buddypress .notifications-options-nav input[disabled="disabled"]:hover,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:focus,
+.bp-user #buddypress .messages-options-nav input[disabled="disabled"]:hover {
+	background: none;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+.bp-user #buddypress .profile {
+	/* Edit profile */
+}
+
+.bp-user #buddypress .profile .bp-widget h2 {
+	background: #6f6e6e;
+	color: #fff;
+	margin-bottom: 0;
+	padding: 0.4em;
+}
+
+.bp-user #buddypress .profile .bp-widget table {
+	margin-top: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:before,
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	content: " ";
+	display: table;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .button-nav:after {
+	clear: both;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav {
+	border-bottom: 1px solid #eaeaea;
+	margin-left: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li {
+	float: left;
+	margin-bottom: 0;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav li.current {
+	border: 1px solid #eaeaea;
+	border-bottom-color: #fff;
+	margin-bottom: -1px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form ul.button-nav a {
+	background: none;
+	border: 0;
+	font-size: 18px;
+	font-size: 1.2857142857rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle {
+	margin: 5px 0 24px;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-toggle,
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link {
+	background: #6f6e6e;
+	border-radius: 2px;
+	background-clip: padding-box;
+	color: #f1f1f1;
+	font-weight: 700;
+	padding: 0.1em 0.5em;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:focus, .bp-user #buddypress .profile #profile-edit-form .field-visibility-settings-close:hover,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:focus,
+.bp-user #buddypress .profile #profile-edit-form .visibility-toggle-link:hover {
+	color: #fff;
+}
+
+.bp-user #buddypress .profile .bp-avatar #bp-delete-avatar a {
+	font-size: inherit;
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+@media screen and (min-width: 77.5em) {
+	.bp-user #buddypress #groups-list li .item {
+		left: 5%;
+		width: 50%;
+	}
+}
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+.bp-user #buddypress #message-thread a {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-thread #message-subject {
+	background: #6f6e6e;
+	color: #fff;
+	padding: 0.3em 0 0.3em 0.2em;
+}
+
+.bp-user #buddypress #message-thread #message-recipients {
+	font-style: italic;
+}
+
+.bp-user #buddypress #message-thread #message-recipients a.confirm {
+	border: 1px solid #eaeaea;
+	font-style: normal;
+}
+
+.bp-user #buddypress #message-thread #message-recipients .highlight {
+	font-size: 100%;
+}
+
+.bp-user #buddypress #message-thread .message-metadata:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-user #buddypress #message-thread .message-metadata img.avatar {
+	float: none;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress #message-thread .message-metadata img.avatar {
+		float: left;
+	}
+}
+
+.bp-user #buddypress #message-thread .message-metadata .message-star-actions {
+	float: right;
+	margin-right: 5px;
+	position: static;
+}
+
+.bp-user #buddypress #message-thread .message-content {
+	background: #f7f7f7;
+	border: 1px solid #eaeaea;
+	margin: 10px 0 0 0;
+	padding: 0.3em;
+}
+
+.bp-user #buddypress #message-thread #send-reply .message-content {
+	background: #fff;
+	border: 0;
+}
+
+.bp-user #buddypress #message-thread .alt {
+	background: #fff;
+}
+
+.bp-user #buddypress #message-threads thead tr {
+	background: #6f6e6e;
+}
+
+.bp-user #buddypress #message-threads tr td {
+	background: #fff;
+	box-sizing: border-box;
+	display: inline-block;
+	float: left;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-bottom: 1px solid #ccc;
+	height: 2.4em;
+	padding-bottom: 0.2em;
+	padding-top: 0.2em;
+}
+
+@media screen and (max-width: 46.25em) {
+	.bp-user #buddypress #message-threads tr td.thread-star,
+	.bp-user #buddypress #message-threads tr td.thread-options {
+		padding-top: 0;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-star,
+.bp-user #buddypress #message-threads tr td.thread-star .message-action-unstar {
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-star span.icon:before {
+	font-size: 14px;
+	font-size: 1rem;
+}
+
+.bp-user #buddypress #message-threads tr td.bulk-select-check,
+.bp-user #buddypress #message-threads tr td.thread-from {
+	height: 3em;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr td.bulk-select-check,
+	.bp-user #buddypress #message-threads tr td.thread-from {
+		height: 5.2em;
+	}
+}
+
+.bp-user #buddypress #message-threads tr td.thread-from,
+.bp-user #buddypress #message-threads tr td.thread-options {
+	border-left: 0 !important;
+	width: calc(100% - 30px);
+	margin-left: 0;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-info {
+	padding-left: 41px;
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options {
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads tr td.thread-options a {
+	font-size: 12px;
+	font-size: 0.8571428571rem;
+	line-height: 1.2;
+}
+
+.bp-user #buddypress #message-threads tr span.from {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads tr span.activity {
+	display: block;
+	float: right;
+	line-height: 2;
+}
+
+@media screen and (max-width: 37.5em) {
+	.bp-user #buddypress #message-threads tr span.activity {
+		clear: both;
+		font-size: 11px;
+		font-size: 0.7857142857rem;
+		width: 100%;
+	}
+}
+
+.bp-user #buddypress #message-threads tr.unread td {
+	border-color: #eaeaea;
+}
+
+.bp-user #buddypress #message-threads th {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all {
+	border-bottom: 0;
+	display: inline-block;
+	text-align: left;
+}
+
+.bp-user #buddypress #message-threads th.bulk-select-all,
+.bp-user #buddypress #message-threads td.bulk-select-check,
+.bp-user #buddypress #message-threads td.thread-star {
+	border-right: 0;
+	width: 30px;
+}
+
+.bp-user #buddypress #send_message_form input,
+.bp-user #buddypress #send_message_form textarea {
+	box-sizing: border-box;
+}
+
+.bp-user #buddypress .acfb-holder {
+	list-style: none;
+}
+
+.bp-user #buddypress .acfb-holder li {
+	margin-left: 0;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab {
+	background: #edf7fa;
+	border: inherit;
+	margin-right: 0;
+	padding: 0.5em;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p {
+	padding-left: 10px;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab span.p:focus, .bp-user #buddypress .acfb-holder li.friend-tab span.p:hover {
+	color: #c82b2b;
+	cursor: pointer;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a {
+	border-bottom: 0;
+	text-decoration: none;
+}
+
+.bp-user #buddypress .acfb-holder li.friend-tab a img {
+	display: inline;
+	height: 20px;
+	vertical-align: middle;
+	width: 20px !important;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices tr {
+	margin: 3em 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td {
+	width: 100%;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td strong {
+	background: #6f6e6e;
+	color: #fff;
+	display: block;
+	margin-bottom: 0.4em;
+	padding-left: 0.2em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a {
+	display: inline-block;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td a.button {
+	border: 1px solid #d4d0ba;
+	line-height: initial;
+	padding: 0.4em 0.3em;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child {
+	display: none;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:nth-child(2) strong {
+	margin: -8px -8px 8px;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td {
+	border-bottom: 0;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:first-child + td + td span {
+	line-height: 1;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child {
+	line-height: 1;
+	padding-bottom: 1em;
+	text-align: right;
+}
+
+.bp-user #buddypress #message-threads.sitewide-notices td:last-child a:last-child:after {
+	content: attr(title);
+	display: block;
+	line-height: initial;
+	text-indent: 0;
+}
+
+.bp-user .ac_results {
+	background: #eee;
+	padding-left: 10px;
+}
+
+.bp-user .ac_results li {
+	margin: 10px 0;
+}
+
+.bp-user .ac_results li:focus, .bp-user .ac_results li:hover {
+	cursor: pointer;
+}
+
+.bp-user .ac_results li img {
+	vertical-align: bottom;
+}
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+.bp-user #buddypress #settings-form > p {
+	font-size: 20px;
+	font-size: 1.4285714286rem;
+	margin: 24px 0 10px;
+}
+
+.bp-user #buddypress table.notification-settings td.yes,
+.bp-user #buddypress table.notification-settings td.no {
+	vertical-align: middle;
+}
+
+.bp-user #buddypress table.profile-settings {
+	width: 100%;
+}
+
+.bp-user #buddypress table.profile-settings th.field-group-name,
+.bp-user #buddypress table.profile-settings td.field-name {
+	width: 50%;
+}
+
+@media screen and (min-width: 46.25em) {
+	.bp-user #buddypress table.profile-settings th.field-group-name,
+	.bp-user #buddypress table.profile-settings td.field-name {
+		width: 70%;
+	}
+}
+
+.bp-user #buddypress table.profile-settings th.title,
+.bp-user #buddypress table.profile-settings td.field-visibility {
+	width: 30%;
+}
+
+.bp-user #buddypress table.profile-settings td.field-visibility select {
+	width: 100%;
+}
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+#main #buddypress .standard-form li {
+	float: none;
+}
+
+#main #buddypress .standard-form input[type="text"],
+#main #buddypress .standard-form input[type="email"],
+#main #buddypress .standard-form input[type="password"],
+#main #buddypress .standard-form textarea {
+	width: 100%;
+}
+
+#buddypress div.activity-comments form .ac-textarea {
+	background: #f7f7f7;
+	border: 1px solid rgba(212, 208, 186, 0.5);
+}
+
+#buddypress div.activity-comments form .ac-textarea textarea {
+	background: none;
+	border: 0;
+}
+
+#buddypress .standard-form input[type="text"],
+#buddypress .standard-form input[type="email"],
+#buddypress .standard-form input[type="password"],
+#buddypress .standard-form button,
+#buddypress .standard-form select,
+#buddypress .standard-form textarea {
+	border-color: rgba(212, 208, 186, 0.5);
+	border-width: 1px;
+}
+
+#buddypress #signup_form.standard-form div.submit {
+	float: none;
+}
+
+#buddypress #signup_form.standard-form div.submit input {
+	margin-right: 0;
+}
+
+#buddypress div.dir-search,
+#buddypress div.message-search,
+#buddypress li.groups-members-search {
+	float: none;
+	margin: 10px 0;
+}
+
+#buddypress div.dir-search form,
+#buddypress div.message-search form,
+#buddypress li.groups-members-search form {
+	border: 1px solid rgba(212, 208, 186, 0.6);
+	overflow: hidden;
+}
+
+#buddypress div.dir-search form label,
+#buddypress div.message-search form label,
+#buddypress li.groups-members-search form label {
+	float: left;
+	width: 70%;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="text"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	font-size: 14px;
+	font-size: 1rem;
+	border: 0;
+	border-radius: 0;
+	line-height: 1.8;
+	min-height: 2rem;
+}
+
+#buddypress div.dir-search form input[type="text"],
+#buddypress div.message-search form input[type="text"],
+#buddypress li.groups-members-search form input[type="text"] {
+	border-right: 1px solid rgba(212, 208, 186, 0.6);
+	float: left;
+	margin: 0;
+	padding: 0 0.2em;
+	width: 100%;
+}
+
+#buddypress div.dir-search form input[type="submit"],
+#buddypress div.message-search form input[type="submit"],
+#buddypress li.groups-members-search form input[type="submit"] {
+	float: right;
+	font-weight: 400;
+	padding: 0 1em;
+	text-align: center;
+	text-transform: none;
+	width: 30%;
+}
+
+@media screen and (min-width: 37.5em) {
+	#buddypress div.dir-search,
+	#buddypress div.message-search,
+	#buddypress li.groups-members-search {
+		float: right;
+		margin-bottom: 5px !important;
+	}
+	#buddypress div.dir-search form label,
+	#buddypress div.dir-search form input[type="text"],
+	#buddypress div.dir-search form input[type="submit"],
+	#buddypress div.message-search form label,
+	#buddypress div.message-search form input[type="text"],
+	#buddypress div.message-search form input[type="submit"],
+	#buddypress li.groups-members-search form label,
+	#buddypress li.groups-members-search form input[type="text"],
+	#buddypress li.groups-members-search form input[type="submit"] {
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 75em) {
+	#buddypress .dir-search form input[type="text"],
+	#buddypress .message-search form input[type="text"] {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+	#buddypress .dir-search form input[type="submit"],
+	#buddypress .message-search form input[type="submit"] {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user #buddypress table {
+	font-size: 14px;
+	font-size: 1rem;
+	margin: 24px 0;
+}
+
+.bp-user #buddypress table tr th {
+	background: #6f6e6e;
+	border-color: #b7b7b7;
+	color: #fff;
+	padding: 0 8px;
+}
+
+.bp-user #buddypress table tr td {
+	padding: 8px;
+}
+
+.bp-user #buddypress table p {
+	margin-bottom: 0.5em;
+}
+
+@media screen and (min-width: 55em) {
+	.bp-user #buddypress table {
+		font-size: 16px;
+		font-size: 1.1428571429rem;
+	}
+}
+
+.bp-user #buddypress .notifications th,
+.bp-user #buddypress .messages-notices th {
+	width: 30%;
+}
+
+.bp-user #buddypress .notifications th.bulk-select-all,
+.bp-user #buddypress .messages-notices th.bulk-select-all {
+	text-align: center;
+	width: 10%;
+}
+
+.bp-user #buddypress .notifications th.actions,
+.bp-user #buddypress .messages-notices th.actions {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .bulk-select-check,
+.bp-user #buddypress .notifications .thread-star,
+.bp-user #buddypress .messages-notices .bulk-select-check,
+.bp-user #buddypress .messages-notices .thread-star {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions,
+.bp-user #buddypress .notifications td.thread-options,
+.bp-user #buddypress .messages-notices .notification-actions,
+.bp-user #buddypress .messages-notices td.thread-options {
+	text-align: center;
+}
+
+.bp-user #buddypress .notifications .notification-actions a,
+.bp-user #buddypress .notifications td.thread-options a,
+.bp-user #buddypress .messages-notices .notification-actions a,
+.bp-user #buddypress .messages-notices td.thread-options a {
+	display: inline-block;
+	margin: 0;
+	padding: 0;
+}
+
+.bp-user #buddypress .notifications td .button,
+.bp-user #buddypress .messages-notices td .button {
+	border: 0;
+	display: block;
+	padding: 0;
+	text-align: center;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+#buddypress div#message p {
+	font-size: 18px;
+	font-size: 1.2857142857rem;
+	font-weight: 700;
+}
+
+#buddypress div#message.info p {
+	background: #c6e4ee;
+	border: 1px solid #78bed6;
+	color: #1e5264;
+}
+
+#buddypress div#message.updated p {
+	background: #dee6b2;
+	border: 1px solid #becc66;
+	color: #454d19;
+}
+
+.delete-group #buddypress div#message.info p {
+	background: #db7e7e;
+	border: 1px solid #be3535;
+	color: #1f0808;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/css/twentytwelve.scss	(revision 12265)
@@ -0,0 +1,2750 @@
+
+// Stylesheet Guidence Notes
+// Table of content represents a guide to sections of the sheet.
+// Note that not all sections may be described in the body of the
+// rulesets only those that require styles for a given theme.
+// Sub sections might not exist but can be added as required
+// as per BP Lists - 'activity' section.
+
+// We follow the dictats of the parent theme in respect of media queries,
+// font sizing etc. Media queries are used in a modular sense (OOCSS)
+// rather than described as blocks to the end of the sheet, this aids ease
+// of managing media queries, scalability & flexibility .
+
+// 2012 Media Queries / Breakpoints.
+// 2012 works two breakpoints 600px/960px
+
+// @media screen and (min-width: 37.5em)
+// @media screen and (min-width: 61.5625em)
+
+
+// In addition BP can use the em breakpoints as referenced below to provide
+// a series of finer breakpoints.
+// @media screen and (max-width: 20em)    xtra small mobile only 320px
+// @media screen and (min-width: 30em)    small only 480px
+// @media screen and (min-width: 46.25em) 740px
+// @media screen and (min-width: 75em)    1200px
+
+// Mixins and Variables
+
+// simplify remembering/writing media queries
+// written as:
+// @include medium-up {
+//  body {property: value;}
+// }
+
+// 600px
+@mixin small-up {
+
+	@media screen and (min-width: 37.5em) {
+
+		@content;
+	}
+}
+// 960px
+@mixin medium-up {
+
+	@media screen and (min-width: 60em) {
+
+		@content;
+	}
+}
+// 1200px
+@mixin large-up {
+
+	@media screen and (min-width: 75em) {
+
+		@content;
+	}
+}
+
+
+// Common spacing values
+$spacing-val-lg: 48px; // Matches 2012 stated values - virt rhythm
+$spacing-val-md: 24px; // Matches 2012 stated values - virt rhythm
+$spacing-val-sm: 10px;
+$spacing-val-xs: 5px;
+
+// A simple mixin to handle font-sizing
+// 2012 sets a html/body size @ 14px/87.50%
+@mixin font-size($font-size: 14) {
+	$rem-font-value: ($font-size / 14);
+	font-size: ($font-size * 1px);
+	font-size: $rem-font-value + rem;
+}
+
+// Border border-radius mixins
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// Box sizing
+@mixin box-model($box-model) {
+	-webkit-box-sizing: $box-model;
+	-moz-box-sizing: $box-model;
+	box-sizing: $box-model;
+}
+
+// Calc
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// BP message boxes
+@mixin message-box($background, $text-color: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: lighten($background, 10%);
+	border: 1px solid darken($background, 10%);
+	color: $text-color;
+}
+
+// 2012 font family
+@mixin default-font-sans() {
+	font-family: "Source Sans Pro", Helvetica, sans-serif;
+}
+
+// Variables: color definitions
+$body-text:          #141412;
+$content-background: #fff;
+$off-white:          lighten(#f9f8ee, 0.7);
+$cream-background:   #f7f5e7;
+$light-background:   #f7f7f7;
+$medium-background:  #ccc;
+$dark-background:    #555;
+$border-color:       #d4d0ba;    // border color is varied using rgba
+$border-light:       #eaeaea; // BP dividers
+$border-med:         #ddd;
+$background-hover:   #db572f; // 2012 link background hover
+$bp-button-hover:    #ededed; // this is the default BP button hover background
+$notice-error:       #c85a6e;
+$notice-warning:     #d05656;
+$notice-info:        #9fd1e2;
+$notice-update:      #ced98c;
+$stripe-odd:         #ebf0ff;
+$stripe-even:        #dbe5ff;
+$unread:             #9dc3c8;
+$link-action:        #c82b2b;
+
+/*--------------------------------------------------------------
+
+This is the BuddyPress companion stylesheet for
+the WordPress Twentytwelve theme.
+
+@version 3.0.0
+
+This sheet supports the primary BuddyPress styles in buddypress.css
+
+If you are running as a child theme of twentytwelve this stylesheet will be
+loaded by default. If you want to modify the styles the normal BP Theme Compat
+hierarchy works and you can copy the file to buddypress/css/ or community/css/
+in your child themes root & either start over or modify the rulesets.
+
+If you do not require the styles you can dequeue them from the themes
+functions file.
+
+Please see this codex article for more information:
+http://codex.buddypress.org/themes/buddypress-companion-stylesheets/
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 Theme Structural Elements & overrides
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+3.0 - Images
+4.0 - BP Lists / Loops Generic
+	4.1 - Activity Loop
+		4.1.1 Whats New Activity
+		4.1.2 - Activity Listing
+		4.1.3 - Activity Comments
+	4.2 - Members Loop
+	4.3 - Groups Loop
+	4.4 - Blogs Loop
+5.0 - Directories - Members, Groups, Blogs, Register, activation
+6.0 - Single Item screens: User Account & Single Group Screens
+	6.1 - Item Headers: Global
+		6.1.1 - item-header: Groups
+		6.1.2 - item-header: User Accounts
+	6.2 - Item Body: Global
+		6.2.1 - item-body: Groups
+			6.2.1.1 - Management settings screens
+			6.2.1.2 - Group members list
+		6.2.2 - item-body: User Accounts
+			6.2.2.1 - classes, pag, filters
+			6.2.2.2 - Extended Profiles
+			6.2.2.3 - Groups
+			6.2.2.4 - friends
+			6.2.2.5 - Private Messaging Threads
+			6.2.2.6 - Settings
+			6.2.2.7 - Notifications
+7.0 - Forms  -  General
+8.0 - Tables -  General
+9.0 - Error / Success Messages
+10.0 - Ajax Loading, Widgets, General classes
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - Theme - Structural Elements. overrides, Globals
+*-------------------------------------------------------------------------------
+*/
+
+
+.buddypress {
+	// Remove any empty clearing elements, CSS provides clearing.
+	div.clear {
+		display: none;
+	}
+
+} // .buddypress (body class)
+
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (item-list-tabs)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// We have a need to override BP specifity
+// so some rulesets sets will appear over weighted in selectors
+
+#buddypress {
+
+	// active/current states all navs
+	div.item-list-tabs {
+
+		ul {
+
+			li.selected,
+			li.current {
+
+				a {
+					background: inherit;
+					opacity: 1;
+				}
+			}
+		}
+	}
+	// global nav styles
+	div.item-list-tabs {
+
+		ul {
+			overflow: hidden;
+			padding: 0;
+
+			li {
+
+				a {
+
+					&:focus,
+					&:hover {
+						color: #555;
+					}
+					// Keep links from inheriting :visited states
+					&:visited {
+						color: #21759b;
+					}
+
+					span {
+						border-radius: 25%;
+					}
+				}
+
+			}
+		}
+	} // close item-list-tabs
+
+} // close #buddypress
+
+// .bp-user #object-nav @ small screen as a dropdown
+
+.bp-user {
+
+	#buddypress {
+
+		@media screen and (max-width: 30em) {
+
+			#object-nav {
+				border: 1px solid $border-light;
+				overflow: visible;
+				padding: $spacing-val-sm;
+
+				ul {
+					border: 0;
+					height: 0;
+					// all latest vendor releases handle transitions, so no prefixes used.
+					transition: height 0.3s ease-in-out 0.7s;
+					visibility: hidden;
+				}
+
+				&:before {
+					content: "Menu \021E9";
+					display: inline;
+					opacity: 0.7;
+				}
+
+				&:hover,
+				&:focus {
+
+					&:before {
+						content: "Menu \021E7";
+					}
+
+					ul {
+						height: 320px;
+						opacity: 1;
+						overflow-y: auto;
+						visibility: visible;
+					}
+				}
+			}
+
+			#subnav {
+				clear: both;
+			}
+
+		} // close @media
+	}
+}
+
+/*__ Horizontal menus __*/
+#buddypress {
+
+	#object-nav {
+
+		ul {
+			overflow: hidden;
+
+			li {
+				float: none;
+			}
+
+			@media screen and (max-width: 37.5em) {
+
+				li:not(:last-child) {
+					border-bottom: 1px solid $border-light;
+				}
+			}
+
+			li:not(.selected) {
+
+				@media screen and (max-width: 37.5em) {
+
+					&:focus,
+					&:hover {
+						background: darken($light-background, 2%);
+					}
+				}
+			}
+
+			@media screen and (min-width: 37.5em) {
+
+				li {
+					float: left;
+				}
+			}
+		}
+	}
+
+	div#subnav.item-list-tabs {
+		margin-top: 0;
+
+		ul {
+			border-bottom: 0;
+			border-top: 1px solid $border-color;
+			margin-top: $spacing-val-xs;
+			padding: 0;
+
+			li.last {
+				background: $content-background;
+				float: right;
+				margin-top: 0;
+				padding: $spacing-val-xs 0 $spacing-val-xs;
+				text-align: right;
+				width: 230px;
+
+				@media screen and (max-width: 30em) {
+					float: none;
+					width: auto;
+				}
+
+				select,
+				select:focus {
+					background: transparent;
+					border: 0;
+					outline: 0;
+					padding: 0;
+				}
+
+				select,
+				label,
+				option {
+
+					@include font-size(14);
+				}
+
+				select {
+					font-style: italic;
+
+					option {
+						font-style: normal;
+					}
+				}
+			}
+
+		} // close ul
+	} // close #subnav
+
+} // close #buddypress
+
+.bp-user {
+
+	#buddypress {
+
+		div#subnav.item-list-tabs {
+
+			li.last {
+				margin-left: 0;
+				text-align: right;
+
+				@media screen and (max-width: 30em) {
+					clear: left;
+					float: none;
+					margin-left: 10px;
+					text-align: left;
+				}
+			}
+		}
+	}
+}
+
+/*__ Vertical menu User Account / Group single screens __*/
+
+// This block contains rules to re-factor the item-body structural element
+// to sit alongside the vert menu
+// 2012 right sidebar is displayed if the widget sidebar has widgets;
+// we'll only display object nav as a vert menu if sidebar not present.
+
+@media screen and (min-width: 60em) {
+
+	body.full-width {
+
+		#buddypress {
+
+			#item-header,
+			#item-body {
+				background: $content-background;
+			}
+
+			#object-nav {
+				border-right: 1px solid $border-med;
+				float: left;
+				margin-right: -1px;
+				width: 200px;
+
+				ul {
+					background: none;
+					border-bottom: 0;
+					padding: 0;
+
+					li {
+						float: none;
+						overflow: hidden;
+
+						span {
+							background: $content-background;
+							border-radius: 10%;
+							float: right;
+							margin-right: 2px;
+						}
+					}
+				}
+			}
+
+			#item-body {
+				border-left: 1px solid $border-med;
+				overflow: hidden;
+				padding: 0 0 0 $spacing-val-md;
+				width: auto;
+
+				#subnav {
+					margin: 0 0 0 (-$spacing-val-md);
+
+					ul {
+						margin-top: 0;
+					}
+				}
+			}
+
+		} // close  #buddypress
+	} // close body:not(.sidebar)
+} // close @media
+
+
+// Tabbed Nav styling
+// Primarilly used for group create screens & ? directory nav.
+
+@media screen and (min-width: 46.25em) {
+
+	#main {
+
+		#buddypress {
+
+			#group-create-tabs.item-list-tabs {
+
+				ul:before,
+				ul:after {
+					content: " ";
+					display: table;
+				}
+
+				ul:after {
+					clear: both;
+				}
+
+				ul {
+					background: none;
+					border: 0;
+					border-bottom: 1px solid $border-med;
+					overflow: visible;
+					padding-bottom: 0;
+
+					li {
+						float: left;
+						width: auto;
+					}
+
+					li.selected,
+					li.current {
+						border: 1px solid $border-med;
+						border-bottom-color: $content-background;
+
+						@include border-top-radius(4px);
+						margin-bottom: -1px;
+
+						a {
+							background: none;
+							color: $body-text;
+							outline: none;
+						}
+					}
+				}
+
+			} // close .item-list-tabs
+
+			#subnav {
+
+				ul {
+					border-bottom: 0;
+				}
+
+			} // close #subnav
+		} // close #buddypress
+	}
+} // close @media
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	div.pagination {
+		box-shadow: none;
+		min-height: 0;
+
+		&:before,
+		&:after {
+			height: 0;
+			width: 0;
+		}
+
+		.pag-count {
+			margin-left: 0;
+		}
+
+		.pagination-links {
+			margin-right: 0;
+
+			span,
+			a {
+				height: auto;
+				line-height: 1;
+				padding: 5px;
+			}
+
+			.prev,
+			.next {
+				background-color: transparent;
+				color: inherit;
+				overflow: visible;
+				width: auto;
+
+				&:before {
+					display: none;
+				}
+			}
+
+			.prev {
+				left: auto;
+				position: static;
+			}
+
+			.next {
+				position: static;
+				right: auto;
+			}
+		} // close .pagination-links
+	} // close .pagination
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Trying to position the action div absolute has bad consequences where
+// aditional items are displayed, floating is an alternative that
+// allows natural flow expansion.
+// This block:
+// * Manages li items, Stacks them small screen, floated left at wider screens.
+// * Adjusts display of action buttons at small screen mobile up.
+// Adjusts Meta items to match 2012 font-family
+
+#buddypress {
+
+	ul.item-list {
+		border-top: 0;
+
+		li {
+			overflow: hidden !important;
+
+			// Undo BP floats to center avatars initially mobile up
+			.item-avatar {
+				margin-bottom: $spacing-val-sm;
+
+				@media screen and (min-width: 37.5em) {
+					margin-bottom: 0;
+				}
+
+				text-align: center;
+
+				a {
+					border-bottom: 0;
+				}
+
+				img.avatar {
+					display: inline-block;
+					float: none;
+					margin-bottom: $spacing-val-sm;
+
+					// Quite early on we need to move avatars to the left(right)
+					@include small-up {
+						display: block;
+						float: left;
+					}
+				}
+			} //close .item-avatar
+
+			.item {
+
+				// counter the BP rule
+				margin-left: 0;
+
+				@media screen and (min-width: 61.5625em) {
+					margin-left: 15%;
+				}
+
+				overflow: hidden;
+
+				.item-meta {
+					text-align: center;
+
+					@media screen and (min-width: 61.5625em) {
+						text-align: left;
+					}
+				}
+
+				span.activity {
+					font-style: italic;
+				}
+
+				.item-desc {
+					margin-left: 0;
+					width: 94%;
+				}
+
+				.item-title {
+
+					@include font-size(16);
+					line-height: 1.2;
+					text-align: center;
+
+					@include small-up {
+						text-align: left;
+					}
+
+					width: 100%;
+
+					> a {
+						text-decoration: none;
+
+						&:visited {
+							color: #21759b;
+						}
+					}
+
+					.update {
+						display: block;
+
+						@include font-size(12);
+
+						@include medium-up {
+
+							@include font-size(14);
+						}
+
+						padding: $spacing-val-sm 0;
+						text-align: left;
+					}
+				}  // close .item-title
+
+			} // close .item
+
+			@include medium-up {
+
+				.item-avatar,
+				.item,
+				.action {
+					float: left;
+				}
+
+				.item {
+					left: 5%;
+					margin-left: 0;
+					position: relative;
+					width: 55%;
+
+					.item-title {
+
+						@include font-size(18);
+					}
+				}
+
+			}// close medium-up
+
+			div.action {
+				clear: left;
+				float: none;
+				margin-bottom: -$spacing-val-md;
+				// remove BP margin left
+				margin-left: 0;
+				padding: $spacing-val-md 0 $spacing-val-xs;
+
+				// push the actions to the right
+				@include medium-up {
+					clear: none;
+					float: right;
+					margin-bottom: 0;
+					padding: 0;
+				}
+
+				position: relative;
+				text-align: left;
+				top: 0;
+
+				div {
+					display: inline-block;
+					margin: $spacing-val-sm 0;
+					width: 100%;
+
+					a {
+						display: block;
+					}
+					// Provisionally lets keep the 'action' div
+					//position but inline-block the buttons
+
+					@media screen and (min-width: 37.5em) {
+						// keep buttons to a row small tablet up
+						margin: 0 $spacing-val-sm $spacing-val-sm 0;
+						width: auto;
+					} // close @media
+
+					@include medium-up {
+						// take buttons to block to stack them when floated right
+						clear: right;
+						float: right;
+						margin: 0 0 $spacing-val-sm 0;
+					} // close @media
+
+				} // close div
+
+				.meta {
+					font-style: italic;
+				}
+
+			} // close .action
+
+		} // close li
+	} // close .item-list
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Activity
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 4.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	form#whats-new-form {
+
+		// Line-height issue inherited from BP rules, override.
+		p.activity-greeting {
+			line-height: 1.4;
+		}
+
+		// corrective measure for clipped elements due to JS inline styling
+
+		@media screen and (max-width: 46.25em) {
+
+			#whats-new-content {
+				clear: left;
+				margin: $spacing-val-sm 0 $spacing-val-md;
+				padding: $spacing-val-sm 0 0;
+			}
+		}
+
+		#whats-new-content.active {
+
+			#whats-new-options[style] {
+
+				#whats-new-post-in-box {
+					border: 1px solid rgba($border-color, 0.5);
+					float: left;
+					line-height: 1.5;
+					margin-top: 12px;
+					padding-left: 0.2em;
+					width: 100%;
+
+					select {
+						background: none;
+						border: 0;
+						float: right;
+						margin: 0;
+						min-height: 1.5em;
+						padding: 0 0 0 0.4em;
+					}
+				}
+
+				@media screen and (min-width: 30em) {
+
+					#whats-new-post-in-box {
+						width: auto;
+					}
+
+					#whats-new-submit {
+						float: right;
+
+						input {
+							padding: 2px 6px;
+						}
+					}
+				}
+
+			} // close #whats-new-options
+		} // close #whats-new-content.active
+
+	} // close #whats-new-form
+
+	// User account form requires matching bp default specificity
+	#item-body {
+
+		form#whats-new-form {
+			margin: $spacing-val-lg 0;
+		}
+	}
+} // close #buddypress
+
+/**
+*-----------------------------------------------------
+* @section 4.1.2 - Activity Listing
+*-----------------------------------------------------
+*/
+
+#buddypress {
+
+	#activity-stream {
+
+		// re-size activity avatars and stack small screen
+		// refactor main elements positioning medium up.
+		li {
+			padding: 25px 0 15px;
+
+			.activity-avatar {
+				float: none;
+				text-align: center;
+
+				a {
+					display: inline-block;
+
+					img.avatar {
+						display: inline;
+						float: none;
+						height: 60px;
+						margin-bottom: $spacing-val-md;
+						margin-left: 0;
+						width: 60px;
+					}
+				}
+			} // close .activity-avatar
+
+			.activity-content,
+			.activity-comments {
+				margin-left: 0;
+			}
+
+			.activity-header {
+
+				@include font-size(14);
+
+				a:visited {
+					color: #21759b;
+				}
+			}
+
+			.activity-inner {
+
+				img {
+					height: auto;
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				.activity-avatar {
+					float: left;
+					margin-right: $spacing-val-sm;
+					text-align: left;
+
+					a {
+						border-bottom: 0;
+					}
+				}
+
+				.activity-content {
+					margin: 0;
+					overflow: hidden;
+
+					.activity-header {
+
+						@include font-size(16);
+					}
+				}
+
+				.activity-comments {
+					margin-left: 70px;
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.mini {
+
+			.activity-avatar {
+
+				a {
+
+					img.avatar {
+						height: 30px;
+						margin-left: 15px;
+						width: 30px;
+					}
+				}
+			}
+
+			.activity-content {
+
+				.activity-header {
+
+					@include font-size(14);
+				}
+
+				.activity-meta {
+
+					a {
+
+						@include font-size(12);
+					}
+				}
+			}
+		} // close li.mini
+
+		.activity-content {
+			margin-top: -12px;
+
+			.activity-header {
+				line-height: inherit;
+				margin-right: 0;
+
+				p {
+					border-bottom: 1px solid rgba($border-light, 0.6);
+					margin-top: 0;
+					padding: 0 0.2em;
+				}
+
+				img.avatar {
+					display: inline-block;
+					margin-bottom: 0 !important; // have to match to BP
+				}
+			} // close .activity-header
+
+			.activity-meta {
+
+				a {
+					display: block;
+
+					@include font-size(14);
+					margin-bottom: $spacing-val-xs;
+
+					@media screen and (min-width: 37.5em) {
+						float: left;
+						margin-bottom: 0;
+					}
+
+					padding: 0.2em 0.5em;
+				}
+			}
+
+		} // close .activity-content
+
+		.load-more {
+			background: $light-background;
+			border: 1px solid transparent;
+			padding: $spacing-val-sm;
+
+			&:focus,
+			&:hover {
+				background: darken($light-background, 1%);
+				border: 1px solid rgba(#9fd1e2, 0.3);
+			}
+
+			a {
+				display: block;
+			}
+		}
+
+	} // close #activity-stream
+} // close #buddypress
+
+/* Single activity view  - activity permalink */
+
+.activity-permalink {
+
+	#buddypress {
+
+		#activity-stream {
+
+			li.activity-item {
+				padding: $spacing-val-md;
+			}
+
+			li.mini {
+
+				.activity-header {
+
+					@include font-size(16);
+
+					@media screen and (min-width: 46.25em) {
+
+						@include font-size(20);
+					}
+
+					margin-bottom: $spacing-val-lg;
+
+					p {
+						padding: $spacing-val-md;
+					}
+				}
+			} // close li.mini
+
+			.activity-comments {
+				margin-left: 0;
+			}
+
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 4.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+#buddypress {
+
+	#activity-stream {
+
+		.activity-comments {
+			position: relative;
+
+			> ul {
+				background: rgba($light-background, 0.6);
+				margin: $spacing-val-sm 0 0 2px;
+
+				> li {
+
+					&:hover * {
+						color: #555;
+					}
+
+					.acomment-meta,
+					.acomment-content {
+
+						@include font-size(12);
+					}
+				}
+
+			}
+
+			a {
+				color: #737373;
+			}
+
+			.ac-form {
+				border: 1px solid $border-color;
+				box-sizing: border-box;
+				margin: $spacing-val-sm 0;
+				width: 100%;
+
+				input[type="submit"],
+				.ac-reply-cancel {
+					color: rgba($body-text, 0.8);
+					display: inline-block;
+					font-family: inherit;
+
+					@include font-size(12);
+					font-weight: 400;
+					line-height: 1.2;
+					padding: 4px 10px;
+					text-transform: lowercase;
+					width: 100px;
+				}
+
+				.ac-reply-cancel {
+					border: 1px solid rgba($border-color, 0.7);
+					text-align: center;
+
+					&:focus,
+					&:hover {
+						background: $bp-button-hover;
+					}
+				}
+
+			} // close .ac-form
+
+		} // close .activity-comments
+
+	} // close #activity-steam
+
+} // close #buddypress
+
+/**
+*----------------------------------------------------------
+* @section 4.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#members-list {
+
+		@include medium-up {
+
+			li {
+
+				.item-avatar,
+				.item {
+					float: left;
+				}
+
+				.action {
+					float: right;
+				}
+			}
+		}
+
+	} // close #members-list
+} //close #BuddyPress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection Registration
+*-----------------------------------------------------------
+*/
+
+#buddypress {
+
+	#signup_form.standard-form {
+
+		#profile-details-section,
+		#basic-details-section,
+		#blog-details-section {
+			float: none;
+			width: 100%;
+
+			@media screen and (min-width: 37.5em) {
+				width: 48%;
+			}
+		}
+
+		@media screen and (min-width: 37.5em) {
+
+			#profile-details-section {
+				float: right;
+			}
+
+			#basic-details-section {
+				float: left;
+			}
+		}
+
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Single Item screens: User Account & Single Group Screens
+*-------------------------------------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+		// On object nav links, table links (notifications etc) we don't really
+		// need :visited inheritance, so set pseudo class to :link color.
+		a {
+
+			&:visited {
+				color: #21759b;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+.bp-user,
+.single-item.groups {
+
+	#buddypress {
+
+		#item-header-content {
+
+			#item-meta {
+
+				@include font-size(14);
+				text-align: left;
+
+				p {
+					margin-bottom: 0.5em;
+				}
+			}
+		}
+	}
+
+	@media screen and (max-width: 37.5em) {
+
+		h1,
+		#item-header-content {
+			text-align: center;
+		}
+	}
+
+	@media screen and (max-width: 46.25em) {
+
+		main {
+
+			header.entry-header {
+				padding-bottom: 1rem;
+			}
+		}
+	}
+
+	#buddypress {
+
+		@media screen and (max-width: 37.5em) {
+
+			h1 {
+				margin-bottom: 0;
+			}
+
+			#item-header-avatar {
+
+				img.avatar {
+					margin-right: 0;
+				}
+			}
+
+			#item-header-content {
+				width: 100%;
+			}
+		} // close @media
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				.generic-button {
+					float: none;
+					margin: 1.5em 0 0;
+				}
+			}
+		}
+	} // close #buddypress
+
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+.single-item.groups {
+
+	#buddypress {
+
+		@media screen and (max-width: 46.25em) {
+
+			#item-header {
+
+				#item-meta {
+					margin-bottom: $spacing-val-md;
+				}
+			}
+		}
+
+		// Move visual flow of avatar & item-actions at narrow width,
+		// avatar first after group title
+		@media screen and (max-width: 50em) {
+
+			div#item-header {
+				display: flex;
+				flex-direction: column;
+
+				#item-header-avatar {
+					order: 1;
+					text-align: center;
+
+					a {
+						border-bottom: 0;
+
+						img {
+							display: inline-block;
+							float: none;
+						}
+					}
+				}
+
+				#item-header-content {
+					order: 2;
+				}
+
+				#item-actions {
+					order: 3;
+
+					h2 {
+						border-bottom: 1px solid $border-light;
+						text-align: center;
+					}
+				}
+			}
+		} // close @media
+
+		div#item-header {
+			padding-bottom: $spacing-val-lg;
+
+			#item-header-content {
+
+				@include box-model(border-box);
+			}
+
+			div#item-actions {
+				margin: 0 !important;
+				width: 100%;
+
+				@media screen and (min-width: 50em) {
+					border-left: 1px solid $border-light;
+					clear: none;
+					float: right;
+					padding-left: 0.2em;
+					width: 30%;
+				} // close @media
+
+				@media screen and (min-width: 46.25em) {
+					width: 40%;
+				}
+
+				ul {
+					margin-top: 0;
+					padding-left: 0;
+				}
+
+				h2 {
+
+					@include font-size(14);
+					padding: 0.2em;
+				}
+
+				#group-admins,
+				#group-mods {
+
+					li {
+						margin: 0;
+					}
+				}
+
+			} // close item-actions
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar,
+				#item-header-content {
+					float: left;
+				}
+
+				#item-header-content {
+					margin-left: 2%;
+					padding: 0 0.5em;
+				}
+
+				div#item-actions {
+					float: right;
+					width: 28%;
+				}
+
+			} // close @media
+
+			@media screen and (min-width: 64em) {
+
+				#item-header-content {
+					width: 40%;
+				}
+			}
+		} // close #item-header
+	} // close #buddypress
+
+	// These rules handle the specific instance of Cover Images on single groups
+	// activated & attempt to address positioning concerns arising from themes
+	// breakpoints.
+	// Due to cover image styles being applied via embedded styles & thus
+	// carrying heavier weight than linked styles the use of !important to
+	// override may be required.
+	#buddypress {
+
+		#cover-image-container {
+
+			#item-header-cover-image {
+
+				#item-actions {
+					margin-top: 225px !important;
+				}
+
+				@media screen and (min-width: 50em) and (max-width: 60em) {
+
+					#item-header-content {
+						max-width: 60% !important;
+						width: 60% !important;
+					}
+				}
+
+				@media screen and (max-width: 64em) {
+
+					#item-actions {
+						border: 0;
+						clear: both;
+						margin-top: 0 !important;
+						max-width: 100% !important;
+						padding-top: 0 !important;
+						width: auto;
+
+						h2 {
+							border-bottom: 1px solid $border-light;
+						}
+					}
+				} // @media
+
+
+			}
+		}
+	}
+
+} // close .single-item.groups
+
+/**
+*-----------------------------------------------------
+* @subsection 6.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	#buddypress {
+
+		#item-header {
+			padding: $spacing-val-md 0;
+
+			#item-header-avatar {
+				text-align: center;
+
+				img.avatar,
+				a {
+					border-bottom: 0;
+					display: inline-block;
+					float: none;
+				}
+			}
+
+			#item-header-content {
+
+				#item-buttons {
+
+					.generic-button {
+						margin-right: 5px;
+					}
+				}
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				#item-header-avatar {
+					float: left;
+
+					a {
+						float: left;
+					}
+				}
+
+				#item-header-content {
+					float: right;
+					margin-right: 0%;
+					width: 69%;
+				}
+
+			} // close @media
+
+		} // close #item-header
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+*-----------------------------------------------------------
+* @subsection 6.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+/**
+*----------------------------------------------------
+* @subsection 6.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.1 - Group Management
+*-----------------------------------------
+*/
+
+// headings settings screens & general global settings styles
+.groups {
+
+	#group-settings-form {
+
+		h3 {
+			background: $dark-background;
+			color: $content-background;
+			padding: 0.2em;
+		}
+	}
+}
+
+.groups.edit-details {
+
+	#group-settings-form {
+
+		label {
+			margin-bottom: 0;
+			padding: 0.2em;
+			width: 80%;
+		}
+
+		textarea + p label {
+			background: none;
+			color: inherit;
+
+			@include font-size(14);
+			width: auto;
+		}
+
+		textarea {
+			height: auto;
+			min-height: 100px;
+			overflow: auto;
+		}
+	}
+} // close .groups.edit-details
+
+.groups.group-settings {
+
+	#group-settings-form {
+
+		div.radio label {
+			border: 1px solid $border-light;
+			padding: 0.2em;
+
+			ul {
+				color: rgba($body-text, 0.6);
+
+				@include font-size(14);
+			}
+		}
+
+	}
+} // close .groups.group-settings
+
+.groups.group-avatar {
+
+	form > p {
+		margin-top: $spacing-val-md;
+	}
+
+}
+
+.groups.manage-members {
+
+	#group-settings-form {
+
+		.item-list {
+
+			li {
+				border-bottom: 1px solid $border-light;
+
+				img,
+				h5 {
+					float: left;
+
+					> a {
+						border-bottom: 0;
+					}
+				}
+
+				span.small {
+					clear: left;
+					display: block;
+					float: none;
+					margin-top: $spacing-val-sm;
+
+					a {
+						display: inline-block;
+						margin: $spacing-val-xs 0;
+						width: 100%;
+
+						@media screen and (min-width: 37.5em) {
+							width: auto;
+						}
+					}
+				}
+
+				h5 {
+					margin: 0;
+				}
+
+			}
+		}
+	}
+} // close .groups.manage-members
+
+/**
+*-----------------------------------------
+* @subsection 6.2.1.2 - Group members
+*-----------------------------------------
+*/
+
+// Massage the members search for groups nav specifically.
+.groups.group-members {
+
+	#subnav {
+
+		li {
+
+			@media screen and (max-width: 37.5em) {
+				background: $content-background;
+				padding: $spacing-val-md 0;
+			}
+
+			width: 100%;
+
+			#search-members-form {
+				float: right;
+
+				@media screen and (max-width: 37.5em) {
+					margin: 0;
+					width: 100%;
+
+					label {
+
+						input[type="text"] {
+							width: 100%;
+						}
+					}
+				}
+
+				margin: $spacing-val-xs $spacing-val-xs 0 0;
+			}
+		}
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 6.2.2 - Item Body User Accounts
+*
+* User Account specific component item body rules
+*-----------------------------------------------------
+*/
+
+.bp-user {
+
+	.entry-title {
+		margin-bottom: 0.5em;
+	}
+}
+
+/**
+*--------------------------------------------
+* @subsection 6.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			th {
+
+				@include font-size(13);
+			}
+
+			td {
+
+				@include font-size(12);
+			}
+
+			@include medium-up {
+
+				th {
+
+					@include font-size(16);
+				}
+
+				td {
+
+					@include font-size(14);
+				}
+			}
+		}
+
+		.pag-count {
+			font-style: italic;
+		}
+
+		.notifications-options-nav,
+		.messages-options-nav {
+			float: left;
+			width: 100%;
+
+			@media screen and (min-width: 37.5em) {
+				width: 40%;
+			}
+
+			select,
+			input {
+
+				@include font-size(14);
+				outline: 0;
+				padding: 0;
+			}
+
+			select {
+				float: left;
+				margin-right: 0;
+				width: 49%;
+
+				@media screen and (min-width: 37.5em) {
+					width: auto;
+				}
+			}
+
+			input {
+				float: left;
+				font-family: inherit;
+				line-height: 20px;
+				margin-left: 1%;
+				width: 50%;
+
+				@media screen and (min-width: 37.5em) {
+					width: auto;
+				}
+
+				&[disabled="disabled"]:focus,
+				&[disabled="disabled"]:hover {
+					background: none;
+				}
+			}
+
+		} // close .notifications-options-nav/messages-options-nav
+
+	} //close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.2 - Extended Profiles
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		.profile {
+
+			.bp-widget {
+
+				h2 {
+					background: lighten($dark-background, 10%);
+					color: #fff;
+					margin-bottom: 0;
+					padding: 0.4em;
+				}
+
+				table {
+					margin-top: 0;
+				}
+			}
+
+			/* Edit profile */
+
+			#profile-edit-form {
+
+				.button-nav:before,
+				.button-nav:after {
+					content: " ";
+					display: table;
+				}
+
+				.button-nav:after {
+					clear: both;
+				}
+
+				ul.button-nav {
+					border-bottom: 1px solid $border-light;
+					margin-left: 0;
+
+					li {
+						float: left;
+						margin-bottom: 0;
+
+						&.current {
+							border: 1px solid $border-light;
+							border-bottom-color: #fff;
+							margin-bottom: -1px;
+						}
+					} // li
+
+					a {
+						background: none;
+						border: 0;
+
+						@include font-size(18);
+					}
+				} //.button-nav
+
+				.field-visibility-settings-toggle {
+					margin: $spacing-val-xs 0 $spacing-val-md;
+				}
+
+				.field-visibility-settings-toggle,
+				.field-visibility-settings {
+
+					@include font-size(14);
+				}
+
+				.field-visibility-settings-close,
+				.visibility-toggle-link {
+					background: lighten($dark-background, 10%);
+
+					@include border-radius(2px);
+					color: #f1f1f1;
+					font-weight: 700;
+					padding: 0.1em 0.5em;
+					text-decoration: none;
+
+					&:focus,
+					&:hover {
+						color: #fff;
+					}
+				}
+			} // close profile form
+
+			.bp-avatar {
+
+				#bp-delete-avatar {
+
+					a {
+						font-size: inherit;
+					}
+				}
+			}
+		} // close .profile
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.3 - Groups
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#groups-list {
+
+			li {
+
+				.item {
+
+					@media screen and (min-width: 77.5em) {
+						left: 5%;
+						width: 50%;
+					}
+				}
+			}
+		}
+
+	} // close #buddypress
+} // close .bp-user
+
+/**
+	*-------------------------------------------
+	* @subsection 6.2.2.5 - Private Messaging
+	*-------------------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#message-thread {
+			//Single message view
+			a {
+				border-bottom: 0;
+			}
+
+			#message-subject {
+				background: lighten($dark-background, 10%);
+				color: $content-background;
+				padding: 0.3em 0 0.3em 0.2em;
+			}
+
+			#message-recipients {
+				font-style: italic;
+
+				a.confirm {
+					border: 1px solid $border-light;
+					font-style: normal;
+				}
+
+				.highlight {
+					font-size: 100%;
+				}
+			}
+
+			.message-metadata {
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+
+				img.avatar {
+					float: none;
+				}
+
+				@media screen and (min-width: 46.25em) {
+
+					img.avatar {
+						float: left;
+					}
+				}
+
+				.message-star-actions {
+					float: right;
+					margin-right: $spacing-val-xs;
+					position: static;
+				}
+			} // close .message-meta
+
+			.message-content {
+				background: $light-background;
+				border: 1px solid $border-light;
+				margin: $spacing-val-sm 0 0 0;
+				padding: 0.3em;
+			}
+
+			#send-reply {
+
+				.message-content {
+					background: $content-background;
+					border: 0;
+				}
+			}
+
+			.alt {
+				background: $content-background;
+			}
+		} // close message-thread  Singular view!
+
+		#message-threads {
+
+			thead {
+
+				tr {
+					background: lighten($dark-background, 10%);
+				}
+			}
+
+			tr {
+
+				td {
+					background: $content-background;
+					box-sizing: border-box;
+					display: inline-block;
+					float: left;
+				}
+
+				td.thread-star,
+				td.thread-options {
+					border-bottom: 1px solid $medium-background;
+					height: 2.4em;
+					padding-bottom: 0.2em;
+					padding-top: 0.2em;
+
+					@media screen and (max-width: 46.25em) {
+						padding-top: 0;
+					}
+				}
+
+				td.thread-star {
+					vertical-align: middle;
+
+					.message-action-star,
+					.message-action-unstar {
+						line-height: 1.2;
+					}
+
+					span.icon:before {
+
+						@include font-size(14);
+					}
+				}
+
+				td.bulk-select-check,
+				td.thread-from {
+					height: 3em;
+
+					@media screen and (max-width: 37.5em) {
+						height: 5.2em;
+					}
+				}
+
+				td.thread-from,
+				td.thread-options {
+					border-left: 0 !important;
+
+					@include calc(width, "100% - 30px");
+					margin-left: 0;
+				}
+
+				td.thread-info {
+					padding-left: 41px;
+					width: 100%;
+				}
+
+				td.thread-options {
+					text-align: right;
+
+					a {
+
+						@include font-size(12);
+						line-height: 1.2;
+					}
+				}
+
+				span.from {
+					display: none;
+				}
+
+				span.activity {
+					display: block;
+					float: right;
+					line-height: 2;
+
+					@media screen and (max-width: 37.5em) {
+						clear: both;
+
+						@include font-size(11);
+						width: 100%;
+					}
+				}
+			}
+
+			tr.unread {
+
+				td {
+					border-color: $border-light;
+				}
+			}
+
+			th {
+				display: none;
+			}
+
+			th.bulk-select-all {
+				border-bottom: 0;
+				display: inline-block;
+				text-align: left;
+			}
+
+			th.bulk-select-all,
+			td.bulk-select-check,
+			td.thread-star {
+				border-right: 0;
+				width: 30px;
+			}
+		}
+
+		#send_message_form {
+
+			input,
+			textarea {
+				box-sizing: border-box;
+			}
+		}
+
+		.acfb-holder {
+			list-style: none;
+
+			li {
+				margin-left: 0;
+			}
+
+			li.friend-tab {
+				background: lighten($notice-info, 20%);
+				border: inherit;
+				margin-right: 0;
+				padding: 0.5em;
+
+				span.p {
+					// the 'X' close
+					padding-left: $spacing-val-sm;
+
+					&:focus,
+					&:hover {
+						color: $link-action;
+						cursor: pointer;
+					}
+				}
+
+				a {
+					border-bottom: 0;
+					text-decoration: none;
+
+					img {
+						// this is the tiny user avatar
+						display: inline;
+						height: 20px;
+						vertical-align: middle;
+						width: 20px !important; // override inline style
+					}
+				}
+			}
+		}
+
+		#message-threads.sitewide-notices {
+
+			tr {
+				margin: 3em 0;
+			}
+
+			td {
+				width: 100%;
+
+				strong {
+					background: lighten($dark-background, 10%);
+					color: $content-background;
+					display: block;
+					margin-bottom: 0.4em;
+					padding-left: 0.2em;
+				}
+
+				a {
+					display: inline-block;
+				}
+
+				a.button {
+					border: 1px solid $border-color;
+					line-height: initial;
+					padding: 0.4em 0.3em;
+				}
+			}
+
+			td:first-child {
+				display: none;
+			}
+
+			td:nth-child(2) {
+
+				strong {
+					margin: -8px -8px 8px;
+				}
+			}
+
+			td:first-child + td + td {
+				// notice date
+				border-bottom: 0;
+
+				span {
+					line-height: 1;
+				}
+			}
+
+			td:last-child {
+				// notice actions
+				line-height: 1;
+				padding-bottom: 1em;
+				text-align: right;
+
+				a:last-child:after {
+					content: attr(title);
+					display: block;
+					line-height: initial;
+					text-indent: 0;
+				}
+			}
+		}
+
+	} // #buddypress
+
+	// acfb auto complete name list sits in doc footer before body close
+	.ac_results {
+		background: #eee;
+		padding-left: $spacing-val-sm;
+
+		li {
+			margin: $spacing-val-sm 0;
+
+			&:focus,
+			&:hover {
+				cursor: pointer;
+			}
+
+			img {
+				vertical-align: bottom;
+			}
+
+		}
+	}
+
+} // close .bp-user
+
+/**
+	*------------------------------
+	* @subsection 6.2.2.6 - Settings
+	*------------------------------
+	*/
+
+.bp-user {
+
+	#buddypress {
+
+		#settings-form {
+
+			// 'p' = email notification screen sub heading
+			> p {
+
+				@include font-size(20);
+				margin: $spacing-val-md 0 $spacing-val-sm;
+			}
+		}
+
+		table.notification-settings {
+
+			td.yes,
+			td.no {
+				vertical-align: middle;
+			}
+		}
+
+		// Profile table td widths
+		table.profile-settings {
+			width: 100%;
+
+			th.field-group-name,
+			td.field-name {
+				width: 50%;
+			}
+
+			@media screen and (min-width: 46.25em) {
+
+				th.field-group-name,
+				td.field-name {
+					width: 70%;
+				}
+			}
+
+			th.title,
+			td.field-visibility {
+				width: 30%;
+			}
+
+			td.field-visibility {
+
+				select {
+					width: 100%;
+				}
+			}
+		}
+
+	} // close #buddyPress
+
+} // close .bp-user
+
+/**
+	*------------------------------------
+	* @subsection 6.2.2.6 - Notifications
+	*------------------------------------
+	*/
+
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Attempt to reset form control widths
+#main {
+
+	#buddypress {
+
+		.standard-form {
+
+			li {
+				float: none;
+			}
+
+			input[type="text"],
+			input[type="email"],
+			input[type="password"],
+			textarea {
+				width: 100%;
+			}
+		}
+	}
+}
+
+
+#buddypress {
+
+	// Match BP form elements to Themes(2012) styling where styles
+	// are not inherited or being overriden.
+	div.activity-comments {
+
+		form {
+
+			.ac-textarea {
+				background: $light-background;
+				border: 1px solid rgba($border-color, 0.5);
+
+				textarea {
+					background: none;
+					border: 0;
+				}
+			}
+		}
+	}
+
+	.standard-form {
+
+		// Keep BP form element borders to 1px rather than 2013 2px
+		input[type="text"],
+		input[type="email"],
+		input[type="password"],
+		button,
+		select,
+		textarea {
+			border-color: rgba($border-color, 0.5);
+			border-width: 1px;
+		}
+
+	} // close .standard-form
+
+	#signup_form.standard-form {
+
+		div.submit {
+			float: none;
+
+			input {
+				margin-right: 0;
+			}
+		}
+	}
+
+} // close #buddypress
+
+#buddypress {
+
+	div.dir-search,
+	div.message-search,
+	li.groups-members-search {
+		float: none;
+		margin: $spacing-val-sm 0;
+
+		// Stylise the seach form elements, in part this deals with padding
+		// issues on the submit & sizing issues between bp styles & 2012
+
+		form { // *sigh* only to bludgeon over specified rules
+			border: 1px solid rgba($border-color, 0.6);
+			overflow: hidden;
+
+			label {
+				float: left;
+				width: 70%;
+			}
+
+			input[type="text"],
+			input[type="submit"] {
+
+				@include font-size(14);
+				border: 0;
+				border-radius: 0;
+				line-height: 1.8;
+				min-height: 2rem;
+			}
+
+			input[type="text"] {
+				border-right: 1px solid rgba($border-color, 0.6);
+				float: left;
+				margin: 0;
+				padding: 0 0.2em;
+				width: 100%;
+			}
+
+			input[type="submit"] {
+				float: right;
+				font-weight: 400;
+				padding: 0 1em;
+				text-align: center;
+				text-transform: none;
+				width: 30%;
+			}
+		} // close form
+
+	} // close .dir-search, .message-search
+
+	// Shift the search parent to the right and allow to shrinkwrap
+
+	@media screen and (min-width: 37.5em) {
+
+		div.dir-search,
+		div.message-search,
+		li.groups-members-search {
+
+			float: right;
+			margin-bottom: 5px !important;
+
+			form {
+
+				label,
+				input[type="text"],
+				input[type="submit"] {
+					width: auto;
+				}
+			}
+		}
+
+	} // close @media
+
+
+	@include large-up {
+
+		.dir-search,
+		.message-search {
+
+			form {
+
+				input[type="text"] {
+
+					@include font-size(16) ;
+				}
+
+				input[type="submit"] {
+
+					@include font-size(16) ;
+				}
+			}
+		}
+	}
+
+} // close #buddypress
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.bp-user {
+
+	#buddypress {
+
+		table {
+
+			@include font-size(14);
+			margin: $spacing-val-md 0;
+
+			tr th {
+				background: lighten($dark-background, 10%);
+				border-color: darken($border-light, 20%);
+				color: $content-background;
+				padding: 0 8px;
+			}
+
+			tr td {
+				padding: 8px;
+			}
+
+			// Reduce the themes inherited paragraph margins in tables
+			p {
+				margin-bottom: 0.5em;
+			}
+		}
+
+		@media screen and (min-width: 55em) {
+
+			table {
+
+				@include font-size(16);
+			}
+		}
+
+		// Manage some table cells widths that are disproportionate to their content
+		.notifications,
+		.messages-notices {
+
+			th {
+				width: 30%;
+
+				&.bulk-select-all {
+					text-align: center;
+					width: 10%;
+				}
+			}
+
+			th.actions {
+				text-align: center;
+			}
+
+			.bulk-select-check,
+			.thread-star {
+				text-align: center;
+			}
+
+			.notification-actions,
+			td.thread-options {
+				text-align: center;
+
+				a {
+					display: inline-block;
+					margin: 0;
+					padding: 0;
+				}
+			}
+
+			// Notices action buttons, this maybe better moved, temp for now to address
+			// styling issues - this will need styling ideas
+			td {
+
+				.button {
+					border: 0;
+					display: block;
+					padding: 0;
+					text-align: center;
+				}
+			}
+
+		} // .notifications, .message-notices
+
+	} // #buddypress
+} // .bp-user
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Error / Success messages
+*-------------------------------------------------------------------------------
+*/
+
+// message args ($variable-color, text-color)
+// Leave text-color undefined to automagically set text color
+// to background color darker by 50%
+
+#buddypress {
+
+	div#message {
+
+		p {
+
+			@include font-size(18);
+			font-weight: 700;
+		}
+
+		&.info {
+
+			p {
+
+				@include message-box($notice-info);
+			}
+		}
+
+		&.updated {
+
+			p {
+
+				@include message-box($notice-update);
+			}
+		}
+
+	} // close #message
+
+} // close  #buddypress
+
+// Without direct classes on our mesages ( 'warning' )
+// we need to use the body classes
+// @todo If this class .warning is included in main BP styles remove from here
+.delete-group {
+
+	#buddypress {
+
+		div#message.info {
+
+			p {
+
+				@include message-box($notice-warning);
+			}
+		}
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 10.0 - Ajax Loading, Widgets, General classes
+*-------------------------------------------------------------------------------
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/js/buddypress.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/js/buddypress.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/js/buddypress.js	(revision 12265)
@@ -0,0 +1,2097 @@
+/* jshint undef: false, unused:false */
+/* @version 3.0.0 */
+// AJAX Functions
+var jq = jQuery;
+
+// Global variable to prevent multiple AJAX requests
+var bp_ajax_request = null;
+
+// Global variables to temporarily store newest activities
+var newest_activities = '';
+var activity_last_recorded  = 0;
+
+var directoryPreferences = {};
+
+jq(document).ready( function() {
+	var activity_oldestpage = 1;
+
+	/**** Page Load Actions *******************************************************/
+
+	/* Activity filter and scope set */
+	bp_init_activity();
+
+	var objects  = [ 'members', 'groups', 'blogs', 'group_members' ],
+		$whats_new = jq('#whats-new');
+
+	/* Object filter and scope set. */
+	bp_init_objects( objects );
+
+	/* @mention Compose Scrolling */
+	if ( $whats_new.length && bp_get_querystring('r') ) {
+		var $member_nicename = $whats_new.val();
+
+		jq('#whats-new-options').slideDown();
+
+		$whats_new.animate({
+			height:'3.8em'
+		});
+
+		jq.scrollTo( $whats_new, 500, {
+			offset:-125,
+			easing:'swing'
+		} );
+
+		$whats_new.val('').focus().val( $member_nicename );
+	} else {
+		jq('#whats-new-options').hide();
+	}
+
+	/**** Activity Posting ********************************************************/
+
+	/* Textarea focus */
+	$whats_new.focus( function(){
+		jq( '#whats-new-options' ).slideDown();
+
+		jq( this ).animate({
+			height:'3.8em'
+		});
+
+		jq('#aw-whats-new-submit').prop('disabled', false);
+
+		jq( this ).parent().addClass( 'active' );
+		jq( '#whats-new-content' ).addClass( 'active' );
+
+		var $whats_new_form = jq('form#whats-new-form'),
+			$activity_all = jq( '#activity-all' );
+
+		if ( $whats_new_form.hasClass('submitted') ) {
+			$whats_new_form.removeClass('submitted');
+		}
+
+		// Return to the 'All Members' tab and 'Everything' filter,
+		// to avoid inconsistencies with the heartbeat integration
+		if ( $activity_all.length  ) {
+			if ( ! $activity_all.hasClass( 'selected' ) ) {
+				// reset to everything
+				jq( '#activity-filter-select select' ).val( '-1' );
+				$activity_all.children( 'a' ).trigger( 'click' );
+			} else if ( '-1' !== jq( '#activity-filter-select select' ).val() ) {
+				jq( '#activity-filter-select select' ).val( '-1' );
+				jq( '#activity-filter-select select' ).trigger( 'change' );
+			}
+		}
+	});
+
+	/* For the "What's New" form, do the following on focusout. */
+	jq( '#whats-new-form' ).on( 'focusout', function( e ) {
+		var elem = jq( this );
+
+		// Let child hover actions passthrough.
+		// This allows click events to go through without focusout.
+		setTimeout( function () {
+			if ( ! elem.find(':hover').length ) {
+				// Do not slide up if textarea has content.
+				if ( '' !== $whats_new.val() ) {
+					return;
+				}
+
+				$whats_new.animate({
+					height:'2.2em'
+				});
+
+				jq( '#whats-new-options' ).slideUp();
+
+				jq('#aw-whats-new-submit').prop( 'disabled', true );
+
+				jq( '#whats-new-content' ).removeClass( 'active' );
+				$whats_new.parent().removeClass( 'active' );
+			}
+		}, 0 );
+	} );
+
+	/* New posts */
+	jq('#aw-whats-new-submit').on( 'click', function() {
+		var last_date_recorded = 0,
+			button = jq(this),
+			form   = button.closest('form#whats-new-form'),
+			inputs = {}, post_data;
+
+		// Get all inputs and organize them into an object {name: value}
+		jq.each( form.serializeArray(), function( key, input ) {
+			// Only include public extra data
+			if ( '_' !== input.name.substr( 0, 1 ) && 'whats-new' !== input.name.substr( 0, 9 ) ) {
+				if ( ! inputs[ input.name ] ) {
+					inputs[ input.name ] = input.value;
+				} else {
+					// Checkboxes/dropdown list can have multiple selected value
+					if ( ! jq.isArray( inputs[ input.name ] ) ) {
+						inputs[ input.name ] = new Array( inputs[ input.name ], input.value );
+					} else {
+						inputs[ input.name ].push( input.value );
+					}
+				}
+			}
+		} );
+
+		form.find( '*' ).each( function() {
+			if ( jq.nodeName( this, 'textarea' ) || jq.nodeName( this, 'input' ) ) {
+				jq(this).prop( 'disabled', true );
+			}
+		} );
+
+		/* Remove any errors */
+		jq('div.error').remove();
+		button.addClass('loading');
+		button.prop('disabled', true);
+		form.addClass('submitted');
+
+		/* Default POST values */
+		object = '';
+		item_id = jq('#whats-new-post-in').val();
+		content = jq('#whats-new').val();
+		firstrow = jq( '#buddypress ul.activity-list li' ).first();
+		activity_row = firstrow;
+		timestamp = null;
+
+		// Checks if at least one activity exists
+		if ( firstrow.length ) {
+
+			if ( activity_row.hasClass( 'load-newest' ) ) {
+				activity_row = firstrow.next();
+			}
+
+			timestamp = activity_row.prop( 'class' ).match( /date-recorded-([0-9]+)/ );
+		}
+
+		if ( timestamp ) {
+			last_date_recorded = timestamp[1];
+		}
+
+		/* Set object for non-profile posts */
+		if ( item_id > 0 ) {
+			object = jq('#whats-new-post-object').val();
+		}
+
+		post_data = jq.extend( {
+			action: 'post_update',
+			'cookie': bp_get_cookies(),
+			'_wpnonce_post_update': jq('#_wpnonce_post_update').val(),
+			'content': content,
+			'object': object,
+			'item_id': item_id,
+			'since': last_date_recorded,
+			'_bp_as_nonce': jq('#_bp_as_nonce').val() || ''
+		}, inputs );
+
+		jq.post( ajaxurl, post_data, function( response ) {
+			form.find( '*' ).each( function() {
+				if ( jq.nodeName( this, 'textarea' ) || jq.nodeName( this, 'input' ) ) {
+					jq(this).prop( 'disabled', false );
+				}
+			});
+
+			/* Check for errors and append if found. */
+			if ( response[0] + response[1] === '-1' ) {
+				form.prepend( response.substr( 2, response.length ) );
+				jq( '#' + form.attr('id') + ' div.error').hide().fadeIn( 200 );
+			} else {
+				if ( 0 === jq('ul.activity-list').length ) {
+					jq('div.error').slideUp(100).remove();
+					jq('#message').slideUp(100).remove();
+					jq('div.activity').append( '<ul id="activity-stream" class="activity-list item-list">' );
+				}
+
+				if ( firstrow.hasClass( 'load-newest' ) ) {
+					firstrow.remove();
+				}
+
+				jq('#activity-stream').prepend(response);
+
+				if ( ! last_date_recorded ) {
+					jq('#activity-stream li:first').addClass('new-update just-posted');
+				}
+
+				if ( 0 !== jq('#latest-update').length ) {
+					var l   = jq('#activity-stream li.new-update .activity-content .activity-inner p').html(),
+						v     = jq('#activity-stream li.new-update .activity-content .activity-header p a.view').attr('href'),
+						ltext = jq('#activity-stream li.new-update .activity-content .activity-inner p').text(),
+						u     = '';
+
+					if ( ltext !== '' ) {
+						u = l + ' ';
+					}
+
+					u += '<a href="' + v + '" rel="nofollow">' + BP_DTheme.view + '</a>';
+
+					jq('#latest-update').slideUp(300,function(){
+						jq('#latest-update').html( u );
+						jq('#latest-update').slideDown(300);
+					});
+				}
+
+				jq('li.new-update').hide().slideDown( 300 );
+				jq('li.new-update').removeClass( 'new-update' );
+				jq('#whats-new').val('');
+				form.get(0).reset();
+
+				// reset vars to get newest activities
+				newest_activities = '';
+				activity_last_recorded  = 0;
+			}
+
+			jq('#whats-new-options').slideUp();
+			jq('#whats-new-form textarea').animate({
+				height:'2.2em'
+			});
+			jq('#aw-whats-new-submit').prop('disabled', true).removeClass('loading');
+			jq( '#whats-new-content' ).removeClass( 'active' );
+		});
+
+		return false;
+	});
+
+	/* List tabs event delegation */
+	jq('div.activity-type-tabs').on( 'click', function(event) {
+		var target = jq(event.target).parent(),
+			scope, filter;
+
+		if ( event.target.nodeName === 'STRONG' || event.target.nodeName === 'SPAN' ) {
+			target = target.parent();
+		} else if ( event.target.nodeName !== 'A' ) {
+			return false;
+		}
+
+		/* Activity Stream Tabs */
+		scope  = target.attr('id').substr( 9, target.attr('id').length );
+		filter = jq('#activity-filter-select select').val();
+
+		if ( scope === 'mentions' ) {
+			jq( '#' + target.attr('id') + ' a strong' ).remove();
+		}
+
+		bp_activity_request(scope, filter);
+
+		return false;
+	});
+
+	/* Activity filter select */
+	jq('#activity-filter-select select').change( function() {
+		var selected_tab = jq( 'div.activity-type-tabs li.selected' ),
+			filter = jq(this).val(),
+			scope;
+
+		if ( !selected_tab.length ) {
+			scope = null;
+		} else {
+			scope = selected_tab.attr('id').substr( 9, selected_tab.attr('id').length );
+		}
+
+		bp_activity_request(scope, filter);
+
+		return false;
+	});
+
+	/* Stream event delegation */
+	jq('div.activity').on( 'click', function(event) {
+		var target = jq(event.target),
+			type, parent, parent_id,
+			li, id, link_href, nonce, timestamp,
+			oldest_page, just_posted;
+
+		/* Favoriting activity stream items */
+		if ( target.hasClass('fav') || target.hasClass('unfav') ) {
+			/* Bail if a request is in progress */
+			if ( target.hasClass( 'loading' ) ) {
+				return false;
+			}
+
+			type      = target.hasClass('fav') ? 'fav' : 'unfav';
+			parent    = target.closest('.activity-item');
+			parent_id = parent.attr('id').substr( 9, parent.attr('id').length );
+			nonce     = bp_get_query_var( '_wpnonce', target.attr( 'href' ) );
+
+			target.addClass('loading');
+
+			jq.post( ajaxurl, {
+				action: 'activity_mark_' + type,
+				'cookie': bp_get_cookies(),
+				'id': parent_id,
+				nonce: nonce
+			},
+			function(response) {
+				target.removeClass('loading');
+
+				target.fadeOut( 200, function() {
+					jq(this).html(response);
+					jq(this).attr('title', 'fav' === type ? BP_DTheme.remove_fav : BP_DTheme.mark_as_fav);
+					jq(this).fadeIn(200);
+				});
+
+				if ( 'fav' === type ) {
+					if ( !jq('.item-list-tabs #activity-favs-personal-li').length ) {
+						if ( !jq('.item-list-tabs #activity-favorites').length ) {
+							jq('.item-list-tabs ul #activity-mentions').before( '<li id="activity-favorites"><a href="#">' + BP_DTheme.my_favs + ' <span>0</span></a></li>');
+						}
+
+						jq('.item-list-tabs ul #activity-favorites span').html( Number( jq('.item-list-tabs ul #activity-favorites span').html() ) + 1 );
+					}
+
+					target.removeClass('fav');
+					target.addClass('unfav');
+
+				} else {
+					target.removeClass('unfav');
+					target.addClass('fav');
+
+					jq('.item-list-tabs ul #activity-favorites span').html( Number( jq('.item-list-tabs ul #activity-favorites span').html() ) - 1 );
+
+					if ( !Number( jq('.item-list-tabs ul #activity-favorites span').html() ) ) {
+						if ( jq('.item-list-tabs ul #activity-favorites').hasClass('selected') ) {
+							bp_activity_request( null, null );
+						}
+
+						jq('.item-list-tabs ul #activity-favorites').remove();
+					}
+				}
+
+				if ( 'activity-favorites' === jq( '.item-list-tabs li.selected').attr('id') ) {
+					target.closest( '.activity-item' ).slideUp( 100 );
+				}
+			});
+
+			return false;
+		}
+
+		/* Delete activity stream items */
+		if ( target.hasClass('delete-activity') ) {
+			li        = target.parents('div.activity ul li');
+			id        = li.attr('id').substr( 9, li.attr('id').length );
+			link_href = target.attr('href');
+			nonce     = link_href.split('_wpnonce=');
+			timestamp = li.prop( 'class' ).match( /date-recorded-([0-9]+)/ );
+			nonce     = nonce[1];
+
+			target.addClass('loading');
+
+			jq.post( ajaxurl, {
+				action: 'delete_activity',
+				'cookie': bp_get_cookies(),
+				'id': id,
+				'_wpnonce': nonce
+			},
+			function(response) {
+
+				if ( response[0] + response[1] === '-1' ) {
+					li.prepend( response.substr( 2, response.length ) );
+					li.children('#message').hide().fadeIn(300);
+				} else {
+					li.slideUp(300);
+
+					// reset vars to get newest activities
+					if ( timestamp && activity_last_recorded === timestamp[1] ) {
+						newest_activities = '';
+						activity_last_recorded  = 0;
+					}
+				}
+			});
+
+			return false;
+		}
+
+		// Spam activity stream items
+		if ( target.hasClass( 'spam-activity' ) ) {
+			li        = target.parents( 'div.activity ul li' );
+			timestamp = li.prop( 'class' ).match( /date-recorded-([0-9]+)/ );
+			target.addClass( 'loading' );
+
+			jq.post( ajaxurl, {
+				action: 'bp_spam_activity',
+				'cookie': encodeURIComponent( document.cookie ),
+				'id': li.attr( 'id' ).substr( 9, li.attr( 'id' ).length ),
+				'_wpnonce': target.attr( 'href' ).split( '_wpnonce=' )[1]
+			},
+
+			function(response) {
+				if ( response[0] + response[1] === '-1' ) {
+					li.prepend( response.substr( 2, response.length ) );
+					li.children( '#message' ).hide().fadeIn(300);
+				} else {
+					li.slideUp( 300 );
+					// reset vars to get newest activities
+					if ( timestamp && activity_last_recorded === timestamp[1] ) {
+						newest_activities = '';
+						activity_last_recorded  = 0;
+					}
+				}
+			});
+
+			return false;
+		}
+
+		/* Load more updates at the end of the page */
+		if ( target.parent().hasClass('load-more') ) {
+			if ( bp_ajax_request ) {
+				bp_ajax_request.abort();
+			}
+
+			jq('#buddypress li.load-more').addClass('loading');
+
+			oldest_page = activity_oldestpage + 1;
+			just_posted = [];
+
+			jq('.activity-list li.just-posted').each( function(){
+				just_posted.push( jq(this).attr('id').replace( 'activity-','' ) );
+			});
+
+			load_more_args = {
+				action: 'activity_get_older_updates',
+				'cookie': bp_get_cookies(),
+				'page': oldest_page,
+				'exclude_just_posted': just_posted.join(',')
+			};
+
+			load_more_search = bp_get_querystring('s');
+
+			if ( load_more_search ) {
+				load_more_args.search_terms = load_more_search;
+			}
+
+			bp_ajax_request = jq.post( ajaxurl, load_more_args,
+			function(response)
+			{
+				jq('#buddypress li.load-more').removeClass('loading');
+				activity_oldestpage = oldest_page;
+				jq('#buddypress ul.activity-list').append(response.contents);
+
+				target.parent().hide();
+			}, 'json' );
+
+			return false;
+		}
+
+		/* Load newest updates at the top of the list */
+		if ( target.parent().hasClass('load-newest') ) {
+
+			event.preventDefault();
+
+			target.parent().hide();
+
+			/**
+			 * If a plugin is updating the recorded_date of an activity
+			 * it will be loaded as a new one. We need to look in the
+			 * stream and eventually remove similar ids to avoid "double".
+			 */
+			activity_html = jq.parseHTML( newest_activities );
+
+			jq.each( activity_html, function( i, el ){
+				if( 'LI' === el.nodeName && jq(el).hasClass( 'just-posted' ) ) {
+					if( jq( '#' + jq(el).attr( 'id' ) ).length ) {
+						jq( '#' + jq(el).attr( 'id' ) ).remove();
+					}
+				}
+			} );
+
+			// Now the stream is cleaned, prepend newest
+			jq( '#buddypress ul.activity-list' ).prepend( newest_activities );
+
+			// reset the newest activities now they're displayed
+			newest_activities = '';
+		}
+	});
+
+	// Activity "Read More" links
+	jq('div.activity').on('click', '.activity-read-more a', function(event) {
+		var target = jq(event.target),
+			link_id = target.parent().attr('id').split('-'),
+			a_id    = link_id[3],
+			type    = link_id[0], /* activity or acomment */
+			inner_class, a_inner;
+
+		inner_class = type === 'acomment' ? 'acomment-content' : 'activity-inner';
+		a_inner = jq('#' + type + '-' + a_id + ' .' + inner_class + ':first' );
+		jq(target).addClass('loading');
+
+		jq.post( ajaxurl, {
+			action: 'get_single_activity_content',
+			'activity_id': a_id
+		},
+		function(response) {
+			jq(a_inner).slideUp(300).html(response).slideDown(300);
+		});
+
+		return false;
+	});
+
+	/**** Activity Comments *******************************************************/
+
+	/* Hide all activity comment forms */
+	jq('form.ac-form').hide();
+
+	/* Hide excess comments */
+	if ( jq('.activity-comments').length ) {
+		bp_legacy_theme_hide_comments();
+	}
+
+	/* Activity list event delegation */
+	jq('div.activity').on( 'click', function(event) {
+		var target = jq(event.target),
+			id, ids, a_id, c_id, form,
+			form_parent, form_id,
+			tmp_id, comment_id, comment,
+			ajaxdata,
+			ak_nonce,
+			show_all_a, new_count,
+			link_href, comment_li, nonce;
+
+		/* Comment / comment reply links */
+		if ( target.hasClass('acomment-reply') || target.parent().hasClass('acomment-reply') ) {
+			if ( target.parent().hasClass('acomment-reply') ) {
+				target = target.parent();
+			}
+
+			id  = target.attr('id');
+			ids = id.split('-');
+
+			a_id = ids[2];
+			c_id = target.attr('href').substr( 10, target.attr('href').length );
+			form = jq( '#ac-form-' + a_id );
+
+			form.css( 'display', 'none' );
+			form.removeClass('root');
+			jq('.ac-form').hide();
+
+			/* Hide any error messages */
+			form.children('div').each( function() {
+				if ( jq(this).hasClass( 'error' ) ) {
+					jq(this).hide();
+				}
+			});
+
+			if ( ids[1] !== 'comment' ) {
+				jq('#acomment-' + c_id).append( form );
+			} else {
+				jq('#activity-' + a_id + ' .activity-comments').append( form );
+			}
+
+			if ( form.parent().hasClass( 'activity-comments' ) ) {
+				form.addClass('root');
+			}
+
+			form.slideDown( 200 );
+			jq.scrollTo( form, 500, {
+				offset:-100,
+				easing:'swing'
+			} );
+			jq('#ac-form-' + ids[2] + ' textarea').focus();
+
+			return false;
+		}
+
+		/* Activity comment posting */
+		if ( target.attr('name') === 'ac_form_submit' ) {
+			form = target.parents( 'form' );
+			form_parent = form.parent();
+			form_id = form.attr('id').split('-');
+
+			if ( !form_parent.hasClass('activity-comments') ) {
+				tmp_id = form_parent.attr('id').split('-');
+				comment_id = tmp_id[1];
+			} else {
+				comment_id = form_id[2];
+			}
+
+			content = jq( '#' + form.attr('id') + ' textarea' );
+
+			/* Hide any error messages */
+			jq( '#' + form.attr('id') + ' div.error').hide();
+			target.addClass('loading').prop('disabled', true);
+			content.addClass('loading').prop('disabled', true);
+
+			ajaxdata = {
+				action: 'new_activity_comment',
+				'cookie': bp_get_cookies(),
+				'_wpnonce_new_activity_comment': jq('#_wpnonce_new_activity_comment').val(),
+				'comment_id': comment_id,
+				'form_id': form_id[2],
+				'content': content.val()
+			};
+
+			// Akismet
+			ak_nonce = jq('#_bp_as_nonce_' + comment_id).val();
+			if ( ak_nonce ) {
+				ajaxdata['_bp_as_nonce_' + comment_id] = ak_nonce;
+			}
+
+			jq.post( ajaxurl, ajaxdata, function(response) {
+				target.removeClass('loading');
+				content.removeClass('loading');
+
+				/* Check for errors and append if found. */
+				if ( response[0] + response[1] === '-1' ) {
+					form.append( jq( response.substr( 2, response.length ) ).hide().fadeIn( 200 ) );
+				} else {
+					var activity_comments = form.parent();
+					form.fadeOut( 200, function() {
+						if ( 0 === activity_comments.children('ul').length ) {
+							if ( activity_comments.hasClass('activity-comments') ) {
+								activity_comments.prepend('<ul></ul>');
+							} else {
+								activity_comments.append('<ul></ul>');
+							}
+						}
+
+						/* Preceding whitespace breaks output with jQuery 1.9.0 */
+						var the_comment = jq.trim( response );
+
+						activity_comments.children('ul').append( jq( the_comment ).hide().fadeIn( 200 ) );
+						form.children('textarea').val('');
+						activity_comments.parent().addClass('has-comments');
+					} );
+					jq( '#' + form.attr('id') + ' textarea').val('');
+
+					/* Increase the "Reply (X)" button count */
+					new_count = Number( jq('#activity-' + form_id[2] + ' a.acomment-reply span').html() ) + 1;
+					jq('#activity-' + form_id[2] + ' a.acomment-reply span').html( new_count );
+
+					// Increment the 'Show all x comments' string, if present
+					show_all_a = activity_comments.parents('.activity-comments').find('.show-all a');
+					if ( show_all_a ) {
+						show_all_a.html( BP_DTheme.show_x_comments.replace( '%d', new_count ) );
+					}
+				}
+
+				jq(target).prop('disabled', false);
+				jq(content).prop('disabled', false);
+			});
+
+			return false;
+		}
+
+		/* Deleting an activity comment */
+		if ( target.hasClass('acomment-delete') ) {
+			link_href = target.attr('href');
+			comment_li = target.parent().parent();
+			form = comment_li.parents('div.activity-comments').children('form');
+
+			nonce = link_href.split('_wpnonce=');
+			nonce = nonce[1];
+
+			comment_id = link_href.split('cid=');
+			comment_id = comment_id[1].split('&');
+			comment_id = comment_id[0];
+
+			target.addClass('loading');
+
+			/* Remove any error messages */
+			jq('.activity-comments ul .error').remove();
+
+			/* Reset the form position */
+			comment_li.parents('.activity-comments').append(form);
+
+			jq.post( ajaxurl, {
+				action: 'delete_activity_comment',
+				'cookie': bp_get_cookies(),
+				'_wpnonce': nonce,
+				'id': comment_id
+			},
+			function(response) {
+				/* Check for errors and append if found. */
+				if ( response[0] + response[1] === '-1' ) {
+					comment_li.prepend( jq( response.substr( 2, response.length ) ).hide().fadeIn( 200 ) );
+				} else {
+					var children  = jq( '#' + comment_li.attr('id') + ' ul' ).children('li'),
+						child_count = 0,
+						count_span, new_count, show_all_a;
+
+					jq(children).each( function() {
+						if ( !jq(this).is(':hidden') ) {
+							child_count++;
+						}
+					});
+					comment_li.fadeOut(200, function() {
+						comment_li.remove();
+					});
+
+					/* Decrease the "Reply (X)" button count */
+					count_span = jq('#' + comment_li.parents('#activity-stream > li').attr('id') + ' a.acomment-reply span');
+					new_count = count_span.html() - ( 1 + child_count );
+					count_span.html(new_count);
+
+					// Change the 'Show all x comments' text
+					show_all_a = comment_li.parents('.activity-comments').find('.show-all a');
+					if ( show_all_a ) {
+						show_all_a.html( BP_DTheme.show_x_comments.replace( '%d', new_count ) );
+					}
+
+					/* If that was the last comment for the item, remove the has-comments class to clean up the styling */
+					if ( 0 === new_count ) {
+						jq(comment_li.parents('#activity-stream > li')).removeClass('has-comments');
+					}
+				}
+			});
+
+			return false;
+		}
+
+		// Spam an activity stream comment
+		if ( target.hasClass( 'spam-activity-comment' ) ) {
+			link_href  = target.attr( 'href' );
+			comment_li = target.parent().parent();
+
+			target.addClass('loading');
+
+			// Remove any error messages
+			jq( '.activity-comments ul div.error' ).remove();
+
+			// Reset the form position
+			comment_li.parents( '.activity-comments' ).append( comment_li.parents( '.activity-comments' ).children( 'form' ) );
+
+			jq.post( ajaxurl, {
+				action: 'bp_spam_activity_comment',
+				'cookie': encodeURIComponent( document.cookie ),
+				'_wpnonce': link_href.split( '_wpnonce=' )[1],
+				'id': link_href.split( 'cid=' )[1].split( '&' )[0]
+			},
+
+			function ( response ) {
+				// Check for errors and append if found.
+				if ( response[0] + response[1] === '-1' ) {
+					comment_li.prepend( jq( response.substr( 2, response.length ) ).hide().fadeIn( 200 ) );
+
+				} else {
+					var children  = jq( '#' + comment_li.attr( 'id' ) + ' ul' ).children( 'li' ),
+						child_count = 0,
+						parent_li;
+
+					jq(children).each( function() {
+						if ( !jq( this ).is( ':hidden' ) ) {
+							child_count++;
+						}
+					});
+					comment_li.fadeOut( 200 );
+
+					// Decrease the "Reply (X)" button count
+					parent_li = comment_li.parents( '#activity-stream > li' );
+					jq( '#' + parent_li.attr( 'id' ) + ' a.acomment-reply span' ).html( jq( '#' + parent_li.attr( 'id' ) + ' a.acomment-reply span' ).html() - ( 1 + child_count ) );
+				}
+			});
+
+			return false;
+		}
+
+		/* Showing hidden comments - pause for half a second */
+		if ( target.parent().hasClass('show-all') ) {
+			target.parent().addClass('loading');
+
+			setTimeout( function() {
+				target.parent().parent().children('li').fadeIn(200, function() {
+					target.parent().remove();
+				});
+			}, 600 );
+
+			return false;
+		}
+
+		// Canceling an activity comment
+		if ( target.hasClass( 'ac-reply-cancel' ) ) {
+			jq(target).closest('.ac-form').slideUp( 200 );
+			return false;
+		}
+	});
+
+	/* Escape Key Press for cancelling comment forms */
+	jq(document).keydown( function(e) {
+		e = e || window.event;
+		if (e.target) {
+			element = e.target;
+		} else if (e.srcElement) {
+			element = e.srcElement;
+		}
+
+		if( element.nodeType === 3) {
+			element = element.parentNode;
+		}
+
+		if( e.ctrlKey === true || e.altKey === true || e.metaKey === true ) {
+			return;
+		}
+
+		var keyCode = (e.keyCode) ? e.keyCode : e.which;
+
+		if ( keyCode === 27 ) {
+			if (element.tagName === 'TEXTAREA') {
+				if ( jq(element).hasClass('ac-input') ) {
+					jq(element).parent().parent().parent().slideUp( 200 );
+				}
+			}
+		}
+	});
+
+	/**** Directory Search ****************************************************/
+
+	/* The search form on all directory pages */
+	jq( '.dir-search, .groups-members-search' ).on( 'click', function(event) {
+		if ( jq(this).hasClass('no-ajax') ) {
+			return;
+		}
+
+		var target = jq(event.target),
+			css_id, object, template, search_terms;
+
+		if ( target.attr('type') === 'submit' ) {
+			css_id = jq('.item-list-tabs li.selected').attr('id').split( '-' );
+			object = css_id[0];
+			template = null;
+			search_terms = target.parent().find( '#' + object + '_search' ).val();
+
+			// The Group Members page specifies its own template
+			if ( event.currentTarget.className === 'groups-members-search' ) {
+				object = 'group_members';
+				template = 'groups/single/members';
+			}
+
+			var scope  = bp_get_directory_preference( object, 'scope' );
+			var filter = bp_get_directory_preference( object, 'filter' );
+			var extras = bp_get_directory_preference( object, 'extras' );
+
+			bp_filter_request( object, filter, scope, 'div.' + object, search_terms, 1, extras, null, template );
+
+			return false;
+		}
+	});
+
+	/**** Tabs and Filters ****************************************************/
+
+	/* When a navigation tab is clicked - e.g. | All Groups | My Groups | */
+	jq('div.item-list-tabs').on( 'click', function(event) {
+		// If on a directory page with a type filter, add no-ajax class.
+		if ( jq( 'body' ).hasClass( 'type' ) && jq( 'body' ).hasClass( 'directory' ) ) {
+			jq(this).addClass( 'no-ajax' );
+		}
+
+		if ( jq(this).hasClass('no-ajax')  || jq( event.target ).hasClass('no-ajax') )  {
+			return;
+		}
+
+		var targetElem = ( event.target.nodeName === 'SPAN' ) ? event.target.parentNode : event.target,
+			target       = jq( targetElem ).parent(),
+			css_id, object, scope, filter, search_terms;
+
+		if ( 'LI' === target[0].nodeName && !target.hasClass( 'last' ) ) {
+			css_id = target.attr('id').split( '-' );
+			object = css_id[0];
+
+			if ( 'activity' === object ) {
+				return false;
+			}
+
+			scope = css_id[1];
+			filter = jq('#' + object + '-order-select select').val();
+			search_terms = jq('#' + object + '_search').val();
+
+			var extras = bp_get_directory_preference( object, 'extras' );
+
+			bp_filter_request( object, filter, scope, 'div.' + object, search_terms, 1, extras );
+
+			return false;
+		}
+	});
+
+	/* When the filter select box is changed re-query */
+	jq('li.filter select').change( function() {
+		var el,
+			css_id, object, scope, filter, search_terms, template,
+			$gm_search;
+
+		if ( jq('.item-list-tabs li.selected').length ) {
+			el = jq('.item-list-tabs li.selected');
+		} else {
+			el = jq(this);
+		}
+
+		css_id = el.attr('id').split('-');
+		object = css_id[0];
+		scope = css_id[1];
+		filter = jq(this).val();
+		search_terms = false;
+		template = null;
+
+		if ( jq('.dir-search input').length ) {
+			search_terms = jq('.dir-search input').val();
+		}
+
+		// The Group Members page has a different selector for its
+		// search terms box
+		$gm_search = jq( '.groups-members-search input' );
+		if ( $gm_search.length ) {
+			search_terms = $gm_search.val();
+			object = 'members';
+			scope = 'groups';
+		}
+
+		// On the Groups Members page, we specify a template
+		if ( 'members' === object && 'groups' === scope ) {
+			object = 'group_members';
+			template = 'groups/single/members';
+		}
+
+		if ( 'friends' === object ) {
+			object = 'members';
+		}
+
+		var extras = bp_get_directory_preference( object, 'extras' );
+
+		bp_filter_request( object, filter, scope, 'div.' + object, search_terms, 1, extras, null, template );
+
+		return false;
+	});
+
+	/* All pagination links run through this function */
+	jq('#buddypress').on( 'click', function(event) {
+		var target = jq(event.target),
+			el,
+			css_id, object, search_terms, pagination_id, template,
+			page_number,
+			$gm_search,
+			caller;
+
+		if ( target.hasClass('button') ) {
+			return true;
+		}
+
+		if ( target.parent().parent().hasClass('pagination') && !target.parent().parent().hasClass('no-ajax') ) {
+			if ( target.hasClass('dots') || target.hasClass('current') ) {
+				return false;
+			}
+
+			if ( jq('.item-list-tabs li.selected').length ) {
+				el = jq('.item-list-tabs li.selected');
+			} else {
+				el = jq('li.filter select');
+			}
+
+			css_id = el.attr('id').split( '-' );
+			object = css_id[0];
+			search_terms = false;
+			pagination_id = jq(target).closest('.pagination-links').attr('id');
+			template = null;
+
+			// Search terms
+			if ( jq('div.dir-search input').length ) {
+				search_terms =  jq('.dir-search input');
+
+				if ( ! search_terms.val() && bp_get_querystring( search_terms.attr( 'name' ) ) ) {
+					search_terms = jq('.dir-search input').prop('placeholder');
+				} else {
+					search_terms = search_terms.val();
+				}
+			}
+
+			// Page number
+			if ( jq(target).hasClass('next') || jq(target).hasClass('prev') ) {
+				page_number = jq('.pagination span.current').html();
+			} else {
+				page_number = jq(target).html();
+			}
+
+			// Remove any non-numeric characters from page number text (commas, etc.)
+			page_number = Number( page_number.replace(/\D/g,'') );
+
+			if ( jq(target).hasClass('next') ) {
+				page_number++;
+			} else if ( jq(target).hasClass('prev') ) {
+				page_number--;
+			}
+
+			// The Group Members page has a different selector for
+			// its search terms box
+			$gm_search = jq( '.groups-members-search input' );
+			if ( $gm_search.length ) {
+				search_terms = $gm_search.val();
+				object = 'members';
+			}
+
+			// On the Groups Members page, we specify a template
+			if ( 'members' === object && 'groups' === css_id[1] ) {
+				object = 'group_members';
+				template = 'groups/single/members';
+			}
+
+			// On the Admin > Requests page, we need to reset the object,
+			// since "admin" isn't specific enough
+			if ( 'admin' === object && jq( 'body' ).hasClass( 'membership-requests' ) ) {
+				object = 'requests';
+			}
+
+			if ( pagination_id.indexOf( 'pag-bottom' ) !== -1 ) {
+				caller = 'pag-bottom';
+			} else {
+				caller = null;
+			}
+
+			var scope  = bp_get_directory_preference( object, 'scope' );
+			var filter = bp_get_directory_preference( object, 'filter' );
+			var extras = bp_get_directory_preference( object, 'extras' );
+
+			bp_filter_request( object, filter, scope, 'div.' + object, search_terms, page_number, extras, caller, template );
+
+			return false;
+		}
+
+	});
+
+	/** Invite Friends Interface ****************************************/
+
+	/* Select a user from the list of friends and add them to the invite list */
+	jq('#send-invite-form').on( 'click', '#invite-list input', function() {
+		// invites-loop template contains a div with the .invite class
+		// We use the existence of this div to check for old- vs new-
+		// style templates.
+		var invites_new_template = jq( '#send-invite-form > .invite' ).length,
+			friend_id, friend_action;
+
+		jq('.ajax-loader').toggle();
+
+		// Dim the form until the response arrives
+		if ( invites_new_template ) {
+			jq( this ).parents( 'ul' ).find( 'input' ).prop( 'disabled', true );
+		}
+
+		friend_id = jq(this).val();
+
+		if ( jq(this).prop('checked') === true ) {
+			friend_action = 'invite';
+		} else {
+			friend_action = 'uninvite';
+		}
+
+		if ( ! invites_new_template ) {
+			jq( '.item-list-tabs li.selected' ).addClass( 'loading' );
+		}
+
+		jq.post( ajaxurl, {
+			action: 'groups_invite_user',
+			'friend_action': friend_action,
+			'cookie': bp_get_cookies(),
+			'_wpnonce': jq('#_wpnonce_invite_uninvite_user').val(),
+			'friend_id': friend_id,
+			'group_id': jq('#group_id').val()
+		},
+		function(response)
+		{
+			if ( jq('#message') ) {
+				jq('#message').hide();
+			}
+
+			if ( invites_new_template ) {
+				// With new-style templates, we refresh the
+				// entire list
+				bp_filter_request( 'invite', 'bp-invite-filter', 'bp-invite-scope', 'div.invite', false, 1, '', '', '' );
+			} else {
+				// Old-style templates manipulate only the
+				// single invitation element
+				jq('.ajax-loader').toggle();
+
+				if ( friend_action === 'invite' ) {
+					jq('#friend-list').append(response);
+				} else if ( friend_action === 'uninvite' ) {
+					jq('#friend-list li#uid-' + friend_id).remove();
+				}
+
+				jq('.item-list-tabs li.selected').removeClass('loading');
+			}
+		});
+	});
+
+	/* Remove a user from the list of users to invite to a group */
+	jq('#send-invite-form').on('click', 'a.remove', function() {
+		// invites-loop template contains a div with the .invite class
+		// We use the existence of this div to check for old- vs new-
+		// style templates.
+		var invites_new_template = jq('#send-invite-form > .invite').length,
+			friend_id = jq(this).attr('id');
+
+		jq('.ajax-loader').toggle();
+
+		friend_id = friend_id.split('-');
+		friend_id = friend_id[1];
+
+		jq.post( ajaxurl, {
+			action: 'groups_invite_user',
+			'friend_action': 'uninvite',
+			'cookie': bp_get_cookies(),
+			'_wpnonce': jq('#_wpnonce_invite_uninvite_user').val(),
+			'friend_id': friend_id,
+			'group_id': jq('#group_id').val()
+		},
+		function(response)
+		{
+			if ( invites_new_template ) {
+				// With new-style templates, we refresh the
+				// entire list
+				bp_filter_request( 'invite', 'bp-invite-filter', 'bp-invite-scope', 'div.invite', false, 1, '', '', '' );
+			} else {
+				// Old-style templates manipulate only the
+				// single invitation element
+				jq('.ajax-loader').toggle();
+				jq('#friend-list #uid-' + friend_id).remove();
+				jq('#invite-list #f-' + friend_id).prop('checked', false);
+			}
+		});
+
+		return false;
+	});
+
+	/** Profile Visibility Settings *********************************/
+	jq( '.visibility-toggle-link' ).on( 'click', function( event ) {
+		event.preventDefault();
+
+		jq( this ).attr( 'aria-expanded', 'true' ).parent().hide().addClass( 'field-visibility-settings-hide' )
+			.siblings( '.field-visibility-settings' ).show().addClass( 'field-visibility-settings-open' );
+	} );
+
+	jq( '.field-visibility-settings-close' ).on( 'click', function( event ) {
+		event.preventDefault();
+
+		jq( '.visibility-toggle-link' ).attr( 'aria-expanded', 'false' );
+
+		var settings_div = jq( this ).parent(),
+			vis_setting_text = settings_div.find( 'input:checked' ).parent().text();
+
+		settings_div.hide().removeClass( 'field-visibility-settings-open' )
+			.siblings( '.field-visibility-settings-toggle' )
+				.children( '.current-visibility-level' ).text( vis_setting_text ).end()
+			.show().removeClass( 'field-visibility-settings-hide' );
+	} );
+
+	jq('#profile-edit-form input:not(:submit), #profile-edit-form textarea, #profile-edit-form select, #signup_form input:not(:submit), #signup_form textarea, #signup_form select').change( function() {
+		var shouldconfirm = true;
+
+		jq('#profile-edit-form input:submit, #signup_form input:submit').on( 'click', function() {
+			shouldconfirm = false;
+		});
+
+		window.onbeforeunload = function(e) {
+			if ( shouldconfirm ) {
+				return BP_DTheme.unsaved_changes;
+			}
+		};
+	});
+
+	/** Friendship Requests **************************************/
+
+	/* Accept and Reject friendship request buttons */
+	jq('#friend-list a.accept, #friend-list a.reject').on( 'click', function() {
+		var button   = jq(this),
+			li         = jq(this).parents('#friend-list li'),
+			action_div = jq(this).parents('li div.action'),
+			id         = li.attr('id').substr( 11, li.attr('id').length ),
+			link_href  = button.attr('href'),
+			nonce      = link_href.split('_wpnonce=')[1],
+			action;
+
+		if ( jq(this).hasClass('accepted') || jq(this).hasClass('rejected') ) {
+			return false;
+		}
+
+		if ( jq(this).hasClass('accept') ) {
+			action = 'accept_friendship';
+			action_div.children('a.reject').css( 'visibility', 'hidden' );
+		} else {
+			action = 'reject_friendship';
+			action_div.children('a.accept').css( 'visibility', 'hidden' );
+		}
+
+		button.addClass('loading');
+
+		jq.post( ajaxurl, {
+			action: action,
+			'cookie': bp_get_cookies(),
+			'id': id,
+			'_wpnonce': nonce
+		},
+		function(response) {
+			button.removeClass('loading');
+
+			if ( response[0] + response[1] === '-1' ) {
+				li.prepend( response.substr( 2, response.length ) );
+				li.children('#message').hide().fadeIn(200);
+			} else {
+				button.fadeOut( 100, function() {
+					if ( jq(this).hasClass('accept') ) {
+						action_div.children('a.reject').hide();
+						jq(this).html( BP_DTheme.accepted ).contents().unwrap();
+					} else {
+						action_div.children('a.accept').hide();
+						jq(this).html( BP_DTheme.rejected ).contents().unwrap();
+					}
+				});
+			}
+		});
+
+		return false;
+	});
+
+	/* Add / Remove friendship buttons */
+	jq( '#members-dir-list, #members-group-list, #item-header' ).on('click', '.friendship-button a', function() {
+		jq(this).parent().addClass('loading');
+		var fid   = jq(this).attr('id'),
+			nonce   = jq(this).attr('href'),
+			thelink = jq(this);
+
+		fid = fid.split('-');
+		fid = fid[1];
+
+		nonce = nonce.split('?_wpnonce=');
+		nonce = nonce[1].split('&');
+		nonce = nonce[0];
+
+		jq.post( ajaxurl, {
+			action: 'addremove_friend',
+			'cookie': bp_get_cookies(),
+			'fid': fid,
+			'_wpnonce': nonce
+		},
+		function(response)
+		{
+			var action  = thelink.attr('rel');
+				parentdiv = thelink.parent();
+
+			if ( action === 'add' ) {
+				jq(parentdiv).fadeOut(200,
+					function() {
+						parentdiv.removeClass('add_friend');
+						parentdiv.removeClass('loading');
+						parentdiv.addClass('pending_friend');
+						parentdiv.fadeIn(200).html(response);
+					}
+					);
+
+			} else if ( action === 'remove' ) {
+				jq(parentdiv).fadeOut(200,
+					function() {
+						parentdiv.removeClass('remove_friend');
+						parentdiv.removeClass('loading');
+						parentdiv.addClass('add');
+						parentdiv.fadeIn(200).html(response);
+					}
+					);
+			}
+		});
+		return false;
+	} );
+
+	/** Group Join / Leave Buttons **************************************/
+
+	// Confirmation when clicking Leave Group in group headers
+	jq('#buddypress').on('click', '.group-button .leave-group', function() {
+		if ( false === confirm( BP_DTheme.leave_group_confirm ) ) {
+			return false;
+		}
+	});
+
+	jq('#groups-dir-list').on('click', '.group-button a', function() {
+		var gid   = jq(this).parent().attr('id'),
+			nonce   = jq(this).attr('href'),
+			thelink = jq(this);
+
+		gid = gid.split('-');
+		gid = gid[1];
+
+		nonce = nonce.split('?_wpnonce=');
+		nonce = nonce[1].split('&');
+		nonce = nonce[0];
+
+		// Leave Group confirmation within directories - must intercept
+		// AJAX request
+		if ( thelink.hasClass( 'leave-group' ) && false === confirm( BP_DTheme.leave_group_confirm ) ) {
+			return false;
+		}
+
+		jq.post( ajaxurl, {
+			action: 'joinleave_group',
+			'cookie': bp_get_cookies(),
+			'gid': gid,
+			'_wpnonce': nonce
+		},
+		function(response)
+		{
+			var parentdiv = thelink.parent();
+
+			// user groups page
+			if ( ! jq('body.directory').length ) {
+				window.location.reload();
+
+			// groups directory
+			} else {
+				jq(parentdiv).fadeOut(200,
+					function() {
+						parentdiv.fadeIn(200).html(response);
+
+						var mygroups = jq('#groups-personal span'),
+							add        = 1;
+
+						if( thelink.hasClass( 'leave-group' ) ) {
+							// hidden groups slide up
+							if ( parentdiv.hasClass( 'hidden' ) ) {
+								parentdiv.closest('li').slideUp( 200 );
+							}
+
+							add = 0;
+						} else if ( thelink.hasClass( 'request-membership' ) ) {
+							add = false;
+						}
+
+						// change the "My Groups" value
+						if ( mygroups.length && add !== false ) {
+							if ( add ) {
+								mygroups.text( ( mygroups.text() >> 0 ) + 1 );
+							} else {
+								mygroups.text( ( mygroups.text() >> 0 ) - 1 );
+							}
+						}
+
+					}
+				);
+			}
+		});
+		return false;
+	} );
+
+	// Fix hidden group visibility with themes using the .hidden CSS rule.
+	jq('#groups-list li.hidden').each(function() {
+		if ( jq(this).css('display') === 'none' ) {
+			jq(this).css('cssText', 'display: list-item !important');
+		}
+	});
+
+	/** Button disabling ************************************************/
+
+	jq('#buddypress').on( 'click', '.pending', function() {
+		return false;
+	});
+
+	/** Registration ***********************************************/
+
+	if ( jq('body').hasClass('register') ) {
+		var blog_checked = jq('#signup_with_blog');
+
+		// hide "Blog Details" block if not checked by default
+		if ( ! blog_checked.prop('checked') ) {
+			jq('#blog-details').toggle();
+		}
+
+		// toggle "Blog Details" block whenever checkbox is checked
+		blog_checked.change(function() {
+			jq('#blog-details').toggle();
+		});
+	}
+
+	/** Private Messaging ******************************************/
+
+	/** Message search */
+	jq('.message-search').on( 'click', function(event) {
+		if ( jq(this).hasClass('no-ajax') ) {
+			return;
+		}
+
+		var target = jq(event.target),
+			object;
+
+		if ( target.attr('type') === 'submit' || target.attr('type') === 'button' ) {
+			object = 'messages';
+
+			var scope  = bp_get_directory_preference( object, 'scope' );
+			var filter = bp_get_directory_preference( object, 'filter' );
+			var extras = bp_get_directory_preference( object, 'extras' );
+
+			bp_filter_request(
+				object,
+				filter,
+				scope,
+				'div.' + object, jq('#messages_search').val(),
+				1,
+				extras
+			);
+
+			return false;
+		}
+	});
+
+	/* AJAX send reply functionality */
+	jq('#send_reply_button').click(
+		function() {
+			var order = jq('#messages_order').val() || 'ASC',
+				offset  = jq('#message-recipients').offset(),
+				button  = jq('#send_reply_button');
+
+			jq(button).addClass('loading').prop( 'disabled', true );
+
+			jq.post( ajaxurl, {
+				action: 'messages_send_reply',
+				'cookie': bp_get_cookies(),
+				'_wpnonce': jq('#send_message_nonce').val(),
+
+				'content': jq('#message_content').val(),
+				'send_to': jq('#send_to').val(),
+				'subject': jq('#subject').val(),
+				'thread_id': jq('#thread_id').val()
+			},
+			function(response)
+			{
+				if ( response[0] + response[1] === '-1' ) {
+					jq('#send-reply').prepend( response.substr( 2, response.length ) );
+				} else {
+					jq('#send-reply #message').remove();
+					jq('#message_content').val('');
+
+					if ( 'ASC' === order ) {
+						jq('#send-reply').before( response );
+					} else {
+						jq('#message-recipients').after( response );
+						jq(window).scrollTop(offset.top);
+					}
+
+					jq('.new-message').hide().slideDown( 200, function() {
+						jq('.new-message').removeClass('new-message');
+					});
+				}
+				jq(button).removeClass('loading').prop( 'disabled', false );
+			});
+
+			return false;
+		}
+	);
+
+	/* Selecting unread and read messages in inbox */
+	jq( 'body.messages #item-body div.messages' ).on( 'change', '#message-type-select', function() {
+		var selection   = this.value,
+			checkboxes    = jq( 'td input[type="checkbox"]' ),
+			checked_value = 'checked';
+
+		checkboxes.each( function(i) {
+			checkboxes[i].checked = '';
+		});
+
+		switch ( selection ) {
+			case 'unread':
+				checkboxes = jq('tr.unread td input[type="checkbox"]');
+				break;
+			case 'read':
+				checkboxes = jq('tr.read td input[type="checkbox"]');
+				break;
+			case '':
+				checked_value = '';
+				break;
+		}
+
+		checkboxes.each( function(i) {
+			checkboxes[i].checked = checked_value;
+		});
+	});
+
+	/* Selecting/Deselecting all messages */
+	jq('#select-all-messages').click(function(event) {
+		if( this.checked ) {
+			jq('.message-check').each(function() {
+				this.checked = true;
+			});
+		} else {
+			jq('.message-check').each(function() {
+				this.checked = false;
+			});
+		}
+	});
+
+	/* Make sure a 'Bulk Action' is selected before submitting the messages bulk action form */
+	jq('#messages-bulk-manage').attr('disabled', 'disabled');
+
+	/* Remove the disabled attribute from the messages form submit button when bulk action has a value */
+	jq('#messages-select').on('change', function(){
+		jq('#messages-bulk-manage').attr('disabled', jq(this).val().length <= 0);
+	});
+
+	/* Star action function */
+	starAction = function() {
+		var link = jq(this);
+
+		jq.post( ajaxurl, {
+			action: 'messages_star',
+			'message_id': link.data('message-id'),
+			'star_status': link.data('star-status'),
+			'nonce': link.data('star-nonce'),
+			'bulk': link.data('star-bulk')
+		},
+		function(response) {
+			if ( 1 === parseInt( response, 10 ) ) {
+				if ( 'unstar' === link.data('star-status') ) {
+					link.data('star-status', 'star');
+					link.removeClass('message-action-unstar').addClass('message-action-star');
+					link.find('.bp-screen-reader-text').text( BP_PM_Star.strings.text_star );
+
+					if ( 1 === BP_PM_Star.is_single_thread ) {
+						link.attr('data-bp-tooltip', BP_PM_Star.strings.title_star );
+					} else {
+						link.attr('data-bp-tooltip', BP_PM_Star.strings.title_star_thread );
+					}
+
+				} else {
+					link.data('star-status', 'unstar');
+					link.removeClass('message-action-star').addClass('message-action-unstar');
+					link.find('.bp-screen-reader-text').text(BP_PM_Star.strings.text_unstar);
+
+					if ( 1 === BP_PM_Star.is_single_thread ) {
+						link.attr('data-bp-tooltip', BP_PM_Star.strings.title_unstar );
+					} else {
+						link.attr('data-bp-tooltip', BP_PM_Star.strings.title_unstar_thread );
+					}
+				}
+			}
+		});
+		return false;
+	};
+
+	/* Star actions */
+	jq('#message-threads').on('click', 'td.thread-star a', starAction );
+	jq('#message-thread').on('click', '.message-star-actions a', starAction );
+
+	/* Star bulk manage - Show only the valid action based on the starred item. */
+	jq('#message-threads td.bulk-select-check :checkbox').on('change', function() {
+		var box = jq(this),
+			star = box.closest('tr').find('.thread-star a');
+
+		if ( box.prop('checked') ) {
+			if( 'unstar' === star.data('star-status') ) {
+				BP_PM_Star.star_counter++;
+			} else {
+				BP_PM_Star.unstar_counter++;
+			}
+		} else {
+			if( 'unstar' === star.data('star-status') ) {
+				BP_PM_Star.star_counter--;
+			} else {
+				BP_PM_Star.unstar_counter--;
+			}
+		}
+
+		if ( BP_PM_Star.star_counter > 0 && parseInt( BP_PM_Star.unstar_counter, 10 ) === 0 ) {
+			jq('option[value="star"]').hide();
+		} else {
+			jq('option[value="star"]').show();
+		}
+
+		if ( BP_PM_Star.unstar_counter > 0 && parseInt( BP_PM_Star.star_counter, 10 ) === 0 ) {
+			jq('option[value="unstar"]').hide();
+		} else {
+			jq('option[value="unstar"]').show();
+		}
+	});
+
+	/** Notifications **********************************************/
+
+	/* Selecting/Deselecting all notifications */
+	jq('#select-all-notifications').click(function(event) {
+		if( this.checked ) {
+			jq('.notification-check').each(function() {
+				this.checked = true;
+			});
+		} else {
+			jq('.notification-check').each(function() {
+				this.checked = false;
+			});
+		}
+	});
+
+	/* Make sure a 'Bulk Action' is selected before submitting the form */
+	jq('#notification-bulk-manage').attr('disabled', 'disabled');
+
+	/* Remove the disabled attribute from the form submit button when bulk action has a value */
+	jq('#notification-select').on('change', function(){
+		jq('#notification-bulk-manage').attr('disabled', jq(this).val().length <= 0);
+	});
+
+	/* Close site wide notices in the sidebar */
+	jq('#close-notice').on( 'click', function() {
+		jq(this).addClass('loading');
+		jq('#sidebar div.error').remove();
+
+		jq.post( ajaxurl, {
+			action: 'messages_close_notice',
+			'notice_id': jq('.notice').attr('rel').substr( 2, jq('.notice').attr('rel').length ),
+			nonce: jq( '#close-notice-nonce' ).val()
+		},
+		function(response) {
+			jq('#close-notice').removeClass('loading');
+
+			if ( response[0] + response[1] === '-1' ) {
+				jq('.notice').prepend( response.substr( 2, response.length ) );
+				jq( '#sidebar div.error').hide().fadeIn( 200 );
+			} else {
+				jq('.notice').slideUp( 100 );
+			}
+		});
+		return false;
+	});
+
+	/* Toolbar & wp_list_pages JavaScript IE6 hover class */
+	jq('#wp-admin-bar ul.main-nav li, #nav li').mouseover( function() {
+		jq(this).addClass('sfhover');
+	});
+
+	jq('#wp-admin-bar ul.main-nav li, #nav li').mouseout( function() {
+		jq(this).removeClass('sfhover');
+	});
+
+	/* Clear BP cookies on logout */
+	jq('#wp-admin-bar-logout, a.logout').on( 'click', function() {
+		jq.removeCookie('bp-activity-scope', {
+			path: '/',
+			secure: ( 'https:' === window.location.protocol )
+		});
+		jq.removeCookie('bp-activity-filter', {
+			path: '/',
+			secure: ( 'https:' === window.location.protocol )
+		});
+		jq.removeCookie('bp-activity-oldestpage', {
+			path: '/',
+			secure: ( 'https:' === window.location.protocol )
+		});
+
+		var objects = [ 'members', 'groups', 'blogs', 'forums' ];
+		jq(objects).each( function(i) {
+			jq.removeCookie('bp-' + objects[i] + '-scope', {
+				path: '/',
+				secure: ( 'https:' === window.location.protocol )
+			} );
+			jq.removeCookie('bp-' + objects[i] + '-filter', {
+				path: '/',
+				secure: ( 'https:' === window.location.protocol )
+			} );
+			jq.removeCookie('bp-' + objects[i] + '-extras', {
+				path: '/',
+				secure: ( 'https:' === window.location.protocol )
+			} );
+		});
+	});
+
+	/* if js is enabled then replace the no-js class by a js one */
+	if( jq('body').hasClass('no-js') ) {
+		jq('body').attr('class', jq('body').attr('class').replace( /no-js/,'js' ) );
+	}
+
+	/** Activity HeartBeat ************************************************/
+
+	// Set the interval and the namespace event
+	if ( typeof wp !== 'undefined' && typeof wp.heartbeat !== 'undefined' && typeof BP_DTheme.pulse !== 'undefined' ) {
+
+		wp.heartbeat.interval( Number( BP_DTheme.pulse ) );
+
+		jq.fn.extend({
+			'heartbeat-send': function() {
+				return this.bind( 'heartbeat-send.buddypress' );
+			}
+		});
+	}
+
+	// Set the last id to request after
+	var first_item_recorded = 0;
+	jq( document ).on( 'heartbeat-send.buddypress', function( e, data ) {
+
+		first_item_recorded = 0;
+
+		// First row is default latest activity id
+		if ( jq( '#buddypress ul.activity-list li' ).first().prop( 'id' ) ) {
+			// getting the timestamp
+			timestamp = jq( '#buddypress ul.activity-list li' ).first().prop( 'class' ).match( /date-recorded-([0-9]+)/ );
+
+			if ( timestamp ) {
+				first_item_recorded = timestamp[1];
+			}
+		}
+
+		if ( 0 === activity_last_recorded || Number( first_item_recorded ) > activity_last_recorded ) {
+			activity_last_recorded = Number( first_item_recorded );
+		}
+
+		data.bp_activity_last_recorded = activity_last_recorded;
+
+		last_recorded_search = bp_get_querystring('s');
+
+		if ( last_recorded_search ) {
+			data.bp_activity_last_recorded_search_terms = last_recorded_search;
+		}
+	});
+
+	// Increment newest_activities and activity_last_recorded if data has been returned
+	jq( document ).on( 'heartbeat-tick', function( e, data ) {
+
+		// Only proceed if we have newest activities
+		if ( ! data.bp_activity_newest_activities ) {
+			return;
+		}
+
+		newest_activities = data.bp_activity_newest_activities.activities + newest_activities;
+		activity_last_recorded  = Number( data.bp_activity_newest_activities.last_recorded );
+
+		if ( jq( '#buddypress ul.activity-list li' ).first().hasClass( 'load-newest' ) ) {
+			return;
+		}
+
+		jq( '#buddypress ul.activity-list' ).prepend( '<li class="load-newest"><a href="#newest">' + BP_DTheme.newest + '</a></li>' );
+	});
+});
+
+/**
+ * Gets the user's current preference for a directory option.
+ */
+function bp_get_directory_preference( directoryType, pref ) {
+	var defaultPrefs = {
+		filter: '',
+		scope: '',
+		extras: ''
+	};
+
+	if ( ! directoryPreferences.hasOwnProperty( directoryType ) ) {
+		var newPreferences = {};
+		for ( var prefName in defaultPrefs ) {
+			if ( defaultPrefs.hasOwnProperty( prefName ) ) {
+				newPreferences[ prefName ] = defaultPrefs[ prefName ];
+			}
+		}
+		directoryPreferences[ directoryType ] = newPreferences;
+	}
+
+	if ( BP_DTheme.store_filter_settings ) {
+		directoryPreferences[ directoryType ][ pref ] = jq.cookie( 'bp-' + directoryType + '-' + pref );
+	}
+
+	return directoryPreferences[ directoryType ][ pref ];
+}
+
+/**
+ * Sets the user's current preference for a directory option.
+ */
+function bp_set_directory_preference( directoryType, pref, value ) {
+	var defaultPrefs = {
+		filter: '',
+		scope: '',
+		extras: ''
+	};
+
+	if ( ! directoryPreferences.hasOwnProperty( directoryType ) ) {
+		var newPreferences = {};
+		for ( var prefName in defaultPrefs ) {
+			if ( defaultPrefs.hasOwnProperty( prefName ) ) {
+				newPreferences[ prefName ] = defaultPrefs[ prefName ];
+			}
+		}
+		directoryPreferences[ directoryType ] = newPreferences;
+	}
+
+	if ( BP_DTheme.store_filter_settings ) {
+		jq.cookie( 'bp-' + directoryType + '-' + pref, value, {
+			path: '/',
+			secure: ( 'https:' === window.location.protocol )
+		} );
+	}
+
+	directoryPreferences[ directoryType ][ pref ] = value;
+}
+
+/* Setup activity scope and filter based on the current cookie settings. */
+function bp_init_activity() {
+	var scope  = bp_get_directory_preference( 'activity', 'scope' );
+	var filter = bp_get_directory_preference( 'activity', 'filter' );
+
+	if ( undefined !== filter && jq('#activity-filter-select').length ) {
+		jq('#activity-filter-select select option[value="' + filter + '"]').prop( 'selected', true );
+	}
+
+	/* Activity Tab Set */
+	if ( undefined !== scope && jq('.activity-type-tabs').length ) {
+		jq('.activity-type-tabs li').each( function() {
+			jq(this).removeClass('selected');
+		});
+		jq('#activity-' + scope + ', .item-list-tabs li.current').addClass('selected');
+	}
+}
+
+/* Setup object scope and filter based on the current cookie settings for the object. */
+function bp_init_objects(objects) {
+	jq(objects).each( function(i) {
+		var scope  = bp_get_directory_preference( objects[i], 'scope' );
+		var filter = bp_get_directory_preference( objects[i], 'filter' );
+
+		if ( undefined !== filter && jq('#' + objects[i] + '-order-select select').length ) {
+			jq('#' + objects[i] + '-order-select select option[value="' + filter + '"]').prop( 'selected', true );
+		}
+
+		if ( undefined !== scope && jq('div.' + objects[i]).length ) {
+			jq('.item-list-tabs li').each( function() {
+				jq(this).removeClass('selected');
+			});
+			jq('#' + objects[i] + '-' + scope + ', #object-nav li.current').addClass('selected');
+		}
+	});
+}
+
+/* Filter the current content list (groups/members/blogs/topics) */
+function bp_filter_request( object, filter, scope, target, search_terms, page, extras, caller, template ) {
+	if ( 'activity' === object ) {
+		return false;
+	}
+
+	if ( null === scope ) {
+		scope = 'all';
+	}
+
+	/* Save the settings we want to remain persistent */
+	bp_set_directory_preference( object, 'scope', scope );
+	bp_set_directory_preference( object, 'filter', filter );
+	bp_set_directory_preference( object, 'extras', extras );
+
+	/* Set the correct selected nav and filter */
+	jq('.item-list-tabs li').each( function() {
+		jq(this).removeClass('selected');
+	});
+	jq('#' + object + '-' + scope + ', #object-nav li.current').addClass('selected');
+	jq('.item-list-tabs li.selected').addClass('loading');
+	jq('.item-list-tabs select option[value="' + filter + '"]').prop( 'selected', true );
+
+	if ( 'friends' === object || 'group_members' === object ) {
+		object = 'members';
+	}
+
+	if ( bp_ajax_request ) {
+		bp_ajax_request.abort();
+	}
+
+	bp_ajax_request = jq.post( ajaxurl, {
+		action: object + '_filter',
+		'cookie': bp_get_cookies(),
+		'object': object,
+		'filter': filter,
+		'search_terms': search_terms,
+		'scope': scope,
+		'page': page,
+		'extras': extras,
+		'template': template
+	},
+	function(response)
+	{
+		/* animate to top if called from bottom pagination */
+		if ( caller === 'pag-bottom' && jq('#subnav').length ) {
+			var top = jq('#subnav').parent();
+			jq('html,body').animate({scrollTop: top.offset().top}, 'slow', function() {
+				jq(target).fadeOut( 100, function() {
+					jq(this).html(response);
+					jq(this).fadeIn(100);
+				});
+			});
+
+		} else {
+			jq(target).fadeOut( 100, function() {
+				jq(this).html(response);
+				jq(this).fadeIn(100);
+			});
+		}
+
+		jq('.item-list-tabs li.selected').removeClass('loading');
+	});
+}
+
+/* Activity Loop Requesting */
+function bp_activity_request(scope, filter) {
+	/* Save the type and filter */
+	bp_set_directory_preference( 'activity', 'scope', scope );
+	bp_set_directory_preference( 'activity', 'filter', filter );
+
+	/* Remove selected and loading classes from tabs */
+	jq('.item-list-tabs li').each( function() {
+		jq(this).removeClass('selected loading');
+	});
+	/* Set the correct selected nav and filter */
+	jq('#activity-' + scope + ', .item-list-tabs li.current').addClass('selected');
+	jq('#object-nav.item-list-tabs li.selected, div.activity-type-tabs li.selected').addClass('loading');
+	jq('#activity-filter-select select option[value="' + filter + '"]').prop( 'selected', true );
+
+	/* Reload the activity stream based on the selection */
+	jq('.widget_bp_activity_widget h2 span.ajax-loader').show();
+
+	if ( bp_ajax_request ) {
+		bp_ajax_request.abort();
+	}
+
+	// Get directory preferences (called "cookie" for legacy reasons).
+	var cookies = {
+		'bp-activity-filter': bp_get_directory_preference( 'activity', 'filter' ),
+		'bp-activity-scope': bp_get_directory_preference( 'activity', 'scope' )
+	};
+
+	var cookie = encodeURIComponent( jq.param( cookies ) );
+
+	bp_ajax_request = jq.post( ajaxurl, {
+		action: 'activity_widget_filter',
+		'cookie': cookie,
+		'_wpnonce_activity_filter': jq('#_wpnonce_activity_filter').val(),
+		'scope': scope,
+		'filter': filter
+	},
+	function(response)
+	{
+		jq('.widget_bp_activity_widget h2 span.ajax-loader').hide();
+
+		jq('div.activity').fadeOut( 100, function() {
+			jq(this).html(response.contents);
+			jq(this).fadeIn(100);
+
+			/* Selectively hide comments */
+			bp_legacy_theme_hide_comments();
+		});
+
+		/* Update the feed link */
+		if ( undefined !== response.feed_url ) {
+			jq('.directory #subnav li.feed a, .home-page #subnav li.feed a').attr('href', response.feed_url);
+		}
+
+		jq('.item-list-tabs li.selected').removeClass('loading');
+
+	}, 'json' );
+}
+
+/* Hide long lists of activity comments, only show the latest five root comments. */
+function bp_legacy_theme_hide_comments() {
+	var comments_divs = jq('div.activity-comments'),
+		parent_li, comment_lis, comment_count;
+
+	if ( !comments_divs.length ) {
+		return false;
+	}
+
+	comments_divs.each( function() {
+		if ( jq(this).children('ul').children('li').length < 5 ) {
+			return;
+		}
+
+		comments_div = jq(this);
+		parent_li = comments_div.parents('#activity-stream > li');
+		comment_lis = jq(this).children('ul').children('li');
+		comment_count = ' ';
+
+		if ( jq('#' + parent_li.attr('id') + ' li').length ) {
+			comment_count = jq('#' + parent_li.attr('id') + ' li').length;
+		}
+
+		comment_lis.each( function(i) {
+			/* Show the latest 5 root comments */
+			if ( i < comment_lis.length - 5 ) {
+				jq(this).hide();
+
+				if ( !i ) {
+					jq(this).before( '<li class="show-all"><a href="#' + parent_li.attr('id') + '/show-all/">' + BP_DTheme.show_x_comments.replace( '%d', comment_count ) + '</a></li>' );
+				}
+			}
+		});
+
+	});
+}
+
+/* Helper Functions */
+
+function checkAll() {
+	var checkboxes = document.getElementsByTagName('input'),
+		i;
+
+	for(i=0; i<checkboxes.length; i++) {
+		if(checkboxes[i].type === 'checkbox') {
+			if($('check_all').checked === '') {
+				checkboxes[i].checked = '';
+			}
+			else {
+				checkboxes[i].checked = 'checked';
+			}
+		}
+	}
+}
+
+/**
+ * Deselects any select options or input options for the specified field element.
+ *
+ * @param {String} container HTML ID of the field
+ * @since 1.2.0
+ */
+function clear( container ) {
+	container = document.getElementById( container );
+	if ( ! container ) {
+		return;
+	}
+
+	var radioButtons = container.getElementsByTagName( 'INPUT' ),
+		options = container.getElementsByTagName( 'OPTION' ),
+		i       = 0;
+
+	if ( radioButtons ) {
+		for ( i = 0; i < radioButtons.length; i++ ) {
+			radioButtons[i].checked = '';
+		}
+	}
+
+	if ( options ) {
+		for ( i = 0; i < options.length; i++ ) {
+			options[i].selected = false;
+		}
+	}
+}
+
+/* Returns a querystring of BP cookies (cookies beginning with 'bp-') */
+function bp_get_cookies() {
+	var allCookies = document.cookie.split(';'),  // get all cookies and split into an array
+		bpCookies      = {},
+		cookiePrefix   = 'bp-',
+		i, cookie, delimiter, name, value;
+
+	// loop through cookies
+	for (i = 0; i < allCookies.length; i++) {
+		cookie    = allCookies[i];
+		delimiter = cookie.indexOf('=');
+		name      = jq.trim( unescape( cookie.slice(0, delimiter) ) );
+		value     = unescape( cookie.slice(delimiter + 1) );
+
+		// if BP cookie, store it
+		if ( name.indexOf(cookiePrefix) === 0 ) {
+			bpCookies[name] = value;
+		}
+	}
+
+	// returns BP cookies as querystring
+	return encodeURIComponent( jq.param(bpCookies) );
+}
+
+/**
+ * Get a querystring parameter from a URL.
+ *
+ * @param {String} Query string parameter name.
+ * @param {String} URL to parse. Defaults to current URL.
+ */
+function bp_get_query_var( param, url ) {
+	var qs = {};
+
+	// Use current URL if no URL passed.
+	if ( typeof url === 'undefined' ) {
+		url = location.search.substr(1).split('&');
+	} else {
+		url = url.split('?')[1].split('&');
+	}
+
+	// Parse querystring into object props.
+	// http://stackoverflow.com/a/21152762
+	url.forEach(function(item) {
+		qs[item.split('=')[0]] = item.split('=')[1] && decodeURIComponent( item.split('=')[1] );
+	});
+
+	if ( qs.hasOwnProperty( param ) && qs[param] != null ) {
+		return qs[param];
+	} else {
+		return false;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-legacy/js/password-verify.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-legacy/js/password-verify.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-legacy/js/password-verify.js	(revision 12265)
@@ -0,0 +1,44 @@
+/* jshint undef: false */
+/* @version 3.0.0 */
+/* Password Verify */
+( function( $ ){
+	function check_pass_strength() {
+		var pass1 = $( '.password-entry' ).val(),
+		    pass2 = $( '.password-entry-confirm' ).val(),
+		    strength;
+
+		// Reset classes and result text
+		$( '#pass-strength-result' ).removeClass( 'short bad good strong' );
+		if ( ! pass1 ) {
+			$( '#pass-strength-result' ).html( pwsL10n.empty );
+			return;
+		}
+
+		strength = wp.passwordStrength.meter( pass1, wp.passwordStrength.userInputBlacklist(), pass2 );
+
+		switch ( strength ) {
+			case 2:
+				$( '#pass-strength-result' ).addClass( 'bad' ).html( pwsL10n.bad );
+				break;
+			case 3:
+				$( '#pass-strength-result' ).addClass( 'good' ).html( pwsL10n.good );
+				break;
+			case 4:
+				$( '#pass-strength-result' ).addClass( 'strong' ).html( pwsL10n.strong );
+				break;
+			case 5:
+				$( '#pass-strength-result' ).addClass( 'short' ).html( pwsL10n.mismatch );
+				break;
+			default:
+				$( '#pass-strength-result' ).addClass( 'short' ).html( pwsL10n['short'] );
+				break;
+		}
+	}
+
+	// Bind check_pass_strength to keyup events in the password fields
+	$( document ).ready( function() {
+		$( '.password-entry' ).val( '' ).keyup( check_pass_strength );
+		$( '.password-entry-confirm' ).val( '' ).keyup( check_pass_strength );
+	});
+
+} )( jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress-functions.php	(revision 12265)
@@ -0,0 +1,659 @@
+<?php
+/**
+ * Functions of BuddyPress's "Nouveau" template pack.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ *
+ * @buddypress-template-pack {
+ *   Template Pack ID:       nouveau
+ *   Template Pack Name:     BP Nouveau
+ *   Version:                1.0.0
+ *   WP required version:    4.5.0
+ *   BP required version:    3.0.0
+ *   Description:            A new template pack for BuddyPress!
+ *   Text Domain:            bp-nouveau
+ *   Domain Path:            /languages/
+ *   Author:                 The BuddyPress community
+ *   Template Pack Supports: activity, blogs, friends, groups, messages, notifications, settings, xprofile
+ * }}
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/** Theme Setup ***************************************************************/
+
+/**
+ * Loads BuddyPress Nouveau Template pack functionality.
+ *
+ * See @link BP_Theme_Compat() for more.
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau extends BP_Theme_Compat {
+	/**
+	 * Instance of this class.
+	 */
+	protected static $instance = null;
+
+	/**
+	 * Return the instance of this class.
+	 *
+	 * @since 3.0.0
+	 */
+	public static function get_instance() {
+		if ( null === self::$instance ) {
+			self::$instance = new self;
+		}
+
+		return self::$instance;
+	}
+
+	/**
+	 * The BP Nouveau constructor.
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		parent::start();
+
+		$this->includes();
+		$this->setup_support();
+	}
+
+	/**
+	 * BP Nouveau global variables.
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$bp = buddypress();
+
+		foreach ( $bp->theme_compat->packages['nouveau'] as $property => $value ) {
+			$this->{$property} = $value;
+		}
+
+		$this->includes_dir  = trailingslashit( $this->dir ) . 'includes/';
+		$this->directory_nav = new BP_Core_Nav();
+	}
+
+	/**
+	 * Includes!
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require $this->includes_dir . 'functions.php';
+		require $this->includes_dir . 'classes.php';
+		require $this->includes_dir . 'template-tags.php';
+
+		// Test suite requires the AJAX functions early.
+		if ( function_exists( 'tests_add_filter' ) ) {
+			require $this->includes_dir . 'ajax.php';
+
+		// Load AJAX code only on AJAX requests.
+		} else {
+			add_action( 'admin_init', function() {
+				if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX ) {
+					require bp_nouveau()->includes_dir . 'ajax.php';
+				}
+			}, 0 );
+		}
+
+		add_action( 'bp_customize_register', function() {
+			if ( bp_is_root_blog() && current_user_can( 'customize' ) ) {
+				require bp_nouveau()->includes_dir . 'customizer.php';
+			}
+		}, 0 );
+
+		foreach ( bp_core_get_packaged_component_ids() as $component ) {
+			$component_loader = trailingslashit( $this->includes_dir ) . $component . '/loader.php';
+
+			if ( ! bp_is_active( $component ) || ! file_exists( $component_loader ) ) {
+				continue;
+			}
+
+			require( $component_loader );
+		}
+
+		/**
+		 * Fires after all of the BuddyPress Nouveau includes have been loaded. Passed by reference.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param BP_Nouveau $value Current BP_Nouveau instance.
+		 */
+		do_action_ref_array( 'bp_nouveau_includes', array( &$this ) );
+	}
+
+	/**
+	 * Setup the Template Pack features support.
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_support() {
+		$width         = 1300;
+		$top_offset    = 150;
+
+		/** This filter is documented in bp-core/bp-core-avatars.php. */
+		$avatar_height = apply_filters( 'bp_core_avatar_full_height', $top_offset );
+
+		if ( $avatar_height > $top_offset ) {
+			$top_offset = $avatar_height;
+		}
+
+		bp_set_theme_compat_feature( $this->id, array(
+			'name'     => 'cover_image',
+			'settings' => array(
+				'components'   => array( 'xprofile', 'groups' ),
+				'width'        => $width,
+				'height'       => $top_offset + round( $avatar_height / 2 ),
+				'callback'     => 'bp_nouveau_theme_cover_image',
+				'theme_handle' => 'bp-nouveau',
+			),
+		) );
+	}
+
+	/**
+	 * Setup the Template Pack common actions.
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		// Filter BuddyPress template hierarchy and look for page templates.
+		add_filter( 'bp_get_buddypress_template', array( $this, 'theme_compat_page_templates' ), 10, 1 );
+
+		// Add our "buddypress" div wrapper to theme compat template parts.
+		add_filter( 'bp_replace_the_content', array( $this, 'theme_compat_wrapper' ), 999 );
+
+		// We need to neutralize the BuddyPress core "bp_core_render_message()" once it has been added.
+		add_action( 'bp_actions', array( $this, 'neutralize_core_template_notices' ), 6 );
+
+		// Scripts
+		add_action( 'bp_enqueue_scripts', array( $this, 'register_scripts' ), 2 ); // Register theme JS
+		remove_action( 'bp_enqueue_scripts', 'bp_core_confirmation_js' );
+		add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); // Enqueue theme CSS
+		add_action( 'bp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); // Enqueue theme JS
+		add_filter( 'bp_enqueue_scripts', array( $this, 'localize_scripts' ) ); // Enqueue theme script localization
+
+		// Body no-js class
+		add_filter( 'body_class', array( $this, 'add_nojs_body_class' ), 20, 1 );
+
+		// Ajax querystring
+		add_filter( 'bp_ajax_querystring', 'bp_nouveau_ajax_querystring', 10, 2 );
+
+		// Register directory nav items
+		add_action( 'bp_screens', array( $this, 'setup_directory_nav' ), 15 );
+
+		// Register the Default front pages Dynamic Sidebars
+		add_action( 'widgets_init', 'bp_nouveau_register_sidebars', 11 );
+
+		// Register the Primary Object nav widget
+		add_action( 'bp_widgets_init', array( 'BP_Nouveau_Object_Nav_Widget', 'register_widget' ) );
+
+		// Set the BP Uri for the Ajax customizer preview
+		add_filter( 'bp_uri', array( $this, 'customizer_set_uri' ), 10, 1 );
+
+		/** Override **********************************************************/
+
+		/**
+		 * Fires after all of the BuddyPress theme compat actions have been added.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param BP_Nouveau $this Current BP_Nouveau instance.
+		 */
+		do_action_ref_array( 'bp_theme_compat_actions', array( &$this ) );
+	}
+
+	/**
+	 * Enqueue the template pack css files
+	 *
+	 * @since 3.0.0
+	 */
+	public function enqueue_styles() {
+		$min = bp_core_get_minified_asset_suffix();
+		$rtl = '';
+
+		if ( is_rtl() ) {
+			$rtl = '-rtl';
+		}
+
+		/**
+		 * Filters the BuddyPress Nouveau CSS dependencies.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $value Array of style dependencies. Default Dashicons.
+		 */
+		$css_dependencies = apply_filters( 'bp_nouveau_css_dependencies', array( 'dashicons' ) );
+
+		/**
+		 * Filters the styles to enqueue for BuddyPress Nouveau.
+		 *
+		 * This filter provides a multidimensional array that will map to arguments used for wp_enqueue_style().
+		 * The primary index should have the stylesheet handle to use, and be assigned an array that has indexes for
+		 * file location, dependencies, and version.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $value Array of styles to enqueue.
+		 */
+		$styles = apply_filters( 'bp_nouveau_enqueue_styles', array(
+			'bp-nouveau' => array(
+				'file' => 'css/buddypress%1$s%2$s.css', 'dependencies' => $css_dependencies, 'version' => $this->version,
+			),
+		) );
+
+		if ( $styles ) {
+
+			foreach ( $styles as $handle => $style ) {
+				if ( ! isset( $style['file'] ) ) {
+					continue;
+				}
+
+				$file = sprintf( $style['file'], $rtl, $min );
+
+				// Locate the asset if needed.
+				if ( false === strpos( $style['file'], '://' ) ) {
+					$asset = bp_locate_template_asset( $file );
+
+					if ( empty( $asset['uri'] ) || false === strpos( $asset['uri'], '://' ) ) {
+						continue;
+					}
+
+					$file = $asset['uri'];
+				}
+
+				$data = bp_parse_args(
+					$style,
+					array(
+						'dependencies' => array(),
+						'version'      => $this->version,
+						'type'         => 'screen',
+					),
+					'nouveau_enqueue_styles'
+				);
+
+				wp_enqueue_style( $handle, $file, $data['dependencies'], $data['version'], $data['type'] );
+
+				if ( $min ) {
+					wp_style_add_data( $handle, 'suffix', $min );
+				}
+			}
+		}
+	}
+
+	/**
+	 * Register Template Pack JavaScript files
+	 *
+	 * @since 3.0.0
+	 */
+	public function register_scripts() {
+		$min          = bp_core_get_minified_asset_suffix();
+		$dependencies = bp_core_get_js_dependencies();
+		$bp_confirm   = array_search( 'bp-confirm', $dependencies );
+
+		unset( $dependencies[ $bp_confirm ] );
+
+		/**
+		 * Filters the scripts to enqueue for BuddyPress Nouveau.
+		 *
+		 * This filter provides a multidimensional array that will map to arguments used for wp_register_script().
+		 * The primary index should have the script handle to use, and be assigned an array that has indexes for
+		 * file location, dependencies, version and if it should load in the footer or not.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $value Array of scripts to register.
+		 */
+		$scripts = apply_filters( 'bp_nouveau_register_scripts', array(
+			'bp-nouveau' => array(
+				'file'         => 'js/buddypress-nouveau%s.js',
+				'dependencies' => $dependencies,
+				'version'      => $this->version,
+				'footer'       => true,
+			),
+		) );
+
+		// Bail if no scripts
+		if ( empty( $scripts ) ) {
+			return;
+		}
+
+		// Add The password verify if needed.
+		if ( bp_is_active( 'settings' ) || bp_get_signup_allowed() ) {
+			$scripts['bp-nouveau-password-verify'] = array(
+				'file'         => 'js/password-verify%s.js',
+				'dependencies' => array( 'bp-nouveau', 'password-strength-meter' ),
+				'footer'       => true,
+			);
+		}
+
+		foreach ( $scripts as $handle => $script ) {
+			if ( ! isset( $script['file'] ) ) {
+				continue;
+			}
+
+			$file = sprintf( $script['file'], $min );
+
+			// Locate the asset if needed.
+			if ( false === strpos( $script['file'], '://' ) ) {
+				$asset = bp_locate_template_asset( $file );
+
+				if ( empty( $asset['uri'] ) || false === strpos( $asset['uri'], '://' ) ) {
+					continue;
+				}
+
+				$file = $asset['uri'];
+			}
+
+			$data = bp_parse_args(
+				$script,
+				array(
+					'dependencies' => array(),
+					'version'      => $this->version,
+					'footer'       => false,
+				),
+				'nouveau_register_scripts'
+			);
+
+			wp_register_script( $handle, $file, $data['dependencies'], $data['version'], $data['footer'] );
+		}
+	}
+
+	/**
+	 * Enqueue the required JavaScript files
+	 *
+	 * @since 3.0.0
+	 */
+	public function enqueue_scripts() {
+		wp_enqueue_script( 'bp-nouveau' );
+
+		if ( bp_is_register_page() || bp_is_user_settings_general() ) {
+			wp_enqueue_script( 'bp-nouveau-password-verify' );
+		}
+
+		if ( is_singular() && bp_is_blog_page() && get_option( 'thread_comments' ) ) {
+			wp_enqueue_script( 'comment-reply' );
+		}
+
+		/**
+		 * Fires after all of the BuddyPress Nouveau scripts have been enqueued.
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( 'bp_nouveau_enqueue_scripts' );
+	}
+
+	/**
+	 * Adds the no-js class to the body tag.
+	 *
+	 * This function ensures that the <body> element will have the 'no-js' class by default. If you're
+	 * using JavaScript for some visual functionality in your theme, and you want to provide noscript
+	 * support, apply those styles to body.no-js.
+	 *
+	 * The no-js class is removed by the JavaScript created in buddypress.js.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $classes Array of classes to append to body tag.
+	 *
+	 * @return array $classes
+	 */
+	public function add_nojs_body_class( $classes ) {
+		$classes[] = 'no-js';
+		return array_unique( $classes );
+	}
+
+	/**
+	 * Load localizations for topic script.
+	 *
+	 * These localizations require information that may not be loaded even by init.
+	 *
+	 * @since 3.0.0
+	 */
+	public function localize_scripts() {
+		$params = array(
+			'ajaxurl'             => bp_core_ajax_url(),
+			'confirm'             => __( 'Are you sure?', 'buddypress' ),
+			'show_x_comments'     => __( 'Show all %d comments', 'buddypress' ),
+			'unsaved_changes'     => __( 'Your profile has unsaved changes. If you leave the page, the changes will be lost.', 'buddypress' ),
+			'object_nav_parent'   => '#buddypress',
+		);
+
+		// If the Object/Item nav are in the sidebar
+		if ( bp_nouveau_is_object_nav_in_sidebar() ) {
+			$params['object_nav_parent'] = '.buddypress_object_nav';
+		}
+
+		/**
+		 * Filters the supported BuddyPress Nouveau components.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $value Array of supported components.
+		 */
+		$supported_objects = (array) apply_filters( 'bp_nouveau_supported_components', bp_core_get_packaged_component_ids() );
+		$object_nonces     = array();
+
+		foreach ( $supported_objects as $key_object => $object ) {
+			if ( ! bp_is_active( $object ) || 'forums' === $object ) {
+				unset( $supported_objects[ $key_object ] );
+				continue;
+			}
+
+			$object_nonces[ $object ] = wp_create_nonce( 'bp_nouveau_' . $object );
+		}
+
+		// Groups require some additional objects.
+		if ( bp_is_active( 'groups' ) ) {
+			$supported_objects = array_merge( $supported_objects, array( 'group_members', 'group_requests' ) );
+		}
+
+		// Add components & nonces
+		$params['objects'] = $supported_objects;
+		$params['nonces']  = $object_nonces;
+
+		// Used to transport the settings inside the Ajax requests
+		if ( is_customize_preview() ) {
+			$params['customizer_settings'] = bp_nouveau_get_temporary_setting( 'any' );
+		}
+
+		/**
+		 * Filters core JavaScript strings for internationalization before AJAX usage.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $params Array of key/value pairs for AJAX usage.
+		 */
+		wp_localize_script( 'bp-nouveau', 'BP_Nouveau', apply_filters( 'bp_core_get_js_strings', $params ) );
+	}
+
+	/**
+	 * Filter the default theme compatibility root template hierarchy, and prepend
+	 * a page template to the front if it's set.
+	 *
+	 * @see https://buddypress.trac.wordpress.org/ticket/6065
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $templates Array of templates.
+	 *
+	 * @return array
+	 */
+	public function theme_compat_page_templates( $templates = array() ) {
+		/**
+		 * Filters whether or not we are looking at a directory to determine if to return early.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param bool $value Whether or not we are viewing a directory.
+		 */
+		if ( true === (bool) apply_filters( 'bp_nouveau_theme_compat_page_templates_directory_only', ! bp_is_directory() ) ) {
+			return $templates;
+		}
+
+		// No page ID yet.
+		$page_id = 0;
+
+		// Get the WordPress Page ID for the current view.
+		foreach ( (array) buddypress()->pages as $component => $bp_page ) {
+
+			// Handles the majority of components.
+			if ( bp_is_current_component( $component ) ) {
+				$page_id = (int) $bp_page->id;
+			}
+
+			// Stop if not on a user page.
+			if ( ! bp_is_user() && ! empty( $page_id ) ) {
+				break;
+			}
+
+			// The Members component requires an explicit check due to overlapping components.
+			if ( bp_is_user() && ( 'members' === $component ) ) {
+				$page_id = (int) $bp_page->id;
+				break;
+			}
+		}
+
+		// Bail if no directory page set.
+		if ( 0 === $page_id ) {
+			return $templates;
+		}
+
+		// Check for page template.
+		$page_template = get_page_template_slug( $page_id );
+
+		// Add it to the beginning of the templates array so it takes precedence over the default hierarchy.
+		if ( ! empty( $page_template ) ) {
+
+			/**
+			 * Check for existence of template before adding it to template
+			 * stack to avoid accidentally including an unintended file.
+			 *
+			 * @see https://buddypress.trac.wordpress.org/ticket/6190
+			 */
+			if ( '' !== locate_template( $page_template ) ) {
+				array_unshift( $templates, $page_template );
+			}
+		}
+
+		return $templates;
+	}
+
+	/**
+	 * Add our special 'buddypress' div wrapper to the theme compat template part.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @see bp_buffer_template_part()
+	 *
+	 * @param string $retval Current template part contents.
+	 *
+	 * @return string
+	 */
+	public function theme_compat_wrapper( $retval ) {
+		if ( false !== strpos( $retval, '<div id="buddypress"' ) ) {
+			return $retval;
+		}
+
+		// Add our 'buddypress' div wrapper.
+		return sprintf(
+			'<div id="buddypress" class="%1$s">%2$s</div><!-- #buddypress -->%3$s',
+			esc_attr( bp_nouveau_get_container_classes() ),
+			$retval,  // Constructed HTML.
+			"\n"
+		);
+	}
+
+	/**
+	 * Define the directory nav items
+	 *
+	 * @since 3.0.0
+	 */
+	public function setup_directory_nav() {
+		$nav_items = array();
+
+		if ( bp_is_members_directory() ) {
+			$nav_items = bp_nouveau_get_members_directory_nav_items();
+		} elseif ( bp_is_activity_directory() ) {
+			$nav_items = bp_nouveau_get_activity_directory_nav_items();
+		} elseif ( bp_is_groups_directory() ) {
+			$nav_items = bp_nouveau_get_groups_directory_nav_items();
+		} elseif ( bp_is_blogs_directory() ) {
+			$nav_items = bp_nouveau_get_blogs_directory_nav_items();
+		}
+
+		if ( empty( $nav_items ) ) {
+			return;
+		}
+
+		foreach ( $nav_items as $nav_item ) {
+			if ( empty( $nav_item['component'] ) || $nav_item['component'] !== bp_current_component() ) {
+				continue;
+			}
+
+			// Define the primary nav for the current component's directory
+			$this->directory_nav->add_nav( $nav_item );
+		}
+	}
+
+	/**
+	 * We'll handle template notices from BP Nouveau.
+	 *
+	 * @since 3.0.0
+	 */
+	public function neutralize_core_template_notices() {
+		remove_action( 'template_notices', 'bp_core_render_message' );
+	}
+
+	/**
+	 * Set the BP Uri for the customizer in case of Ajax requests.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param  string $path the BP Uri.
+	 * @return string       the BP Uri.
+	 */
+	public function customizer_set_uri( $path ) {
+		if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
+			return $path;
+		}
+
+		$uri = parse_url( $path );
+
+		if ( false === strpos( $uri['path'], 'customize.php' ) ) {
+			return $path;
+		} else {
+			$vars = bp_parse_args(
+				$uri['query'],
+				array(),
+				'customizer_set_uri'
+			);
+
+			if ( ! empty( $vars['url'] ) ) {
+				$path = str_replace( get_site_url(), '', urldecode( $vars['url'] ) );
+			}
+		}
+
+		return $path;
+	}
+}
+
+/**
+ * Get a unique instance of BP Nouveau
+ *
+ * @since 3.0.0
+ *
+ * @return BP_Nouveau the main instance of the class
+ */
+function bp_nouveau() {
+	return BP_Nouveau::get_instance();
+}
+
+/**
+ * Launch BP Nouveau!
+ */
+bp_nouveau();
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/activity-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/activity-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/activity-loop.php	(revision 12265)
@@ -0,0 +1,43 @@
+<?php
+/**
+ * BuddyPress - Activity Loop
+ *
+ * @version 3.1.0
+ */
+
+bp_nouveau_before_loop(); ?>
+
+<?php if ( bp_has_activities( bp_ajax_querystring( 'activity' ) ) ) : ?>
+
+	<?php if ( empty( $_POST['page'] ) || 1 === (int) $_POST['page'] ) : ?>
+		<ul class="activity-list item-list bp-list">
+	<?php endif; ?>
+
+	<?php
+	while ( bp_activities() ) :
+		bp_the_activity();
+	?>
+
+		<?php bp_get_template_part( 'activity/entry' ); ?>
+
+	<?php endwhile; ?>
+
+	<?php if ( bp_activity_has_more_items() ) : ?>
+
+		<li class="load-more">
+			<a href="<?php bp_activity_load_more_link(); ?>"><?php echo esc_html_x( 'Load More', 'button', 'buddypress' ); ?></a>
+		</li>
+
+	<?php endif; ?>
+
+	<?php if ( empty( $_POST['page'] ) || 1 === (int) $_POST['page'] ) : ?>
+		</ul>
+	<?php endif; ?>
+
+<?php else : ?>
+
+		<?php bp_nouveau_user_feedback( 'activity-loop-none' ); ?>
+
+<?php endif; ?>
+
+<?php bp_nouveau_after_loop(); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/comment-form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/comment-form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/comment-form.php	(revision 12265)
@@ -0,0 +1,34 @@
+<?php
+/**
+ * BP Nouveau Activity Comment form template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+if ( ! bp_nouveau_current_user_can( 'comment_activity' ) || ! bp_activity_can_comment() ) {
+	return;
+} ?>
+
+<form action="<?php bp_activity_comment_form_action(); ?>" method="post" id="ac-form-<?php bp_activity_id(); ?>" class="ac-form"<?php bp_activity_comment_form_nojs_display(); ?>>
+
+	<div class="ac-reply-avatar"><?php bp_loggedin_user_avatar( array( 'type' => 'thumb' ) ); ?></div>
+	<div class="ac-reply-content">
+		<div class="ac-textarea">
+			<label for="ac-input-<?php bp_activity_id(); ?>" class="bp-screen-reader-text">
+				<?php echo esc_html( _x( 'Comment', 'heading', 'buddypress' ) ); ?>
+			</label>
+			<textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input bp-suggestions" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
+		</div>
+		<input type="hidden" name="comment_form_id" value="<?php bp_activity_id(); ?>" />
+
+		<?php
+		bp_nouveau_submit_button( 'activity-new-comment' );
+		printf(
+			'&nbsp; <button type="button" class="ac-reply-cancel">%s</button>',
+			esc_html( _x( 'Cancel', 'button', 'buddypress' ) )
+		);
+		?>
+	</div>
+
+</form>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/comment.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/comment.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/comment.php	(revision 12265)
@@ -0,0 +1,38 @@
+<?php
+/**
+ * BuddyPress - Activity Stream Comment
+ *
+ * This template is used by bp_activity_comments() functions to show
+ * each activity.
+ *
+ * @version 3.0.0
+ */
+
+	?>
+
+<li id="acomment-<?php bp_activity_comment_id(); ?>" class="comment-item" data-bp-activity-comment-id="<?php bp_activity_comment_id(); ?>">
+	<div class="acomment-avatar item-avatar">
+		<a href="<?php bp_activity_comment_user_link(); ?>">
+			<?php
+			bp_activity_avatar(
+				array(
+					'type'    => 'thumb',
+					'user_id' => bp_get_activity_comment_user_id(),
+				)
+			);
+			?>
+		</a>
+	</div>
+
+	<div class="acomment-meta">
+
+		<?php bp_nouveau_activity_comment_action(); ?>
+
+	</div>
+
+	<div class="acomment-content"><?php bp_activity_comment_content(); ?></div>
+
+	<?php bp_nouveau_activity_comment_buttons( array( 'container' => 'div' ) ); ?>
+
+	<?php bp_nouveau_activity_recurse_comments( bp_activity_current_comment() ); ?>
+</li>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/entry.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/entry.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/entry.php	(revision 12265)
@@ -0,0 +1,67 @@
+<?php
+/**
+ * BuddyPress - Activity Stream (Single Item)
+ *
+ * This template is used by activity-loop.php and AJAX functions to show
+ * each activity.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_activity_hook( 'before', 'entry' ); ?>
+
+<li class="<?php bp_activity_css_class(); ?>" id="activity-<?php bp_activity_id(); ?>" data-bp-activity-id="<?php bp_activity_id(); ?>" data-bp-timestamp="<?php bp_nouveau_activity_timestamp(); ?>">
+
+	<div class="activity-avatar item-avatar">
+
+		<a href="<?php bp_activity_user_link(); ?>">
+
+			<?php bp_activity_avatar( array( 'type' => 'full' ) ); ?>
+
+		</a>
+
+	</div>
+
+	<div class="activity-content">
+
+		<div class="activity-header">
+
+			<?php bp_activity_action(); ?>
+
+		</div>
+
+		<?php if ( bp_nouveau_activity_has_content() ) : ?>
+
+			<div class="activity-inner">
+
+				<?php bp_nouveau_activity_content(); ?>
+
+			</div>
+
+		<?php endif; ?>
+
+		<?php bp_nouveau_activity_entry_buttons(); ?>
+
+	</div>
+
+	<?php bp_nouveau_activity_hook( 'before', 'entry_comments' ); ?>
+
+	<?php if ( bp_activity_get_comment_count() || ( is_user_logged_in() && ( bp_activity_can_comment() || bp_is_single_activity() ) ) ) : ?>
+
+		<div class="activity-comments">
+
+			<?php bp_activity_comments(); ?>
+
+			<?php bp_nouveau_activity_comment_form(); ?>
+
+		</div>
+
+	<?php endif; ?>
+
+	<?php bp_nouveau_activity_hook( 'after', 'entry_comments' ); ?>
+
+</li>
+
+<?php
+bp_nouveau_activity_hook( 'after', 'entry' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/index.php	(revision 12265)
@@ -0,0 +1,41 @@
+<?php
+/**
+ * BuddyPress Activity templates
+ *
+ * @since 2.3.0
+ * @version 3.0.0
+ */
+?>
+
+	<?php bp_nouveau_before_activity_directory_content(); ?>
+
+	<?php if ( is_user_logged_in() ) : ?>
+
+		<?php bp_get_template_part( 'activity/post-form' ); ?>
+
+	<?php endif; ?>
+
+	<?php bp_nouveau_template_notices(); ?>
+
+	<?php if ( ! bp_nouveau_is_object_nav_in_sidebar() ) : ?>
+
+		<?php bp_get_template_part( 'common/nav/directory-nav' ); ?>
+
+	<?php endif; ?>
+
+	<div class="screen-content">
+
+		<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+		<?php bp_nouveau_activity_hook( 'before_directory', 'list' ); ?>
+
+		<div id="activity-stream" class="activity" data-bp-list="activity">
+
+				<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'directory-activity-loading' ); ?></div>
+
+		</div><!-- .activity -->
+
+		<?php bp_nouveau_after_activity_directory_content(); ?>
+
+	</div><!-- // .screen-content -->
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/post-form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/post-form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/post-form.php	(revision 12265)
@@ -0,0 +1,25 @@
+<?php
+/**
+ * BuddyPress - Activity Post Form
+ *
+ * @version 3.1.0
+ */
+
+?>
+
+<?php
+/*
+ * Template tag to prepare the activity post form checks capability and enqueue needed scripts.
+ */
+bp_nouveau_before_activity_post_form();
+?>
+
+<h2 class="bp-screen-reader-text"><?php echo esc_html_x( 'Post Update', 'heading', 'buddypress' ); ?></h2>
+
+<div id="bp-nouveau-activity-form" class="activity-update-form"></div>
+
+<?php
+/*
+ * Template tag to load the Javascript templates of the Post form UI.
+ */
+bp_nouveau_after_activity_post_form();
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/single/home.php	(revision 12265)
@@ -0,0 +1,20 @@
+<?php
+/**
+ * BuddyPress - Home
+ *
+ * @version 3.0.0
+ */
+
+?>
+
+	<?php bp_nouveau_template_notices(); ?>
+
+	<div class="activity" data-bp-single="<?php echo esc_attr( bp_current_action() ); ?>">
+
+		<ul id="activity-stream" class="activity-list item-list bp-list" data-bp-list="activity">
+
+			<li id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'single-activity-loading' ); ?></li>
+
+		</ul>
+
+	</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/widget.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/widget.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/activity/widget.php	(revision 12265)
@@ -0,0 +1,63 @@
+<?php
+/**
+ * BP Nouveau Activity Widget template.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<?php if ( bp_has_activities( bp_nouveau_activity_widget_query() ) ) : ?>
+
+	<div class="activity-list item-list">
+
+		<?php
+		while ( bp_activities() ) :
+			bp_the_activity();
+		?>
+
+			<?php if ( bp_activity_has_content() ) : ?>
+
+				<blockquote>
+
+					<?php bp_activity_content_body(); ?>
+
+					<footer>
+
+						<cite>
+							<a href="<?php bp_activity_user_link(); ?>" class="bp-tooltip" data-bp-tooltip="<?php echo esc_attr( bp_activity_member_display_name() ); ?>">
+								<?php
+								bp_activity_avatar(
+									array(
+										'type'   => 'thumb',
+										'width'  => '40',
+										'height' => '40',
+									)
+								);
+								?>
+							</a>
+						</cite>
+
+						<?php echo bp_insert_activity_meta(); ?>
+
+					</footer>
+
+				</blockquote>
+
+			<?php else : ?>
+
+				<p><?php bp_activity_action(); ?></p>
+
+			<?php endif; ?>
+
+		<?php endwhile; ?>
+
+	</div>
+
+<?php else : ?>
+
+	<div class="widget-error">
+		<?php bp_nouveau_user_feedback( 'activity-loop-none' ); ?>
+	</div>
+
+<?php endif; ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/camera.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/camera.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/camera.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * BuddyPress Avatars camera template.
+ *
+ * This template is used to create the camera Backbone views.
+ *
+ * @since 2.3.0
+ * @version 3.1.0
+ */
+
+?>
+<script id="tmpl-bp-avatar-webcam" type="text/html">
+	<# if ( ! data.user_media ) { #>
+		<div id="bp-webcam-message">
+			<p class="warning"><?php esc_html_e( 'Your browser does not support this feature.', 'buddypress' ); ?></p>
+		</div>
+	<# } else { #>
+		<div id="avatar-to-crop"></div>
+		<div class="avatar-crop-management">
+			<div id="avatar-crop-pane" class="avatar" style="width:{{data.w}}px; height:{{data.h}}px"></div>
+			<div id="avatar-crop-actions">
+				<button type="button" class="button avatar-webcam-capture"><?php echo esc_html_x( 'Capture', 'button', 'buddypress' ); ?></button>
+				<button type="button" class="button avatar-webcam-save"><?php echo esc_html_x( 'Save', 'button', 'buddypress' ); ?></button>
+			</div>
+		</div>
+	<# } #>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/crop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/crop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/crop.php	(revision 12265)
@@ -0,0 +1,24 @@
+<?php
+/**
+ * BuddyPress Avatars crop template.
+ *
+ * This template is used to create the crop Backbone views.
+ *
+ * @since 2.3.0
+ * @version 3.1.0
+ */
+
+?>
+<script id="tmpl-bp-avatar-item" type="text/html">
+	<div id="avatar-to-crop">
+		<img src="{{data.url}}"/>
+	</div>
+	<div class="avatar-crop-management">
+		<div id="avatar-crop-pane" class="avatar" style="width:{{data.full_w}}px; height:{{data.full_h}}px">
+			<img src="{{data.url}}" id="avatar-crop-preview"/>
+		</div>
+		<div id="avatar-crop-actions">
+			<button type="button" class="button avatar-crop-submit"><?php echo esc_html_x( 'Crop Image', 'button', 'buddypress' ); ?></button>
+		</div>
+	</div>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/avatars/index.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * BuddyPress Avatars main template.
+ *
+ * This template is used to inject the BuddyPress Backbone views
+ * dealing with avatars.
+ *
+ * It's also used to create the common Backbone views.
+ *
+ * @since 2.3.0
+ * @version 3.1.0
+ */
+
+/**
+ * This action is for internal use, please do not use it
+ */
+do_action( 'bp_attachments_avatar_check_template' );
+?>
+<div class="bp-avatar-nav"></div>
+<div class="bp-avatar"></div>
+<div class="bp-avatar-status"></div>
+
+<script type="text/html" id="tmpl-bp-avatar-nav">
+	<a href="{{data.href}}" class="bp-avatar-nav-item" data-nav="{{data.id}}">{{data.name}}</a>
+</script>
+
+<?php bp_attachments_get_template_part( 'uploader' ); ?>
+
+<?php bp_attachments_get_template_part( 'avatars/crop' ); ?>
+
+<?php bp_attachments_get_template_part( 'avatars/camera' ); ?>
+
+<script id="tmpl-bp-avatar-delete" type="text/html">
+	<# if ( 'user' === data.object ) { #>
+		<p><?php esc_html_e( "If you'd like to delete your current profile photo, use the delete profile photo button.", 'buddypress' ); ?></p>
+		<button type="button" class="button edit" id="bp-delete-avatar"><?php esc_html_e( 'Delete My Profile Photo', 'buddypress' ); ?></button>
+	<# } else if ( 'group' === data.object ) { #>
+		<?php bp_nouveau_user_feedback( 'group-avatar-delete-info' ); ?>
+		<button type="button" class="button edit" id="bp-delete-avatar"><?php esc_html_e( 'Delete Group Profile Photo', 'buddypress' ); ?></button>
+	<# } else { #>
+		<?php
+			/**
+			 * Fires inside the avatar delete frontend template markup if no other data.object condition is met.
+			 *
+			 * @since 3.0.0
+			 */
+			do_action( 'bp_attachments_avatar_delete_template' ); ?>
+	<# } #>
+</script>
+
+<?php
+	/**
+	 * Fires after the avatar main frontend template markup.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_attachments_avatar_main_template' ); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/cover-images/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/cover-images/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/cover-images/index.php	(revision 12265)
@@ -0,0 +1,54 @@
+<?php
+/**
+ * BuddyPress Cover Images main template.
+ *
+ * This template is used to inject the BuddyPress Backbone views
+ * dealing with cover images.
+ *
+ * It's also used to create the common Backbone views.
+ *
+ * @since 2.4.0
+ * @version 3.1.0
+ */
+
+?>
+
+<div class="bp-cover-image"></div>
+<div class="bp-cover-image-status"></div>
+<div class="bp-cover-image-manage"></div>
+
+<?php bp_attachments_get_template_part( 'uploader' ); ?>
+
+<script id="tmpl-bp-cover-image-delete" type="text/html">
+	<# if ( 'user' === data.object ) { #>
+		<p><?php esc_html_e( "If you'd like to delete your current cover image, use the delete Cover Image button.", 'buddypress' ); ?></p>
+		<button type="button" class="button edit" id="bp-delete-cover-image">
+			<?php
+			echo esc_html_x( 'Delete My Cover Image', 'button', 'buddypress' );
+			?>
+		</button>
+	<# } else if ( 'group' === data.object ) { #>
+		<p><?php esc_html_e( "If you'd like to remove the existing group cover image but not upload a new one, please use the delete group cover image button.", 'buddypress' ); ?></p>
+		<button type="button" class="button edit" id="bp-delete-cover-image">
+			<?php
+			echo esc_html_x( 'Delete Group Cover Image', 'button', 'buddypress' );
+			?>
+		</button>
+	<# } else { #>
+		<?php
+			/**
+			 * Fires inside the cover image delete frontend template markup if no other data.object condition is met.
+			 *
+			 * @since 3.0.0
+			 */
+			do_action( 'bp_attachments_cover_image_delete_template' ); ?>
+	<# } #>
+</script>
+
+<?php
+	/**
+	 * Fires after the cover image main frontend template markup.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_attachments_cover_image_main_template' ); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/uploader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/uploader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/_attachments/uploader.php	(revision 12265)
@@ -0,0 +1,42 @@
+<?php
+/**
+ * BuddyPress Uploader templates.
+ *
+ * This template is used to create the BuddyPress Uploader Backbone views.
+ *
+ * @since 2.3.0
+ * @version 3.1.0
+ */
+
+?>
+<script type="text/html" id="tmpl-upload-window">
+	<?php if ( ! _device_can_upload() ) : ?>
+		<h3 class="upload-instructions"><?php esc_html_e( 'The web browser on your device cannot be used to upload files.', 'buddypress' ); ?></h3>
+	<?php elseif ( is_multisite() && ! is_upload_space_available() ) : ?>
+		<h3 class="upload-instructions"><?php esc_html_e( 'Upload Limit Exceeded', 'buddypress' ); ?></h3>
+	<?php else : ?>
+		<div id="{{data.container}}">
+			<div id="{{data.drop_element}}">
+				<div class="drag-drop-inside">
+					<p class="drag-drop-info"><?php esc_html_e( 'Drop your file here', 'buddypress' ); ?></p>
+
+					<p class="drag-drop-buttons">
+						<label for="{{data.browse_button}}" class="<?php echo is_admin() ? 'screen-reader-text' : 'bp-screen-reader-text'; ?>">
+							<?php esc_html_e( 'Select your file', 'buddypress' ); ?>
+						</label>
+						<input id="{{data.browse_button}}" type="button" value="<?php echo esc_attr_x( 'Select your file', 'button', 'buddypress' ); ?>" class="button" />
+					</p>
+				</div>
+			</div>
+		</div>
+	<?php endif; ?>
+</script>
+
+<script type="text/html" id="tmpl-progress-window">
+	<div id="{{data.id}}">
+		<div class="bp-progress">
+			<div class="bp-bar"></div>
+		</div>
+		<div class="filename">{{data.filename}}</div>
+	</div>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/emails/single-bp-email.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/emails/single-bp-email.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/emails/single-bp-email.php	(revision 12265)
@@ -0,0 +1,249 @@
+<?php
+/**
+ * BuddyPress email template.
+ *
+ * Magic numbers:
+ *  1.618 = golden mean.
+ *  1.35  = default body_text_size multipler. Gives default heading of 20px.
+ *
+ * @since 2.5.0
+ * @version 3.1.0
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+/*
+Based on the Cerberus "Fluid" template by Ted Goas (http://tedgoas.github.io/Cerberus/).
+License for the original template:
+
+
+The MIT License (MIT)
+
+Copyright (c) 2017 Ted Goas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+$settings = bp_email_get_appearance_settings();
+
+?><!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+<head>
+	<meta charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>">
+	<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
+	<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
+	<meta name="x-apple-disable-message-reformatting">  <!-- Disable auto-scale in iOS 10 Mail entirely -->
+	<title></title> <!-- The title tag shows in email notifications, like Android 4.4. -->
+
+	<!-- CSS Reset -->
+	<style type="text/css">
+		/* What it does: Remove spaces around the email design added by some email clients. */
+		/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
+		html,
+		body {
+			Margin: 0 !important;
+			padding: 0 !important;
+			height: 100% !important;
+			width: 100% !important;
+		}
+
+		/* What it does: Stops email clients resizing small text. */
+		* {
+			-ms-text-size-adjust: 100%;
+			-webkit-text-size-adjust: 100%;
+		}
+
+		/* What is does: Centers email on Android 4.4 */
+		div[style*="margin: 16px 0"] {
+			margin: 0 !important;
+		}
+
+		/* What it does: Stops Outlook from adding extra spacing to tables. */
+		table,
+		td {
+			mso-table-lspace: 0pt !important;
+			mso-table-rspace: 0pt !important;
+		}
+
+		/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
+		table {
+			border-spacing: 0 !important;
+			border-collapse: collapse !important;
+			table-layout: fixed !important;
+			Margin: 0 auto !important;
+		}
+		table table table {
+			table-layout: auto;
+		}
+
+		/* What it does: Uses a better rendering method when resizing images in IE. */
+		/* & manages img max widths to ensure content body images don't exceed template width. */
+		img {
+			-ms-interpolation-mode:bicubic;
+			height: auto;
+			max-width: 100%;
+		}
+
+		/* What it does: A work-around for email clients meddling in triggered links. */
+		*[x-apple-data-detectors],  /* iOS */
+		.x-gmail-data-detectors,    /* Gmail */
+		.x-gmail-data-detectors *,
+		.aBn {
+			border-bottom: 0 !important;
+			cursor: default !important;
+			color: inherit !important;
+			text-decoration: none !important;
+			font-size: inherit !important;
+			font-family: inherit !important;
+			font-weight: inherit !important;
+			line-height: inherit !important;
+		}
+
+		/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
+		.a6S {
+			display: none !important;
+			opacity: 0.01 !important;
+		}
+
+		/* If the above doesn't work, add a .g-img class to any image in question. */
+			img.g-img + div {
+			display: none !important;
+		}
+
+		/* What it does: Prevents underlining the button text in Windows 10 */
+		.button-link {
+			text-decoration: none !important;
+		}
+	</style>
+
+</head>
+<body class="email_bg" width="100%" bgcolor="<?php echo esc_attr( $settings['email_bg'] ); ?>" style="margin: 0; mso-line-height-rule: exactly;">
+<table cellpadding="0" cellspacing="0" border="0" height="100%" width="100%" bgcolor="<?php echo esc_attr( $settings['email_bg'] ); ?>" style="border-collapse:collapse;" class="email_bg"><tr><td valign="top">
+	<center style="width: 100%; text-align: <?php echo esc_attr( $settings['direction'] ); ?>;">
+
+		<!-- Visually Hidden Preheader Text : BEGIN -->
+		<div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
+			{{email.preheader}}
+		</div>
+		<!-- Visually Hidden Preheader Text : END -->
+
+		<div style="max-width: 600px; margin: auto;" class="email-container">
+			<!--[if mso]>
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" align="center">
+			<tr>
+			<td>
+			<![endif]-->
+
+			<!-- Email Header : BEGIN -->
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 600px; border-top: 7px solid <?php echo esc_attr( $settings['highlight_color'] ); ?>" bgcolor="<?php echo esc_attr( $settings['header_bg'] ); ?>" class="header_bg">
+				<tr>
+					<td style="text-align: center; padding: 15px 0; font-family: sans-serif; mso-height-rule: exactly; font-weight: bold; color: <?php echo esc_attr( $settings['header_text_color'] ); ?>; font-size: <?php echo esc_attr( $settings['header_text_size'] . 'px' ); ?>" class="header_text_color header_text_size">
+						<?php
+						/**
+						 * Fires before the display of the email template header.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_before_email_header' );
+
+						echo bp_get_option( 'blogname' );
+
+						/**
+						 * Fires after the display of the email template header.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_after_email_header' );
+						?>
+					</td>
+				</tr>
+			</table>
+			<!-- Email Header : END -->
+
+			<!-- Email Body : BEGIN -->
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" bgcolor="<?php echo esc_attr( $settings['body_bg'] ); ?>" width="100%" style="max-width: 600px; border-radius: 5px;" class="body_bg">
+
+				<!-- 1 Column Text : BEGIN -->
+				<tr>
+					<td>
+						<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
+							<tr>
+								<td style="padding: 20px; font-family: sans-serif; mso-height-rule: exactly; line-height: <?php echo esc_attr( floor( $settings['body_text_size'] * 1.618 ) . 'px' ); ?>; color: <?php echo esc_attr( $settings['body_text_color'] ); ?>; font-size: <?php echo esc_attr( $settings['body_text_size'] . 'px' ); ?>" class="body_text_color body_text_size">
+									<span style="font-weight: bold; font-size: <?php echo esc_attr( floor( $settings['body_text_size'] * 1.35 ) . 'px' ); ?>" class="welcome"><?php bp_email_the_salutation( $settings ); ?></span>
+									<hr color="<?php echo esc_attr( $settings['email_bg'] ); ?>"><br>
+									{{{content}}}
+								</td>
+							</tr>
+						</table>
+					</td>
+				</tr>
+				<!-- 1 Column Text : BEGIN -->
+
+			</table>
+			<!-- Email Body : END -->
+
+			<!-- Email Footer : BEGIN -->
+			<br>
+			<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="<?php echo esc_attr( $settings['direction'] ); ?>" width="100%" style="max-width: 600px; border-radius: 5px;" bgcolor="<?php echo esc_attr( $settings['footer_bg'] ); ?>" class="footer_bg">
+				<tr>
+					<td style="padding: 20px; width: 100%; font-size: <?php echo esc_attr( $settings['footer_text_size'] . 'px' ); ?>; font-family: sans-serif; mso-height-rule: exactly; line-height: <?php echo esc_attr( floor( $settings['footer_text_size'] * 1.618 ) . 'px' ); ?>; text-align: <?php echo esc_attr( $settings['direction'] ); ?>; color: <?php echo esc_attr( $settings['footer_text_color'] ); ?>;" class="footer_text_color footer_text_size">
+						<?php
+						/**
+						 * Fires before the display of the email template footer.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_before_email_footer' );
+						?>
+
+						<span class="footer_text"><?php echo nl2br( stripslashes( $settings['footer_text'] ) ); ?></span>
+						<br><br>
+						<a href="{{{unsubscribe}}}" style="text-decoration: underline;"><?php echo esc_html_x( 'unsubscribe', 'email', 'buddypress' ); ?></a>
+
+						<?php
+						/**
+						 * Fires after the display of the email template footer.
+						 *
+						 * @since 2.5.0
+						 */
+						do_action( 'bp_after_email_footer' );
+						?>
+					</td>
+				</tr>
+			</table>
+			<!-- Email Footer : END -->
+
+			<!--[if mso]>
+			</td>
+			</tr>
+			</table>
+			<![endif]-->
+		</div>
+	</center>
+</td></tr></table>
+<?php
+if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) {
+	wp_footer();
+}
+?>
+</body>
+</html>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/activity.php	(revision 12265)
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+
+if ( bp_activity_embed_has_activity( bp_current_action() ) ) :
+?>
+
+	<?php
+	while ( bp_activities() ) :
+		bp_the_activity();
+	?>
+
+		<div class="bp-embed-excerpt"><?php bp_activity_embed_excerpt(); ?></div>
+
+		<?php bp_activity_embed_media(); ?>
+
+	<?php endwhile; ?>
+
+<?php
+endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/footer.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/footer.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/footer.php	(revision 12265)
@@ -0,0 +1,15 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+			<div class="wp-embed-footer">
+				<?php the_embed_site_title(); ?>
+
+				<div class="wp-embed-meta">
+					<?php
+					/** This action is documented in wp-includes/theme-compat/embed-content.php */
+					do_action( 'embed_content_meta' );
+					?>
+				</div>
+			</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/header-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/header-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/header-activity.php	(revision 12265)
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+<div id="bp-embed-header">
+	<div class="bp-embed-avatar">
+		<a href="<?php bp_displayed_user_link(); ?>">
+			<?php bp_displayed_user_avatar( 'type=thumb&width=45&height=45' ); ?>
+		</a>
+	</div>
+
+	<?php if ( bp_activity_embed_has_activity( bp_current_action() ) ) : ?>
+
+		<?php
+		while ( bp_activities() ) :
+			bp_the_activity();
+		?>
+			<p class="bp-embed-activity-action">
+				<?php bp_activity_action( array( 'no_timestamp' => true ) ); ?>
+			</p>
+		<?php endwhile; ?>
+
+	<?php endif; ?>
+
+	<p class="bp-embed-header-meta">
+		<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+			<span class="bp-embed-mentionname">@<?php bp_displayed_user_mentionname(); ?> &middot; </span>
+		<?php endif; ?>
+
+		<span class="bp-embed-timestamp"><a href="<?php bp_activity_thread_permalink(); ?>"><?php echo date_i18n( get_option( 'time_format' ) . ' - ' . get_option( 'date_format' ), strtotime( bp_get_activity_date_recorded() ) ); ?></a></span>
+	</p>
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/assets/embeds/header.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @version 3.0.0
+ */
+?>
+
+		<div id="bp-embed-header">
+			<div class="bp-embed-avatar">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_avatar( 'type=thumb&width=36&height=36' ); ?>
+				</a>
+			</div>
+
+			<p class="wp-embed-heading">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_fullname(); ?>
+				</a>
+			</p>
+
+			<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+				<p class="bp-embed-mentionname">@<?php bp_displayed_user_mentionname(); ?></p>
+			<?php endif; ?>
+		</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/blogs-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/blogs-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/blogs-loop.php	(revision 12265)
@@ -0,0 +1,71 @@
+<?php
+/**
+ * BuddyPress - Blogs Loop
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_before_loop(); ?>
+
+<?php if ( bp_has_blogs( bp_ajax_querystring( 'blogs' ) ) ) : ?>
+
+	<?php bp_nouveau_pagination( 'top' ); ?>
+
+	<ul id="blogs-list" class="<?php bp_nouveau_loop_classes(); ?>">
+
+	<?php
+	while ( bp_blogs() ) :
+		bp_the_blog();
+	?>
+
+		<li <?php bp_blog_class( array( 'item-entry' ) ); ?>>
+			<div class="list-wrap">
+
+				<div class="item-avatar">
+					<a href="<?php bp_blog_permalink(); ?>"><?php bp_blog_avatar( bp_nouveau_avatar_args() ); ?></a>
+				</div>
+
+				<div class="item">
+
+					<div class="item-block">
+
+						<h2 class="list-title blogs-title"><a href="<?php bp_blog_permalink(); ?>"><?php bp_blog_name(); ?></a></h2>
+
+						<p class="last-activity item-meta"><?php bp_blog_last_active(); ?></p>
+
+						<?php if ( bp_nouveau_blog_has_latest_post() ) : ?>
+							<p class="meta last-post">
+
+								<?php bp_blog_latest_post(); ?>
+
+							</p>
+						<?php endif; ?>
+
+						<?php bp_nouveau_blogs_loop_buttons( array( 'container' => 'ul' ) ); ?>
+
+					</div>
+
+					<?php bp_nouveau_blogs_loop_item(); ?>
+
+				</div>
+
+
+
+			</div>
+		</li>
+
+	<?php endwhile; ?>
+
+	</ul>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+<?php else : ?>
+
+	bp_nouveau_user_feedback( 'blogs-loop-none' );
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_after_loop();
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/create.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/create.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/create.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * BuddyPress - Blogs Create
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_blogs_create_hook( 'before', 'content_template' ); ?>
+
+<?php bp_nouveau_template_notices(); ?>
+
+<?php bp_nouveau_blogs_create_hook( 'before', 'content' ); ?>
+
+<?php if ( bp_blog_signup_enabled() ) : ?>
+
+	<?php bp_show_blog_signup_form(); ?>
+
+<?php
+else :
+
+	bp_nouveau_user_feedback( 'blogs-no-signup' );
+
+endif;
+?>
+
+<?php
+bp_nouveau_blogs_create_hook( 'after', 'content' );
+
+bp_nouveau_blogs_create_hook( 'after', 'content_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/blogs/index.php	(revision 12265)
@@ -0,0 +1,28 @@
+<?php
+/**
+ * BuddyPress - Blogs Directory
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+	<?php bp_nouveau_before_blogs_directory_content(); ?>
+
+	<?php if ( ! bp_nouveau_is_object_nav_in_sidebar() ) : ?>
+
+		<?php bp_get_template_part( 'common/nav/directory-nav' ); ?>
+
+	<?php endif; ?>
+
+	<div class="screen-content">
+
+	<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+		<div id="blogs-dir-list" class="blogs dir-list" data-bp-list="blogs">
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'directory-blogs-loading' ); ?></div>
+		</div><!-- #blogs-dir-list -->
+
+		<?php bp_nouveau_after_blogs_directory_content(); ?>
+	</div><!-- // .screen-content -->
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/directory-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/directory-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/directory-filters.php	(revision 12265)
@@ -0,0 +1,24 @@
+<?php
+/**
+ * BP Nouveau Component's  filters template.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<div id="dir-filters" class="component-filters clearfix">
+	<div id="<?php bp_nouveau_filter_container_id(); ?>" class="last filter">
+		<label class="bp-screen-reader-text" for="<?php bp_nouveau_filter_id(); ?>">
+			<span ><?php bp_nouveau_filter_label(); ?></span>
+		</label>
+		<div class="select-wrap">
+			<select id="<?php bp_nouveau_filter_id(); ?>" data-bp-filter="<?php bp_nouveau_filter_component(); ?>">
+
+				<?php bp_nouveau_filter_options(); ?>
+
+			</select>
+			<span class="select-arrow" aria-hidden="true"></span>
+		</div>
+	</div>
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/groups-screens-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/groups-screens-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/groups-screens-filters.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BP Nouveau Groups screens filters
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+<div id="comp-filters" class="component-filters clearfix">
+		<div id="<?php bp_nouveau_filter_container_id(); ?>" class="last filter">
+			<label for="<?php bp_nouveau_filter_id(); ?>" class="bp-screen-reader-text">
+				<span ><?php bp_nouveau_filter_label(); ?></span>
+			</label>
+			<div class="select-wrap">
+				<select id="<?php bp_nouveau_filter_id(); ?>" data-bp-filter="<?php bp_nouveau_filter_component(); ?>">
+
+					<?php bp_nouveau_filter_options(); ?>
+
+				</select>
+				<span class="select-arrow" aria-hidden="true"></span>
+			</div>
+		</div>
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/user-screens-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/user-screens-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/filters/user-screens-filters.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BP Nouveau User screens filters
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+<div id="comp-filters" class="component-filters clearfix">
+		<div id="<?php bp_nouveau_filter_container_id(); ?>" class="last filter">
+			<label for="<?php bp_nouveau_filter_id(); ?>" class="bp-screen-reader-text">
+				<span ><?php bp_nouveau_filter_label(); ?></span>
+			</label>
+			<div class="select-wrap">
+				<select id="<?php bp_nouveau_filter_id(); ?>" data-bp-filter="<?php bp_nouveau_filter_component(); ?>">
+
+					<?php bp_nouveau_filter_options(); ?>
+
+				</select>
+				<span class="select-arrow" aria-hidden="true"></span>
+			</div>
+		</div>
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/activity/form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/activity/form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/activity/form.php	(revision 12265)
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Activity Post form JS Templates
+ *
+ * @version 3.1.0
+ */
+?>
+
+<script type="text/html" id="tmpl-activity-post-form-feedback">
+	<span class="bp-icon" aria-hidden="true"></span><p>{{{data.message}}}</p>
+</script>
+
+<script type="text/html" id="tmpl-activity-post-form-avatar">
+	<# if ( data.display_avatar ) { #>
+		<a href="{{data.user_domain}}">
+			<img src="{{data.avatar_url}}" class="avatar user-{{data.user_id}}-avatar avatar-{{data.avatar_width}} photo" width="{{data.avatar_width}}" height="{{data.avatar_width}}" alt="{{data.avatar_alt}}" />
+		</a>
+	<# } #>
+</script>
+
+<script type="text/html" id="tmpl-activity-post-form-options">
+	<?php bp_nouveau_activity_hook( '', 'post_form_options' ); ?>
+</script>
+
+<script type="text/html" id="tmpl-activity-post-form-buttons">
+	<button type="button" class="button dashicons {{data.icon}}" data-button="{{data.id}}"><span class="bp-screen-reader-text">{{data.caption}}</span></button>
+</script>
+
+<script type="text/html" id="tmpl-activity-target-item">
+	<# if ( data.selected ) { #>
+		<input type="hidden" value="{{data.id}}">
+	<# } #>
+
+	<# if ( data.avatar_url ) { #>
+		<img src="{{data.avatar_url}}" class="avatar {{data.object_type}}-{{data.id}}-avatar photo" alt="" />
+	<# } #>
+
+	<span class="bp-item-name">{{data.name}}</span>
+
+	<# if ( data.selected ) { #>
+		<button type="button" class="bp-remove-item dashicons dashicons-no" data-item_id="{{data.id}}">
+			<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Remove item', 'button', 'buddypress' ); ?></span>
+		</button>
+	<# } #>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/invites/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/invites/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/invites/index.php	(revision 12265)
@@ -0,0 +1,155 @@
+<?php
+/**
+ * BP Nouveau Invites main template.
+ *
+ * This template is used to inject the BuddyPress Backbone views
+ * dealing with invites.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<?php if ( bp_is_group_create() ) : ?>
+
+	<h3 class="bp-screen-title creation-step-name">
+		<?php esc_html_e( 'Invite Members', 'buddypress' ); ?>
+	</h3>
+
+<?php else : ?>
+
+	<h2 class="bp-screen-title">
+		<?php esc_html_e( 'Invite Members', 'buddypress' ); ?>
+	</h2>
+
+<?php endif; ?>
+
+<div id="group-invites-container">
+
+	<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Group invitations menu', 'buddypress' ); ?>"></nav>
+
+	<div class="group-invites-column">
+		<div class="subnav-filters group-subnav-filters bp-invites-filters"></div>
+		<div class="bp-invites-feedback"></div>
+		<div class="members bp-invites-content"></div>
+	</div>
+
+</div>
+
+<script type="text/html" id="tmpl-bp-group-invites-feedback">
+	<div class="bp-feedback {{data.type}}">
+		<span class="bp-icon" aria-hidden="true"></span>
+		<p>{{{data.message}}}</p>
+	</div>
+</script>
+
+<script type="text/html" id="tmpl-bp-invites-nav">
+	<a href="{{data.href}}" class="bp-invites-nav-item" data-nav="{{data.id}}">{{data.name}}</a>
+</script>
+
+<script type="text/html" id="tmpl-bp-invites-users">
+	<div class="item-avatar">
+		<img src="{{data.avatar}}" class="avatar" alt="">
+	</div>
+
+	<div class="item">
+		<div class="list-title member-name">
+			{{data.name}}
+		</div>
+
+		<# if ( undefined !== data.is_sent ) { #>
+			<div class="item-meta">
+
+				<# if ( undefined !== data.invited_by ) { #>
+					<ul class="group-inviters">
+						<li><?php esc_html_e( 'Invited by:', 'buddypress' ); ?></li>
+						<# for ( i in data.invited_by ) { #>
+							<li><a href="{{data.invited_by[i].user_link}}" class="bp-tooltip" data-bp-tooltip="{{data.invited_by[i].user_name}}"><img src="{{data.invited_by[i].avatar}}" width="30px" class="avatar mini" alt="{{data.invited_by[i].user_name}}"></a></li>
+						<# } #>
+					</ul>
+				<# } #>
+
+				<p class="status">
+					<# if ( false === data.is_sent ) { #>
+						<?php esc_html_e( 'The invite has not been sent yet.', 'buddypress' ); ?>
+					<# } else { #>
+						<?php esc_html_e( 'The invite has been sent.', 'buddypress' ); ?>
+					<# } #>
+				</p>
+
+			</div>
+		<# } #>
+	</div>
+
+	<div class="action">
+		<# if ( undefined === data.is_sent || ( false === data.is_sent && true === data.can_edit ) ) { #>
+			<button type="button" class="button invite-button group-add-remove-invite-button bp-tooltip bp-icons<# if ( data.selected ) { #> selected<# } #>" data-bp-tooltip="<# if ( data.selected ) { #><?php esc_attr_e( 'Cancel invitation', 'buddypress' ); ?><# } else { #><?php echo esc_attr_x( 'Invite', 'button', 'buddypress' ); ?><# } #>">
+				<span class="icons" aria-hidden="true"></span>
+				<span class="bp-screen-reader-text">
+					<# if ( data.selected ) { #>
+						<?php echo esc_html_x( 'Cancel invitation', 'button', 'buddypress' ); ?>
+					<# } else { #>
+						<?php echo esc_html_x( 'Invite', 'button', 'buddypress' ); ?>
+					<# } #>
+				</span>
+			</button>
+		<# } #>
+
+		<# if ( undefined !== data.can_edit && true === data.can_edit ) { #>
+			<button type="button" class="button invite-button group-remove-invite-button bp-tooltip bp-icons" data-bp-tooltip="<?php echo esc_attr_x( 'Cancel invitation', 'button', 'buddypress' ); ?>">
+				<span class=" icons" aria-hidden="true"></span>
+				<span class="bp-screen-reader-text"><?php echo esc_attr_x( 'Cancel invitation', 'button', 'buddypress' ); ?></span>
+			</button>
+		<# } #>
+	</div>
+
+</script>
+
+<script type="text/html" id="tmpl-bp-invites-selection">
+	<a href="#uninvite-user-{{data.id}}" class="bp-tooltip" data-bp-tooltip="{{data.uninviteTooltip}}" aria-label="{{data.uninviteTooltip}}">
+		<img src="{{data.avatar}}" class="avatar" alt=""/>
+	</a>
+</script>
+
+<script type="text/html" id="tmpl-bp-invites-form">
+
+	<label for="send-invites-control"><?php esc_html_e( 'Optional: add a message to your invite.', 'buddypress' ); ?></label>
+	<textarea id="send-invites-control" class="bp-faux-placeholder-label"></textarea>
+
+	<div class="action">
+		<button type="button" id="bp-invites-reset" class="button bp-secondary-action"><?php echo esc_html_x( 'Cancel', 'button', 'buddypress' ); ?></button>
+		<button type="button" id="bp-invites-send" class="button bp-primary-action"><?php echo esc_html_x( 'Send', 'button', 'buddypress' ); ?></button>
+	</div>
+</script>
+
+<script type="text/html" id="tmpl-bp-invites-filters">
+	<div class="group-invites-search subnav-search clearfix" role="search" >
+		<div class="bp-search">
+			<form action="" method="get" id="group_invites_search_form" class="bp-invites-search-form" data-bp-search="{{data.scope}}">
+				<label for="group_invites_search" class="bp-screen-reader-text"><?php bp_nouveau_search_default_text( _x( 'Search Members', 'heading', 'buddypress' ), false ); ?></label>
+				<input type="search" id="group_invites_search" placeholder="<?php echo esc_attr_x( 'Search', 'search placeholder text', 'buddypress' ); ?>"/>
+
+				<button type="submit" id="group_invites_search_submit" class="nouveau-search-submit">
+					<span class="dashicons dashicons-search" aria-hidden="true"></span>
+					<span id="button-text" class="bp-screen-reader-text"><?php echo esc_html_x( 'Search', 'button', 'buddypress' ); ?></span>
+				</button>
+			</form>
+		</div>
+	</div>
+</script>
+
+<script type="text/html" id="tmpl-bp-invites-paginate">
+	<# if ( 1 !== data.page ) { #>
+		<a href="#previous-page" id="bp-invites-prev-page" class="button invite-button bp-tooltip" data-bp-tooltip="<?php echo esc_attr_x( 'Previous page', 'link', 'buddypress' ); ?>">
+			<span class="dashicons dashicons-arrow-left" aria-hidden="true"></span>
+			<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Previous page', 'link', 'buddypress' ); ?></span>
+		</a>
+	<# } #>
+
+	<# if ( data.total_page !== data.page ) { #>
+		<a href="#next-page" id="bp-invites-next-page" class="button invite-button bp-tooltip" data-bp-tooltip="<?php echo esc_attr_x( 'Next page', 'link', 'buddypress' ); ?>">
+			<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Next page', 'link', 'buddypress' ); ?></span>
+			<span class="dashicons dashicons-arrow-right" aria-hidden="true"></span>
+		</button>
+	<# } #>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/messages/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/messages/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/messages/index.php	(revision 12265)
@@ -0,0 +1,351 @@
+<?php
+/**
+ * BP Nouveau Messages main template.
+ *
+ * This template is used to inject the BuddyPress Backbone views
+ * dealing with user's private messages.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+<div class="subnav-filters filters user-subnav bp-messages-filters" id="subsubnav"></div>
+
+<div class="bp-messages-feedback"></div>
+<div class="bp-messages-content"></div>
+
+<script type="text/html" id="tmpl-bp-messages-feedback">
+	<div class="bp-feedback {{data.type}}">
+		<span class="bp-icon" aria-hidden="true"></span>
+		<p>{{{data.message}}}</p>
+	</div>
+</script>
+
+<?php
+/**
+ * This view is used to inject hooks buffer
+ */
+?>
+<script type="text/html" id="tmpl-bp-messages-hook">
+	{{{data.extraContent}}}
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-form">
+	<?php bp_nouveau_messages_hook( 'before', 'compose_content' ); ?>
+
+	<label for="send-to-input"><?php esc_html_e( 'Send @Username', 'buddypress' ); ?></label>
+	<input type="text" name="send_to" class="send-to-input" id="send-to-input" />
+
+	<label for="subject"><?php _e( 'Subject', 'buddypress' ); ?></label>
+	<input type="text" name="subject" id="subject"/>
+
+	<div id="bp-message-content"></div>
+
+	<?php bp_nouveau_messages_hook( 'after', 'compose_content' ); ?>
+
+	<div class="submit">
+		<input type="button" id="bp-messages-send" class="button bp-primary-action" value="<?php echo esc_attr_x( 'Send', 'button', 'buddypress' ); ?>"/>
+		<input type="button" id="bp-messages-reset" class="text-button small bp-secondary-action" value="<?php echo esc_attr_x( 'Reset', 'form reset button', 'buddypress' ); ?>"/>
+	</div>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-editor">
+	<?php
+	// Add a temporary filter on editor buttons
+	add_filter( 'mce_buttons', 'bp_nouveau_messages_mce_buttons', 10, 1 );
+
+	wp_editor(
+		'',
+		'message_content',
+		array(
+			'textarea_name' => 'message_content',
+			'teeny'         => false,
+			'media_buttons' => false,
+			'dfw'           => false,
+			'tinymce'       => true,
+			'quicktags'     => false,
+			'tabindex'      => '3',
+			'textarea_rows' => 5,
+		)
+	);
+
+	// Remove the temporary filter on editor buttons
+	remove_filter( 'mce_buttons', 'bp_nouveau_messages_mce_buttons', 10, 1 );
+	?>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-paginate">
+	<# if ( 1 !== data.page ) { #>
+		<button id="bp-messages-prev-page"class="button messages-button">
+			<span class="dashicons dashicons-arrow-left"></span>
+			<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Previous page', 'link', 'buddypress' ); ?></span>
+		</button>
+	<# } #>
+
+	<# if ( data.total_page !== data.page ) { #>
+		<button id="bp-messages-next-page"class="button messages-button">
+			<span class="dashicons dashicons-arrow-right"></span>
+			<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Next page', 'link', 'buddypress' ); ?></span>
+		</button>
+	<# } #>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-filters">
+	<li class="user-messages-search" role="search" data-bp-search="{{data.box}}">
+		<div class="bp-search messages-search">
+			<?php bp_nouveau_message_search_form(); ?>
+		</div>
+	</li>
+	<li class="user-messages-bulk-actions"></li>
+</script>
+
+<script type="text/html" id="tmpl-bp-bulk-actions">
+	<input type="checkbox" id="user_messages_select_all" value="1"/>
+	<label for="user_messages_select_all"><?php esc_html_e( 'All Messages', 'buddypress' ); ?></label>
+	<div class="bulk-actions-wrap bp-hide">
+		<div class="bulk-actions select-wrap">
+			<label for="user-messages-bulk-actions" class="bp-screen-reader-text">
+				<?php esc_html_e( 'Select bulk action', 'buddypress' ); ?>
+			</label>
+			<select id="user-messages-bulk-actions">
+				<# for ( i in data ) { #>
+					<option value="{{data[i].value}}">{{data[i].label}}</option>
+				<# } #>
+			</select>
+			<span class="select-arrow" aria-hidden="true"></span>
+		</div>
+		<button class="messages-button bulk-apply bp-tooltip" type="submit" data-bp-tooltip="<?php echo esc_attr_x( 'Apply', 'button', 'buddypress' ); ?>">
+			<span class="dashicons dashicons-yes" aria-hidden="true"></span>
+			<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Apply', 'button', 'buddypress' ); ?></span>
+		</button>
+	</div>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-thread">
+	<div class="thread-cb">
+		<input class="message-check" type="checkbox" name="message_ids[]" id="bp-message-thread-{{data.id}}" value="{{data.id}}">
+		<label for="bp-message-thread-{{data.id}}" class="bp-screen-reader-text"><?php esc_html_e( 'Select message:', 'buddypress' ); ?> {{data.subject}}</label>
+	</div>
+
+	<# if ( ! data.recipientsCount ) { #>
+		<div class="thread-from">
+			<a class="user-link" href="{{data.sender_link}}">
+				<img class="avatar" src="{{data.sender_avatar}}" alt="" />
+				<span class="bp-screen-reader-text"><?php esc_html_e( 'From:', 'buddypress' ); ?></span>
+				<span class="user-name">{{data.sender_name}}</span>
+			</a>
+		</div>
+	<# } else {
+		var recipient = _.first( data.recipients );
+		#>
+		<div class="thread-to">
+			<a class="user-link" href="{{recipient.user_link}}">
+				<img class="avatar" src="{{recipient.avatar}}" alt="" />
+				<span class="bp-screen-reader-text"><?php esc_html_e( 'To:', 'buddypress' ); ?></span>
+				<span class="user-name">{{recipient.user_name}}</span>
+			</a>
+
+			<# if ( data.toOthers ) { #>
+				<span class="num-recipients">{{data.toOthers}}</span>
+			<# } #>
+		</div>
+	<# } #>
+
+	<div class="thread-content" data-thread-id="{{data.id}}">
+		<div class="thread-subject">
+			<span class="thread-count">({{data.count}})</span>
+			<a class="subject" href="../view/{{data.id}}/">{{data.subject}}</a>
+		</div>
+		<p class="excerpt">{{data.excerpt}}</p>
+	</div>
+	<div class="thread-date">
+		<time datetime="{{data.date.toISOString()}}">{{data.display_date}}</time>
+	</div>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-preview">
+	<# if ( undefined !== data.content ) { #>
+
+		<h2 class="message-title preview-thread-title"><?php esc_html_e( 'Active conversation:', 'buddypress' ); ?><span class="messages-title">{{{data.subject}}}</span></h2>
+		<div class="preview-content">
+			<header class="preview-pane-header">
+
+				<# if ( undefined !== data.recipients ) { #>
+					<dl class="thread-participants">
+						<dt><?php esc_html_e( 'Participants:', 'buddypress' ); ?></dt>
+						<dd>
+							<ul class="participants-list">
+								<# for ( i in data.recipients ) { #>
+									<li><a href="{{data.recipients[i].user_link}}" class="bp-tooltip" data-bp-tooltip="{{data.recipients[i].user_name}}"><img class="avatar mini" src="{{data.recipients[i].avatar}}" alt="{{data.recipients[i].user_name}}" /></a></li>
+								<# } #>
+							</ul>
+						</dd>
+					</dl>
+				<# } #>
+
+				<div class="actions">
+
+					<button type="button" class="message-action-delete bp-tooltip bp-icons" data-bp-action="delete" data-bp-tooltip="<?php esc_attr_e( 'Delete conversation.', 'buddypress' ); ?>">
+						<span class="bp-screen-reader-text"><?php esc_html_e( 'Delete conversation.', 'buddypress' ); ?></span>
+					</button>
+
+					<# if ( undefined !== data.star_link ) { #>
+
+						<# if ( false !== data.is_starred ) { #>
+							<a role="button" class="message-action-unstar bp-tooltip bp-icons" href="{{data.star_link}}" data-bp-action="unstar" aria-pressed="true" data-bp-tooltip="<?php esc_attr_e( 'Unstar Conversation', 'buddypress' ); ?>">
+								<span class="bp-screen-reader-text"><?php esc_html_e( 'Unstar Conversation', 'buddypress' ); ?></span>
+							</a>
+						<# } else { #>
+							<a role="button" class="message-action-star bp-tooltip bp-icons" href="{{data.star_link}}" data-bp-action="star" aria-pressed="false" data-bp-tooltip="<?php esc_attr_e( 'Star Conversation', 'buddypress' ); ?>">
+								<span class="bp-screen-reader-text"><?php esc_html_e( 'Star Conversation', 'buddypress' ); ?></span>
+							</a>
+						<# } #>
+
+					<# } #>
+
+					<a href="../view/{{data.id}}/" class="message-action-view bp-tooltip bp-icons" data-bp-action="view" data-bp-tooltip="<?php esc_attr_e( 'View full conversation and reply.', 'buddypress' ); ?>">
+						<span class="bp-screen-reader-text"><?php esc_html_e( 'View full conversation and reply.', 'buddypress' ); ?></span>
+					</a>
+
+					<# if ( data.threadOptions ) { #>
+						<span class="bp-messages-hook thread-options">
+							{{{data.threadOptions}}}
+						</span>
+					<# } #>
+				</div>
+			</header>
+
+			<div class='preview-message'>
+				{{{data.content}}}
+			</div>
+
+			<# if ( data.inboxListItem ) { #>
+				<table class="bp-messages-hook inbox-list-item">
+					<tbody>
+						<tr>{{{data.inboxListItem}}}</tr>
+					</tbody>
+				</table>
+			<# } #>
+		</div>
+	<# } #>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-single-header">
+	<h2 id="message-subject" class="message-title single-thread-title">{{{data.subject}}}</h2>
+	<header class="single-message-thread-header">
+		<# if ( undefined !== data.recipients ) { #>
+			<dl class="thread-participants">
+				<dt><?php esc_html_e( 'Participants:', 'buddypress' ); ?></dt>
+				<dd>
+					<ul class="participants-list">
+						<# for ( i in data.recipients ) { #>
+							<li><a href="{{data.recipients[i].user_link}}" class="bp-tooltip" data-bp-tooltip="{{data.recipients[i].user_name}}"><img class="avatar mini" src="{{data.recipients[i].avatar}}" alt="{{data.recipients[i].user_name}}" /></a></li>
+						<# } #>
+					</ul>
+				</dd>
+			</dl>
+		<# } #>
+
+		<div class="actions">
+			<button type="button" class="message-action-delete bp-tooltip bp-icons" data-bp-action="delete" data-bp-tooltip="<?php esc_attr_e( 'Delete conversation.', 'buddypress' ); ?>">
+				<span class="bp-screen-reader-text"><?php esc_html_e( 'Delete conversation.', 'buddypress' ); ?></span>
+			</button>
+		</div>
+	</header>
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-single-list">
+	<div class="message-metadata">
+		<# if ( data.beforeMeta ) { #>
+			<div class="bp-messages-hook before-message-meta">{{{data.beforeMeta}}}</div>
+		<# } #>
+
+		<a href="{{data.sender_link}}" class="user-link">
+			<img class="avatar" src="{{data.sender_avatar}}" alt="" />
+			<strong>{{data.sender_name}}</strong>
+		</a>
+
+		<time datetime="{{data.date.toISOString()}}" class="activity">{{data.display_date}}</time>
+
+		<div class="actions">
+			<# if ( undefined !== data.star_link ) { #>
+
+				<button type="button" class="message-action-unstar bp-tooltip bp-icons <# if ( false === data.is_starred ) { #>bp-hide<# } #>" data-bp-star-link="{{data.star_link}}" data-bp-action="unstar" data-bp-tooltip="<?php esc_attr_e( 'Unstar Message', 'buddypress' ); ?>">
+					<span class="bp-screen-reader-text"><?php esc_html_e( 'Unstar Message', 'buddypress' ); ?></span>
+				</button>
+
+				<button type="button" class="message-action-star bp-tooltip bp-icons <# if ( false !== data.is_starred ) { #>bp-hide<# } #>" data-bp-star-link="{{data.star_link}}" data-bp-action="star" data-bp-tooltip="<?php esc_attr_e( 'Star Message', 'buddypress' ); ?>">
+					<span class="bp-screen-reader-text"><?php esc_html_e( 'Star Message', 'buddypress' ); ?></span>
+				</button>
+
+			<# } #>
+		</div>
+
+		<# if ( data.afterMeta ) { #>
+			<div class="bp-messages-hook after-message-meta">{{{data.afterMeta}}}</div>
+		<# } #>
+	</div>
+
+	<# if ( data.beforeContent ) { #>
+		<div class="bp-messages-hook before-message-content">{{{data.beforeContent}}}</div>
+	<# } #>
+
+	<div class="message-content">{{{data.content}}}</div>
+
+	<# if ( data.afterContent ) { #>
+		<div class="bp-messages-hook after-message-content">{{{data.afterContent}}}</div>
+	<# } #>
+
+</script>
+
+<script type="text/html" id="tmpl-bp-messages-single">
+	<?php bp_nouveau_messages_hook( 'before', 'thread_content' ); ?>
+
+	<div id="bp-message-thread-header" class="message-thread-header"></div>
+
+	<?php bp_nouveau_messages_hook( 'before', 'thread_list' ); ?>
+
+	<ul id="bp-message-thread-list"></ul>
+
+	<?php bp_nouveau_messages_hook( 'after', 'thread_list' ); ?>
+
+	<?php bp_nouveau_messages_hook( 'before', 'thread_reply' ); ?>
+
+	<form id="send-reply" class="standard-form send-reply">
+		<div class="message-box">
+			<div class="message-metadata">
+
+				<?php bp_nouveau_messages_hook( 'before', 'reply_meta' ); ?>
+
+				<div class="avatar-box">
+					<?php bp_loggedin_user_avatar( 'type=thumb&height=30&width=30' ); ?>
+
+					<strong><?php esc_html_e( 'Send a Reply', 'buddypress' ); ?></strong>
+				</div>
+
+				<?php bp_nouveau_messages_hook( 'after', 'reply_meta' ); ?>
+
+			</div><!-- .message-metadata -->
+
+			<div class="message-content">
+
+				<?php bp_nouveau_messages_hook( 'before', 'reply_box' ); ?>
+
+				<label for="message_content" class="bp-screen-reader-text"><?php _e( 'Reply to Message', 'buddypress' ); ?></label>
+				<div id="bp-message-content"></div>
+
+				<?php bp_nouveau_messages_hook( 'after', 'reply_box' ); ?>
+
+				<div class="submit">
+					<input type="submit" name="send" value="<?php echo esc_attr_x( 'Send Reply', 'button', 'buddypress' ); ?>" id="send_reply_button"/>
+				</div>
+
+			</div><!-- .message-content -->
+
+		</div><!-- .message-box -->
+	</form>
+
+	<?php bp_nouveau_messages_hook( 'after', 'thread_reply' ); ?>
+
+	<?php bp_nouveau_messages_hook( 'after', 'thread_content' ); ?>
+</script>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/messages/search-form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/messages/search-form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/js-templates/messages/search-form.php	(revision 12265)
@@ -0,0 +1,18 @@
+<?php
+/**
+ * BP Nouveau Messages search form template.
+ *
+ * @since 3.2.0
+ * @version 3.2.0
+ */
+?>
+<form action="" method="get" id="user_messages_search_form" class="bp-messages-search-form" data-bp-search="messages">
+	<label for="user_messages_search" class="bp-screen-reader-text">
+		<?php _e( 'Search Messages', 'buddypress' ); ?>
+	</label>
+	<input type="search" id="user_messages_search" placeholder="<?php echo esc_attr_x( 'Search', 'search placeholder text', 'buddypress' ); ?>"/>
+	<button type="submit" id="user_messages_search_submit">
+		<span class="dashicons dashicons-search" aria-hidden="true"></span>
+		<span class="bp-screen-reader-text"><?php echo esc_html_x( 'Search', 'button', 'buddypress' ); ?></span>
+	</button>
+</form>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/nav/directory-nav.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/nav/directory-nav.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/nav/directory-nav.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BP Nouveau Component's directory nav template.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_directory_type_navs_class(); ?>" role="navigation" aria-label="<?php esc_attr_e( 'Directory menu', 'buddypress' ); ?>">
+
+	<?php if ( bp_nouveau_has_nav( array( 'object' => 'directory' ) ) ) : ?>
+
+		<ul class="component-navigation <?php bp_nouveau_directory_list_class(); ?>">
+
+			<?php
+			while ( bp_nouveau_nav_items() ) :
+				bp_nouveau_nav_item();
+			?>
+
+				<li id="<?php bp_nouveau_nav_id(); ?>" class="<?php bp_nouveau_nav_classes(); ?>" <?php bp_nouveau_nav_scope(); ?> data-bp-object="<?php bp_nouveau_directory_nav_object(); ?>">
+					<a href="<?php bp_nouveau_nav_link(); ?>">
+						<?php bp_nouveau_nav_link_text(); ?>
+
+						<?php if ( bp_nouveau_nav_has_count() ) : ?>
+							<span class="count"><?php bp_nouveau_nav_count(); ?></span>
+						<?php endif; ?>
+					</a>
+				</li>
+
+			<?php endwhile; ?>
+
+		</ul><!-- .component-navigation -->
+
+	<?php endif; ?>
+
+</nav><!-- .bp-navs -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/notices/template-notices.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/notices/template-notices.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/notices/template-notices.php	(revision 12265)
@@ -0,0 +1,18 @@
+<?php
+/**
+ * BP Nouveau template notices template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+<aside class="<?php bp_nouveau_template_message_classes(); ?>">
+	<span class="bp-icon" aria-hidden="true"></span>
+	<?php bp_nouveau_template_message(); ?>
+
+	<?php if ( bp_nouveau_has_dismiss_button() ) : ?>
+
+		<button type="button" class="bp-tooltip" data-bp-tooltip="<?php echo esc_attr_x( 'Close', 'button', 'buddypress' ); ?>" aria-label="<?php esc_attr_e( 'Close this notice', 'buddypress' ); ?>" data-bp-close="<?php bp_nouveau_dismiss_button_type(); ?>"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span></button>
+
+	<?php endif; ?>
+</aside>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/search-and-filters-bar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/search-and-filters-bar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/search-and-filters-bar.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * BP Nouveau Search & filters bar
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+<div class="subnav-filters filters no-ajax" id="subnav-filters">
+
+	<?php if ( 'friends' !== bp_current_component() ) : ?>
+	<div class="subnav-search clearfix">
+
+		<?php if ( 'activity' === bp_current_component() ) : ?>
+			<div class="feed"><a href="<?php bp_sitewide_activity_feed_link(); ?>" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( 'RSS Feed', 'buddypress' ); ?>"><span class="bp-screen-reader-text"><?php esc_html_e( 'RSS', 'buddypress' ); ?></span></a></div>
+		<?php endif; ?>
+
+		<?php bp_nouveau_search_form(); ?>
+
+	</div>
+	<?php endif; ?>
+
+		<?php if ( bp_is_user() && ! bp_is_current_action( 'requests' ) ) : ?>
+			<?php bp_get_template_part( 'common/filters/user-screens-filters' ); ?>
+		<?php elseif ( 'groups' === bp_current_component() ) : ?>
+			<?php bp_get_template_part( 'common/filters/groups-screens-filters' ); ?>
+		<?php else : ?>
+			<?php bp_get_template_part( 'common/filters/directory-filters' ); ?>
+		<?php endif; ?>
+
+</div><!-- search & filters -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/search/search-form.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/search/search-form.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/common/search/search-form.php	(revision 12265)
@@ -0,0 +1,23 @@
+<?php
+/**
+ * BP Object search form
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<div class="<?php bp_nouveau_search_container_class(); ?> bp-search" data-bp-search="<?php bp_nouveau_search_object_data_attr() ;?>">
+	<form action="" method="get" class="bp-dir-search-form" id="<?php bp_nouveau_search_selector_id( 'search-form' ); ?>" role="search">
+
+		<label for="<?php bp_nouveau_search_selector_id( 'search' ); ?>" class="bp-screen-reader-text"><?php bp_nouveau_search_default_text( '', false ); ?></label>
+
+		<input id="<?php bp_nouveau_search_selector_id( 'search' ); ?>" name="<?php bp_nouveau_search_selector_name(); ?>" type="search"  placeholder="<?php bp_nouveau_search_default_text(); ?>" />
+
+		<button type="submit" id="<?php bp_nouveau_search_selector_id( 'search-submit' ); ?>" class="nouveau-search-submit" name="<?php bp_nouveau_search_selector_name( 'search_submit' ); ?>">
+			<span class="dashicons dashicons-search" aria-hidden="true"></span>
+			<span id="button-text" class="bp-screen-reader-text"><?php echo esc_html_x( 'Search', 'button', 'buddypress' ); ?></span>
+		</button>
+
+	</form>
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/create.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/create.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/create.php	(revision 12265)
@@ -0,0 +1,50 @@
+<?php
+/**
+ * BuddyPress - Groups Create
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_groups_create_hook( 'before', 'page' ); ?>
+
+	<h2 class="bp-subhead"><?php esc_html_e( 'Create A New Group', 'buddypress' ); ?></h2>
+
+	<?php bp_nouveau_groups_create_hook( 'before', 'content_template' ); ?>
+
+	<?php if ( 'group-invites' !== bp_get_groups_current_create_step() ) : ?>
+		<form action="<?php bp_group_creation_form_action(); ?>" method="post" id="create-group-form" class="standard-form" enctype="multipart/form-data">
+	<?php else : ?>
+		<div id="create-group-form" class="standard-form">
+	<?php endif; ?>
+
+		<?php bp_nouveau_groups_create_hook( 'before' ); ?>
+
+		<?php bp_nouveau_template_notices(); ?>
+
+		<div class="item-body" id="group-create-body">
+
+			<nav class="<?php bp_nouveau_groups_create_steps_classes(); ?>" id="group-create-tabs" role="navigation" aria-label="<?php esc_attr_e( 'Group creation menu', 'buddypress' ); ?>">
+				<ol class="group-create-buttons button-tabs">
+
+					<?php bp_group_creation_tabs(); ?>
+
+				</ol>
+			</nav>
+
+			<?php bp_nouveau_group_creation_screen(); ?>
+
+		</div><!-- .item-body -->
+
+		<?php bp_nouveau_groups_create_hook( 'after' ); ?>
+
+	<?php if ( 'group-invites' !== bp_get_groups_current_create_step() ) : ?>
+		</form><!-- #create-group-form -->
+	<?php else : ?>
+		</div><!-- #create-group-form -->
+	<?php endif; ?>
+
+	<?php bp_nouveau_groups_create_hook( 'after', 'content_template' ); ?>
+
+<?php
+bp_nouveau_groups_create_hook( 'after', 'page' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/groups-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/groups-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/groups-loop.php	(revision 12265)
@@ -0,0 +1,84 @@
+<?php
+/**
+ * BuddyPress - Groups Loop
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_before_loop(); ?>
+
+<?php if ( bp_get_current_group_directory_type() ) : ?>
+	<p class="current-group-type"><?php bp_current_group_directory_type_message(); ?></p>
+<?php endif; ?>
+
+<?php if ( bp_has_groups( bp_ajax_querystring( 'groups' ) ) ) : ?>
+
+	<?php bp_nouveau_pagination( 'top' ); ?>
+
+	<ul id="groups-list" class="<?php bp_nouveau_loop_classes(); ?>">
+
+	<?php
+	while ( bp_groups() ) :
+		bp_the_group();
+	?>
+
+		<li <?php bp_group_class( array( 'item-entry' ) ); ?> data-bp-item-id="<?php bp_group_id(); ?>" data-bp-item-component="groups">
+			<div class="list-wrap">
+
+				<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+					<div class="item-avatar">
+						<a href="<?php bp_group_permalink(); ?>"><?php bp_group_avatar( bp_nouveau_avatar_args() ); ?></a>
+					</div>
+				<?php endif; ?>
+
+				<div class="item">
+
+					<div class="item-block">
+
+						<h2 class="list-title groups-title"><?php bp_group_link(); ?></h2>
+
+						<?php if ( bp_nouveau_group_has_meta() ) : ?>
+
+							<p class="item-meta group-details"><?php bp_nouveau_group_meta(); ?></p>
+
+						<?php endif; ?>
+
+						<p class="last-activity item-meta">
+							<?php
+							printf(
+								/* translators: %s = last activity timestamp (e.g. "active 1 hour ago") */
+								__( 'active %s', 'buddypress' ),
+								bp_get_group_last_active()
+							);
+							?>
+						</p>
+
+					</div>
+
+					<div class="group-desc"><p><?php bp_nouveau_group_description_excerpt(); ?></p></div>
+
+					<?php bp_nouveau_groups_loop_item(); ?>
+
+					<?php bp_nouveau_groups_loop_buttons(); ?>
+
+				</div>
+
+
+			</div>
+		</li>
+
+	<?php endwhile; ?>
+
+	</ul>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+<?php else : ?>
+
+	<?php bp_nouveau_user_feedback( 'groups-loop-none' ); ?>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_after_loop();
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/index.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * BP Nouveau - Groups Directory
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+	<?php bp_nouveau_before_groups_directory_content(); ?>
+
+	<?php bp_nouveau_template_notices(); ?>
+
+	<?php if ( ! bp_nouveau_is_object_nav_in_sidebar() ) : ?>
+
+		<?php bp_get_template_part( 'common/nav/directory-nav' ); ?>
+
+	<?php endif; ?>
+
+	<div class="screen-content">
+
+	<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+		<div id="groups-dir-list" class="groups dir-list" data-bp-list="groups">
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'directory-groups-loading' ); ?></div>
+		</div><!-- #groups-dir-list -->
+
+	<?php bp_nouveau_after_groups_directory_content(); ?>
+	</div><!-- // .screen-content -->
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/activity.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * BuddyPress - Groups Activity
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+?>
+
+<h2 class="bp-screen-title<?php echo ( ! bp_is_group_home() ) ? ' bp-screen-reader-text' : ''; ?>">
+	<?php esc_html_e( 'Group Activities', 'buddypress' ); ?>
+</h2>
+
+<?php bp_nouveau_groups_activity_post_form(); ?>
+
+<div class="subnav-filters filters clearfix">
+
+	<ul>
+
+		<li class="feed"><a href="<?php bp_group_activity_feed_link(); ?>" class="bp-tooltip no-ajax" data-bp-tooltip="<?php esc_attr_e( 'RSS Feed', 'buddypress' ); ?>"><span class="bp-screen-reader-text"><?php esc_html_e( 'RSS', 'buddypress' ); ?></span></a></li>
+
+		<li class="group-act-search"><?php bp_nouveau_search_form(); ?></li>
+
+	</ul>
+
+		<?php bp_get_template_part( 'common/filters/groups-screens-filters' ); ?>
+</div><!-- // .subnav-filters -->
+
+<?php bp_nouveau_group_hook( 'before', 'activity_content' ); ?>
+
+<div id="activity-stream" class="activity single-group" data-bp-list="activity">
+
+		<li id="bp-activity-ajax-loader"><?php bp_nouveau_user_feedback( 'group-activity-loading' ); ?></li>
+
+</div><!-- .activity -->
+
+<?php
+bp_nouveau_group_hook( 'after', 'activity_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin.php	(revision 12265)
@@ -0,0 +1,16 @@
+<?php
+/**
+ * BuddyPress - Groups Admin
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_get_template_part( 'groups/single/parts/admin-subnav' ); ?>
+
+<form action="<?php bp_group_admin_form_action(); ?>" name="group-settings-form" id="group-settings-form" class="standard-form" method="post" enctype="multipart/form-data">
+
+	<?php bp_nouveau_group_manage_screen(); ?>
+
+</form><!-- #group-settings-form -->
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/delete-group.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/delete-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/delete-group.php	(revision 12265)
@@ -0,0 +1,19 @@
+<?php
+/**
+ * BP Nouveau Group's delete group template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<h2 class="bp-screen-title warn">
+	<?php esc_html_e( 'Delete this group', 'buddypress' ); ?>
+</h2>
+
+<?php bp_nouveau_user_feedback( 'group-delete-warning' ); ?>
+
+<label for="delete-group-understand" class="bp-label-text warn">
+	<input type="checkbox" name="delete-group-understand" id="delete-group-understand" value="1" onclick="if(this.checked) { document.getElementById( 'delete-group-button' ).disabled = ''; } else { document.getElementById( 'delete-group-button' ).disabled = 'disabled'; }" />
+	<?php esc_html_e( 'I understand the consequences of deleting this group.', 'buddypress' ); ?>
+</label>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/edit-details.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/edit-details.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/edit-details.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * BP Nouveau Group's edit details template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<?php if ( bp_is_group_create() ) : ?>
+
+	<h3 class="bp-screen-title creation-step-name">
+		<?php esc_html_e( 'Enter Group Name &amp; Description', 'buddypress' ); ?>
+	</h3>
+
+<?php else : ?>
+
+	<h2 class="bp-screen-title">
+		<?php esc_html_e( 'Edit Group Name &amp; Description', 'buddypress' ); ?>
+	</h2>
+
+<?php endif; ?>
+
+<label for="group-name"><?php esc_html_e( 'Group Name (required)', 'buddypress' ); ?></label>
+<input type="text" name="group-name" id="group-name" value="<?php bp_is_group_create() ? bp_new_group_name() : bp_group_name(); ?>" aria-required="true" />
+
+<label for="group-desc"><?php esc_html_e( 'Group Description (required)', 'buddypress' ); ?></label>
+<textarea name="group-desc" id="group-desc" aria-required="true"><?php bp_is_group_create() ? bp_new_group_description() : bp_group_description_editable(); ?></textarea>
+
+<?php if ( ! bp_is_group_create() ) : ?>
+	<p class="bp-controls-wrap">
+		<label for="group-notify-members" class="bp-label-text">
+			<input type="checkbox" name="group-notify-members" id="group-notify-members" value="1" /> <?php esc_html_e( 'Notify group members of these changes via email', 'buddypress' ); ?>
+		</label>
+	</p>
+<?php endif; ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-avatar.php	(revision 12265)
@@ -0,0 +1,121 @@
+<?php
+/**
+ * BP Nouveau Group's avatar template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<?php if ( bp_is_group_create() ) : ?>
+
+	<h3 class="bp-screen-title creation-step-name">
+		<?php esc_html_e( 'Upload Group Avatar', 'buddypress' ); ?>
+	</h3>
+
+<?php else : ?>
+
+	<h2 class="bp-screen-title">
+		<?php esc_html_e( 'Change Group Avatar', 'buddypress' ); ?>
+	</h2>
+
+<?php endif; ?>
+
+<?php if ( ! bp_is_group_create() ) : ?>
+	<?php if ( ! bp_get_group_has_avatar() ) : ?>
+		<p class="bp-help-text"><?php esc_html_e( 'Add an image to use as a profile photo for this group. The image will be shown on the main group page, and in search results.', 'buddypress' ); ?></p>
+	<?php else : ?>
+		<p class="bp-help-text"><?php esc_html_e( 'Edit or update your avatar image for this group.', 'buddypress' ); ?></p>
+	<?php endif; ?>
+<?php endif; ?>
+
+
+<?php if ( 'upload-image' === bp_get_avatar_admin_step() ) : ?>
+	<?php if ( bp_is_group_create() ) : ?>
+
+
+		<div class="left-menu">
+
+			<?php bp_new_group_avatar(); ?>
+
+		</div><!-- .left-menu -->
+
+		<div class="main-column">
+	<?php endif; ?>
+
+			<p class="bp-help-text"><?php esc_html_e( 'Upload an image to use as a profile photo for this group. The image will be shown on the main group page, and in search results.', 'buddypress' ); ?></p>
+
+			<p>
+				<label for="file" class="bp-screen-reader-text"><?php esc_html_e( 'Select an image', 'buddypress' ); ?></label>
+				<input type="file" name="file" id="file" />
+				<input type="submit" name="upload" id="upload" value="<?php esc_attr_e( 'Upload Image', 'buddypress' ); ?>" />
+				<input type="hidden" name="action" id="action" value="bp_avatar_upload" />
+			</p>
+
+	<?php if ( bp_is_group_create() ) : ?>
+			<p class="bp-help-text"><?php esc_html_e( 'To skip the group profile photo upload process, hit the "Next Step" button.', 'buddypress' ); ?></p>
+		</div><!-- .main-column -->
+
+	<?php elseif ( bp_get_group_has_avatar() ) : ?>
+
+		<p><?php esc_html_e( "If you'd like to remove the existing group profile photo but not upload a new one, please use the delete group profile photo button.", 'buddypress' ); ?></p>
+
+		<?php
+		bp_button(
+			array(
+				'id'         => 'delete_group_avatar',
+				'component'  => 'groups',
+				'wrapper_id' => 'delete-group-avatar-button',
+				'link_class' => 'edit',
+				'link_href'  => bp_get_group_avatar_delete_link(),
+				'link_title' => __( 'Delete Group Profile Photo', 'buddypress' ),
+				'link_text'  => __( 'Delete Group Profile Photo', 'buddypress' ),
+			)
+		);
+		?>
+
+	<?php
+	endif;
+
+	/**
+	 * Load the Avatar UI templates
+	 *
+	 * @since 2.3.0
+	 */
+	bp_avatar_get_templates();
+
+	if ( ! bp_is_group_create() ) {
+		wp_nonce_field( 'bp_avatar_upload' );
+	}
+	?>
+
+<?php
+endif;
+
+if ( 'crop-image' === bp_get_avatar_admin_step() ) :
+?>
+
+	<h2><?php esc_html_e( 'Crop Group Profile Photo', 'buddypress' ); ?></h2>
+
+	<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-to-crop" class="avatar" alt="<?php esc_attr_e( 'Profile photo to crop', 'buddypress' ); ?>" />
+
+	<div id="avatar-crop-pane">
+		<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-crop-preview" class="avatar" alt="<?php esc_attr_e( 'Profile photo preview', 'buddypress' ); ?>" />
+	</div>
+
+	<input type="submit" name="avatar-crop-submit" id="avatar-crop-submit" value="<?php esc_attr_e( 'Crop Image', 'buddypress' ); ?>" />
+
+	<input type="hidden" name="image_src" id="image_src" value="<?php bp_avatar_to_crop_src(); ?>" />
+	<input type="hidden" id="x" name="x" />
+	<input type="hidden" id="y" name="y" />
+	<input type="hidden" id="w" name="w" />
+	<input type="hidden" id="h" name="h" />
+
+	<?php
+	if ( ! bp_is_group_create() ) {
+		wp_nonce_field( 'bp_avatar_cropstore' );
+	}
+	?>
+
+<?php
+endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-cover-image.php	(revision 12265)
@@ -0,0 +1,29 @@
+<?php
+/**
+ * BP Nouveau Group's cover image template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<?php if ( bp_is_group_create() ) : ?>
+
+	<h2 class="bp-screen-title creation-step-name">
+		<?php esc_html_e( 'Upload Cover Image', 'buddypress' ); ?>
+	</h2>
+
+	<div id="header-cover-image"></div>
+
+<?php else : ?>
+
+	<h2 class="bp-screen-title">
+		<?php esc_html_e( 'Change Cover Image', 'buddypress' ); ?>
+	</h2>
+
+<?php endif; ?>
+
+<p><?php esc_html_e( 'The Cover Image will be used to customize the header of your group.', 'buddypress' ); ?></p>
+
+<?php
+bp_attachments_get_template_part( 'cover-images/index' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/group-settings.php	(revision 12265)
@@ -0,0 +1,111 @@
+<?php
+/**
+ * BP Nouveau Group's edit settings template.
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+<?php if ( bp_is_group_create() ) : ?>
+
+	<h3 class="bp-screen-title creation-step-name">
+		<?php esc_html_e( 'Select Group Settings', 'buddypress' ); ?>
+	</h3>
+
+<?php else : ?>
+
+	<h2 class="bp-screen-title">
+		<?php esc_html_e( 'Change Group Settings', 'buddypress' ); ?>
+	</h2>
+
+<?php endif; ?>
+
+<div class="group-settings-selections">
+
+	<fieldset class="radio group-status-type">
+		<legend><?php esc_html_e( 'Privacy Options', 'buddypress' ); ?></legend>
+
+		<label for="group-status-public">
+			<input type="radio" name="group-status" id="group-status-public" value="public"<?php if ( 'public' === bp_get_new_group_status() || ! bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="public-group-description" /> <?php esc_html_e( 'This is a public group', 'buddypress' ); ?>
+		</label>
+
+		<ul id="public-group-description">
+			<li><?php esc_html_e( 'Any site member can join this group.', 'buddypress' ); ?></li>
+			<li><?php esc_html_e( 'This group will be listed in the groups directory and in search results.', 'buddypress' ); ?></li>
+			<li><?php esc_html_e( 'Group content and activity will be visible to any site member.', 'buddypress' ); ?></li>
+		</ul>
+
+		<label for="group-status-private">
+			<input type="radio" name="group-status" id="group-status-private" value="private"<?php if ( 'private' === bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="private-group-description" /> <?php esc_html_e( 'This is a private group', 'buddypress' ); ?>
+		</label>
+
+		<ul id="private-group-description">
+			<li><?php esc_html_e( 'Only people who request membership and are accepted can join the group.', 'buddypress' ); ?></li>
+			<li><?php esc_html_e( 'This group will be listed in the groups directory and in search results.', 'buddypress' ); ?></li>
+			<li><?php esc_html_e( 'Group content and activity will only be visible to members of the group.', 'buddypress' ); ?></li>
+		</ul>
+
+		<label for="group-status-hidden">
+			<input type="radio" name="group-status" id="group-status-hidden" value="hidden"<?php if ( 'hidden' === bp_get_new_group_status() ) { ?> checked="checked"<?php } ?> aria-describedby="hidden-group-description" /> <?php esc_html_e( 'This is a hidden group', 'buddypress' ); ?>
+		</label>
+
+		<ul id="hidden-group-description">
+			<li><?php esc_html_e( 'Only people who are invited can join the group.', 'buddypress' ); ?></li>
+			<li><?php esc_html_e( 'This group will not be listed in the groups directory or search results.', 'buddypress' ); ?></li>
+			<li><?php esc_html_e( 'Group content and activity will only be visible to members of the group.', 'buddypress' ); ?></li>
+		</ul>
+
+	</fieldset>
+
+<?php
+// Group type selection
+$group_types = bp_groups_get_group_types( array( 'show_in_create_screen' => true ), 'objects' );
+if ( $group_types ) : ?>
+
+	<fieldset class="group-create-types">
+		<legend><?php esc_html_e( 'Group Types', 'buddypress' ); ?></legend>
+
+		<p tabindex="0"><?php esc_html_e( 'Select the types this group should be a part of.', 'buddypress' ); ?></p>
+
+		<?php foreach ( $group_types as $type ) : ?>
+			<div class="checkbox">
+				<label for="<?php printf( 'group-type-%s', $type->name ); ?>">
+					<input type="checkbox" name="group-types[]" id="<?php printf( 'group-type-%s', $type->name ); ?>" value="<?php echo esc_attr( $type->name ); ?>" <?php bp_nouveau_group_type_checked( $type ); ?>/> <?php echo esc_html( $type->labels['name'] ); ?>
+					<?php
+					if ( ! empty( $type->description ) ) {
+						printf( '&ndash; %s', '<span class="bp-group-type-desc">' . esc_html( $type->description ) . '</span>' );
+					}
+					?>
+				</label>
+			</div>
+
+		<?php endforeach; ?>
+
+	</fieldset>
+
+<?php endif; ?>
+
+	<fieldset class="radio group-invitations">
+		<legend><?php esc_html_e( 'Group Invitations', 'buddypress' ); ?></legend>
+
+		<p tabindex="0"><?php esc_html_e( 'Which members of this group are allowed to invite others?', 'buddypress' ); ?></p>
+
+		<label for="group-invite-status-members">
+			<input type="radio" name="group-invite-status" id="group-invite-status-members" value="members"<?php bp_group_show_invite_status_setting( 'members' ); ?> />
+				<?php esc_html_e( 'All group members', 'buddypress' ); ?>
+		</label>
+
+		<label for="group-invite-status-mods">
+			<input type="radio" name="group-invite-status" id="group-invite-status-mods" value="mods"<?php bp_group_show_invite_status_setting( 'mods' ); ?> />
+				<?php esc_html_e( 'Group admins and mods only', 'buddypress' ); ?>
+		</label>
+
+		<label for="group-invite-status-admins">
+			<input type="radio" name="group-invite-status" id="group-invite-status-admins" value="admins"<?php bp_group_show_invite_status_setting( 'admins' ); ?> />
+				<?php esc_html_e( 'Group admins only', 'buddypress' ); ?>
+		</label>
+
+	</fieldset>
+
+</div><!-- // .group-settings-selections -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/manage-members.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/manage-members.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/manage-members.php	(revision 12265)
@@ -0,0 +1,123 @@
+<?php
+/**
+ * BP Nouveau Group's manage members template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<h2 class="bp-screen-title <?php if ( bp_is_group_create() ) { echo esc_attr( 'creation-step-name' ); } ?>">
+	<?php esc_html_e( 'Manage Group Members', 'buddypress' ); ?>
+</h2>
+
+	<p class="bp-help-text"><?php esc_html_e( 'Manage your group members; promote to moderators, admins or demote or ban.', 'buddypress' ); ?></p>
+
+	<dl class="groups-manage-members-list">
+
+	<dt class="admin-section section-title"><?php esc_html_e( 'Administrators', 'buddypress' ); ?></dt>
+
+	<?php if ( bp_has_members( '&include=' . bp_group_admin_ids() ) ) : ?>
+		<dd class="admin-listing">
+			<ul id="admins-list" class="item-list single-line">
+
+				<?php while ( bp_members() ) : bp_the_member(); ?>
+				<li class="member-entry clearfix">
+
+					<?php echo bp_core_fetch_avatar( array( 'item_id' => bp_get_member_user_id(), 'type' => 'thumb', 'width' => 30, 'height' => 30, 'alt' => '' ) ); ?>
+					<p class="list-title member-name">
+						<a href="<?php bp_member_permalink(); ?>"> <?php bp_member_name(); ?></a>
+					</p>
+
+					<?php if ( count( bp_group_admin_ids( false, 'array' ) ) > 1 ) : ?>
+
+						<p class="action text-links-list">
+							<a class="button confirm admin-demote-to-member" href="<?php bp_group_member_demote_link( bp_get_member_user_id() ); ?>"><?php esc_html_e( 'Demote to Member', 'buddypress' ); ?></a>
+						</p>
+
+					<?php endif; ?>
+
+				</li>
+				<?php endwhile; ?>
+
+			</ul>
+		</dd>
+	<?php endif; ?>
+
+	<?php if ( bp_group_has_moderators() ) : ?>
+
+		<dt class="moderator-section section-title"><?php esc_html_e( 'Moderators', 'buddypress' ); ?></dt>
+
+		<dd class="moderator-listing">
+		<?php if ( bp_has_members( '&include=' . bp_group_mod_ids() ) ) : ?>
+			<ul id="mods-list" class="item-list single-line">
+
+				<?php while ( bp_members() ) : bp_the_member(); ?>
+				<li class="members-entry clearfix">
+
+					<?php echo bp_core_fetch_avatar( array( 'item_id' => bp_get_member_user_id(), 'type' => 'thumb', 'width' => 30, 'height' => 30, 'alt' => '' ) ); ?>
+					<p class="list-title member-name">
+						<a href="<?php bp_member_permalink(); ?>"> <?php bp_member_name(); ?></a>
+					</p>
+
+					<div class="members-manage-buttons action text-links-list">
+						<a href="<?php bp_group_member_promote_admin_link( array( 'user_id' => bp_get_member_user_id() ) ); ?>" class="button confirm mod-promote-to-admin"><?php esc_html_e( 'Promote to Admin', 'buddypress' ); ?></a>
+						<a class="button confirm mod-demote-to-member" href="<?php bp_group_member_demote_link( bp_get_member_user_id() ); ?>"><?php esc_html_e( 'Demote to Member', 'buddypress' ); ?></a>
+					</div>
+
+				</li>
+
+				<?php endwhile; ?>
+
+			</ul>
+
+		<?php endif; ?>
+	</dd>
+	<?php endif ?>
+
+
+	<dt class="gen-members-section section-title"><?php esc_html_e( 'Members', 'buddypress' ); ?></dt>
+
+	<dd class="general-members-listing">
+		<?php if ( bp_group_has_members( 'per_page=15&exclude_banned=0' ) ) : ?>
+
+			<?php if ( bp_group_member_needs_pagination() ) : ?>
+
+				<?php bp_nouveau_pagination( 'top' ) ; ?>
+
+			<?php endif; ?>
+
+			<ul id="members-list" class="item-list single-line">
+				<?php while ( bp_group_members() ) : bp_group_the_member(); ?>
+
+					<li class="<?php bp_group_member_css_class(); ?> members-entry clearfix">
+						<?php bp_group_member_avatar_mini(); ?>
+
+						<p class="list-title member-name">
+							<?php bp_group_member_link(); ?>
+							<span class="banned warn">
+								<?php if ( bp_get_group_member_is_banned() ) : ?>
+									<?php
+									/* translators: indicates a user is banned from a group, e.g. "Mike (banned)". */
+									esc_html_e( '(banned)', 'buddypress' );
+									?>
+								<?php endif; ?>
+							</span>
+						</p>
+
+						<?php bp_nouveau_groups_manage_members_buttons( array( 'container' => 'div', 'container_classes' => array( 'members-manage-buttons', 'text-links-list' ), 'parent_element' => '  ' ) ) ; ?>
+
+					</li>
+
+				<?php endwhile; ?>
+			</ul>
+	</dd>
+
+</dl>
+
+	<?php else:
+
+		bp_nouveau_user_feedback( 'group-manage-members-none' );
+
+	endif; ?>
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/membership-requests.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/membership-requests.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/admin/membership-requests.php	(revision 12265)
@@ -0,0 +1,14 @@
+<?php
+/**
+ * BP Nouveau Group's membership requests template.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<div class="requests" data-bp-list="group_requests">
+
+	<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'group-requests-loading' ); ?></div>
+
+</div><!-- .requests -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/cover-image-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/cover-image-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/cover-image-header.php	(revision 12265)
@@ -0,0 +1,64 @@
+<?php
+/**
+ * BuddyPress - Groups Cover Image Header.
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+<div id="cover-image-container">
+	<div id="header-cover-image"></div>
+
+	<div id="item-header-cover-image">
+		<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+			<div id="item-header-avatar">
+				<a href="<?php echo esc_url( bp_get_group_permalink() ); ?>" title="<?php echo esc_attr( bp_get_group_name() ); ?>">
+
+					<?php bp_group_avatar(); ?>
+
+				</a>
+			</div><!-- #item-header-avatar -->
+		<?php endif; ?>
+
+<?php	if ( ! bp_nouveau_groups_front_page_description() ) : ?>
+		<div id="item-header-content">
+
+			<p class="highlight group-status"><strong><?php echo esc_html( bp_nouveau_group_meta()->status ); ?></strong></p>
+			<p class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_last_active( 0, array( 'relative' => false ) ) ); ?>">
+				<?php
+				/* translators: %s = last activity timestamp (e.g. "active 1 hour ago") */
+				printf( __( 'active %s', 'buddypress' ), bp_get_group_last_active() );
+				?>
+			</p>
+
+			<?php echo bp_nouveau_group_meta()->group_type_list; ?>
+			<?php bp_nouveau_group_hook( 'before', 'header_meta' ); ?>
+
+			<?php if ( bp_nouveau_group_has_meta_extra() ) : ?>
+				<div class="item-meta">
+
+					<?php echo bp_nouveau_group_meta()->extra; ?>
+
+				</div><!-- .item-meta -->
+			<?php endif; ?>
+
+			<?php bp_nouveau_group_header_buttons(); ?>
+
+		</div><!-- #item-header-content -->
+<?php endif; ?>
+
+		<?php bp_get_template_part( 'groups/single/parts/header-item-actions' ); ?>
+
+	</div><!-- #item-header-cover-image -->
+
+
+</div><!-- #cover-image-container -->
+
+<?php if ( ! bp_nouveau_groups_front_page_description() && bp_nouveau_group_has_meta( 'description' ) ) : ?>
+	<div class="desc-wrap">
+		<div class="group-description">
+			<?php bp_group_description(); ?>
+		</div><!-- //.group_description -->
+	</div>
+<?php endif; ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/default-front.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/default-front.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/default-front.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * BP Nouveau Default group's front template.
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+<div class="group-front-page">
+
+	<?php if ( ! is_active_sidebar( 'sidebar-buddypress-groups' ) || ! bp_nouveau_groups_do_group_boxes() ) : ?>
+		<?php if ( ! is_customize_preview() && bp_current_user_can( 'bp_moderate' ) ) : ?>
+
+			<div class="bp-feedback custom-homepage-info info no-icon">
+				<strong><?php esc_html_e( 'Manage the Groups default front page', 'buddypress' ); ?></strong>
+
+				<p>
+				<?php
+				printf(
+					esc_html__( 'You can set your preferences for the %1$s or add %2$s to it.', 'buddypress' ),
+					bp_nouveau_groups_get_customizer_option_link(),
+					bp_nouveau_groups_get_customizer_widgets_link()
+				);
+				?>
+				</p>
+
+			</div>
+
+		<?php endif; ?>
+	<?php endif; ?>
+
+	<?php if ( bp_nouveau_groups_front_page_description() && bp_nouveau_group_has_meta( 'description' ) ) : ?>
+		<div class="group-description">
+
+			<?php bp_group_description(); ?>
+
+		</div><!-- .group-description -->
+	<?php endif; ?>
+
+	<?php if ( bp_nouveau_groups_do_group_boxes() ) : ?>
+		<div class="bp-plugin-widgets">
+
+			<?php bp_custom_group_boxes(); ?>
+
+		</div><!-- .bp-plugin-widgets -->
+	<?php endif; ?>
+
+	<?php if ( is_active_sidebar( 'sidebar-buddypress-groups' ) ) : ?>
+		<div id="group-front-widgets" class="bp-sidebar bp-widget-area" role="complementary">
+
+			<?php dynamic_sidebar( 'sidebar-buddypress-groups' ); ?>
+
+		</div><!-- .bp-sidebar.bp-widget-area -->
+	<?php endif; ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/group-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/group-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/group-header.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * BuddyPress - Groups Header
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+<?php bp_get_template_part( 'groups/single/parts/header-item-actions' ); ?>
+
+<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+	<div id="item-header-avatar">
+		<a href="<?php echo esc_url( bp_get_group_permalink() ); ?>" class="bp-tooltip" data-bp-tooltip="<?php echo esc_attr( bp_get_group_name() ); ?>">
+
+			<?php bp_group_avatar(); ?>
+
+		</a>
+	</div><!-- #item-header-avatar -->
+<?php endif; ?>
+
+<div id="item-header-content">
+
+	<p class="highlight group-status"><strong><?php echo esc_html( bp_nouveau_group_meta()->status ); ?></strong></p>
+
+	<p class="activity" data-livestamp="<?php bp_core_iso8601_date( bp_get_group_last_active( 0, array( 'relative' => false ) ) ); ?>">
+		<?php
+		echo esc_html(
+			sprintf(
+				/* translators: %s = last activity timestamp (e.g. "active 1 hour ago") */
+				__( 'active %s', 'buddypress' ),
+				bp_get_group_last_active()
+			)
+		);
+		?>
+	</p>
+
+	<?php bp_nouveau_group_hook( 'before', 'header_meta' ); ?>
+
+	<?php if ( bp_nouveau_group_has_meta_extra() ) : ?>
+		<div class="item-meta">
+
+			<?php echo bp_nouveau_group_meta()->extra; ?>
+
+		</div><!-- .item-meta -->
+	<?php endif; ?>
+
+
+		<?php if ( ! bp_nouveau_groups_front_page_description() && bp_nouveau_group_has_meta( 'description' ) ) : ?>
+			<div class="group-description">
+				<?php bp_group_description(); ?>
+			</div><!-- //.group_description -->
+		<?php endif; ?>
+
+</div><!-- #item-header-content -->
+
+<?php bp_nouveau_group_header_buttons(); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/home.php	(revision 12265)
@@ -0,0 +1,43 @@
+<?php
+/**
+ * BuddyPress - Groups Home
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+if ( bp_has_groups() ) :
+	while ( bp_groups() ) :
+		bp_the_group();
+	?>
+
+		<?php bp_nouveau_group_hook( 'before', 'home_content' ); ?>
+
+		<div id="item-header" role="complementary" data-bp-item-id="<?php bp_group_id(); ?>" data-bp-item-component="groups" class="groups-header single-headers">
+
+			<?php bp_nouveau_group_header_template_part(); ?>
+
+		</div><!-- #item-header -->
+
+		<div class="bp-wrap">
+
+			<?php if ( ! bp_nouveau_is_object_nav_in_sidebar() ) : ?>
+
+				<?php bp_get_template_part( 'groups/single/parts/item-nav' ); ?>
+
+			<?php endif; ?>
+
+			<div id="item-body" class="item-body">
+
+				<?php bp_nouveau_group_template_part(); ?>
+
+			</div><!-- #item-body -->
+
+		</div><!-- // .bp-wrap -->
+
+		<?php bp_nouveau_group_hook( 'after', 'home_content' ); ?>
+
+	<?php endwhile; ?>
+
+<?php
+endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/members-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/members-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/members-loop.php	(revision 12265)
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Group Members Loop template
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+<?php if ( bp_group_has_members( bp_ajax_querystring( 'group_members' ) ) ) : ?>
+
+	<?php bp_nouveau_group_hook( 'before', 'members_content' ); ?>
+
+	<?php bp_nouveau_pagination( 'top' ); ?>
+
+	<?php bp_nouveau_group_hook( 'before', 'members_list' ); ?>
+
+	<ul id="members-list" class="<?php bp_nouveau_loop_classes(); ?>">
+
+		<?php
+		while ( bp_group_members() ) :
+			bp_group_the_member();
+		?>
+
+			<li <?php bp_member_class( array( 'item-entry' ) ); ?> data-bp-item-id="<?php echo esc_attr( bp_get_group_member_id() ); ?>" data-bp-item-component="members">
+
+				<div class="list-wrap">
+
+					<div class="item-avatar">
+						<a href="<?php bp_group_member_domain(); ?>">
+							<?php bp_group_member_avatar(); ?>
+						</a>
+					</div>
+
+					<div class="item">
+
+						<div class="item-block">
+							<h3 class="list-title member-name"><?php bp_group_member_link(); ?></h3>
+
+							<p class="joined item-meta">
+								<?php bp_group_member_joined_since(); ?>
+							</p>
+
+							<?php bp_nouveau_group_hook( '', 'members_list_item' ); ?>
+
+							<?php bp_nouveau_members_loop_buttons(); ?>
+						</div>
+
+					</div>
+
+				</div><!-- // .list-wrap -->
+
+			</li>
+
+		<?php endwhile; ?>
+
+	</ul>
+
+	<?php bp_nouveau_group_hook( 'after', 'members_list' ); ?>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+	<?php bp_nouveau_group_hook( 'after', 'members_content' ); ?>
+
+<?php else :
+
+	bp_nouveau_user_feedback( 'group-members-none' );
+
+endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/members.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/members.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/members.php	(revision 12265)
@@ -0,0 +1,28 @@
+<?php
+/**
+ * BuddyPress - Groups Members
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+
+<div class="subnav-filters filters clearfix no-subnav">
+
+	<?php bp_nouveau_search_form(); ?>
+
+	<?php bp_get_template_part( 'common/filters/groups-screens-filters' ); ?>
+
+</div>
+
+<h2 class="bp-screen-title">
+	<?php esc_html_e( 'Membership List', 'buddypress' ); ?>
+</h2>
+
+
+<div id="members-group-list" class="group_members dir-list" data-bp-list="group_members">
+
+	<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'group-members-loading' ); ?></div>
+
+</div><!-- .group_members.dir-list -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/admin-subnav.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/admin-subnav.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/admin-subnav.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress Single Groups Admin Navigation
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Group administration menu', 'buddypress' ); ?>">
+
+	<?php if ( bp_nouveau_has_nav( array( 'object' => 'group_manage' ) ) ) : ?>
+
+		<ul class="subnav">
+
+			<?php
+			while ( bp_nouveau_nav_items() ) :
+				bp_nouveau_nav_item();
+			?>
+
+				<li id="<?php bp_nouveau_nav_id(); ?>" class="<?php bp_nouveau_nav_classes(); ?>">
+					<a href="<?php bp_nouveau_nav_link(); ?>" id="<?php bp_nouveau_nav_link_id(); ?>">
+						<?php bp_nouveau_nav_link_text(); ?>
+
+						<?php if ( bp_nouveau_nav_has_count() ) : ?>
+							<span class="count"><?php bp_nouveau_nav_count(); ?></span>
+						<?php endif; ?>
+					</a>
+				</li>
+
+			<?php endwhile; ?>
+
+		</ul>
+
+	<?php endif; ?>
+
+</nav><!-- #isubnav -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/header-item-actions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/header-item-actions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/header-item-actions.php	(revision 12265)
@@ -0,0 +1,41 @@
+<?php
+/**
+ * BuddyPress - Groups Header item-actions.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+<div id="item-actions" class="group-item-actions">
+
+	<?php if ( bp_current_user_can( 'groups_access_group' ) ) : ?>
+
+		<h2 class="bp-screen-reader-text"><?php esc_html_e( 'Group Leadership', 'buddypress' ); ?></h2>
+
+		<dl class="moderators-lists">
+			<dt class="moderators-title"><?php esc_html_e( 'Group Administrators', 'buddypress' ); ?></dt>
+			<dd class="user-list admins"><?php bp_group_list_admins(); ?>
+				<?php bp_nouveau_group_hook( 'after', 'menu_admins' ); ?>
+			</dd>
+		</dl>
+
+		<?php
+		if ( bp_group_has_moderators() ) :
+			  bp_nouveau_group_hook( 'before', 'menu_mods' );
+		?>
+
+			<dl class="moderators-lists">
+				<dt class="moderators-title"><?php esc_html_e( 'Group Mods', 'buddypress' ); ?></dt>
+				<dd class="user-list moderators">
+					<?php
+					bp_group_list_mods();
+					bp_nouveau_group_hook( 'after', 'menu_mods' );
+					?>
+				</dd>
+			</dl>
+
+		<?php endif; ?>
+
+	<?php endif; ?>
+
+</div><!-- .item-actions -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/item-nav.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/item-nav.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/parts/item-nav.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * BuddyPress Single Groups item Navigation
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_nav_classes(); ?>" id="object-nav" role="navigation" aria-label="<?php esc_attr_e( 'Group menu', 'buddypress' ); ?>">
+
+	<?php if ( bp_nouveau_has_nav( array( 'object' => 'groups' ) ) ) : ?>
+
+		<ul>
+
+			<?php
+			while ( bp_nouveau_nav_items() ) :
+				bp_nouveau_nav_item();
+			?>
+
+				<li id="<?php bp_nouveau_nav_id(); ?>" class="<?php bp_nouveau_nav_classes(); ?>">
+					<a href="<?php bp_nouveau_nav_link(); ?>" id="<?php bp_nouveau_nav_link_id(); ?>">
+						<?php bp_nouveau_nav_link_text(); ?>
+
+						<?php if ( bp_nouveau_nav_has_count() ) : ?>
+							<span class="count"><?php bp_nouveau_nav_count(); ?></span>
+						<?php endif; ?>
+					</a>
+				</li>
+
+			<?php endwhile; ?>
+
+			<?php bp_nouveau_group_hook( '', 'options_nav' ); ?>
+
+		</ul>
+
+	<?php endif; ?>
+
+</nav>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/plugins.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/plugins.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/plugins.php	(revision 12265)
@@ -0,0 +1,13 @@
+<?php
+/**
+ * BuddyPress - Groups plugins
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_group_hook( 'before', 'plugin_template' );
+
+bp_nouveau_plugin_hook( 'content' );
+
+bp_nouveau_group_hook( 'after', 'plugin_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/request-membership.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/request-membership.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/request-membership.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress - Groups Request Membership
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_group_hook( 'before', 'request_membership_content' ); ?>
+
+<?php if ( ! bp_group_has_requested_membership() ) : ?>
+	<p>
+		<?php
+		echo esc_html(
+			sprintf(
+				/* translators: %s = group name */
+				__( 'You are requesting to become a member of the group "%s".', 'buddypress' ),
+				bp_get_group_name()
+			)
+		);
+		?>
+	</p>
+
+	<form action="<?php bp_group_form_action( 'request-membership' ); ?>" method="post" name="request-membership-form" id="request-membership-form" class="standard-form">
+		<label for="group-request-membership-comments"><?php esc_html( 'Comments (optional)', 'buddypress' ); ?></label>
+		<textarea name="group-request-membership-comments" id="group-request-membership-comments"></textarea>
+
+		<?php bp_nouveau_group_hook( '', 'request_membership_content' ); ?>
+
+		<p><input type="submit" name="group-request-send" id="group-request-send" value="<?php echo esc_attr_x( 'Send Request', 'button', 'buddypress' ); ?>" />
+
+		<?php wp_nonce_field( 'groups_request_membership' ); ?>
+	</form><!-- #request-membership-form -->
+<?php endif; ?>
+
+<?php
+bp_nouveau_group_hook( 'after', 'request_membership_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/requests-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/requests-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/requests-loop.php	(revision 12265)
@@ -0,0 +1,56 @@
+<?php
+/**
+ * BuddyPress - Groups Requests Loop
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<?php if ( bp_group_has_membership_requests( bp_ajax_querystring( 'membership_requests' ) ) ) : ?>
+
+	<h2 class="bp-screen-title">
+		<?php esc_html_e( 'Manage Membership Requests', 'buddypress' ); ?>
+	</h2>
+
+	<?php bp_nouveau_pagination( 'top' ); ?>
+
+	<ul id="request-list" class="item-list bp-list membership-requests-list">
+		<?php
+		while ( bp_group_membership_requests() ) :
+			bp_group_the_membership_request();
+		?>
+
+			<li>
+				<div class="item-avatar">
+					<?php bp_group_request_user_avatar_thumb(); ?>
+				</div>
+
+				<div class="item">
+
+					<div class="item-title">
+						<h3><?php bp_group_request_user_link(); ?></h3>
+					</div>
+
+					<div class="item-meta">
+						<span class="comments"><?php bp_group_request_comment(); ?></span>
+						<span class="activity"><?php bp_group_request_time_since_requested(); ?></span>
+						<?php bp_nouveau_group_hook( '', 'membership_requests_admin_item' ); ?>
+					</div>
+
+				</div>
+
+				<?php bp_nouveau_groups_request_buttons(); ?>
+			</li>
+
+		<?php endwhile; ?>
+	</ul>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+<?php else : ?>
+
+	<?php bp_nouveau_user_feedback( 'group-requests-none' ); ?>
+
+<?php
+endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/send-invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/send-invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/groups/single/send-invites.php	(revision 12265)
@@ -0,0 +1,9 @@
+<?php
+/**
+ * BuddyPress - Groups Send Invites
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+// Runs do_action & calls common/js-templates/invites/index
+bp_nouveau_group_invites_interface();
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/activate.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/activate.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/activate.php	(revision 12265)
@@ -0,0 +1,55 @@
+<?php
+/**
+ * BuddyPress - Members Activate
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+	<?php bp_nouveau_activation_hook( 'before', 'page' ); ?>
+
+	<div class="page" id="activate-page">
+
+		<?php bp_nouveau_template_notices(); ?>
+
+		<?php bp_nouveau_activation_hook( 'before', 'content' ); ?>
+
+		<?php if ( bp_account_was_activated() ) : ?>
+
+			<?php if ( isset( $_GET['e'] ) ) : ?>
+				<p><?php esc_html_e( 'Your account was activated successfully! Your account details have been sent to you in a separate email.', 'buddypress' ); ?></p>
+			<?php else : ?>
+				<p><?php esc_html_e( 'Your account was activated successfully! You can now log in with the username and password you provided when you signed up.', 'buddypress' ); ?></p>
+			<?php endif; ?>
+
+			<?php
+			printf(
+				'<p><a href="%1$s">%2$s</a></p>',
+				esc_url( wp_login_url( bp_get_root_domain() ) ),
+				esc_html__( 'Log In', 'buddypress' )
+			);
+			?>
+
+		<?php else : ?>
+
+			<p><?php esc_html_e( 'Please provide a valid activation key.', 'buddypress' ); ?></p>
+
+			<form action="" method="post" class="standard-form" id="activation-form">
+
+				<label for="key"><?php esc_html_e( 'Activation Key:', 'buddypress' ); ?></label>
+				<input type="text" name="key" id="key" value="<?php echo esc_attr( bp_get_current_activation_key() ); ?>" />
+
+				<p class="submit">
+					<input type="submit" name="submit" value="<?php echo esc_attr_x( 'Activate', 'button', 'buddypress' ); ?>" />
+				</p>
+
+			</form>
+
+		<?php endif; ?>
+
+		<?php bp_nouveau_activation_hook( 'after', 'content' ); ?>
+
+	</div><!-- .page -->
+
+	<?php bp_nouveau_activation_hook( 'after', 'page' ); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/index.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/index.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/index.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * BuddyPress Members Directory
+ *
+ * @version 3.0.0
+ */
+
+?>
+
+	<?php bp_nouveau_before_members_directory_content(); ?>
+
+	<?php if ( ! bp_nouveau_is_object_nav_in_sidebar() ) : ?>
+
+		<?php bp_get_template_part( 'common/nav/directory-nav' ); ?>
+
+	<?php endif; ?>
+
+	<div class="screen-content">
+
+	<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+		<div id="members-dir-list" class="members dir-list" data-bp-list="members">
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'directory-members-loading' ); ?></div>
+		</div><!-- #members-dir-list -->
+
+		<?php bp_nouveau_after_members_directory_content(); ?>
+	</div><!-- // .screen-content -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/members-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/members-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/members-loop.php	(revision 12265)
@@ -0,0 +1,82 @@
+<?php
+/**
+ * BuddyPress - Members Loop
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_before_loop(); ?>
+
+<?php if ( bp_get_current_member_type() ) : ?>
+	<p class="current-member-type"><?php bp_current_member_type_message(); ?></p>
+<?php endif; ?>
+
+<?php if ( bp_has_members( bp_ajax_querystring( 'members' ) ) ) : ?>
+
+	<?php bp_nouveau_pagination( 'top' ); ?>
+
+	<ul id="members-list" class="<?php bp_nouveau_loop_classes(); ?>">
+
+	<?php while ( bp_members() ) : bp_the_member(); ?>
+
+		<li <?php bp_member_class( array( 'item-entry' ) ); ?> data-bp-item-id="<?php bp_member_user_id(); ?>" data-bp-item-component="members">
+			<div class="list-wrap">
+
+				<div class="item-avatar">
+					<a href="<?php bp_member_permalink(); ?>"><?php bp_member_avatar( bp_nouveau_avatar_args() ); ?></a>
+				</div>
+
+				<div class="item">
+
+					<div class="item-block">
+
+						<h2 class="list-title member-name">
+							<a href="<?php bp_member_permalink(); ?>"><?php bp_member_name(); ?></a>
+						</h2>
+
+						<?php if ( bp_nouveau_member_has_meta() ) : ?>
+							<p class="item-meta last-activity">
+								<?php bp_nouveau_member_meta(); ?>
+							</p><!-- #item-meta -->
+						<?php endif; ?>
+
+						<?php
+						bp_nouveau_members_loop_buttons(
+							array(
+								'container'      => 'ul',
+								'button_element' => 'button',
+							)
+						);
+?>
+
+					</div>
+
+					<?php if ( bp_get_member_latest_update() && ! bp_nouveau_loop_is_grid() ) : ?>
+					<div class="user-update">
+						<p class="update"> <?php bp_member_latest_update(); ?></p>
+					</div>
+						<?php endif; ?>
+
+				</div><!-- // .item -->
+
+
+
+			</div>
+		</li>
+
+	<?php endwhile; ?>
+
+	</ul>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+<?php
+else :
+
+	bp_nouveau_user_feedback( 'members-loop-none' );
+
+endif;
+?>
+
+<?php bp_nouveau_after_loop(); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/register.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/register.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/register.php	(revision 12265)
@@ -0,0 +1,125 @@
+<?php
+/**
+ * BuddyPress - Members/Blogs Registration forms
+ *
+ * @since 3.0.0
+ * @version 4.0.0
+ */
+
+?>
+
+	<?php bp_nouveau_signup_hook( 'before', 'page' ); ?>
+
+	<div id="register-page"class="page register-page">
+
+		<?php bp_nouveau_template_notices(); ?>
+
+			<?php bp_nouveau_user_feedback( bp_get_current_signup_step() ); ?>
+
+			<form action="" name="signup_form" id="signup-form" class="standard-form signup-form clearfix" method="post" enctype="multipart/form-data">
+
+			<div class="layout-wrap">
+
+			<?php if ( 'request-details' === bp_get_current_signup_step() ) : ?>
+
+				<?php bp_nouveau_signup_hook( 'before', 'account_details' ); ?>
+
+				<div class="register-section default-profile" id="basic-details-section">
+
+					<?php /***** Basic Account Details ******/ ?>
+
+					<h2 class="bp-heading"><?php esc_html_e( 'Account Details', 'buddypress' ); ?></h2>
+
+					<?php bp_nouveau_signup_form(); ?>
+
+				</div><!-- #basic-details-section -->
+
+				<?php bp_nouveau_signup_hook( 'after', 'account_details' ); ?>
+
+				<?php /***** Extra Profile Details ******/ ?>
+
+				<?php if ( bp_is_active( 'xprofile' ) && bp_nouveau_base_account_has_xprofile() ) : ?>
+
+					<?php bp_nouveau_signup_hook( 'before', 'signup_profile' ); ?>
+
+					<div class="register-section extended-profile" id="profile-details-section">
+
+						<h2 class="bp-heading"><?php esc_html_e( 'Profile Details', 'buddypress' ); ?></h2>
+
+						<?php /* Use the profile field loop to render input fields for the 'base' profile field group */ ?>
+						<?php while ( bp_profile_groups() ) : bp_the_profile_group(); ?>
+
+							<?php while ( bp_profile_fields() ) : bp_the_profile_field(); ?>
+
+								<div<?php bp_field_css_class( 'editfield' ); ?>>
+									<fieldset>
+
+									<?php
+									$field_type = bp_xprofile_create_field_type( bp_get_the_profile_field_type() );
+									$field_type->edit_field_html();
+
+									bp_nouveau_xprofile_edit_visibilty();
+									?>
+
+									</fieldset>
+								</div>
+
+							<?php endwhile; ?>
+
+						<input type="hidden" name="signup_profile_field_ids" id="signup_profile_field_ids" value="<?php bp_the_profile_field_ids(); ?>" />
+
+						<?php endwhile; ?>
+
+						<?php bp_nouveau_signup_hook( '', 'signup_profile' ); ?>
+
+					</div><!-- #profile-details-section -->
+
+					<?php bp_nouveau_signup_hook( 'after', 'signup_profile' ); ?>
+
+				<?php endif; ?>
+
+				<?php if ( bp_get_blog_signup_allowed() ) : ?>
+
+					<?php bp_nouveau_signup_hook( 'before', 'blog_details' ); ?>
+
+					<?php /***** Blog Creation Details ******/ ?>
+
+					<div class="register-section blog-details" id="blog-details-section">
+
+						<h2><?php esc_html_e( 'Site Details', 'buddypress' ); ?></h2>
+
+						<p><label for="signup_with_blog"><input type="checkbox" name="signup_with_blog" id="signup_with_blog" value="1" <?php checked( (int) bp_get_signup_with_blog_value(), 1 ); ?>/> <?php esc_html_e( "Yes, i'd like to create a new site", 'buddypress' ); ?></label></p>
+
+						<div id="blog-details"<?php if ( (int) bp_get_signup_with_blog_value() ) : ?>class="show"<?php endif; ?>>
+
+							<?php bp_nouveau_signup_form( 'blog_details' ); ?>
+
+						</div>
+
+					</div><!-- #blog-details-section -->
+
+					<?php bp_nouveau_signup_hook( 'after', 'blog_details' ); ?>
+
+				<?php endif; ?>
+
+			<?php endif; // request-details signup step ?>
+
+			</div><!-- //.layout-wrap -->
+
+			<?php bp_nouveau_signup_hook( 'custom', 'steps' ); ?>
+
+			<?php if ( 'request-details' === bp_get_current_signup_step() ) : ?>
+
+				<?php if ( bp_signup_requires_privacy_policy_acceptance() ) : ?>
+					<?php bp_nouveau_signup_privacy_policy_acceptance_section(); ?>
+				<?php endif; ?>
+
+				<?php bp_nouveau_submit_button( 'register' ); ?>
+
+			<?php endif; ?>
+
+			</form>
+
+	</div>
+
+	<?php bp_nouveau_signup_hook( 'after', 'page' ); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/activity.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+/**
+ * BuddyPress - Users Activity
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Activity menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+
+		<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+	</ul>
+</nav><!-- .item-list-tabs#subnav -->
+
+<h2 class="bp-screen-title<?php echo ( bp_displayed_user_has_front_template() ) ? ' bp-screen-reader-text' : ''; ?>">
+	<?php esc_html_e( 'Member Activities', 'buddypress' ); ?>
+</h2>
+
+<?php bp_nouveau_activity_member_post_form(); ?>
+
+<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+<?php bp_nouveau_member_hook( 'before', 'activity_content' ); ?>
+
+<div id="activity-stream" class="activity single-user" data-bp-list="activity">
+
+	<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'member-activity-loading' ); ?></div>
+
+	<ul  class="<?php bp_nouveau_loop_classes(); ?>" >
+
+	</ul>
+
+</div><!-- .activity -->
+
+<?php
+bp_nouveau_member_hook( 'after', 'activity_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/blogs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/blogs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/blogs.php	(revision 12265)
@@ -0,0 +1,42 @@
+<?php
+/**
+ * BuddyPress - Users Blogs
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Sites menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+
+		<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+	</ul>
+</nav><!-- .bp-navs -->
+
+<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+<?php
+switch ( bp_current_action() ) :
+
+	// Home/My Blogs
+	case 'my-sites':
+		bp_nouveau_member_hook( 'before', 'blogs_content' );
+		?>
+
+		<div class="blogs myblogs" data-bp-list="blogs">
+
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'member-blogs-loading' ); ?></div>
+
+		</div><!-- .blogs.myblogs -->
+
+		<?php
+		bp_nouveau_member_hook( 'after', 'blogs_content' );
+		break;
+
+	// Any other
+	default:
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/cover-image-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/cover-image-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/cover-image-header.php	(revision 12265)
@@ -0,0 +1,51 @@
+<?php
+/**
+ * BuddyPress - Users Cover Image Header
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<div id="cover-image-container">
+	<div id="header-cover-image"></div>
+
+	<div id="item-header-cover-image">
+		<div id="item-header-avatar">
+			<a href="<?php bp_displayed_user_link(); ?>">
+
+				<?php bp_displayed_user_avatar( 'type=full' ); ?>
+
+			</a>
+		</div><!-- #item-header-avatar -->
+
+		<div id="item-header-content">
+
+			<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+				<h2 class="user-nicename">@<?php bp_displayed_user_mentionname(); ?></h2>
+			<?php endif; ?>
+
+			<?php
+			bp_nouveau_member_header_buttons(
+				array(
+					'container'         => 'ul',
+					'button_element'    => 'button',
+					'container_classes' => array( 'member-header-actions' ),
+				)
+			);
+?>
+
+			<?php bp_nouveau_member_hook( 'before', 'header_meta' ); ?>
+
+			<?php if ( bp_nouveau_member_has_meta() ) : ?>
+				<div class="item-meta">
+
+					<?php bp_nouveau_member_meta(); ?>
+
+				</div><!-- #item-meta -->
+			<?php endif; ?>
+
+		</div><!-- #item-header-content -->
+
+	</div><!-- #item-header-cover-image -->
+</div><!-- #cover-image-container -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/default-front.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/default-front.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/default-front.php	(revision 12265)
@@ -0,0 +1,58 @@
+<?php
+/**
+ * BP Nouveau Default user's front template.
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<div class="member-front-page">
+
+	<?php if ( ! is_customize_preview() && bp_current_user_can( 'bp_moderate' ) && ! is_active_sidebar( 'sidebar-buddypress-members' ) ) : ?>
+
+		<div class="bp-feedback custom-homepage-info info">
+			<strong><?php esc_html_e( 'Manage the members default front page', 'buddypress' ); ?></strong>
+			<button type="button" class="bp-tooltip" data-bp-tooltip="<?php echo esc_attr_x( 'Close', 'button', 'buddypress' ); ?>" aria-label="<?php esc_attr_e( 'Close this notice', 'buddypress' ); ?>" data-bp-close="remove"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span></button><br/>
+			<?php
+			printf(
+				esc_html__( 'You can set the preferences of the %1$s or add %2$s to it.', 'buddypress' ),
+				bp_nouveau_members_get_customizer_option_link(),
+				bp_nouveau_members_get_customizer_widgets_link()
+			);
+			?>
+		</div>
+
+	<?php endif; ?>
+
+	<?php if ( bp_nouveau_members_wp_bio_info() ) : ?>
+
+		<div class="member-description">
+
+			<?php if ( get_the_author_meta( 'description', bp_displayed_user_id() ) ) : ?>
+				<blockquote class="member-bio">
+					<?php bp_nouveau_member_description( bp_displayed_user_id() ); ?>
+				</blockquote><!-- .member-bio -->
+			<?php endif; ?>
+
+			<?php
+			if ( bp_is_my_profile() ) :
+
+				bp_nouveau_member_description_edit_link();
+
+			endif;
+			?>
+
+		</div><!-- .member-description -->
+
+	<?php endif; ?>
+
+	<?php if ( is_active_sidebar( 'sidebar-buddypress-members' ) ) : ?>
+
+		<div id="member-front-widgets" class="bp-sidebar bp-widget-area" role="complementary">
+			<?php dynamic_sidebar( 'sidebar-buddypress-members' ); ?>
+		</div><!-- .bp-sidebar.bp-widget-area -->
+
+	<?php endif; ?>
+
+</div>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/friends.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/friends.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/friends.php	(revision 12265)
@@ -0,0 +1,48 @@
+<?php
+/**
+ * BuddyPress - Users Friends
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Friends menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+		<?php if ( bp_is_my_profile() ) : ?>
+
+			<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+		<?php endif; ?>
+	</ul>
+</nav><!-- .bp-navs -->
+
+<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+<?php
+switch ( bp_current_action() ) :
+
+	// Home/My Friends
+	case 'my-friends':
+		bp_nouveau_member_hook( 'before', 'friends_content' );
+		?>
+
+		<div class="members friends" data-bp-list="members">
+
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'member-friends-loading' ); ?></div>
+
+		</div><!-- .members.friends -->
+
+		<?php
+		bp_nouveau_member_hook( 'after', 'friends_content' );
+		break;
+
+	case 'requests':
+		bp_get_template_part( 'members/single/friends/requests' );
+		break;
+
+	// Any other
+	default:
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/friends/requests.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/friends/requests.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/friends/requests.php	(revision 12265)
@@ -0,0 +1,53 @@
+<?php
+/**
+ * BuddyPress - Members Friends Requests
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<h2 class="screen-heading friendship-requests-screen"><?php esc_html_e( 'Friendship Requests', 'buddypress' ); ?></h2>
+
+<?php bp_nouveau_member_hook( 'before', 'friend_requests_content' ); ?>
+
+<?php if ( bp_has_members( 'type=alphabetical&include=' . bp_get_friendship_requests() ) ) : ?>
+
+	<?php bp_nouveau_pagination( 'top' ); ?>
+
+	<ul id="friend-list" class="<?php bp_nouveau_loop_classes(); ?>" data-bp-list="friendship_requests">
+		<?php
+		while ( bp_members() ) :
+			bp_the_member();
+		?>
+
+			<li id="friendship-<?php bp_friend_friendship_id(); ?>" <?php bp_member_class( array( 'item-entry' ) ); ?> data-bp-item-id="<?php bp_friend_friendship_id(); ?>" data-bp-item-component="members">
+				<div class="item-avatar">
+					<a href="<?php bp_member_link(); ?>"><?php bp_member_avatar( array( 'type' => 'full' ) ); ?></a>
+				</div>
+
+				<div class="item">
+					<div class="item-title"><a href="<?php bp_member_link(); ?>"><?php bp_member_name(); ?></a></div>
+					<div class="item-meta"><span class="activity"><?php bp_member_last_active(); ?></span></div>
+
+					<?php bp_nouveau_friend_hook( 'requests_item' ); ?>
+				</div>
+
+				<?php bp_nouveau_members_loop_buttons(); ?>
+			</li>
+
+		<?php endwhile; ?>
+	</ul>
+
+	<?php bp_nouveau_friend_hook( 'requests_content' ); ?>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+<?php else : ?>
+
+	<?php bp_nouveau_user_feedback( 'member-requests-none' ); ?>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_member_hook( 'after', 'friend_requests_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/groups.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/groups.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/groups.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+/**
+ * BuddyPress - Users Groups
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Groups menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+
+		<?php if ( bp_is_my_profile() ) : ?>
+
+			<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+		<?php endif; ?>
+
+	</ul>
+</nav><!-- .bp-navs -->
+
+<?php if ( ! bp_is_current_action( 'invites' ) ) : ?>
+
+
+	<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+<?php endif; ?>
+
+<?php
+
+switch ( bp_current_action() ) :
+
+	// Home/My Groups
+	case 'my-groups':
+		bp_nouveau_member_hook( 'before', 'groups_content' );
+		?>
+
+		<div class="groups mygroups" data-bp-list="groups">
+
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'member-groups-loading' ); ?></div>
+
+		</div>
+
+		<?php
+		bp_nouveau_member_hook( 'after', 'groups_content' );
+		break;
+
+	// Group Invitations
+	case 'invites':
+		bp_get_template_part( 'members/single/groups/invites' );
+		break;
+
+	// Any other
+	default:
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/groups/invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/groups/invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/groups/invites.php	(revision 12265)
@@ -0,0 +1,81 @@
+<?php
+/**
+ * BuddyPress - Members Single Group Invites
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<h2 class="screen-heading group-invites-screen"><?php esc_html_e( 'Group Invites', 'buddypress' ); ?></h2>
+
+<?php bp_nouveau_group_hook( 'before', 'invites_content' ); ?>
+
+<?php if ( bp_has_groups( 'type=invites&user_id=' . bp_loggedin_user_id() ) ) : ?>
+
+	<ul id="group-list" class="invites item-list bp-list" data-bp-list="groups_invites">
+
+		<?php
+		while ( bp_groups() ) :
+			bp_the_group();
+		?>
+
+			<li class="item-entry invites-list" data-bp-item-id="<?php bp_group_id(); ?>" data-bp-item-component="groups">
+
+				<div class="wrap">
+
+				<?php if ( ! bp_disable_group_avatar_uploads() ) : ?>
+					<div class="item-avatar">
+						<a href="<?php bp_group_permalink(); ?>"><?php bp_group_avatar(); ?></a>
+					</div>
+				<?php endif; ?>
+
+					<div class="item">
+						<h2 class="list-title groups-title"><?php bp_group_link(); ?></h2>
+						<p class="meta group-details">
+							<span class="small">
+							<?php
+							printf(
+								/* translators: %s = number of members */
+								_n(
+									'%s member',
+									'%s members',
+									bp_get_group_total_members( false ),
+									'buddypress'
+								),
+								number_format_i18n( bp_get_group_total_members( false ) )
+							);
+							?>
+							</span>
+						</p>
+
+						<p class="desc">
+							<?php bp_group_description_excerpt(); ?>
+						</p>
+
+						<?php bp_nouveau_group_hook( '', 'invites_item' ); ?>
+
+						<?php
+						bp_nouveau_groups_invite_buttons(
+							array(
+								'container'      => 'ul',
+								'button_element' => 'button',
+							)
+						);
+						?>
+					</div>
+
+				</div>
+			</li>
+
+		<?php endwhile; ?>
+	</ul>
+
+<?php else : ?>
+
+	<?php bp_nouveau_user_feedback( 'member-invites-none' ); ?>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_group_hook( 'after', 'invites_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/home.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/home.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/home.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * BuddyPress - Members Home
+ *
+ * @since   1.0.0
+ * @version 3.0.0
+ */
+?>
+
+	<?php bp_nouveau_member_hook( 'before', 'home_content' ); ?>
+
+	<div id="item-header" role="complementary" data-bp-item-id="<?php echo esc_attr( bp_displayed_user_id() ); ?>" data-bp-item-component="members" class="users-header single-headers">
+
+		<?php bp_nouveau_member_header_template_part(); ?>
+
+	</div><!-- #item-header -->
+
+	<div class="bp-wrap">
+		<?php if ( ! bp_nouveau_is_object_nav_in_sidebar() ) : ?>
+
+			<?php bp_get_template_part( 'members/single/parts/item-nav' ); ?>
+
+		<?php endif; ?>
+
+		<div id="item-body" class="item-body">
+
+			<?php bp_nouveau_member_template_part(); ?>
+
+		</div><!-- #item-body -->
+	</div><!-- // .bp-wrap -->
+
+	<?php bp_nouveau_member_hook( 'after', 'home_content' ); ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/member-header.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/member-header.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/member-header.php	(revision 12265)
@@ -0,0 +1,35 @@
+<?php
+/**
+ * BuddyPress - Users Header
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<div id="item-header-avatar">
+	<a href="<?php bp_displayed_user_link(); ?>">
+
+		<?php bp_displayed_user_avatar( 'type=full' ); ?>
+
+	</a>
+</div><!-- #item-header-avatar -->
+
+<div id="item-header-content">
+
+	<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+		<h2 class="user-nicename">@<?php bp_displayed_user_mentionname(); ?></h2>
+	<?php endif; ?>
+
+	<?php bp_nouveau_member_hook( 'before', 'header_meta' ); ?>
+
+	<?php if ( bp_nouveau_member_has_meta() ) : ?>
+		<div class="item-meta">
+
+			<?php bp_nouveau_member_meta(); ?>
+
+		</div><!-- #item-meta -->
+	<?php endif; ?>
+
+	<?php bp_nouveau_member_header_buttons( array( 'container_classes' => array( 'member-header-actions' ) ) ); ?>
+</div><!-- #item-header-content -->
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/messages.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/messages.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/messages.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * BuddyPress - Users Messages
+ *
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Messages menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+
+		<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+	</ul>
+</nav><!-- .bp-navs -->
+
+<?php
+if ( ! in_array( bp_current_action(), array( 'inbox', 'sentbox', 'starred', 'view', 'compose', 'notices' ), true ) ) :
+
+	bp_get_template_part( 'members/single/plugins' );
+
+else :
+
+	bp_nouveau_messages_member_interface();
+
+endif;
+?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/notifications.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+/**
+ * BuddyPress - Users Notifications
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Notifications menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+
+		<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+	</ul>
+</nav>
+
+<?php
+switch ( bp_current_action() ) :
+
+	case 'unread':
+	case 'read':
+	?>
+
+
+	<?php bp_get_template_part( 'common/search-and-filters-bar' ); ?>
+
+
+		<div id="notifications-user-list" class="notifications dir-list" data-bp-list="notifications">
+			<div id="bp-ajax-loader"><?php bp_nouveau_user_feedback( 'member-notifications-loading' ); ?></div>
+		</div><!-- #groups-dir-list -->
+
+		<?php
+		break;
+
+	// Any other actions.
+	default:
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/notifications/notifications-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/notifications/notifications-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/notifications/notifications-loop.php	(revision 12265)
@@ -0,0 +1,61 @@
+<?php
+/**
+ * BuddyPress - Members Notifications Loop
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+if ( bp_has_notifications( bp_ajax_querystring( 'notifications' ) ) ) :
+
+	bp_nouveau_pagination( 'top' ); ?>
+
+	<form action="" method="post" id="notifications-bulk-management" class="standard-form">
+		<table class="notifications bp-tables-user">
+			<thead>
+				<tr>
+					<th class="icon"></th>
+					<th class="bulk-select-all"><input id="select-all-notifications" type="checkbox"><label class="bp-screen-reader-text" for="select-all-notifications"><?php esc_html_e( 'Select all', 'buddypress' ); ?></label></th>
+					<th class="title"><?php esc_html_e( 'Notification', 'buddypress' ); ?></th>
+					<th class="date">
+						<?php esc_html_e( 'Date Received', 'buddypress' ); ?>
+						<?php bp_nouveau_notifications_sort_order_links(); ?>
+					</th>
+					<th class="actions"><?php esc_html_e( 'Actions', 'buddypress' ); ?></th>
+				</tr>
+			</thead>
+
+			<tbody>
+
+				<?php
+				while ( bp_the_notifications() ) :
+					bp_the_notification();
+				?>
+
+					<tr>
+						<td></td>
+						<td class="bulk-select-check"><label for="<?php bp_the_notification_id(); ?>"><input id="<?php bp_the_notification_id(); ?>" type="checkbox" name="notifications[]" value="<?php bp_the_notification_id(); ?>" class="notification-check"><span class="bp-screen-reader-text"><?php esc_html_e( 'Select this notification', 'buddypress' ); ?></span></label></td>
+						<td class="notification-description"><?php bp_the_notification_description(); ?></td>
+						<td class="notification-since"><?php bp_the_notification_time_since(); ?></td>
+						<td class="notification-actions"><?php bp_the_notification_action_links(); ?></td>
+					</tr>
+
+				<?php endwhile; ?>
+
+			</tbody>
+		</table>
+
+		<div class="notifications-options-nav">
+			<?php bp_nouveau_notifications_bulk_management_dropdown(); ?>
+		</div><!-- .notifications-options-nav -->
+
+		<?php wp_nonce_field( 'notifications_bulk_nonce', 'notifications_bulk_nonce' ); ?>
+	</form>
+
+	<?php bp_nouveau_pagination( 'bottom' ); ?>
+
+<?php else : ?>
+
+	<?php bp_nouveau_user_feedback( 'member-notifications-none' ); ?>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/item-nav.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/item-nav.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/item-nav.php	(revision 12265)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * BuddyPress Single Members item Navigation
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_nav_classes(); ?>" id="object-nav" role="navigation" aria-label="<?php esc_attr_e( 'Member menu', 'buddypress' ); ?>">
+
+	<?php if ( bp_nouveau_has_nav( array( 'type' => 'primary' ) ) ) : ?>
+
+		<ul>
+
+			<?php
+			while ( bp_nouveau_nav_items() ) :
+				bp_nouveau_nav_item();
+			?>
+
+				<li id="<?php bp_nouveau_nav_id(); ?>" class="<?php bp_nouveau_nav_classes(); ?>">
+					<a href="<?php bp_nouveau_nav_link(); ?>" id="<?php bp_nouveau_nav_link_id(); ?>">
+						<?php bp_nouveau_nav_link_text(); ?>
+
+						<?php if ( bp_nouveau_nav_has_count() ) : ?>
+							<span class="count"><?php bp_nouveau_nav_count(); ?></span>
+						<?php endif; ?>
+					</a>
+				</li>
+
+			<?php endwhile; ?>
+
+			<?php bp_nouveau_member_hook( '', 'options_nav' ); ?>
+
+		</ul>
+
+	<?php endif; ?>
+
+</nav>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/item-subnav.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/item-subnav.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/item-subnav.php	(revision 12265)
@@ -0,0 +1,29 @@
+<?php
+/**
+ * BuddyPress Single Members item Sub Navigation
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<?php if ( bp_nouveau_has_nav( array( 'type' => 'secondary' ) ) ) : ?>
+
+	<?php
+	while ( bp_nouveau_nav_items() ) :
+		bp_nouveau_nav_item();
+	?>
+
+		<li id="<?php bp_nouveau_nav_id(); ?>" class="<?php bp_nouveau_nav_classes(); ?>" <?php bp_nouveau_nav_scope(); ?>>
+			<a href="<?php bp_nouveau_nav_link(); ?>" id="<?php bp_nouveau_nav_link_id(); ?>">
+				<?php bp_nouveau_nav_link_text(); ?>
+
+				<?php if ( bp_nouveau_nav_has_count() ) : ?>
+					<span class="count"><?php bp_nouveau_nav_count(); ?></span>
+				<?php endif; ?>
+			</a>
+		</li>
+
+	<?php endwhile; ?>
+
+<?php endif; ?>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/profile-visibility.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/profile-visibility.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/parts/profile-visibility.php	(revision 12265)
@@ -0,0 +1,50 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile Edit Field visibility
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+if ( empty( $GLOBALS['profile_template'] ) ) {
+	return;
+}
+?>
+
+<?php if ( bp_current_user_can( 'bp_xprofile_change_field_visibility' ) ) : ?>
+
+	<p class="field-visibility-settings-toggle field-visibility-settings-header" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id(); ?>">
+
+		<?php
+		printf(
+			/* translators: field visibility level, e.g. "...seen by: everyone". */
+			__( 'This field may be seen by: %s', 'buddypress' ),
+			'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+		);
+		?>
+		<button class="visibility-toggle-link text-button" type="button"><?php echo esc_html_x( 'Change', 'button', 'buddypress' ); ?></button>
+
+	</p>
+
+	<div class="field-visibility-settings" id="field-visibility-settings-<?php bp_the_profile_field_id(); ?>">
+		<fieldset>
+			<legend><?php esc_html_e( 'Who is allowed to see this field?', 'buddypress' ); ?></legend>
+
+			<?php bp_profile_visibility_radio_buttons(); ?>
+
+		</fieldset>
+		<button class="field-visibility-settings-close button" type="button"><?php echo esc_html_x( 'Close', 'button', 'buddypress' ); ?></button>
+	</div>
+
+<?php else : ?>
+
+	<p class="field-visibility-settings-notoggle field-visibility-settings-header" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id(); ?>">
+		<?php
+		printf(
+			esc_html__( 'This field may be seen by: %s', 'buddypress' ),
+			'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+		);
+		?>
+	</p>
+
+<?php endif;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/plugins.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/plugins.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/plugins.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress - Users Plugins Template
+ *
+ * 3rd-party plugins should use this template to easily add template
+ * support to their plugins for the members component.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_member_hook( 'before', 'plugin_template' ); ?>
+
+<?php if ( ! bp_is_current_component_core() ) : ?>
+
+	<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav">
+		<ul class="subnav">
+
+			<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+			<?php bp_nouveau_member_hook( '', 'plugin_options_nav' ); ?>
+
+		</ul>
+	</nav>
+
+<?php endif; ?>
+
+<?php if ( has_action( 'bp_template_title' ) ) : ?>
+
+	<h2><?php bp_nouveau_plugin_hook( 'title' ); ?></h2>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_plugin_hook( 'content' );
+
+bp_nouveau_member_hook( 'after', 'plugin_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile.php	(revision 12265)
@@ -0,0 +1,62 @@
+<?php
+/**
+ * BuddyPress - Users Profile
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Profile menu', 'buddypress' ); ?>">
+	<ul class="subnav">
+
+		<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+	</ul>
+</nav><!-- .item-list-tabs -->
+
+<?php bp_nouveau_member_hook( 'before', 'profile_content' ); ?>
+
+<div class="profile <?php echo bp_current_action(); ?>">
+
+<?php
+switch ( bp_current_action() ) :
+
+	// Edit
+	case 'edit':
+		bp_get_template_part( 'members/single/profile/edit' );
+		break;
+
+	// Change Avatar
+	case 'change-avatar':
+		bp_get_template_part( 'members/single/profile/change-avatar' );
+		break;
+
+	// Change Cover Image
+	case 'change-cover-image':
+		bp_get_template_part( 'members/single/profile/change-cover-image' );
+		break;
+
+	// Compose
+	case 'public':
+		// Display XProfile
+		if ( bp_is_active( 'xprofile' ) ) {
+			bp_get_template_part( 'members/single/profile/profile-loop' );
+
+		// Display WordPress profile (fallback)
+		} else {
+			bp_get_template_part( 'members/single/profile/profile-wp' );
+		}
+
+		break;
+
+	// Any other
+	default:
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
+?>
+</div><!-- .profile -->
+
+<?php
+bp_nouveau_member_hook( 'after', 'profile_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/change-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/change-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/change-avatar.php	(revision 12265)
@@ -0,0 +1,96 @@
+<?php
+/**
+ * BuddyPress - Members Profile Change Avatar
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+?>
+
+<h2 class="screen-heading change-avatar-screen"><?php esc_html_e( 'Change Profile Photo', 'buddypress' ); ?></h2>
+
+<?php bp_nouveau_member_hook( 'before', 'avatar_upload_content' ); ?>
+
+<?php if ( ! (int) bp_get_option( 'bp-disable-avatar-uploads' ) ) : ?>
+
+	<p class="bp-feedback info">
+		<span class="bp-icon" aria-hidden="true"></span>
+		<span class="bp-help-text">
+			<?php
+			/* Translators: %s is used to output the link to the Gravatar site */
+			printf( esc_html__( 'Your profile photo will be used on your profile and throughout the site. If there is a %s associated with your account email we will use that, or you can upload an image from your computer.', 'buddypress' ),
+				/* Translators: Url to the Gravatar site, you can use the one for your country eg: https://fr.gravatar.com for French translation */
+				'<a href="' . esc_url( __( 'https://gravatar.com', 'buddypress' ) ) . '">Gravatar</a>'
+			); ?>
+		</span>
+	</p>
+
+	<form action="" method="post" id="avatar-upload-form" class="standard-form" enctype="multipart/form-data">
+
+		<?php if ( 'upload-image' === bp_get_avatar_admin_step() ) : ?>
+
+			<?php wp_nonce_field( 'bp_avatar_upload' ); ?>
+			<p class="bp-help-text"><?php esc_html_e( "Click below to select a JPG, GIF or PNG format photo from your computer and then click 'Upload Image' to proceed.", 'buddypress' ); ?></p>
+
+			<p id="avatar-upload">
+				<label for="file" class="bp-screen-reader-text"><?php esc_html_e( 'Select an image', 'buddypress' ); ?></label>
+				<input type="file" name="file" id="file" />
+				<input type="submit" name="upload" id="upload" value="<?php esc_attr_e( 'Upload Image', 'buddypress' ); ?>" />
+				<input type="hidden" name="action" id="action" value="bp_avatar_upload" />
+			</p>
+
+			<?php if ( bp_get_user_has_avatar() ) : ?>
+				<p class="bp-help-text"><?php esc_html_e( "If you'd like to delete your current profile photo, use the delete profile photo button.", 'buddypress' ); ?></p>
+				<p><a class="button edit" href="<?php bp_avatar_delete_link(); ?>"><?php esc_html_e( 'Delete My Profile Photo', 'buddypress' ); ?></a></p>
+			<?php endif; ?>
+
+		<?php endif; ?>
+
+		<?php if ( 'crop-image' === bp_get_avatar_admin_step() ) : ?>
+
+			<p class="bp-help-text screen-header"><?php esc_html_e( 'Crop Your New Profile Photo', 'buddypress' ); ?></p>
+
+			<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-to-crop" class="avatar" alt="<?php esc_attr_e( 'Profile photo to crop', 'buddypress' ); ?>" />
+
+			<div id="avatar-crop-pane">
+				<img src="<?php bp_avatar_to_crop(); ?>" id="avatar-crop-preview" class="avatar" alt="<?php esc_attr_e( 'Profile photo preview', 'buddypress' ); ?>" />
+			</div>
+
+			<input type="submit" name="avatar-crop-submit" id="avatar-crop-submit" value="<?php esc_attr_e( 'Crop Image', 'buddypress' ); ?>" />
+
+			<input type="hidden" name="image_src" id="image_src" value="<?php bp_avatar_to_crop_src(); ?>" />
+			<input type="hidden" id="x" name="x" />
+			<input type="hidden" id="y" name="y" />
+			<input type="hidden" id="w" name="w" />
+			<input type="hidden" id="h" name="h" />
+
+			<?php wp_nonce_field( 'bp_avatar_cropstore' ); ?>
+
+		<?php endif; ?>
+
+	</form>
+
+	<?php
+	/**
+	 * Load the Avatar UI templates
+	 *
+	 * @since 2.3.0
+	 */
+	bp_avatar_get_templates();
+	?>
+
+<?php else : ?>
+
+	<p class="bp-help-text">
+		<?php
+		/* Translators: %s is used to output the link to the Gravatar site */
+		printf( esc_html__( 'Your profile photo will be used on your profile and throughout the site. To change your profile photo, create an account with %s using the same email address as you used to register with this site.', 'buddypress' ),
+			/* Translators: Url to the Gravatar site, you can use the one for your country eg: https://fr.gravatar.com for French translation */
+			'<a href="' . esc_url( __( 'https://gravatar.com', 'buddypress' ) ) . '">Gravatar</a>'
+		); ?>
+	</p>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_member_hook( 'after', 'avatar_upload_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/change-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/change-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/change-cover-image.php	(revision 12265)
@@ -0,0 +1,24 @@
+<?php
+/**
+ * BuddyPress - Members Profile Change Cover Image
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+?>
+
+<h2 class="screen-heading change-cover-image-screen"><?php esc_html_e( 'Change Cover Image', 'buddypress' ); ?></h2>
+
+<?php bp_nouveau_member_hook( 'before', 'edit_cover_image' ); ?>
+
+<p class="info bp-feedback">
+	<span class="bp-icon" aria-hidden="true"></span>
+	<span class="bp-help-text"><?php esc_html_e( 'Your Cover Image will be used to customize the header of your profile.', 'buddypress' ); ?></span>
+</p>
+
+<?php
+// Load the cover image UI
+bp_attachments_get_template_part( 'cover-images/index' );
+
+bp_nouveau_member_hook( 'after', 'edit_cover_image' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/edit.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/edit.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/edit.php	(revision 12265)
@@ -0,0 +1,73 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile Edit
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_xprofile_hook( 'before', 'edit_content' ); ?>
+
+<h2 class="screen-heading edit-profile-screen"><?php esc_html_e( 'Edit Profile', 'buddypress' ); ?></h2>
+
+<?php if ( bp_has_profile( 'profile_group_id=' . bp_get_current_profile_group_id() ) ) :
+	while ( bp_profile_groups() ) :
+		bp_the_profile_group();
+	?>
+
+		<form action="<?php bp_the_profile_group_edit_form_action(); ?>" method="post" id="profile-edit-form" class="standard-form profile-edit <?php bp_the_profile_group_slug(); ?>">
+
+			<?php bp_nouveau_xprofile_hook( 'before', 'field_content' ); ?>
+
+				<?php if ( bp_profile_has_multiple_groups() ) : ?>
+					<ul class="button-tabs button-nav">
+
+						<?php bp_profile_group_tabs(); ?>
+
+					</ul>
+				<?php endif; ?>
+
+				<h3 class="screen-heading profile-group-title edit">
+					<?php
+					printf(
+						/* translators: %s = profile field group name */
+						__( 'Editing "%s" Profile Group', 'buddypress' ),
+						bp_get_the_profile_group_name()
+					)
+					?>
+				</h3>
+
+				<?php
+				while ( bp_profile_fields() ) :
+					bp_the_profile_field();
+				?>
+
+					<div<?php bp_field_css_class( 'editfield' ); ?>>
+						<fieldset>
+
+						<?php
+						$field_type = bp_xprofile_create_field_type( bp_get_the_profile_field_type() );
+						$field_type->edit_field_html();
+						?>
+
+						<?php bp_nouveau_xprofile_edit_visibilty(); ?>
+
+						</fieldset>
+					</div>
+
+				<?php endwhile; ?>
+
+			<?php bp_nouveau_xprofile_hook( 'after', 'field_content' ); ?>
+
+			<input type="hidden" name="field_ids" id="field_ids" value="<?php bp_the_profile_field_ids(); ?>" />
+
+			<?php bp_nouveau_submit_button( 'member-profile-edit' ); ?>
+
+		</form>
+
+	<?php endwhile; ?>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_xprofile_hook( 'after', 'edit_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/profile-loop.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/profile-loop.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/profile-loop.php	(revision 12265)
@@ -0,0 +1,69 @@
+<?php
+/**
+ * BuddyPress - Members Profile Loop
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+?>
+
+<h2 class="screen-heading view-profile-screen"><?php esc_html_e( 'View Profile', 'buddypress' ); ?></h2>
+
+<?php bp_nouveau_xprofile_hook( 'before', 'loop_content' ); ?>
+
+<?php if ( bp_has_profile() ) : ?>
+
+	<?php
+	while ( bp_profile_groups() ) :
+		bp_the_profile_group();
+	?>
+
+		<?php if ( bp_profile_group_has_fields() ) : ?>
+
+			<?php bp_nouveau_xprofile_hook( 'before', 'field_content' ); ?>
+
+			<div class="bp-widget <?php bp_the_profile_group_slug(); ?>">
+
+				<h3 class="screen-heading profile-group-title">
+					<?php bp_the_profile_group_name(); ?>
+				</h3>
+
+				<table class="profile-fields bp-tables-user">
+
+					<?php
+					while ( bp_profile_fields() ) :
+						bp_the_profile_field();
+					?>
+
+						<?php if ( bp_field_has_data() ) : ?>
+
+							<tr<?php bp_field_css_class(); ?>>
+
+								<td class="label"><?php bp_the_profile_field_name(); ?></td>
+
+								<td class="data"><?php bp_the_profile_field_value(); ?></td>
+
+							</tr>
+
+						<?php endif; ?>
+
+						<?php bp_nouveau_xprofile_hook( '', 'field_item' ); ?>
+
+					<?php endwhile; ?>
+
+				</table>
+			</div>
+
+			<?php bp_nouveau_xprofile_hook( 'after', 'field_content' ); ?>
+
+		<?php endif; ?>
+
+	<?php endwhile; ?>
+
+	<?php bp_nouveau_xprofile_hook( '', 'field_buttons' ); ?>
+
+<?php endif; ?>
+
+<?php
+bp_nouveau_xprofile_hook( 'after', 'loop_content' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/profile-wp.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/profile-wp.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/profile/profile-wp.php	(revision 12265)
@@ -0,0 +1,55 @@
+<?php
+/**
+ * BuddyPress - Members Single Profile WP
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_wp_profile_hooks( 'before' ); ?>
+
+<div class="bp-widget wp-profile">
+
+	<h2 class="screen-heading wp-profile-screen">
+		<?php
+		if ( bp_is_my_profile() ) {
+			esc_html_e( 'My Profile', 'buddypress' );
+		} else {
+			printf(
+				/* Translators: a member's profile, e.g. "Paul's profile". */
+				__( "%s's Profile", 'buddypress' ),
+				bp_get_displayed_user_fullname()
+			);
+		}
+		?>
+	</h2>
+
+	<?php if ( bp_nouveau_has_wp_profile_fields() ) : ?>
+
+		<table class="wp-profile-fields">
+
+			<?php
+			while ( bp_nouveau_wp_profile_fields() ) :
+				bp_nouveau_wp_profile_field();
+			?>
+
+				<tr id="<?php bp_nouveau_wp_profile_field_id(); ?>">
+					<td class="label"><?php bp_nouveau_wp_profile_field_label(); ?></td>
+					<td class="data"><?php bp_nouveau_wp_profile_field_data(); ?></td>
+				</tr>
+
+			<?php endwhile; ?>
+
+		</table>
+
+	<?php else : ?>
+
+		<?php bp_nouveau_user_feedback( 'member-wp-profile-none' ); ?>
+
+	<?php endif; ?>
+
+</div>
+
+<?php
+bp_nouveau_wp_profile_hooks( 'after' );
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings.php	(revision 12265)
@@ -0,0 +1,45 @@
+<?php
+/**
+ * BuddyPress - Users Settings
+ *
+ * @version 3.0.0
+ */
+
+?>
+
+<?php if ( bp_core_can_edit_settings() ) : ?>
+
+	<nav class="<?php bp_nouveau_single_item_subnav_classes(); ?>" id="subnav" role="navigation" aria-label="<?php esc_attr_e( 'Settings menu', 'buddypress' ); ?>">
+		<ul class="subnav">
+
+			<?php bp_get_template_part( 'members/single/parts/item-subnav' ); ?>
+
+		</ul>
+	</nav>
+
+<?php
+endif;
+
+switch ( bp_current_action() ) :
+	case 'notifications':
+		bp_get_template_part( 'members/single/settings/notifications' );
+		break;
+	case 'capabilities':
+		bp_get_template_part( 'members/single/settings/capabilities' );
+		break;
+	case 'delete-account':
+		bp_get_template_part( 'members/single/settings/delete-account' );
+		break;
+	case 'general':
+		bp_get_template_part( 'members/single/settings/general' );
+		break;
+	case 'profile':
+		bp_get_template_part( 'members/single/settings/profile' );
+		break;
+	case 'invites':
+		bp_get_template_part( 'members/single/settings/group-invites' );
+		break;
+	default:
+		bp_get_template_part( 'members/single/plugins' );
+		break;
+endswitch;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/capabilities.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/capabilities.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/capabilities.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * BuddyPress - Members Settings ( Capabilities )
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_member_hook( 'before', 'settings_template' ); ?>
+
+<h2 class="screen-heading member-capabilities-screen">
+	<?php esc_html_e( 'Members Capabilities', 'buddypress' ); ?>
+</h2>
+
+<form action="<?php echo esc_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/capabilities/' ); ?>" name="account-capabilities-form" id="account-capabilities-form" class="standard-form" method="post">
+
+	<label for="user-spammer">
+		<input type="checkbox" name="user-spammer" id="user-spammer" value="1" <?php checked( bp_is_user_spammer( bp_displayed_user_id() ) ); ?> />
+			<?php esc_html_e( 'This member is a spammer.', 'buddypress' ); ?>
+	</label>
+
+	<?php bp_nouveau_submit_button( 'member-capabilities' ); ?>
+
+</form>
+
+<?php
+bp_nouveau_member_hook( 'after', 'settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/delete-account.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/delete-account.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/delete-account.php	(revision 12265)
@@ -0,0 +1,29 @@
+<?php
+/**
+ * BuddyPress - Members Settings ( Delete Account )
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_member_hook( 'before', 'settings_template' ); ?>
+
+<h2 class="screen-heading delete-account-screen warn">
+	<?php esc_html_e( 'Delete Account', 'buddypress' ); ?>
+</h2>
+
+<?php bp_nouveau_user_feedback( 'member-delete-account' ); ?>
+
+<form action="<?php echo esc_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/delete-account' ); ?>" name="account-delete-form" id="#account-delete-form" class="standard-form" method="post">
+
+	<label class="warn" for="delete-account-understand">
+		<input id="delete-account-understand" class="disabled" type="checkbox" name="delete-account-understand" value="1" data-bp-disable-input="#delete-account-button" />
+		<?php esc_html_e( 'I understand the consequences.', 'buddypress' ); ?>
+	</label>
+
+	<?php bp_nouveau_submit_button( 'member-delete-account' ); ?>
+
+</form>
+
+<?php
+bp_nouveau_member_hook( 'after', 'settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/general.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/general.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/general.php	(revision 12265)
@@ -0,0 +1,49 @@
+<?php
+/**
+ * BuddyPress - Members Settings ( General )
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_member_hook( 'before', 'settings_template' ); ?>
+
+<h2 class="screen-heading general-settings-screen">
+	<?php _e( 'Email & Password', 'buddypress' ); ?>
+</h2>
+
+<p class="info email-pwd-info">
+	<?php _e( 'Update your email and or password.', 'buddypress' ); ?>
+</p>
+
+<form action="<?php echo esc_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/general' ); ?>" method="post" class="standard-form" id="settings-form">
+
+	<?php if ( ! is_super_admin() ) : ?>
+
+		<label for="pwd"><?php _e( 'Current Password <span>(required to update email or change current password)</span>', 'buddypress' ); ?></label>
+		<input type="password" name="pwd" id="pwd" size="16" value="" class="settings-input small" <?php bp_form_field_attributes( 'password' ); ?>/> &nbsp;<a href="<?php echo esc_url( wp_lostpassword_url() ); ?>"><?php esc_html_e( 'Lost your password?', 'buddypress' ); ?></a>
+
+	<?php endif; ?>
+
+	<label for="email"><?php _e( 'Account Email', 'buddypress' ); ?></label>
+	<input type="email" name="email" id="email" value="<?php echo esc_attr( bp_get_displayed_user_email() ); ?>" class="settings-input" <?php bp_form_field_attributes( 'email' ); ?>/>
+
+	<div class="info bp-feedback">
+		<span class="bp-icon" aria-hidden="true"></span>
+		<p class="text"><?php esc_html_e( 'Leave password fields blank for no change', 'buddypress' ); ?></p>
+	</div>
+
+	<label for="pass1"><?php esc_html_e( 'Add Your New Password', 'buddypress' ); ?></label>
+	<input type="password" name="pass1" id="pass1" size="16" value="" class="settings-input small password-entry" <?php bp_form_field_attributes( 'password' ); ?>/>
+
+	<label for="pass2" class="repeated-pwd"><?php esc_html_e( 'Repeat Your New Password', 'buddypress' ); ?></label>
+	<input type="password" name="pass2" id="pass2" size="16" value="" class="settings-input small password-entry-confirm" <?php bp_form_field_attributes( 'password' ); ?>/>
+
+	<div id="pass-strength-result"></div>
+
+	<?php bp_nouveau_submit_button( 'members-general-settings' ); ?>
+
+</form>
+
+<?php
+bp_nouveau_member_hook( 'after', 'settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/group-invites.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/group-invites.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/group-invites.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+/**
+ * BuddyPress - Members Settings ( Group Invites )
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+?>
+
+<h2 class="screen-heading group-invites-screen">
+	<?php _e( 'Group Invites', 'buddypress' ); ?>
+</h2>
+
+<?php
+if ( 1 === bp_nouveau_groups_get_group_invites_setting() ) {
+	 bp_nouveau_user_feedback( 'member-group-invites-friends-only' );
+} else {
+	 bp_nouveau_user_feedback( 'member-group-invites-all' );
+}
+?>
+
+
+<form action="<?php echo esc_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/invites/' ); ?>" name="account-group-invites-form" id="account-group-invites-form" class="standard-form" method="post">
+
+	<label for="account-group-invites-preferences">
+		<input type="checkbox" name="account-group-invites-preferences" id="account-group-invites-preferences" value="1" <?php checked( 1, bp_nouveau_groups_get_group_invites_setting() ); ?>/>
+			<?php esc_html_e( 'I want to restrict Group invites to my friends only.', 'buddypress' ); ?>
+	</label>
+
+	<?php bp_nouveau_submit_button( 'member-group-invites' ); ?>
+
+</form>
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/notifications.php	(revision 12265)
@@ -0,0 +1,28 @@
+<?php
+/**
+ * BuddyPress - Members Settings ( Notifications )
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+bp_nouveau_member_hook( 'before', 'settings_template' ); ?>
+
+<h2 class="screen-heading email-settings-screen">
+	<?php _e( 'Email Notifications', 'buddypress' ); ?>
+</h2>
+
+<p class="bp-help-text email-notifications-info">
+	<?php _e( 'Set your email notification preferences.', 'buddypress' ); ?>
+</p>
+
+<form action="<?php echo esc_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/notifications' ); ?>" method="post" class="standard-form" id="settings-form">
+
+	<?php bp_nouveau_member_email_notice_settings(); ?>
+
+	<?php bp_nouveau_submit_button( 'member-notifications-settings' ); ?>
+
+</form>
+
+<?php
+bp_nouveau_member_hook( 'after', 'settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/profile.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/profile.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/buddypress/members/single/settings/profile.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * BuddyPress - Members Settings ( Profile )
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+bp_nouveau_member_hook( 'before', 'settings_template' ); ?>
+
+<h2 class="screen-heading profile-settings-screen">
+	<?php esc_html_e( 'Profile Visibility Settings', 'buddypress' ); ?>
+</h2>
+
+<p class="bp-help-text profile-visibility-info">
+	<?php esc_html_e( 'Select who may see your profile details.', 'buddypress' ); ?>
+</p>
+
+<form action="<?php echo esc_url( bp_displayed_user_domain() . bp_get_settings_slug() . '/profile/' ); ?>" method="post" class="standard-form" id="settings-form">
+
+	<?php if ( bp_xprofile_get_settings_fields() ) : ?>
+
+		<?php
+		while ( bp_profile_groups() ) :
+			bp_the_profile_group();
+		?>
+
+			<?php if ( bp_profile_fields() ) : ?>
+
+				<table class="profile-settings bp-tables-user" id="<?php echo esc_attr( 'xprofile-settings-' . bp_get_the_profile_group_slug() ); ?>">
+					<thead>
+						<tr>
+							<th class="title field-group-name"><?php bp_the_profile_group_name(); ?></th>
+							<th class="title"><?php esc_html_e( 'Visibility', 'buddypress' ); ?></th>
+						</tr>
+					</thead>
+
+					<tbody>
+
+						<?php
+						while ( bp_profile_fields() ) :
+							bp_the_profile_field();
+						?>
+
+							<tr <?php bp_field_css_class(); ?>>
+								<td class="field-name"><?php bp_the_profile_field_name(); ?></td>
+								<td class="field-visibility"><?php bp_profile_settings_visibility_select(); ?></td>
+							</tr>
+
+						<?php endwhile; ?>
+
+					</tbody>
+				</table>
+
+			<?php endif; ?>
+
+		<?php endwhile; ?>
+
+	<?php endif; ?>
+
+	<input type="hidden" name="field_ids" id="field_ids" value="<?php bp_the_profile_field_ids(); ?>" />
+
+	<?php bp_nouveau_submit_button( 'members-profile-settings' ); ?>
+
+</form>
+
+<?php
+bp_nouveau_member_hook( 'after', 'settings_template' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp-mixins.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp-mixins.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp-mixins.scss	(revision 12265)
@@ -0,0 +1,664 @@
+//
+// BuddyPress Sass Mixins
+//
+// @version 3.0.0
+
+// This file provides some basic mixins for BP elements
+// such as box sizing, radius, calc etc
+
+
+// ======= Typography ======
+
+// BP provides two mixins for font size, one basic  that states
+// a value and can produce rem & px versions the other that calculates
+// a reduced size for small
+// screen sizes or series of screens described as breakpoints & can
+// be termed responsive font sizing.
+
+
+// Font size decs are used with caution BP doesn't manage
+// layout sizes & must take care to work with existing layout sizes.
+
+@mixin font-size($font-size: 16) {
+	$rem-font-value: ($font-size / 10);
+	font-size: ($font-size * 1px);
+	//	font-size: $rem-font-value + rem;
+}
+
+
+// The following Sass map handles some basic re-sizing of BP fonts.
+// Sizes are set to change on the medium breakpoint.
+// It is hard for BP to know what the theme is doing so we are
+// limited in what we can do here.
+// The dev can feed a value to the mixin which will then be calculated
+// below to find a reduced size.
+
+@mixin responsive-font($res-size: 16) {
+
+	$the-sizes: $res-size;
+
+	$small: round(($the-sizes / 1.2));
+	$larger: $the-sizes;
+
+	$the-sizes: (
+		null:   $small,
+		46.8em: $larger
+	);
+
+	@each $font-breakpoint, $resonsive-font-size in $the-sizes {
+
+		@if $font-breakpoint == null {
+			font-size: $resonsive-font-size + px;
+		} @else {
+
+			@media screen and (min-width: $font-breakpoint) {
+				font-size: $resonsive-font-size + px;
+			}
+		}
+	}
+
+}
+
+// Essential em based breakpoints
+// mixin wraps rulesets or properties.
+// written as:
+// @include medium-up() {
+//  body {property: value;}
+// }
+
+
+@mixin small-up {
+
+	@media screen and (min-width: 24em) {
+
+		@content;
+	}
+}
+
+@mixin medium-small-max {
+
+	@media screen and (max-width: 32em) {
+
+		@content;
+	}
+}
+
+@mixin medium-small-up {
+
+	@media screen and (min-width: 32em) {
+
+		@content;
+	}
+}
+
+@mixin medium-max {
+
+	@media screen and (max-width: 46.8em) {
+
+		@content;
+	}
+}
+
+@mixin medium-up {
+
+	@media screen and (min-width: 46.8em) {
+
+		@content;
+	}
+}
+
+@mixin medium-lrg-up {
+
+	@media screen and (min-width: 55em) {
+
+		@content;
+	}
+}
+
+@mixin large-up {
+
+	@media screen and (min-width: 75em) {
+
+		@content;
+	}
+}
+
+// 'clearfix-element' allows us to pass an element, class or id to the mixin.
+// Use where we need to have float containment and where getting to
+// the physical element to add a class of 'clearfix' is problematic.
+
+@mixin clearfix-element($item) {
+
+	#{$item}:before,
+	#{$item}:after {
+		content: " ";
+		display: table;
+	}
+
+	#{$item}:after {
+		clear: both;
+	}
+
+}
+
+// These two mixins add hide/show properties for clicked/focussed elements.
+// They may be added to existing rulesets or added to a class selector
+// rulesets to use hardcoded to template markup.
+
+// Convenience helper to add hide properties to rulesets
+
+@mixin hide() {
+	display: none;
+}
+
+// companion mixins properties for showing the element for :focus/:hover
+// or via JS added hooks.
+
+@mixin show() {
+	height: auto;
+	left: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+// Box model - defaults to value 'border-box'
+// Vendor prefixes are pretty much redundant for this property,
+// consider removing
+
+@mixin box-model($box-type: null) {
+
+	@if $box-type {
+		// if a param was passed through
+		$box-type: $box-type;
+	} @else {
+		$box-type: border-box;
+	}
+
+	-webkit-box-sizing: $box-type;
+	-moz-box-sizing: $box-type;
+	box-sizing: $box-type;
+}
+
+// Border radius
+
+@mixin border-radius($radius) {
+	-webkit-border-radius: $radius;
+	-moz-border-radius: $radius;
+	-ms-border-radius: $radius;
+	border-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-top-radius($radius) {
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-right-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-top-right-radius: $radius;
+	border-top-right-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-bottom-radius($radius) {
+	-webkit-border-bottom-right-radius: $radius;
+	border-bottom-right-radius: $radius;
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+@mixin border-left-radius($radius) {
+	-webkit-border-bottom-left-radius: $radius;
+	border-bottom-left-radius: $radius;
+	-webkit-border-top-left-radius: $radius;
+	border-top-left-radius: $radius;
+	background-clip: padding-box;
+}
+
+// very basic box-shadow mixin  - this needs to be
+// updated & improved
+
+@mixin box-shadow($values...) {
+	-webkit-box-shadow: $values;
+	-moz-box-shadow: $values;
+	box-shadow: $values;
+}
+
+// There can be a need to override the themes global styles
+// Provide a box-shadow: none;
+
+@mixin box-shadow-none() {
+	-moz-box-shadow: none;
+	-webkit-box-shadow: none;
+	box-shadow: none;
+}
+
+// Calc e.g (100% - 10px)
+
+@mixin calc($property, $expression) {
+	#{$property}: -webkit-calc(#{$expression});
+	#{$property}: -moz-calc(#{$expression});
+	#{$property}: calc(#{$expression});
+}
+
+// Flexbox Mixins
+
+// Set display to box flex & set the direction and wrapping behavior
+// shorthand for flex-direction & flex wrap - default ( row wrap )
+
+@mixin flex-box-dir($flex-dir: "row", $flex-wrap: "nowrap") {
+	display: -webkit-flex;
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-webkit-flex-flow: unquote($flex-dir) unquote($flex-wrap);
+	-moz-flex-flow: unquote($flex-dir) unquote($flex-wrap);
+	-ms-flex-flow: unquote($flex-dir) unquote($flex-wrap);
+	-o-flex-flow: unquote($flex-dir) unquote($flex-wrap);
+	flex-flow: unquote($flex-dir) unquote($flex-wrap);
+}
+
+// align child items
+// flex-start | flex-end | center | baseline | stretch (default)
+
+@mixin flex-align($align: stretch) {
+	-ms-flex-align: $align; //older?
+	-webkit-align-items: $align;
+	-webkit-box-align: $align; // old
+	align-items: $align; // current specs
+}
+
+// Box-order  - shuffle item order for columns
+
+@mixin box-order($box-order-number: 1) {
+	-webkit-box-order: $box-order-number;
+	-moz-order: $box-order-number;
+	-ms-order: $box-order-number;
+	-o-order: $box-order-number;
+	order: $box-order-number;
+}
+
+// shorthand: flex-grow, flex-shrink, flex-basis for child items
+// defaults 0 | 1 | auto
+
+@mixin box-item-size($grow: 0, $shrink: 1, $basis: auto) {
+	-webkit-flex: $grow $shrink $basis;
+	-moz-flex: $grow $shrink $basis;
+	-ms-flex: $grow $shrink $basis;
+	-o-flex: $grow $shrink $basis;
+	flex: $grow $shrink $basis;
+}
+
+// Vertical centering - all in one, this is a
+// fixed mixin for one result/requirement.
+// This class allows us to center child elements
+// using flexbox. This is a progressive enhancement,
+// it won't work in all browsers, older browser will simply
+// fall back to non centered or using older techniques.
+// N.B It would be preferable to remove the older property syntax
+
+@mixin center-vert() {
+	display: -webkit-box; // older
+	display: -ms-flexbox; // older
+	display: -webkit-flex; // current prefix
+	display: flex; // current specs
+	-ms-flex-align: center; //older?
+	-webkit-align-items: center;
+	-webkit-box-align: center; // old
+	align-items: center; // current specs
+}
+
+// Layout elements / lists as grids
+// A combined mixin to set flex-flow & flex.
+// This mixin must be called on the parent i.e 'ul'
+
+@mixin build-flex-layout($row-wrap: wrap, $align: stretch, $basis: auto, $grow: 0 ) {
+
+	@include flex-box-dir($flex-dir: "row", $flex-wrap: $row-wrap);
+	@include flex-align($align: $align);
+
+	> * {
+
+		@include box-item-size($grow: $grow, $shrink: 1, $basis: $basis);
+	}
+}
+
+// Links as a tabbed effect.
+// Renders links in a horizontal layout as a
+// tab effect on the current selected links.
+
+@mixin tabbed-links() {
+
+	.tabbed-links {
+
+		ul,
+		ol {
+			border-bottom: 1px solid $bp-border-dark;
+			float: none;
+			margin: $marg-lrg 0 $marg-sml;
+
+			&:before,
+			&:after {
+				content: " ";
+				display: block;
+			}
+
+			&:after {
+				clear: both;
+			}
+
+			li {
+				float: left;
+				list-style: none;
+				margin: 0 $marg-sml 0 0;
+
+				a,
+				span:not(.count) {
+					background: none;
+					border: none;
+					display: block;
+					padding: 4px 10px;
+				}
+
+				a:focus,
+				a:hover {
+					background: none;
+				}
+			}
+
+			li:not(.current) {
+				margin-bottom: 2px;
+			}
+
+			li.current {
+				border-color: $bp-border-dark $bp-border-dark #fff;
+				border-style: solid;
+				border-top-left-radius: 4px;
+				border-top-right-radius: 4px;
+				border-width: 1px;
+				margin-bottom: -1px;
+				padding: 0 $pad-sml 1px;
+
+				a {
+					background: none;
+					color: $black;
+				}
+			}
+		}
+	}
+
+	// we don't want top margin if tabs are on the top subnav position
+	.bp-subnavs.tabbed-links > ul {
+		margin-top: 0;
+	}
+}
+
+// Provides the properties to style vert navs in a visual tab style.
+// This allows rulesets to be applied without having to re-write.
+// e.g dir-navs use this in _bp_layouts.scss if the navs tabs class set.
+
+@mixin primary-navs-vert-tabs() {
+
+	ul {
+
+		li:not(.selected) {
+
+			a:hover,
+			a:focus {
+				background: none;
+			}
+		}
+
+		li.selected {
+			background: none;
+			border: 1px solid $med-light-grey;
+			border-right-color: #fff;
+
+			a {
+				background: none;
+				color: $black;
+				font-weight: 600;
+
+				span {
+					background: $dark-grey;
+					border: 1px solid $med-light-grey;
+					color: $white;
+				}
+			}
+		}
+
+	}
+}
+
+// BP message boxes
+
+// BP message boxes
+// Creates a message box with border, background color
+// If  no text-color passed in then the background used for
+// text is darkened by 50%.
+@mixin message-box($background: #fff, $text-color: null, $border: null) {
+
+	@if $text-color {
+		// if a param was passed through
+		$text-color: $text-color;
+	} @else {
+		$text-color: darken($background, 50%);
+	}
+
+	background: $background;
+
+	@if $border != none {
+
+		@if $border {
+			border: $border;
+		} @else {
+			border: 1px solid darken($background, 10%);
+		}
+	}
+
+	color: $text-color;
+}
+
+// Password warn colors
+
+@mixin pwd-bad-colors($color: inherit, $background: null, $border: null) {
+
+	@if $background {
+		$background: $background;
+	} @else {
+		$background: $background-bad;
+	}
+
+	background-color: $background;
+
+	@if $border {
+		$border: $border;
+	} @else {
+		$border: $border-bad;
+	}
+
+	border-color: $border;
+
+	color: $color;
+}
+
+@mixin pwd-short-colors($color: inherit, $background: null, $border: null) {
+
+	@if $background {
+		$background: $background;
+	} @else {
+		$background: $background-short;
+	}
+
+	background-color: $background;
+
+	@if $border {
+		$border: $border;
+	} @else {
+		$border: $border-short;
+	}
+
+	border-color: $border;
+
+	color: $color;
+}
+
+@mixin pwd-good-colors($color: inherit, $background: null, $border: null) {
+
+	@if $background {
+		$background: $background;
+	} @else {
+		$background: $background-good;
+	}
+
+	background-color: $background;
+
+	@if $border {
+		$border: $border;
+	} @else {
+		$border: $border-good;
+	}
+
+	border-color: $border;
+
+	color: $color;
+}
+
+// BP Tooltips
+
+// Bottom center tooltip - Default
+
+@mixin bp-tooltip-default {
+
+	&:after {
+		left: 50%;
+		margin-top: $tooltip-tip-area;
+		top: 110%;
+		-webkit-transform: translate(-50%, 0);
+		-ms-transform: translate(-50%, 0);
+		transform: translate(-50%, 0);
+	}
+
+}
+
+// Bottom left tooltip
+
+@mixin bp-tooltip-bottom-left {
+
+	&:after {
+		left: 0;
+		-webkit-transform: translate(0, 0);
+		-ms-transform: translate(0, 0);
+		transform: translate(0, 0);
+	}
+
+}
+
+// Bottom right tooltip
+
+@mixin bp-tooltip-bottom-right {
+
+	&:after {
+		left: auto;
+		right: 0;
+		-webkit-transform: translate(0, 0);
+		-ms-transform: translate(0, 0);
+		transform: translate(0, 0);
+	}
+
+}
+
+// Top center tooltip
+
+@mixin bp-tooltip-top {
+
+	&:after {
+		bottom: 110%;
+		left: 50%;
+		margin-bottom: $tooltip-tip-area;
+		margin-top: 0;
+		top: auto;
+		-webkit-transform: translateX(-50%);
+		-ms-transform: translateX(-50%);
+		transform: translateX(-50%);
+	}
+
+}
+
+// Top left tooltip
+
+@mixin bp-tooltip-top-left {
+
+	&:after {
+		bottom: 110%;
+		left: 0;
+		margin-bottom: $tooltip-tip-area;
+		margin-top: 0;
+		top: auto;
+		-webkit-transform: translateX(0);
+		-ms-transform: translateX(0);
+		transform: translateX(0);
+	}
+
+}
+
+// Top right tooltip
+
+@mixin bp-tooltip-top-right {
+
+	&:after {
+		bottom: 110%;
+		left: auto;
+		margin-bottom: $tooltip-tip-area;
+		margin-top: 0;
+		right: 0;
+		top: auto;
+		-webkit-transform: translateX(0);
+		-ms-transform: translateX(0);
+		transform: translateX(0);
+	}
+
+}
+
+// Left tooltip
+
+@mixin bp-tooltip-left {
+
+	&:after {
+		bottom: 50%;
+		left: auto;
+		margin-right: 10px;
+		margin-top: 0;
+		right: 100%;
+		top: auto;
+		-webkit-transform: translate(0, 50%);
+		-ms-transform: translate(0, 50%);
+		transform: translate(0, 50%);
+	}
+
+}
+
+// Right tooltip
+
+@mixin bp-tooltip-right {
+
+	&:after {
+		bottom: 50%;
+		left: 100%;
+		margin-left: 10px;
+		margin-top: 0;
+		top: auto;
+		-webkit-transform: translate(0, 50%);
+		-ms-transform: translate(0, 50%);
+		transform: translate(0, 50%);
+	}
+
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp-variables.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp-variables.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp-variables.scss	(revision 12265)
@@ -0,0 +1,99 @@
+//
+// BuddyPress scss Variables
+//
+// @version 3.0.0
+
+// This file provides colors, and any other values that benefit
+// from a variable definition that can be managed and changed from one
+// central position for all instances of the property/value.
+
+// Spacing values - used primarily for main elements to allow us
+// to adjust values in a grouped manner.
+
+$pad-xsml: 0.3em !default;
+$pad-sml:  0.5em !default;
+$pad-med:  1em !default;
+$pad-lrg:  2em !default;
+$pad-xlrg: 4em !default;
+
+$marg-xsml: 5px !default;
+$marg-sml:  10px !default;
+$marg-med:  15px !default;
+$marg-lrg:  20px !default;
+$marg-xlrg: 30px !default;
+
+// Colours
+
+$dark-grey:            #555 !default;
+$light-text:           #767676 !default; // lightest gray on #fff with 4.5:1 contrast ratio
+$light-text-plus:      #737373 !default; // $light-text on #fafafa background
+$primary-grey:         #ccc !default;
+$light-grey:           #eaeaea !default;
+$med-light-grey:       #d6d6d6 !default;
+$blue:                 #5087e5 !default;
+$black:                #333 !default;
+$grey:                 $primary-grey !default;
+$golden:               #edbb34 !default;
+
+$highlight:            $blue !default; // for various elements such as span qnt indications
+$text-link-hover:      #5087e5 !default;
+$text-link-visited:    $med-light-grey !default;
+$bp-border-color:      #eee !default;
+$bp-border-dark:       $primary-grey !default;
+$buttons-background:   #ededed !default;
+$off-white:            #fafafa !default;
+$white:                #fff !default;
+$form-border-color:    $med-light-grey !default;
+$bp-text:              $dark-grey !default;
+$form-text:            $light-text-plus !default;
+$primary-headings:     $light-text !default;
+$textarea-bck:         $white !default;
+$meta-text:            $light-text-plus !default;
+$meta-text-dark:       $dark-grey !default;
+$focussed-border-clr:  rgba(31, 179, 221, 0.9) !default; // textareas? e.g whats-new
+
+// Message colors
+
+// used on text == red & input validation pseudo classes
+$warn:           #b71717 !default;
+$valid:          #91cc2c !default;
+
+// The colors for boxes
+$warnings:       #d33 !default;
+$informational:  #0b80a4 !default;
+$loading:        #ffd087 !default;
+$update-success: #8a2 !default;
+
+// password warn colors & border-short
+// These may be overidden on the @include mixin call
+$pwd-background:    #eee !default;
+$background-short:  #ffa0a0 !default;
+$border-short:      #f04040 !default;
+$background-good:   #66d66e !default;
+$border-good:       #438c48 !default;
+$background-bad:    #ffb78c !default;
+$border-bad:        #ff853c !default;
+
+// Animation loader-pulsate border / shadow colors
+
+$border-color-from:        #aaa !default;
+$border-color-to:          #ccc !default;
+$shadow-color-from:        #ccc !default;
+$shadow-color-to:          #f8f8f8 !default;
+
+// BP Tooltips
+
+$tooltip-background:        $white !default;
+$tooltip-border:            $light-text-plus !default;
+$tooltip-border-radius:     1px !default;
+$tooltip-box-shadow:        4px 4px 8px rgba(0, 0, 0, 0.2) !default; // 1px 1px 0 1px rgba(132, 132, 132, 0.3) !default;
+$tooltip-font-family:       "Helvetica Neue", helvetica, arial, san-serif;
+$tooltip-font-size:         12px !default;
+$tooltip-font-weight:       400 !default;
+$tooltip-max-width:         200px !default;
+$tooltip-padding-hor:       8px !default;
+$tooltip-padding-vert:      5px !default;
+$tooltip-text-color:        $black !default;
+$tooltip-tip-area:          7px !default; // bp-tooltip arrow's height, width, & margin
+$tooltip-tip-background:    $white !default;
+$tooltip-z-index:           100000 !default;
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_activity_comments.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_activity_comments.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_activity_comments.scss	(revision 12265)
@@ -0,0 +1,156 @@
+// BP activity comment entries in response to activity loop entries
+// @version 3.0.0
+.buddypress-wrap {
+
+	.activity-comments {
+		clear: both;
+		margin: 0 5%;
+		overflow: hidden;
+		position: relative;
+		width: auto;
+
+		ul {
+			clear: both;
+			list-style: none;
+			margin: $marg-med 0 0;
+			padding: 0;
+
+			li {
+				border-top: 1px solid $bp-border-color;
+				border-bottom: 0;
+				padding: $pad-med 0 0;
+
+				ul {
+					// indent any nested comments - comment to a comment
+					margin-left: 5%;
+				}
+			}
+
+			li:first-child {
+				border-top: 0;
+			}
+
+			li:last-child {
+				margin-bottom: 0;
+			}
+		} // close ul
+
+		div.acomment-avatar {
+			width: auto;
+
+			img {
+				border-width: 1px;
+				float: left;
+				height: 25px;
+				max-width: none;
+				width: 25px;
+			}
+		}
+
+		.acomment-meta,
+		.acomment-content p {
+
+			@include font-size(14);
+		}
+
+		.acomment-meta {
+			color: $dark-grey;
+			overflow: hidden;
+			padding-left: 2%;
+		}
+
+		.acomment-content {
+			border-left: 1px solid $bp-border-dark;
+			margin: $marg-med 0 0 10%;
+			padding: $pad-sml $pad-med;
+
+			p {
+				margin-bottom: 0.5em;
+			}
+		}
+
+		.acomment-options {
+			float: left;
+			margin: $marg-sml 0 $marg-sml $marg-lrg;
+
+			a {
+				color: $light-text;
+
+				@include font-size(14);
+
+				&:focus,
+				&:hover {
+					color: inherit;
+				}
+			}
+
+		} // close .acomment-options
+
+		.activity-meta.action {
+			background: none;
+			margin-top: $marg-sml;
+
+			// if button element in use
+			button {
+
+				@include font-size(14);
+				font-weight: 400;
+				text-transform: none;
+			}
+		}
+
+		.show-all {
+
+			button {
+
+				@include font-size(14);
+				text-decoration: underline;
+				padding-left: $pad-sml;
+
+				span {
+					text-decoration: none;
+				}
+
+				&:hover,
+				&:focus {
+
+					span {
+						color: $blue;
+					}
+				}
+			}
+		}
+
+	} // close .activity-comments
+
+	// Activity li.mini Comments
+
+	.mini {
+
+		.activity-comments {
+			clear: both;
+			margin-top: 0;
+		}
+
+	}
+
+} // close .buddypress-wrap
+
+// Single Activity Comment Entry View
+body.activity-permalink {
+
+	.activity-comments {
+		background: none;
+		width: auto;
+
+		> ul {
+			padding: 0 $pad-sml 0 $pad-med;
+		}
+
+		ul li > ul {
+			margin-top: $marg-sml;
+		}
+
+	}
+
+} //close .activity-permalink
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_activity_entries.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_activity_entries.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_activity_entries.scss	(revision 12265)
@@ -0,0 +1,428 @@
+// BP Activity Entries - activity loop
+// @version 3.0.0
+
+.activity-list {
+
+	padding: $pad-sml;
+
+	.activity-item:before,
+	.activity-item:after {
+		content: " ";
+		display: table;
+	}
+
+	.activity-item:after {
+		clear: both;
+	}
+
+	.activity-item { // li element - (entries)
+		list-style: none;
+		padding: $pad-med;
+
+		&.has-comments {
+			padding-bottom: $pad-med;
+		}
+
+
+		div.item-avatar { // 'div' for weight
+			margin: 0 auto;
+			text-align: center;
+			width: auto;
+
+			img {
+				height: auto;
+				max-width: 40%;
+			}
+		}
+
+		@include medium-up() {
+
+			div.item-avatar { // 'div' for weight
+				margin: 0 2% 0 0;
+				text-align: left;
+				width: 15%;
+
+				img {
+					max-width: 80%;
+				}
+			}
+		}
+
+		&.mini {
+
+			@include font-size(13);
+			position: relative;
+
+			.activity-avatar {
+				margin-left: 0 auto;
+				text-align: center;
+				width: auto;
+
+				img.avatar,
+				img.FB_profile_pic { /* stylelint-disable-line selector-class-pattern */
+					max-width: 15%;
+				}
+			}
+
+			@include medium-up() {
+
+				.activity-avatar {
+					margin-left: $marg-med;
+					text-align: left;
+					width: 15%;
+
+					img.avatar,
+					img.FB_profile_pic { /* stylelint-disable-line selector-class-pattern */
+						max-width: 60%;
+					}
+				}
+			}
+		} // close .mini
+
+		&.new_forum_post,
+		&.new_forum_topic {
+
+			.activity-inner {
+				border-left: 2px solid $light-grey;
+				margin-left: $marg-sml;
+				padding-left: $pad-med;
+			}
+		} // close li forum elements
+
+
+		&.newest_mentions_activity,
+		&.newest_friends_activity,
+		&.newest_groups_activity,
+		&.newest_blogs_activity {
+			// why?
+			background: rgba(31, 179, 221, 0.1);
+		}
+
+		.activity-inreplyto {
+			color: $light-text;
+
+			@include font-size(13);
+
+			> p {
+				display: inline;
+				margin: 0;
+			}
+
+			blockquote,
+			.activity-inner {
+				background: none;
+				border: 0;
+				display: inline;
+				margin: 0;
+				overflow: hidden;
+				padding: 0;
+			}
+		}
+
+		// The header elements: date stamp, author etc;
+		.activity-header {
+			margin: 0 auto;
+			width: 80%;
+
+			// ensure elements are display inline, some themes setting avatars as block
+			a,
+			img {
+				display: inline;
+			}
+
+			.avatar {
+				display: inline-block;
+				margin: 0 $marg-xsml;
+				vertical-align: bottom;
+			}
+
+			.time-since {
+
+				@include font-size(14);
+				color: $light-text;
+				text-decoration: none;
+
+				&:hover {
+					color: $light-text;
+					cursor: pointer;
+					text-decoration: underline;
+				}
+			}
+
+		} // close .activity-header
+
+		.activity-content {
+
+			.activity-header,
+			.comment-header {
+				color: $light-text;
+				margin-bottom: $marg-sml;
+			}
+
+			.activity-inner,
+			blockquote {
+				background: $off-white;
+				margin: $marg-med 0 $marg-sml;
+				overflow: hidden;
+				padding: $pad-med;
+			}
+
+			p {
+				margin: 0;
+			}
+
+		} // close .activity-content
+
+		.activity-inner {
+
+			p {
+				word-wrap: break-word;
+			}
+		}
+
+		.activity-read-more {
+			margin-left: 1em; // proportional to the text size please!
+			white-space: nowrap;
+		}
+
+		// The footer elements, buttons etc
+
+		// if this is a ul then...
+		// else we'll skip this for the rules below.
+		ul.activity-meta {
+			margin: 0;
+			padding-left: 0;
+
+			li {
+				// if a list is being used keep it inline
+				border: 0;
+				display: inline-block;
+			}
+		}
+
+		.activity-meta.action {
+			border: 1px solid transparent;
+			background: $off-white;
+			padding: 2px;
+			position: relative;
+			text-align: left;
+
+			// this wrapper has generic margins in _bp_lists.scss
+			// remove for act stream actions.
+			div.generic-button {
+				margin: 0;
+			}
+
+			.button {
+				background: transparent;
+			}
+
+			a {
+				padding: 4px 8px;
+			}
+
+			// we don't want a background hover if icons used
+			.button:focus,
+			.button:hover {
+				background: none;
+			}
+
+			// Uncomment .acomment-reply class for icon us
+			.button:before,
+			.icons:before {
+				font-family: dashicons;
+				font-size: 18px;
+				vertical-align: middle;
+			}
+
+			.acomment-reply.button:before {
+				content: "\f101";
+			}
+
+			.view:before {
+				content: "\f125";
+			}
+
+			.fav:before {
+				content: "\f154";
+			}
+
+			.unfav:before {
+				content: "\f155";
+			}
+
+			.delete-activity:before {
+				content: "\f153";
+			}
+
+			.delete-activity:hover {
+				color: #800;
+			}
+
+			.button {
+				border: 0;
+				box-shadow: none;
+
+				span {
+					background: none;
+					color: #555;
+					font-weight: 700;
+				}
+			}
+
+		} // close .activity-meta
+
+	} // close .activity-item
+
+} // close .activity-list
+
+@include medium-up() {
+
+	.activity-list.bp-list {
+		padding: 30px;
+	}
+
+	.activity-list {
+
+		.activity-item {
+
+			.activity-content {
+				margin: 0;
+				position: relative;
+
+				&:after {
+					clear: both;
+					content: "";
+					display: table;
+				}
+			}
+
+			.activity-header {
+				margin: 0 $marg-med 0 0;
+				width: auto;
+			}
+
+		} // li entry item
+	}
+}
+
+.buddypress-wrap {
+
+	// load more link
+	.activity-list {
+
+		.load-more,
+		.load-newest {
+			background: $off-white;
+			border: 1px solid $bp-border-color;
+			font-size: 110%;
+			margin: $marg-med 0;
+			padding: 0;
+			text-align: center;
+
+			a {
+				color: $dark-grey;
+				display: block;
+				padding: $pad-sml 0;
+
+				&:focus,
+				&:hover, {
+					background: $white;
+					color: $black;
+				}
+			}
+
+			&:focus,
+			&:hover {
+				border-color: darken($bp-border-color, 5%);
+
+				@include box-shadow(0 0 6px 0 $light-grey);
+			}
+		}
+
+	}
+}
+
+// Single Activity Entry View
+body.activity-permalink {
+
+	.activity-list {
+
+		li {
+			border-width: 1px;
+			padding: $pad-med 0 0 0;
+
+			&:first-child {
+				padding-top: 0;
+			}
+
+			&.has-comments {
+				padding-bottom: 0;
+			}
+
+		} // close li
+
+		.activity-avatar {
+			width: auto;
+
+			a {
+				display: block;
+			}
+
+			img {
+				max-width: 100%;
+			}
+		}
+
+		.activity-content {
+			border: 0;
+			font-size: 100%;
+			line-height: 1.5;
+			padding: 0;
+
+			.activity-header {
+				margin: 0;
+				padding: $pad-sml 0 0 0;
+				text-align: center;
+				width: 100%;
+			}
+
+			.activity-inner,
+			blockquote {
+				margin-left: 0;
+				margin-top: $marg-sml;
+			}
+		}
+
+		.activity-meta {
+			margin: $marg-sml 0 $marg-sml;
+		}
+
+		.activity-comments {
+			margin-bottom: $marg-sml;
+		}
+
+		@include medium-up() {
+
+			.activity-avatar {
+				left: -20px;
+				margin-right: 0;
+				position: relative;
+				top: -20px;
+			}
+
+			.activity-content {
+				margin-right: $marg-sml;
+
+				.activity-header {
+
+					p {
+						text-align: left;
+					}
+				}
+			}
+
+		} // close @media
+
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_animations.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_animations.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_animations.scss	(revision 12265)
@@ -0,0 +1,85 @@
+// Provides any animation or transition type effects for BP elements
+// @version 3.0.0
+
+// Fade in hover transition
+#buddypress-wrap {
+
+	* {
+		transition: opacity 0.1s ease-in-out 0.1s;
+	}
+
+	button,
+	a.generic-button,
+	a.button,
+	input[type="submit"],
+	input[type="reset"] {
+		transition: background 0.1s ease-in-out 0.1s, color 0.1s ease-in-out 0.1s, border-color 0.1s ease-in-out 0.1s;
+	}
+
+}
+
+// Historical animation sequence for ajax loading effects.
+// Ajax Animations and styles
+// scss-lint:disable VendorPrefix
+.buddypress-wrap {
+
+	a.loading,
+	input.loading {
+		-moz-animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+		-webkit-animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+		animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+		border-color: #aaa;
+	}
+
+	@-webkit-keyframes loader-pulsate {
+
+		from {
+			border-color: $border-color-from;
+			-webkit-box-shadow: 0 0 6px $shadow-color-from;
+			box-shadow: 0 0 6px $shadow-color-from;
+		}
+
+		to {
+			border-color: $border-color-to;
+			-webkit-box-shadow: 0 0 6px $shadow-color-to;
+			box-shadow: 0 0 6px $shadow-color-to;
+		}
+	}
+
+	@-moz-keyframes loader-pulsate {
+
+		from {
+			border-color: $border-color-from;
+			-moz-box-shadow: 0 0 6px $shadow-color-from;
+			box-shadow: 0 0 6px $shadow-color-from;
+		}
+
+		to {
+			border-color: $border-color-to;
+			-moz-box-shadow: 0 0 6px $shadow-color-to;
+			box-shadow: 0 0 6px $shadow-color-to;
+		}
+	}
+
+	@keyframes loader-pulsate {
+
+		from {
+			border-color: $border-color-from;
+			-moz-box-shadow: 0 0 6px $shadow-color-from;
+			box-shadow: 0 0 6px $shadow-color-from;
+		}
+
+		to {
+			border-color: $border-color-to;
+			-moz-box-shadow: 0 0 6px $shadow-color-to;
+			box-shadow: 0 0 6px $shadow-color-to;
+		}
+	}
+
+	a.loading:hover,
+	input.loading:hover {
+		color: #777;
+	}
+
+}
+// scss-lint:enable VendorPrefix
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_blogs_loop.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_blogs_loop.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_blogs_loop.scss	(revision 12265)
@@ -0,0 +1,46 @@
+// BP Blogs loop.
+// @version 3.0.0
+
+.buddypress-wrap {
+
+	.blogs-list { // ul
+
+		li {
+
+			@include medium-up() {
+
+				.item-block {
+					float: none;
+					width: auto;
+				}
+
+				.item-meta {
+					clear: left;
+					float: none;
+				}
+			} // close @media
+
+		} // close li
+
+	} // close .blogs-list
+}
+
+// If BP Dir Navs are selected as vertical
+// We need to just adjust the loop item elements a little
+// to cope with the decreased width.
+
+@include medium-up() {
+
+	.buddypress-wrap {
+
+		.bp-dir-vert-nav {
+
+			.blogs-list {
+
+				.list-title {
+					width: auto;
+				}
+			}
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_buttons.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_buttons.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_buttons.scss	(revision 12265)
@@ -0,0 +1,404 @@
+// BP Buttons, Submits, Icons and general clicky things styling.
+// @version 3.0.0
+
+// Buttons by default are styled as simple boxes but we may
+// change that by adding a parent class ( on any close parent to
+// the element ) .text-link-list
+
+.buddypress {
+
+	// this doubling avoids issues applying styles from body class
+	// to general theme elements - changing body class would be better though.
+	.buddypress-wrap {
+
+		button,
+		a.button,
+		input[type="submit"],
+		input[type="button"],
+		input[type="reset"],
+		ul.button-nav:not(.button-tabs) li a,
+		.generic-button a,
+		.comment-reply-link,
+		a.bp-title-button,
+		.activity-read-more a {
+			background: $white; /* Old browsers */
+			border-color: $bp-border-dark;
+			border-style: solid;
+			border-width: 1px;
+			color: $dark-grey;
+			cursor: pointer;
+			font-size: inherit;
+			font-weight: 400;
+			outline: none;
+			padding: $pad-xsml $pad-sml;
+			text-align: center;
+			text-decoration: none;
+			width: auto;
+		}
+
+		// Re-instate the quick-tag padding to avoid the above ruleset
+		// causing styling issues.
+		.button-small[type="button"] {
+			padding: 0 8px 1px;
+		}
+
+		button:hover,
+		button:focus,
+		a.button:focus,
+		a.button:hover,
+		input[type="submit"]:focus,
+		input[type="submit"]:hover,
+		input[type="button"]:focus,
+		input[type="button"]:hover,
+		input[type="reset"]:focus,
+		input[type="reset"]:hover,
+		.button-nav li a:focus,
+		.button-nav li a:hover,
+		.button-nav li.current a,
+		.generic-button a:focus,
+		.generic-button a:hover,
+		.comment-reply-link:focus,
+		.comment-reply-link:hover,
+		.activity-read-more a:focus,
+		.activity-read-more a:hover {
+			background: $buttons-background;
+			border-color: darken($bp-border-dark, 20%);
+			color: $black;
+			outline: none;
+			text-decoration: none;
+		}
+
+		input[type="submit"].pending,
+		input[type="button"].pending,
+		input[type="reset"].pending,
+		input[type="button"].disabled,
+		input[type="reset"].disabled,
+		input[type="submit"][disabled="disabled"],
+		button.pending,
+		button.disabled,
+		div.pending a,
+		a.disabled {
+			border-color: $bp-border-color;
+			color: $light-text;
+			cursor: default;
+		}
+
+		input[type="submit"]:hover.pending,
+		input[type="button"]:hover.pending,
+		input[type="reset"]:hover.pending,
+		input[type="submit"]:hover.disabled,
+		input[type="button"]:hover.disabled,
+		input[type="reset"]:hover.disabled,
+		button.pending:hover,
+		button.disabled:hover,
+		div.pending a:hover,
+		a.disabled:hover {
+			border-color: $bp-border-color;
+			color: $light-text;
+		}
+
+		// provide a class to style elements as text only non button.
+		button.text-button,
+		input.text-button {
+			background: none;
+			border: 0;
+
+			@include box-shadow-none();
+			color: $light-text;
+
+			&.small {
+
+				@include font-size(13);
+			}
+
+			&:focus,
+			&:hover {
+				background: none;
+				text-decoration: underline;
+			}
+		}
+
+		.activity-list {
+
+			a.button {
+				border: none;
+			}
+		}
+
+		.bp-invites-content {
+
+			ul.bp-list {
+
+				li {
+
+					a.invite-button:hover {
+						color: #1fb3dd;
+					}
+				}
+
+				li.selected,
+				li {
+
+					a.invite-button:hover,
+					a.group-remove-invite-button:hover {
+						color: #a00;
+					}
+				}
+
+			}
+		}
+
+		#item-buttons:empty {
+			display: none;
+		}
+
+		input:disabled {
+
+			&:hover,
+			&:focus {
+				background: none;
+			}
+		}
+
+		// Style action links as text links
+		// .text-links-list would be stated for anchors parent wrapping element
+		// Example usage on group members manage screen.
+
+		.text-links-list {
+
+			a.button {
+				background: none;
+				border: none;
+				border-right: 1px solid $bp-border-color;
+				color: $light-text-plus;
+				display: inline-block;
+				padding: $pad-xsml $pad-med;
+
+				&:visited {
+					color: $text-link-visited;
+				}
+
+				&:focus,
+				&:hover {
+					color: $text-link-hover;
+				}
+
+			}
+
+			a:first-child {
+				padding-left: 0;
+			}
+
+			a:last-child {
+				border-right: none;
+			}
+
+		} // close text-links-list
+
+		// In grids style the basic action button a little differently
+
+		.bp-list.grid {
+
+			.action {
+
+				a,
+				button {
+					border: 1px solid $bp-border-dark;
+					display: block;
+					margin: 0;
+
+					&:focus,
+					&:hover {
+						background: $buttons-background;
+					}
+				}
+			} // .action
+		}
+
+	} // close .buddypress
+
+	// The group or site create link as either a button or plain link.
+	// @todo this will need further classes & styles depending on nav layouts?
+	// The class 'create-button' will be hardcoded for the moment on the li
+	// this could be a user selected choice to style as a button though?
+
+	#buddypress {
+
+		// Style the create link as a button - this would be better served
+		// as a modifier class but we need to be able to allow user selection
+		// between button style & plain text. For the moment we'll force a
+		// choice, vert dir navs get button style, horizontal dir navs a text link.
+
+		.create-button { // the parent, i.e li element.
+			background: none;
+
+			a:focus,
+			a:hover {
+				text-decoration: underline;
+			}
+
+			@include medium-up {
+				float: right;
+			}
+			text-align: center;
+
+			a {
+				border: 1px solid $bp-border-dark;
+
+				@include border-radius(5px);
+				@include box-shadow(inset 0 0 6px 0 $light-grey);
+				margin: 0.2em 0;
+				width: auto;
+
+				&:focus,
+				&:hover {
+					background: none;
+					border-color: $bp-border-dark;
+
+					@include box-shadow(inset 0 0 12px 0 $light-grey );
+				}
+			}
+
+		} // .create-button
+
+		&.bp-dir-vert-nav {
+
+			@include medium-up {
+
+				.create-button {
+					float: none;
+					padding-top: $pad-lrg;
+
+
+					a {
+						margin-right: 0.5em;
+					}
+				}
+			}
+		}
+
+		&.bp-dir-hori-nav {
+
+			.create-button {
+				float: left;
+
+				a,
+				a:hover {
+					background: none;
+					border: 0;
+
+					@include box-shadow-none();
+					margin: 0;
+				}
+			}
+		}
+	} // close #buddypress
+
+} // close body class .buddypress @todo this should be removed in time
+
+// ==== Icons ====
+
+.buddypress-wrap {
+
+	// buttons that are visually rendering as icons only won't
+	// want the general button styles.
+	// We'll use a class hook to identify those elements.
+	button {
+
+		&.bp-icons,
+		&.ac-reply-cancel {
+
+			background: none;
+			border: 0;
+		}
+
+		&.bp-icons {
+
+			&:focus,
+			&:hover {
+				background: none;
+			}
+		}
+
+		&.ac-reply-cancel {
+
+			&:focus,
+			&:hover {
+				background: none;
+				text-decoration: underline;
+			}
+		}
+	}
+
+	.filter label:before,
+	.feed a:before,
+	.bp-invites-filters  .invite-button span.icons:before,
+	.bp-messages-filters li a.messages-button:before,
+	.bp-invites-content li .invite-button span.icons:before {
+		font-family: dashicons;
+		font-size: 18px;
+	}
+
+	.bp-invites-content .item-list li .invite-button span.icons:before {
+
+		@include responsive-font(32);
+	}
+
+	// If icons are required for anchors e.g '<a class="button"'
+	// then we may have .button background hovers being inherited
+	// Use this grouped selector ruleset to add elements that need
+	// no change. As we need weight this is specific to bp-lists
+
+	// This is probably a less than optimal solution & a better one will exist to
+	// implemented when time allows.
+	.bp-list {
+
+		a.button.invite-button {
+
+			&:focus,
+			&:hover {
+				background: none;
+			}
+		}
+	}
+
+	.filter label:before {
+		content: "\f536";
+	}
+
+	div.feed a:before,
+	li.feed a:before {
+		content: "\f303";
+	}
+
+	ul.item-list {
+
+		li {
+
+			.invite-button:not(.group-remove-invite-button) {
+
+				span.icons:before {
+					content: "\f502";
+				}
+			}
+		}
+
+		li.selected .invite-button,
+		li .group-remove-invite-button {
+
+			span.icons:before {
+				content: "\f153";
+			}
+		}
+	} // close ul.item-list
+
+	.bp-invites-filters ul li #bp-invites-next-page:before,
+	.bp-messages-filters ul li #bp-messages-next-page:before {
+		content: "\f345";
+	}
+
+	.bp-invites-filters ul li #bp-invites-prev-page:before,
+	.bp-messages-filters ul li #bp-messages-prev-page:before {
+		content: "\f341";
+	}
+} // close .buddypress-wrap
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_comment_form.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_comment_form.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_comment_form.scss	(revision 12265)
@@ -0,0 +1,61 @@
+// BP Activity Comments Form
+// @version 3.0.0
+form.ac-form {
+	display: none;
+	padding: $pad-med;
+
+	.ac-reply-avatar {
+		float: left;
+
+		img {
+			border: 1px solid #eee;
+		}
+	}
+
+	.ac-reply-content {
+		color: $light-text;
+		padding-left: $pad-med;
+
+		a {
+			text-decoration: none;
+		}
+
+		.ac-textarea {
+			margin-bottom: $marg-med;
+			padding: 0 $pad-sml;
+			overflow: hidden;
+
+			textarea {
+				background: transparent;
+				box-shadow: none;
+				color: $bp-text;
+				font-family: inherit;
+				font-size: 100%;
+				height: 60px;
+				margin: 0;
+				outline: none;
+				padding: 0.5em;
+				width: 100%;
+
+				&:focus {
+
+					@include box-shadow( 0 0 6px $med-light-grey );
+				}
+			}
+		}
+
+		input {
+			margin-top: $marg-sml;
+		}
+	}
+
+}
+
+.activity-comments li form.ac-form {
+	clear: both;
+	margin-right: $marg-med;
+}
+
+.activity-comments form.root {
+	margin-left: 0;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_cover_image.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_cover_image.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_cover_image.scss	(revision 12265)
@@ -0,0 +1,42 @@
+// Cover image container generic styles.
+// These properties manage the required positioning of
+// the image container overlay.
+// @version 3.0.0
+
+/*
+* Default required cover image rules
+*/
+
+#cover-image-container {
+	position: relative;
+}
+
+#header-cover-image {
+	background-color: #c5c5c5;
+	background-position: center top;
+	background-repeat: no-repeat;
+	background-size: cover;
+	border: 0;
+	display: block;
+	left: 0;
+	margin: 0;
+	padding: 0;
+	position: absolute;
+	top: 0;
+	width: 100%;
+	z-index: 1;
+}
+
+#item-header-cover-image {
+	//padding: 0 1em;
+	position: relative;
+	z-index: 2;
+
+	#item-header-avatar {
+		padding: 0 1em;
+	}
+}
+
+/*
+* end cover image block
+*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_filters.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_filters.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_filters.scss	(revision 12265)
@@ -0,0 +1,204 @@
+// BP Directory Filters
+// Specific Styling & Positioning
+// N.B. See _bp_navigation for additional rules and properties.
+// @version 3.0.0.
+.buddypress-wrap {
+
+	.dir-component-filters { // ul
+
+		.filter { // li
+
+			label {
+				display: inline;
+			}
+		}
+	}
+
+	// User account filters .subnav-filters
+
+	@include clearfix-element(".subnav-filters");
+
+	.subnav-filters {
+		background: none;
+		list-style: none;
+		margin: $marg-med 0 0;
+		padding: 0;
+
+		div {
+			margin: 0;
+		}
+
+		> ul {
+			float: left;
+			list-style: none;
+		}
+
+		&.bp-messages-filters {
+
+			ul {
+				width: 100%;
+			}
+
+			.messages-search {
+				margin-bottom: 1em;
+
+				@include medium-up {
+
+					margin-bottom: 0;
+				}
+			}
+		}
+
+		div {
+			float: none;
+
+			select,
+			input[type="search"] {
+
+				@include font-size(16);
+			}
+
+			button.nouveau-search-submit {
+				padding: 5px 0.8em 6px;
+			}
+
+			button#user_messages_search_submit {
+				padding: 7px 0.8em;
+			}
+		}
+
+		.component-filters {
+			margin-top: $marg-sml;
+		}
+
+		.feed {
+			margin-right: $marg-med;
+		}
+
+		.last.filter {
+
+			label {
+				display: inline;
+			}
+
+		}
+
+		.user-messages-bulk-actions {
+
+			@include clearfix-element(".bulk-actions-wrap");
+
+			.bulk-actions-wrap {
+
+				&.bp-show {
+					display: inline-block;
+				}
+
+				&.bp-hide {
+					display: none;
+				}
+			}
+
+			.select-wrap {
+				border: 0;
+			}
+
+			.select-wrap:focus,
+			.select-wrap:hover {
+				outline: 1px solid $med-light-grey;
+			}
+
+			.bulk-actions {
+				float: left;
+			}
+
+			label {
+				display: inline-block;
+				font-weight: 300;
+				margin-right: 25px;
+				padding: 5px 0;
+			}
+
+			div {
+
+				select {
+					-webkit-appearance: textfield;
+				}
+			}
+
+			.bulk-apply {
+				border: 0;
+				border-radius: none;
+				font-weight: 400;
+				line-height: 1.8;
+				margin: 0 0 0 10px;
+				padding: 3px 5px;
+				text-align: center;
+				text-transform: none;
+				width: auto;
+
+				span {
+					vertical-align: middle;
+				}
+			}
+		}
+
+		//position for wider screens
+
+		@include medium-small-up() {
+
+			li {
+				margin-bottom: 0;
+			}
+
+			.subnav-search,
+			.subnav-search form,
+			.feed,
+			.bp-search,
+			.dir-search,
+			.user-messages-bulk-actions,
+			.user-messages-search,
+			.group-invites-search,
+			.group-act-search {
+				float: left;
+			}
+
+			.last,
+			.component-filters {
+				float: right;
+				margin-top: 0;
+				width: auto;
+
+				select {
+					max-width: 250px;
+				}
+			}
+
+			.user-messages-search {
+				float: right;
+			}
+
+		} // @media
+
+	} // close .subnav-filters
+
+	.notifications-options-nav {
+
+		input#notification-bulk-manage {
+			border: 0;
+			border-radius: 0;
+			line-height: 1.6;
+		}
+	}
+
+	.group-subnav-filters {
+
+		.group-invites-search {
+			margin-bottom: 1em;
+		}
+
+		.last {
+			text-align: center;
+		}
+	}
+
+} // close .buddypress-wrap
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_forms.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_forms.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_forms.scss	(revision 12265)
@@ -0,0 +1,408 @@
+// BP Forms Elements Global Styles
+// @version 3.0.0
+
+// Some essential classes used for forms elements:
+// labels - sizing especially for standalone controls
+// 'bp-label-text'
+//
+// Where a 'p' element is used to wrap form controls
+// that element should be classed with 'bp-controls-wrap'
+
+.buddypress-wrap {
+
+	.filter,
+	#whats-new-post-in-box { // has no form element and can't hold rules below.
+
+		select {
+			border: 1px solid $form-border-color;
+		}
+	}
+
+	input.action[disabled] {
+		cursor: pointer;
+		opacity: 0.4;
+	}
+
+	#notification-bulk-manage[disabled] {
+		display: none;
+	}
+
+	fieldset {
+
+		legend {
+			font-size: inherit;
+			font-weight: 600;
+		}
+	}
+
+	textarea,
+	input[type="text"],
+	input[type="email"],
+	input[type="url"],
+	input[type="tel"],
+	input[type="password"] {
+
+		&:focus {
+
+			@include box-shadow(0 0 8px $light-grey);
+		}
+	}
+
+	// Ensure select elements aren't given a relative/fixed height.
+
+	select {
+		height: auto;
+	}
+
+	// Preserve fluid widths, prevent horizontal resize of textareas.
+	textarea {
+		resize: vertical;
+	}
+
+	.standard-form {
+
+		.bp-controls-wrap {
+			margin: 1em 0;
+		}
+
+		textarea,
+		input[type="text"],
+		input[type="color"],
+		input[type="date"],
+		input[type="datetime"],
+		input[type="datetime-local"],
+		input[type="email"],
+		input[type="month"],
+		input[type="number"],
+		input[type="range"],
+		input[type="search"],
+		input[type="tel"],
+		input[type="time"],
+		input[type="url"],
+		input[type="week"],
+		select,
+		input[type="password"],
+		[data-bp-search] input[type="search"],
+		[data-bp-search] input[type="text"],
+		.groups-members-search input[type="search"],
+		.groups-members-search input[type="text"] {
+			background: $off-white;
+			border: 1px solid $form-border-color;
+			border-radius: 0;
+			font: inherit;
+			font-size: 100%;
+			padding: $pad-sml;
+		}
+
+		// Manage browser pseudo class validation states & static 'invalid' class
+		input,
+		textarea,
+		select {
+
+			&[required] {
+
+				// FF adds red shadow on password inputs, remove!
+				box-shadow: none;
+				border-width: 2px;
+
+				// IE Edge uses outline for invalid controls, remove
+				outline: 0;
+
+				// Sadly this does mean all inputs are considered to be
+				// invalid from page load & bordered.
+				&:invalid {
+					border-color: $warn;
+				}
+
+				&:valid {
+					border-color: $valid;
+				}
+
+				&:focus {
+					border-color: $form-border-color;
+					border-width: 1px;
+				}
+			}
+
+			// Class set when BP global signup errors exist.
+			// This needs to be stated after the  pseudo :valid
+			// as technically the input has a valid value.
+			&.invalid[required] {
+				border-color: $warn;
+			}
+		}
+
+		input:not(.button-small),
+		textarea {
+			width: 100%;
+		}
+
+		input[type="radio"],
+		input[type="checkbox"] {
+			margin-right: $marg-xsml;
+			width: auto;
+		}
+
+		select {
+			padding: 3px;
+		}
+
+		textarea {
+			height: 120px; // bad rule! ?
+		}
+
+		textarea#message_content {
+			height: 200px; // bad rule! ?
+		}
+
+		input[type="password"] {
+			margin-bottom: $marg-xsml;
+		}
+
+		input:focus,
+		textarea:focus,
+		select:focus {
+			background: $off-white;
+			color: $bp-text;
+			outline: 0;
+		}
+
+		label,
+		span.label {
+			display: block;
+			font-weight: 600;
+			margin: $marg-med 0 $marg-xsml;
+			width: auto;
+		}
+
+		a.clear-value {
+			display: block;
+			margin-top: $marg-xsml;
+			outline: none;
+		}
+
+		.submit {
+			clear: both;
+			padding: $marg-med 0 0;
+		}
+
+		p.submit {
+			margin-bottom: 0;
+		}
+
+		div.submit input {
+			margin-right: $marg-med;
+		}
+
+		p label,
+		#invite-list label {
+			font-weight: 400;
+			margin: auto;
+		}
+
+		p.description {
+			color: $form-text;
+			margin: $marg-xsml 0;
+		}
+
+		div.checkbox label:nth-child(n+2),
+		div.radio div label {
+			color: $form-text;
+			font-size: 100%;
+			font-weight: 400;
+			margin: $marg-xsml 0 0;
+		}
+
+		&#send-reply {
+
+			textarea {
+				width: 97.5%;
+			}
+		}
+
+		&#sidebar-login-form {
+
+			label {
+				margin-top: $marg-xsml;
+			}
+
+			input[type="text"],
+			input[type="password"] {
+				padding: 4px;
+				width: 95%;
+			}
+		} // close &#sidebar-login-form
+
+		&.profile-edit {
+
+			input:focus {
+				background: $white;
+			}
+		}
+
+		@include medium-up() {
+
+			// The Group invites form lists (not required if Ajax templates in use)
+			.left-menu {
+				float: left;
+			}
+
+			#invite-list ul {
+				list-style: none;
+				margin: 1%;
+
+				li {
+					margin: 0 0 0 1%;
+				}
+			}
+
+			.main-column {
+				margin-left: 190px;
+
+				ul#friend-list {
+					clear: none;
+					float: left;
+				}
+
+				ul#friend-list h4 {
+					clear: none;
+				}
+			}
+
+		} // close @media
+
+		// In tables elements like checkboxes best aligned to middle
+		// remove margins in these cases.
+		.bp-tables-user {
+
+			label {
+				margin: 0;
+			}
+		}
+
+	} // close .standard-form
+
+	// Register page
+	.signup-form {
+
+		label,
+		legend {
+			font-weight: 400;
+		}
+	}
+
+} // close .buddypress-wrap
+
+// These rules do not belong here really but it's
+// where original styles placed them
+body.no-js {
+
+	.buddypress {
+
+		#notifications-bulk-management #select-all-notifications,
+		label[for="message-type-select"],
+		#message-type-select,
+		#delete_inbox_messages,
+		#delete_sentbox_messages,
+		#messages-bulk-management #select-all-messages {
+			display: none;
+		}
+	}
+}
+
+/* Overrides for embedded WP editors */
+.buddypress-wrap {
+
+	.wp-editor-wrap {
+
+		a.button,
+		.wp-editor-wrap button,
+		.wp-editor-wrap input[type="submit"],
+		.wp-editor-wrap input[type="button"],
+		input[type="reset"] {
+			padding: 0 8px 1px;
+		}
+	}
+}
+
+// Style the select element  - generic sitewide styling
+// Remove the browser chrome & add our own arrow styles, borders, hovers.
+
+.buddypress-wrap {
+
+	.select-wrap {
+		border: 1px solid $bp-border-color;
+
+		label {
+			display: inline;
+		}
+
+		select::-ms-expand {
+			display: none;
+		}
+
+		select {
+			-moz-appearance: none;
+			-webkit-appearance: none;
+			-o-appearance: none;
+			appearance: none;
+			border: 0;
+			cursor: pointer;
+			margin-right: -25px;
+			padding: 6px 25px 6px $marg-sml;
+			position: relative;
+			text-indent: -2px;
+			z-index: 1;
+			width: 100%;
+		}
+
+		select,
+		select:focus,
+		select:active {
+			background: transparent;
+		}
+
+		span.select-arrow {
+			display: inline-block;
+			position: relative;
+			z-index: 0;
+
+			&:before {
+				color: $primary-grey;
+				content: "\25BC";
+			}
+		}
+
+		&:focus,
+		&:hover {
+
+			.select-arrow:before {
+				color: darken($primary-grey, 15%);
+			}
+		}
+	} // close .select-wrap
+
+	// Add a little on hover inset shadow for  subnav search & filters
+	.select-wrap,
+	.bp-search form {
+
+		&:focus,
+		&:hover {
+			border: 1px solid darken($bp-border-color, 10%);
+			box-shadow: inset 0 0 3px #eee;
+		}
+	}
+
+	// Manage select wrap for notification actions, wide screens
+	// ensures a shrink wrap width.
+
+	@include medium-small-up() {
+
+		.notifications-options-nav {
+
+			.select-wrap {
+				float: left;
+			}
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_general_classes.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_general_classes.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_general_classes.scss	(revision 12265)
@@ -0,0 +1,55 @@
+// BP General CLasses: screen reader, clearfix etc.
+// @version 3.0.0
+
+.bp-screen-reader-text {
+	// See also _bp_mixins for mixin to add properties on block
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+	word-wrap: normal !important;
+}
+
+.clearfix {
+
+	&:before,
+	&:after {
+		content: " ";
+		display: table;
+	}
+}
+
+.clearfix:after {
+	clear: both;
+}
+
+// Vertical centering
+// This class allows us to center child elements
+// using flexbox this is a progressive enhancement,
+// it won't work in all browsers, older browser will simply
+// fall back to non centered or to using older techniques.
+// N.B. A mixin exists '@include center-vert()' for rulesets
+// to use to add the properties below.
+.center-vert {
+	display: -ms-flexbox;
+	display: -webkit-flex;
+	display: flex;
+	-ms-flex-align: center;
+	-webkit-align-items: center;
+	-webkit-box-align: center;
+	align-items: center;
+}
+
+.bp-hide {
+
+	@include hide();
+}
+
+.bp-show {
+
+	@include show();
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_generic_and_typography.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_generic_and_typography.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_generic_and_typography.scss	(revision 12265)
@@ -0,0 +1,255 @@
+// BP Generic elements, Typography and Images.
+// @version 4.0.0
+
+// ========= Generic Sitewide elements ========
+body {
+
+	#buddypress { // needs the weight of the id here
+
+		* {
+			// some WP themes add anchor border bottom :(
+			a {
+				box-shadow: none;
+				text-decoration: none;
+			}
+		} // close * the kitchen sink of elements
+
+		// Some themes have a negative left margin on the blockquote element??
+		#item-body,
+		.bp-lists {
+
+			blockquote {
+				margin-left: 0;
+			}
+		}
+
+		// CSS Box Model - some themes will set all blocks to border box
+		// some will cherry pick - we will set elements as we need to,
+		// ( setting a global change might be better)
+		// box-modal() defaults to 'border-box' if no param passed
+		.bp-list .action {
+
+			@include box-model();
+		}
+	}
+}
+
+// Some themes have very restricted content widths for pages
+// or entry-content (2013, 2014, 2017) Lets adjust these restrictions
+body.buddypress {
+
+	@include medium-up() {
+
+		.entry-header,
+		.site-content .entry-header,
+		.entry-content {
+			max-width: none;
+		}
+
+		.entry-header {
+			float: none;
+			max-width: none;
+		}
+
+		.entry-content {
+			float: none;
+			max-width: none;
+			//padding: 0 $pad-xlrg;
+		}
+
+		// 2017 has a very large top padding we'll reduce that for bp screens
+		.site-content {
+			padding-top: 2.5em;
+		}
+
+		// Some themes are styling on ID's making it hard to override
+		// Adding a commononly used ID for site here to add weight '#page'
+		// In part this will help with 2017 floats & narrow widths
+
+
+		#page {
+
+			// If 2017 page choice is 'One Column' #primary is a narrow width
+			// Current RC has removed the customizer column switch !!
+			// remove width to allow BP screens to mirror post layout widths
+			// The .wrap element will serve still to provide a max width @ 1000px
+
+			#primary {
+				max-width: none;
+
+				.entry-header,
+				.entry-content {
+					float: none;
+					width: auto;
+				}
+			}
+		}
+
+
+	} // @media
+}
+
+// BP Theme Compatibility isn't technically a theme and has to
+// inherit and move over for a themes styles so with generic styling of
+// elements we have limited scope
+
+// Ensure themes can't cause issues with headings.
+body.buddypress { // add weight
+
+	.buddypress-wrap {
+
+		h1,
+		h2,
+		h3,
+		h4,
+		h5,
+		h6 {
+			clear: none;
+			// some themes set massive margins ensure we avoid these in bp elements
+			margin: 1em 0;
+			// some themes add padding top lets manage that generically sitewide
+			padding: 0;
+		}
+	}
+}
+
+// Ensure the .bp-wrap element always contains it's children,
+// this may cause issues but groups single requires it.
+
+/* Ensure .bp-wrap encloses it's children */
+
+@include clearfix-element(".bp-wrap");
+
+// General site avatars round or square ?
+// User set
+.buddypress-wrap.round-avatars {
+
+	.avatar {
+		border-radius: 50%;
+	}
+}
+
+// 'Boxes' i.e display:block when given definition with borders
+// look nice if corners subtly rounded by a couple of pixels
+div,
+dl,
+li,
+textarea,
+select,
+input[type="search"],
+input[type="submit"],
+input[type="reset"] {
+
+	@include border-radius(2px);
+}
+
+// ====== BP Typographic Elements ======
+
+// === Font sizing ===
+
+
+// Directory component titles, groups/user single, create & register
+// Entry headers are outside the BP injection point for pages.
+// & get styles by the theme, we'll attempt to over specify
+// the selector to ensure we target the right elements
+// (no BP classes available other than body classes!)
+
+body.buddypress {
+
+	article.page {
+
+		> .entry-header {
+			margin-bottom: 2em;
+			padding: 0;
+
+			.entry-title {
+
+				@include responsive-font(34);
+				font-weight: inherit;
+				color: $primary-headings;
+			}
+		}
+	}
+}
+
+// wrap block in bp content region class,
+// avoid styles filtering through
+// to site areas such as sidebars/footers etc.
+.buddypress-wrap {
+
+	dt.section-title {
+
+		@include responsive-font(22);
+	}
+
+	.bp-label-text,
+	.message-threads {
+
+		@include responsive-font(16);
+	}
+
+	.activity-header {
+
+		@include responsive-font(16);
+	}
+
+	.activity-inner {
+
+		@include responsive-font(18);
+	}
+
+	#whats-new-post-in {
+
+		@include font-size(16);
+	}
+
+	.mini .activity-header,
+	.acomment-meta {
+
+		@include font-size(16);
+	}
+
+	.dir-component-filters {
+
+		#activity-filter-by {
+
+			@include responsive-font(16);
+		}
+	}
+
+	$search-element-size: 15;
+	$search-element-button-size: $search-element-size + 5;
+
+	.bp-tables-user {
+
+		th {
+
+			@include responsive-font(16);
+		}
+
+		td {
+
+			@include responsive-font(14);
+		}
+	}
+
+	// Setting the profile fields to be a larger
+	// font than all general user tables is open for review.
+	.profile-fields {
+
+		th {
+
+			@include responsive-font(18);
+		}
+
+		td {
+
+			@include responsive-font(16);
+		}
+	}
+
+	#notification-select {
+
+		@include responsive-font(14);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_group_header.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_group_header.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_group_header.scss	(revision 12265)
@@ -0,0 +1,105 @@
+// BP Single Group Header Styles.
+// @version 3.0.0
+
+.groups-header {
+
+	.bp-group-type-list {
+		margin: 0;
+	}
+
+	.bp-feedback {
+		clear: both;
+	}
+
+	// groups admin/mod wrapper block
+	.group-item-actions {
+		float: left;
+		margin: 0 0 15px 15px;
+		padding-top: 0;
+		width: 100%;
+	}
+
+
+	// The admin & mod list display for a group.
+	// 'moderators-lists' is used to refer to
+	// generic members with elevated access.
+	.moderators-lists {
+		margin-top: 0;
+
+		.moderators-title {
+
+			@include font-size(14);
+		}
+
+		.user-list {
+			margin: 0 0 $marg-xsml;
+
+			ul:after {
+				clear: both;
+				content: "";
+				display: table;
+			}
+
+			li {
+				display: inline-block;
+				float: none;
+				margin-left: 4px;
+				padding: 4px;
+			}
+		}
+
+		img.avatar {
+
+			@include box-shadow-none();
+			float: none;
+			height: 30px;
+			margin: 0;
+			max-width: 100%;
+			width: 30px;
+		}
+	} // close moderators-lists
+
+	@include medium-up {
+
+		div#item-header-content {
+			float: left;
+			margin-left: 10%;
+			text-align: left;
+			padding-top: $marg-med;
+			width: 42%;
+		}
+
+		.group-item-actions {
+			float: right;
+			margin: 0 0 15px 15px;
+			text-align: right;
+			width: 20%;
+		}
+
+		.groups-meta {
+			clear: both;
+		}
+	}
+
+	.desc-wrap {
+		background: $light-grey;
+		border: 1px solid $med-light-grey;
+		margin: 0 0 $marg-med;
+		padding: $pad-med;
+		text-align: center;
+
+		.group-description {
+			background: $off-white;
+
+			@include box-shadow(inset 0 0 9px $primary-grey);
+			padding: $pad-med;
+			text-align: left;
+
+			p {
+				margin: 0;
+				padding: 0;
+			}
+		}
+	}
+} // close groups-header
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_activity.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_activity.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_activity.scss	(revision 12265)
@@ -0,0 +1,8 @@
+// Groups single screen - Activity
+// @version 3.0.0
+.buddypress.groups {
+
+	.activity-update-form {
+		margin-top: 0;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_create.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_create.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_create.scss	(revision 12265)
@@ -0,0 +1,14 @@
+// Group Creation Steps - Screens
+// @version 3.0.0
+
+#group-create-body {
+	padding: $pad-sml;
+
+	.creation-step-name {
+		text-align: center;
+	}
+
+	.avatar-nav-items {
+		margin-top: $marg-med;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_item_body.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_item_body.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_item_body.scss	(revision 12265)
@@ -0,0 +1,19 @@
+// Groups specific Item-body content
+// @version 3.0.0
+
+// Members list doesn't generate  subnav items items
+// so we negate the rules using :not()
+.single-item.group-members {
+
+	.item-body {
+
+		.filters:not(.no-subnav) {
+			border-top: 5px solid $light-grey;
+			padding-top: $pad-med;
+		}
+
+		.filters {
+			margin-top: 0;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_loop.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_loop.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_loop.scss	(revision 12265)
@@ -0,0 +1,118 @@
+// BP Groups loop.
+// @version 3.0.0
+
+.buddypress-wrap {
+
+	.groups-list { // ul
+
+		li {
+
+			.list-title {
+				text-align: center;
+			}
+
+			.group-details {
+				clear: left;
+			}
+
+			.group-desc {
+				border: 1px solid $light-grey;
+
+				@include border-radius(10px);
+				@include responsive-font(16);
+				color: $meta-text;
+				font-style: italic;
+				margin: $marg-sml auto 0;
+				padding: $pad-med;
+			}
+
+			p {
+				margin: 0 0 0.5em;
+			}
+
+			@include medium-up() {
+
+				.item {
+					margin-right: 0;
+				}
+
+				.list-title,
+				.item-meta {
+					text-align: left;
+					width: auto;
+				}
+
+				.item-meta {
+					margin-bottom: $marg-lrg;
+				}
+
+				.last-activity {
+					clear: left;
+					margin-top: -20px;
+				}
+
+			} // close @media
+
+		} // close li
+
+		li.group-no-avatar div.group-desc {
+			margin-left: 0;
+		}
+
+	} // close .groups-list
+
+	// User account group loop
+	.mygroups {
+
+		.groups-list.grid {
+
+			.wrap {
+				min-height: 450px;
+				padding-bottom: 0;
+			}
+		}
+	}
+
+}
+
+// If groups loop is in grid mode then description on narrow multi grids
+// can be too large so modify font-size by grid class parents.
+
+.buddypress-wrap {
+
+	.groups-list.grid {
+
+		@include medium-up {
+
+			&.three,
+			&.four {
+
+				.group-desc {
+
+					@include font-size(14);
+				}
+			}
+		} // @media
+	}
+}
+
+// If BP Dir Navs or user screen main navs are selected as vertical
+// We need to just adjust the loop item elements a little
+// to cope with the decreased width.
+
+@include medium-up() {
+
+	.buddypress { // body class
+
+		.bp-vertical-navs {
+
+			.groups-list {
+
+				.item-avatar {
+					margin-right: 3%;
+					width: 15%;
+				}
+			}
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_management.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_management.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_groups_management.scss	(revision 12265)
@@ -0,0 +1,141 @@
+// Single Groups Management screens
+// @version 3.0.0
+
+// Groups settings screen
+.buddypress-wrap {
+
+	.group-status-type {
+
+		ul {
+			margin: 0 0 $marg-lrg $marg-lrg;
+		}
+	}
+}
+
+// ==== Members manage ====
+
+.groups-manage-members-list {
+	padding: $pad-sml 0;
+
+	dd {
+		margin: 0;
+		padding: $pad-med 0;
+	}
+
+	.section-title {
+		background: $light-grey;
+		padding-left: $pad-xsml;
+	}
+
+	ul {
+		list-style: none;
+		margin-bottom: 0;
+
+		li {
+			border-bottom: 1px solid $bp-border-color;
+			margin-bottom: $marg-sml;
+			padding: $pad-sml $pad-xsml $pad-xsml;
+		}
+
+		li:only-child,
+		li:last-child {
+			border-bottom: 0;
+		}
+
+		li:nth-child(even) {
+			background: $off-white;
+		}
+
+		li.banned-user {
+			background: lighten($warn, 50%);
+		}
+
+		.member-name {
+			margin-bottom: 0;
+			text-align: center;
+		}
+
+		img {
+			display: block;
+			margin: 0 auto;
+			width: 20%;
+		}
+
+		@include medium-small-up() {
+
+			.member-name {
+				text-align: left;
+			}
+
+			img {
+				display: inline;
+				width: 50px;
+			}
+		}
+
+		@include clearfix-element(".members-manage-buttons");
+
+		.members-manage-buttons {
+			margin: $marg-med 0 $marg-xsml;
+
+			a.button {
+				color: $light-text;
+				display: block;
+
+				@include font-size(13);
+			}
+
+			@include medium-small-up() {
+
+				a.button {
+					display: inline-block;
+				}
+			}
+		}
+
+		.members-manage-buttons.text-links-list {
+			margin-bottom: 0;
+
+			@include medium-small-max() {
+
+				a.button {
+					background: #fafafa;
+					border: 1px solid $bp-border-color;
+					display: block;
+					margin-bottom: $marg-sml;
+				}
+			}
+		}
+
+		.action:not(.text-links-list) {
+
+			a.button {
+
+				@include font-size(12);
+			}
+		}
+
+		@include medium-up() {
+
+			li {
+
+				.avatar,
+				.member-name {
+					float: left;
+				}
+
+				.avatar {
+					margin-right: $marg-med;
+				}
+
+				.action {
+					clear: both;
+					float: left;
+				}
+
+			}
+		} // close @media
+
+	}
+
+} //close .groups-manage-members-list
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_info_messages.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_info_messages.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_info_messages.scss	(revision 12265)
@@ -0,0 +1,314 @@
+// Sitewide template error & info messages constructs.
+// @version 3.0.0.
+.buddypress-wrap {
+
+	.warn {
+		color: $warn;
+	}
+
+	.bp-messages {
+		border: 1px solid $bp-border-dark;
+
+		margin: 0 0 $marg-med;
+
+		.sitewide-notices {
+			display: block;
+			margin: $marg-xsml;
+			padding: $pad-sml;
+		}
+
+		// General info boxes
+		&.info {
+			margin-bottom: 0;
+		}
+
+		// Update success message boxes
+		&.updated {
+			clear: both;
+			display: block;
+		}
+
+		// Error message boxes
+		&.error {
+
+			p {
+				// oh linter you're too boringly pedantic, there will be properties!
+			}
+		}
+
+		// Warning boxes
+		//		&.warning {
+
+		//			p {
+		//			}
+		//		}
+
+	} // close .bp-messages
+
+	// user message screens handled separately due to backbone templates
+	// & looped messages
+	.bp-messages.bp-user-messages-feedback {
+		border: 0;
+	}
+
+	// This is an anomaly, this screen message is locked away in
+	// core avatar styles and ought to be pulled out, markup & styles to nouveau
+	#group-create-body {
+
+		.bp-cover-image-status {
+
+			p.warning {
+				background: $informational;
+				border: 0;
+
+				@include box-shadow( 0 0 3px 0 rgba(0, 0, 0, 0.2) );
+				color: $white;
+			}
+		}
+	}
+
+
+	// message-box() may be have params passed through:
+	// ($background: #fff, $text-color: #000, $border: 1px solid #faf0f0)
+	// '$border: none' ensures border turned off in favour of
+	// box shadow, default is 1px solid.
+	// text-color default value is background color 50% darker.
+	// Border color default is background 10% darker.
+
+	.bp-feedback:not(.custom-homepage-info) {
+
+		@include flex-box-dir();
+		@include flex-align();
+	}
+
+	.bp-feedback {
+
+		@include message-box($border: none);
+		@include box-shadow( 0 1px 1px 1px rgba(0, 0, 0, 0.1) );
+		color: $light-text-plus;
+
+		margin: 10px 0;
+		position: relative;
+
+		p {
+			margin: 0;
+		}
+
+		span.bp-icon {
+			color: $white;
+			display: block;
+			font-family: dashicons;
+			left: 0;
+			margin-right: $marg-sml;
+			position: relative;
+			padding: 0  0.5em;
+		}
+
+		.bp-help-text {
+			font-style: italic;
+		}
+
+		.text {
+
+			@include font-size(14);
+			margin: 0;
+			padding: $pad-sml 0;
+		}
+	}
+
+	.bp-feedback.no-icon {
+		padding: $pad-sml;
+	}
+
+	.bp-feedback.small:before {
+		line-height: inherit;
+	}
+
+	a[data-bp-close] span:before,
+	button[data-bp-close] span:before {
+		font-size: 32px;
+	}
+
+	a[data-bp-close],
+	button[data-bp-close] {
+		border: 0;
+		position: absolute;
+		top: 10px;
+		right: 10px;
+		width: 32px;
+	}
+
+	.bp-feedback.no-icon {
+
+		a[data-bp-close],
+		button[data-bp-close] {
+			top: -6px;
+			right: 6px;
+		}
+	}
+
+	button[data-bp-close]:hover {
+		background-color: transparent;
+	}
+
+	.bp-feedback {
+
+		p {
+			margin: 0;
+		}
+
+		.bp-icon {
+			font-size: 20px;
+			padding: 0 2px;
+		}
+	}
+
+	.bp-feedback.info,
+	.bp-feedback.help,
+	.bp-feedback.error,
+	.bp-feedback.warning,
+	.bp-feedback.loading,
+	.bp-feedback.success,
+	.bp-feedback.updated {
+
+		.bp-icon {
+
+			@include center-vert();
+		}
+	}
+
+	.bp-feedback.info,
+	.bp-feedback.help {
+
+		.bp-icon {
+			background-color: $informational;
+
+			&:before {
+				content: "\f348";
+			}
+		}
+	}
+
+	.bp-feedback.error,
+	.bp-feedback.warning {
+
+		.bp-icon {
+			background-color: $warnings;
+
+			&:before {
+				content: "\f534";
+			}
+		}
+	}
+
+	.bp-feedback.loading {
+
+		.bp-icon {
+			background-color: $loading;
+
+			&:before {
+				content: "\f469";
+			}
+		}
+	}
+
+	.bp-feedback.success,
+	.bp-feedback.updated {
+
+		.bp-icon {
+			background-color: $update-success;
+
+			&:before {
+				content: "\f147";
+			}
+		}
+	}
+
+	.bp-feedback.help {
+
+		.bp-icon {
+
+			&:before {
+				content: "\f468";
+			}
+		}
+	}
+
+	#pass-strength-result {
+		background-color: $pwd-background;
+		border-color: #ddd;
+		border-style: solid;
+		border-width: 1px;
+		display: none;
+		font-weight: 700;
+		margin: $marg-sml 0 $marg-sml 0;
+		padding: $pad-sml;
+		text-align: center;
+		width: auto;
+
+		// Show the feedback message when fields populated
+		&.show {
+			display: block;
+		}
+
+		&.mismatch {
+
+			@include pwd-bad-colors($color: $white, $background: $black, $border: transparent);
+		}
+
+		&.error,
+		&.bad {
+
+			@include pwd-bad-colors($color: $white);
+		}
+
+		&.short {
+
+			@include pwd-short-colors($color: $white);
+		}
+
+		&.strong {
+
+			@include pwd-good-colors($color: $white);
+		}
+
+	} // close #pass-strength-result
+
+	.standard-form#signup_form div div.error {
+		background: #faa;
+		color: #a00;
+		margin: 0 0 $marg-sml 0;
+		padding: $pad-sml;
+		width: 90%;
+	}
+
+	// these two are really helpful???!!
+	.accept,
+	.reject {
+		float: left;
+		margin-left: $marg-sml;
+	}
+
+	// .bp-feedback messages  - Ajax specific (.bp-ajax-message)
+
+	// Members action button errors in grid layouts
+
+	.members-list.grid { // this probably ought to serve the group loop too
+
+		.bp-ajax-message {
+			background: rgba($white, 0.9);
+			border: 1px solid $bp-border-color;
+
+			@include font-size(14);
+			left: 2%;
+
+			// postion absolute to prevent the element from expanding
+			// content height & breaking grid box heights.
+			position: absolute;
+			padding: $pad-sml $pad-med;
+			right: 2%;
+			top: 30px;
+		}
+	}
+
+} // close .buddypress-wrap
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_item_body_general.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_item_body_general.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_item_body_general.scss	(revision 12265)
@@ -0,0 +1,34 @@
+// BP Single Screens Item Body General Styles.
+// @version 3.0.0
+
+.buddypress-wrap {
+
+	.item-body {
+		margin: $marg-lrg 0;
+
+		.screen-heading {
+
+			@include font-size(20);
+
+			font-weight: 400;
+		}
+
+		.button-tabs {
+			margin: $marg-xlrg 0 $marg-med;
+		}
+
+	}
+}
+
+// If vertical primary nav is in use the item body
+// needs a little left padding on loops to let the content breath.
+
+.buddypress-wrap.bp-single-vert-nav {
+
+	.bp-list:not(.grid) {
+
+		.item-entry {
+			padding-left: $pad-sml;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_item_header_general.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_item_header_general.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_item_header_general.scss	(revision 12265)
@@ -0,0 +1,178 @@
+// BP Single Screens Item Header General Styles.
+// @version 3.0.0
+
+@include clearfix-element(".single-headers");
+
+.single-headers {
+	margin-bottom: $marg-med;
+
+	// Breakpoint element positioning
+
+	#item-header-avatar { // todo: this ought to be a class!
+
+		a {
+			display: block;
+			text-align: center;
+
+			img {
+				float: none;
+			}
+		}
+	}
+
+	div#item-header-content {
+		float: none;
+	}
+
+	@include medium-up() {
+
+		#item-header-avatar { // todo: this ought to be a class!
+
+			a {
+				text-align: left;
+
+				img {
+					float: left;
+
+				}
+			}
+		}
+
+		#item-header-content {
+			padding-left: $pad-lrg;
+		}
+	}
+
+	// End breakpoint positioning
+
+	.group-status,
+	.activity {
+		display: inline;
+	}
+
+	.group-status {
+
+		@include font-size(18);
+		color: $black;
+		padding-right: $pad-med;
+	}
+
+	.activity {
+		display: inline-block;
+
+		@include font-size(12);
+		padding: 0;
+	}
+
+	div#message,
+	#sitewide-notice {
+
+		p {
+			background-color: #ffd;
+			border: 1px solid #cb2;
+			color: #440;
+			font-weight: 400;
+			margin-top: 3px;
+			text-decoration: none;
+		}
+	}
+
+	// @mention user name
+	h2 {
+		line-height: 1.2;
+		margin: 0 0 $marg-xsml;
+
+		a {
+			color: $light-text;
+			text-decoration: none;
+		}
+
+		span.highlight {
+			display: inline-block;
+			font-size: 60%;
+			font-weight: 400;
+			line-height: 1.7;
+			vertical-align: middle;
+
+			span {
+				background: #a1dcfa;
+				color: $white;
+				cursor: pointer;
+				font-size: 80%;
+				font-weight: 700;
+				margin-bottom: 2px;
+				padding: 1px 4px;
+				position: relative;
+				right: -2px;
+				top: -2px;
+				vertical-align: middle;
+			}
+
+		}
+
+	} // close h2
+
+	img.avatar {
+		float: left;
+		margin: 0 15px 19px 0;
+	}
+
+	.item-meta {
+		color: $light-text;
+
+		@include font-size(14);
+		margin: $marg-med 0 $marg-xsml;
+		padding-bottom: $pad-sml;
+	}
+
+	ul {
+		margin-bottom: $marg-med;
+
+		li {
+			float: right;
+			list-style: none;
+		}
+	}
+
+	div.generic-button {
+		text-align: center;
+	}
+
+	li.generic-button {
+		display: inline-block;
+		text-align: center;
+	}
+
+	@include medium-up() {
+
+		div.generic-button,
+		a.button,
+		li.generic-button {
+			float: left;
+		}
+	}
+
+	div.generic-button,
+	a.button {
+		margin: $marg-sml $marg-sml 0 0;
+	}
+
+	// if these are list constructs
+	li.generic-button {
+		margin: 2px 10px;
+	}
+
+	li.generic-button:first-child {
+		margin-left: 0;
+	}
+
+	div#message.info {
+		line-height: 0.8;
+	}
+
+} // close .single-headers
+
+body.no-js  .single-item-header .js-self-profile-button {
+	display: none;
+}
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_layouts.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_layouts.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_layouts.scss	(revision 12265)
@@ -0,0 +1,663 @@
+// BP Layouts
+// Layouts provides classes to handle specific module
+// layouts on a user selectable basis.
+// The sheet also adds layout properties such as border-box
+// @version 3.0.0
+
+#item-body,
+.single-screen-navs {
+
+	@include box-model();
+}
+
+
+// ==== The Grid Layout Classes ====
+
+// BP Lists (loops) may be suited to a grid layout e.g members loops
+// These classes added to the elements will provide these styles.
+
+.grid {
+	// the parent element (usually the ul)
+
+	> li,
+	> li .generic-button a {
+
+		@include box-model( border-box );
+	}
+
+	> li {
+		border-bottom: 0;
+		padding-bottom: 10px;
+		padding-top: 0;
+
+		.list-wrap {
+			background: $off-white;
+			border: 1px solid $bp-border-color;
+			padding-bottom: 15px;
+			position: relative;
+			overflow: hidden;
+			padding-top: 14px;
+
+			.list-title {
+				padding: $pad-sml;
+			}
+
+			.update {
+				color: $meta-text;
+				padding: $pad-sml $pad-lrg;
+			}
+		}
+
+		.item-avatar {
+			text-align: center;
+
+			.avatar {
+				border-radius: 50%;
+				display: inline-block;
+				width: 50%;
+			}
+		}
+	} // close > li
+
+	// min-height rules to line up grid box elements
+	// to prevent breaking grid layout through uneven heights
+	// one column or single column mobile can have varying heights
+
+	@include small-up() {
+
+		&.members-list {
+
+			.list-wrap {
+				min-height: 340px;
+
+				.item-block {
+					margin: 0 auto;
+					min-height: 7rem;
+				}
+			}
+		}
+
+		&.members-group-list {
+
+			.list-wrap {
+
+				.item-block {
+					margin: 0 auto;
+					min-height: 7rem;
+				}
+			}
+		}
+
+		&.groups-list {
+
+			.list-wrap {
+				min-height: 470px;
+
+				.item-block {
+					min-height: 6rem;
+				}
+
+				.group-desc {
+					margin: $marg-med auto 0;
+					min-height: 5em;
+					overflow: hidden;
+				}
+
+				.last-activity,
+				.group-details,
+				.item-desc {
+					margin-bottom: 0;
+
+					p {
+						margin-bottom: 0;
+					}
+				}
+			}
+		}
+
+		&.blogs-list {
+
+			.list-wrap {
+				min-height: 350px;
+
+				.item-block {
+					margin: 0 auto;
+					min-height: 7rem;
+				}
+			}
+		}
+	} // close @media small-up
+
+} // close .grid
+
+/* Build the two column class small up */
+
+@include small-up() {
+
+	// Set the column  or item numbers to span horizontally
+	.grid {
+
+		> li.item-entry {
+			float: left;
+			margin: 0;
+		}
+
+		&.two {
+
+			> li {
+				padding-bottom: 20px;
+
+				// With two to a row large screens could allow
+				// unchecked fluid widths resulting in overlarge avatars.
+				// This ruleset adds a max width to inner element where
+				// max page widths aren't set.
+				@include large-up() {
+
+					.list-wrap {
+						max-width: 500px;
+						margin: 0 auto;
+					}
+				}
+			}
+		}
+
+		&.two,
+		&.three {
+
+			> li {
+				width: 50%;
+
+				&:nth-child(odd) {
+					padding-right: 10px;
+				}
+
+				&:nth-child(even) {
+					padding-left: 10px;
+				}
+
+				.item {
+					margin: 1rem auto 0;
+					width: 80%;
+
+					.item-title {
+						width: auto;
+					}
+				}
+			} // close > li
+		}
+	}
+}
+
+/* Build the three column class medium up */
+
+@include medium-up() {
+
+	.grid {
+
+		// Set three to a row
+		&.three {
+
+			> li {
+				padding-top: 0;
+				width: 33.333333%;
+				width: calc(100% / 3);
+
+				&:nth-child(1n+1) {
+					padding-left: 5px;
+					padding-right: 5px;
+				}
+
+				&:nth-child(3n+3) {
+					padding-left: 5px;
+					padding-right: 0;
+				}
+
+				&:nth-child(3n+1) {
+					padding-left: 0;
+					padding-right: 5px;
+				}
+
+			} // close >li
+		}
+	} // .grid
+} // close @media
+
+/* Build the four column class medium up */
+
+@include medium-up() {
+
+	// Set four to a row
+	.grid {
+
+		&.four {
+
+			> li {
+				width: 25%;
+
+				&:nth-child(1n+1) {
+					padding-left: 5px;
+					padding-right: 5px;
+				}
+
+				&:nth-child(4n+4) {
+					padding-left: 5px;
+					padding-right: 0;
+				}
+
+				&:nth-child(4n+1) {
+					padding-left: 0;
+					padding-right: 5px;
+				}
+
+			} // close > li
+		}
+	} // .grid
+} // close @media
+
+// This block styles the loop items for a grid layout
+// The block might be better located in the generic bp-lists section
+
+// Style the .bp-list li elements if a 'grid' class is set on the ul
+
+// if grid is set & this is a member, group or blog loop
+// we want to re-style some of the elements in the item
+.buddypress-wrap {
+
+	.grid.bp-list {
+		padding-top: $pad-med;
+
+		> li {
+			border-bottom: none;
+
+			.list-wrap {
+				padding-bottom: 3em;
+			}
+
+			.item-avatar {
+				margin: 0;
+				text-align: center;
+				width: auto;
+
+				img.avatar {
+					display: inline-block;
+					height: auto;
+					width: 50%;
+				}
+			}
+
+			.item-meta,
+			.list-title {
+				float: none;
+				text-align: center;
+			}
+
+			.list-title {
+				font-size: inherit;
+				line-height: 1.1;
+			}
+
+			.item {
+
+				@include responsive-font( 22 );
+				left: 0;
+				margin: 0 auto;
+				text-align: center;
+				width: 96%;
+
+				.item-block,
+				.group-desc {
+					float: none;
+					width: 96%;
+				}
+
+				.item-block {
+					margin-bottom: $marg-sml;
+				}
+
+				.last-activity {
+					margin-top: 5px;
+				}
+
+				.group-desc {
+					clear: none;
+				}
+
+				.user-update {
+					clear: both;
+					text-align: left;
+				}
+
+				.activity-read-more a {
+					display: inline;
+				}
+			}
+
+			.action {
+				bottom: 5px;
+				float: none;
+				height: auto;
+				left: 0;
+				margin: 0;
+				padding: 0 5px;
+				position: absolute;
+				text-align: center;
+				top: auto;
+				width: 100%;
+
+				.generic-button {
+					float: none;
+					margin: $marg-xsml 0 0;
+					text-align: center;
+					width: 100%;
+
+					a,
+					button {
+						width: 100%;
+					}
+				}
+			}
+
+			.item-avatar,
+			.avatar,
+			.item {
+				float: none;
+			}
+
+		} // close > li
+	} // close .grid.bp-list
+
+
+	// Specific grid layout adjustments by grid row qnt & component
+
+	.blogs-list.grid.two {
+
+		> li {
+
+			.blogs-title {
+				min-height: 5em;
+			}
+		}
+	}
+
+	// where three or four items to a row we need to increase the desc height as
+	// narrow widths force the box taller & adjust some padding values.
+	// In addition blogs grid in narrow theme widths need a little more height.
+
+	.grid.three,
+	.grid.four {
+
+		> li {
+
+			.group-desc {
+				min-height: 8em;
+			}
+		}
+	}
+
+	.blogs-list.grid.three,
+	.blogs-list.grid.four {
+
+		> li {
+			min-height: 350px;
+
+			.last-activity {
+				margin-bottom: 0;
+			}
+
+			.last-post {
+				margin-top: 0;
+			}
+		}
+	}
+
+} // close .buddypress-wrap
+
+// If we're logged out remove additional padding designed to help
+// provide spaces for action button elements & reduce min-heights.
+
+.buddypress:not(.logged-in) {
+
+	.grid.bp-list {
+
+		.list-wrap {
+			padding-bottom: $marg-xsml;
+		}
+	}
+
+	.grid.groups-list {
+
+		.list-wrap {
+			min-height: 430px;
+		}
+	}
+
+	.grid.members-list, {
+
+		.list-wrap {
+			min-height: 300px;
+		}
+	}
+
+	.grid.blogs-list, {
+
+		.list-wrap {
+			min-height: 320px;
+		}
+	}
+}
+
+// ==== Vertical Navigation Classes ====
+
+// These classes added to the BP user navigation elements
+// will provide rules to modify the layout to present the
+// parent object navigation in a vertical column & sub navs
+// horizontally across the item-body
+
+@include medium-up() {
+
+	.bp-single-vert-nav { // described for the #buddypress element
+
+		.bp-navs.vertical {
+			overflow: visible;
+
+			ul {
+				border-right: 1px solid $med-light-grey;
+				border-bottom: 0;
+				float: left;
+				margin-right: -1px;
+				width: 25%;
+			}
+
+			li {
+				float: none;
+				margin-right: 0;
+
+				&.selected a {
+					background: $grey;
+					color: $black;
+				}
+
+				&:focus,
+				&:hover {
+					background: $grey;
+				}
+
+				span {
+					background: $med-light-grey;
+					border-radius: 10%;
+					float: right;
+					margin-right: 2px;
+				}
+
+				&:hover span {
+					border-color: $light-grey;
+				}
+			} // li
+
+		}
+
+		.bp-navs.vertical.tabbed-links {
+
+			li.selected {
+
+				a {
+					padding-left: 0;
+				}
+			}
+		}
+
+		// re-factor the related elements like the #item-body so it sits
+		// to the side(floated)
+		.bp-wrap {
+			margin-bottom: $marg-med;
+
+			.user-nav-tabs.users-nav,
+			.group-nav-tabs.groups-nav {
+
+				ul {
+
+					li {
+						left: 1px;
+						position: relative;
+					}
+				}
+			}
+		}
+
+		.item-body:not(#group-create-body) {
+			background: #fff;
+			border-left: 1px solid $med-light-grey;
+			float: right;
+			margin: 0;
+			min-height: 400px;
+			padding: 0 0 0 $pad-med;
+			width: calc(75% + 1px);
+
+			#subnav:not(.tabbed-links) {
+				background: $light-grey;
+				margin: 0 0 0 -5px;
+				width: auto;
+
+				li {
+
+					@include font-size(16);
+					margin: $marg-sml 0;
+
+					a {
+						border-right: 1px solid $bp-border-dark;
+						padding: 0 $pad-sml;
+					}
+
+					a:focus,
+					a:hover {
+						background: none;
+					}
+				}
+
+				li.current {
+
+					a {
+						background: none;
+						color: $black;
+						text-decoration: underline;
+					}
+				}
+
+				li:last-child {
+
+					a {
+						border: none;
+					}
+				}
+
+			} // close #subnav
+		}
+	}
+
+	// Set the directory screens main navs as vertical aligned
+	// medium breakpoint up only
+
+	.bp-dir-vert-nav { // described for the #buddypress element
+
+		.dir-navs {
+			float: left;
+			left: 1px;
+			position: relative;
+			width: 20%;
+
+			ul {
+
+				li {
+					float: none;
+					overflow: hidden;
+					width: auto;
+
+					&.selected {
+						border: 1px solid #eee;
+
+						a {
+							background: $dark-grey;
+							color: $white;
+
+							span {
+								background: $light-grey;
+								border-color: $grey;
+								color: $highlight;
+							}
+						}
+					} // close .selected
+				}
+
+				li {
+
+					a:hover,
+					a:focus {
+						background: $grey;
+						color: $black;
+
+						span {
+							border: 1px solid $dark-grey;
+						}
+					}
+				}
+
+			}
+		} // close .dir-navs
+
+		.screen-content {
+			border-left: 1px solid $med-light-grey;
+			margin-left: 20%;
+			overflow: hidden;
+			padding: 0 0 $pad-lrg $pad-med;
+
+			.subnav-filters {
+				margin-top: 0;
+			}
+		}
+	}
+
+	// Style main navs as visual tabs effect
+	// if user selects options in the Customizer
+	// and classes are set.
+	// N.B This is in addition to the  standalone visual tab style classes/mixin
+	// provided so any elements may be styled to represent tabs.
+
+	.buddypress-wrap {
+
+		&.bp-vertical-navs {
+
+			.dir-navs.activity-nav-tabs,
+			.dir-navs.sites-nav-tabs,
+			.dir-navs.groups-nav-tabs,
+			.dir-navs.members-nav-tabs,
+			.main-navs.user-nav-tabs,
+			.main-navs.group-nav-tabs {
+
+				@include primary-navs-vert-tabs();
+			}
+		}
+	}
+
+} // close @media
+
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_lists.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_lists.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_lists.scss	(revision 12265)
@@ -0,0 +1,257 @@
+// BP Lists Global Styles.
+// @version 3.0.0
+
+// @todo decide whether using .buddypress quasi namespace parent
+// is useful, causing issue with specificity on other
+// lists if they do not use it.
+
+@include clearfix-element(".bp-list");
+
+.bp-list {
+
+	@include box-model();
+	border-top: 1px solid $light-grey;
+	clear: both;
+	list-style: none;
+	margin: $marg-lrg 0;
+	padding: $pad-sml 0;
+	width: 100%;
+
+	// these clearfix rules simply because the element can't be classed
+	li:before,
+	li:after {
+		content: " ";
+		display: table;
+	}
+
+	li:after {
+		clear: both;
+	}
+
+	> li {
+		border-bottom: 1px solid $light-grey;
+	}
+
+	li {
+		list-style: none;
+		margin: $marg-sml 0;
+		padding: $pad-sml 0;
+		position: relative;
+
+		.item-avatar {
+			text-align: center;
+
+			img.avatar {
+				display: inline;
+			}
+		}
+
+		.item {
+
+			.item-avatar,
+			.list-title,
+			.item-meta,
+			.group-details {
+				text-align: center;
+			}
+
+			.list-title {
+				clear: none; // some WP themes state clear:both
+
+				@include responsive-font(26);
+				font-weight: 400;
+				line-height: 1.1;
+				margin: 0 auto;
+			}
+		}
+
+		.meta,
+		.item-meta {
+			color: $light-text-plus;
+
+			@include font-size(12);
+			margin-bottom: $marg-sml;
+			margin-top: $marg-sml;
+		}
+
+		.last-post {
+			text-align: center;
+		}
+
+		.action {
+			margin: 0;
+			text-align: center;
+
+			.generic-button {
+				display: inline-block;
+
+				@include font-size(12);
+				margin: 0 $marg-sml 0 0;
+			}
+
+			// if this is a div wrapper
+			div.generic-button {
+				margin: $marg-sml 0;
+			}
+		}
+
+		@include medium-up() {
+
+			.item-avatar {
+				float: left;
+				margin-right: 5%;
+			}
+
+			.item {
+				margin: 0;
+				overflow: hidden;
+
+				.item-block { // element is a styling div for positional purposes only
+					float: left;
+					margin-right: 2%;
+					width: 50%;
+				}
+
+				.list-title,
+				.item-meta {
+					float: left;
+					text-align: left;
+				}
+
+				.group-details,
+				.last-post {
+					text-align: left;
+				}
+			}
+
+			.group-desc,
+			.user-update,
+			.last-post {
+				clear: none;
+				overflow: hidden;
+				width: auto;
+			}
+
+			.action {
+				clear: left;
+				padding: 0;
+				text-align: left;
+
+				// if it's a ul/li wrapper
+				li.generic-button {
+					margin-right: 0;
+				}
+
+				// if this is a div wrapper
+				div.generic-button {
+					margin: 0 0 $marg-sml;
+				}
+			}
+
+			.generic-button {
+				display: block;
+				margin: 0  0 $marg-xsml 0;
+			}
+
+		} // close @media
+
+	} // close li
+
+} // close .bp-list
+
+// Ensure there's space between parent act list wrapper and filters bar
+@include medium-small-up() {
+
+	#activity-stream {
+		clear: both;
+		padding-top: $pad-med;
+	}
+}
+
+.activity-list.bp-list {
+
+	background: $off-white;
+	border: 1px solid $bp-border-color;
+
+	.activity-item {
+		background: $white;
+		border: 1px solid #b7b7b7;
+
+		@include box-shadow(0 0 6px #d2d2d2);
+		margin: $marg-lrg 0;
+	}
+
+	li:first-child {
+		margin-top: 0;
+	}
+}
+
+.friends-list {
+	list-style-type: none;
+}
+
+.friends-request-list,
+.membership-requests-list {
+
+	.item-title {
+		text-align: center;
+	}
+
+	@include medium-up() {
+
+		li {
+
+			@include flex-box-dir();
+
+			.item {
+
+				@include box-item-size($grow: 1);
+			}
+
+			.action {
+				text-align: right;
+			}
+
+			.item-title {
+
+				@include font-size(22);
+				text-align: left;
+
+				h3 {
+					margin: 0;
+				}
+			}
+		}
+	}
+
+}
+
+#notifications-user-list {
+	clear: both;
+	padding-top: $pad-med;
+}
+
+// If logged out we don't display action buttons
+// so lets remove the margin right creating the white-space
+// for the buttons - max out the item element width.
+body:not(.logged-in) {
+
+	.bp-list {
+
+		@include medium-up() {
+
+			.item {
+				margin-right: 0;
+			}
+		}
+	}
+}
+
+// body class: single act items screens.
+.activity-permalink {
+
+	.item-list,
+	.item-list li.activity-item {
+		border: 0;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_members_loop.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_members_loop.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_members_loop.scss	(revision 12265)
@@ -0,0 +1,95 @@
+// BP Members loop.
+// @version 3.0.0
+
+.buddypress-wrap {
+
+	.members-list { // ul
+
+		li {
+
+			.member-name {
+				margin-bottom: $marg-sml;
+			}
+
+			.user-update {
+				border: 1px solid $light-grey;
+
+				@include border-radius(10px);
+				color: $meta-text;
+
+				font-style: italic;
+
+				@include responsive-font(16);
+				margin: $marg-med auto;
+				padding: $pad-med;
+
+				.activity-read-more {
+
+					display: block;
+
+					@include font-size(12);
+					font-style: normal;
+					margin-top: $marg-sml;
+					padding-left: 2px;
+				}
+			}
+
+			@include medium-up() {
+
+				.last-activity {
+					clear: left;
+					margin-top: -10px;
+				}
+			}
+
+		} // close li
+
+	} // close .members-list
+
+	// Members group specific list
+	.members-group-list {
+
+		li {
+
+			@include medium-up() {
+
+				.joined {
+					clear: left;
+					float: none;
+				}
+			}
+		}
+	}
+} // close .buddypress-wrap
+
+// If logged out or if the loop item is
+// current user we don't display action buttons
+// so lets remove the update width making room
+// for the buttons - max out the user-update width.
+
+body:not(.logged-in) {
+
+	@include medium-small-up() {
+
+		.members-list {
+
+			.user-update {
+				width: 96%;
+			}
+		}
+	}
+}
+
+// Is current user - i.e it's me!
+
+//@include medium-small-up() {
+//	.buddypress-wrap {
+
+//		.members-list {
+
+//			.is-current-user {
+
+//			}
+//		}
+//	}
+//}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_messages.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_messages.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_messages.scss	(revision 12265)
@@ -0,0 +1,130 @@
+// BP Private Messages:
+// messages lists, message single views, bulk actions
+// @version 3.0.0
+
+#message-threads { // table element
+
+	tr.unread {
+
+		td {
+			background: #fff9db;
+			border-bottom: 1px solid #ffe8c4;
+			border-top: 1px solid #ffe8c4;
+			font-weight: 700;
+
+			&.thread-options,
+			.activity,
+			.thread-excerpt {
+				font-weight: 400;
+			}
+
+		}
+
+		span.unread-count {
+			background: #d00;
+			color: #fff;
+			font-weight: 700;
+			padding: 2px 8px;
+		}
+
+	} // tr.unread
+
+} // close table#message-threads
+
+#message-thread {
+
+	div.message-box {
+		margin: 0;
+		padding: 15px;
+
+	}
+
+	div.alt {
+		background: #f4f4f4;
+	}
+
+	#message-recipients {
+		margin: 10px 0 20px;
+	}
+
+	img.avatar {
+		float: left;
+		margin: 0 10px 0 0;
+		vertical-align: middle;
+	}
+
+	strong {
+		font-size: 100%;
+		margin: 0;
+
+		a {
+			text-decoration: none;
+		}
+
+		span.activity {
+			margin-top: 4px;
+		}
+	} // strong
+
+	.message-metadata {
+		overflow: hidden;
+		position: relative;
+	}
+
+	.message-content {
+		margin-left: 45px;
+	}
+
+	.message-options {
+		text-align: right;
+	}
+
+	img.avatar {
+		max-width: none;
+	}
+
+	.message-search {
+		float: right;
+		margin: 0 20px;
+	}
+
+}
+
+.message-star-actions {
+	position: absolute;
+	right: 0;
+	top: 0;
+}
+
+.message-action-star,
+.message-action-unstar,
+.message-action-view,
+.message-action-delete {
+	border-bottom: 0;
+	outline: none;
+	text-decoration: none;
+}
+
+a.message-action-star {
+	opacity: 0.7;
+}
+
+a.message-action-star:hover {
+	opacity: 1;
+}
+
+.message-action-star span.icon:before,
+.message-action-unstar span.icon:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.message-action-star span.icon:before {
+	color: #aaa;
+	content: "\f154";
+}
+
+.message-action-unstar span.icon:before {
+	color: #fcdd77;
+	content: "\f155";
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_navigation.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_navigation.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_navigation.scss	(revision 12265)
@@ -0,0 +1,528 @@
+// BP primary & secondary navigation - directory and single screens
+// @version 3.0.0
+
+// all devices & generic styles sitewide
+.bp-navs {
+	background: transparent;
+	clear: both;
+	overflow: hidden;
+
+	ul {
+		margin: 0;
+		padding: 0;
+
+		li {
+			list-style: none;
+			margin: 0;
+		}
+
+		li.last {
+
+			select {
+				max-width: 185px;
+			}
+		}
+
+		li {
+
+			a,
+			span {
+				border: 0;
+				display: block;
+				padding: 5px 10px;
+				text-decoration: none;
+			}
+
+			.count {
+				background: $light-grey;
+				border: 1px solid $bp-border-dark;
+				border-radius: 50%;
+				color: $meta-text-dark;
+				display: inline;
+
+				@include font-size(12);
+				margin-left: 2px;
+				padding: 3px 6px;
+				text-align: center;
+				vertical-align: middle;
+			}
+		}
+
+		// these selected/current should apply to all navs
+		// if not pull out & move to end.
+		li.selected,
+		li.current {
+
+			a {
+				color: $black;
+				opacity: 1;
+			}
+		}
+
+	} // close ul
+
+	&.bp-invites-filters,
+	&.bp-messages-filters {
+
+		ul {
+
+			li {
+
+				a {
+					border: 1px solid $bp-border-dark;
+					display: inline-block;
+				}
+			}
+		}
+	}
+
+} // close .bp-navs
+
+.main-navs.dir-navs {
+	margin-bottom: $marg-lrg;
+}
+
+// Primary, default current/selected states
+.buddypress-wrap {
+
+	.bp-navs {
+
+		li.selected,
+		li.current,
+		li a:hover {
+
+			a {
+
+				.count {
+					background-color: $grey;
+				}
+			}
+		}
+
+		li:not(.current),
+		li:not(.selected), {
+
+			a:focus,
+			a:hover {
+				background: $grey;
+				color: $black;
+			}
+		}
+
+		li.selected,
+		li.current {
+
+			a,
+			a:focus,
+			a:hover {
+				background: $dark-grey;
+				color: $off-white;
+			}
+		}
+	}
+
+	@include medium-up() {
+
+		.main-navs:not(.dir-navs) {
+
+			li.selected,
+			li.current {
+
+				a {
+					background: $white;
+					color: $black;
+					font-weight: 600;
+				}
+			}
+		}
+
+		.main-navs.vertical { // single screen navs
+
+			li.selected,
+			li.current {
+
+				a {
+					background: $dark-grey;
+					color: $off-white;
+					text-decoration: none;
+				}
+			}
+		}
+
+		&.bp-dir-hori-nav:not(.bp-vertical-navs) {
+
+			nav:not(.tabbed-links) {
+				border-bottom: 1px solid $bp-border-color;
+				border-top: 1px solid $bp-border-color;
+
+				@include box-shadow( 0 2px 12px 0 $off-white);
+			}
+		}
+	} // close @media
+
+	.bp-subnavs {
+
+		li.selected,
+		li.current {
+
+			a {
+				background: $white;
+				color: $black;
+				font-weight: 600;
+			}
+		}
+	}
+} // close .buddypress-wrap - current & selected states.
+
+// visual styling of default single navs - adds background/padding to
+// the parent elements if vertical nav not selected
+.buddypress-wrap:not(.bp-single-vert-nav) {
+
+	@include medium-max {
+
+		.bp-navs {
+
+			li {
+				background: $light-grey;
+			}
+		}
+	}
+
+	.main-navs {
+
+		> ul > li {
+
+			> a {
+				padding: $pad-sml calc(0.5em + 2px);
+			}
+		}
+	}
+
+	.user-subnav#subsubnav,
+	.group-subnav#subsubnav {
+		background: none;
+	}
+}
+
+// Specifically default subnav elements
+.buddypress-wrap {
+
+	.bp-subnavs,
+	ul.subnav {
+		width: 100%;
+	}
+
+	.bp-subnavs {
+
+		margin: $marg-sml 0;
+		overflow: hidden;
+
+		ul {
+
+			li {
+				margin-top: 0;
+
+				&.selected,
+				&.current {
+
+					:focus,
+					:hover {
+						background: none;
+						color: $black;
+					}
+				}
+
+			}
+		}
+	}
+
+	ul.subnav {
+		width: auto;
+	}
+
+	.bp-navs.bp-invites-nav#subnav,
+	.bp-navs.bp-invites-filters#subsubnav,
+	.bp-navs.bp-messages-filters#subsubnav {
+
+		ul {
+
+			li.last {
+				margin-top: 0;
+			}
+		}
+	}
+
+} // close .buddypress-wrap
+
+// Single screens object navs
+// Adjusts visual styling for small screens only
+
+@include medium-max {
+
+	.buddypress-wrap {
+
+		.single-screen-navs {
+			border: 1px solid $bp-border-color;
+
+			li {
+				border-bottom: 1px solid $bp-border-color;
+
+				&:last-child {
+					border-bottom: none;
+				}
+			}
+		}
+
+		.bp-subnavs {
+
+			li {
+
+				a {
+
+					@include font-size(14);
+				}
+
+				&.selected,
+				&.current {
+
+					a,
+					a:focus,
+					a:hover {
+						background: $dark-grey;
+						color: $white;
+					}
+				}
+			}
+		}
+	}
+}
+
+.buddypress_object_nav,
+.buddypress-wrap {
+
+	.bp-navs {
+
+		li.selected,
+		li.current {
+
+			a {
+
+				.count {
+					background-color: $white;
+				}
+			}
+		} // close li
+
+		li.dynamic,
+		li.dynamic.selected,
+		li.dynamic.current {
+
+			a {
+
+				.count {
+					background-color: $highlight;
+					border: 0;
+					color: $off-white;
+				}
+			}
+		}
+
+		li.dynamic {
+
+			a:hover {
+
+				.count {
+					background-color: $highlight;
+					border: 0;
+					color: $white;
+				}
+			}
+		}
+
+		li {
+
+			a {
+
+				.count:empty {
+					display: none;
+				}
+			}
+		}
+	} // bp-navs
+
+	// Create steps current position tabs highlight
+	.bp-navs.group-create-links {
+
+		ul {
+
+			li:not(.current) {
+				color: $light-text;
+
+				a {
+					color: $light-text;
+
+					&:focus,
+					&:hover {
+						background: none;
+						color: $black;
+					}
+				}
+
+				a[disabled] {
+
+					&:focus,
+					&:hover {
+						color: $light-text;
+					}
+				}
+			}
+
+			li.current {
+
+				a {
+					text-align: center;
+				}
+			}
+		}
+	}
+}
+
+.buddypress-wrap {
+
+	// position our nav elements at larger screen widths
+
+	@include medium-up() {
+
+		.bp-navs {
+
+			li { // set the list links of all navs to shrinkwrap/width auto
+				float: left;
+			}
+		}
+
+		.subnav {
+			float: left;
+		}
+
+		ul.subnav {
+			width: auto;
+		}
+
+		// user screen last filter
+		#subsubnav {
+
+			.activity-search {
+				float: left;
+			}
+
+			.filter {
+				float: right;
+			}
+		}
+
+	} // close @media
+
+} // close .buddypress-wrap
+
+
+// Just buddypress_object_nav rules
+.buddypress_object_nav {
+
+	.bp-navs {
+
+		li {
+
+			a {
+
+				.count {
+					display: inline-block;
+					float: right;
+				}
+			}
+		}
+
+	}
+}
+
+// Directory screens vertical nav rules
+
+@include medium-up() {
+
+	// the top level class activating vert layout
+	.bp-dir-vert-nav {
+
+		.bp-navs.dir-navs {
+			background: none;
+
+			a {
+
+				.count {
+					float: right;
+
+				}
+			}
+		}
+	}
+}
+
+// Tabbed links
+
+// Our tabbed links are pulled in via a mixin
+// UL parent element must have 'tabbed-links' added and the ul 'button-tabs'
+// medium screens up
+
+.buddypress-wrap {
+
+	@include medium-up {
+
+		// Profile group labels links
+		// Button navigation as tabbed effect for wide screen
+
+		@include tabbed-links();
+
+		.bp-navs.tabbed-links {
+			background: none;
+			margin-top: 2px;
+
+			&.main-navs {
+
+			}
+
+			// For tabbed nav we remove any default button nav styles.
+			ul {
+
+				li {
+
+					a {
+						border-right: 0;
+						font-size: inherit;
+					}
+				}
+
+				li.last {
+					float: right;
+					margin: 0;
+
+					a {
+						margin-top: -0.5em;
+					}
+				}
+
+				li,
+				li.current {
+
+					a,
+					a:focus,
+					a:hover {
+						background: none;
+						border: 0;
+					}
+
+					a:active {
+						outline: none;
+					}
+				}
+			}
+		} // close .bp-navs.tabbed-links
+	} // @media
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_pagination.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_pagination.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_pagination.scss	(revision 12265)
@@ -0,0 +1,47 @@
+// BP pagination  - generic styling for all screens
+// @version 3.0.0
+.buddypress-wrap {
+
+	.bp-pagination {
+
+		background: transparent;
+		border: 0;
+		color: $light-text;
+		float: left;
+		font-size: small;
+		margin: 0;
+		padding: $pad-sml 0;
+		position: relative;
+		width: 100%;
+
+		.pag-count {
+			float: left;
+		}
+
+		.bp-pagination-links {
+			float: right;
+			margin-right: $marg-sml;
+
+			span,
+			a {
+				font-size: small;
+				padding: 0 5px;
+			}
+
+		}
+
+		.bp-pagination-links {
+
+			a:focus,
+			a:hover {
+				opacity: 1;
+			}
+		}
+
+		p {
+			margin: 0;
+		}
+
+	} // close .bp-pagination
+
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_registration.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_registration.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_registration.scss	(revision 12265)
@@ -0,0 +1,118 @@
+// Registration Screen
+// Styles the registration index.php form and elements.
+// @version 3.0.0
+
+.register-page {
+
+	.register-section {
+
+		@include box-model();
+	}
+
+	.signup-form {
+		margin-top: $marg-lrg;
+
+		.default-profile {
+
+			input {
+				margin-bottom: $marg-lrg;
+			}
+		}
+
+		label,
+		legend {
+			margin: $marg-sml 0 0;
+		}
+
+		// profile extra element wrapper
+		.editfield {
+			margin: $marg-med 0;
+
+			fieldset {
+				border: 0;
+				padding: 0;
+
+				legend {
+					margin: 0 0 5px;
+					text-indent: 0;
+				}
+			}
+
+			.field-visibility-settings {
+				padding: $pad-sml;
+
+				fieldset {
+					margin: 0 0 $marg-sml;
+				}
+			}
+		} // editfield
+
+		#signup-avatar img {
+			margin: 0 $marg-med $marg-sml 0;
+		}
+
+		.password-entry,
+		.password-entry-confirm {
+			border: 1px solid $bp-border-color;
+		}
+	} // close .signup-form
+
+} // close .register-page
+
+// Flex layout containers for registration sections
+
+@include medium-up() {
+
+	.buddypress-wrap {
+
+		.register-page {
+
+			.layout-wrap {
+				display: flex;
+				flex-flow: row wrap;
+				justify-content: space-around;
+
+				.default-profile {
+					flex: 1;
+					padding-right: $pad-lrg;
+				}
+
+				.blog-details {
+					flex: 1;
+					padding-left: $pad-lrg;
+				}
+			}
+
+			.submit {
+				clear: both;
+			}
+
+		}
+	}
+}
+
+// have we got extended profiles to factor in,
+// if so we'll adjust the child item elements.
+
+@include medium-up() {
+
+	.buddypress-wrap.extended-default-reg {
+
+		.register-page {
+
+			.default-profile {
+				flex: 1;
+				padding-right: $pad-med;
+			}
+
+			.extended-profile {
+				flex: 2;
+				padding-left: $pad-med;
+			}
+
+			.blog-details {
+				flex: 1 100%;
+			}
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_search.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_search.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_search.scss	(revision 12265)
@@ -0,0 +1,131 @@
+// BP Directory Search elements
+// @version 3.0.0
+
+.buddypress-wrap { // the ID is required to add weight to rules
+
+	@include clearfix-element(".bp-dir-search-form, .bp-messages-search-form");
+
+	form.bp-dir-search-form,
+	form.bp-messages-search-form,
+	form.bp-invites-search-form {
+		border: 1px solid $bp-border-color;
+
+		width: 100%;
+
+		@include medium-lrg-up() {
+			width: 15em;
+		}
+
+		label {
+			margin: 0;
+		}
+
+		input[type="search"],
+		input[type="text"],
+		button[type="submit"] {
+			background: none;
+			border: 0;
+
+			@include border-radius(0);
+		}
+
+		input[type="search"],
+		input[type="text"] {
+			float: left;
+			line-height: 1.5;
+			padding: 3px 10px;
+			width: 80%;
+		}
+
+		button[type="submit"] {
+			float: right;
+			font-size: inherit;
+			font-weight: 400;
+			line-height: 1.5;
+			padding: 3px 0.7em;
+			text-align: center;
+			text-transform: none;
+			width: 20%;
+
+			span {
+				font-family: dashicons;
+
+				@include font-size(18);
+				line-height: 1.6;
+			}
+		}
+
+		button[type="submit"].bp-show {
+
+			@include show();
+		}
+
+		input[type="search"]::-webkit-search-cancel-button {
+			-webkit-appearance: searchfield-cancel-button;
+		}
+
+		input[type="search"]::-webkit-search-results-button,
+		input[type="search"]::-webkit-search-results-decoration {
+			display: none;
+		}
+
+	} // close form
+
+	// this block needs to be moved really.
+	ul.filters {
+
+		li {
+
+			form {
+
+				label {
+
+					input {
+						line-height: 1.4;
+						padding: 0.1em 0.7em;
+					}
+				}
+			}
+		}
+	}
+
+	.current-member-type {
+		font-style: italic;
+	}
+
+	.dir-form {
+		clear: both;
+	}
+
+} // close .buddypress-wrap
+
+// If js disabled ensure we show the submit overriding earlier rule
+// @todo the whole show/hide could be wrapped in a :not(.no-js)
+.budypress.no-js {
+
+	form.bp-dir-search-form {
+
+		button[type="submit"] {
+
+			@include show();
+		}
+	}
+}
+
+// Try and apply correct tweaks for group/user screens search
+
+.bp-user {
+
+	[data-bp-search] {
+
+		form {
+
+			input[type="search"],
+			input[type="text"] {
+				padding: 6px 10px 7px;
+			}
+
+		}
+	}
+}
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_tables.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_tables.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_tables.scss	(revision 12265)
@@ -0,0 +1,199 @@
+// BP default/generic Table styles.
+// @version 3.0.0
+.buddypress-wrap {
+
+	.bp-tables-user,
+	table.wp-profile-fields,
+	table.forum {
+		width: 100%;
+	}
+
+	.bp-tables-user,
+	table.wp-profile-fields,
+	table.forum {
+
+		thead {
+
+			tr {
+				background: none;
+				border-bottom: 2px solid $bp-border-dark;
+			}
+		}
+
+		tbody {
+
+			tr {
+				background: $off-white;
+			}
+		}
+
+	}
+
+	.bp-tables-user,
+	table.wp-profile-fields,
+	table.forum {
+
+		tr {
+
+			th,
+			td {
+				padding: $pad-sml;
+				vertical-align: middle;
+			}
+		}
+	}
+
+	.bp-tables-user,
+	table.wp-profile-fields,
+	table.forum {
+
+		tr {
+
+			td.label {
+				border-right: 1px solid $light-grey;
+				font-weight: 600;
+				width: 25%;
+			}
+		}
+	}
+
+	.bp-tables-user,
+	table.wp-profile-fields, {
+
+		tr.alt {
+
+			td {
+				background: $off-white;
+			}
+		}
+	}
+
+
+	table.profile-fields {
+
+		.data {
+			padding: $pad-sml $pad-med;
+		}
+
+		tr:last-child {
+			border-bottom: none;
+		}
+	}
+
+	// Set various tables th/td cell sizes
+	table.notifications {
+
+		// Increase the cell padding to give better separation of lines
+		// when some text lines wrap
+
+		td {
+			padding: $pad-med $pad-sml;
+		}
+
+		.bulk-select-all,
+		.bulk-select-check {
+			width: 7%;
+		}
+
+		.bulk-select-check {
+			vertical-align: middle;
+		}
+
+		.title,
+		.notification-description,
+		.date,
+		.notification-since {
+			width: 39%;
+		}
+
+		.actions,
+		.notification-actions {
+			width: 15%;
+		}
+	}
+
+	table.notification-settings,
+	table.profile-settings {
+
+		th.title {
+			width: 80%;
+		}
+	}
+	// end cell sizings
+
+	table.notifications {
+
+		.notification-actions {
+
+			a.delete,
+			a.mark-read {
+				display: inline-block;
+			}
+		}
+	}
+
+	table.notification-settings {
+		margin-bottom: $marg-med;
+		text-align: left;
+	}
+
+	#groups-notification-settings {
+		margin-bottom: 0;
+	}
+
+	table.notifications,
+	table.notification-settings {
+
+		th.icon,
+		td:first-child {
+			display: none;
+		}
+	}
+
+	table.notification-settings {
+
+		.no,
+		.yes {
+			text-align: center;
+			width: 40px;
+			vertical-align: middle;
+		}
+	}
+
+	table#message-threads {
+		clear: both;
+
+		.thread-info {
+			min-width: 40%;
+
+			p {
+				margin: 0;
+			}
+
+			p.thread-excerpt {
+				color: $form-text;
+
+				@include font-size(12);
+				margin-top: 3px;
+			}
+		}
+	}
+
+	table.profile-fields {
+		margin-bottom: 20px;
+
+		&:last-child {
+			margin-bottom: 0;
+		}
+	}
+
+	table.profile-fields p {
+		margin: 0;
+
+		&:last-child {
+			margin-top: 0;
+		}
+	}
+
+} // close .buddypress-wrap
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_tooltips.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_tooltips.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_tooltips.scss	(revision 12265)
@@ -0,0 +1,113 @@
+// BuddyPress Tooltips
+// @version 4.0.0
+
+[data-bp-tooltip] {
+	position: relative;
+
+	// Removed :before
+	&:after {
+		background-color: $tooltip-background;
+		display: none;
+		opacity: 0;
+		position: absolute;
+		-webkit-transform: translate3d(0, 0, 0);
+		-ms-transform: translate3d(0, 0, 0);
+		transform: translate3d(0, 0, 0);
+		visibility: hidden;
+	}
+
+	// BP Tooltip body and text
+	&:after {
+		border: 1px solid $tooltip-border;
+		border-radius: $tooltip-border-radius;
+		box-shadow: $tooltip-box-shadow;
+		color: $tooltip-text-color;
+		content: attr(data-bp-tooltip);
+		font-family: $tooltip-font-family;
+		font-size: $tooltip-font-size;
+		font-weight: $tooltip-font-weight;
+		letter-spacing: normal;
+		line-height: 1.25;
+		max-width: $tooltip-max-width;
+		padding: $tooltip-padding-vert $tooltip-padding-hor;
+		pointer-events: none;
+		text-shadow: none;
+		text-transform: none;
+		-webkit-transition: all 1.5s ease;
+		-ms-transition: all 1.5s ease;
+		transition: all 1.5s ease;
+		white-space: nowrap;
+		word-wrap: break-word;
+		z-index: $tooltip-z-index;
+	}
+
+	// BP Tooltip arrow tip - removed :before
+
+	&:hover,
+	&:active,
+	&:focus {
+
+		// Removed :before
+		&:after {
+
+			display: block;
+			opacity: 1;
+			overflow: visible;
+			visibility: visible;
+		}
+	}
+}
+
+[data-bp-tooltip=""] {
+	display: none;
+	opacity: 0;
+	visibility: hidden;
+}
+
+// Bottom Centered Tooltip - Default
+
+.bp-tooltip {
+
+	@include bp-tooltip-default;
+}
+
+// Bottom Left Tooltip for mobile and Bottom Right Tooltip for tablet/desktop
+
+.user-list .bp-tooltip {
+
+	@include bp-tooltip-bottom-left;
+
+	@include medium-up() {
+
+		@include bp-tooltip-bottom-right;
+	}
+}
+
+// Bottom Left Tooltip
+
+.activity-list .bp-tooltip,
+.activity-meta-action .bp-tooltip,
+.avatar-block .item-avatar .bp-tooltip,
+.notification-actions .bp-tooltip,
+.participants-list .bp-tooltip {
+
+	@include bp-tooltip-bottom-left;
+}
+
+// Bottom Right Tooltip
+
+.bp-invites-content .bp-tooltip,
+.message-metadata .actions .bp-tooltip,
+.single-message-thread-header .actions .bp-tooltip {
+
+	@include bp-tooltip-bottom-right;
+}
+
+.bp-invites-content #send-invites-editor .bp-tooltip {
+
+	// override .bp-invites-content .bp-tooltip
+	&:after {
+		left: 0;
+		right: auto;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_update_form.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_update_form.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_update_form.scss	(revision 12265)
@@ -0,0 +1,149 @@
+// BP Whats new form handles user updates to groups or profiles
+// @version 3.0.0
+
+.activity-update-form {
+	padding: 10px 10px 0;
+}
+
+.item-body {
+
+	.activity-update-form {
+
+		.activity-form {
+			margin: 0;
+			padding: 0;
+		}
+	}
+}
+
+.activity-update-form {
+	border: 1px solid $bp-border-dark;
+
+	@include box-shadow( inset 0 0 6px #eee);
+
+	margin: $marg-med 0;
+
+	#whats-new-avatar {
+		margin: $marg-sml 0;
+		text-align: center;
+
+		img {
+			box-shadow: none;
+			display: inline-block;
+		}
+	}
+
+	// these ID's need replacing with this!
+	//.content-wrap { // #whats-new-content
+
+	#whats-new-content {
+		padding: 0 0 $marg-lrg 0;
+	}
+
+	// these ID's need replacing with this!
+	//.textarea-wrap { // #whats-new-textarea
+
+	#whats-new-textarea {
+
+		textarea {
+			background: $textarea-bck;
+
+			@include box-model();
+			color: #333;
+			font-family: inherit;
+			font-size: medium;
+			height: 2.2em;
+			line-height: 1.4;
+			padding: 6px;
+			width: 100%;
+
+			&:focus {
+
+				@include box-shadow(0 0 6px 0 $med-light-grey);
+			}
+		}
+	}
+
+	#whats-new-post-in-box {
+		margin: $marg-sml 0;
+
+		#whats-new-post-in-box-items {
+			list-style: none;
+			margin: $marg-sml 0;
+
+			li {
+				margin-bottom: $marg-sml;
+			}
+
+			#activity-autocomplete {
+				padding: 0.3em;
+			}
+
+			.bp-activity-object {
+
+				@include center-vert();
+				padding: 0.2em;
+
+				.avatar {
+					width: 30px;
+				}
+
+				span {
+					padding-left: $marg-sml;
+					vertical-align: middle;
+				}
+
+				&:focus,
+				&:hover {
+					background: $light-grey;
+					cursor: pointer;
+				}
+
+				&.selected {
+					border: 1px solid $med-light-grey;
+				}
+			}
+		}
+
+	}
+
+	#whats-new-submit {
+		margin: $marg-med 0 $marg-sml;
+
+		input {
+
+			@include font-size(14);
+			line-height: inherit;
+			margin-bottom: $marg-sml;
+			margin-right: $marg-sml;
+			padding: 0.2em 0;
+			text-align: center;
+			width: 100%;
+		}
+	}
+
+	@include medium-up() {
+
+		#whats-new-avatar {
+			display: block;
+			float: left;
+			margin: 0;
+		}
+
+		#whats-new-content,
+		#whats-new-post-in-box,
+		#whats-new-submit {
+			margin-left: 8.5%;
+
+		}
+
+		#whats-new-submit {
+
+			input {
+				margin-bottom: 0;
+				margin-right: $marg-sml;
+				width: 8em;
+			}
+		}
+	} // close @media
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_header.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_header.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_header.scss	(revision 12265)
@@ -0,0 +1,20 @@
+// BP Single User Specific Header Styles
+// @version 3.0.0
+
+.bp-user {
+
+	.users-header {
+
+		.user-nicename {
+			margin-bottom: $marg-xsml;
+		}
+	}
+
+	.member-header-actions {
+		overflow: hidden;
+
+		* > * {
+			display: block;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_profile.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_profile.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_profile.scss	(revision 12265)
@@ -0,0 +1,238 @@
+// BP User Profiles screen
+// @version 3.0.0
+//
+// 0/ General :)
+// 1/ Profile Edit
+// 2/ Profile Public Display
+// 3/ Profile Photo
+
+// General
+
+.buddypress-wrap {
+
+	.profile {
+		margin-top: $marg-xlrg;
+	}
+
+	.public {
+
+		.profile-fields {
+
+			td.label {
+				width: 30%;
+			}
+		}
+	}
+
+}
+
+// Edit Screen
+
+.buddypress-wrap {
+
+	.profile.edit {
+
+		// The groups tabs
+		.button-nav {
+			list-style: none;
+			margin: $marg-xlrg 0 $marg-sml;
+
+			li {
+				display: inline-block;
+				margin-right: $marg-sml;
+
+				a {
+
+					@include font-size(18);
+				}
+			}
+		}
+
+		.editfield {
+			background: $off-white;
+			border: 1px solid $bp-border-color;
+			margin: $marg-med 0;
+			padding: $pad-med;
+
+			fieldset {
+				border: 0;
+
+				label {
+					font-weight: 400;
+
+					&.xprofile-field-label {
+						display: inline;
+					}
+				}
+			}
+		}
+
+		// The visual layout of the form controls and group description
+		.editfield {
+			display: flex;
+			flex-direction: column;
+
+			.description {
+				margin-top: $marg-sml;
+				order: 2;
+			}
+
+			> fieldset {
+				order: 1;
+			}
+
+			.field-visibility-settings-toggle,
+			.field-visibility-settings {
+				order: 3;
+			}
+		}
+
+	} // .profile
+} // close .buddypress-wrap
+
+body.no-js {
+
+	.buddypress-wrap .field-visibility-settings-toggle,
+	.buddypress-wrap .field-visibility-settings-close {
+		display: none;
+	}
+
+	.buddypress-wrap .field-visibility-settings {
+		display: block;
+	}
+}
+
+.buddypress-wrap {
+
+	.field-visibility-settings {
+		margin: $marg-sml 0;
+	}
+
+	.current-visibility-level {
+		font-style: normal;
+		font-weight: 700;
+	}
+
+	.field-visibility-settings,
+	.field-visibility-settings-header {
+		color: $light-text-plus;
+	}
+
+	.field-visibility-settings {
+
+		fieldset {
+			margin: $marg-xsml 0;
+		}
+	}
+
+	.standard-form { // this needs to be a specific profile edit form class
+
+		.editfield {
+
+			fieldset {
+				margin: 0;
+			}
+		}
+
+		.field-visibility-settings {
+
+			label {
+				font-weight: 400;
+				margin: 0;
+			}
+
+			.radio {
+				list-style: none;
+				margin-bottom: 0;
+			}
+
+			.field-visibility-settings-close {
+
+				@include font-size(12);
+			}
+		}
+
+		.wp-editor-container {
+			border: 1px solid #dedede;
+
+			textarea {
+				background: $white;
+				width: 100%;
+			}
+		}
+
+		// field item description
+		.description {
+			background: $off-white;
+			font-size: inherit;
+		}
+
+		.field-visibility-settings legend,
+		.field-visibility-settings-header {
+			font-style: italic;
+		}
+
+		.field-visibility-settings-header {
+
+			@include font-size(14);
+		}
+
+		.field-visibility-settings {
+
+			legend,
+			label {
+
+				@include font-size(14);
+			}
+		}
+
+		.field-visibility select {
+			margin: 0;
+		}
+
+	} // close .standard-form
+
+	.html-active button.switch-html {
+		background: #f5f5f5;
+		border-bottom-color: transparent;
+		border-bottom-left-radius: 0;
+		border-bottom-right-radius: 0;
+	}
+
+	.tmce-active button.switch-tmce {
+		background: #f5f5f5;
+		border-bottom-color: transparent;
+		border-bottom-left-radius: 0;
+		border-bottom-right-radius: 0;
+	}
+
+} // close .buddypress-wrap
+
+// Public Profile
+
+.buddypress-wrap {
+
+	.profile.public {
+
+		.profile-group-title {
+			border-bottom: 1px solid $bp-border-dark;
+		}
+	}
+}
+
+// This needs to be reviewed and re-located!?
+body.register .buddypress-wrap .page ul {
+	list-style: none;
+}
+
+// Avatars & Cover Image
+
+.buddypress-wrap {
+
+	.profile {
+
+		.bp-avatar-nav {
+			margin-top: $marg-lrg;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_settings.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_settings.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_user_settings.scss	(revision 12265)
@@ -0,0 +1,43 @@
+// BP User settings screens
+// @version 3.0.0
+
+// 1. Settings Global
+// 1. General (email/password change)
+// 2. Email notifications
+// 3. Profile visibility
+// 4. group Invites
+
+// Settings Global
+
+/*__ Settings Global __*/
+
+// required extra specificity to override later table settings
+.buddypress.settings {
+
+	.profile-settings.bp-tables-user {
+
+		select {
+			width: 100%;
+		}
+	}
+
+}
+
+
+// General
+
+/*__ General __*/
+
+
+// Email notifications
+
+/*__ Email notifications __*/
+
+
+// Profile visibility
+
+/*__ Profile visibility __*/
+
+// Group Invites
+
+/*__ Group Invites __*/
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_widgets.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_widgets.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/common-styles/_bp_widgets.scss	(revision 12265)
@@ -0,0 +1,155 @@
+// BP Widgets
+// @version 4.0.0
+
+.buddypress.widget {
+
+	.item-options {
+
+		@include font-size(12);
+
+		margin: 0 0 1em;
+		padding: 1em 0;
+	}
+
+	.bp-login-widget-user-avatar {
+		float: left;
+	}
+
+	.bp-login-widget-user-links {
+		margin-left: 70px;
+	}
+
+	ul.item-list {
+		list-style: none;
+		margin: $marg-sml 0;
+	}
+
+	ul.activity-list {
+
+		padding: 0;
+
+		blockquote {
+			margin: 0 0 1.5em;
+			overflow: visible;
+			padding: 0 0 0.75em 0.75em;
+		}
+
+		img {
+			margin-bottom: 0.5em;
+		}
+
+		li {
+			border-bottom: 1px solid $primary-grey;
+			margin-bottom: 1em;
+
+			.activity-header p {
+				margin-bottom: 0.5em;
+			}
+
+			&:last-child {
+				border-bottom: 0;
+			}
+		}
+	}
+
+	.avatar-block {
+
+		@include flex-box-dir($flex-dir: row, $flex-wrap: wrap);
+
+		img {
+			margin: 0.5em;
+		}
+	}
+
+	ul#friends-list,
+	ul#groups-list,
+	ul#members-list {
+
+		@include clearfix-element("li");
+
+		li {
+
+			margin-bottom: 1em;
+
+			.item-avatar {
+				float: left;
+				width: 60px;
+			}
+
+			.item {
+				margin-left: 70px;
+			}
+		}
+	}
+}
+
+// BP Widgets in Main content area.
+// User's and Group's widgetized default home pages.
+
+.buddypress-wrap {
+
+	.buddypress.widget {
+
+		ul#friends-list,
+		ul#groups-list,
+		ul#members-list {
+
+			@include flex-box-dir($flex-dir: column);
+
+			@include medium-small-up() {
+
+				@include flex-box-dir($flex-dir: row, $flex-wrap: wrap);
+			}
+
+
+			li {
+				border: 1px solid $bp-border-color;
+
+				@include flex-align();
+				@include box-item-size($grow: 1, $basis: 46%);
+
+				@include large-up() {
+
+					@include box-item-size($grow: 0, $basis: 20%);
+				}
+
+				margin: 2%;
+
+				.item-avatar {
+					padding: $pad-sml;
+					text-align: center;
+				}
+
+				.item {
+					padding: $pad-sml;
+
+
+					.item-meta {
+
+						@include font-size(12);
+						overflow-wrap: break-word;
+					}
+				}
+			} // close li
+
+			@include large-up() {
+
+				margin: $marg-sml -2%;
+				width: 100%;
+
+				li {
+
+					@include box-item-size($grow: 0, $basis: auto);
+					margin: $marg-sml 2% 1%;
+					width: 46%;
+				}
+			}
+		}
+
+		blockquote {
+			margin: 0 0 1.5em;
+			overflow: visible;
+			padding: 0 0 0.75em 0.75em;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/buddypress-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/buddypress-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/buddypress-rtl.css	(revision 12265)
@@ -0,0 +1,5176 @@
+/*--------------------------------------------------------------
+Hello, this is the BuddyPress Nouveau stylesheet.
+
+@version 4.0.0
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 - BP Generic, Typography & Imagery
+
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+
+3.0 - BP Lists / Loops Generic & filters
+	3.1 - Activity Loop
+		3.1.1 Whats New Activity
+		3.1.2 - Activity Entries
+		3.1.3 - Activity Comments
+	3.2 - Blogs Loop
+	3.3 - Groups Loop
+	3.4 - Members Loop
+
+4.0 - Directories - Members, Groups, Blogs, Register, Activation
+	4.1 - Groups Creation Steps Screens
+5.0 - Single Item screens: User Account & Single Group Screens
+	5.1 - Item Headers: Global
+		5.1.1 - item-header: Groups
+		5.1.2 - item-header: User Accounts
+	5.2 - Item Body: Global
+		5.2.1 - item-body: Groups
+			5.2.1.1 - Management settings screens
+			5.2.1.2 - Group Members list
+			5.2.1.3 - Group Invite list
+			5.2.1.4 - Group Activity
+		5.2.2 - item-body: User Accounts
+			5.2.2.1 - classes, pag, filters
+			5.2.2.2 - Extended Profiles
+			5.2.2.3 - Groups
+			5.2.2.4 - friends
+			5.2.2.5 - Private Messaging Threads
+			5.2.2.6 - Settings
+
+6.0 - Forms  -  General
+	6.1 - Dir Search
+
+7.0 - Tables -  General
+
+8.0 - Classes - Messages, Ajax, Widgets, Buttons, Tooltips
+
+9.0 - Layout Classes.
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - BP Generic, Typography & Imagery
+*-------------------------------------------------------------------------------
+*/
+body #buddypress * a {
+	box-shadow: none;
+	text-decoration: none;
+}
+
+body #buddypress #item-body blockquote,
+body #buddypress .bp-lists blockquote {
+	margin-right: 0;
+}
+
+body #buddypress .bp-list .action {
+	box-sizing: border-box;
+}
+
+@media screen and (min-width: 46.8em) {
+	body.buddypress .entry-header,
+	body.buddypress .site-content .entry-header,
+	body.buddypress .entry-content {
+		max-width: none;
+	}
+	body.buddypress .entry-header {
+		float: none;
+		max-width: none;
+	}
+	body.buddypress .entry-content {
+		float: none;
+		max-width: none;
+	}
+	body.buddypress .site-content {
+		padding-top: 2.5em;
+	}
+	body.buddypress #page #primary {
+		max-width: none;
+	}
+	body.buddypress #page #primary .entry-header,
+	body.buddypress #page #primary .entry-content {
+		float: none;
+		width: auto;
+	}
+}
+
+body.buddypress .buddypress-wrap h1,
+body.buddypress .buddypress-wrap h2,
+body.buddypress .buddypress-wrap h3,
+body.buddypress .buddypress-wrap h4,
+body.buddypress .buddypress-wrap h5,
+body.buddypress .buddypress-wrap h6 {
+	clear: none;
+	margin: 1em 0;
+	padding: 0;
+}
+
+/* Ensure .bp-wrap encloses it's children */
+.bp-wrap:before,
+.bp-wrap:after {
+	content: " ";
+	display: table;
+}
+
+.bp-wrap:after {
+	clear: both;
+}
+
+.buddypress-wrap.round-avatars .avatar {
+	border-radius: 50%;
+}
+
+div,
+dl,
+li,
+textarea,
+select,
+input[type="search"],
+input[type="submit"],
+input[type="reset"] {
+	border-radius: 2px;
+	background-clip: padding-box;
+}
+
+body.buddypress article.page > .entry-header {
+	margin-bottom: 2em;
+	padding: 0;
+}
+
+body.buddypress article.page > .entry-header .entry-title {
+	font-size: 28px;
+	font-weight: inherit;
+	color: #767676;
+}
+
+@media screen and (min-width: 46.8em) {
+	body.buddypress article.page > .entry-header .entry-title {
+		font-size: 34px;
+	}
+}
+
+.buddypress-wrap dt.section-title {
+	font-size: 18px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap dt.section-title {
+		font-size: 22px;
+	}
+}
+
+.buddypress-wrap .bp-label-text,
+.buddypress-wrap .message-threads {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-label-text,
+	.buddypress-wrap .message-threads {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .activity-header {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .activity-header {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .activity-inner {
+	font-size: 15px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .activity-inner {
+		font-size: 18px;
+	}
+}
+
+.buddypress-wrap #whats-new-post-in {
+	font-size: 16px;
+}
+
+.buddypress-wrap .mini .activity-header,
+.buddypress-wrap .acomment-meta {
+	font-size: 16px;
+}
+
+.buddypress-wrap .dir-component-filters #activity-filter-by {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .dir-component-filters #activity-filter-by {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .bp-tables-user th {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-tables-user th {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .bp-tables-user td {
+	font-size: 12px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-tables-user td {
+		font-size: 14px;
+	}
+}
+
+.buddypress-wrap .profile-fields th {
+	font-size: 15px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .profile-fields th {
+		font-size: 18px;
+	}
+}
+
+.buddypress-wrap .profile-fields td {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .profile-fields td {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap #notification-select {
+	font-size: 12px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap #notification-select {
+		font-size: 14px;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (bp-list)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+.bp-navs {
+	background: transparent;
+	clear: both;
+	overflow: hidden;
+}
+
+.bp-navs ul {
+	margin: 0;
+	padding: 0;
+}
+
+.bp-navs ul li {
+	list-style: none;
+	margin: 0;
+}
+
+.bp-navs ul li.last select {
+	max-width: 185px;
+}
+
+.bp-navs ul li a,
+.bp-navs ul li span {
+	border: 0;
+	display: block;
+	padding: 5px 10px;
+	text-decoration: none;
+}
+
+.bp-navs ul li .count {
+	background: #eaeaea;
+	border: 1px solid #ccc;
+	border-radius: 50%;
+	color: #555;
+	display: inline;
+	font-size: 12px;
+	margin-right: 2px;
+	padding: 3px 6px;
+	text-align: center;
+	vertical-align: middle;
+}
+
+.bp-navs ul li.selected a,
+.bp-navs ul li.current a {
+	color: #333;
+	opacity: 1;
+}
+
+.bp-navs.bp-invites-filters ul li a, .bp-navs.bp-messages-filters ul li a {
+	border: 1px solid #ccc;
+	display: inline-block;
+}
+
+.main-navs.dir-navs {
+	margin-bottom: 20px;
+}
+
+.buddypress-wrap .bp-navs li.selected a .count,
+.buddypress-wrap .bp-navs li.current a .count,
+.buddypress-wrap .bp-navs li a:hover a .count {
+	background-color: #ccc;
+}
+
+.buddypress-wrap .bp-navs li:not(.current) a:focus,
+.buddypress-wrap .bp-navs li:not(.current) a:hover,
+.buddypress-wrap .bp-navs li:not(.selected) a:focus,
+.buddypress-wrap .bp-navs li:not(.selected) a:hover {
+	background: #ccc;
+	color: #333;
+}
+
+.buddypress-wrap .bp-navs li.selected a,
+.buddypress-wrap .bp-navs li.selected a:focus,
+.buddypress-wrap .bp-navs li.selected a:hover,
+.buddypress-wrap .bp-navs li.current a,
+.buddypress-wrap .bp-navs li.current a:focus,
+.buddypress-wrap .bp-navs li.current a:hover {
+	background: #555;
+	color: #fafafa;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .main-navs:not(.dir-navs) li.selected a,
+	.buddypress-wrap .main-navs:not(.dir-navs) li.current a {
+		background: #fff;
+		color: #333;
+		font-weight: 600;
+	}
+	.buddypress-wrap .main-navs.vertical li.selected a,
+	.buddypress-wrap .main-navs.vertical li.current a {
+		background: #555;
+		color: #fafafa;
+		text-decoration: none;
+	}
+	.buddypress-wrap.bp-dir-hori-nav:not(.bp-vertical-navs) nav:not(.tabbed-links) {
+		border-bottom: 1px solid #eee;
+		border-top: 1px solid #eee;
+		box-shadow: 0 2px 12px 0 #fafafa;
+	}
+}
+
+.buddypress-wrap .bp-subnavs li.selected a,
+.buddypress-wrap .bp-subnavs li.current a {
+	background: #fff;
+	color: #333;
+	font-weight: 600;
+}
+
+@media screen and (max-width: 46.8em) {
+	.buddypress-wrap:not(.bp-single-vert-nav) .bp-navs li {
+		background: #eaeaea;
+	}
+}
+
+.buddypress-wrap:not(.bp-single-vert-nav) .main-navs > ul > li > a {
+	padding: 0.5em calc(0.5em + 2px);
+}
+
+.buddypress-wrap:not(.bp-single-vert-nav) .user-subnav#subsubnav,
+.buddypress-wrap:not(.bp-single-vert-nav) .group-subnav#subsubnav {
+	background: none;
+}
+
+.buddypress-wrap .bp-subnavs,
+.buddypress-wrap ul.subnav {
+	width: 100%;
+}
+
+.buddypress-wrap .bp-subnavs {
+	margin: 10px 0;
+	overflow: hidden;
+}
+
+.buddypress-wrap .bp-subnavs ul li {
+	margin-top: 0;
+}
+
+.buddypress-wrap .bp-subnavs ul li.selected :focus,
+.buddypress-wrap .bp-subnavs ul li.selected :hover, .buddypress-wrap .bp-subnavs ul li.current :focus,
+.buddypress-wrap .bp-subnavs ul li.current :hover {
+	background: none;
+	color: #333;
+}
+
+.buddypress-wrap ul.subnav {
+	width: auto;
+}
+
+.buddypress-wrap .bp-navs.bp-invites-nav#subnav ul li.last,
+.buddypress-wrap .bp-navs.bp-invites-filters#subsubnav ul li.last,
+.buddypress-wrap .bp-navs.bp-messages-filters#subsubnav ul li.last {
+	margin-top: 0;
+}
+
+@media screen and (max-width: 46.8em) {
+	.buddypress-wrap .single-screen-navs {
+		border: 1px solid #eee;
+	}
+	.buddypress-wrap .single-screen-navs li {
+		border-bottom: 1px solid #eee;
+	}
+	.buddypress-wrap .single-screen-navs li:last-child {
+		border-bottom: none;
+	}
+	.buddypress-wrap .bp-subnavs li a {
+		font-size: 14px;
+	}
+	.buddypress-wrap .bp-subnavs li.selected a,
+	.buddypress-wrap .bp-subnavs li.selected a:focus,
+	.buddypress-wrap .bp-subnavs li.selected a:hover, .buddypress-wrap .bp-subnavs li.current a,
+	.buddypress-wrap .bp-subnavs li.current a:focus,
+	.buddypress-wrap .bp-subnavs li.current a:hover {
+		background: #555;
+		color: #fff;
+	}
+}
+
+.buddypress_object_nav .bp-navs li.selected a .count,
+.buddypress_object_nav .bp-navs li.current a .count,
+.buddypress-wrap .bp-navs li.selected a .count,
+.buddypress-wrap .bp-navs li.current a .count {
+	background-color: #fff;
+}
+
+.buddypress_object_nav .bp-navs li.dynamic a .count,
+.buddypress_object_nav .bp-navs li.dynamic.selected a .count,
+.buddypress_object_nav .bp-navs li.dynamic.current a .count,
+.buddypress-wrap .bp-navs li.dynamic a .count,
+.buddypress-wrap .bp-navs li.dynamic.selected a .count,
+.buddypress-wrap .bp-navs li.dynamic.current a .count {
+	background-color: #5087e5;
+	border: 0;
+	color: #fafafa;
+}
+
+.buddypress_object_nav .bp-navs li.dynamic a:hover .count,
+.buddypress-wrap .bp-navs li.dynamic a:hover .count {
+	background-color: #5087e5;
+	border: 0;
+	color: #fff;
+}
+
+.buddypress_object_nav .bp-navs li a .count:empty,
+.buddypress-wrap .bp-navs li a .count:empty {
+	display: none;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current),
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) {
+	color: #767676;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a {
+	color: #767676;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a:focus, .buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a:hover,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a:focus,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a:hover {
+	background: none;
+	color: #333;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a[disabled]:focus, .buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a[disabled]:hover,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a[disabled]:focus,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a[disabled]:hover {
+	color: #767676;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li.current a,
+.buddypress-wrap .bp-navs.group-create-links ul li.current a {
+	text-align: center;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-navs li {
+		float: right;
+	}
+	.buddypress-wrap .subnav {
+		float: right;
+	}
+	.buddypress-wrap ul.subnav {
+		width: auto;
+	}
+	.buddypress-wrap #subsubnav .activity-search {
+		float: right;
+	}
+	.buddypress-wrap #subsubnav .filter {
+		float: left;
+	}
+}
+
+.buddypress_object_nav .bp-navs li a .count {
+	display: inline-block;
+	float: left;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-dir-vert-nav .bp-navs.dir-navs {
+		background: none;
+	}
+	.bp-dir-vert-nav .bp-navs.dir-navs a .count {
+		float: left;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .tabbed-links ul,
+	.buddypress-wrap .tabbed-links ol {
+		border-bottom: 1px solid #ccc;
+		float: none;
+		margin: 20px 0 10px;
+	}
+	.buddypress-wrap .tabbed-links ul:before, .buddypress-wrap .tabbed-links ul:after,
+	.buddypress-wrap .tabbed-links ol:before,
+	.buddypress-wrap .tabbed-links ol:after {
+		content: " ";
+		display: block;
+	}
+	.buddypress-wrap .tabbed-links ul:after,
+	.buddypress-wrap .tabbed-links ol:after {
+		clear: both;
+	}
+	.buddypress-wrap .tabbed-links ul li,
+	.buddypress-wrap .tabbed-links ol li {
+		float: right;
+		list-style: none;
+		margin: 0 0 0 10px;
+	}
+	.buddypress-wrap .tabbed-links ul li a,
+	.buddypress-wrap .tabbed-links ul li span:not(.count),
+	.buddypress-wrap .tabbed-links ol li a,
+	.buddypress-wrap .tabbed-links ol li span:not(.count) {
+		background: none;
+		border: none;
+		display: block;
+		padding: 4px 10px;
+	}
+	.buddypress-wrap .tabbed-links ul li a:focus,
+	.buddypress-wrap .tabbed-links ul li a:hover,
+	.buddypress-wrap .tabbed-links ol li a:focus,
+	.buddypress-wrap .tabbed-links ol li a:hover {
+		background: none;
+	}
+	.buddypress-wrap .tabbed-links ul li:not(.current),
+	.buddypress-wrap .tabbed-links ol li:not(.current) {
+		margin-bottom: 2px;
+	}
+	.buddypress-wrap .tabbed-links ul li.current,
+	.buddypress-wrap .tabbed-links ol li.current {
+		border-color: #ccc #ccc #fff;
+		border-style: solid;
+		border-top-right-radius: 4px;
+		border-top-left-radius: 4px;
+		border-width: 1px;
+		margin-bottom: -1px;
+		padding: 0 0.5em 1px;
+	}
+	.buddypress-wrap .tabbed-links ul li.current a,
+	.buddypress-wrap .tabbed-links ol li.current a {
+		background: none;
+		color: #333;
+	}
+	.buddypress-wrap .bp-subnavs.tabbed-links > ul {
+		margin-top: 0;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links {
+		background: none;
+		margin-top: 2px;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li a {
+		border-left: 0;
+		font-size: inherit;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li.last {
+		float: left;
+		margin: 0;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li.last a {
+		margin-top: -0.5em;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li a,
+	.buddypress-wrap .bp-navs.tabbed-links ul li a:focus,
+	.buddypress-wrap .bp-navs.tabbed-links ul li a:hover,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a:focus,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a:hover {
+		background: none;
+		border: 0;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li a:active,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a:active {
+		outline: none;
+	}
+}
+
+.buddypress-wrap .dir-component-filters .filter label {
+	display: inline;
+}
+
+.buddypress-wrap .subnav-filters:before,
+.buddypress-wrap .subnav-filters:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress-wrap .subnav-filters:after {
+	clear: both;
+}
+
+.buddypress-wrap .subnav-filters {
+	background: none;
+	list-style: none;
+	margin: 15px 0 0;
+	padding: 0;
+}
+
+.buddypress-wrap .subnav-filters div {
+	margin: 0;
+}
+
+.buddypress-wrap .subnav-filters > ul {
+	float: right;
+	list-style: none;
+}
+
+.buddypress-wrap .subnav-filters.bp-messages-filters ul {
+	width: 100%;
+}
+
+.buddypress-wrap .subnav-filters.bp-messages-filters .messages-search {
+	margin-bottom: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .subnav-filters.bp-messages-filters .messages-search {
+		margin-bottom: 0;
+	}
+}
+
+.buddypress-wrap .subnav-filters div {
+	float: none;
+}
+
+.buddypress-wrap .subnav-filters div select,
+.buddypress-wrap .subnav-filters div input[type="search"] {
+	font-size: 16px;
+}
+
+.buddypress-wrap .subnav-filters div button.nouveau-search-submit {
+	padding: 5px 0.8em 6px;
+}
+
+.buddypress-wrap .subnav-filters div button#user_messages_search_submit {
+	padding: 7px 0.8em;
+}
+
+.buddypress-wrap .subnav-filters .component-filters {
+	margin-top: 10px;
+}
+
+.buddypress-wrap .subnav-filters .feed {
+	margin-left: 15px;
+}
+
+.buddypress-wrap .subnav-filters .last.filter label {
+	display: inline;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap:before,
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap:after {
+	clear: both;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap.bp-show {
+	display: inline-block;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap.bp-hide {
+	display: none;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .select-wrap {
+	border: 0;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .select-wrap:focus,
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .select-wrap:hover {
+	outline: 1px solid #d6d6d6;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions {
+	float: right;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions label {
+	display: inline-block;
+	font-weight: 300;
+	margin-left: 25px;
+	padding: 5px 0;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions div select {
+	-webkit-appearance: textfield;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-apply {
+	border: 0;
+	border-radius: none;
+	font-weight: 400;
+	line-height: 1.8;
+	margin: 0 10px 0 0;
+	padding: 3px 5px;
+	text-align: center;
+	text-transform: none;
+	width: auto;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-apply span {
+	vertical-align: middle;
+}
+
+@media screen and (min-width: 32em) {
+	.buddypress-wrap .subnav-filters li {
+		margin-bottom: 0;
+	}
+	.buddypress-wrap .subnav-filters .subnav-search,
+	.buddypress-wrap .subnav-filters .subnav-search form,
+	.buddypress-wrap .subnav-filters .feed,
+	.buddypress-wrap .subnav-filters .bp-search,
+	.buddypress-wrap .subnav-filters .dir-search,
+	.buddypress-wrap .subnav-filters .user-messages-bulk-actions,
+	.buddypress-wrap .subnav-filters .user-messages-search,
+	.buddypress-wrap .subnav-filters .group-invites-search,
+	.buddypress-wrap .subnav-filters .group-act-search {
+		float: right;
+	}
+	.buddypress-wrap .subnav-filters .last,
+	.buddypress-wrap .subnav-filters .component-filters {
+		float: left;
+		margin-top: 0;
+		width: auto;
+	}
+	.buddypress-wrap .subnav-filters .last select,
+	.buddypress-wrap .subnav-filters .component-filters select {
+		max-width: 250px;
+	}
+	.buddypress-wrap .subnav-filters .user-messages-search {
+		float: left;
+	}
+}
+
+.buddypress-wrap .notifications-options-nav input#notification-bulk-manage {
+	border: 0;
+	border-radius: 0;
+	line-height: 1.6;
+}
+
+.buddypress-wrap .group-subnav-filters .group-invites-search {
+	margin-bottom: 1em;
+}
+
+.buddypress-wrap .group-subnav-filters .last {
+	text-align: center;
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+.buddypress-wrap .bp-pagination {
+	background: transparent;
+	border: 0;
+	color: #767676;
+	float: right;
+	font-size: small;
+	margin: 0;
+	padding: 0.5em 0;
+	position: relative;
+	width: 100%;
+}
+
+.buddypress-wrap .bp-pagination .pag-count {
+	float: right;
+}
+
+.buddypress-wrap .bp-pagination .bp-pagination-links {
+	float: left;
+	margin-left: 10px;
+}
+
+.buddypress-wrap .bp-pagination .bp-pagination-links span,
+.buddypress-wrap .bp-pagination .bp-pagination-links a {
+	font-size: small;
+	padding: 0 5px;
+}
+
+.buddypress-wrap .bp-pagination .bp-pagination-links a:focus,
+.buddypress-wrap .bp-pagination .bp-pagination-links a:hover {
+	opacity: 1;
+}
+
+.buddypress-wrap .bp-pagination p {
+	margin: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 3.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+.bp-list:before,
+.bp-list:after {
+	content: " ";
+	display: table;
+}
+
+.bp-list:after {
+	clear: both;
+}
+
+.bp-list {
+	box-sizing: border-box;
+	border-top: 1px solid #eaeaea;
+	clear: both;
+	list-style: none;
+	margin: 20px 0;
+	padding: 0.5em 0;
+	width: 100%;
+}
+
+.bp-list li:before,
+.bp-list li:after {
+	content: " ";
+	display: table;
+}
+
+.bp-list li:after {
+	clear: both;
+}
+
+.bp-list > li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.bp-list li {
+	list-style: none;
+	margin: 10px 0;
+	padding: 0.5em 0;
+	position: relative;
+}
+
+.bp-list li .item-avatar {
+	text-align: center;
+}
+
+.bp-list li .item-avatar img.avatar {
+	display: inline;
+}
+
+.bp-list li .item .item-avatar,
+.bp-list li .item .list-title,
+.bp-list li .item .item-meta,
+.bp-list li .item .group-details {
+	text-align: center;
+}
+
+.bp-list li .item .list-title {
+	clear: none;
+	font-size: 22px;
+	font-weight: 400;
+	line-height: 1.1;
+	margin: 0 auto;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-list li .item .list-title {
+		font-size: 26px;
+	}
+}
+
+.bp-list li .meta,
+.bp-list li .item-meta {
+	color: #737373;
+	font-size: 12px;
+	margin-bottom: 10px;
+	margin-top: 10px;
+}
+
+.bp-list li .last-post {
+	text-align: center;
+}
+
+.bp-list li .action {
+	margin: 0;
+	text-align: center;
+}
+
+.bp-list li .action .generic-button {
+	display: inline-block;
+	font-size: 12px;
+	margin: 0 0 0 10px;
+}
+
+.bp-list li .action div.generic-button {
+	margin: 10px 0;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-list li .item-avatar {
+		float: right;
+		margin-left: 5%;
+	}
+	.bp-list li .item {
+		margin: 0;
+		overflow: hidden;
+	}
+	.bp-list li .item .item-block {
+		float: right;
+		margin-left: 2%;
+		width: 50%;
+	}
+	.bp-list li .item .list-title,
+	.bp-list li .item .item-meta {
+		float: right;
+		text-align: right;
+	}
+	.bp-list li .item .group-details,
+	.bp-list li .item .last-post {
+		text-align: right;
+	}
+	.bp-list li .group-desc,
+	.bp-list li .user-update,
+	.bp-list li .last-post {
+		clear: none;
+		overflow: hidden;
+		width: auto;
+	}
+	.bp-list li .action {
+		clear: right;
+		padding: 0;
+		text-align: right;
+	}
+	.bp-list li .action li.generic-button {
+		margin-left: 0;
+	}
+	.bp-list li .action div.generic-button {
+		margin: 0 0 10px;
+	}
+	.bp-list li .generic-button {
+		display: block;
+		margin: 0 0 5px 0;
+	}
+}
+
+@media screen and (min-width: 32em) {
+	#activity-stream {
+		clear: both;
+		padding-top: 1em;
+	}
+}
+
+.activity-list.bp-list {
+	background: #fafafa;
+	border: 1px solid #eee;
+}
+
+.activity-list.bp-list .activity-item {
+	background: #fff;
+	border: 1px solid #b7b7b7;
+	box-shadow: 0 0 6px #d2d2d2;
+	margin: 20px 0;
+}
+
+.activity-list.bp-list li:first-child {
+	margin-top: 0;
+}
+
+.friends-list {
+	list-style-type: none;
+}
+
+.friends-request-list .item-title,
+.membership-requests-list .item-title {
+	text-align: center;
+}
+
+@media screen and (min-width: 46.8em) {
+	.friends-request-list li,
+	.membership-requests-list li {
+		display: -moz-flex;
+		display: -ms-flex;
+		display: -o-flex;
+		display: flex;
+		-o-flex-flow: row nowrap;
+		flex-flow: row nowrap;
+	}
+	.friends-request-list li .item,
+	.membership-requests-list li .item {
+		-moz-flex: 1 1 auto;
+		-o-flex: 1 1 auto;
+		flex: 1 1 auto;
+	}
+	.friends-request-list li .action,
+	.membership-requests-list li .action {
+		text-align: left;
+	}
+	.friends-request-list li .item-title,
+	.membership-requests-list li .item-title {
+		font-size: 22px;
+		text-align: right;
+	}
+	.friends-request-list li .item-title h3,
+	.membership-requests-list li .item-title h3 {
+		margin: 0;
+	}
+}
+
+#notifications-user-list {
+	clear: both;
+	padding-top: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	body:not(.logged-in) .bp-list .item {
+		margin-left: 0;
+	}
+}
+
+.activity-permalink .item-list,
+.activity-permalink .item-list li.activity-item {
+	border: 0;
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.1 - Activity Loop
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 3.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+.activity-update-form {
+	padding: 10px 10px 0;
+}
+
+.item-body .activity-update-form .activity-form {
+	margin: 0;
+	padding: 0;
+}
+
+.activity-update-form {
+	border: 1px solid #ccc;
+	box-shadow: inset 0 0 6px #eee;
+	margin: 15px 0;
+}
+
+.activity-update-form #whats-new-avatar {
+	margin: 10px 0;
+	text-align: center;
+}
+
+.activity-update-form #whats-new-avatar img {
+	box-shadow: none;
+	display: inline-block;
+}
+
+.activity-update-form #whats-new-content {
+	padding: 0 0 20px 0;
+}
+
+.activity-update-form #whats-new-textarea textarea {
+	background: #fff;
+	box-sizing: border-box;
+	color: #333;
+	font-family: inherit;
+	font-size: medium;
+	height: 2.2em;
+	line-height: 1.4;
+	padding: 6px;
+	width: 100%;
+}
+
+.activity-update-form #whats-new-textarea textarea:focus {
+	box-shadow: 0 0 6px 0 #d6d6d6;
+}
+
+.activity-update-form #whats-new-post-in-box {
+	margin: 10px 0;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items {
+	list-style: none;
+	margin: 10px 0;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items li {
+	margin-bottom: 10px;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items #activity-autocomplete {
+	padding: 0.3em;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object {
+	display: flex;
+	align-items: center;
+	padding: 0.2em;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object .avatar {
+	width: 30px;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object span {
+	padding-right: 10px;
+	vertical-align: middle;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object:focus, .activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object:hover {
+	background: #eaeaea;
+	cursor: pointer;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object.selected {
+	border: 1px solid #d6d6d6;
+}
+
+.activity-update-form #whats-new-submit {
+	margin: 15px 0 10px;
+}
+
+.activity-update-form #whats-new-submit input {
+	font-size: 14px;
+	line-height: inherit;
+	margin-bottom: 10px;
+	margin-left: 10px;
+	padding: 0.2em 0;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-update-form #whats-new-avatar {
+		display: block;
+		float: right;
+		margin: 0;
+	}
+	.activity-update-form #whats-new-content,
+	.activity-update-form #whats-new-post-in-box,
+	.activity-update-form #whats-new-submit {
+		margin-right: 8.5%;
+	}
+	.activity-update-form #whats-new-submit input {
+		margin-bottom: 0;
+		margin-left: 10px;
+		width: 8em;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 3.1.2 - Activity Entries
+*-----------------------------------------------------
+*/
+.activity-list {
+	padding: 0.5em;
+}
+
+.activity-list .activity-item:before,
+.activity-list .activity-item:after {
+	content: " ";
+	display: table;
+}
+
+.activity-list .activity-item:after {
+	clear: both;
+}
+
+.activity-list .activity-item {
+	list-style: none;
+	padding: 1em;
+}
+
+.activity-list .activity-item.has-comments {
+	padding-bottom: 1em;
+}
+
+.activity-list .activity-item div.item-avatar {
+	margin: 0 auto;
+	text-align: center;
+	width: auto;
+}
+
+.activity-list .activity-item div.item-avatar img {
+	height: auto;
+	max-width: 40%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-list .activity-item div.item-avatar {
+		margin: 0 0 0 2%;
+		text-align: right;
+		width: 15%;
+	}
+	.activity-list .activity-item div.item-avatar img {
+		max-width: 80%;
+	}
+}
+
+.activity-list .activity-item.mini {
+	font-size: 13px;
+	position: relative;
+}
+
+.activity-list .activity-item.mini .activity-avatar {
+	margin-right: 0 auto;
+	text-align: center;
+	width: auto;
+}
+
+.activity-list .activity-item.mini .activity-avatar img.avatar,
+.activity-list .activity-item.mini .activity-avatar img.FB_profile_pic {
+	/* stylelint-disable-line selector-class-pattern */
+	max-width: 15%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-list .activity-item.mini .activity-avatar {
+		margin-right: 15px;
+		text-align: right;
+		width: 15%;
+	}
+	.activity-list .activity-item.mini .activity-avatar img.avatar,
+	.activity-list .activity-item.mini .activity-avatar img.FB_profile_pic {
+		/* stylelint-disable-line selector-class-pattern */
+		max-width: 60%;
+	}
+}
+
+.activity-list .activity-item.new_forum_post .activity-inner, .activity-list .activity-item.new_forum_topic .activity-inner {
+	border-right: 2px solid #eaeaea;
+	margin-right: 10px;
+	padding-right: 1em;
+}
+
+.activity-list .activity-item.newest_mentions_activity, .activity-list .activity-item.newest_friends_activity, .activity-list .activity-item.newest_groups_activity, .activity-list .activity-item.newest_blogs_activity {
+	background: rgba(31, 179, 221, 0.1);
+}
+
+.activity-list .activity-item .activity-inreplyto {
+	color: #767676;
+	font-size: 13px;
+}
+
+.activity-list .activity-item .activity-inreplyto > p {
+	display: inline;
+	margin: 0;
+}
+
+.activity-list .activity-item .activity-inreplyto blockquote,
+.activity-list .activity-item .activity-inreplyto .activity-inner {
+	background: none;
+	border: 0;
+	display: inline;
+	margin: 0;
+	overflow: hidden;
+	padding: 0;
+}
+
+.activity-list .activity-item .activity-header {
+	margin: 0 auto;
+	width: 80%;
+}
+
+.activity-list .activity-item .activity-header a,
+.activity-list .activity-item .activity-header img {
+	display: inline;
+}
+
+.activity-list .activity-item .activity-header .avatar {
+	display: inline-block;
+	margin: 0 5px;
+	vertical-align: bottom;
+}
+
+.activity-list .activity-item .activity-header .time-since {
+	font-size: 14px;
+	color: #767676;
+	text-decoration: none;
+}
+
+.activity-list .activity-item .activity-header .time-since:hover {
+	color: #767676;
+	cursor: pointer;
+	text-decoration: underline;
+}
+
+.activity-list .activity-item .activity-content .activity-header,
+.activity-list .activity-item .activity-content .comment-header {
+	color: #767676;
+	margin-bottom: 10px;
+}
+
+.activity-list .activity-item .activity-content .activity-inner,
+.activity-list .activity-item .activity-content blockquote {
+	background: #fafafa;
+	margin: 15px 0 10px;
+	overflow: hidden;
+	padding: 1em;
+}
+
+.activity-list .activity-item .activity-content p {
+	margin: 0;
+}
+
+.activity-list .activity-item .activity-inner p {
+	word-wrap: break-word;
+}
+
+.activity-list .activity-item .activity-read-more {
+	margin-right: 1em;
+	white-space: nowrap;
+}
+
+.activity-list .activity-item ul.activity-meta {
+	margin: 0;
+	padding-right: 0;
+}
+
+.activity-list .activity-item ul.activity-meta li {
+	border: 0;
+	display: inline-block;
+}
+
+.activity-list .activity-item .activity-meta.action {
+	border: 1px solid transparent;
+	background: #fafafa;
+	padding: 2px;
+	position: relative;
+	text-align: right;
+}
+
+.activity-list .activity-item .activity-meta.action div.generic-button {
+	margin: 0;
+}
+
+.activity-list .activity-item .activity-meta.action .button {
+	background: transparent;
+}
+
+.activity-list .activity-item .activity-meta.action a {
+	padding: 4px 8px;
+}
+
+.activity-list .activity-item .activity-meta.action .button:focus,
+.activity-list .activity-item .activity-meta.action .button:hover {
+	background: none;
+}
+
+.activity-list .activity-item .activity-meta.action .button:before,
+.activity-list .activity-item .activity-meta.action .icons:before {
+	font-family: dashicons;
+	font-size: 18px;
+	vertical-align: middle;
+}
+
+.activity-list .activity-item .activity-meta.action .acomment-reply.button:before {
+	content: "\f101";
+}
+
+.activity-list .activity-item .activity-meta.action .view:before {
+	content: "\f125";
+}
+
+.activity-list .activity-item .activity-meta.action .fav:before {
+	content: "\f154";
+}
+
+.activity-list .activity-item .activity-meta.action .unfav:before {
+	content: "\f155";
+}
+
+.activity-list .activity-item .activity-meta.action .delete-activity:before {
+	content: "\f153";
+}
+
+.activity-list .activity-item .activity-meta.action .delete-activity:hover {
+	color: #800;
+}
+
+.activity-list .activity-item .activity-meta.action .button {
+	border: 0;
+	box-shadow: none;
+}
+
+.activity-list .activity-item .activity-meta.action .button span {
+	background: none;
+	color: #555;
+	font-weight: 700;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-list.bp-list {
+		padding: 30px;
+	}
+	.activity-list .activity-item .activity-content {
+		margin: 0;
+		position: relative;
+	}
+	.activity-list .activity-item .activity-content:after {
+		clear: both;
+		content: "";
+		display: table;
+	}
+	.activity-list .activity-item .activity-header {
+		margin: 0 0 0 15px;
+		width: auto;
+	}
+}
+
+.buddypress-wrap .activity-list .load-more,
+.buddypress-wrap .activity-list .load-newest {
+	background: #fafafa;
+	border: 1px solid #eee;
+	font-size: 110%;
+	margin: 15px 0;
+	padding: 0;
+	text-align: center;
+}
+
+.buddypress-wrap .activity-list .load-more a,
+.buddypress-wrap .activity-list .load-newest a {
+	color: #555;
+	display: block;
+	padding: 0.5em 0;
+}
+
+.buddypress-wrap .activity-list .load-more a:focus, .buddypress-wrap .activity-list .load-more a:hover,
+.buddypress-wrap .activity-list .load-newest a:focus,
+.buddypress-wrap .activity-list .load-newest a:hover {
+	background: #fff;
+	color: #333;
+}
+
+.buddypress-wrap .activity-list .load-more:focus, .buddypress-wrap .activity-list .load-more:hover,
+.buddypress-wrap .activity-list .load-newest:focus,
+.buddypress-wrap .activity-list .load-newest:hover {
+	border-color: #e1e1e1;
+	box-shadow: 0 0 6px 0 #eaeaea;
+}
+
+body.activity-permalink .activity-list li {
+	border-width: 1px;
+	padding: 1em 0 0 0;
+}
+
+body.activity-permalink .activity-list li:first-child {
+	padding-top: 0;
+}
+
+body.activity-permalink .activity-list li.has-comments {
+	padding-bottom: 0;
+}
+
+body.activity-permalink .activity-list .activity-avatar {
+	width: auto;
+}
+
+body.activity-permalink .activity-list .activity-avatar a {
+	display: block;
+}
+
+body.activity-permalink .activity-list .activity-avatar img {
+	max-width: 100%;
+}
+
+body.activity-permalink .activity-list .activity-content {
+	border: 0;
+	font-size: 100%;
+	line-height: 1.5;
+	padding: 0;
+}
+
+body.activity-permalink .activity-list .activity-content .activity-header {
+	margin: 0;
+	padding: 0.5em 0 0 0;
+	text-align: center;
+	width: 100%;
+}
+
+body.activity-permalink .activity-list .activity-content .activity-inner,
+body.activity-permalink .activity-list .activity-content blockquote {
+	margin-right: 0;
+	margin-top: 10px;
+}
+
+body.activity-permalink .activity-list .activity-meta {
+	margin: 10px 0 10px;
+}
+
+body.activity-permalink .activity-list .activity-comments {
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 46.8em) {
+	body.activity-permalink .activity-list .activity-avatar {
+		right: -20px;
+		margin-left: 0;
+		position: relative;
+		top: -20px;
+	}
+	body.activity-permalink .activity-list .activity-content {
+		margin-left: 10px;
+	}
+	body.activity-permalink .activity-list .activity-content .activity-header p {
+		text-align: right;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 3.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+.buddypress-wrap .activity-comments {
+	clear: both;
+	margin: 0 5%;
+	overflow: hidden;
+	position: relative;
+	width: auto;
+}
+
+.buddypress-wrap .activity-comments ul {
+	clear: both;
+	list-style: none;
+	margin: 15px 0 0;
+	padding: 0;
+}
+
+.buddypress-wrap .activity-comments ul li {
+	border-top: 1px solid #eee;
+	border-bottom: 0;
+	padding: 1em 0 0;
+}
+
+.buddypress-wrap .activity-comments ul li ul {
+	margin-right: 5%;
+}
+
+.buddypress-wrap .activity-comments ul li:first-child {
+	border-top: 0;
+}
+
+.buddypress-wrap .activity-comments ul li:last-child {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .activity-comments div.acomment-avatar {
+	width: auto;
+}
+
+.buddypress-wrap .activity-comments div.acomment-avatar img {
+	border-width: 1px;
+	float: right;
+	height: 25px;
+	max-width: none;
+	width: 25px;
+}
+
+.buddypress-wrap .activity-comments .acomment-meta,
+.buddypress-wrap .activity-comments .acomment-content p {
+	font-size: 14px;
+}
+
+.buddypress-wrap .activity-comments .acomment-meta {
+	color: #555;
+	overflow: hidden;
+	padding-right: 2%;
+}
+
+.buddypress-wrap .activity-comments .acomment-content {
+	border-right: 1px solid #ccc;
+	margin: 15px 10% 0 0;
+	padding: 0.5em 1em;
+}
+
+.buddypress-wrap .activity-comments .acomment-content p {
+	margin-bottom: 0.5em;
+}
+
+.buddypress-wrap .activity-comments .acomment-options {
+	float: right;
+	margin: 10px 20px 10px 0;
+}
+
+.buddypress-wrap .activity-comments .acomment-options a {
+	color: #767676;
+	font-size: 14px;
+}
+
+.buddypress-wrap .activity-comments .acomment-options a:focus, .buddypress-wrap .activity-comments .acomment-options a:hover {
+	color: inherit;
+}
+
+.buddypress-wrap .activity-comments .activity-meta.action {
+	background: none;
+	margin-top: 10px;
+}
+
+.buddypress-wrap .activity-comments .activity-meta.action button {
+	font-size: 14px;
+	font-weight: 400;
+	text-transform: none;
+}
+
+.buddypress-wrap .activity-comments .show-all button {
+	font-size: 14px;
+	text-decoration: underline;
+	padding-right: 0.5em;
+}
+
+.buddypress-wrap .activity-comments .show-all button span {
+	text-decoration: none;
+}
+
+.buddypress-wrap .activity-comments .show-all button:hover span, .buddypress-wrap .activity-comments .show-all button:focus span {
+	color: #5087e5;
+}
+
+.buddypress-wrap .mini .activity-comments {
+	clear: both;
+	margin-top: 0;
+}
+
+body.activity-permalink .activity-comments {
+	background: none;
+	width: auto;
+}
+
+body.activity-permalink .activity-comments > ul {
+	padding: 0 1em 0 0.5em;
+}
+
+body.activity-permalink .activity-comments ul li > ul {
+	margin-top: 10px;
+}
+
+form.ac-form {
+	display: none;
+	padding: 1em;
+}
+
+form.ac-form .ac-reply-avatar {
+	float: right;
+}
+
+form.ac-form .ac-reply-avatar img {
+	border: 1px solid #eee;
+}
+
+form.ac-form .ac-reply-content {
+	color: #767676;
+	padding-right: 1em;
+}
+
+form.ac-form .ac-reply-content a {
+	text-decoration: none;
+}
+
+form.ac-form .ac-reply-content .ac-textarea {
+	margin-bottom: 15px;
+	padding: 0 0.5em;
+	overflow: hidden;
+}
+
+form.ac-form .ac-reply-content .ac-textarea textarea {
+	background: transparent;
+	box-shadow: none;
+	color: #555;
+	font-family: inherit;
+	font-size: 100%;
+	height: 60px;
+	margin: 0;
+	outline: none;
+	padding: 0.5em;
+	width: 100%;
+}
+
+form.ac-form .ac-reply-content .ac-textarea textarea:focus {
+	box-shadow: 0 0 6px #d6d6d6;
+}
+
+form.ac-form .ac-reply-content input {
+	margin-top: 10px;
+}
+
+.activity-comments li form.ac-form {
+	clear: both;
+	margin-left: 15px;
+}
+
+.activity-comments form.root {
+	margin-right: 0;
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Blogs Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .blogs-list li .item-block {
+		float: none;
+		width: auto;
+	}
+	.buddypress-wrap .blogs-list li .item-meta {
+		clear: right;
+		float: none;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-dir-vert-nav .blogs-list .list-title {
+		width: auto;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Groups Loop
+*----------------------------------------------------------
+*/
+.buddypress-wrap .groups-list li .list-title {
+	text-align: center;
+}
+
+.buddypress-wrap .groups-list li .group-details {
+	clear: right;
+}
+
+.buddypress-wrap .groups-list li .group-desc {
+	border: 1px solid #eaeaea;
+	border-radius: 10px;
+	background-clip: padding-box;
+	font-size: 13px;
+	color: #737373;
+	font-style: italic;
+	margin: 10px auto 0;
+	padding: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .groups-list li .group-desc {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .groups-list li p {
+	margin: 0 0 0.5em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .groups-list li .item {
+		margin-left: 0;
+	}
+	.buddypress-wrap .groups-list li .list-title,
+	.buddypress-wrap .groups-list li .item-meta {
+		text-align: right;
+		width: auto;
+	}
+	.buddypress-wrap .groups-list li .item-meta {
+		margin-bottom: 20px;
+	}
+	.buddypress-wrap .groups-list li .last-activity {
+		clear: right;
+		margin-top: -20px;
+	}
+}
+
+.buddypress-wrap .groups-list li.group-no-avatar div.group-desc {
+	margin-right: 0;
+}
+
+.buddypress-wrap .mygroups .groups-list.grid .wrap {
+	min-height: 450px;
+	padding-bottom: 0;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .groups-list.grid.three .group-desc, .buddypress-wrap .groups-list.grid.four .group-desc {
+		font-size: 14px;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress .bp-vertical-navs .groups-list .item-avatar {
+		margin-left: 3%;
+		width: 15%;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Members Loop
+*----------------------------------------------------------
+*/
+.buddypress-wrap .members-list li .member-name {
+	margin-bottom: 10px;
+}
+
+.buddypress-wrap .members-list li .user-update {
+	border: 1px solid #eaeaea;
+	border-radius: 10px;
+	background-clip: padding-box;
+	color: #737373;
+	font-style: italic;
+	font-size: 13px;
+	margin: 15px auto;
+	padding: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .members-list li .user-update {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .members-list li .user-update .activity-read-more {
+	display: block;
+	font-size: 12px;
+	font-style: normal;
+	margin-top: 10px;
+	padding-right: 2px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .members-list li .last-activity {
+		clear: right;
+		margin-top: -10px;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .members-group-list li .joined {
+		clear: right;
+		float: none;
+	}
+}
+
+@media screen and (min-width: 32em) {
+	body:not(.logged-in) .members-list .user-update {
+		width: 96%;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+.register-page .register-section {
+	box-sizing: border-box;
+}
+
+.register-page .signup-form {
+	margin-top: 20px;
+}
+
+.register-page .signup-form .default-profile input {
+	margin-bottom: 20px;
+}
+
+.register-page .signup-form label,
+.register-page .signup-form legend {
+	margin: 10px 0 0;
+}
+
+.register-page .signup-form .editfield {
+	margin: 15px 0;
+}
+
+.register-page .signup-form .editfield fieldset {
+	border: 0;
+	padding: 0;
+}
+
+.register-page .signup-form .editfield fieldset legend {
+	margin: 0 0 5px;
+	text-indent: 0;
+}
+
+.register-page .signup-form .editfield .field-visibility-settings {
+	padding: 0.5em;
+}
+
+.register-page .signup-form .editfield .field-visibility-settings fieldset {
+	margin: 0 0 10px;
+}
+
+.register-page .signup-form #signup-avatar img {
+	margin: 0 0 10px 15px;
+}
+
+.register-page .signup-form .password-entry,
+.register-page .signup-form .password-entry-confirm {
+	border: 1px solid #eee;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .register-page .layout-wrap {
+		display: flex;
+		flex-flow: row wrap;
+		justify-content: space-around;
+	}
+	.buddypress-wrap .register-page .layout-wrap .default-profile {
+		flex: 1;
+		padding-left: 2em;
+	}
+	.buddypress-wrap .register-page .layout-wrap .blog-details {
+		flex: 1;
+		padding-right: 2em;
+	}
+	.buddypress-wrap .register-page .submit {
+		clear: both;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap.extended-default-reg .register-page .default-profile {
+		flex: 1;
+		padding-left: 1em;
+	}
+	.buddypress-wrap.extended-default-reg .register-page .extended-profile {
+		flex: 2;
+		padding-right: 1em;
+	}
+	.buddypress-wrap.extended-default-reg .register-page .blog-details {
+		flex: 1 100%;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Groups Creation Steps
+*----------------------------------------------------------
+*/
+#group-create-body {
+	padding: 0.5em;
+}
+
+#group-create-body .creation-step-name {
+	text-align: center;
+}
+
+#group-create-body .avatar-nav-items {
+	margin-top: 15px;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Single Item screens: Groups, Users
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 5.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.single-headers:before,
+.single-headers:after {
+	content: " ";
+	display: table;
+}
+
+.single-headers:after {
+	clear: both;
+}
+
+.single-headers {
+	margin-bottom: 15px;
+}
+
+.single-headers #item-header-avatar a {
+	display: block;
+	text-align: center;
+}
+
+.single-headers #item-header-avatar a img {
+	float: none;
+}
+
+.single-headers div#item-header-content {
+	float: none;
+}
+
+@media screen and (min-width: 46.8em) {
+	.single-headers #item-header-avatar a {
+		text-align: right;
+	}
+	.single-headers #item-header-avatar a img {
+		float: right;
+	}
+	.single-headers #item-header-content {
+		padding-right: 2em;
+	}
+}
+
+.single-headers .group-status,
+.single-headers .activity {
+	display: inline;
+}
+
+.single-headers .group-status {
+	font-size: 18px;
+	color: #333;
+	padding-left: 1em;
+}
+
+.single-headers .activity {
+	display: inline-block;
+	font-size: 12px;
+	padding: 0;
+}
+
+.single-headers div#message p,
+.single-headers #sitewide-notice p {
+	background-color: #ffd;
+	border: 1px solid #cb2;
+	color: #440;
+	font-weight: 400;
+	margin-top: 3px;
+	text-decoration: none;
+}
+
+.single-headers h2 {
+	line-height: 1.2;
+	margin: 0 0 5px;
+}
+
+.single-headers h2 a {
+	color: #767676;
+	text-decoration: none;
+}
+
+.single-headers h2 span.highlight {
+	display: inline-block;
+	font-size: 60%;
+	font-weight: 400;
+	line-height: 1.7;
+	vertical-align: middle;
+}
+
+.single-headers h2 span.highlight span {
+	background: #a1dcfa;
+	color: #fff;
+	cursor: pointer;
+	font-size: 80%;
+	font-weight: 700;
+	margin-bottom: 2px;
+	padding: 1px 4px;
+	position: relative;
+	left: -2px;
+	top: -2px;
+	vertical-align: middle;
+}
+
+.single-headers img.avatar {
+	float: right;
+	margin: 0 0 19px 15px;
+}
+
+.single-headers .item-meta {
+	color: #767676;
+	font-size: 14px;
+	margin: 15px 0 5px;
+	padding-bottom: 0.5em;
+}
+
+.single-headers ul {
+	margin-bottom: 15px;
+}
+
+.single-headers ul li {
+	float: left;
+	list-style: none;
+}
+
+.single-headers div.generic-button {
+	text-align: center;
+}
+
+.single-headers li.generic-button {
+	display: inline-block;
+	text-align: center;
+}
+
+@media screen and (min-width: 46.8em) {
+	.single-headers div.generic-button,
+	.single-headers a.button,
+	.single-headers li.generic-button {
+		float: right;
+	}
+}
+
+.single-headers div.generic-button,
+.single-headers a.button {
+	margin: 10px 0 0 10px;
+}
+
+.single-headers li.generic-button {
+	margin: 2px 10px;
+}
+
+.single-headers li.generic-button:first-child {
+	margin-right: 0;
+}
+
+.single-headers div#message.info {
+	line-height: 0.8;
+}
+
+body.no-js .single-item-header .js-self-profile-button {
+	display: none;
+}
+
+/*
+* Default required cover image rules
+*/
+#cover-image-container {
+	position: relative;
+}
+
+#header-cover-image {
+	background-color: #c5c5c5;
+	background-position: center top;
+	background-repeat: no-repeat;
+	background-size: cover;
+	border: 0;
+	display: block;
+	right: 0;
+	margin: 0;
+	padding: 0;
+	position: absolute;
+	top: 0;
+	width: 100%;
+	z-index: 1;
+}
+
+#item-header-cover-image {
+	position: relative;
+	z-index: 2;
+}
+
+#item-header-cover-image #item-header-avatar {
+	padding: 0 1em;
+}
+
+/*
+* end cover image block
+*/
+/**
+*-----------------------------------------------------
+* @subsection 5.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+.groups-header .bp-group-type-list {
+	margin: 0;
+}
+
+.groups-header .bp-feedback {
+	clear: both;
+}
+
+.groups-header .group-item-actions {
+	float: right;
+	margin: 0 15px 15px 0;
+	padding-top: 0;
+	width: 100%;
+}
+
+.groups-header .moderators-lists {
+	margin-top: 0;
+}
+
+.groups-header .moderators-lists .moderators-title {
+	font-size: 14px;
+}
+
+.groups-header .moderators-lists .user-list {
+	margin: 0 0 5px;
+}
+
+.groups-header .moderators-lists .user-list ul:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.groups-header .moderators-lists .user-list li {
+	display: inline-block;
+	float: none;
+	margin-right: 4px;
+	padding: 4px;
+}
+
+.groups-header .moderators-lists img.avatar {
+	box-shadow: none;
+	float: none;
+	height: 30px;
+	margin: 0;
+	max-width: 100%;
+	width: 30px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.groups-header div#item-header-content {
+		float: right;
+		margin-right: 10%;
+		text-align: right;
+		padding-top: 15px;
+		width: 42%;
+	}
+	.groups-header .group-item-actions {
+		float: left;
+		margin: 0 15px 15px 0;
+		text-align: left;
+		width: 20%;
+	}
+	.groups-header .groups-meta {
+		clear: both;
+	}
+}
+
+.groups-header .desc-wrap {
+	background: #eaeaea;
+	border: 1px solid #d6d6d6;
+	margin: 0 0 15px;
+	padding: 1em;
+	text-align: center;
+}
+
+.groups-header .desc-wrap .group-description {
+	background: #fafafa;
+	box-shadow: inset 0 0 9px #ccc;
+	padding: 1em;
+	text-align: right;
+}
+
+.groups-header .desc-wrap .group-description p {
+	margin: 0;
+	padding: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 5.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user .users-header .user-nicename {
+	margin-bottom: 5px;
+}
+
+.bp-user .member-header-actions {
+	overflow: hidden;
+}
+
+.bp-user .member-header-actions * > * {
+	display: block;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 5.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+.buddypress-wrap .item-body {
+	margin: 20px 0;
+}
+
+.buddypress-wrap .item-body .screen-heading {
+	font-size: 20px;
+	font-weight: 400;
+}
+
+.buddypress-wrap .item-body .button-tabs {
+	margin: 30px 0 15px;
+}
+
+.buddypress-wrap.bp-single-vert-nav .bp-list:not(.grid) .item-entry {
+	padding-right: 0.5em;
+}
+
+/**
+*----------------------------------------------------
+* @subsection 5.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+.single-item.group-members .item-body .filters:not(.no-subnav) {
+	border-top: 5px solid #eaeaea;
+	padding-top: 1em;
+}
+
+.single-item.group-members .item-body .filters {
+	margin-top: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 5.2.1.1 - Management Settings Screens
+*-----------------------------------------
+*/
+.buddypress-wrap .group-status-type ul {
+	margin: 0 20px 20px 0;
+}
+
+.groups-manage-members-list {
+	padding: 0.5em 0;
+}
+
+.groups-manage-members-list dd {
+	margin: 0;
+	padding: 1em 0;
+}
+
+.groups-manage-members-list .section-title {
+	background: #eaeaea;
+	padding-right: 0.3em;
+}
+
+.groups-manage-members-list ul {
+	list-style: none;
+	margin-bottom: 0;
+}
+
+.groups-manage-members-list ul li {
+	border-bottom: 1px solid #eee;
+	margin-bottom: 10px;
+	padding: 0.5em 0.3em 0.3em;
+}
+
+.groups-manage-members-list ul li:only-child,
+.groups-manage-members-list ul li:last-child {
+	border-bottom: 0;
+}
+
+.groups-manage-members-list ul li:nth-child(even) {
+	background: #fafafa;
+}
+
+.groups-manage-members-list ul li.banned-user {
+	background: #fad3d3;
+}
+
+.groups-manage-members-list ul .member-name {
+	margin-bottom: 0;
+	text-align: center;
+}
+
+.groups-manage-members-list ul img {
+	display: block;
+	margin: 0 auto;
+	width: 20%;
+}
+
+@media screen and (min-width: 32em) {
+	.groups-manage-members-list ul .member-name {
+		text-align: right;
+	}
+	.groups-manage-members-list ul img {
+		display: inline;
+		width: 50px;
+	}
+}
+
+.groups-manage-members-list ul .members-manage-buttons:before,
+.groups-manage-members-list ul .members-manage-buttons:after {
+	content: " ";
+	display: table;
+}
+
+.groups-manage-members-list ul .members-manage-buttons:after {
+	clear: both;
+}
+
+.groups-manage-members-list ul .members-manage-buttons {
+	margin: 15px 0 5px;
+}
+
+.groups-manage-members-list ul .members-manage-buttons a.button {
+	color: #767676;
+	display: block;
+	font-size: 13px;
+}
+
+@media screen and (min-width: 32em) {
+	.groups-manage-members-list ul .members-manage-buttons a.button {
+		display: inline-block;
+	}
+}
+
+.groups-manage-members-list ul .members-manage-buttons.text-links-list {
+	margin-bottom: 0;
+}
+
+@media screen and (max-width: 32em) {
+	.groups-manage-members-list ul .members-manage-buttons.text-links-list a.button {
+		background: #fafafa;
+		border: 1px solid #eee;
+		display: block;
+		margin-bottom: 10px;
+	}
+}
+
+.groups-manage-members-list ul .action:not(.text-links-list) a.button {
+	font-size: 12px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.groups-manage-members-list ul li .avatar,
+	.groups-manage-members-list ul li .member-name {
+		float: right;
+	}
+	.groups-manage-members-list ul li .avatar {
+		margin-left: 15px;
+	}
+	.groups-manage-members-list ul li .action {
+		clear: both;
+		float: right;
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 5.2.1.2 - Group Members List
+*-----------------------------------------
+*/
+/*
+*-----------------------------------------
+* @subsection 5.2.1.3 - Group Invites List
+*-----------------------------------------
+*/
+/*
+ * bp-nouveau styling: invite members, sent invites
+ * @version 3.0.0
+ */
+.buddypress .bp-invites-content ul.item-list {
+	border-top: 0;
+}
+
+.buddypress .bp-invites-content ul.item-list li {
+	border: 1px solid #eaeaea;
+	margin: 0 0 1%;
+	padding-right: 5px;
+	padding-left: 5px;
+	position: relative;
+	width: auto;
+}
+
+.buddypress .bp-invites-content ul.item-list li .list-title {
+	margin: 0 auto;
+	width: 80%;
+}
+
+.buddypress .bp-invites-content ul.item-list li .action {
+	position: absolute;
+	top: 10px;
+	left: 10px;
+}
+
+.buddypress .bp-invites-content ul.item-list li .action a.button.invite-button {
+	border: 0;
+}
+
+.buddypress .bp-invites-content ul.item-list li .action a.button.invite-button:focus, .buddypress .bp-invites-content ul.item-list li .action a.button.invite-button:hover {
+	color: #1fb3dd;
+}
+
+.buddypress .bp-invites-content ul.item-list li.selected {
+	box-shadow: inset 0 0 12px 0 rgba(237, 187, 52, 0.2);
+}
+
+.buddypress .bp-invites-content .item-list .item-meta span,
+.buddypress .bp-invites-content .group-inviters li {
+	color: #767676;
+}
+
+.buddypress .bp-invites-content li ul.group-inviters {
+	clear: both;
+	margin: 0;
+	overflow: hidden;
+}
+
+.buddypress .bp-invites-content li ul.group-inviters li {
+	border: 0;
+	float: right;
+	font-size: 20px;
+	width: inherit;
+}
+
+.buddypress .bp-invites-content li .status {
+	font-size: 20px;
+	font-style: italic;
+	clear: both;
+	color: #555;
+	margin: 10px 0;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul:before,
+.buddypress .bp-invites-content #send-invites-editor ul:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul:after {
+	clear: both;
+}
+
+.buddypress .bp-invites-content #send-invites-editor textarea {
+	width: 100%;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul {
+	clear: both;
+	list-style: none;
+	margin: 10px 0;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul li {
+	float: right;
+	margin: 0.5%;
+	max-height: 50px;
+	max-width: 50px;
+}
+
+.buddypress .bp-invites-content #send-invites-editor #bp-send-invites-form {
+	clear: both;
+	margin-top: 10px;
+}
+
+.buddypress .bp-invites-content #send-invites-editor .action {
+	margin-top: 10px;
+	padding-top: 10px;
+}
+
+.buddypress .bp-invites-content #send-invites-editor.bp-hide {
+	display: none;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress .bp-invites-content ul.item-list > li {
+		box-sizing: border-box;
+		border: 1px solid #eaeaea;
+		float: right;
+		padding-right: 0.5em;
+		padding-left: 0.5em;
+		width: 49.5%;
+	}
+	.buddypress .bp-invites-content ul.item-list > li:nth-child(odd) {
+		margin-left: 0.5%;
+	}
+	.buddypress .bp-invites-content ul.item-list > li:nth-child(even) {
+		margin-right: 0.5%;
+	}
+	.buddypress .bp-invites-content ul.item-list ul.group-inviters {
+		float: right;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	:not(.vertical) + .item-body #group-invites-container {
+		display: -ms-grid;
+		display: grid;
+		-ms-grid-columns: 25% auto;
+		grid-template-columns: 25% auto;
+		grid-template-areas: "group-invites-nav group-invites-column";
+	}
+	:not(.vertical) + .item-body #group-invites-container .bp-invites-nav {
+		-ms-grid-row: 1;
+		-ms-grid-column: 1;
+		grid-area: group-invites-nav;
+	}
+	:not(.vertical) + .item-body #group-invites-container .bp-invites-nav li {
+		display: block;
+		float: none;
+	}
+	:not(.vertical) + .item-body #group-invites-container .group-invites-column {
+		-ms-grid-row: 1;
+		-ms-grid-column: 2;
+		grid-area: group-invites-column;
+	}
+}
+
+/*
+*-----------------------------------------
+* @subsection 5.2.1.4 - Group Activity
+*-----------------------------------------
+*/
+.buddypress.groups .activity-update-form {
+	margin-top: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 5.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+/**
+*--------------------------------------------
+* @subsection 5.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+/**
+*-------------------------------------------
+* @subsection 5.2.2.2 - Extended Profiles
+*-------------------------------------------
+*/
+.buddypress-wrap .profile {
+	margin-top: 30px;
+}
+
+.buddypress-wrap .public .profile-fields td.label {
+	width: 30%;
+}
+
+.buddypress-wrap .profile.edit .button-nav {
+	list-style: none;
+	margin: 30px 0 10px;
+}
+
+.buddypress-wrap .profile.edit .button-nav li {
+	display: inline-block;
+	margin-left: 10px;
+}
+
+.buddypress-wrap .profile.edit .button-nav li a {
+	font-size: 18px;
+}
+
+.buddypress-wrap .profile.edit .editfield {
+	background: #fafafa;
+	border: 1px solid #eee;
+	margin: 15px 0;
+	padding: 1em;
+}
+
+.buddypress-wrap .profile.edit .editfield fieldset {
+	border: 0;
+}
+
+.buddypress-wrap .profile.edit .editfield fieldset label {
+	font-weight: 400;
+}
+
+.buddypress-wrap .profile.edit .editfield fieldset label.xprofile-field-label {
+	display: inline;
+}
+
+.buddypress-wrap .profile.edit .editfield {
+	display: flex;
+	flex-direction: column;
+}
+
+.buddypress-wrap .profile.edit .editfield .description {
+	margin-top: 10px;
+	order: 2;
+}
+
+.buddypress-wrap .profile.edit .editfield > fieldset {
+	order: 1;
+}
+
+.buddypress-wrap .profile.edit .editfield .field-visibility-settings-toggle,
+.buddypress-wrap .profile.edit .editfield .field-visibility-settings {
+	order: 3;
+}
+
+body.no-js .buddypress-wrap .field-visibility-settings-toggle,
+body.no-js .buddypress-wrap .field-visibility-settings-close {
+	display: none;
+}
+
+body.no-js .buddypress-wrap .field-visibility-settings {
+	display: block;
+}
+
+.buddypress-wrap .field-visibility-settings {
+	margin: 10px 0;
+}
+
+.buddypress-wrap .current-visibility-level {
+	font-style: normal;
+	font-weight: 700;
+}
+
+.buddypress-wrap .field-visibility-settings,
+.buddypress-wrap .field-visibility-settings-header {
+	color: #737373;
+}
+
+.buddypress-wrap .field-visibility-settings fieldset {
+	margin: 5px 0;
+}
+
+.buddypress-wrap .standard-form .editfield fieldset {
+	margin: 0;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings label {
+	font-weight: 400;
+	margin: 0;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings .radio {
+	list-style: none;
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings .field-visibility-settings-close {
+	font-size: 12px;
+}
+
+.buddypress-wrap .standard-form .wp-editor-container {
+	border: 1px solid #dedede;
+}
+
+.buddypress-wrap .standard-form .wp-editor-container textarea {
+	background: #fff;
+	width: 100%;
+}
+
+.buddypress-wrap .standard-form .description {
+	background: #fafafa;
+	font-size: inherit;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings legend,
+.buddypress-wrap .standard-form .field-visibility-settings-header {
+	font-style: italic;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings-header {
+	font-size: 14px;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings legend,
+.buddypress-wrap .standard-form .field-visibility-settings label {
+	font-size: 14px;
+}
+
+.buddypress-wrap .standard-form .field-visibility select {
+	margin: 0;
+}
+
+.buddypress-wrap .html-active button.switch-html {
+	background: #f5f5f5;
+	border-bottom-color: transparent;
+	border-bottom-right-radius: 0;
+	border-bottom-left-radius: 0;
+}
+
+.buddypress-wrap .tmce-active button.switch-tmce {
+	background: #f5f5f5;
+	border-bottom-color: transparent;
+	border-bottom-right-radius: 0;
+	border-bottom-left-radius: 0;
+}
+
+.buddypress-wrap .profile.public .profile-group-title {
+	border-bottom: 1px solid #ccc;
+}
+
+body.register .buddypress-wrap .page ul {
+	list-style: none;
+}
+
+.buddypress-wrap .profile .bp-avatar-nav {
+	margin-top: 20px;
+}
+
+/**
+*-------------------------------------------
+* @subsection 5.2.2.3 - Groups
+*-------------------------------------------
+*/
+/**
+*-------------------------------------------
+* @subsection 5.2.2.5 - Private Messaging
+*-------------------------------------------
+*/
+.message-action-star:before,
+.message-action-unstar:before,
+.message-action-view:before,
+.message-action-delete:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.message-action-star:before {
+	color: #aaa;
+	content: "\f154";
+}
+
+.message-action-unstar:before {
+	color: #fcdd77;
+	content: "\f155";
+}
+
+.message-action-view:before {
+	content: "\f473";
+}
+
+.message-action-delete:before {
+	content: "\f153";
+}
+
+.message-action-delete:hover:before {
+	color: #a00;
+}
+
+.preview-content .actions a {
+	text-decoration: none;
+}
+
+.bp-messages-content {
+	margin: 15px 0;
+}
+
+.bp-messages-content .avatar {
+	box-shadow: none;
+}
+
+.bp-messages-content .thread-participants {
+	list-style: none;
+}
+
+.bp-messages-content .thread-participants dd {
+	margin-right: 0;
+}
+
+.bp-messages-content time {
+	color: #737373;
+	font-size: 12px;
+}
+
+#message-threads {
+	border-top: 1px solid #eaeaea;
+	clear: both;
+	list-style: none;
+	margin: 0;
+	max-height: 220px;
+	overflow-x: hidden;
+	overflow-y: auto;
+	padding: 0;
+	width: 100%;
+}
+
+#message-threads li {
+	border-bottom: 1px solid #eaeaea;
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: row nowrap;
+	flex-flow: row nowrap;
+	margin: 0;
+	overflow: hidden;
+	padding: 0.5em 0;
+}
+
+#message-threads li .thread-cb {
+	display: flex;
+	align-items: center;
+	-moz-flex: 1 2 5%;
+	-o-flex: 1 2 5%;
+	flex: 1 2 5%;
+}
+
+#message-threads li .thread-from,
+#message-threads li .thread-to {
+	-moz-flex: 1 2 20%;
+	-o-flex: 1 2 20%;
+	flex: 1 2 20%;
+}
+
+#message-threads li .thread-from img.avatar,
+#message-threads li .thread-to img.avatar {
+	float: right;
+	margin: 0 0 0 10px;
+}
+
+#message-threads li .thread-from .user-name,
+#message-threads li .thread-to .user-name {
+	display: inline-block;
+	line-height: 1.1;
+}
+
+#message-threads li .thread-from .num-recipients,
+#message-threads li .thread-to .num-recipients {
+	color: #737373;
+	font-weight: 400;
+	font-size: 12px;
+	margin: 0;
+}
+
+#message-threads li .thread-content {
+	-moz-flex: 1 2 60%;
+	-o-flex: 1 2 60%;
+	flex: 1 2 60%;
+}
+
+#message-threads li .thread-date {
+	-moz-flex: 1 2 15%;
+	-o-flex: 1 2 15%;
+	flex: 1 2 15%;
+}
+
+#message-threads li.selected {
+	background-color: #fafafa;
+}
+
+#message-threads li.selected .thread-subject .subject {
+	color: #5087e5;
+}
+
+#message-threads li.unread {
+	font-weight: 700;
+}
+
+#message-threads li .thread-content .excerpt {
+	color: #737373;
+	font-size: 12px;
+	margin: 0;
+}
+
+#message-threads li .thread-content .thread-from,
+#message-threads li .thread-content .thread-to,
+#message-threads li .thread-content .thread-subject {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	#message-threads li .thread-content .thread-from,
+	#message-threads li .thread-content .thread-to,
+	#message-threads li .thread-content .thread-subject {
+		font-size: 16px;
+	}
+}
+
+#message-threads li .thread-content .thread-subject {
+	vertical-align: top;
+}
+
+#message-threads li .thread-content .thread-subject .excerpt {
+	font-weight: 400;
+}
+
+#message-threads li .thread-date {
+	padding-left: 5px;
+	text-align: left;
+}
+
+.bp-messages-content .actions {
+	float: left;
+	max-width: 30%;
+}
+
+.bp-messages-content .actions .bp-icons:not(.bp-hide) {
+	display: inline-block;
+	margin: 0;
+	padding: 0.3em 0.5em;
+}
+
+.bp-messages-content .actions .bp-icons:not(.bp-hide):before {
+	font-size: 26px;
+}
+
+.bp-messages-content #thread-preview {
+	border: 1px solid #eaeaea;
+	margin-top: 20px;
+}
+
+.bp-messages-content #thread-preview .preview-message {
+	overflow: hidden;
+}
+
+.bp-messages-content #thread-preview .preview-content {
+	margin: 0.5em;
+}
+
+.bp-messages-content #thread-preview .preview-content .preview-message {
+	background: #fafafa;
+	margin: 10px 0;
+	padding: 1em 0.3em 0.3em;
+}
+
+.bp-messages-content #bp-message-thread-list {
+	border-top: 1px solid #eaeaea;
+	clear: both;
+	list-style: none;
+	padding: 1em 0 0.3em;
+}
+
+.bp-messages-content #bp-message-thread-list li {
+	padding: 0.5em;
+}
+
+.bp-messages-content #bp-message-thread-list li:nth-child(2n) .message-content {
+	background: #fafafa;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata {
+	border-bottom: 1px solid #ccc;
+	box-shadow: 2px 1px 9px 0 #eee;
+	display: table;
+	padding: 0.2em;
+	width: 100%;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata .avatar {
+	width: 30px;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata .user-link {
+	display: block;
+	font-size: 13px;
+	float: right;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-messages-content #bp-message-thread-list .message-metadata .user-link {
+		font-size: 16px;
+	}
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata time {
+	color: #737373;
+	font-size: 12px;
+	padding: 0 0.5em;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata button {
+	padding: 0 0.3em;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata button:before {
+	font-size: 20px;
+}
+
+.bp-messages-content #bp-message-thread-list .message-content {
+	overflow: hidden;
+	margin: 1em auto 0;
+	width: 90%;
+}
+
+.bp-messages-content #bp-message-thread-list img.avatar {
+	float: right;
+	margin: 0 0 0 10px;
+}
+
+.bp-messages-content #bp-message-thread-list .actions a:before {
+	font-size: 18px;
+}
+
+.bp-messages-content form.send-reply .avatar-box {
+	padding: 0.5em 0;
+}
+
+.bp-messages-content .preview-pane-header,
+.bp-messages-content .single-message-thread-header {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.bp-messages-content .preview-pane-header:after,
+.bp-messages-content .single-message-thread-header:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-messages-content .preview-thread-title,
+.bp-messages-content .single-thread-title {
+	font-size: 16px;
+}
+
+.bp-messages-content .preview-thread-title .messages-title,
+.bp-messages-content .single-thread-title .messages-title {
+	padding-right: 2em;
+}
+
+.bp-messages-content .thread-participants {
+	float: right;
+	margin: 5px 0;
+	width: 70%;
+}
+
+.bp-messages-content .thread-participants dd,
+.bp-messages-content .thread-participants ul {
+	margin-bottom: 10px;
+}
+
+.bp-messages-content .thread-participants ul {
+	list-style: none;
+}
+
+.bp-messages-content .thread-participants ul:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-messages-content .thread-participants li {
+	float: right;
+	margin-right: 5px;
+}
+
+.bp-messages-content .thread-participants img {
+	width: 30px;
+}
+
+.bp-messages-content #thread-preview .preview-message ul,
+.bp-messages-content #thread-preview .preview-message ol,
+.bp-messages-content #thread-preview .preview-message blockquote,
+.bp-messages-content #bp-message-thread-list li .message-content ul,
+.bp-messages-content #bp-message-thread-list li .message-content ol,
+.bp-messages-content #bp-message-thread-list li .message-content blockquote {
+	list-style-position: inside;
+	margin-right: 0;
+}
+
+.bp-messages-content ul#message-threads:empty,
+.bp-messages-content #thread-preview:empty {
+	display: none;
+}
+
+.bp-messages-content #thread-preview h2:first-child,
+.bp-messages-content #bp-message-thread-header h2:first-child {
+	background-color: #eaeaea;
+	color: #555;
+	font-weight: 700;
+	margin: 0;
+	padding: 0.5em;
+}
+
+.bp-messages-content #message-threads .thread-content a,
+.bp-messages-content #bp-message-thread-list li a.user-link {
+	border: 0;
+	text-decoration: none;
+}
+
+.bp-messages-content .standard-form #subject {
+	margin-bottom: 20px;
+}
+
+div.bp-navs#subsubnav.bp-messages-filters .user-messages-bulk-actions {
+	margin-left: 15px;
+	max-width: 42.5%;
+}
+
+/**
+*------------------------------------------
+* @subsection 5.2.2.6 - Settings
+*------------------------------------------
+*/
+/*__ Settings Global __*/
+.buddypress.settings .profile-settings.bp-tables-user select {
+	width: 100%;
+}
+
+/*__ General __*/
+/*__ Email notifications __*/
+/*__ Profile visibility __*/
+/*__ Group Invites __*/
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+.buddypress-wrap .filter select,
+.buddypress-wrap #whats-new-post-in-box select {
+	border: 1px solid #d6d6d6;
+}
+
+.buddypress-wrap input.action[disabled] {
+	cursor: pointer;
+	opacity: 0.4;
+}
+
+.buddypress-wrap #notification-bulk-manage[disabled] {
+	display: none;
+}
+
+.buddypress-wrap fieldset legend {
+	font-size: inherit;
+	font-weight: 600;
+}
+
+.buddypress-wrap textarea:focus,
+.buddypress-wrap input[type="text"]:focus,
+.buddypress-wrap input[type="email"]:focus,
+.buddypress-wrap input[type="url"]:focus,
+.buddypress-wrap input[type="tel"]:focus,
+.buddypress-wrap input[type="password"]:focus {
+	box-shadow: 0 0 8px #eaeaea;
+}
+
+.buddypress-wrap select {
+	height: auto;
+}
+
+.buddypress-wrap textarea {
+	resize: vertical;
+}
+
+.buddypress-wrap .standard-form .bp-controls-wrap {
+	margin: 1em 0;
+}
+
+.buddypress-wrap .standard-form textarea,
+.buddypress-wrap .standard-form input[type="text"],
+.buddypress-wrap .standard-form input[type="color"],
+.buddypress-wrap .standard-form input[type="date"],
+.buddypress-wrap .standard-form input[type="datetime"],
+.buddypress-wrap .standard-form input[type="datetime-local"],
+.buddypress-wrap .standard-form input[type="email"],
+.buddypress-wrap .standard-form input[type="month"],
+.buddypress-wrap .standard-form input[type="number"],
+.buddypress-wrap .standard-form input[type="range"],
+.buddypress-wrap .standard-form input[type="search"],
+.buddypress-wrap .standard-form input[type="tel"],
+.buddypress-wrap .standard-form input[type="time"],
+.buddypress-wrap .standard-form input[type="url"],
+.buddypress-wrap .standard-form input[type="week"],
+.buddypress-wrap .standard-form select,
+.buddypress-wrap .standard-form input[type="password"],
+.buddypress-wrap .standard-form [data-bp-search] input[type="search"],
+.buddypress-wrap .standard-form [data-bp-search] input[type="text"],
+.buddypress-wrap .standard-form .groups-members-search input[type="search"],
+.buddypress-wrap .standard-form .groups-members-search input[type="text"] {
+	background: #fafafa;
+	border: 1px solid #d6d6d6;
+	border-radius: 0;
+	font: inherit;
+	font-size: 100%;
+	padding: 0.5em;
+}
+
+.buddypress-wrap .standard-form input[required],
+.buddypress-wrap .standard-form textarea[required],
+.buddypress-wrap .standard-form select[required] {
+	box-shadow: none;
+	border-width: 2px;
+	outline: 0;
+}
+
+.buddypress-wrap .standard-form input[required]:invalid,
+.buddypress-wrap .standard-form textarea[required]:invalid,
+.buddypress-wrap .standard-form select[required]:invalid {
+	border-color: #b71717;
+}
+
+.buddypress-wrap .standard-form input[required]:valid,
+.buddypress-wrap .standard-form textarea[required]:valid,
+.buddypress-wrap .standard-form select[required]:valid {
+	border-color: #91cc2c;
+}
+
+.buddypress-wrap .standard-form input[required]:focus,
+.buddypress-wrap .standard-form textarea[required]:focus,
+.buddypress-wrap .standard-form select[required]:focus {
+	border-color: #d6d6d6;
+	border-width: 1px;
+}
+
+.buddypress-wrap .standard-form input.invalid[required],
+.buddypress-wrap .standard-form textarea.invalid[required],
+.buddypress-wrap .standard-form select.invalid[required] {
+	border-color: #b71717;
+}
+
+.buddypress-wrap .standard-form input:not(.button-small),
+.buddypress-wrap .standard-form textarea {
+	width: 100%;
+}
+
+.buddypress-wrap .standard-form input[type="radio"],
+.buddypress-wrap .standard-form input[type="checkbox"] {
+	margin-left: 5px;
+	width: auto;
+}
+
+.buddypress-wrap .standard-form select {
+	padding: 3px;
+}
+
+.buddypress-wrap .standard-form textarea {
+	height: 120px;
+}
+
+.buddypress-wrap .standard-form textarea#message_content {
+	height: 200px;
+}
+
+.buddypress-wrap .standard-form input[type="password"] {
+	margin-bottom: 5px;
+}
+
+.buddypress-wrap .standard-form input:focus,
+.buddypress-wrap .standard-form textarea:focus,
+.buddypress-wrap .standard-form select:focus {
+	background: #fafafa;
+	color: #555;
+	outline: 0;
+}
+
+.buddypress-wrap .standard-form label,
+.buddypress-wrap .standard-form span.label {
+	display: block;
+	font-weight: 600;
+	margin: 15px 0 5px;
+	width: auto;
+}
+
+.buddypress-wrap .standard-form a.clear-value {
+	display: block;
+	margin-top: 5px;
+	outline: none;
+}
+
+.buddypress-wrap .standard-form .submit {
+	clear: both;
+	padding: 15px 0 0;
+}
+
+.buddypress-wrap .standard-form p.submit {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .standard-form div.submit input {
+	margin-left: 15px;
+}
+
+.buddypress-wrap .standard-form p label,
+.buddypress-wrap .standard-form #invite-list label {
+	font-weight: 400;
+	margin: auto;
+}
+
+.buddypress-wrap .standard-form p.description {
+	color: #737373;
+	margin: 5px 0;
+}
+
+.buddypress-wrap .standard-form div.checkbox label:nth-child(n+2),
+.buddypress-wrap .standard-form div.radio div label {
+	color: #737373;
+	font-size: 100%;
+	font-weight: 400;
+	margin: 5px 0 0;
+}
+
+.buddypress-wrap .standard-form#send-reply textarea {
+	width: 97.5%;
+}
+
+.buddypress-wrap .standard-form#sidebar-login-form label {
+	margin-top: 5px;
+}
+
+.buddypress-wrap .standard-form#sidebar-login-form input[type="text"],
+.buddypress-wrap .standard-form#sidebar-login-form input[type="password"] {
+	padding: 4px;
+	width: 95%;
+}
+
+.buddypress-wrap .standard-form.profile-edit input:focus {
+	background: #fff;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .standard-form .left-menu {
+		float: right;
+	}
+	.buddypress-wrap .standard-form #invite-list ul {
+		list-style: none;
+		margin: 1%;
+	}
+	.buddypress-wrap .standard-form #invite-list ul li {
+		margin: 0 1% 0 0;
+	}
+	.buddypress-wrap .standard-form .main-column {
+		margin-right: 190px;
+	}
+	.buddypress-wrap .standard-form .main-column ul#friend-list {
+		clear: none;
+		float: right;
+	}
+	.buddypress-wrap .standard-form .main-column ul#friend-list h4 {
+		clear: none;
+	}
+}
+
+.buddypress-wrap .standard-form .bp-tables-user label {
+	margin: 0;
+}
+
+.buddypress-wrap .signup-form label,
+.buddypress-wrap .signup-form legend {
+	font-weight: 400;
+}
+
+body.no-js .buddypress #notifications-bulk-management #select-all-notifications,
+body.no-js .buddypress label[for="message-type-select"],
+body.no-js .buddypress #message-type-select,
+body.no-js .buddypress #delete_inbox_messages,
+body.no-js .buddypress #delete_sentbox_messages,
+body.no-js .buddypress #messages-bulk-management #select-all-messages {
+	display: none;
+}
+
+/* Overrides for embedded WP editors */
+.buddypress-wrap .wp-editor-wrap a.button,
+.buddypress-wrap .wp-editor-wrap .wp-editor-wrap button,
+.buddypress-wrap .wp-editor-wrap .wp-editor-wrap input[type="submit"],
+.buddypress-wrap .wp-editor-wrap .wp-editor-wrap input[type="button"],
+.buddypress-wrap .wp-editor-wrap input[type="reset"] {
+	padding: 0 8px 1px;
+}
+
+.buddypress-wrap .select-wrap {
+	border: 1px solid #eee;
+}
+
+.buddypress-wrap .select-wrap label {
+	display: inline;
+}
+
+.buddypress-wrap .select-wrap select::-ms-expand {
+	display: none;
+}
+
+.buddypress-wrap .select-wrap select {
+	-moz-appearance: none;
+	-webkit-appearance: none;
+	-o-appearance: none;
+	appearance: none;
+	border: 0;
+	cursor: pointer;
+	margin-left: -25px;
+	padding: 6px 10px 6px 25px;
+	position: relative;
+	text-indent: -2px;
+	z-index: 1;
+	width: 100%;
+}
+
+.buddypress-wrap .select-wrap select,
+.buddypress-wrap .select-wrap select:focus,
+.buddypress-wrap .select-wrap select:active {
+	background: transparent;
+}
+
+.buddypress-wrap .select-wrap span.select-arrow {
+	display: inline-block;
+	position: relative;
+	z-index: 0;
+}
+
+.buddypress-wrap .select-wrap span.select-arrow:before {
+	color: #ccc;
+	content: "\25BC";
+}
+
+.buddypress-wrap .select-wrap:focus .select-arrow:before, .buddypress-wrap .select-wrap:hover .select-arrow:before {
+	color: #a6a6a6;
+}
+
+.buddypress-wrap .select-wrap:focus, .buddypress-wrap .select-wrap:hover,
+.buddypress-wrap .bp-search form:focus,
+.buddypress-wrap .bp-search form:hover {
+	border: 1px solid #d5d4d4;
+	box-shadow: inset 0 0 3px #eee;
+}
+
+@media screen and (min-width: 32em) {
+	.buddypress-wrap .notifications-options-nav .select-wrap {
+		float: right;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 6.1 - Directory Search
+*
+* The Search form & controls in directory pages
+*----------------------------------------------------------
+*/
+.buddypress-wrap .bp-dir-search-form, .buddypress-wrap .bp-messages-search-form:before,
+.buddypress-wrap .bp-dir-search-form, .buddypress-wrap .bp-messages-search-form:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress-wrap .bp-dir-search-form, .buddypress-wrap .bp-messages-search-form:after {
+	clear: both;
+}
+
+.buddypress-wrap form.bp-dir-search-form,
+.buddypress-wrap form.bp-messages-search-form,
+.buddypress-wrap form.bp-invites-search-form {
+	border: 1px solid #eee;
+	width: 100%;
+}
+
+@media screen and (min-width: 55em) {
+	.buddypress-wrap form.bp-dir-search-form,
+	.buddypress-wrap form.bp-messages-search-form,
+	.buddypress-wrap form.bp-invites-search-form {
+		width: 15em;
+	}
+}
+
+.buddypress-wrap form.bp-dir-search-form label,
+.buddypress-wrap form.bp-messages-search-form label,
+.buddypress-wrap form.bp-invites-search-form label {
+	margin: 0;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"],
+.buddypress-wrap form.bp-dir-search-form input[type="text"],
+.buddypress-wrap form.bp-dir-search-form button[type="submit"],
+.buddypress-wrap form.bp-messages-search-form input[type="search"],
+.buddypress-wrap form.bp-messages-search-form input[type="text"],
+.buddypress-wrap form.bp-messages-search-form button[type="submit"],
+.buddypress-wrap form.bp-invites-search-form input[type="search"],
+.buddypress-wrap form.bp-invites-search-form input[type="text"],
+.buddypress-wrap form.bp-invites-search-form button[type="submit"] {
+	background: none;
+	border: 0;
+	border-radius: 0;
+	background-clip: padding-box;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"],
+.buddypress-wrap form.bp-dir-search-form input[type="text"],
+.buddypress-wrap form.bp-messages-search-form input[type="search"],
+.buddypress-wrap form.bp-messages-search-form input[type="text"],
+.buddypress-wrap form.bp-invites-search-form input[type="search"],
+.buddypress-wrap form.bp-invites-search-form input[type="text"] {
+	float: right;
+	line-height: 1.5;
+	padding: 3px 10px;
+	width: 80%;
+}
+
+.buddypress-wrap form.bp-dir-search-form button[type="submit"],
+.buddypress-wrap form.bp-messages-search-form button[type="submit"],
+.buddypress-wrap form.bp-invites-search-form button[type="submit"] {
+	float: left;
+	font-size: inherit;
+	font-weight: 400;
+	line-height: 1.5;
+	padding: 3px 0.7em;
+	text-align: center;
+	text-transform: none;
+	width: 20%;
+}
+
+.buddypress-wrap form.bp-dir-search-form button[type="submit"] span,
+.buddypress-wrap form.bp-messages-search-form button[type="submit"] span,
+.buddypress-wrap form.bp-invites-search-form button[type="submit"] span {
+	font-family: dashicons;
+	font-size: 18px;
+	line-height: 1.6;
+}
+
+.buddypress-wrap form.bp-dir-search-form button[type="submit"].bp-show,
+.buddypress-wrap form.bp-messages-search-form button[type="submit"].bp-show,
+.buddypress-wrap form.bp-invites-search-form button[type="submit"].bp-show {
+	height: auto;
+	right: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"]::-webkit-search-cancel-button,
+.buddypress-wrap form.bp-messages-search-form input[type="search"]::-webkit-search-cancel-button,
+.buddypress-wrap form.bp-invites-search-form input[type="search"]::-webkit-search-cancel-button {
+	-webkit-appearance: searchfield-cancel-button;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"]::-webkit-search-results-button,
+.buddypress-wrap form.bp-dir-search-form input[type="search"]::-webkit-search-results-decoration,
+.buddypress-wrap form.bp-messages-search-form input[type="search"]::-webkit-search-results-button,
+.buddypress-wrap form.bp-messages-search-form input[type="search"]::-webkit-search-results-decoration,
+.buddypress-wrap form.bp-invites-search-form input[type="search"]::-webkit-search-results-button,
+.buddypress-wrap form.bp-invites-search-form input[type="search"]::-webkit-search-results-decoration {
+	display: none;
+}
+
+.buddypress-wrap ul.filters li form label input {
+	line-height: 1.4;
+	padding: 0.1em 0.7em;
+}
+
+.buddypress-wrap .current-member-type {
+	font-style: italic;
+}
+
+.buddypress-wrap .dir-form {
+	clear: both;
+}
+
+.budypress.no-js form.bp-dir-search-form button[type="submit"] {
+	height: auto;
+	right: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+.bp-user [data-bp-search] form input[type="search"],
+.bp-user [data-bp-search] form input[type="text"] {
+	padding: 6px 10px 7px;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.buddypress-wrap .bp-tables-user,
+.buddypress-wrap table.wp-profile-fields,
+.buddypress-wrap table.forum {
+	width: 100%;
+}
+
+.buddypress-wrap .bp-tables-user thead tr,
+.buddypress-wrap table.wp-profile-fields thead tr,
+.buddypress-wrap table.forum thead tr {
+	background: none;
+	border-bottom: 2px solid #ccc;
+}
+
+.buddypress-wrap .bp-tables-user tbody tr,
+.buddypress-wrap table.wp-profile-fields tbody tr,
+.buddypress-wrap table.forum tbody tr {
+	background: #fafafa;
+}
+
+.buddypress-wrap .bp-tables-user tr th,
+.buddypress-wrap .bp-tables-user tr td,
+.buddypress-wrap table.wp-profile-fields tr th,
+.buddypress-wrap table.wp-profile-fields tr td,
+.buddypress-wrap table.forum tr th,
+.buddypress-wrap table.forum tr td {
+	padding: 0.5em;
+	vertical-align: middle;
+}
+
+.buddypress-wrap .bp-tables-user tr td.label,
+.buddypress-wrap table.wp-profile-fields tr td.label,
+.buddypress-wrap table.forum tr td.label {
+	border-left: 1px solid #eaeaea;
+	font-weight: 600;
+	width: 25%;
+}
+
+.buddypress-wrap .bp-tables-user tr.alt td,
+.buddypress-wrap table.wp-profile-fields tr.alt td {
+	background: #fafafa;
+}
+
+.buddypress-wrap table.profile-fields .data {
+	padding: 0.5em 1em;
+}
+
+.buddypress-wrap table.profile-fields tr:last-child {
+	border-bottom: none;
+}
+
+.buddypress-wrap table.notifications td {
+	padding: 1em 0.5em;
+}
+
+.buddypress-wrap table.notifications .bulk-select-all,
+.buddypress-wrap table.notifications .bulk-select-check {
+	width: 7%;
+}
+
+.buddypress-wrap table.notifications .bulk-select-check {
+	vertical-align: middle;
+}
+
+.buddypress-wrap table.notifications .title,
+.buddypress-wrap table.notifications .notification-description,
+.buddypress-wrap table.notifications .date,
+.buddypress-wrap table.notifications .notification-since {
+	width: 39%;
+}
+
+.buddypress-wrap table.notifications .actions,
+.buddypress-wrap table.notifications .notification-actions {
+	width: 15%;
+}
+
+.buddypress-wrap table.notification-settings th.title,
+.buddypress-wrap table.profile-settings th.title {
+	width: 80%;
+}
+
+.buddypress-wrap table.notifications .notification-actions a.delete,
+.buddypress-wrap table.notifications .notification-actions a.mark-read {
+	display: inline-block;
+}
+
+.buddypress-wrap table.notification-settings {
+	margin-bottom: 15px;
+	text-align: right;
+}
+
+.buddypress-wrap #groups-notification-settings {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap table.notifications th.icon,
+.buddypress-wrap table.notifications td:first-child,
+.buddypress-wrap table.notification-settings th.icon,
+.buddypress-wrap table.notification-settings td:first-child {
+	display: none;
+}
+
+.buddypress-wrap table.notification-settings .no,
+.buddypress-wrap table.notification-settings .yes {
+	text-align: center;
+	width: 40px;
+	vertical-align: middle;
+}
+
+.buddypress-wrap table#message-threads {
+	clear: both;
+}
+
+.buddypress-wrap table#message-threads .thread-info {
+	min-width: 40%;
+}
+
+.buddypress-wrap table#message-threads .thread-info p {
+	margin: 0;
+}
+
+.buddypress-wrap table#message-threads .thread-info p.thread-excerpt {
+	color: #737373;
+	font-size: 12px;
+	margin-top: 3px;
+}
+
+.buddypress-wrap table.profile-fields {
+	margin-bottom: 20px;
+}
+
+.buddypress-wrap table.profile-fields:last-child {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap table.profile-fields p {
+	margin: 0;
+}
+
+.buddypress-wrap table.profile-fields p:last-child {
+	margin-top: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Classes - Messages, Ajax, Widgets, Buttons
+*-------------------------------------------------------------------------------
+*/
+.bp-screen-reader-text {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+	word-wrap: normal !important;
+}
+
+.clearfix:before, .clearfix:after {
+	content: " ";
+	display: table;
+}
+
+.clearfix:after {
+	clear: both;
+}
+
+.center-vert {
+	display: flex;
+	align-items: center;
+}
+
+.bp-hide {
+	display: none;
+}
+
+.bp-show {
+	height: auto;
+	right: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+.buddypress .buddypress-wrap button,
+.buddypress .buddypress-wrap a.button,
+.buddypress .buddypress-wrap input[type="submit"],
+.buddypress .buddypress-wrap input[type="button"],
+.buddypress .buddypress-wrap input[type="reset"],
+.buddypress .buddypress-wrap ul.button-nav:not(.button-tabs) li a,
+.buddypress .buddypress-wrap .generic-button a,
+.buddypress .buddypress-wrap .comment-reply-link,
+.buddypress .buddypress-wrap a.bp-title-button,
+.buddypress .buddypress-wrap .activity-read-more a {
+	background: #fff;
+	/* Old browsers */
+	border-color: #ccc;
+	border-style: solid;
+	border-width: 1px;
+	color: #555;
+	cursor: pointer;
+	font-size: inherit;
+	font-weight: 400;
+	outline: none;
+	padding: 0.3em 0.5em;
+	text-align: center;
+	text-decoration: none;
+	width: auto;
+}
+
+.buddypress .buddypress-wrap .button-small[type="button"] {
+	padding: 0 8px 1px;
+}
+
+.buddypress .buddypress-wrap button:hover,
+.buddypress .buddypress-wrap button:focus,
+.buddypress .buddypress-wrap a.button:focus,
+.buddypress .buddypress-wrap a.button:hover,
+.buddypress .buddypress-wrap input[type="submit"]:focus,
+.buddypress .buddypress-wrap input[type="submit"]:hover,
+.buddypress .buddypress-wrap input[type="button"]:focus,
+.buddypress .buddypress-wrap input[type="button"]:hover,
+.buddypress .buddypress-wrap input[type="reset"]:focus,
+.buddypress .buddypress-wrap input[type="reset"]:hover,
+.buddypress .buddypress-wrap .button-nav li a:focus,
+.buddypress .buddypress-wrap .button-nav li a:hover,
+.buddypress .buddypress-wrap .button-nav li.current a,
+.buddypress .buddypress-wrap .generic-button a:focus,
+.buddypress .buddypress-wrap .generic-button a:hover,
+.buddypress .buddypress-wrap .comment-reply-link:focus,
+.buddypress .buddypress-wrap .comment-reply-link:hover,
+.buddypress .buddypress-wrap .activity-read-more a:focus,
+.buddypress .buddypress-wrap .activity-read-more a:hover {
+	background: #ededed;
+	border-color: #999999;
+	color: #333;
+	outline: none;
+	text-decoration: none;
+}
+
+.buddypress .buddypress-wrap input[type="submit"].pending,
+.buddypress .buddypress-wrap input[type="button"].pending,
+.buddypress .buddypress-wrap input[type="reset"].pending,
+.buddypress .buddypress-wrap input[type="button"].disabled,
+.buddypress .buddypress-wrap input[type="reset"].disabled,
+.buddypress .buddypress-wrap input[type="submit"][disabled="disabled"],
+.buddypress .buddypress-wrap button.pending,
+.buddypress .buddypress-wrap button.disabled,
+.buddypress .buddypress-wrap div.pending a,
+.buddypress .buddypress-wrap a.disabled {
+	border-color: #eee;
+	color: #767676;
+	cursor: default;
+}
+
+.buddypress .buddypress-wrap input[type="submit"]:hover.pending,
+.buddypress .buddypress-wrap input[type="button"]:hover.pending,
+.buddypress .buddypress-wrap input[type="reset"]:hover.pending,
+.buddypress .buddypress-wrap input[type="submit"]:hover.disabled,
+.buddypress .buddypress-wrap input[type="button"]:hover.disabled,
+.buddypress .buddypress-wrap input[type="reset"]:hover.disabled,
+.buddypress .buddypress-wrap button.pending:hover,
+.buddypress .buddypress-wrap button.disabled:hover,
+.buddypress .buddypress-wrap div.pending a:hover,
+.buddypress .buddypress-wrap a.disabled:hover {
+	border-color: #eee;
+	color: #767676;
+}
+
+.buddypress .buddypress-wrap button.text-button,
+.buddypress .buddypress-wrap input.text-button {
+	background: none;
+	border: 0;
+	box-shadow: none;
+	color: #767676;
+}
+
+.buddypress .buddypress-wrap button.text-button.small,
+.buddypress .buddypress-wrap input.text-button.small {
+	font-size: 13px;
+}
+
+.buddypress .buddypress-wrap button.text-button:focus, .buddypress .buddypress-wrap button.text-button:hover,
+.buddypress .buddypress-wrap input.text-button:focus,
+.buddypress .buddypress-wrap input.text-button:hover {
+	background: none;
+	text-decoration: underline;
+}
+
+.buddypress .buddypress-wrap .activity-list a.button {
+	border: none;
+}
+
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li a.invite-button:hover {
+	color: #1fb3dd;
+}
+
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li.selected a.invite-button:hover,
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li.selected a.group-remove-invite-button:hover,
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li a.invite-button:hover,
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li a.group-remove-invite-button:hover {
+	color: #a00;
+}
+
+.buddypress .buddypress-wrap #item-buttons:empty {
+	display: none;
+}
+
+.buddypress .buddypress-wrap input:disabled:hover, .buddypress .buddypress-wrap input:disabled:focus {
+	background: none;
+}
+
+.buddypress .buddypress-wrap .text-links-list a.button {
+	background: none;
+	border: none;
+	border-left: 1px solid #eee;
+	color: #737373;
+	display: inline-block;
+	padding: 0.3em 1em;
+}
+
+.buddypress .buddypress-wrap .text-links-list a.button:visited {
+	color: #d6d6d6;
+}
+
+.buddypress .buddypress-wrap .text-links-list a.button:focus, .buddypress .buddypress-wrap .text-links-list a.button:hover {
+	color: #5087e5;
+}
+
+.buddypress .buddypress-wrap .text-links-list a:first-child {
+	padding-right: 0;
+}
+
+.buddypress .buddypress-wrap .text-links-list a:last-child {
+	border-left: none;
+}
+
+.buddypress .buddypress-wrap .bp-list.grid .action a,
+.buddypress .buddypress-wrap .bp-list.grid .action button {
+	border: 1px solid #ccc;
+	display: block;
+	margin: 0;
+}
+
+.buddypress .buddypress-wrap .bp-list.grid .action a:focus, .buddypress .buddypress-wrap .bp-list.grid .action a:hover,
+.buddypress .buddypress-wrap .bp-list.grid .action button:focus,
+.buddypress .buddypress-wrap .bp-list.grid .action button:hover {
+	background: #ededed;
+}
+
+.buddypress #buddypress .create-button {
+	background: none;
+	text-align: center;
+}
+
+.buddypress #buddypress .create-button a:focus,
+.buddypress #buddypress .create-button a:hover {
+	text-decoration: underline;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress #buddypress .create-button {
+		float: left;
+	}
+}
+
+.buddypress #buddypress .create-button a {
+	border: 1px solid #ccc;
+	border-radius: 5px;
+	background-clip: padding-box;
+	box-shadow: inset 0 0 6px 0 #eaeaea;
+	margin: 0.2em 0;
+	width: auto;
+}
+
+.buddypress #buddypress .create-button a:focus, .buddypress #buddypress .create-button a:hover {
+	background: none;
+	border-color: #ccc;
+	box-shadow: inset 0 0 12px 0 #eaeaea;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress #buddypress.bp-dir-vert-nav .create-button {
+		float: none;
+		padding-top: 2em;
+	}
+	.buddypress #buddypress.bp-dir-vert-nav .create-button a {
+		margin-left: 0.5em;
+	}
+}
+
+.buddypress #buddypress.bp-dir-hori-nav .create-button {
+	float: right;
+}
+
+.buddypress #buddypress.bp-dir-hori-nav .create-button a,
+.buddypress #buddypress.bp-dir-hori-nav .create-button a:hover {
+	background: none;
+	border: 0;
+	box-shadow: none;
+	margin: 0;
+}
+
+.buddypress-wrap button.bp-icons, .buddypress-wrap button.ac-reply-cancel {
+	background: none;
+	border: 0;
+}
+
+.buddypress-wrap button.bp-icons:focus, .buddypress-wrap button.bp-icons:hover {
+	background: none;
+}
+
+.buddypress-wrap button.ac-reply-cancel:focus, .buddypress-wrap button.ac-reply-cancel:hover {
+	background: none;
+	text-decoration: underline;
+}
+
+.buddypress-wrap .filter label:before,
+.buddypress-wrap .feed a:before,
+.buddypress-wrap .bp-invites-filters .invite-button span.icons:before,
+.buddypress-wrap .bp-messages-filters li a.messages-button:before,
+.buddypress-wrap .bp-invites-content li .invite-button span.icons:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.buddypress-wrap .bp-invites-content .item-list li .invite-button span.icons:before {
+	font-size: 27px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-invites-content .item-list li .invite-button span.icons:before {
+		font-size: 32px;
+	}
+}
+
+.buddypress-wrap .bp-list a.button.invite-button:focus, .buddypress-wrap .bp-list a.button.invite-button:hover {
+	background: none;
+}
+
+.buddypress-wrap .filter label:before {
+	content: "\f536";
+}
+
+.buddypress-wrap div.feed a:before,
+.buddypress-wrap li.feed a:before {
+	content: "\f303";
+}
+
+.buddypress-wrap ul.item-list li .invite-button:not(.group-remove-invite-button) span.icons:before {
+	content: "\f502";
+}
+
+.buddypress-wrap ul.item-list li.selected .invite-button span.icons:before,
+.buddypress-wrap ul.item-list li .group-remove-invite-button span.icons:before {
+	content: "\f153";
+}
+
+.buddypress-wrap .bp-invites-filters ul li #bp-invites-next-page:before,
+.buddypress-wrap .bp-messages-filters ul li #bp-messages-next-page:before {
+	content: "\f345";
+}
+
+.buddypress-wrap .bp-invites-filters ul li #bp-invites-prev-page:before,
+.buddypress-wrap .bp-messages-filters ul li #bp-messages-prev-page:before {
+	content: "\f341";
+}
+
+.buddypress-wrap .warn {
+	color: #b71717;
+}
+
+.buddypress-wrap .bp-messages {
+	border: 1px solid #ccc;
+	margin: 0 0 15px;
+}
+
+.buddypress-wrap .bp-messages .sitewide-notices {
+	display: block;
+	margin: 5px;
+	padding: 0.5em;
+}
+
+.buddypress-wrap .bp-messages.info {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .bp-messages.updated {
+	clear: both;
+	display: block;
+}
+
+.buddypress-wrap .bp-messages.bp-user-messages-feedback {
+	border: 0;
+}
+
+.buddypress-wrap #group-create-body .bp-cover-image-status p.warning {
+	background: #0b80a4;
+	border: 0;
+	box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.2);
+	color: #fff;
+}
+
+.buddypress-wrap .bp-feedback:not(.custom-homepage-info) {
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: row nowrap;
+	flex-flow: row nowrap;
+	align-items: stretch;
+}
+
+.buddypress-wrap .bp-feedback {
+	background: #fff;
+	color: #807f7f;
+	box-shadow: 0 1px 1px 1px rgba(0, 0, 0, 0.1);
+	color: #737373;
+	margin: 10px 0;
+	position: relative;
+}
+
+.buddypress-wrap .bp-feedback p {
+	margin: 0;
+}
+
+.buddypress-wrap .bp-feedback span.bp-icon {
+	color: #fff;
+	display: block;
+	font-family: dashicons;
+	right: 0;
+	margin-left: 10px;
+	position: relative;
+	padding: 0  0.5em;
+}
+
+.buddypress-wrap .bp-feedback .bp-help-text {
+	font-style: italic;
+}
+
+.buddypress-wrap .bp-feedback .text {
+	font-size: 14px;
+	margin: 0;
+	padding: 0.5em 0;
+}
+
+.buddypress-wrap .bp-feedback.no-icon {
+	padding: 0.5em;
+}
+
+.buddypress-wrap .bp-feedback.small:before {
+	line-height: inherit;
+}
+
+.buddypress-wrap a[data-bp-close] span:before,
+.buddypress-wrap button[data-bp-close] span:before {
+	font-size: 32px;
+}
+
+.buddypress-wrap a[data-bp-close],
+.buddypress-wrap button[data-bp-close] {
+	border: 0;
+	position: absolute;
+	top: 10px;
+	left: 10px;
+	width: 32px;
+}
+
+.buddypress-wrap .bp-feedback.no-icon a[data-bp-close],
+.buddypress-wrap .bp-feedback.no-icon button[data-bp-close] {
+	top: -6px;
+	left: 6px;
+}
+
+.buddypress-wrap button[data-bp-close]:hover {
+	background-color: transparent;
+}
+
+.buddypress-wrap .bp-feedback p {
+	margin: 0;
+}
+
+.buddypress-wrap .bp-feedback .bp-icon {
+	font-size: 20px;
+	padding: 0 2px;
+}
+
+.buddypress-wrap .bp-feedback.info .bp-icon,
+.buddypress-wrap .bp-feedback.help .bp-icon,
+.buddypress-wrap .bp-feedback.error .bp-icon,
+.buddypress-wrap .bp-feedback.warning .bp-icon,
+.buddypress-wrap .bp-feedback.loading .bp-icon,
+.buddypress-wrap .bp-feedback.success .bp-icon,
+.buddypress-wrap .bp-feedback.updated .bp-icon {
+	display: flex;
+	align-items: center;
+}
+
+.buddypress-wrap .bp-feedback.info .bp-icon,
+.buddypress-wrap .bp-feedback.help .bp-icon {
+	background-color: #0b80a4;
+}
+
+.buddypress-wrap .bp-feedback.info .bp-icon:before,
+.buddypress-wrap .bp-feedback.help .bp-icon:before {
+	content: "\f348";
+}
+
+.buddypress-wrap .bp-feedback.error .bp-icon,
+.buddypress-wrap .bp-feedback.warning .bp-icon {
+	background-color: #d33;
+}
+
+.buddypress-wrap .bp-feedback.error .bp-icon:before,
+.buddypress-wrap .bp-feedback.warning .bp-icon:before {
+	content: "\f534";
+}
+
+.buddypress-wrap .bp-feedback.loading .bp-icon {
+	background-color: #ffd087;
+}
+
+.buddypress-wrap .bp-feedback.loading .bp-icon:before {
+	content: "\f469";
+}
+
+.buddypress-wrap .bp-feedback.success .bp-icon,
+.buddypress-wrap .bp-feedback.updated .bp-icon {
+	background-color: #8a2;
+}
+
+.buddypress-wrap .bp-feedback.success .bp-icon:before,
+.buddypress-wrap .bp-feedback.updated .bp-icon:before {
+	content: "\f147";
+}
+
+.buddypress-wrap .bp-feedback.help .bp-icon:before {
+	content: "\f468";
+}
+
+.buddypress-wrap #pass-strength-result {
+	background-color: #eee;
+	border-color: #ddd;
+	border-style: solid;
+	border-width: 1px;
+	display: none;
+	font-weight: 700;
+	margin: 10px 0 10px 0;
+	padding: 0.5em;
+	text-align: center;
+	width: auto;
+}
+
+.buddypress-wrap #pass-strength-result.show {
+	display: block;
+}
+
+.buddypress-wrap #pass-strength-result.mismatch {
+	background-color: #333;
+	border-color: transparent;
+	color: #fff;
+}
+
+.buddypress-wrap #pass-strength-result.error, .buddypress-wrap #pass-strength-result.bad {
+	background-color: #ffb78c;
+	border-color: #ff853c;
+	color: #fff;
+}
+
+.buddypress-wrap #pass-strength-result.short {
+	background-color: #ffa0a0;
+	border-color: #f04040;
+	color: #fff;
+}
+
+.buddypress-wrap #pass-strength-result.strong {
+	background-color: #66d66e;
+	border-color: #438c48;
+	color: #fff;
+}
+
+.buddypress-wrap .standard-form#signup_form div div.error {
+	background: #faa;
+	color: #a00;
+	margin: 0 0 10px 0;
+	padding: 0.5em;
+	width: 90%;
+}
+
+.buddypress-wrap .accept,
+.buddypress-wrap .reject {
+	float: right;
+	margin-right: 10px;
+}
+
+.buddypress-wrap .members-list.grid .bp-ajax-message {
+	background: rgba(255, 255, 255, 0.9);
+	border: 1px solid #eee;
+	font-size: 14px;
+	right: 2%;
+	position: absolute;
+	padding: 0.5em 1em;
+	left: 2%;
+	top: 30px;
+}
+
+.buddypress.widget .item-options {
+	font-size: 12px;
+	margin: 0 0 1em;
+	padding: 1em 0;
+}
+
+.buddypress.widget .bp-login-widget-user-avatar {
+	float: right;
+}
+
+.buddypress.widget .bp-login-widget-user-links {
+	margin-right: 70px;
+}
+
+.buddypress.widget ul.item-list {
+	list-style: none;
+	margin: 10px 0;
+}
+
+.buddypress.widget ul.activity-list {
+	padding: 0;
+}
+
+.buddypress.widget ul.activity-list blockquote {
+	margin: 0 0 1.5em;
+	overflow: visible;
+	padding: 0 0.75em 0.75em 0;
+}
+
+.buddypress.widget ul.activity-list img {
+	margin-bottom: 0.5em;
+}
+
+.buddypress.widget ul.activity-list li {
+	border-bottom: 1px solid #ccc;
+	margin-bottom: 1em;
+}
+
+.buddypress.widget ul.activity-list li .activity-header p {
+	margin-bottom: 0.5em;
+}
+
+.buddypress.widget ul.activity-list li:last-child {
+	border-bottom: 0;
+}
+
+.buddypress.widget .avatar-block {
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: row wrap;
+	flex-flow: row wrap;
+}
+
+.buddypress.widget .avatar-block img {
+	margin: 0.5em;
+}
+
+.buddypress.widget ul#friends-list li:before,
+.buddypress.widget ul#friends-list li:after,
+.buddypress.widget ul#groups-list li:before,
+.buddypress.widget ul#groups-list li:after,
+.buddypress.widget ul#members-list li:before,
+.buddypress.widget ul#members-list li:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress.widget ul#friends-list li:after,
+.buddypress.widget ul#groups-list li:after,
+.buddypress.widget ul#members-list li:after {
+	clear: both;
+}
+
+.buddypress.widget ul#friends-list li,
+.buddypress.widget ul#groups-list li,
+.buddypress.widget ul#members-list li {
+	margin-bottom: 1em;
+}
+
+.buddypress.widget ul#friends-list li .item-avatar,
+.buddypress.widget ul#groups-list li .item-avatar,
+.buddypress.widget ul#members-list li .item-avatar {
+	float: right;
+	width: 60px;
+}
+
+.buddypress.widget ul#friends-list li .item,
+.buddypress.widget ul#groups-list li .item,
+.buddypress.widget ul#members-list li .item {
+	margin-right: 70px;
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list,
+.buddypress-wrap .buddypress.widget ul#groups-list,
+.buddypress-wrap .buddypress.widget ul#members-list {
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: column nowrap;
+	flex-flow: column nowrap;
+}
+
+@media screen and (min-width: 32em) {
+	.buddypress-wrap .buddypress.widget ul#friends-list,
+	.buddypress-wrap .buddypress.widget ul#groups-list,
+	.buddypress-wrap .buddypress.widget ul#members-list {
+		display: -moz-flex;
+		display: -ms-flex;
+		display: -o-flex;
+		display: flex;
+		-o-flex-flow: row wrap;
+		flex-flow: row wrap;
+	}
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li,
+.buddypress-wrap .buddypress.widget ul#groups-list li,
+.buddypress-wrap .buddypress.widget ul#members-list li {
+	border: 1px solid #eee;
+	align-items: stretch;
+	-moz-flex: 1 1 46%;
+	-o-flex: 1 1 46%;
+	flex: 1 1 46%;
+	margin: 2%;
+}
+
+@media screen and (min-width: 75em) {
+	.buddypress-wrap .buddypress.widget ul#friends-list li,
+	.buddypress-wrap .buddypress.widget ul#groups-list li,
+	.buddypress-wrap .buddypress.widget ul#members-list li {
+		-moz-flex: 0 1 20%;
+		-o-flex: 0 1 20%;
+		flex: 0 1 20%;
+	}
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li .item-avatar,
+.buddypress-wrap .buddypress.widget ul#groups-list li .item-avatar,
+.buddypress-wrap .buddypress.widget ul#members-list li .item-avatar {
+	padding: 0.5em;
+	text-align: center;
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li .item,
+.buddypress-wrap .buddypress.widget ul#groups-list li .item,
+.buddypress-wrap .buddypress.widget ul#members-list li .item {
+	padding: 0.5em;
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li .item .item-meta,
+.buddypress-wrap .buddypress.widget ul#groups-list li .item .item-meta,
+.buddypress-wrap .buddypress.widget ul#members-list li .item .item-meta {
+	font-size: 12px;
+	overflow-wrap: break-word;
+}
+
+@media screen and (min-width: 75em) {
+	.buddypress-wrap .buddypress.widget ul#friends-list,
+	.buddypress-wrap .buddypress.widget ul#groups-list,
+	.buddypress-wrap .buddypress.widget ul#members-list {
+		margin: 10px -2%;
+		width: 100%;
+	}
+	.buddypress-wrap .buddypress.widget ul#friends-list li,
+	.buddypress-wrap .buddypress.widget ul#groups-list li,
+	.buddypress-wrap .buddypress.widget ul#members-list li {
+		-moz-flex: 0 1 auto;
+		-o-flex: 0 1 auto;
+		flex: 0 1 auto;
+		margin: 10px 2% 1%;
+		width: 46%;
+	}
+}
+
+.buddypress-wrap .buddypress.widget blockquote {
+	margin: 0 0 1.5em;
+	overflow: visible;
+	padding: 0 0.75em 0.75em 0;
+}
+
+#buddypress-wrap * {
+	transition: opacity 0.1s ease-in-out 0.1s;
+}
+
+#buddypress-wrap button,
+#buddypress-wrap a.generic-button,
+#buddypress-wrap a.button,
+#buddypress-wrap input[type="submit"],
+#buddypress-wrap input[type="reset"] {
+	transition: background 0.1s ease-in-out 0.1s, color 0.1s ease-in-out 0.1s, border-color 0.1s ease-in-out 0.1s;
+}
+
+.buddypress-wrap a.loading,
+.buddypress-wrap input.loading {
+	animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+	border-color: #aaa;
+}
+
+@keyframes loader-pulsate {
+	from {
+		border-color: #aaa;
+		box-shadow: 0 0 6px #ccc;
+	}
+	to {
+		border-color: #ccc;
+		box-shadow: 0 0 6px #f8f8f8;
+	}
+}
+
+.buddypress-wrap a.loading:hover,
+.buddypress-wrap input.loading:hover {
+	color: #777;
+}
+
+[data-bp-tooltip] {
+	position: relative;
+}
+
+[data-bp-tooltip]:after {
+	background-color: #fff;
+	display: none;
+	opacity: 0;
+	position: absolute;
+	transform: translate3d(0, 0, 0);
+	visibility: hidden;
+}
+
+[data-bp-tooltip]:after {
+	border: 1px solid #737373;
+	border-radius: 1px;
+	box-shadow: -4px 4px 8px rgba(0, 0, 0, 0.2);
+	color: #333;
+	content: attr(data-bp-tooltip);
+	font-family: "Helvetica Neue", helvetica, arial, san-serif;
+	font-size: 12px;
+	font-weight: 400;
+	letter-spacing: normal;
+	line-height: 1.25;
+	max-width: 200px;
+	padding: 5px 8px;
+	pointer-events: none;
+	text-shadow: none;
+	text-transform: none;
+	transition: all 1.5s ease;
+	white-space: nowrap;
+	word-wrap: break-word;
+	z-index: 100000;
+}
+
+[data-bp-tooltip]:hover:after, [data-bp-tooltip]:active:after, [data-bp-tooltip]:focus:after {
+	display: block;
+	opacity: 1;
+	overflow: visible;
+	visibility: visible;
+}
+
+[data-bp-tooltip=""] {
+	display: none;
+	opacity: 0;
+	visibility: hidden;
+}
+
+.bp-tooltip:after {
+	right: 50%;
+	margin-top: 7px;
+	top: 110%;
+	transform: translate(50%, 0);
+}
+
+.user-list .bp-tooltip:after {
+	right: 0;
+	transform: translate(0, 0);
+}
+
+@media screen and (min-width: 46.8em) {
+	.user-list .bp-tooltip:after {
+		right: auto;
+		left: 0;
+		transform: translate(0, 0);
+	}
+}
+
+.activity-list .bp-tooltip:after,
+.activity-meta-action .bp-tooltip:after,
+.avatar-block .item-avatar .bp-tooltip:after,
+.notification-actions .bp-tooltip:after,
+.participants-list .bp-tooltip:after {
+	right: 0;
+	transform: translate(0, 0);
+}
+
+.bp-invites-content .bp-tooltip:after,
+.message-metadata .actions .bp-tooltip:after,
+.single-message-thread-header .actions .bp-tooltip:after {
+	right: auto;
+	left: 0;
+	transform: translate(0, 0);
+}
+
+.bp-invites-content #send-invites-editor .bp-tooltip:after {
+	right: 0;
+	left: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Layout classes
+*-------------------------------------------------------------------------------
+*/
+#item-body,
+.single-screen-navs {
+	box-sizing: border-box;
+}
+
+.grid > li,
+.grid > li .generic-button a {
+	box-sizing: border-box;
+}
+
+.grid > li {
+	border-bottom: 0;
+	padding-bottom: 10px;
+	padding-top: 0;
+}
+
+.grid > li .list-wrap {
+	background: #fafafa;
+	border: 1px solid #eee;
+	padding-bottom: 15px;
+	position: relative;
+	overflow: hidden;
+	padding-top: 14px;
+}
+
+.grid > li .list-wrap .list-title {
+	padding: 0.5em;
+}
+
+.grid > li .list-wrap .update {
+	color: #737373;
+	padding: 0.5em 2em;
+}
+
+.grid > li .item-avatar {
+	text-align: center;
+}
+
+.grid > li .item-avatar .avatar {
+	border-radius: 50%;
+	display: inline-block;
+	width: 50%;
+}
+
+@media screen and (min-width: 24em) {
+	.grid.members-list .list-wrap {
+		min-height: 340px;
+	}
+	.grid.members-list .list-wrap .item-block {
+		margin: 0 auto;
+		min-height: 7rem;
+	}
+	.grid.members-group-list .list-wrap .item-block {
+		margin: 0 auto;
+		min-height: 7rem;
+	}
+	.grid.groups-list .list-wrap {
+		min-height: 470px;
+	}
+	.grid.groups-list .list-wrap .item-block {
+		min-height: 6rem;
+	}
+	.grid.groups-list .list-wrap .group-desc {
+		margin: 15px auto 0;
+		min-height: 5em;
+		overflow: hidden;
+	}
+	.grid.groups-list .list-wrap .last-activity,
+	.grid.groups-list .list-wrap .group-details,
+	.grid.groups-list .list-wrap .item-desc {
+		margin-bottom: 0;
+	}
+	.grid.groups-list .list-wrap .last-activity p,
+	.grid.groups-list .list-wrap .group-details p,
+	.grid.groups-list .list-wrap .item-desc p {
+		margin-bottom: 0;
+	}
+	.grid.blogs-list .list-wrap {
+		min-height: 350px;
+	}
+	.grid.blogs-list .list-wrap .item-block {
+		margin: 0 auto;
+		min-height: 7rem;
+	}
+}
+
+/* Build the two column class small up */
+@media screen and (min-width: 24em) {
+	.grid > li.item-entry {
+		float: right;
+		margin: 0;
+	}
+	.grid.two > li {
+		padding-bottom: 20px;
+	}
+}
+
+@media screen and (min-width: 24em) and (min-width: 75em) {
+	.grid.two > li .list-wrap {
+		max-width: 500px;
+		margin: 0 auto;
+	}
+}
+
+@media screen and (min-width: 24em) {
+	.grid.two > li, .grid.three > li {
+		width: 50%;
+	}
+	.grid.two > li:nth-child(odd), .grid.three > li:nth-child(odd) {
+		padding-left: 10px;
+	}
+	.grid.two > li:nth-child(even), .grid.three > li:nth-child(even) {
+		padding-right: 10px;
+	}
+	.grid.two > li .item, .grid.three > li .item {
+		margin: 1rem auto 0;
+		width: 80%;
+	}
+	.grid.two > li .item .item-title, .grid.three > li .item .item-title {
+		width: auto;
+	}
+}
+
+/* Build the three column class medium up */
+@media screen and (min-width: 46.8em) {
+	.grid.three > li {
+		padding-top: 0;
+		width: 33.333333%;
+		width: calc(100% / 3);
+	}
+	.grid.three > li:nth-child(1n+1) {
+		padding-right: 5px;
+		padding-left: 5px;
+	}
+	.grid.three > li:nth-child(3n+3) {
+		padding-right: 5px;
+		padding-left: 0;
+	}
+	.grid.three > li:nth-child(3n+1) {
+		padding-right: 0;
+		padding-left: 5px;
+	}
+}
+
+/* Build the four column class medium up */
+@media screen and (min-width: 46.8em) {
+	.grid.four > li {
+		width: 25%;
+	}
+	.grid.four > li:nth-child(1n+1) {
+		padding-right: 5px;
+		padding-left: 5px;
+	}
+	.grid.four > li:nth-child(4n+4) {
+		padding-right: 5px;
+		padding-left: 0;
+	}
+	.grid.four > li:nth-child(4n+1) {
+		padding-right: 0;
+		padding-left: 5px;
+	}
+}
+
+.buddypress-wrap .grid.bp-list {
+	padding-top: 1em;
+}
+
+.buddypress-wrap .grid.bp-list > li {
+	border-bottom: none;
+}
+
+.buddypress-wrap .grid.bp-list > li .list-wrap {
+	padding-bottom: 3em;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-avatar {
+	margin: 0;
+	text-align: center;
+	width: auto;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-avatar img.avatar {
+	display: inline-block;
+	height: auto;
+	width: 50%;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-meta,
+.buddypress-wrap .grid.bp-list > li .list-title {
+	float: none;
+	text-align: center;
+}
+
+.buddypress-wrap .grid.bp-list > li .list-title {
+	font-size: inherit;
+	line-height: 1.1;
+}
+
+.buddypress-wrap .grid.bp-list > li .item {
+	font-size: 18px;
+	right: 0;
+	margin: 0 auto;
+	text-align: center;
+	width: 96%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .grid.bp-list > li .item {
+		font-size: 22px;
+	}
+}
+
+.buddypress-wrap .grid.bp-list > li .item .item-block,
+.buddypress-wrap .grid.bp-list > li .item .group-desc {
+	float: none;
+	width: 96%;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .item-block {
+	margin-bottom: 10px;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .last-activity {
+	margin-top: 5px;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .group-desc {
+	clear: none;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .user-update {
+	clear: both;
+	text-align: right;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .activity-read-more a {
+	display: inline;
+}
+
+.buddypress-wrap .grid.bp-list > li .action {
+	bottom: 5px;
+	float: none;
+	height: auto;
+	right: 0;
+	margin: 0;
+	padding: 0 5px;
+	position: absolute;
+	text-align: center;
+	top: auto;
+	width: 100%;
+}
+
+.buddypress-wrap .grid.bp-list > li .action .generic-button {
+	float: none;
+	margin: 5px 0 0;
+	text-align: center;
+	width: 100%;
+}
+
+.buddypress-wrap .grid.bp-list > li .action .generic-button a,
+.buddypress-wrap .grid.bp-list > li .action .generic-button button {
+	width: 100%;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-avatar,
+.buddypress-wrap .grid.bp-list > li .avatar,
+.buddypress-wrap .grid.bp-list > li .item {
+	float: none;
+}
+
+.buddypress-wrap .blogs-list.grid.two > li .blogs-title {
+	min-height: 5em;
+}
+
+.buddypress-wrap .grid.three > li .group-desc,
+.buddypress-wrap .grid.four > li .group-desc {
+	min-height: 8em;
+}
+
+.buddypress-wrap .blogs-list.grid.three > li,
+.buddypress-wrap .blogs-list.grid.four > li {
+	min-height: 350px;
+}
+
+.buddypress-wrap .blogs-list.grid.three > li .last-activity,
+.buddypress-wrap .blogs-list.grid.four > li .last-activity {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .blogs-list.grid.three > li .last-post,
+.buddypress-wrap .blogs-list.grid.four > li .last-post {
+	margin-top: 0;
+}
+
+.buddypress:not(.logged-in) .grid.bp-list .list-wrap {
+	padding-bottom: 5px;
+}
+
+.buddypress:not(.logged-in) .grid.groups-list .list-wrap {
+	min-height: 430px;
+}
+
+.buddypress:not(.logged-in) .grid.members-list .list-wrap {
+	min-height: 300px;
+}
+
+.buddypress:not(.logged-in) .grid.blogs-list .list-wrap {
+	min-height: 320px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-single-vert-nav .bp-navs.vertical {
+		overflow: visible;
+	}
+	.bp-single-vert-nav .bp-navs.vertical ul {
+		border-left: 1px solid #d6d6d6;
+		border-bottom: 0;
+		float: right;
+		margin-left: -1px;
+		width: 25%;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li {
+		float: none;
+		margin-left: 0;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li.selected a {
+		background: #ccc;
+		color: #333;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li:focus, .bp-single-vert-nav .bp-navs.vertical li:hover {
+		background: #ccc;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li span {
+		background: #d6d6d6;
+		border-radius: 10%;
+		float: left;
+		margin-left: 2px;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li:hover span {
+		border-color: #eaeaea;
+	}
+	.bp-single-vert-nav .bp-navs.vertical.tabbed-links li.selected a {
+		padding-right: 0;
+	}
+	.bp-single-vert-nav .bp-wrap {
+		margin-bottom: 15px;
+	}
+	.bp-single-vert-nav .bp-wrap .user-nav-tabs.users-nav ul li,
+	.bp-single-vert-nav .bp-wrap .group-nav-tabs.groups-nav ul li {
+		right: 1px;
+		position: relative;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) {
+		background: #fff;
+		border-right: 1px solid #d6d6d6;
+		float: left;
+		margin: 0;
+		min-height: 400px;
+		padding: 0 1em 0 0;
+		width: calc(75% + 1px);
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) {
+		background: #eaeaea;
+		margin: 0 -5px 0 0;
+		width: auto;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li {
+		font-size: 16px;
+		margin: 10px 0;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li a {
+		border-left: 1px solid #ccc;
+		padding: 0 0.5em;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li a:focus,
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li a:hover {
+		background: none;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li.current a {
+		background: none;
+		color: #333;
+		text-decoration: underline;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li:last-child a {
+		border: none;
+	}
+	.bp-dir-vert-nav .dir-navs {
+		float: right;
+		right: 1px;
+		position: relative;
+		width: 20%;
+	}
+	.bp-dir-vert-nav .dir-navs ul li {
+		float: none;
+		overflow: hidden;
+		width: auto;
+	}
+	.bp-dir-vert-nav .dir-navs ul li.selected {
+		border: 1px solid #eee;
+	}
+	.bp-dir-vert-nav .dir-navs ul li.selected a {
+		background: #555;
+		color: #fff;
+	}
+	.bp-dir-vert-nav .dir-navs ul li.selected a span {
+		background: #eaeaea;
+		border-color: #ccc;
+		color: #5087e5;
+	}
+	.bp-dir-vert-nav .dir-navs ul li a:hover,
+	.bp-dir-vert-nav .dir-navs ul li a:focus {
+		background: #ccc;
+		color: #333;
+	}
+	.bp-dir-vert-nav .dir-navs ul li a:hover span,
+	.bp-dir-vert-nav .dir-navs ul li a:focus span {
+		border: 1px solid #555;
+	}
+	.bp-dir-vert-nav .screen-content {
+		border-right: 1px solid #d6d6d6;
+		margin-right: 20%;
+		overflow: hidden;
+		padding: 0 1em 2em 0;
+	}
+	.bp-dir-vert-nav .screen-content .subnav-filters {
+		margin-top: 0;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li:not(.selected) a:focus {
+		background: none;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li.selected {
+		background: none;
+		border: 1px solid #d6d6d6;
+		border-left-color: #fff;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li.selected a {
+		background: none;
+		color: #333;
+		font-weight: 600;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li.selected a span {
+		background: #555;
+		border: 1px solid #d6d6d6;
+		color: #fff;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/buddypress.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/buddypress.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/buddypress.css	(revision 12265)
@@ -0,0 +1,5176 @@
+/*--------------------------------------------------------------
+Hello, this is the BuddyPress Nouveau stylesheet.
+
+@version 4.0.0
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 - BP Generic, Typography & Imagery
+
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+
+3.0 - BP Lists / Loops Generic & filters
+	3.1 - Activity Loop
+		3.1.1 Whats New Activity
+		3.1.2 - Activity Entries
+		3.1.3 - Activity Comments
+	3.2 - Blogs Loop
+	3.3 - Groups Loop
+	3.4 - Members Loop
+
+4.0 - Directories - Members, Groups, Blogs, Register, Activation
+	4.1 - Groups Creation Steps Screens
+5.0 - Single Item screens: User Account & Single Group Screens
+	5.1 - Item Headers: Global
+		5.1.1 - item-header: Groups
+		5.1.2 - item-header: User Accounts
+	5.2 - Item Body: Global
+		5.2.1 - item-body: Groups
+			5.2.1.1 - Management settings screens
+			5.2.1.2 - Group Members list
+			5.2.1.3 - Group Invite list
+			5.2.1.4 - Group Activity
+		5.2.2 - item-body: User Accounts
+			5.2.2.1 - classes, pag, filters
+			5.2.2.2 - Extended Profiles
+			5.2.2.3 - Groups
+			5.2.2.4 - friends
+			5.2.2.5 - Private Messaging Threads
+			5.2.2.6 - Settings
+
+6.0 - Forms  -  General
+	6.1 - Dir Search
+
+7.0 - Tables -  General
+
+8.0 - Classes - Messages, Ajax, Widgets, Buttons, Tooltips
+
+9.0 - Layout Classes.
+--------------------------------------------------------------*/
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - BP Generic, Typography & Imagery
+*-------------------------------------------------------------------------------
+*/
+body #buddypress * a {
+	box-shadow: none;
+	text-decoration: none;
+}
+
+body #buddypress #item-body blockquote,
+body #buddypress .bp-lists blockquote {
+	margin-left: 0;
+}
+
+body #buddypress .bp-list .action {
+	box-sizing: border-box;
+}
+
+@media screen and (min-width: 46.8em) {
+	body.buddypress .entry-header,
+	body.buddypress .site-content .entry-header,
+	body.buddypress .entry-content {
+		max-width: none;
+	}
+	body.buddypress .entry-header {
+		float: none;
+		max-width: none;
+	}
+	body.buddypress .entry-content {
+		float: none;
+		max-width: none;
+	}
+	body.buddypress .site-content {
+		padding-top: 2.5em;
+	}
+	body.buddypress #page #primary {
+		max-width: none;
+	}
+	body.buddypress #page #primary .entry-header,
+	body.buddypress #page #primary .entry-content {
+		float: none;
+		width: auto;
+	}
+}
+
+body.buddypress .buddypress-wrap h1,
+body.buddypress .buddypress-wrap h2,
+body.buddypress .buddypress-wrap h3,
+body.buddypress .buddypress-wrap h4,
+body.buddypress .buddypress-wrap h5,
+body.buddypress .buddypress-wrap h6 {
+	clear: none;
+	margin: 1em 0;
+	padding: 0;
+}
+
+/* Ensure .bp-wrap encloses it's children */
+.bp-wrap:before,
+.bp-wrap:after {
+	content: " ";
+	display: table;
+}
+
+.bp-wrap:after {
+	clear: both;
+}
+
+.buddypress-wrap.round-avatars .avatar {
+	border-radius: 50%;
+}
+
+div,
+dl,
+li,
+textarea,
+select,
+input[type="search"],
+input[type="submit"],
+input[type="reset"] {
+	border-radius: 2px;
+	background-clip: padding-box;
+}
+
+body.buddypress article.page > .entry-header {
+	margin-bottom: 2em;
+	padding: 0;
+}
+
+body.buddypress article.page > .entry-header .entry-title {
+	font-size: 28px;
+	font-weight: inherit;
+	color: #767676;
+}
+
+@media screen and (min-width: 46.8em) {
+	body.buddypress article.page > .entry-header .entry-title {
+		font-size: 34px;
+	}
+}
+
+.buddypress-wrap dt.section-title {
+	font-size: 18px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap dt.section-title {
+		font-size: 22px;
+	}
+}
+
+.buddypress-wrap .bp-label-text,
+.buddypress-wrap .message-threads {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-label-text,
+	.buddypress-wrap .message-threads {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .activity-header {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .activity-header {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .activity-inner {
+	font-size: 15px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .activity-inner {
+		font-size: 18px;
+	}
+}
+
+.buddypress-wrap #whats-new-post-in {
+	font-size: 16px;
+}
+
+.buddypress-wrap .mini .activity-header,
+.buddypress-wrap .acomment-meta {
+	font-size: 16px;
+}
+
+.buddypress-wrap .dir-component-filters #activity-filter-by {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .dir-component-filters #activity-filter-by {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .bp-tables-user th {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-tables-user th {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .bp-tables-user td {
+	font-size: 12px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-tables-user td {
+		font-size: 14px;
+	}
+}
+
+.buddypress-wrap .profile-fields th {
+	font-size: 15px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .profile-fields th {
+		font-size: 18px;
+	}
+}
+
+.buddypress-wrap .profile-fields td {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .profile-fields td {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap #notification-select {
+	font-size: 12px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap #notification-select {
+		font-size: 14px;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (bp-list)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+.bp-navs {
+	background: transparent;
+	clear: both;
+	overflow: hidden;
+}
+
+.bp-navs ul {
+	margin: 0;
+	padding: 0;
+}
+
+.bp-navs ul li {
+	list-style: none;
+	margin: 0;
+}
+
+.bp-navs ul li.last select {
+	max-width: 185px;
+}
+
+.bp-navs ul li a,
+.bp-navs ul li span {
+	border: 0;
+	display: block;
+	padding: 5px 10px;
+	text-decoration: none;
+}
+
+.bp-navs ul li .count {
+	background: #eaeaea;
+	border: 1px solid #ccc;
+	border-radius: 50%;
+	color: #555;
+	display: inline;
+	font-size: 12px;
+	margin-left: 2px;
+	padding: 3px 6px;
+	text-align: center;
+	vertical-align: middle;
+}
+
+.bp-navs ul li.selected a,
+.bp-navs ul li.current a {
+	color: #333;
+	opacity: 1;
+}
+
+.bp-navs.bp-invites-filters ul li a, .bp-navs.bp-messages-filters ul li a {
+	border: 1px solid #ccc;
+	display: inline-block;
+}
+
+.main-navs.dir-navs {
+	margin-bottom: 20px;
+}
+
+.buddypress-wrap .bp-navs li.selected a .count,
+.buddypress-wrap .bp-navs li.current a .count,
+.buddypress-wrap .bp-navs li a:hover a .count {
+	background-color: #ccc;
+}
+
+.buddypress-wrap .bp-navs li:not(.current) a:focus,
+.buddypress-wrap .bp-navs li:not(.current) a:hover,
+.buddypress-wrap .bp-navs li:not(.selected) a:focus,
+.buddypress-wrap .bp-navs li:not(.selected) a:hover {
+	background: #ccc;
+	color: #333;
+}
+
+.buddypress-wrap .bp-navs li.selected a,
+.buddypress-wrap .bp-navs li.selected a:focus,
+.buddypress-wrap .bp-navs li.selected a:hover,
+.buddypress-wrap .bp-navs li.current a,
+.buddypress-wrap .bp-navs li.current a:focus,
+.buddypress-wrap .bp-navs li.current a:hover {
+	background: #555;
+	color: #fafafa;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .main-navs:not(.dir-navs) li.selected a,
+	.buddypress-wrap .main-navs:not(.dir-navs) li.current a {
+		background: #fff;
+		color: #333;
+		font-weight: 600;
+	}
+	.buddypress-wrap .main-navs.vertical li.selected a,
+	.buddypress-wrap .main-navs.vertical li.current a {
+		background: #555;
+		color: #fafafa;
+		text-decoration: none;
+	}
+	.buddypress-wrap.bp-dir-hori-nav:not(.bp-vertical-navs) nav:not(.tabbed-links) {
+		border-bottom: 1px solid #eee;
+		border-top: 1px solid #eee;
+		box-shadow: 0 2px 12px 0 #fafafa;
+	}
+}
+
+.buddypress-wrap .bp-subnavs li.selected a,
+.buddypress-wrap .bp-subnavs li.current a {
+	background: #fff;
+	color: #333;
+	font-weight: 600;
+}
+
+@media screen and (max-width: 46.8em) {
+	.buddypress-wrap:not(.bp-single-vert-nav) .bp-navs li {
+		background: #eaeaea;
+	}
+}
+
+.buddypress-wrap:not(.bp-single-vert-nav) .main-navs > ul > li > a {
+	padding: 0.5em calc(0.5em + 2px);
+}
+
+.buddypress-wrap:not(.bp-single-vert-nav) .user-subnav#subsubnav,
+.buddypress-wrap:not(.bp-single-vert-nav) .group-subnav#subsubnav {
+	background: none;
+}
+
+.buddypress-wrap .bp-subnavs,
+.buddypress-wrap ul.subnav {
+	width: 100%;
+}
+
+.buddypress-wrap .bp-subnavs {
+	margin: 10px 0;
+	overflow: hidden;
+}
+
+.buddypress-wrap .bp-subnavs ul li {
+	margin-top: 0;
+}
+
+.buddypress-wrap .bp-subnavs ul li.selected :focus,
+.buddypress-wrap .bp-subnavs ul li.selected :hover, .buddypress-wrap .bp-subnavs ul li.current :focus,
+.buddypress-wrap .bp-subnavs ul li.current :hover {
+	background: none;
+	color: #333;
+}
+
+.buddypress-wrap ul.subnav {
+	width: auto;
+}
+
+.buddypress-wrap .bp-navs.bp-invites-nav#subnav ul li.last,
+.buddypress-wrap .bp-navs.bp-invites-filters#subsubnav ul li.last,
+.buddypress-wrap .bp-navs.bp-messages-filters#subsubnav ul li.last {
+	margin-top: 0;
+}
+
+@media screen and (max-width: 46.8em) {
+	.buddypress-wrap .single-screen-navs {
+		border: 1px solid #eee;
+	}
+	.buddypress-wrap .single-screen-navs li {
+		border-bottom: 1px solid #eee;
+	}
+	.buddypress-wrap .single-screen-navs li:last-child {
+		border-bottom: none;
+	}
+	.buddypress-wrap .bp-subnavs li a {
+		font-size: 14px;
+	}
+	.buddypress-wrap .bp-subnavs li.selected a,
+	.buddypress-wrap .bp-subnavs li.selected a:focus,
+	.buddypress-wrap .bp-subnavs li.selected a:hover, .buddypress-wrap .bp-subnavs li.current a,
+	.buddypress-wrap .bp-subnavs li.current a:focus,
+	.buddypress-wrap .bp-subnavs li.current a:hover {
+		background: #555;
+		color: #fff;
+	}
+}
+
+.buddypress_object_nav .bp-navs li.selected a .count,
+.buddypress_object_nav .bp-navs li.current a .count,
+.buddypress-wrap .bp-navs li.selected a .count,
+.buddypress-wrap .bp-navs li.current a .count {
+	background-color: #fff;
+}
+
+.buddypress_object_nav .bp-navs li.dynamic a .count,
+.buddypress_object_nav .bp-navs li.dynamic.selected a .count,
+.buddypress_object_nav .bp-navs li.dynamic.current a .count,
+.buddypress-wrap .bp-navs li.dynamic a .count,
+.buddypress-wrap .bp-navs li.dynamic.selected a .count,
+.buddypress-wrap .bp-navs li.dynamic.current a .count {
+	background-color: #5087e5;
+	border: 0;
+	color: #fafafa;
+}
+
+.buddypress_object_nav .bp-navs li.dynamic a:hover .count,
+.buddypress-wrap .bp-navs li.dynamic a:hover .count {
+	background-color: #5087e5;
+	border: 0;
+	color: #fff;
+}
+
+.buddypress_object_nav .bp-navs li a .count:empty,
+.buddypress-wrap .bp-navs li a .count:empty {
+	display: none;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current),
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) {
+	color: #767676;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a {
+	color: #767676;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a:focus, .buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a:hover,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a:focus,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a:hover {
+	background: none;
+	color: #333;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a[disabled]:focus, .buddypress_object_nav .bp-navs.group-create-links ul li:not(.current) a[disabled]:hover,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a[disabled]:focus,
+.buddypress-wrap .bp-navs.group-create-links ul li:not(.current) a[disabled]:hover {
+	color: #767676;
+}
+
+.buddypress_object_nav .bp-navs.group-create-links ul li.current a,
+.buddypress-wrap .bp-navs.group-create-links ul li.current a {
+	text-align: center;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-navs li {
+		float: left;
+	}
+	.buddypress-wrap .subnav {
+		float: left;
+	}
+	.buddypress-wrap ul.subnav {
+		width: auto;
+	}
+	.buddypress-wrap #subsubnav .activity-search {
+		float: left;
+	}
+	.buddypress-wrap #subsubnav .filter {
+		float: right;
+	}
+}
+
+.buddypress_object_nav .bp-navs li a .count {
+	display: inline-block;
+	float: right;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-dir-vert-nav .bp-navs.dir-navs {
+		background: none;
+	}
+	.bp-dir-vert-nav .bp-navs.dir-navs a .count {
+		float: right;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .tabbed-links ul,
+	.buddypress-wrap .tabbed-links ol {
+		border-bottom: 1px solid #ccc;
+		float: none;
+		margin: 20px 0 10px;
+	}
+	.buddypress-wrap .tabbed-links ul:before, .buddypress-wrap .tabbed-links ul:after,
+	.buddypress-wrap .tabbed-links ol:before,
+	.buddypress-wrap .tabbed-links ol:after {
+		content: " ";
+		display: block;
+	}
+	.buddypress-wrap .tabbed-links ul:after,
+	.buddypress-wrap .tabbed-links ol:after {
+		clear: both;
+	}
+	.buddypress-wrap .tabbed-links ul li,
+	.buddypress-wrap .tabbed-links ol li {
+		float: left;
+		list-style: none;
+		margin: 0 10px 0 0;
+	}
+	.buddypress-wrap .tabbed-links ul li a,
+	.buddypress-wrap .tabbed-links ul li span:not(.count),
+	.buddypress-wrap .tabbed-links ol li a,
+	.buddypress-wrap .tabbed-links ol li span:not(.count) {
+		background: none;
+		border: none;
+		display: block;
+		padding: 4px 10px;
+	}
+	.buddypress-wrap .tabbed-links ul li a:focus,
+	.buddypress-wrap .tabbed-links ul li a:hover,
+	.buddypress-wrap .tabbed-links ol li a:focus,
+	.buddypress-wrap .tabbed-links ol li a:hover {
+		background: none;
+	}
+	.buddypress-wrap .tabbed-links ul li:not(.current),
+	.buddypress-wrap .tabbed-links ol li:not(.current) {
+		margin-bottom: 2px;
+	}
+	.buddypress-wrap .tabbed-links ul li.current,
+	.buddypress-wrap .tabbed-links ol li.current {
+		border-color: #ccc #ccc #fff;
+		border-style: solid;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		border-width: 1px;
+		margin-bottom: -1px;
+		padding: 0 0.5em 1px;
+	}
+	.buddypress-wrap .tabbed-links ul li.current a,
+	.buddypress-wrap .tabbed-links ol li.current a {
+		background: none;
+		color: #333;
+	}
+	.buddypress-wrap .bp-subnavs.tabbed-links > ul {
+		margin-top: 0;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links {
+		background: none;
+		margin-top: 2px;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li a {
+		border-right: 0;
+		font-size: inherit;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li.last {
+		float: right;
+		margin: 0;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li.last a {
+		margin-top: -0.5em;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li a,
+	.buddypress-wrap .bp-navs.tabbed-links ul li a:focus,
+	.buddypress-wrap .bp-navs.tabbed-links ul li a:hover,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a:focus,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a:hover {
+		background: none;
+		border: 0;
+	}
+	.buddypress-wrap .bp-navs.tabbed-links ul li a:active,
+	.buddypress-wrap .bp-navs.tabbed-links ul li.current a:active {
+		outline: none;
+	}
+}
+
+.buddypress-wrap .dir-component-filters .filter label {
+	display: inline;
+}
+
+.buddypress-wrap .subnav-filters:before,
+.buddypress-wrap .subnav-filters:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress-wrap .subnav-filters:after {
+	clear: both;
+}
+
+.buddypress-wrap .subnav-filters {
+	background: none;
+	list-style: none;
+	margin: 15px 0 0;
+	padding: 0;
+}
+
+.buddypress-wrap .subnav-filters div {
+	margin: 0;
+}
+
+.buddypress-wrap .subnav-filters > ul {
+	float: left;
+	list-style: none;
+}
+
+.buddypress-wrap .subnav-filters.bp-messages-filters ul {
+	width: 100%;
+}
+
+.buddypress-wrap .subnav-filters.bp-messages-filters .messages-search {
+	margin-bottom: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .subnav-filters.bp-messages-filters .messages-search {
+		margin-bottom: 0;
+	}
+}
+
+.buddypress-wrap .subnav-filters div {
+	float: none;
+}
+
+.buddypress-wrap .subnav-filters div select,
+.buddypress-wrap .subnav-filters div input[type="search"] {
+	font-size: 16px;
+}
+
+.buddypress-wrap .subnav-filters div button.nouveau-search-submit {
+	padding: 5px 0.8em 6px;
+}
+
+.buddypress-wrap .subnav-filters div button#user_messages_search_submit {
+	padding: 7px 0.8em;
+}
+
+.buddypress-wrap .subnav-filters .component-filters {
+	margin-top: 10px;
+}
+
+.buddypress-wrap .subnav-filters .feed {
+	margin-right: 15px;
+}
+
+.buddypress-wrap .subnav-filters .last.filter label {
+	display: inline;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap:before,
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap:after {
+	clear: both;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap.bp-show {
+	display: inline-block;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions-wrap.bp-hide {
+	display: none;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .select-wrap {
+	border: 0;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .select-wrap:focus,
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .select-wrap:hover {
+	outline: 1px solid #d6d6d6;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-actions {
+	float: left;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions label {
+	display: inline-block;
+	font-weight: 300;
+	margin-right: 25px;
+	padding: 5px 0;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions div select {
+	-webkit-appearance: textfield;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-apply {
+	border: 0;
+	border-radius: none;
+	font-weight: 400;
+	line-height: 1.8;
+	margin: 0 0 0 10px;
+	padding: 3px 5px;
+	text-align: center;
+	text-transform: none;
+	width: auto;
+}
+
+.buddypress-wrap .subnav-filters .user-messages-bulk-actions .bulk-apply span {
+	vertical-align: middle;
+}
+
+@media screen and (min-width: 32em) {
+	.buddypress-wrap .subnav-filters li {
+		margin-bottom: 0;
+	}
+	.buddypress-wrap .subnav-filters .subnav-search,
+	.buddypress-wrap .subnav-filters .subnav-search form,
+	.buddypress-wrap .subnav-filters .feed,
+	.buddypress-wrap .subnav-filters .bp-search,
+	.buddypress-wrap .subnav-filters .dir-search,
+	.buddypress-wrap .subnav-filters .user-messages-bulk-actions,
+	.buddypress-wrap .subnav-filters .user-messages-search,
+	.buddypress-wrap .subnav-filters .group-invites-search,
+	.buddypress-wrap .subnav-filters .group-act-search {
+		float: left;
+	}
+	.buddypress-wrap .subnav-filters .last,
+	.buddypress-wrap .subnav-filters .component-filters {
+		float: right;
+		margin-top: 0;
+		width: auto;
+	}
+	.buddypress-wrap .subnav-filters .last select,
+	.buddypress-wrap .subnav-filters .component-filters select {
+		max-width: 250px;
+	}
+	.buddypress-wrap .subnav-filters .user-messages-search {
+		float: right;
+	}
+}
+
+.buddypress-wrap .notifications-options-nav input#notification-bulk-manage {
+	border: 0;
+	border-radius: 0;
+	line-height: 1.6;
+}
+
+.buddypress-wrap .group-subnav-filters .group-invites-search {
+	margin-bottom: 1em;
+}
+
+.buddypress-wrap .group-subnav-filters .last {
+	text-align: center;
+}
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+.buddypress-wrap .bp-pagination {
+	background: transparent;
+	border: 0;
+	color: #767676;
+	float: left;
+	font-size: small;
+	margin: 0;
+	padding: 0.5em 0;
+	position: relative;
+	width: 100%;
+}
+
+.buddypress-wrap .bp-pagination .pag-count {
+	float: left;
+}
+
+.buddypress-wrap .bp-pagination .bp-pagination-links {
+	float: right;
+	margin-right: 10px;
+}
+
+.buddypress-wrap .bp-pagination .bp-pagination-links span,
+.buddypress-wrap .bp-pagination .bp-pagination-links a {
+	font-size: small;
+	padding: 0 5px;
+}
+
+.buddypress-wrap .bp-pagination .bp-pagination-links a:focus,
+.buddypress-wrap .bp-pagination .bp-pagination-links a:hover {
+	opacity: 1;
+}
+
+.buddypress-wrap .bp-pagination p {
+	margin: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 3.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+.bp-list:before,
+.bp-list:after {
+	content: " ";
+	display: table;
+}
+
+.bp-list:after {
+	clear: both;
+}
+
+.bp-list {
+	box-sizing: border-box;
+	border-top: 1px solid #eaeaea;
+	clear: both;
+	list-style: none;
+	margin: 20px 0;
+	padding: 0.5em 0;
+	width: 100%;
+}
+
+.bp-list li:before,
+.bp-list li:after {
+	content: " ";
+	display: table;
+}
+
+.bp-list li:after {
+	clear: both;
+}
+
+.bp-list > li {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.bp-list li {
+	list-style: none;
+	margin: 10px 0;
+	padding: 0.5em 0;
+	position: relative;
+}
+
+.bp-list li .item-avatar {
+	text-align: center;
+}
+
+.bp-list li .item-avatar img.avatar {
+	display: inline;
+}
+
+.bp-list li .item .item-avatar,
+.bp-list li .item .list-title,
+.bp-list li .item .item-meta,
+.bp-list li .item .group-details {
+	text-align: center;
+}
+
+.bp-list li .item .list-title {
+	clear: none;
+	font-size: 22px;
+	font-weight: 400;
+	line-height: 1.1;
+	margin: 0 auto;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-list li .item .list-title {
+		font-size: 26px;
+	}
+}
+
+.bp-list li .meta,
+.bp-list li .item-meta {
+	color: #737373;
+	font-size: 12px;
+	margin-bottom: 10px;
+	margin-top: 10px;
+}
+
+.bp-list li .last-post {
+	text-align: center;
+}
+
+.bp-list li .action {
+	margin: 0;
+	text-align: center;
+}
+
+.bp-list li .action .generic-button {
+	display: inline-block;
+	font-size: 12px;
+	margin: 0 10px 0 0;
+}
+
+.bp-list li .action div.generic-button {
+	margin: 10px 0;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-list li .item-avatar {
+		float: left;
+		margin-right: 5%;
+	}
+	.bp-list li .item {
+		margin: 0;
+		overflow: hidden;
+	}
+	.bp-list li .item .item-block {
+		float: left;
+		margin-right: 2%;
+		width: 50%;
+	}
+	.bp-list li .item .list-title,
+	.bp-list li .item .item-meta {
+		float: left;
+		text-align: left;
+	}
+	.bp-list li .item .group-details,
+	.bp-list li .item .last-post {
+		text-align: left;
+	}
+	.bp-list li .group-desc,
+	.bp-list li .user-update,
+	.bp-list li .last-post {
+		clear: none;
+		overflow: hidden;
+		width: auto;
+	}
+	.bp-list li .action {
+		clear: left;
+		padding: 0;
+		text-align: left;
+	}
+	.bp-list li .action li.generic-button {
+		margin-right: 0;
+	}
+	.bp-list li .action div.generic-button {
+		margin: 0 0 10px;
+	}
+	.bp-list li .generic-button {
+		display: block;
+		margin: 0 0 5px 0;
+	}
+}
+
+@media screen and (min-width: 32em) {
+	#activity-stream {
+		clear: both;
+		padding-top: 1em;
+	}
+}
+
+.activity-list.bp-list {
+	background: #fafafa;
+	border: 1px solid #eee;
+}
+
+.activity-list.bp-list .activity-item {
+	background: #fff;
+	border: 1px solid #b7b7b7;
+	box-shadow: 0 0 6px #d2d2d2;
+	margin: 20px 0;
+}
+
+.activity-list.bp-list li:first-child {
+	margin-top: 0;
+}
+
+.friends-list {
+	list-style-type: none;
+}
+
+.friends-request-list .item-title,
+.membership-requests-list .item-title {
+	text-align: center;
+}
+
+@media screen and (min-width: 46.8em) {
+	.friends-request-list li,
+	.membership-requests-list li {
+		display: -moz-flex;
+		display: -ms-flex;
+		display: -o-flex;
+		display: flex;
+		-o-flex-flow: row nowrap;
+		flex-flow: row nowrap;
+	}
+	.friends-request-list li .item,
+	.membership-requests-list li .item {
+		-moz-flex: 1 1 auto;
+		-o-flex: 1 1 auto;
+		flex: 1 1 auto;
+	}
+	.friends-request-list li .action,
+	.membership-requests-list li .action {
+		text-align: right;
+	}
+	.friends-request-list li .item-title,
+	.membership-requests-list li .item-title {
+		font-size: 22px;
+		text-align: left;
+	}
+	.friends-request-list li .item-title h3,
+	.membership-requests-list li .item-title h3 {
+		margin: 0;
+	}
+}
+
+#notifications-user-list {
+	clear: both;
+	padding-top: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	body:not(.logged-in) .bp-list .item {
+		margin-right: 0;
+	}
+}
+
+.activity-permalink .item-list,
+.activity-permalink .item-list li.activity-item {
+	border: 0;
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.1 - Activity Loop
+*----------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------
+* @section 3.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+.activity-update-form {
+	padding: 10px 10px 0;
+}
+
+.item-body .activity-update-form .activity-form {
+	margin: 0;
+	padding: 0;
+}
+
+.activity-update-form {
+	border: 1px solid #ccc;
+	box-shadow: inset 0 0 6px #eee;
+	margin: 15px 0;
+}
+
+.activity-update-form #whats-new-avatar {
+	margin: 10px 0;
+	text-align: center;
+}
+
+.activity-update-form #whats-new-avatar img {
+	box-shadow: none;
+	display: inline-block;
+}
+
+.activity-update-form #whats-new-content {
+	padding: 0 0 20px 0;
+}
+
+.activity-update-form #whats-new-textarea textarea {
+	background: #fff;
+	box-sizing: border-box;
+	color: #333;
+	font-family: inherit;
+	font-size: medium;
+	height: 2.2em;
+	line-height: 1.4;
+	padding: 6px;
+	width: 100%;
+}
+
+.activity-update-form #whats-new-textarea textarea:focus {
+	box-shadow: 0 0 6px 0 #d6d6d6;
+}
+
+.activity-update-form #whats-new-post-in-box {
+	margin: 10px 0;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items {
+	list-style: none;
+	margin: 10px 0;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items li {
+	margin-bottom: 10px;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items #activity-autocomplete {
+	padding: 0.3em;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object {
+	display: flex;
+	align-items: center;
+	padding: 0.2em;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object .avatar {
+	width: 30px;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object span {
+	padding-left: 10px;
+	vertical-align: middle;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object:focus, .activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object:hover {
+	background: #eaeaea;
+	cursor: pointer;
+}
+
+.activity-update-form #whats-new-post-in-box #whats-new-post-in-box-items .bp-activity-object.selected {
+	border: 1px solid #d6d6d6;
+}
+
+.activity-update-form #whats-new-submit {
+	margin: 15px 0 10px;
+}
+
+.activity-update-form #whats-new-submit input {
+	font-size: 14px;
+	line-height: inherit;
+	margin-bottom: 10px;
+	margin-right: 10px;
+	padding: 0.2em 0;
+	text-align: center;
+	width: 100%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-update-form #whats-new-avatar {
+		display: block;
+		float: left;
+		margin: 0;
+	}
+	.activity-update-form #whats-new-content,
+	.activity-update-form #whats-new-post-in-box,
+	.activity-update-form #whats-new-submit {
+		margin-left: 8.5%;
+	}
+	.activity-update-form #whats-new-submit input {
+		margin-bottom: 0;
+		margin-right: 10px;
+		width: 8em;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 3.1.2 - Activity Entries
+*-----------------------------------------------------
+*/
+.activity-list {
+	padding: 0.5em;
+}
+
+.activity-list .activity-item:before,
+.activity-list .activity-item:after {
+	content: " ";
+	display: table;
+}
+
+.activity-list .activity-item:after {
+	clear: both;
+}
+
+.activity-list .activity-item {
+	list-style: none;
+	padding: 1em;
+}
+
+.activity-list .activity-item.has-comments {
+	padding-bottom: 1em;
+}
+
+.activity-list .activity-item div.item-avatar {
+	margin: 0 auto;
+	text-align: center;
+	width: auto;
+}
+
+.activity-list .activity-item div.item-avatar img {
+	height: auto;
+	max-width: 40%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-list .activity-item div.item-avatar {
+		margin: 0 2% 0 0;
+		text-align: left;
+		width: 15%;
+	}
+	.activity-list .activity-item div.item-avatar img {
+		max-width: 80%;
+	}
+}
+
+.activity-list .activity-item.mini {
+	font-size: 13px;
+	position: relative;
+}
+
+.activity-list .activity-item.mini .activity-avatar {
+	margin-left: 0 auto;
+	text-align: center;
+	width: auto;
+}
+
+.activity-list .activity-item.mini .activity-avatar img.avatar,
+.activity-list .activity-item.mini .activity-avatar img.FB_profile_pic {
+	/* stylelint-disable-line selector-class-pattern */
+	max-width: 15%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-list .activity-item.mini .activity-avatar {
+		margin-left: 15px;
+		text-align: left;
+		width: 15%;
+	}
+	.activity-list .activity-item.mini .activity-avatar img.avatar,
+	.activity-list .activity-item.mini .activity-avatar img.FB_profile_pic {
+		/* stylelint-disable-line selector-class-pattern */
+		max-width: 60%;
+	}
+}
+
+.activity-list .activity-item.new_forum_post .activity-inner, .activity-list .activity-item.new_forum_topic .activity-inner {
+	border-left: 2px solid #eaeaea;
+	margin-left: 10px;
+	padding-left: 1em;
+}
+
+.activity-list .activity-item.newest_mentions_activity, .activity-list .activity-item.newest_friends_activity, .activity-list .activity-item.newest_groups_activity, .activity-list .activity-item.newest_blogs_activity {
+	background: rgba(31, 179, 221, 0.1);
+}
+
+.activity-list .activity-item .activity-inreplyto {
+	color: #767676;
+	font-size: 13px;
+}
+
+.activity-list .activity-item .activity-inreplyto > p {
+	display: inline;
+	margin: 0;
+}
+
+.activity-list .activity-item .activity-inreplyto blockquote,
+.activity-list .activity-item .activity-inreplyto .activity-inner {
+	background: none;
+	border: 0;
+	display: inline;
+	margin: 0;
+	overflow: hidden;
+	padding: 0;
+}
+
+.activity-list .activity-item .activity-header {
+	margin: 0 auto;
+	width: 80%;
+}
+
+.activity-list .activity-item .activity-header a,
+.activity-list .activity-item .activity-header img {
+	display: inline;
+}
+
+.activity-list .activity-item .activity-header .avatar {
+	display: inline-block;
+	margin: 0 5px;
+	vertical-align: bottom;
+}
+
+.activity-list .activity-item .activity-header .time-since {
+	font-size: 14px;
+	color: #767676;
+	text-decoration: none;
+}
+
+.activity-list .activity-item .activity-header .time-since:hover {
+	color: #767676;
+	cursor: pointer;
+	text-decoration: underline;
+}
+
+.activity-list .activity-item .activity-content .activity-header,
+.activity-list .activity-item .activity-content .comment-header {
+	color: #767676;
+	margin-bottom: 10px;
+}
+
+.activity-list .activity-item .activity-content .activity-inner,
+.activity-list .activity-item .activity-content blockquote {
+	background: #fafafa;
+	margin: 15px 0 10px;
+	overflow: hidden;
+	padding: 1em;
+}
+
+.activity-list .activity-item .activity-content p {
+	margin: 0;
+}
+
+.activity-list .activity-item .activity-inner p {
+	word-wrap: break-word;
+}
+
+.activity-list .activity-item .activity-read-more {
+	margin-left: 1em;
+	white-space: nowrap;
+}
+
+.activity-list .activity-item ul.activity-meta {
+	margin: 0;
+	padding-left: 0;
+}
+
+.activity-list .activity-item ul.activity-meta li {
+	border: 0;
+	display: inline-block;
+}
+
+.activity-list .activity-item .activity-meta.action {
+	border: 1px solid transparent;
+	background: #fafafa;
+	padding: 2px;
+	position: relative;
+	text-align: left;
+}
+
+.activity-list .activity-item .activity-meta.action div.generic-button {
+	margin: 0;
+}
+
+.activity-list .activity-item .activity-meta.action .button {
+	background: transparent;
+}
+
+.activity-list .activity-item .activity-meta.action a {
+	padding: 4px 8px;
+}
+
+.activity-list .activity-item .activity-meta.action .button:focus,
+.activity-list .activity-item .activity-meta.action .button:hover {
+	background: none;
+}
+
+.activity-list .activity-item .activity-meta.action .button:before,
+.activity-list .activity-item .activity-meta.action .icons:before {
+	font-family: dashicons;
+	font-size: 18px;
+	vertical-align: middle;
+}
+
+.activity-list .activity-item .activity-meta.action .acomment-reply.button:before {
+	content: "\f101";
+}
+
+.activity-list .activity-item .activity-meta.action .view:before {
+	content: "\f125";
+}
+
+.activity-list .activity-item .activity-meta.action .fav:before {
+	content: "\f154";
+}
+
+.activity-list .activity-item .activity-meta.action .unfav:before {
+	content: "\f155";
+}
+
+.activity-list .activity-item .activity-meta.action .delete-activity:before {
+	content: "\f153";
+}
+
+.activity-list .activity-item .activity-meta.action .delete-activity:hover {
+	color: #800;
+}
+
+.activity-list .activity-item .activity-meta.action .button {
+	border: 0;
+	box-shadow: none;
+}
+
+.activity-list .activity-item .activity-meta.action .button span {
+	background: none;
+	color: #555;
+	font-weight: 700;
+}
+
+@media screen and (min-width: 46.8em) {
+	.activity-list.bp-list {
+		padding: 30px;
+	}
+	.activity-list .activity-item .activity-content {
+		margin: 0;
+		position: relative;
+	}
+	.activity-list .activity-item .activity-content:after {
+		clear: both;
+		content: "";
+		display: table;
+	}
+	.activity-list .activity-item .activity-header {
+		margin: 0 15px 0 0;
+		width: auto;
+	}
+}
+
+.buddypress-wrap .activity-list .load-more,
+.buddypress-wrap .activity-list .load-newest {
+	background: #fafafa;
+	border: 1px solid #eee;
+	font-size: 110%;
+	margin: 15px 0;
+	padding: 0;
+	text-align: center;
+}
+
+.buddypress-wrap .activity-list .load-more a,
+.buddypress-wrap .activity-list .load-newest a {
+	color: #555;
+	display: block;
+	padding: 0.5em 0;
+}
+
+.buddypress-wrap .activity-list .load-more a:focus, .buddypress-wrap .activity-list .load-more a:hover,
+.buddypress-wrap .activity-list .load-newest a:focus,
+.buddypress-wrap .activity-list .load-newest a:hover {
+	background: #fff;
+	color: #333;
+}
+
+.buddypress-wrap .activity-list .load-more:focus, .buddypress-wrap .activity-list .load-more:hover,
+.buddypress-wrap .activity-list .load-newest:focus,
+.buddypress-wrap .activity-list .load-newest:hover {
+	border-color: #e1e1e1;
+	box-shadow: 0 0 6px 0 #eaeaea;
+}
+
+body.activity-permalink .activity-list li {
+	border-width: 1px;
+	padding: 1em 0 0 0;
+}
+
+body.activity-permalink .activity-list li:first-child {
+	padding-top: 0;
+}
+
+body.activity-permalink .activity-list li.has-comments {
+	padding-bottom: 0;
+}
+
+body.activity-permalink .activity-list .activity-avatar {
+	width: auto;
+}
+
+body.activity-permalink .activity-list .activity-avatar a {
+	display: block;
+}
+
+body.activity-permalink .activity-list .activity-avatar img {
+	max-width: 100%;
+}
+
+body.activity-permalink .activity-list .activity-content {
+	border: 0;
+	font-size: 100%;
+	line-height: 1.5;
+	padding: 0;
+}
+
+body.activity-permalink .activity-list .activity-content .activity-header {
+	margin: 0;
+	padding: 0.5em 0 0 0;
+	text-align: center;
+	width: 100%;
+}
+
+body.activity-permalink .activity-list .activity-content .activity-inner,
+body.activity-permalink .activity-list .activity-content blockquote {
+	margin-left: 0;
+	margin-top: 10px;
+}
+
+body.activity-permalink .activity-list .activity-meta {
+	margin: 10px 0 10px;
+}
+
+body.activity-permalink .activity-list .activity-comments {
+	margin-bottom: 10px;
+}
+
+@media screen and (min-width: 46.8em) {
+	body.activity-permalink .activity-list .activity-avatar {
+		left: -20px;
+		margin-right: 0;
+		position: relative;
+		top: -20px;
+	}
+	body.activity-permalink .activity-list .activity-content {
+		margin-right: 10px;
+	}
+	body.activity-permalink .activity-list .activity-content .activity-header p {
+		text-align: left;
+	}
+}
+
+/**
+*-----------------------------------------------------
+* @section 3.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+.buddypress-wrap .activity-comments {
+	clear: both;
+	margin: 0 5%;
+	overflow: hidden;
+	position: relative;
+	width: auto;
+}
+
+.buddypress-wrap .activity-comments ul {
+	clear: both;
+	list-style: none;
+	margin: 15px 0 0;
+	padding: 0;
+}
+
+.buddypress-wrap .activity-comments ul li {
+	border-top: 1px solid #eee;
+	border-bottom: 0;
+	padding: 1em 0 0;
+}
+
+.buddypress-wrap .activity-comments ul li ul {
+	margin-left: 5%;
+}
+
+.buddypress-wrap .activity-comments ul li:first-child {
+	border-top: 0;
+}
+
+.buddypress-wrap .activity-comments ul li:last-child {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .activity-comments div.acomment-avatar {
+	width: auto;
+}
+
+.buddypress-wrap .activity-comments div.acomment-avatar img {
+	border-width: 1px;
+	float: left;
+	height: 25px;
+	max-width: none;
+	width: 25px;
+}
+
+.buddypress-wrap .activity-comments .acomment-meta,
+.buddypress-wrap .activity-comments .acomment-content p {
+	font-size: 14px;
+}
+
+.buddypress-wrap .activity-comments .acomment-meta {
+	color: #555;
+	overflow: hidden;
+	padding-left: 2%;
+}
+
+.buddypress-wrap .activity-comments .acomment-content {
+	border-left: 1px solid #ccc;
+	margin: 15px 0 0 10%;
+	padding: 0.5em 1em;
+}
+
+.buddypress-wrap .activity-comments .acomment-content p {
+	margin-bottom: 0.5em;
+}
+
+.buddypress-wrap .activity-comments .acomment-options {
+	float: left;
+	margin: 10px 0 10px 20px;
+}
+
+.buddypress-wrap .activity-comments .acomment-options a {
+	color: #767676;
+	font-size: 14px;
+}
+
+.buddypress-wrap .activity-comments .acomment-options a:focus, .buddypress-wrap .activity-comments .acomment-options a:hover {
+	color: inherit;
+}
+
+.buddypress-wrap .activity-comments .activity-meta.action {
+	background: none;
+	margin-top: 10px;
+}
+
+.buddypress-wrap .activity-comments .activity-meta.action button {
+	font-size: 14px;
+	font-weight: 400;
+	text-transform: none;
+}
+
+.buddypress-wrap .activity-comments .show-all button {
+	font-size: 14px;
+	text-decoration: underline;
+	padding-left: 0.5em;
+}
+
+.buddypress-wrap .activity-comments .show-all button span {
+	text-decoration: none;
+}
+
+.buddypress-wrap .activity-comments .show-all button:hover span, .buddypress-wrap .activity-comments .show-all button:focus span {
+	color: #5087e5;
+}
+
+.buddypress-wrap .mini .activity-comments {
+	clear: both;
+	margin-top: 0;
+}
+
+body.activity-permalink .activity-comments {
+	background: none;
+	width: auto;
+}
+
+body.activity-permalink .activity-comments > ul {
+	padding: 0 0.5em 0 1em;
+}
+
+body.activity-permalink .activity-comments ul li > ul {
+	margin-top: 10px;
+}
+
+form.ac-form {
+	display: none;
+	padding: 1em;
+}
+
+form.ac-form .ac-reply-avatar {
+	float: left;
+}
+
+form.ac-form .ac-reply-avatar img {
+	border: 1px solid #eee;
+}
+
+form.ac-form .ac-reply-content {
+	color: #767676;
+	padding-left: 1em;
+}
+
+form.ac-form .ac-reply-content a {
+	text-decoration: none;
+}
+
+form.ac-form .ac-reply-content .ac-textarea {
+	margin-bottom: 15px;
+	padding: 0 0.5em;
+	overflow: hidden;
+}
+
+form.ac-form .ac-reply-content .ac-textarea textarea {
+	background: transparent;
+	box-shadow: none;
+	color: #555;
+	font-family: inherit;
+	font-size: 100%;
+	height: 60px;
+	margin: 0;
+	outline: none;
+	padding: 0.5em;
+	width: 100%;
+}
+
+form.ac-form .ac-reply-content .ac-textarea textarea:focus {
+	box-shadow: 0 0 6px #d6d6d6;
+}
+
+form.ac-form .ac-reply-content input {
+	margin-top: 10px;
+}
+
+.activity-comments li form.ac-form {
+	clear: both;
+	margin-right: 15px;
+}
+
+.activity-comments form.root {
+	margin-left: 0;
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Blogs Loop
+*----------------------------------------------------------
+*/
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .blogs-list li .item-block {
+		float: none;
+		width: auto;
+	}
+	.buddypress-wrap .blogs-list li .item-meta {
+		clear: left;
+		float: none;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-dir-vert-nav .blogs-list .list-title {
+		width: auto;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Groups Loop
+*----------------------------------------------------------
+*/
+.buddypress-wrap .groups-list li .list-title {
+	text-align: center;
+}
+
+.buddypress-wrap .groups-list li .group-details {
+	clear: left;
+}
+
+.buddypress-wrap .groups-list li .group-desc {
+	border: 1px solid #eaeaea;
+	border-radius: 10px;
+	background-clip: padding-box;
+	font-size: 13px;
+	color: #737373;
+	font-style: italic;
+	margin: 10px auto 0;
+	padding: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .groups-list li .group-desc {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .groups-list li p {
+	margin: 0 0 0.5em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .groups-list li .item {
+		margin-right: 0;
+	}
+	.buddypress-wrap .groups-list li .list-title,
+	.buddypress-wrap .groups-list li .item-meta {
+		text-align: left;
+		width: auto;
+	}
+	.buddypress-wrap .groups-list li .item-meta {
+		margin-bottom: 20px;
+	}
+	.buddypress-wrap .groups-list li .last-activity {
+		clear: left;
+		margin-top: -20px;
+	}
+}
+
+.buddypress-wrap .groups-list li.group-no-avatar div.group-desc {
+	margin-left: 0;
+}
+
+.buddypress-wrap .mygroups .groups-list.grid .wrap {
+	min-height: 450px;
+	padding-bottom: 0;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .groups-list.grid.three .group-desc, .buddypress-wrap .groups-list.grid.four .group-desc {
+		font-size: 14px;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress .bp-vertical-navs .groups-list .item-avatar {
+		margin-right: 3%;
+		width: 15%;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Members Loop
+*----------------------------------------------------------
+*/
+.buddypress-wrap .members-list li .member-name {
+	margin-bottom: 10px;
+}
+
+.buddypress-wrap .members-list li .user-update {
+	border: 1px solid #eaeaea;
+	border-radius: 10px;
+	background-clip: padding-box;
+	color: #737373;
+	font-style: italic;
+	font-size: 13px;
+	margin: 15px auto;
+	padding: 1em;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .members-list li .user-update {
+		font-size: 16px;
+	}
+}
+
+.buddypress-wrap .members-list li .user-update .activity-read-more {
+	display: block;
+	font-size: 12px;
+	font-style: normal;
+	margin-top: 10px;
+	padding-left: 2px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .members-list li .last-activity {
+		clear: left;
+		margin-top: -10px;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .members-group-list li .joined {
+		clear: left;
+		float: none;
+	}
+}
+
+@media screen and (min-width: 32em) {
+	body:not(.logged-in) .members-list .user-update {
+		width: 96%;
+	}
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+.register-page .register-section {
+	box-sizing: border-box;
+}
+
+.register-page .signup-form {
+	margin-top: 20px;
+}
+
+.register-page .signup-form .default-profile input {
+	margin-bottom: 20px;
+}
+
+.register-page .signup-form label,
+.register-page .signup-form legend {
+	margin: 10px 0 0;
+}
+
+.register-page .signup-form .editfield {
+	margin: 15px 0;
+}
+
+.register-page .signup-form .editfield fieldset {
+	border: 0;
+	padding: 0;
+}
+
+.register-page .signup-form .editfield fieldset legend {
+	margin: 0 0 5px;
+	text-indent: 0;
+}
+
+.register-page .signup-form .editfield .field-visibility-settings {
+	padding: 0.5em;
+}
+
+.register-page .signup-form .editfield .field-visibility-settings fieldset {
+	margin: 0 0 10px;
+}
+
+.register-page .signup-form #signup-avatar img {
+	margin: 0 15px 10px 0;
+}
+
+.register-page .signup-form .password-entry,
+.register-page .signup-form .password-entry-confirm {
+	border: 1px solid #eee;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .register-page .layout-wrap {
+		display: flex;
+		flex-flow: row wrap;
+		justify-content: space-around;
+	}
+	.buddypress-wrap .register-page .layout-wrap .default-profile {
+		flex: 1;
+		padding-right: 2em;
+	}
+	.buddypress-wrap .register-page .layout-wrap .blog-details {
+		flex: 1;
+		padding-left: 2em;
+	}
+	.buddypress-wrap .register-page .submit {
+		clear: both;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap.extended-default-reg .register-page .default-profile {
+		flex: 1;
+		padding-right: 1em;
+	}
+	.buddypress-wrap.extended-default-reg .register-page .extended-profile {
+		flex: 2;
+		padding-left: 1em;
+	}
+	.buddypress-wrap.extended-default-reg .register-page .blog-details {
+		flex: 1 100%;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Groups Creation Steps
+*----------------------------------------------------------
+*/
+#group-create-body {
+	padding: 0.5em;
+}
+
+#group-create-body .creation-step-name {
+	text-align: center;
+}
+
+#group-create-body .avatar-nav-items {
+	margin-top: 15px;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Single Item screens: Groups, Users
+*-------------------------------------------------------------------------------
+*/
+/**
+*-----------------------------------------------------------
+* @subsection 5.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+.single-headers:before,
+.single-headers:after {
+	content: " ";
+	display: table;
+}
+
+.single-headers:after {
+	clear: both;
+}
+
+.single-headers {
+	margin-bottom: 15px;
+}
+
+.single-headers #item-header-avatar a {
+	display: block;
+	text-align: center;
+}
+
+.single-headers #item-header-avatar a img {
+	float: none;
+}
+
+.single-headers div#item-header-content {
+	float: none;
+}
+
+@media screen and (min-width: 46.8em) {
+	.single-headers #item-header-avatar a {
+		text-align: left;
+	}
+	.single-headers #item-header-avatar a img {
+		float: left;
+	}
+	.single-headers #item-header-content {
+		padding-left: 2em;
+	}
+}
+
+.single-headers .group-status,
+.single-headers .activity {
+	display: inline;
+}
+
+.single-headers .group-status {
+	font-size: 18px;
+	color: #333;
+	padding-right: 1em;
+}
+
+.single-headers .activity {
+	display: inline-block;
+	font-size: 12px;
+	padding: 0;
+}
+
+.single-headers div#message p,
+.single-headers #sitewide-notice p {
+	background-color: #ffd;
+	border: 1px solid #cb2;
+	color: #440;
+	font-weight: 400;
+	margin-top: 3px;
+	text-decoration: none;
+}
+
+.single-headers h2 {
+	line-height: 1.2;
+	margin: 0 0 5px;
+}
+
+.single-headers h2 a {
+	color: #767676;
+	text-decoration: none;
+}
+
+.single-headers h2 span.highlight {
+	display: inline-block;
+	font-size: 60%;
+	font-weight: 400;
+	line-height: 1.7;
+	vertical-align: middle;
+}
+
+.single-headers h2 span.highlight span {
+	background: #a1dcfa;
+	color: #fff;
+	cursor: pointer;
+	font-size: 80%;
+	font-weight: 700;
+	margin-bottom: 2px;
+	padding: 1px 4px;
+	position: relative;
+	right: -2px;
+	top: -2px;
+	vertical-align: middle;
+}
+
+.single-headers img.avatar {
+	float: left;
+	margin: 0 15px 19px 0;
+}
+
+.single-headers .item-meta {
+	color: #767676;
+	font-size: 14px;
+	margin: 15px 0 5px;
+	padding-bottom: 0.5em;
+}
+
+.single-headers ul {
+	margin-bottom: 15px;
+}
+
+.single-headers ul li {
+	float: right;
+	list-style: none;
+}
+
+.single-headers div.generic-button {
+	text-align: center;
+}
+
+.single-headers li.generic-button {
+	display: inline-block;
+	text-align: center;
+}
+
+@media screen and (min-width: 46.8em) {
+	.single-headers div.generic-button,
+	.single-headers a.button,
+	.single-headers li.generic-button {
+		float: left;
+	}
+}
+
+.single-headers div.generic-button,
+.single-headers a.button {
+	margin: 10px 10px 0 0;
+}
+
+.single-headers li.generic-button {
+	margin: 2px 10px;
+}
+
+.single-headers li.generic-button:first-child {
+	margin-left: 0;
+}
+
+.single-headers div#message.info {
+	line-height: 0.8;
+}
+
+body.no-js .single-item-header .js-self-profile-button {
+	display: none;
+}
+
+/*
+* Default required cover image rules
+*/
+#cover-image-container {
+	position: relative;
+}
+
+#header-cover-image {
+	background-color: #c5c5c5;
+	background-position: center top;
+	background-repeat: no-repeat;
+	background-size: cover;
+	border: 0;
+	display: block;
+	left: 0;
+	margin: 0;
+	padding: 0;
+	position: absolute;
+	top: 0;
+	width: 100%;
+	z-index: 1;
+}
+
+#item-header-cover-image {
+	position: relative;
+	z-index: 2;
+}
+
+#item-header-cover-image #item-header-avatar {
+	padding: 0 1em;
+}
+
+/*
+* end cover image block
+*/
+/**
+*-----------------------------------------------------
+* @subsection 5.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+.groups-header .bp-group-type-list {
+	margin: 0;
+}
+
+.groups-header .bp-feedback {
+	clear: both;
+}
+
+.groups-header .group-item-actions {
+	float: left;
+	margin: 0 0 15px 15px;
+	padding-top: 0;
+	width: 100%;
+}
+
+.groups-header .moderators-lists {
+	margin-top: 0;
+}
+
+.groups-header .moderators-lists .moderators-title {
+	font-size: 14px;
+}
+
+.groups-header .moderators-lists .user-list {
+	margin: 0 0 5px;
+}
+
+.groups-header .moderators-lists .user-list ul:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.groups-header .moderators-lists .user-list li {
+	display: inline-block;
+	float: none;
+	margin-left: 4px;
+	padding: 4px;
+}
+
+.groups-header .moderators-lists img.avatar {
+	box-shadow: none;
+	float: none;
+	height: 30px;
+	margin: 0;
+	max-width: 100%;
+	width: 30px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.groups-header div#item-header-content {
+		float: left;
+		margin-left: 10%;
+		text-align: left;
+		padding-top: 15px;
+		width: 42%;
+	}
+	.groups-header .group-item-actions {
+		float: right;
+		margin: 0 0 15px 15px;
+		text-align: right;
+		width: 20%;
+	}
+	.groups-header .groups-meta {
+		clear: both;
+	}
+}
+
+.groups-header .desc-wrap {
+	background: #eaeaea;
+	border: 1px solid #d6d6d6;
+	margin: 0 0 15px;
+	padding: 1em;
+	text-align: center;
+}
+
+.groups-header .desc-wrap .group-description {
+	background: #fafafa;
+	box-shadow: inset 0 0 9px #ccc;
+	padding: 1em;
+	text-align: left;
+}
+
+.groups-header .desc-wrap .group-description p {
+	margin: 0;
+	padding: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 5.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+.bp-user .users-header .user-nicename {
+	margin-bottom: 5px;
+}
+
+.bp-user .member-header-actions {
+	overflow: hidden;
+}
+
+.bp-user .member-header-actions * > * {
+	display: block;
+}
+
+/**
+*-----------------------------------------------------------
+* @subsection 5.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+.buddypress-wrap .item-body {
+	margin: 20px 0;
+}
+
+.buddypress-wrap .item-body .screen-heading {
+	font-size: 20px;
+	font-weight: 400;
+}
+
+.buddypress-wrap .item-body .button-tabs {
+	margin: 30px 0 15px;
+}
+
+.buddypress-wrap.bp-single-vert-nav .bp-list:not(.grid) .item-entry {
+	padding-left: 0.5em;
+}
+
+/**
+*----------------------------------------------------
+* @subsection 5.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+.single-item.group-members .item-body .filters:not(.no-subnav) {
+	border-top: 5px solid #eaeaea;
+	padding-top: 1em;
+}
+
+.single-item.group-members .item-body .filters {
+	margin-top: 0;
+}
+
+/**
+*-----------------------------------------
+* @subsection 5.2.1.1 - Management Settings Screens
+*-----------------------------------------
+*/
+.buddypress-wrap .group-status-type ul {
+	margin: 0 0 20px 20px;
+}
+
+.groups-manage-members-list {
+	padding: 0.5em 0;
+}
+
+.groups-manage-members-list dd {
+	margin: 0;
+	padding: 1em 0;
+}
+
+.groups-manage-members-list .section-title {
+	background: #eaeaea;
+	padding-left: 0.3em;
+}
+
+.groups-manage-members-list ul {
+	list-style: none;
+	margin-bottom: 0;
+}
+
+.groups-manage-members-list ul li {
+	border-bottom: 1px solid #eee;
+	margin-bottom: 10px;
+	padding: 0.5em 0.3em 0.3em;
+}
+
+.groups-manage-members-list ul li:only-child,
+.groups-manage-members-list ul li:last-child {
+	border-bottom: 0;
+}
+
+.groups-manage-members-list ul li:nth-child(even) {
+	background: #fafafa;
+}
+
+.groups-manage-members-list ul li.banned-user {
+	background: #fad3d3;
+}
+
+.groups-manage-members-list ul .member-name {
+	margin-bottom: 0;
+	text-align: center;
+}
+
+.groups-manage-members-list ul img {
+	display: block;
+	margin: 0 auto;
+	width: 20%;
+}
+
+@media screen and (min-width: 32em) {
+	.groups-manage-members-list ul .member-name {
+		text-align: left;
+	}
+	.groups-manage-members-list ul img {
+		display: inline;
+		width: 50px;
+	}
+}
+
+.groups-manage-members-list ul .members-manage-buttons:before,
+.groups-manage-members-list ul .members-manage-buttons:after {
+	content: " ";
+	display: table;
+}
+
+.groups-manage-members-list ul .members-manage-buttons:after {
+	clear: both;
+}
+
+.groups-manage-members-list ul .members-manage-buttons {
+	margin: 15px 0 5px;
+}
+
+.groups-manage-members-list ul .members-manage-buttons a.button {
+	color: #767676;
+	display: block;
+	font-size: 13px;
+}
+
+@media screen and (min-width: 32em) {
+	.groups-manage-members-list ul .members-manage-buttons a.button {
+		display: inline-block;
+	}
+}
+
+.groups-manage-members-list ul .members-manage-buttons.text-links-list {
+	margin-bottom: 0;
+}
+
+@media screen and (max-width: 32em) {
+	.groups-manage-members-list ul .members-manage-buttons.text-links-list a.button {
+		background: #fafafa;
+		border: 1px solid #eee;
+		display: block;
+		margin-bottom: 10px;
+	}
+}
+
+.groups-manage-members-list ul .action:not(.text-links-list) a.button {
+	font-size: 12px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.groups-manage-members-list ul li .avatar,
+	.groups-manage-members-list ul li .member-name {
+		float: left;
+	}
+	.groups-manage-members-list ul li .avatar {
+		margin-right: 15px;
+	}
+	.groups-manage-members-list ul li .action {
+		clear: both;
+		float: left;
+	}
+}
+
+/**
+*-----------------------------------------
+* @subsection 5.2.1.2 - Group Members List
+*-----------------------------------------
+*/
+/*
+*-----------------------------------------
+* @subsection 5.2.1.3 - Group Invites List
+*-----------------------------------------
+*/
+/*
+ * bp-nouveau styling: invite members, sent invites
+ * @version 3.0.0
+ */
+.buddypress .bp-invites-content ul.item-list {
+	border-top: 0;
+}
+
+.buddypress .bp-invites-content ul.item-list li {
+	border: 1px solid #eaeaea;
+	margin: 0 0 1%;
+	padding-left: 5px;
+	padding-right: 5px;
+	position: relative;
+	width: auto;
+}
+
+.buddypress .bp-invites-content ul.item-list li .list-title {
+	margin: 0 auto;
+	width: 80%;
+}
+
+.buddypress .bp-invites-content ul.item-list li .action {
+	position: absolute;
+	top: 10px;
+	right: 10px;
+}
+
+.buddypress .bp-invites-content ul.item-list li .action a.button.invite-button {
+	border: 0;
+}
+
+.buddypress .bp-invites-content ul.item-list li .action a.button.invite-button:focus, .buddypress .bp-invites-content ul.item-list li .action a.button.invite-button:hover {
+	color: #1fb3dd;
+}
+
+.buddypress .bp-invites-content ul.item-list li.selected {
+	box-shadow: inset 0 0 12px 0 rgba(237, 187, 52, 0.2);
+}
+
+.buddypress .bp-invites-content .item-list .item-meta span,
+.buddypress .bp-invites-content .group-inviters li {
+	color: #767676;
+}
+
+.buddypress .bp-invites-content li ul.group-inviters {
+	clear: both;
+	margin: 0;
+	overflow: hidden;
+}
+
+.buddypress .bp-invites-content li ul.group-inviters li {
+	border: 0;
+	float: left;
+	font-size: 20px;
+	width: inherit;
+}
+
+.buddypress .bp-invites-content li .status {
+	font-size: 20px;
+	font-style: italic;
+	clear: both;
+	color: #555;
+	margin: 10px 0;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul:before,
+.buddypress .bp-invites-content #send-invites-editor ul:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul:after {
+	clear: both;
+}
+
+.buddypress .bp-invites-content #send-invites-editor textarea {
+	width: 100%;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul {
+	clear: both;
+	list-style: none;
+	margin: 10px 0;
+}
+
+.buddypress .bp-invites-content #send-invites-editor ul li {
+	float: left;
+	margin: 0.5%;
+	max-height: 50px;
+	max-width: 50px;
+}
+
+.buddypress .bp-invites-content #send-invites-editor #bp-send-invites-form {
+	clear: both;
+	margin-top: 10px;
+}
+
+.buddypress .bp-invites-content #send-invites-editor .action {
+	margin-top: 10px;
+	padding-top: 10px;
+}
+
+.buddypress .bp-invites-content #send-invites-editor.bp-hide {
+	display: none;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress .bp-invites-content ul.item-list > li {
+		box-sizing: border-box;
+		border: 1px solid #eaeaea;
+		float: left;
+		padding-left: 0.5em;
+		padding-right: 0.5em;
+		width: 49.5%;
+	}
+	.buddypress .bp-invites-content ul.item-list > li:nth-child(odd) {
+		margin-right: 0.5%;
+	}
+	.buddypress .bp-invites-content ul.item-list > li:nth-child(even) {
+		margin-left: 0.5%;
+	}
+	.buddypress .bp-invites-content ul.item-list ul.group-inviters {
+		float: left;
+		width: auto;
+	}
+}
+
+@media screen and (min-width: 46.8em) {
+	:not(.vertical) + .item-body #group-invites-container {
+		display: -ms-grid;
+		display: grid;
+		-ms-grid-columns: 25% auto;
+		grid-template-columns: 25% auto;
+		grid-template-areas: "group-invites-nav group-invites-column";
+	}
+	:not(.vertical) + .item-body #group-invites-container .bp-invites-nav {
+		-ms-grid-row: 1;
+		-ms-grid-column: 1;
+		grid-area: group-invites-nav;
+	}
+	:not(.vertical) + .item-body #group-invites-container .bp-invites-nav li {
+		display: block;
+		float: none;
+	}
+	:not(.vertical) + .item-body #group-invites-container .group-invites-column {
+		-ms-grid-row: 1;
+		-ms-grid-column: 2;
+		grid-area: group-invites-column;
+	}
+}
+
+/*
+*-----------------------------------------
+* @subsection 5.2.1.4 - Group Activity
+*-----------------------------------------
+*/
+.buddypress.groups .activity-update-form {
+	margin-top: 0;
+}
+
+/**
+*-----------------------------------------------------
+* @subsection 5.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+/**
+*--------------------------------------------
+* @subsection 5.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+/**
+*-------------------------------------------
+* @subsection 5.2.2.2 - Extended Profiles
+*-------------------------------------------
+*/
+.buddypress-wrap .profile {
+	margin-top: 30px;
+}
+
+.buddypress-wrap .public .profile-fields td.label {
+	width: 30%;
+}
+
+.buddypress-wrap .profile.edit .button-nav {
+	list-style: none;
+	margin: 30px 0 10px;
+}
+
+.buddypress-wrap .profile.edit .button-nav li {
+	display: inline-block;
+	margin-right: 10px;
+}
+
+.buddypress-wrap .profile.edit .button-nav li a {
+	font-size: 18px;
+}
+
+.buddypress-wrap .profile.edit .editfield {
+	background: #fafafa;
+	border: 1px solid #eee;
+	margin: 15px 0;
+	padding: 1em;
+}
+
+.buddypress-wrap .profile.edit .editfield fieldset {
+	border: 0;
+}
+
+.buddypress-wrap .profile.edit .editfield fieldset label {
+	font-weight: 400;
+}
+
+.buddypress-wrap .profile.edit .editfield fieldset label.xprofile-field-label {
+	display: inline;
+}
+
+.buddypress-wrap .profile.edit .editfield {
+	display: flex;
+	flex-direction: column;
+}
+
+.buddypress-wrap .profile.edit .editfield .description {
+	margin-top: 10px;
+	order: 2;
+}
+
+.buddypress-wrap .profile.edit .editfield > fieldset {
+	order: 1;
+}
+
+.buddypress-wrap .profile.edit .editfield .field-visibility-settings-toggle,
+.buddypress-wrap .profile.edit .editfield .field-visibility-settings {
+	order: 3;
+}
+
+body.no-js .buddypress-wrap .field-visibility-settings-toggle,
+body.no-js .buddypress-wrap .field-visibility-settings-close {
+	display: none;
+}
+
+body.no-js .buddypress-wrap .field-visibility-settings {
+	display: block;
+}
+
+.buddypress-wrap .field-visibility-settings {
+	margin: 10px 0;
+}
+
+.buddypress-wrap .current-visibility-level {
+	font-style: normal;
+	font-weight: 700;
+}
+
+.buddypress-wrap .field-visibility-settings,
+.buddypress-wrap .field-visibility-settings-header {
+	color: #737373;
+}
+
+.buddypress-wrap .field-visibility-settings fieldset {
+	margin: 5px 0;
+}
+
+.buddypress-wrap .standard-form .editfield fieldset {
+	margin: 0;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings label {
+	font-weight: 400;
+	margin: 0;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings .radio {
+	list-style: none;
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings .field-visibility-settings-close {
+	font-size: 12px;
+}
+
+.buddypress-wrap .standard-form .wp-editor-container {
+	border: 1px solid #dedede;
+}
+
+.buddypress-wrap .standard-form .wp-editor-container textarea {
+	background: #fff;
+	width: 100%;
+}
+
+.buddypress-wrap .standard-form .description {
+	background: #fafafa;
+	font-size: inherit;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings legend,
+.buddypress-wrap .standard-form .field-visibility-settings-header {
+	font-style: italic;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings-header {
+	font-size: 14px;
+}
+
+.buddypress-wrap .standard-form .field-visibility-settings legend,
+.buddypress-wrap .standard-form .field-visibility-settings label {
+	font-size: 14px;
+}
+
+.buddypress-wrap .standard-form .field-visibility select {
+	margin: 0;
+}
+
+.buddypress-wrap .html-active button.switch-html {
+	background: #f5f5f5;
+	border-bottom-color: transparent;
+	border-bottom-left-radius: 0;
+	border-bottom-right-radius: 0;
+}
+
+.buddypress-wrap .tmce-active button.switch-tmce {
+	background: #f5f5f5;
+	border-bottom-color: transparent;
+	border-bottom-left-radius: 0;
+	border-bottom-right-radius: 0;
+}
+
+.buddypress-wrap .profile.public .profile-group-title {
+	border-bottom: 1px solid #ccc;
+}
+
+body.register .buddypress-wrap .page ul {
+	list-style: none;
+}
+
+.buddypress-wrap .profile .bp-avatar-nav {
+	margin-top: 20px;
+}
+
+/**
+*-------------------------------------------
+* @subsection 5.2.2.3 - Groups
+*-------------------------------------------
+*/
+/**
+*-------------------------------------------
+* @subsection 5.2.2.5 - Private Messaging
+*-------------------------------------------
+*/
+.message-action-star:before,
+.message-action-unstar:before,
+.message-action-view:before,
+.message-action-delete:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.message-action-star:before {
+	color: #aaa;
+	content: "\f154";
+}
+
+.message-action-unstar:before {
+	color: #fcdd77;
+	content: "\f155";
+}
+
+.message-action-view:before {
+	content: "\f473";
+}
+
+.message-action-delete:before {
+	content: "\f153";
+}
+
+.message-action-delete:hover:before {
+	color: #a00;
+}
+
+.preview-content .actions a {
+	text-decoration: none;
+}
+
+.bp-messages-content {
+	margin: 15px 0;
+}
+
+.bp-messages-content .avatar {
+	box-shadow: none;
+}
+
+.bp-messages-content .thread-participants {
+	list-style: none;
+}
+
+.bp-messages-content .thread-participants dd {
+	margin-left: 0;
+}
+
+.bp-messages-content time {
+	color: #737373;
+	font-size: 12px;
+}
+
+#message-threads {
+	border-top: 1px solid #eaeaea;
+	clear: both;
+	list-style: none;
+	margin: 0;
+	max-height: 220px;
+	overflow-x: hidden;
+	overflow-y: auto;
+	padding: 0;
+	width: 100%;
+}
+
+#message-threads li {
+	border-bottom: 1px solid #eaeaea;
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: row nowrap;
+	flex-flow: row nowrap;
+	margin: 0;
+	overflow: hidden;
+	padding: 0.5em 0;
+}
+
+#message-threads li .thread-cb {
+	display: flex;
+	align-items: center;
+	-moz-flex: 1 2 5%;
+	-o-flex: 1 2 5%;
+	flex: 1 2 5%;
+}
+
+#message-threads li .thread-from,
+#message-threads li .thread-to {
+	-moz-flex: 1 2 20%;
+	-o-flex: 1 2 20%;
+	flex: 1 2 20%;
+}
+
+#message-threads li .thread-from img.avatar,
+#message-threads li .thread-to img.avatar {
+	float: left;
+	margin: 0 10px 0 0;
+}
+
+#message-threads li .thread-from .user-name,
+#message-threads li .thread-to .user-name {
+	display: inline-block;
+	line-height: 1.1;
+}
+
+#message-threads li .thread-from .num-recipients,
+#message-threads li .thread-to .num-recipients {
+	color: #737373;
+	font-weight: 400;
+	font-size: 12px;
+	margin: 0;
+}
+
+#message-threads li .thread-content {
+	-moz-flex: 1 2 60%;
+	-o-flex: 1 2 60%;
+	flex: 1 2 60%;
+}
+
+#message-threads li .thread-date {
+	-moz-flex: 1 2 15%;
+	-o-flex: 1 2 15%;
+	flex: 1 2 15%;
+}
+
+#message-threads li.selected {
+	background-color: #fafafa;
+}
+
+#message-threads li.selected .thread-subject .subject {
+	color: #5087e5;
+}
+
+#message-threads li.unread {
+	font-weight: 700;
+}
+
+#message-threads li .thread-content .excerpt {
+	color: #737373;
+	font-size: 12px;
+	margin: 0;
+}
+
+#message-threads li .thread-content .thread-from,
+#message-threads li .thread-content .thread-to,
+#message-threads li .thread-content .thread-subject {
+	font-size: 13px;
+}
+
+@media screen and (min-width: 46.8em) {
+	#message-threads li .thread-content .thread-from,
+	#message-threads li .thread-content .thread-to,
+	#message-threads li .thread-content .thread-subject {
+		font-size: 16px;
+	}
+}
+
+#message-threads li .thread-content .thread-subject {
+	vertical-align: top;
+}
+
+#message-threads li .thread-content .thread-subject .excerpt {
+	font-weight: 400;
+}
+
+#message-threads li .thread-date {
+	padding-right: 5px;
+	text-align: right;
+}
+
+.bp-messages-content .actions {
+	float: right;
+	max-width: 30%;
+}
+
+.bp-messages-content .actions .bp-icons:not(.bp-hide) {
+	display: inline-block;
+	margin: 0;
+	padding: 0.3em 0.5em;
+}
+
+.bp-messages-content .actions .bp-icons:not(.bp-hide):before {
+	font-size: 26px;
+}
+
+.bp-messages-content #thread-preview {
+	border: 1px solid #eaeaea;
+	margin-top: 20px;
+}
+
+.bp-messages-content #thread-preview .preview-message {
+	overflow: hidden;
+}
+
+.bp-messages-content #thread-preview .preview-content {
+	margin: 0.5em;
+}
+
+.bp-messages-content #thread-preview .preview-content .preview-message {
+	background: #fafafa;
+	margin: 10px 0;
+	padding: 1em 0.3em 0.3em;
+}
+
+.bp-messages-content #bp-message-thread-list {
+	border-top: 1px solid #eaeaea;
+	clear: both;
+	list-style: none;
+	padding: 1em 0 0.3em;
+}
+
+.bp-messages-content #bp-message-thread-list li {
+	padding: 0.5em;
+}
+
+.bp-messages-content #bp-message-thread-list li:nth-child(2n) .message-content {
+	background: #fafafa;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata {
+	border-bottom: 1px solid #ccc;
+	box-shadow: -2px 1px 9px 0 #eee;
+	display: table;
+	padding: 0.2em;
+	width: 100%;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata .avatar {
+	width: 30px;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata .user-link {
+	display: block;
+	font-size: 13px;
+	float: left;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-messages-content #bp-message-thread-list .message-metadata .user-link {
+		font-size: 16px;
+	}
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata time {
+	color: #737373;
+	font-size: 12px;
+	padding: 0 0.5em;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata button {
+	padding: 0 0.3em;
+}
+
+.bp-messages-content #bp-message-thread-list .message-metadata button:before {
+	font-size: 20px;
+}
+
+.bp-messages-content #bp-message-thread-list .message-content {
+	overflow: hidden;
+	margin: 1em auto 0;
+	width: 90%;
+}
+
+.bp-messages-content #bp-message-thread-list img.avatar {
+	float: left;
+	margin: 0 10px 0 0;
+}
+
+.bp-messages-content #bp-message-thread-list .actions a:before {
+	font-size: 18px;
+}
+
+.bp-messages-content form.send-reply .avatar-box {
+	padding: 0.5em 0;
+}
+
+.bp-messages-content .preview-pane-header,
+.bp-messages-content .single-message-thread-header {
+	border-bottom: 1px solid #eaeaea;
+}
+
+.bp-messages-content .preview-pane-header:after,
+.bp-messages-content .single-message-thread-header:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-messages-content .preview-thread-title,
+.bp-messages-content .single-thread-title {
+	font-size: 16px;
+}
+
+.bp-messages-content .preview-thread-title .messages-title,
+.bp-messages-content .single-thread-title .messages-title {
+	padding-left: 2em;
+}
+
+.bp-messages-content .thread-participants {
+	float: left;
+	margin: 5px 0;
+	width: 70%;
+}
+
+.bp-messages-content .thread-participants dd,
+.bp-messages-content .thread-participants ul {
+	margin-bottom: 10px;
+}
+
+.bp-messages-content .thread-participants ul {
+	list-style: none;
+}
+
+.bp-messages-content .thread-participants ul:after {
+	clear: both;
+	content: "";
+	display: table;
+}
+
+.bp-messages-content .thread-participants li {
+	float: left;
+	margin-left: 5px;
+}
+
+.bp-messages-content .thread-participants img {
+	width: 30px;
+}
+
+.bp-messages-content #thread-preview .preview-message ul,
+.bp-messages-content #thread-preview .preview-message ol,
+.bp-messages-content #thread-preview .preview-message blockquote,
+.bp-messages-content #bp-message-thread-list li .message-content ul,
+.bp-messages-content #bp-message-thread-list li .message-content ol,
+.bp-messages-content #bp-message-thread-list li .message-content blockquote {
+	list-style-position: inside;
+	margin-left: 0;
+}
+
+.bp-messages-content ul#message-threads:empty,
+.bp-messages-content #thread-preview:empty {
+	display: none;
+}
+
+.bp-messages-content #thread-preview h2:first-child,
+.bp-messages-content #bp-message-thread-header h2:first-child {
+	background-color: #eaeaea;
+	color: #555;
+	font-weight: 700;
+	margin: 0;
+	padding: 0.5em;
+}
+
+.bp-messages-content #message-threads .thread-content a,
+.bp-messages-content #bp-message-thread-list li a.user-link {
+	border: 0;
+	text-decoration: none;
+}
+
+.bp-messages-content .standard-form #subject {
+	margin-bottom: 20px;
+}
+
+div.bp-navs#subsubnav.bp-messages-filters .user-messages-bulk-actions {
+	margin-right: 15px;
+	max-width: 42.5%;
+}
+
+/**
+*------------------------------------------
+* @subsection 5.2.2.6 - Settings
+*------------------------------------------
+*/
+/*__ Settings Global __*/
+.buddypress.settings .profile-settings.bp-tables-user select {
+	width: 100%;
+}
+
+/*__ General __*/
+/*__ Email notifications __*/
+/*__ Profile visibility __*/
+/*__ Group Invites __*/
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+.buddypress-wrap .filter select,
+.buddypress-wrap #whats-new-post-in-box select {
+	border: 1px solid #d6d6d6;
+}
+
+.buddypress-wrap input.action[disabled] {
+	cursor: pointer;
+	opacity: 0.4;
+}
+
+.buddypress-wrap #notification-bulk-manage[disabled] {
+	display: none;
+}
+
+.buddypress-wrap fieldset legend {
+	font-size: inherit;
+	font-weight: 600;
+}
+
+.buddypress-wrap textarea:focus,
+.buddypress-wrap input[type="text"]:focus,
+.buddypress-wrap input[type="email"]:focus,
+.buddypress-wrap input[type="url"]:focus,
+.buddypress-wrap input[type="tel"]:focus,
+.buddypress-wrap input[type="password"]:focus {
+	box-shadow: 0 0 8px #eaeaea;
+}
+
+.buddypress-wrap select {
+	height: auto;
+}
+
+.buddypress-wrap textarea {
+	resize: vertical;
+}
+
+.buddypress-wrap .standard-form .bp-controls-wrap {
+	margin: 1em 0;
+}
+
+.buddypress-wrap .standard-form textarea,
+.buddypress-wrap .standard-form input[type="text"],
+.buddypress-wrap .standard-form input[type="color"],
+.buddypress-wrap .standard-form input[type="date"],
+.buddypress-wrap .standard-form input[type="datetime"],
+.buddypress-wrap .standard-form input[type="datetime-local"],
+.buddypress-wrap .standard-form input[type="email"],
+.buddypress-wrap .standard-form input[type="month"],
+.buddypress-wrap .standard-form input[type="number"],
+.buddypress-wrap .standard-form input[type="range"],
+.buddypress-wrap .standard-form input[type="search"],
+.buddypress-wrap .standard-form input[type="tel"],
+.buddypress-wrap .standard-form input[type="time"],
+.buddypress-wrap .standard-form input[type="url"],
+.buddypress-wrap .standard-form input[type="week"],
+.buddypress-wrap .standard-form select,
+.buddypress-wrap .standard-form input[type="password"],
+.buddypress-wrap .standard-form [data-bp-search] input[type="search"],
+.buddypress-wrap .standard-form [data-bp-search] input[type="text"],
+.buddypress-wrap .standard-form .groups-members-search input[type="search"],
+.buddypress-wrap .standard-form .groups-members-search input[type="text"] {
+	background: #fafafa;
+	border: 1px solid #d6d6d6;
+	border-radius: 0;
+	font: inherit;
+	font-size: 100%;
+	padding: 0.5em;
+}
+
+.buddypress-wrap .standard-form input[required],
+.buddypress-wrap .standard-form textarea[required],
+.buddypress-wrap .standard-form select[required] {
+	box-shadow: none;
+	border-width: 2px;
+	outline: 0;
+}
+
+.buddypress-wrap .standard-form input[required]:invalid,
+.buddypress-wrap .standard-form textarea[required]:invalid,
+.buddypress-wrap .standard-form select[required]:invalid {
+	border-color: #b71717;
+}
+
+.buddypress-wrap .standard-form input[required]:valid,
+.buddypress-wrap .standard-form textarea[required]:valid,
+.buddypress-wrap .standard-form select[required]:valid {
+	border-color: #91cc2c;
+}
+
+.buddypress-wrap .standard-form input[required]:focus,
+.buddypress-wrap .standard-form textarea[required]:focus,
+.buddypress-wrap .standard-form select[required]:focus {
+	border-color: #d6d6d6;
+	border-width: 1px;
+}
+
+.buddypress-wrap .standard-form input.invalid[required],
+.buddypress-wrap .standard-form textarea.invalid[required],
+.buddypress-wrap .standard-form select.invalid[required] {
+	border-color: #b71717;
+}
+
+.buddypress-wrap .standard-form input:not(.button-small),
+.buddypress-wrap .standard-form textarea {
+	width: 100%;
+}
+
+.buddypress-wrap .standard-form input[type="radio"],
+.buddypress-wrap .standard-form input[type="checkbox"] {
+	margin-right: 5px;
+	width: auto;
+}
+
+.buddypress-wrap .standard-form select {
+	padding: 3px;
+}
+
+.buddypress-wrap .standard-form textarea {
+	height: 120px;
+}
+
+.buddypress-wrap .standard-form textarea#message_content {
+	height: 200px;
+}
+
+.buddypress-wrap .standard-form input[type="password"] {
+	margin-bottom: 5px;
+}
+
+.buddypress-wrap .standard-form input:focus,
+.buddypress-wrap .standard-form textarea:focus,
+.buddypress-wrap .standard-form select:focus {
+	background: #fafafa;
+	color: #555;
+	outline: 0;
+}
+
+.buddypress-wrap .standard-form label,
+.buddypress-wrap .standard-form span.label {
+	display: block;
+	font-weight: 600;
+	margin: 15px 0 5px;
+	width: auto;
+}
+
+.buddypress-wrap .standard-form a.clear-value {
+	display: block;
+	margin-top: 5px;
+	outline: none;
+}
+
+.buddypress-wrap .standard-form .submit {
+	clear: both;
+	padding: 15px 0 0;
+}
+
+.buddypress-wrap .standard-form p.submit {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .standard-form div.submit input {
+	margin-right: 15px;
+}
+
+.buddypress-wrap .standard-form p label,
+.buddypress-wrap .standard-form #invite-list label {
+	font-weight: 400;
+	margin: auto;
+}
+
+.buddypress-wrap .standard-form p.description {
+	color: #737373;
+	margin: 5px 0;
+}
+
+.buddypress-wrap .standard-form div.checkbox label:nth-child(n+2),
+.buddypress-wrap .standard-form div.radio div label {
+	color: #737373;
+	font-size: 100%;
+	font-weight: 400;
+	margin: 5px 0 0;
+}
+
+.buddypress-wrap .standard-form#send-reply textarea {
+	width: 97.5%;
+}
+
+.buddypress-wrap .standard-form#sidebar-login-form label {
+	margin-top: 5px;
+}
+
+.buddypress-wrap .standard-form#sidebar-login-form input[type="text"],
+.buddypress-wrap .standard-form#sidebar-login-form input[type="password"] {
+	padding: 4px;
+	width: 95%;
+}
+
+.buddypress-wrap .standard-form.profile-edit input:focus {
+	background: #fff;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .standard-form .left-menu {
+		float: left;
+	}
+	.buddypress-wrap .standard-form #invite-list ul {
+		list-style: none;
+		margin: 1%;
+	}
+	.buddypress-wrap .standard-form #invite-list ul li {
+		margin: 0 0 0 1%;
+	}
+	.buddypress-wrap .standard-form .main-column {
+		margin-left: 190px;
+	}
+	.buddypress-wrap .standard-form .main-column ul#friend-list {
+		clear: none;
+		float: left;
+	}
+	.buddypress-wrap .standard-form .main-column ul#friend-list h4 {
+		clear: none;
+	}
+}
+
+.buddypress-wrap .standard-form .bp-tables-user label {
+	margin: 0;
+}
+
+.buddypress-wrap .signup-form label,
+.buddypress-wrap .signup-form legend {
+	font-weight: 400;
+}
+
+body.no-js .buddypress #notifications-bulk-management #select-all-notifications,
+body.no-js .buddypress label[for="message-type-select"],
+body.no-js .buddypress #message-type-select,
+body.no-js .buddypress #delete_inbox_messages,
+body.no-js .buddypress #delete_sentbox_messages,
+body.no-js .buddypress #messages-bulk-management #select-all-messages {
+	display: none;
+}
+
+/* Overrides for embedded WP editors */
+.buddypress-wrap .wp-editor-wrap a.button,
+.buddypress-wrap .wp-editor-wrap .wp-editor-wrap button,
+.buddypress-wrap .wp-editor-wrap .wp-editor-wrap input[type="submit"],
+.buddypress-wrap .wp-editor-wrap .wp-editor-wrap input[type="button"],
+.buddypress-wrap .wp-editor-wrap input[type="reset"] {
+	padding: 0 8px 1px;
+}
+
+.buddypress-wrap .select-wrap {
+	border: 1px solid #eee;
+}
+
+.buddypress-wrap .select-wrap label {
+	display: inline;
+}
+
+.buddypress-wrap .select-wrap select::-ms-expand {
+	display: none;
+}
+
+.buddypress-wrap .select-wrap select {
+	-moz-appearance: none;
+	-webkit-appearance: none;
+	-o-appearance: none;
+	appearance: none;
+	border: 0;
+	cursor: pointer;
+	margin-right: -25px;
+	padding: 6px 25px 6px 10px;
+	position: relative;
+	text-indent: -2px;
+	z-index: 1;
+	width: 100%;
+}
+
+.buddypress-wrap .select-wrap select,
+.buddypress-wrap .select-wrap select:focus,
+.buddypress-wrap .select-wrap select:active {
+	background: transparent;
+}
+
+.buddypress-wrap .select-wrap span.select-arrow {
+	display: inline-block;
+	position: relative;
+	z-index: 0;
+}
+
+.buddypress-wrap .select-wrap span.select-arrow:before {
+	color: #ccc;
+	content: "\25BC";
+}
+
+.buddypress-wrap .select-wrap:focus .select-arrow:before, .buddypress-wrap .select-wrap:hover .select-arrow:before {
+	color: #a6a6a6;
+}
+
+.buddypress-wrap .select-wrap:focus, .buddypress-wrap .select-wrap:hover,
+.buddypress-wrap .bp-search form:focus,
+.buddypress-wrap .bp-search form:hover {
+	border: 1px solid #d5d4d4;
+	box-shadow: inset 0 0 3px #eee;
+}
+
+@media screen and (min-width: 32em) {
+	.buddypress-wrap .notifications-options-nav .select-wrap {
+		float: left;
+	}
+}
+
+/**
+*----------------------------------------------------------
+* @section 6.1 - Directory Search
+*
+* The Search form & controls in directory pages
+*----------------------------------------------------------
+*/
+.buddypress-wrap .bp-dir-search-form, .buddypress-wrap .bp-messages-search-form:before,
+.buddypress-wrap .bp-dir-search-form, .buddypress-wrap .bp-messages-search-form:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress-wrap .bp-dir-search-form, .buddypress-wrap .bp-messages-search-form:after {
+	clear: both;
+}
+
+.buddypress-wrap form.bp-dir-search-form,
+.buddypress-wrap form.bp-messages-search-form,
+.buddypress-wrap form.bp-invites-search-form {
+	border: 1px solid #eee;
+	width: 100%;
+}
+
+@media screen and (min-width: 55em) {
+	.buddypress-wrap form.bp-dir-search-form,
+	.buddypress-wrap form.bp-messages-search-form,
+	.buddypress-wrap form.bp-invites-search-form {
+		width: 15em;
+	}
+}
+
+.buddypress-wrap form.bp-dir-search-form label,
+.buddypress-wrap form.bp-messages-search-form label,
+.buddypress-wrap form.bp-invites-search-form label {
+	margin: 0;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"],
+.buddypress-wrap form.bp-dir-search-form input[type="text"],
+.buddypress-wrap form.bp-dir-search-form button[type="submit"],
+.buddypress-wrap form.bp-messages-search-form input[type="search"],
+.buddypress-wrap form.bp-messages-search-form input[type="text"],
+.buddypress-wrap form.bp-messages-search-form button[type="submit"],
+.buddypress-wrap form.bp-invites-search-form input[type="search"],
+.buddypress-wrap form.bp-invites-search-form input[type="text"],
+.buddypress-wrap form.bp-invites-search-form button[type="submit"] {
+	background: none;
+	border: 0;
+	border-radius: 0;
+	background-clip: padding-box;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"],
+.buddypress-wrap form.bp-dir-search-form input[type="text"],
+.buddypress-wrap form.bp-messages-search-form input[type="search"],
+.buddypress-wrap form.bp-messages-search-form input[type="text"],
+.buddypress-wrap form.bp-invites-search-form input[type="search"],
+.buddypress-wrap form.bp-invites-search-form input[type="text"] {
+	float: left;
+	line-height: 1.5;
+	padding: 3px 10px;
+	width: 80%;
+}
+
+.buddypress-wrap form.bp-dir-search-form button[type="submit"],
+.buddypress-wrap form.bp-messages-search-form button[type="submit"],
+.buddypress-wrap form.bp-invites-search-form button[type="submit"] {
+	float: right;
+	font-size: inherit;
+	font-weight: 400;
+	line-height: 1.5;
+	padding: 3px 0.7em;
+	text-align: center;
+	text-transform: none;
+	width: 20%;
+}
+
+.buddypress-wrap form.bp-dir-search-form button[type="submit"] span,
+.buddypress-wrap form.bp-messages-search-form button[type="submit"] span,
+.buddypress-wrap form.bp-invites-search-form button[type="submit"] span {
+	font-family: dashicons;
+	font-size: 18px;
+	line-height: 1.6;
+}
+
+.buddypress-wrap form.bp-dir-search-form button[type="submit"].bp-show,
+.buddypress-wrap form.bp-messages-search-form button[type="submit"].bp-show,
+.buddypress-wrap form.bp-invites-search-form button[type="submit"].bp-show {
+	height: auto;
+	left: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"]::-webkit-search-cancel-button,
+.buddypress-wrap form.bp-messages-search-form input[type="search"]::-webkit-search-cancel-button,
+.buddypress-wrap form.bp-invites-search-form input[type="search"]::-webkit-search-cancel-button {
+	-webkit-appearance: searchfield-cancel-button;
+}
+
+.buddypress-wrap form.bp-dir-search-form input[type="search"]::-webkit-search-results-button,
+.buddypress-wrap form.bp-dir-search-form input[type="search"]::-webkit-search-results-decoration,
+.buddypress-wrap form.bp-messages-search-form input[type="search"]::-webkit-search-results-button,
+.buddypress-wrap form.bp-messages-search-form input[type="search"]::-webkit-search-results-decoration,
+.buddypress-wrap form.bp-invites-search-form input[type="search"]::-webkit-search-results-button,
+.buddypress-wrap form.bp-invites-search-form input[type="search"]::-webkit-search-results-decoration {
+	display: none;
+}
+
+.buddypress-wrap ul.filters li form label input {
+	line-height: 1.4;
+	padding: 0.1em 0.7em;
+}
+
+.buddypress-wrap .current-member-type {
+	font-style: italic;
+}
+
+.buddypress-wrap .dir-form {
+	clear: both;
+}
+
+.budypress.no-js form.bp-dir-search-form button[type="submit"] {
+	height: auto;
+	left: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+.bp-user [data-bp-search] form input[type="search"],
+.bp-user [data-bp-search] form input[type="text"] {
+	padding: 6px 10px 7px;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+.buddypress-wrap .bp-tables-user,
+.buddypress-wrap table.wp-profile-fields,
+.buddypress-wrap table.forum {
+	width: 100%;
+}
+
+.buddypress-wrap .bp-tables-user thead tr,
+.buddypress-wrap table.wp-profile-fields thead tr,
+.buddypress-wrap table.forum thead tr {
+	background: none;
+	border-bottom: 2px solid #ccc;
+}
+
+.buddypress-wrap .bp-tables-user tbody tr,
+.buddypress-wrap table.wp-profile-fields tbody tr,
+.buddypress-wrap table.forum tbody tr {
+	background: #fafafa;
+}
+
+.buddypress-wrap .bp-tables-user tr th,
+.buddypress-wrap .bp-tables-user tr td,
+.buddypress-wrap table.wp-profile-fields tr th,
+.buddypress-wrap table.wp-profile-fields tr td,
+.buddypress-wrap table.forum tr th,
+.buddypress-wrap table.forum tr td {
+	padding: 0.5em;
+	vertical-align: middle;
+}
+
+.buddypress-wrap .bp-tables-user tr td.label,
+.buddypress-wrap table.wp-profile-fields tr td.label,
+.buddypress-wrap table.forum tr td.label {
+	border-right: 1px solid #eaeaea;
+	font-weight: 600;
+	width: 25%;
+}
+
+.buddypress-wrap .bp-tables-user tr.alt td,
+.buddypress-wrap table.wp-profile-fields tr.alt td {
+	background: #fafafa;
+}
+
+.buddypress-wrap table.profile-fields .data {
+	padding: 0.5em 1em;
+}
+
+.buddypress-wrap table.profile-fields tr:last-child {
+	border-bottom: none;
+}
+
+.buddypress-wrap table.notifications td {
+	padding: 1em 0.5em;
+}
+
+.buddypress-wrap table.notifications .bulk-select-all,
+.buddypress-wrap table.notifications .bulk-select-check {
+	width: 7%;
+}
+
+.buddypress-wrap table.notifications .bulk-select-check {
+	vertical-align: middle;
+}
+
+.buddypress-wrap table.notifications .title,
+.buddypress-wrap table.notifications .notification-description,
+.buddypress-wrap table.notifications .date,
+.buddypress-wrap table.notifications .notification-since {
+	width: 39%;
+}
+
+.buddypress-wrap table.notifications .actions,
+.buddypress-wrap table.notifications .notification-actions {
+	width: 15%;
+}
+
+.buddypress-wrap table.notification-settings th.title,
+.buddypress-wrap table.profile-settings th.title {
+	width: 80%;
+}
+
+.buddypress-wrap table.notifications .notification-actions a.delete,
+.buddypress-wrap table.notifications .notification-actions a.mark-read {
+	display: inline-block;
+}
+
+.buddypress-wrap table.notification-settings {
+	margin-bottom: 15px;
+	text-align: left;
+}
+
+.buddypress-wrap #groups-notification-settings {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap table.notifications th.icon,
+.buddypress-wrap table.notifications td:first-child,
+.buddypress-wrap table.notification-settings th.icon,
+.buddypress-wrap table.notification-settings td:first-child {
+	display: none;
+}
+
+.buddypress-wrap table.notification-settings .no,
+.buddypress-wrap table.notification-settings .yes {
+	text-align: center;
+	width: 40px;
+	vertical-align: middle;
+}
+
+.buddypress-wrap table#message-threads {
+	clear: both;
+}
+
+.buddypress-wrap table#message-threads .thread-info {
+	min-width: 40%;
+}
+
+.buddypress-wrap table#message-threads .thread-info p {
+	margin: 0;
+}
+
+.buddypress-wrap table#message-threads .thread-info p.thread-excerpt {
+	color: #737373;
+	font-size: 12px;
+	margin-top: 3px;
+}
+
+.buddypress-wrap table.profile-fields {
+	margin-bottom: 20px;
+}
+
+.buddypress-wrap table.profile-fields:last-child {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap table.profile-fields p {
+	margin: 0;
+}
+
+.buddypress-wrap table.profile-fields p:last-child {
+	margin-top: 0;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Classes - Messages, Ajax, Widgets, Buttons
+*-------------------------------------------------------------------------------
+*/
+.bp-screen-reader-text {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+	word-wrap: normal !important;
+}
+
+.clearfix:before, .clearfix:after {
+	content: " ";
+	display: table;
+}
+
+.clearfix:after {
+	clear: both;
+}
+
+.center-vert {
+	display: flex;
+	align-items: center;
+}
+
+.bp-hide {
+	display: none;
+}
+
+.bp-show {
+	height: auto;
+	left: 0;
+	overflow: visible;
+	position: static;
+	top: 0;
+}
+
+.buddypress .buddypress-wrap button,
+.buddypress .buddypress-wrap a.button,
+.buddypress .buddypress-wrap input[type="submit"],
+.buddypress .buddypress-wrap input[type="button"],
+.buddypress .buddypress-wrap input[type="reset"],
+.buddypress .buddypress-wrap ul.button-nav:not(.button-tabs) li a,
+.buddypress .buddypress-wrap .generic-button a,
+.buddypress .buddypress-wrap .comment-reply-link,
+.buddypress .buddypress-wrap a.bp-title-button,
+.buddypress .buddypress-wrap .activity-read-more a {
+	background: #fff;
+	/* Old browsers */
+	border-color: #ccc;
+	border-style: solid;
+	border-width: 1px;
+	color: #555;
+	cursor: pointer;
+	font-size: inherit;
+	font-weight: 400;
+	outline: none;
+	padding: 0.3em 0.5em;
+	text-align: center;
+	text-decoration: none;
+	width: auto;
+}
+
+.buddypress .buddypress-wrap .button-small[type="button"] {
+	padding: 0 8px 1px;
+}
+
+.buddypress .buddypress-wrap button:hover,
+.buddypress .buddypress-wrap button:focus,
+.buddypress .buddypress-wrap a.button:focus,
+.buddypress .buddypress-wrap a.button:hover,
+.buddypress .buddypress-wrap input[type="submit"]:focus,
+.buddypress .buddypress-wrap input[type="submit"]:hover,
+.buddypress .buddypress-wrap input[type="button"]:focus,
+.buddypress .buddypress-wrap input[type="button"]:hover,
+.buddypress .buddypress-wrap input[type="reset"]:focus,
+.buddypress .buddypress-wrap input[type="reset"]:hover,
+.buddypress .buddypress-wrap .button-nav li a:focus,
+.buddypress .buddypress-wrap .button-nav li a:hover,
+.buddypress .buddypress-wrap .button-nav li.current a,
+.buddypress .buddypress-wrap .generic-button a:focus,
+.buddypress .buddypress-wrap .generic-button a:hover,
+.buddypress .buddypress-wrap .comment-reply-link:focus,
+.buddypress .buddypress-wrap .comment-reply-link:hover,
+.buddypress .buddypress-wrap .activity-read-more a:focus,
+.buddypress .buddypress-wrap .activity-read-more a:hover {
+	background: #ededed;
+	border-color: #999999;
+	color: #333;
+	outline: none;
+	text-decoration: none;
+}
+
+.buddypress .buddypress-wrap input[type="submit"].pending,
+.buddypress .buddypress-wrap input[type="button"].pending,
+.buddypress .buddypress-wrap input[type="reset"].pending,
+.buddypress .buddypress-wrap input[type="button"].disabled,
+.buddypress .buddypress-wrap input[type="reset"].disabled,
+.buddypress .buddypress-wrap input[type="submit"][disabled="disabled"],
+.buddypress .buddypress-wrap button.pending,
+.buddypress .buddypress-wrap button.disabled,
+.buddypress .buddypress-wrap div.pending a,
+.buddypress .buddypress-wrap a.disabled {
+	border-color: #eee;
+	color: #767676;
+	cursor: default;
+}
+
+.buddypress .buddypress-wrap input[type="submit"]:hover.pending,
+.buddypress .buddypress-wrap input[type="button"]:hover.pending,
+.buddypress .buddypress-wrap input[type="reset"]:hover.pending,
+.buddypress .buddypress-wrap input[type="submit"]:hover.disabled,
+.buddypress .buddypress-wrap input[type="button"]:hover.disabled,
+.buddypress .buddypress-wrap input[type="reset"]:hover.disabled,
+.buddypress .buddypress-wrap button.pending:hover,
+.buddypress .buddypress-wrap button.disabled:hover,
+.buddypress .buddypress-wrap div.pending a:hover,
+.buddypress .buddypress-wrap a.disabled:hover {
+	border-color: #eee;
+	color: #767676;
+}
+
+.buddypress .buddypress-wrap button.text-button,
+.buddypress .buddypress-wrap input.text-button {
+	background: none;
+	border: 0;
+	box-shadow: none;
+	color: #767676;
+}
+
+.buddypress .buddypress-wrap button.text-button.small,
+.buddypress .buddypress-wrap input.text-button.small {
+	font-size: 13px;
+}
+
+.buddypress .buddypress-wrap button.text-button:focus, .buddypress .buddypress-wrap button.text-button:hover,
+.buddypress .buddypress-wrap input.text-button:focus,
+.buddypress .buddypress-wrap input.text-button:hover {
+	background: none;
+	text-decoration: underline;
+}
+
+.buddypress .buddypress-wrap .activity-list a.button {
+	border: none;
+}
+
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li a.invite-button:hover {
+	color: #1fb3dd;
+}
+
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li.selected a.invite-button:hover,
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li.selected a.group-remove-invite-button:hover,
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li a.invite-button:hover,
+.buddypress .buddypress-wrap .bp-invites-content ul.bp-list li a.group-remove-invite-button:hover {
+	color: #a00;
+}
+
+.buddypress .buddypress-wrap #item-buttons:empty {
+	display: none;
+}
+
+.buddypress .buddypress-wrap input:disabled:hover, .buddypress .buddypress-wrap input:disabled:focus {
+	background: none;
+}
+
+.buddypress .buddypress-wrap .text-links-list a.button {
+	background: none;
+	border: none;
+	border-right: 1px solid #eee;
+	color: #737373;
+	display: inline-block;
+	padding: 0.3em 1em;
+}
+
+.buddypress .buddypress-wrap .text-links-list a.button:visited {
+	color: #d6d6d6;
+}
+
+.buddypress .buddypress-wrap .text-links-list a.button:focus, .buddypress .buddypress-wrap .text-links-list a.button:hover {
+	color: #5087e5;
+}
+
+.buddypress .buddypress-wrap .text-links-list a:first-child {
+	padding-left: 0;
+}
+
+.buddypress .buddypress-wrap .text-links-list a:last-child {
+	border-right: none;
+}
+
+.buddypress .buddypress-wrap .bp-list.grid .action a,
+.buddypress .buddypress-wrap .bp-list.grid .action button {
+	border: 1px solid #ccc;
+	display: block;
+	margin: 0;
+}
+
+.buddypress .buddypress-wrap .bp-list.grid .action a:focus, .buddypress .buddypress-wrap .bp-list.grid .action a:hover,
+.buddypress .buddypress-wrap .bp-list.grid .action button:focus,
+.buddypress .buddypress-wrap .bp-list.grid .action button:hover {
+	background: #ededed;
+}
+
+.buddypress #buddypress .create-button {
+	background: none;
+	text-align: center;
+}
+
+.buddypress #buddypress .create-button a:focus,
+.buddypress #buddypress .create-button a:hover {
+	text-decoration: underline;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress #buddypress .create-button {
+		float: right;
+	}
+}
+
+.buddypress #buddypress .create-button a {
+	border: 1px solid #ccc;
+	border-radius: 5px;
+	background-clip: padding-box;
+	box-shadow: inset 0 0 6px 0 #eaeaea;
+	margin: 0.2em 0;
+	width: auto;
+}
+
+.buddypress #buddypress .create-button a:focus, .buddypress #buddypress .create-button a:hover {
+	background: none;
+	border-color: #ccc;
+	box-shadow: inset 0 0 12px 0 #eaeaea;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress #buddypress.bp-dir-vert-nav .create-button {
+		float: none;
+		padding-top: 2em;
+	}
+	.buddypress #buddypress.bp-dir-vert-nav .create-button a {
+		margin-right: 0.5em;
+	}
+}
+
+.buddypress #buddypress.bp-dir-hori-nav .create-button {
+	float: left;
+}
+
+.buddypress #buddypress.bp-dir-hori-nav .create-button a,
+.buddypress #buddypress.bp-dir-hori-nav .create-button a:hover {
+	background: none;
+	border: 0;
+	box-shadow: none;
+	margin: 0;
+}
+
+.buddypress-wrap button.bp-icons, .buddypress-wrap button.ac-reply-cancel {
+	background: none;
+	border: 0;
+}
+
+.buddypress-wrap button.bp-icons:focus, .buddypress-wrap button.bp-icons:hover {
+	background: none;
+}
+
+.buddypress-wrap button.ac-reply-cancel:focus, .buddypress-wrap button.ac-reply-cancel:hover {
+	background: none;
+	text-decoration: underline;
+}
+
+.buddypress-wrap .filter label:before,
+.buddypress-wrap .feed a:before,
+.buddypress-wrap .bp-invites-filters .invite-button span.icons:before,
+.buddypress-wrap .bp-messages-filters li a.messages-button:before,
+.buddypress-wrap .bp-invites-content li .invite-button span.icons:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.buddypress-wrap .bp-invites-content .item-list li .invite-button span.icons:before {
+	font-size: 27px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .bp-invites-content .item-list li .invite-button span.icons:before {
+		font-size: 32px;
+	}
+}
+
+.buddypress-wrap .bp-list a.button.invite-button:focus, .buddypress-wrap .bp-list a.button.invite-button:hover {
+	background: none;
+}
+
+.buddypress-wrap .filter label:before {
+	content: "\f536";
+}
+
+.buddypress-wrap div.feed a:before,
+.buddypress-wrap li.feed a:before {
+	content: "\f303";
+}
+
+.buddypress-wrap ul.item-list li .invite-button:not(.group-remove-invite-button) span.icons:before {
+	content: "\f502";
+}
+
+.buddypress-wrap ul.item-list li.selected .invite-button span.icons:before,
+.buddypress-wrap ul.item-list li .group-remove-invite-button span.icons:before {
+	content: "\f153";
+}
+
+.buddypress-wrap .bp-invites-filters ul li #bp-invites-next-page:before,
+.buddypress-wrap .bp-messages-filters ul li #bp-messages-next-page:before {
+	content: "\f345";
+}
+
+.buddypress-wrap .bp-invites-filters ul li #bp-invites-prev-page:before,
+.buddypress-wrap .bp-messages-filters ul li #bp-messages-prev-page:before {
+	content: "\f341";
+}
+
+.buddypress-wrap .warn {
+	color: #b71717;
+}
+
+.buddypress-wrap .bp-messages {
+	border: 1px solid #ccc;
+	margin: 0 0 15px;
+}
+
+.buddypress-wrap .bp-messages .sitewide-notices {
+	display: block;
+	margin: 5px;
+	padding: 0.5em;
+}
+
+.buddypress-wrap .bp-messages.info {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .bp-messages.updated {
+	clear: both;
+	display: block;
+}
+
+.buddypress-wrap .bp-messages.bp-user-messages-feedback {
+	border: 0;
+}
+
+.buddypress-wrap #group-create-body .bp-cover-image-status p.warning {
+	background: #0b80a4;
+	border: 0;
+	box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.2);
+	color: #fff;
+}
+
+.buddypress-wrap .bp-feedback:not(.custom-homepage-info) {
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: row nowrap;
+	flex-flow: row nowrap;
+	align-items: stretch;
+}
+
+.buddypress-wrap .bp-feedback {
+	background: #fff;
+	color: #807f7f;
+	box-shadow: 0 1px 1px 1px rgba(0, 0, 0, 0.1);
+	color: #737373;
+	margin: 10px 0;
+	position: relative;
+}
+
+.buddypress-wrap .bp-feedback p {
+	margin: 0;
+}
+
+.buddypress-wrap .bp-feedback span.bp-icon {
+	color: #fff;
+	display: block;
+	font-family: dashicons;
+	left: 0;
+	margin-right: 10px;
+	position: relative;
+	padding: 0  0.5em;
+}
+
+.buddypress-wrap .bp-feedback .bp-help-text {
+	font-style: italic;
+}
+
+.buddypress-wrap .bp-feedback .text {
+	font-size: 14px;
+	margin: 0;
+	padding: 0.5em 0;
+}
+
+.buddypress-wrap .bp-feedback.no-icon {
+	padding: 0.5em;
+}
+
+.buddypress-wrap .bp-feedback.small:before {
+	line-height: inherit;
+}
+
+.buddypress-wrap a[data-bp-close] span:before,
+.buddypress-wrap button[data-bp-close] span:before {
+	font-size: 32px;
+}
+
+.buddypress-wrap a[data-bp-close],
+.buddypress-wrap button[data-bp-close] {
+	border: 0;
+	position: absolute;
+	top: 10px;
+	right: 10px;
+	width: 32px;
+}
+
+.buddypress-wrap .bp-feedback.no-icon a[data-bp-close],
+.buddypress-wrap .bp-feedback.no-icon button[data-bp-close] {
+	top: -6px;
+	right: 6px;
+}
+
+.buddypress-wrap button[data-bp-close]:hover {
+	background-color: transparent;
+}
+
+.buddypress-wrap .bp-feedback p {
+	margin: 0;
+}
+
+.buddypress-wrap .bp-feedback .bp-icon {
+	font-size: 20px;
+	padding: 0 2px;
+}
+
+.buddypress-wrap .bp-feedback.info .bp-icon,
+.buddypress-wrap .bp-feedback.help .bp-icon,
+.buddypress-wrap .bp-feedback.error .bp-icon,
+.buddypress-wrap .bp-feedback.warning .bp-icon,
+.buddypress-wrap .bp-feedback.loading .bp-icon,
+.buddypress-wrap .bp-feedback.success .bp-icon,
+.buddypress-wrap .bp-feedback.updated .bp-icon {
+	display: flex;
+	align-items: center;
+}
+
+.buddypress-wrap .bp-feedback.info .bp-icon,
+.buddypress-wrap .bp-feedback.help .bp-icon {
+	background-color: #0b80a4;
+}
+
+.buddypress-wrap .bp-feedback.info .bp-icon:before,
+.buddypress-wrap .bp-feedback.help .bp-icon:before {
+	content: "\f348";
+}
+
+.buddypress-wrap .bp-feedback.error .bp-icon,
+.buddypress-wrap .bp-feedback.warning .bp-icon {
+	background-color: #d33;
+}
+
+.buddypress-wrap .bp-feedback.error .bp-icon:before,
+.buddypress-wrap .bp-feedback.warning .bp-icon:before {
+	content: "\f534";
+}
+
+.buddypress-wrap .bp-feedback.loading .bp-icon {
+	background-color: #ffd087;
+}
+
+.buddypress-wrap .bp-feedback.loading .bp-icon:before {
+	content: "\f469";
+}
+
+.buddypress-wrap .bp-feedback.success .bp-icon,
+.buddypress-wrap .bp-feedback.updated .bp-icon {
+	background-color: #8a2;
+}
+
+.buddypress-wrap .bp-feedback.success .bp-icon:before,
+.buddypress-wrap .bp-feedback.updated .bp-icon:before {
+	content: "\f147";
+}
+
+.buddypress-wrap .bp-feedback.help .bp-icon:before {
+	content: "\f468";
+}
+
+.buddypress-wrap #pass-strength-result {
+	background-color: #eee;
+	border-color: #ddd;
+	border-style: solid;
+	border-width: 1px;
+	display: none;
+	font-weight: 700;
+	margin: 10px 0 10px 0;
+	padding: 0.5em;
+	text-align: center;
+	width: auto;
+}
+
+.buddypress-wrap #pass-strength-result.show {
+	display: block;
+}
+
+.buddypress-wrap #pass-strength-result.mismatch {
+	background-color: #333;
+	border-color: transparent;
+	color: #fff;
+}
+
+.buddypress-wrap #pass-strength-result.error, .buddypress-wrap #pass-strength-result.bad {
+	background-color: #ffb78c;
+	border-color: #ff853c;
+	color: #fff;
+}
+
+.buddypress-wrap #pass-strength-result.short {
+	background-color: #ffa0a0;
+	border-color: #f04040;
+	color: #fff;
+}
+
+.buddypress-wrap #pass-strength-result.strong {
+	background-color: #66d66e;
+	border-color: #438c48;
+	color: #fff;
+}
+
+.buddypress-wrap .standard-form#signup_form div div.error {
+	background: #faa;
+	color: #a00;
+	margin: 0 0 10px 0;
+	padding: 0.5em;
+	width: 90%;
+}
+
+.buddypress-wrap .accept,
+.buddypress-wrap .reject {
+	float: left;
+	margin-left: 10px;
+}
+
+.buddypress-wrap .members-list.grid .bp-ajax-message {
+	background: rgba(255, 255, 255, 0.9);
+	border: 1px solid #eee;
+	font-size: 14px;
+	left: 2%;
+	position: absolute;
+	padding: 0.5em 1em;
+	right: 2%;
+	top: 30px;
+}
+
+.buddypress.widget .item-options {
+	font-size: 12px;
+	margin: 0 0 1em;
+	padding: 1em 0;
+}
+
+.buddypress.widget .bp-login-widget-user-avatar {
+	float: left;
+}
+
+.buddypress.widget .bp-login-widget-user-links {
+	margin-left: 70px;
+}
+
+.buddypress.widget ul.item-list {
+	list-style: none;
+	margin: 10px 0;
+}
+
+.buddypress.widget ul.activity-list {
+	padding: 0;
+}
+
+.buddypress.widget ul.activity-list blockquote {
+	margin: 0 0 1.5em;
+	overflow: visible;
+	padding: 0 0 0.75em 0.75em;
+}
+
+.buddypress.widget ul.activity-list img {
+	margin-bottom: 0.5em;
+}
+
+.buddypress.widget ul.activity-list li {
+	border-bottom: 1px solid #ccc;
+	margin-bottom: 1em;
+}
+
+.buddypress.widget ul.activity-list li .activity-header p {
+	margin-bottom: 0.5em;
+}
+
+.buddypress.widget ul.activity-list li:last-child {
+	border-bottom: 0;
+}
+
+.buddypress.widget .avatar-block {
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: row wrap;
+	flex-flow: row wrap;
+}
+
+.buddypress.widget .avatar-block img {
+	margin: 0.5em;
+}
+
+.buddypress.widget ul#friends-list li:before,
+.buddypress.widget ul#friends-list li:after,
+.buddypress.widget ul#groups-list li:before,
+.buddypress.widget ul#groups-list li:after,
+.buddypress.widget ul#members-list li:before,
+.buddypress.widget ul#members-list li:after {
+	content: " ";
+	display: table;
+}
+
+.buddypress.widget ul#friends-list li:after,
+.buddypress.widget ul#groups-list li:after,
+.buddypress.widget ul#members-list li:after {
+	clear: both;
+}
+
+.buddypress.widget ul#friends-list li,
+.buddypress.widget ul#groups-list li,
+.buddypress.widget ul#members-list li {
+	margin-bottom: 1em;
+}
+
+.buddypress.widget ul#friends-list li .item-avatar,
+.buddypress.widget ul#groups-list li .item-avatar,
+.buddypress.widget ul#members-list li .item-avatar {
+	float: left;
+	width: 60px;
+}
+
+.buddypress.widget ul#friends-list li .item,
+.buddypress.widget ul#groups-list li .item,
+.buddypress.widget ul#members-list li .item {
+	margin-left: 70px;
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list,
+.buddypress-wrap .buddypress.widget ul#groups-list,
+.buddypress-wrap .buddypress.widget ul#members-list {
+	display: -moz-flex;
+	display: -ms-flex;
+	display: -o-flex;
+	display: flex;
+	-o-flex-flow: column nowrap;
+	flex-flow: column nowrap;
+}
+
+@media screen and (min-width: 32em) {
+	.buddypress-wrap .buddypress.widget ul#friends-list,
+	.buddypress-wrap .buddypress.widget ul#groups-list,
+	.buddypress-wrap .buddypress.widget ul#members-list {
+		display: -moz-flex;
+		display: -ms-flex;
+		display: -o-flex;
+		display: flex;
+		-o-flex-flow: row wrap;
+		flex-flow: row wrap;
+	}
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li,
+.buddypress-wrap .buddypress.widget ul#groups-list li,
+.buddypress-wrap .buddypress.widget ul#members-list li {
+	border: 1px solid #eee;
+	align-items: stretch;
+	-moz-flex: 1 1 46%;
+	-o-flex: 1 1 46%;
+	flex: 1 1 46%;
+	margin: 2%;
+}
+
+@media screen and (min-width: 75em) {
+	.buddypress-wrap .buddypress.widget ul#friends-list li,
+	.buddypress-wrap .buddypress.widget ul#groups-list li,
+	.buddypress-wrap .buddypress.widget ul#members-list li {
+		-moz-flex: 0 1 20%;
+		-o-flex: 0 1 20%;
+		flex: 0 1 20%;
+	}
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li .item-avatar,
+.buddypress-wrap .buddypress.widget ul#groups-list li .item-avatar,
+.buddypress-wrap .buddypress.widget ul#members-list li .item-avatar {
+	padding: 0.5em;
+	text-align: center;
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li .item,
+.buddypress-wrap .buddypress.widget ul#groups-list li .item,
+.buddypress-wrap .buddypress.widget ul#members-list li .item {
+	padding: 0.5em;
+}
+
+.buddypress-wrap .buddypress.widget ul#friends-list li .item .item-meta,
+.buddypress-wrap .buddypress.widget ul#groups-list li .item .item-meta,
+.buddypress-wrap .buddypress.widget ul#members-list li .item .item-meta {
+	font-size: 12px;
+	overflow-wrap: break-word;
+}
+
+@media screen and (min-width: 75em) {
+	.buddypress-wrap .buddypress.widget ul#friends-list,
+	.buddypress-wrap .buddypress.widget ul#groups-list,
+	.buddypress-wrap .buddypress.widget ul#members-list {
+		margin: 10px -2%;
+		width: 100%;
+	}
+	.buddypress-wrap .buddypress.widget ul#friends-list li,
+	.buddypress-wrap .buddypress.widget ul#groups-list li,
+	.buddypress-wrap .buddypress.widget ul#members-list li {
+		-moz-flex: 0 1 auto;
+		-o-flex: 0 1 auto;
+		flex: 0 1 auto;
+		margin: 10px 2% 1%;
+		width: 46%;
+	}
+}
+
+.buddypress-wrap .buddypress.widget blockquote {
+	margin: 0 0 1.5em;
+	overflow: visible;
+	padding: 0 0 0.75em 0.75em;
+}
+
+#buddypress-wrap * {
+	transition: opacity 0.1s ease-in-out 0.1s;
+}
+
+#buddypress-wrap button,
+#buddypress-wrap a.generic-button,
+#buddypress-wrap a.button,
+#buddypress-wrap input[type="submit"],
+#buddypress-wrap input[type="reset"] {
+	transition: background 0.1s ease-in-out 0.1s, color 0.1s ease-in-out 0.1s, border-color 0.1s ease-in-out 0.1s;
+}
+
+.buddypress-wrap a.loading,
+.buddypress-wrap input.loading {
+	animation: loader-pulsate 0.5s infinite ease-in-out alternate;
+	border-color: #aaa;
+}
+
+@keyframes loader-pulsate {
+	from {
+		border-color: #aaa;
+		box-shadow: 0 0 6px #ccc;
+	}
+	to {
+		border-color: #ccc;
+		box-shadow: 0 0 6px #f8f8f8;
+	}
+}
+
+.buddypress-wrap a.loading:hover,
+.buddypress-wrap input.loading:hover {
+	color: #777;
+}
+
+[data-bp-tooltip] {
+	position: relative;
+}
+
+[data-bp-tooltip]:after {
+	background-color: #fff;
+	display: none;
+	opacity: 0;
+	position: absolute;
+	transform: translate3d(0, 0, 0);
+	visibility: hidden;
+}
+
+[data-bp-tooltip]:after {
+	border: 1px solid #737373;
+	border-radius: 1px;
+	box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.2);
+	color: #333;
+	content: attr(data-bp-tooltip);
+	font-family: "Helvetica Neue", helvetica, arial, san-serif;
+	font-size: 12px;
+	font-weight: 400;
+	letter-spacing: normal;
+	line-height: 1.25;
+	max-width: 200px;
+	padding: 5px 8px;
+	pointer-events: none;
+	text-shadow: none;
+	text-transform: none;
+	transition: all 1.5s ease;
+	white-space: nowrap;
+	word-wrap: break-word;
+	z-index: 100000;
+}
+
+[data-bp-tooltip]:hover:after, [data-bp-tooltip]:active:after, [data-bp-tooltip]:focus:after {
+	display: block;
+	opacity: 1;
+	overflow: visible;
+	visibility: visible;
+}
+
+[data-bp-tooltip=""] {
+	display: none;
+	opacity: 0;
+	visibility: hidden;
+}
+
+.bp-tooltip:after {
+	left: 50%;
+	margin-top: 7px;
+	top: 110%;
+	transform: translate(-50%, 0);
+}
+
+.user-list .bp-tooltip:after {
+	left: 0;
+	transform: translate(0, 0);
+}
+
+@media screen and (min-width: 46.8em) {
+	.user-list .bp-tooltip:after {
+		left: auto;
+		right: 0;
+		transform: translate(0, 0);
+	}
+}
+
+.activity-list .bp-tooltip:after,
+.activity-meta-action .bp-tooltip:after,
+.avatar-block .item-avatar .bp-tooltip:after,
+.notification-actions .bp-tooltip:after,
+.participants-list .bp-tooltip:after {
+	left: 0;
+	transform: translate(0, 0);
+}
+
+.bp-invites-content .bp-tooltip:after,
+.message-metadata .actions .bp-tooltip:after,
+.single-message-thread-header .actions .bp-tooltip:after {
+	left: auto;
+	right: 0;
+	transform: translate(0, 0);
+}
+
+.bp-invites-content #send-invites-editor .bp-tooltip:after {
+	left: 0;
+	right: auto;
+}
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Layout classes
+*-------------------------------------------------------------------------------
+*/
+#item-body,
+.single-screen-navs {
+	box-sizing: border-box;
+}
+
+.grid > li,
+.grid > li .generic-button a {
+	box-sizing: border-box;
+}
+
+.grid > li {
+	border-bottom: 0;
+	padding-bottom: 10px;
+	padding-top: 0;
+}
+
+.grid > li .list-wrap {
+	background: #fafafa;
+	border: 1px solid #eee;
+	padding-bottom: 15px;
+	position: relative;
+	overflow: hidden;
+	padding-top: 14px;
+}
+
+.grid > li .list-wrap .list-title {
+	padding: 0.5em;
+}
+
+.grid > li .list-wrap .update {
+	color: #737373;
+	padding: 0.5em 2em;
+}
+
+.grid > li .item-avatar {
+	text-align: center;
+}
+
+.grid > li .item-avatar .avatar {
+	border-radius: 50%;
+	display: inline-block;
+	width: 50%;
+}
+
+@media screen and (min-width: 24em) {
+	.grid.members-list .list-wrap {
+		min-height: 340px;
+	}
+	.grid.members-list .list-wrap .item-block {
+		margin: 0 auto;
+		min-height: 7rem;
+	}
+	.grid.members-group-list .list-wrap .item-block {
+		margin: 0 auto;
+		min-height: 7rem;
+	}
+	.grid.groups-list .list-wrap {
+		min-height: 470px;
+	}
+	.grid.groups-list .list-wrap .item-block {
+		min-height: 6rem;
+	}
+	.grid.groups-list .list-wrap .group-desc {
+		margin: 15px auto 0;
+		min-height: 5em;
+		overflow: hidden;
+	}
+	.grid.groups-list .list-wrap .last-activity,
+	.grid.groups-list .list-wrap .group-details,
+	.grid.groups-list .list-wrap .item-desc {
+		margin-bottom: 0;
+	}
+	.grid.groups-list .list-wrap .last-activity p,
+	.grid.groups-list .list-wrap .group-details p,
+	.grid.groups-list .list-wrap .item-desc p {
+		margin-bottom: 0;
+	}
+	.grid.blogs-list .list-wrap {
+		min-height: 350px;
+	}
+	.grid.blogs-list .list-wrap .item-block {
+		margin: 0 auto;
+		min-height: 7rem;
+	}
+}
+
+/* Build the two column class small up */
+@media screen and (min-width: 24em) {
+	.grid > li.item-entry {
+		float: left;
+		margin: 0;
+	}
+	.grid.two > li {
+		padding-bottom: 20px;
+	}
+}
+
+@media screen and (min-width: 24em) and (min-width: 75em) {
+	.grid.two > li .list-wrap {
+		max-width: 500px;
+		margin: 0 auto;
+	}
+}
+
+@media screen and (min-width: 24em) {
+	.grid.two > li, .grid.three > li {
+		width: 50%;
+	}
+	.grid.two > li:nth-child(odd), .grid.three > li:nth-child(odd) {
+		padding-right: 10px;
+	}
+	.grid.two > li:nth-child(even), .grid.three > li:nth-child(even) {
+		padding-left: 10px;
+	}
+	.grid.two > li .item, .grid.three > li .item {
+		margin: 1rem auto 0;
+		width: 80%;
+	}
+	.grid.two > li .item .item-title, .grid.three > li .item .item-title {
+		width: auto;
+	}
+}
+
+/* Build the three column class medium up */
+@media screen and (min-width: 46.8em) {
+	.grid.three > li {
+		padding-top: 0;
+		width: 33.333333%;
+		width: calc(100% / 3);
+	}
+	.grid.three > li:nth-child(1n+1) {
+		padding-left: 5px;
+		padding-right: 5px;
+	}
+	.grid.three > li:nth-child(3n+3) {
+		padding-left: 5px;
+		padding-right: 0;
+	}
+	.grid.three > li:nth-child(3n+1) {
+		padding-left: 0;
+		padding-right: 5px;
+	}
+}
+
+/* Build the four column class medium up */
+@media screen and (min-width: 46.8em) {
+	.grid.four > li {
+		width: 25%;
+	}
+	.grid.four > li:nth-child(1n+1) {
+		padding-left: 5px;
+		padding-right: 5px;
+	}
+	.grid.four > li:nth-child(4n+4) {
+		padding-left: 5px;
+		padding-right: 0;
+	}
+	.grid.four > li:nth-child(4n+1) {
+		padding-left: 0;
+		padding-right: 5px;
+	}
+}
+
+.buddypress-wrap .grid.bp-list {
+	padding-top: 1em;
+}
+
+.buddypress-wrap .grid.bp-list > li {
+	border-bottom: none;
+}
+
+.buddypress-wrap .grid.bp-list > li .list-wrap {
+	padding-bottom: 3em;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-avatar {
+	margin: 0;
+	text-align: center;
+	width: auto;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-avatar img.avatar {
+	display: inline-block;
+	height: auto;
+	width: 50%;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-meta,
+.buddypress-wrap .grid.bp-list > li .list-title {
+	float: none;
+	text-align: center;
+}
+
+.buddypress-wrap .grid.bp-list > li .list-title {
+	font-size: inherit;
+	line-height: 1.1;
+}
+
+.buddypress-wrap .grid.bp-list > li .item {
+	font-size: 18px;
+	left: 0;
+	margin: 0 auto;
+	text-align: center;
+	width: 96%;
+}
+
+@media screen and (min-width: 46.8em) {
+	.buddypress-wrap .grid.bp-list > li .item {
+		font-size: 22px;
+	}
+}
+
+.buddypress-wrap .grid.bp-list > li .item .item-block,
+.buddypress-wrap .grid.bp-list > li .item .group-desc {
+	float: none;
+	width: 96%;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .item-block {
+	margin-bottom: 10px;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .last-activity {
+	margin-top: 5px;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .group-desc {
+	clear: none;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .user-update {
+	clear: both;
+	text-align: left;
+}
+
+.buddypress-wrap .grid.bp-list > li .item .activity-read-more a {
+	display: inline;
+}
+
+.buddypress-wrap .grid.bp-list > li .action {
+	bottom: 5px;
+	float: none;
+	height: auto;
+	left: 0;
+	margin: 0;
+	padding: 0 5px;
+	position: absolute;
+	text-align: center;
+	top: auto;
+	width: 100%;
+}
+
+.buddypress-wrap .grid.bp-list > li .action .generic-button {
+	float: none;
+	margin: 5px 0 0;
+	text-align: center;
+	width: 100%;
+}
+
+.buddypress-wrap .grid.bp-list > li .action .generic-button a,
+.buddypress-wrap .grid.bp-list > li .action .generic-button button {
+	width: 100%;
+}
+
+.buddypress-wrap .grid.bp-list > li .item-avatar,
+.buddypress-wrap .grid.bp-list > li .avatar,
+.buddypress-wrap .grid.bp-list > li .item {
+	float: none;
+}
+
+.buddypress-wrap .blogs-list.grid.two > li .blogs-title {
+	min-height: 5em;
+}
+
+.buddypress-wrap .grid.three > li .group-desc,
+.buddypress-wrap .grid.four > li .group-desc {
+	min-height: 8em;
+}
+
+.buddypress-wrap .blogs-list.grid.three > li,
+.buddypress-wrap .blogs-list.grid.four > li {
+	min-height: 350px;
+}
+
+.buddypress-wrap .blogs-list.grid.three > li .last-activity,
+.buddypress-wrap .blogs-list.grid.four > li .last-activity {
+	margin-bottom: 0;
+}
+
+.buddypress-wrap .blogs-list.grid.three > li .last-post,
+.buddypress-wrap .blogs-list.grid.four > li .last-post {
+	margin-top: 0;
+}
+
+.buddypress:not(.logged-in) .grid.bp-list .list-wrap {
+	padding-bottom: 5px;
+}
+
+.buddypress:not(.logged-in) .grid.groups-list .list-wrap {
+	min-height: 430px;
+}
+
+.buddypress:not(.logged-in) .grid.members-list .list-wrap {
+	min-height: 300px;
+}
+
+.buddypress:not(.logged-in) .grid.blogs-list .list-wrap {
+	min-height: 320px;
+}
+
+@media screen and (min-width: 46.8em) {
+	.bp-single-vert-nav .bp-navs.vertical {
+		overflow: visible;
+	}
+	.bp-single-vert-nav .bp-navs.vertical ul {
+		border-right: 1px solid #d6d6d6;
+		border-bottom: 0;
+		float: left;
+		margin-right: -1px;
+		width: 25%;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li {
+		float: none;
+		margin-right: 0;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li.selected a {
+		background: #ccc;
+		color: #333;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li:focus, .bp-single-vert-nav .bp-navs.vertical li:hover {
+		background: #ccc;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li span {
+		background: #d6d6d6;
+		border-radius: 10%;
+		float: right;
+		margin-right: 2px;
+	}
+	.bp-single-vert-nav .bp-navs.vertical li:hover span {
+		border-color: #eaeaea;
+	}
+	.bp-single-vert-nav .bp-navs.vertical.tabbed-links li.selected a {
+		padding-left: 0;
+	}
+	.bp-single-vert-nav .bp-wrap {
+		margin-bottom: 15px;
+	}
+	.bp-single-vert-nav .bp-wrap .user-nav-tabs.users-nav ul li,
+	.bp-single-vert-nav .bp-wrap .group-nav-tabs.groups-nav ul li {
+		left: 1px;
+		position: relative;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) {
+		background: #fff;
+		border-left: 1px solid #d6d6d6;
+		float: right;
+		margin: 0;
+		min-height: 400px;
+		padding: 0 0 0 1em;
+		width: calc(75% + 1px);
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) {
+		background: #eaeaea;
+		margin: 0 0 0 -5px;
+		width: auto;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li {
+		font-size: 16px;
+		margin: 10px 0;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li a {
+		border-right: 1px solid #ccc;
+		padding: 0 0.5em;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li a:focus,
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li a:hover {
+		background: none;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li.current a {
+		background: none;
+		color: #333;
+		text-decoration: underline;
+	}
+	.bp-single-vert-nav .item-body:not(#group-create-body) #subnav:not(.tabbed-links) li:last-child a {
+		border: none;
+	}
+	.bp-dir-vert-nav .dir-navs {
+		float: left;
+		left: 1px;
+		position: relative;
+		width: 20%;
+	}
+	.bp-dir-vert-nav .dir-navs ul li {
+		float: none;
+		overflow: hidden;
+		width: auto;
+	}
+	.bp-dir-vert-nav .dir-navs ul li.selected {
+		border: 1px solid #eee;
+	}
+	.bp-dir-vert-nav .dir-navs ul li.selected a {
+		background: #555;
+		color: #fff;
+	}
+	.bp-dir-vert-nav .dir-navs ul li.selected a span {
+		background: #eaeaea;
+		border-color: #ccc;
+		color: #5087e5;
+	}
+	.bp-dir-vert-nav .dir-navs ul li a:hover,
+	.bp-dir-vert-nav .dir-navs ul li a:focus {
+		background: #ccc;
+		color: #333;
+	}
+	.bp-dir-vert-nav .dir-navs ul li a:hover span,
+	.bp-dir-vert-nav .dir-navs ul li a:focus span {
+		border: 1px solid #555;
+	}
+	.bp-dir-vert-nav .screen-content {
+		border-left: 1px solid #d6d6d6;
+		margin-left: 20%;
+		overflow: hidden;
+		padding: 0 0 2em 1em;
+	}
+	.bp-dir-vert-nav .screen-content .subnav-filters {
+		margin-top: 0;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li:not(.selected) a:focus,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li:not(.selected) a:hover,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li:not(.selected) a:focus {
+		background: none;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li.selected,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li.selected {
+		background: none;
+		border: 1px solid #d6d6d6;
+		border-right-color: #fff;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li.selected a,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li.selected a {
+		background: none;
+		color: #333;
+		font-weight: 600;
+	}
+	.buddypress-wrap.bp-vertical-navs .dir-navs.activity-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.sites-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.groups-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .dir-navs.members-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .main-navs.user-nav-tabs ul li.selected a span,
+	.buddypress-wrap.bp-vertical-navs .main-navs.group-nav-tabs ul li.selected a span {
+		background: #555;
+		border: 1px solid #d6d6d6;
+		color: #fff;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/embeds-activity-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/embeds-activity-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/embeds-activity-rtl.css	(revision 12265)
@@ -0,0 +1,158 @@
+/**
+ * Embeds Activity styles
+ *
+ * @version 3.0.0
+ */
+#bp-embed-header:after {
+	clear: both;
+	content: "";
+	display: table;
+	margin-bottom: 1em;
+}
+
+.bp-embed-avatar {
+	float: right;
+	margin: 0 0 0 0.75em;
+}
+
+p.bp-embed-activity-action {
+	font-size: 15px;
+	margin-bottom: 0;
+}
+
+p.bp-embed-activity-action a:first-child {
+	color: #32373c;
+	font-weight: 700;
+}
+
+p.bp-embed-activity-action img.avatar {
+	padding: 0 3px 0 4px;
+	vertical-align: text-bottom;
+}
+
+.bp-embed-excerpt {
+	margin-bottom: 1em;
+}
+
+.bp-embed-excerpt a {
+	color: #21759b;
+	display: inline-block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	vertical-align: top;
+	white-space: nowrap;
+	max-width: 250px;
+}
+
+.activity-read-more {
+	margin-right: 0.5em;
+}
+
+.activity-read-more a {
+	color: #b4b9be;
+}
+
+.wp-embed-footer {
+	margin-top: 20px;
+}
+
+span.bp-embed-timestamp {
+	font-size: 0.9em;
+}
+
+video {
+	width: 100%;
+	height: auto;
+}
+
+.bp-activity-embed-display-media {
+	border: 1px solid #ccc;
+	border-radius: 6px;
+}
+
+.bp-activity-embed-display-media.one-col,
+.bp-activity-embed-display-media.one-col .thumb,
+.bp-activity-embed-display-media.one-col .thumb img {
+	width: 100%;
+}
+
+.bp-activity-embed-display-media.two-col .thumb,
+.bp-activity-embed-display-media.two-col .caption {
+	display: table-cell;
+}
+
+.bp-activity-embed-display-media.two-col .thumb {
+	background: #000;
+	vertical-align: middle;
+}
+
+.bp-activity-embed-display-media.two-col .caption {
+	vertical-align: top;
+}
+
+.bp-activity-embed-display-media.two-col .thumb img {
+	border-left: 1px solid #ccc;
+	display: block;
+	width: 100%;
+}
+
+.bp-activity-embed-display-media .thumb {
+	position: relative;
+}
+
+.bp-activity-embed-display-media .caption {
+	padding: 0.2em 0.5em 0.5em;
+}
+
+a.play-btn {
+	background: rgba(0, 0, 0, 0.75);
+	border-radius: 50%;
+	height: 50px;
+	right: 50%;
+	margin: 0;
+	padding: 1em;
+	position: absolute;
+	text-indent: 0.25em;
+	top: 50%;
+	transform: translateY(-50%) translateX(50%);
+	-webkit-transform: translateY(-50%) translateX(50%);
+	transition: all 0.2s ease-out;
+	width: 50px;
+}
+
+.bp-activity-embed-display-media.two-col a.play-btn {
+	height: 35px;
+	width: 35px;
+}
+
+a.play-btn:hover {
+	background: rgba(0, 0, 0, 0.95);
+	transform: translateY(-50%) translateX(50%) scale(1.05);
+	-webkit-transform: translateY(-50%) translateX(50%) scale(1.05);
+	transition: all 0.2s ease-out;
+}
+
+.bp-activity-embed-display-media .thumb svg {
+	fill: #fff;
+	overflow: hidden;
+}
+
+.bp-activity-embed-display-media .caption-description {
+	font-size: 90%;
+	margin: 0.4em 0;
+}
+
+@media only screen and (max-width: 480px) {
+
+	.bp-activity-embed-display-media.two-col .thumb {
+		border-bottom: 1px solid #ccc;
+		border-left: 0;
+		display: block;
+		max-width: none !important;
+	}
+
+	a.play-btn {
+		height: 35px;
+		width: 35px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/embeds-activity.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/embeds-activity.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/css/embeds-activity.css	(revision 12265)
@@ -0,0 +1,158 @@
+/**
+ * Embeds Activity styles
+ *
+ * @version 3.0.0
+ */
+#bp-embed-header:after {
+	clear: both;
+	content: "";
+	display: table;
+	margin-bottom: 1em;
+}
+
+.bp-embed-avatar {
+	float: left;
+	margin: 0 0.75em 0 0;
+}
+
+p.bp-embed-activity-action {
+	font-size: 15px;
+	margin-bottom: 0;
+}
+
+p.bp-embed-activity-action a:first-child {
+	color: #32373c;
+	font-weight: 700;
+}
+
+p.bp-embed-activity-action img.avatar {
+	padding: 0 4px 0 3px;
+	vertical-align: text-bottom;
+}
+
+.bp-embed-excerpt {
+	margin-bottom: 1em;
+}
+
+.bp-embed-excerpt a {
+	color: #21759b;
+	display: inline-block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	vertical-align: top;
+	white-space: nowrap;
+	max-width: 250px;
+}
+
+.activity-read-more {
+	margin-left: 0.5em;
+}
+
+.activity-read-more a {
+	color: #b4b9be;
+}
+
+.wp-embed-footer {
+	margin-top: 20px;
+}
+
+span.bp-embed-timestamp {
+	font-size: 0.9em;
+}
+
+video {
+	width: 100%;
+	height: auto;
+}
+
+.bp-activity-embed-display-media {
+	border: 1px solid #ccc;
+	border-radius: 6px;
+}
+
+.bp-activity-embed-display-media.one-col,
+.bp-activity-embed-display-media.one-col .thumb,
+.bp-activity-embed-display-media.one-col .thumb img {
+	width: 100%;
+}
+
+.bp-activity-embed-display-media.two-col .thumb,
+.bp-activity-embed-display-media.two-col .caption {
+	display: table-cell;
+}
+
+.bp-activity-embed-display-media.two-col .thumb {
+	background: #000;
+	vertical-align: middle;
+}
+
+.bp-activity-embed-display-media.two-col .caption {
+	vertical-align: top;
+}
+
+.bp-activity-embed-display-media.two-col .thumb img {
+	border-right: 1px solid #ccc;
+	display: block;
+	width: 100%;
+}
+
+.bp-activity-embed-display-media .thumb {
+	position: relative;
+}
+
+.bp-activity-embed-display-media .caption {
+	padding: 0.2em 0.5em 0.5em;
+}
+
+a.play-btn {
+	background: rgba(0, 0, 0, 0.75);
+	border-radius: 50%;
+	height: 50px;
+	left: 50%;
+	margin: 0;
+	padding: 1em;
+	position: absolute;
+	text-indent: 0.25em;
+	top: 50%;
+	transform: translateY(-50%) translateX(-50%);
+	-webkit-transform: translateY(-50%) translateX(-50%);
+	transition: all 0.2s ease-out;
+	width: 50px;
+}
+
+.bp-activity-embed-display-media.two-col a.play-btn {
+	height: 35px;
+	width: 35px;
+}
+
+a.play-btn:hover {
+	background: rgba(0, 0, 0, 0.95);
+	transform: translateY(-50%) translateX(-50%) scale(1.05);
+	-webkit-transform: translateY(-50%) translateX(-50%) scale(1.05);
+	transition: all 0.2s ease-out;
+}
+
+.bp-activity-embed-display-media .thumb svg {
+	fill: #fff;
+	overflow: hidden;
+}
+
+.bp-activity-embed-display-media .caption-description {
+	font-size: 90%;
+	margin: 0.4em 0;
+}
+
+@media only screen and (max-width: 480px) {
+
+	.bp-activity-embed-display-media.two-col .thumb {
+		border-bottom: 1px solid #ccc;
+		border-right: 0;
+		display: block;
+		max-width: none !important;
+	}
+
+	a.play-btn {
+		height: 35px;
+		width: 35px;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/ajax.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/ajax.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/ajax.php	(revision 12265)
@@ -0,0 +1,636 @@
+<?php
+/**
+ * Activity Ajax functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_action( 'admin_init', function() {
+	$ajax_actions = array(
+		array(
+			'activity_filter' => array(
+				'function' => 'bp_nouveau_ajax_object_template_loader',
+				'nopriv'   => true,
+			),
+		),
+		array(
+			'get_single_activity_content' => array(
+				'function' => 'bp_nouveau_ajax_get_single_activity_content',
+				'nopriv'   => true,
+			),
+		),
+		array(
+			'activity_mark_fav' => array(
+				'function' => 'bp_nouveau_ajax_mark_activity_favorite',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'activity_mark_unfav' => array(
+				'function' => 'bp_nouveau_ajax_unmark_activity_favorite',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'activity_clear_new_mentions' => array(
+				'function' => 'bp_nouveau_ajax_clear_new_mentions',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'delete_activity' => array(
+				'function' => 'bp_nouveau_ajax_delete_activity',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'new_activity_comment' => array(
+				'function' => 'bp_nouveau_ajax_new_activity_comment',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'bp_nouveau_get_activity_objects' => array(
+				'function' => 'bp_nouveau_ajax_get_activity_objects',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'post_update' => array(
+				'function' => 'bp_nouveau_ajax_post_update',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'bp_spam_activity' => array(
+				'function' => 'bp_nouveau_ajax_spam_activity',
+				'nopriv'   => false,
+			),
+		),
+	);
+
+	foreach ( $ajax_actions as $ajax_action ) {
+		$action = key( $ajax_action );
+
+		add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+		if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+			add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+		}
+	}
+}, 12 );
+
+/**
+ * Mark an activity as a favourite via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_mark_activity_favorite() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_activity' ) ) {
+		wp_send_json_error();
+	}
+
+	if ( bp_activity_add_user_favorite( $_POST['id'] ) ) {
+		$response = array( 'content' => __( 'Remove Favorite', 'buddypress' ) );
+
+		if ( ! bp_is_user() ) {
+			$fav_count = (int) bp_get_total_favorite_count_for_user( bp_loggedin_user_id() );
+
+			if ( 1 === $fav_count ) {
+				$response['directory_tab'] = '<li id="activity-favorites" data-bp-scope="favorites" data-bp-object="activity">
+					<a href="' . bp_loggedin_user_domain() . bp_get_activity_slug() . '/favorites/">
+						' . esc_html__( 'My Favorites', 'buddypress' ) . '
+					</a>
+				</li>';
+			} else {
+				$response['fav_count'] = $fav_count;
+			}
+		}
+
+		wp_send_json_success( $response );
+	} else {
+		wp_send_json_error();
+	}
+}
+
+/**
+ * Un-favourite an activity via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_unmark_activity_favorite() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_activity' ) ) {
+		wp_send_json_error();
+	}
+
+	if ( bp_activity_remove_user_favorite( $_POST['id'] ) ) {
+		$response = array( 'content' => __( 'Favorite', 'buddypress' ) );
+
+		$fav_count = (int) bp_get_total_favorite_count_for_user( bp_loggedin_user_id() );
+
+		if ( 0 === $fav_count && ! bp_is_single_activity() ) {
+			$response['no_favorite'] = '<li><div class="bp-feedback bp-messages info">
+				' . __( 'Sorry, there was no activity found. Please try a different filter.', 'buddypress' ) . '
+			</div></li>';
+		} else {
+			$response['fav_count'] = $fav_count;
+		}
+
+		wp_send_json_success( $response );
+	} else {
+		wp_send_json_error();
+	}
+}
+
+/**
+ * Clear mentions if the directory tab is clicked
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_clear_new_mentions() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_activity' ) ) {
+		wp_send_json_error();
+	}
+
+	bp_activity_clear_new_mentions( bp_loggedin_user_id() );
+	wp_send_json_success();
+}
+
+/**
+ * Deletes an Activity item/Activity comment item received via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply.
+ */
+function bp_nouveau_ajax_delete_activity() {
+	$response = array(
+		'feedback' => sprintf(
+			'<div class="bp-feedback bp-messages error">%s</div>',
+			esc_html__( 'There was a problem when deleting. Please try again.', 'buddypress' )
+		),
+	);
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error( $response );
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'bp_activity_delete_link' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( ! is_user_logged_in() ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['id'] ) || ! is_numeric( $_POST['id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$activity = new BP_Activity_Activity( (int) $_POST['id'] );
+
+	// Check access.
+	if ( ! bp_activity_user_can_delete( $activity ) ) {
+		wp_send_json_error( $response );
+	}
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_before_action_delete_activity', $activity->id, $activity->user_id );
+
+	// Deleting an activity comment.
+	if ( ! empty( $_POST['is_comment'] ) ) {
+		if ( ! bp_activity_delete_comment( $activity->item_id, $activity->id ) ) {
+			wp_send_json_error( $response );
+		}
+
+	// Deleting an activity.
+	} else {
+		if ( ! bp_activity_delete( array( 'id' => $activity->id, 'user_id' => $activity->user_id ) ) ) {
+			wp_send_json_error( $response );
+		}
+	}
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_action_delete_activity', $activity->id, $activity->user_id );
+
+	// The activity has been deleted successfully
+	$response = array( 'deleted' => $activity->id );
+
+	// If on a single activity redirect to user's home.
+	if ( ! empty( $_POST['is_single'] ) ) {
+		$response['redirect'] = bp_core_get_user_domain( $activity->user_id );
+		bp_core_add_message( __( 'Activity deleted successfully', 'buddypress' ) );
+	}
+
+	wp_send_json_success( $response );
+}
+
+/**
+ * Fetches an activity's full, non-excerpted content via a POST request.
+ * Used for the 'Read More' link on long activity items.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_get_single_activity_content() {
+	$response = array(
+		'feedback' => sprintf(
+			'<div class="bp-feedback bp-messages error">%s</div>',
+			esc_html__( 'There was a problem displaying the content. Please try again.', 'buddypress' )
+		),
+	);
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error( $response );
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_activity' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$activity_array = bp_activity_get_specific(
+		array(
+			'activity_ids'     => $_POST['id'],
+			'display_comments' => 'stream',
+		)
+	);
+
+	if ( empty( $activity_array['activities'][0] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$activity = $activity_array['activities'][0];
+
+	/**
+	 * Fires before the return of an activity's full, non-excerpted content via a POST request.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $activity Activity content. Passed by reference.
+	 */
+	do_action_ref_array( 'bp_nouveau_get_single_activity_content', array( &$activity ) );
+
+	// Activity content retrieved through AJAX should run through normal filters, but not be truncated.
+	remove_filter( 'bp_get_activity_content_body', 'bp_activity_truncate_entry', 5 );
+
+	/** This filter is documented in bp-activity/bp-activity-template.php */
+	$content = apply_filters( 'bp_get_activity_content_body', $activity->content );
+
+	wp_send_json_success( array( 'contents' => $content ) );
+}
+
+/**
+ * Posts new Activity comments received via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @global BP_Activity_Template $activities_template
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_new_activity_comment() {
+	global $activities_template;
+	$bp = buddypress();
+
+	$response = array(
+		'feedback' => sprintf(
+			'<div class="bp-feedback bp-messages error">%s</div>',
+			esc_html__( 'There was an error posting your reply. Please try again.', 'buddypress' )
+		),
+	);
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error( $response );
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['_wpnonce_new_activity_comment'] ) || ! wp_verify_nonce( $_POST['_wpnonce_new_activity_comment'], 'new_activity_comment' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( ! is_user_logged_in() ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['content'] ) ) {
+		wp_send_json_error( array( 'feedback' => sprintf(
+			'<div class="bp-feedback bp-messages error">%s</div>',
+			esc_html__( 'Please do not leave the comment area blank.', 'buddypress' )
+		) ) );
+	}
+
+	if ( empty( $_POST['form_id'] ) || empty( $_POST['comment_id'] ) || ! is_numeric( $_POST['form_id'] ) || ! is_numeric( $_POST['comment_id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$comment_id = bp_activity_new_comment( array(
+		'activity_id' => $_POST['form_id'],
+		'content'     => $_POST['content'],
+		'parent_id'   => $_POST['comment_id'],
+	) );
+
+	if ( ! $comment_id ) {
+		if ( ! empty( $bp->activity->errors['new_comment'] ) && is_wp_error( $bp->activity->errors['new_comment'] ) ) {
+			$response = array( 'feedback' => sprintf(
+				'<div class="bp-feedback bp-messages error">%s</div>',
+				esc_html( $bp->activity->errors['new_comment']->get_error_message() )
+			) );
+			unset( $bp->activity->errors['new_comment'] );
+		}
+
+		wp_send_json_error( $response );
+	}
+
+	// Load the new activity item into the $activities_template global.
+	bp_has_activities(
+		array(
+			'display_comments' => 'stream',
+			'hide_spam'        => false,
+			'show_hidden'      => true,
+			'include'          => $comment_id,
+		)
+	);
+
+	// Swap the current comment with the activity item we just loaded.
+	if ( isset( $activities_template->activities[0] ) ) {
+		$activities_template->activity                  = new stdClass();
+		$activities_template->activity->id              = $activities_template->activities[0]->item_id;
+		$activities_template->activity->current_comment = $activities_template->activities[0];
+
+		// Because the whole tree has not been loaded, we manually
+		// determine depth.
+		$depth     = 1;
+		$parent_id = (int) $activities_template->activities[0]->secondary_item_id;
+		while ( $parent_id !== (int) $activities_template->activities[0]->item_id ) {
+			$depth++;
+			$p_obj     = new BP_Activity_Activity( $parent_id );
+			$parent_id = (int) $p_obj->secondary_item_id;
+		}
+		$activities_template->activity->current_comment->depth = $depth;
+	}
+
+	ob_start();
+	// Get activity comment template part.
+	bp_get_template_part( 'activity/comment' );
+	$response = array( 'contents' => ob_get_contents() );
+	ob_end_clean();
+
+	unset( $activities_template );
+
+	wp_send_json_success( $response );
+}
+
+/**
+ * Get items to attach the activity to.
+ *
+ * This is used within the activity post form autocomplete field.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_get_activity_objects() {
+	$response = array();
+
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_activity' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( 'group' === $_POST['type'] ) {
+		$groups = groups_get_groups(
+			array(
+				'user_id'      => bp_loggedin_user_id(),
+				'search_terms' => $_POST['search'],
+				'show_hidden'  => true,
+				'per_page'     => 2,
+			)
+		);
+
+		wp_send_json_success( array_map( 'bp_nouveau_prepare_group_for_js', $groups['groups'] ) );
+	} else {
+
+		/**
+		 * Filters the response for custom activity objects.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $response Array of custom response objects to send to AJAX return.
+		 * @param array $value    Activity object type from $_POST global.
+		 */
+		$response = apply_filters( 'bp_nouveau_get_activity_custom_objects', $response, $_POST['type'] );
+	}
+
+	if ( empty( $response ) ) {
+		wp_send_json_error( array( 'error' => __( 'No activities were found.', 'buddypress' ) ) );
+	} else {
+		wp_send_json_success( $response );
+	}
+}
+
+/**
+ * Processes Activity updates received via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply
+ */
+function bp_nouveau_ajax_post_update() {
+	$bp = buddypress();
+
+	if ( ! is_user_logged_in() || empty( $_POST['_wpnonce_post_update'] ) || ! wp_verify_nonce( $_POST['_wpnonce_post_update'], 'post_update' ) ) {
+		wp_send_json_error();
+	}
+
+	if ( empty( $_POST['content'] ) ) {
+		wp_send_json_error(
+			array(
+				'message' => __( 'Please enter some content to post.', 'buddypress' ),
+			)
+		);
+	}
+
+	$activity_id = 0;
+	$item_id     = 0;
+	$object      = '';
+	$is_private  = false;
+
+	// Try to get the item id from posted variables.
+	if ( ! empty( $_POST['item_id'] ) ) {
+		$item_id = (int) $_POST['item_id'];
+	}
+
+	// Try to get the object from posted variables.
+	if ( ! empty( $_POST['object'] ) ) {
+		$object = sanitize_key( $_POST['object'] );
+
+	// If the object is not set and we're in a group, set the item id and the object
+	} elseif ( bp_is_group() ) {
+		$item_id = bp_get_current_group_id();
+		$object  = 'group';
+		$status  = groups_get_current_group()->status;
+	}
+
+	if ( 'user' === $object && bp_is_active( 'activity' ) ) {
+		$activity_id = bp_activity_post_update( array( 'content' => $_POST['content'] ) );
+
+	} elseif ( 'group' === $object ) {
+		if ( $item_id && bp_is_active( 'groups' ) ) {
+			// This function is setting the current group!
+			$activity_id = groups_post_update(
+				array(
+					'content'  => $_POST['content'],
+					'group_id' => $item_id,
+				)
+			);
+
+			if ( empty( $status ) ) {
+				if ( ! empty( $bp->groups->current_group->status ) ) {
+					$status = $bp->groups->current_group->status;
+				} else {
+					$group  = groups_get_group( array( 'group_id' => $group_id ) );
+					$status = $group->status;
+				}
+
+				$is_private = 'public' !== $status;
+			}
+		}
+
+	} else {
+		/** This filter is documented in bp-activity/bp-activity-actions.php */
+		$activity_id = apply_filters( 'bp_activity_custom_update', false, $object, $item_id, $_POST['content'] );
+	}
+
+	if ( empty( $activity_id ) ) {
+		wp_send_json_error(
+			array(
+				'message' => __( 'There was a problem posting your update. Please try again.', 'buddypress' ),
+			)
+		);
+	}
+
+	ob_start();
+	if ( bp_has_activities( array( 'include' => $activity_id, 'show_hidden' => $is_private ) ) ) {
+		while ( bp_activities() ) {
+			bp_the_activity();
+			bp_get_template_part( 'activity/entry' );
+		}
+	}
+	$acivity = ob_get_contents();
+	ob_end_clean();
+
+	wp_send_json_success( array(
+		'id'           => $activity_id,
+		'message'      => esc_html__( 'Update posted.', 'buddypress' ) . ' ' . sprintf( '<a href="%s" class="just-posted">%s</a>', esc_url( bp_activity_get_permalink( $activity_id ) ), esc_html__( 'View activity.', 'buddypress' ) ),
+		'activity'     => $acivity,
+
+		/**
+		 * Filters whether or not an AJAX post update is private.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string/bool $is_private Privacy status for the update.
+		 */
+		'is_private'   => apply_filters( 'bp_nouveau_ajax_post_update_is_private', $is_private ),
+		'is_directory' => bp_is_activity_directory(),
+	) );
+}
+
+/**
+ * AJAX spam an activity item or comment.
+ *
+ * @since 3.0.0
+ *
+ * @return string JSON reply.
+ */
+function bp_nouveau_ajax_spam_activity() {
+	$bp = buddypress();
+
+	$response = array(
+		'feedback' => sprintf(
+			'<div class="bp-feedback bp-messages error">%s</div>',
+			esc_html__( 'There was a problem marking this activity as spam. Please try again.', 'buddypress' )
+		),
+	);
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( ! is_user_logged_in() || ! bp_is_active( 'activity' ) || empty( $bp->activity->akismet ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['id'] ) || ! is_numeric( $_POST['id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Is the current user allowed to spam items?
+	if ( ! bp_activity_user_can_mark_spam() ) {
+		wp_send_json_error( $response );
+	}
+
+	$activity = new BP_Activity_Activity( (int) $_POST['id'] );
+
+	if ( empty( $activity->component ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'bp_activity_akismet_spam_' . $activity->id ) ) {
+		wp_send_json_error( $response );
+	}
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_before_action_spam_activity', $activity->id, $activity );
+
+	// Mark as spam.
+	bp_activity_mark_as_spam( $activity );
+	$activity->save();
+
+	/** This action is documented in bp-activity/bp-activity-actions.php */
+	do_action( 'bp_activity_action_spam_activity', $activity->id, $activity->user_id );
+
+	// Prepare the successfull reply
+	$response = array( 'spammed' => $activity->id );
+
+	// If on a single activity redirect to user's home.
+	if ( ! empty( $_POST['is_single'] ) ) {
+		$response['redirect'] = bp_core_get_user_domain( $activity->user_id );
+		bp_core_add_message( __( 'This activity has been marked as spam and is no longer visible.', 'buddypress' ) );
+	}
+
+	// Send the json reply
+	wp_send_json_success( $response );
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/functions.php	(revision 12265)
@@ -0,0 +1,534 @@
+<?php
+/**
+ * Activity functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register Scripts for the Activity component
+ *
+ * @since 3.0.0
+ *
+ * @param array $scripts  The array of scripts to register.
+ *
+ * @return array The same array with the specific activity scripts.
+ */
+function bp_nouveau_activity_register_scripts( $scripts = array() ) {
+	if ( ! isset( $scripts['bp-nouveau'] ) ) {
+		return $scripts;
+	}
+
+	return array_merge( $scripts, array(
+		'bp-nouveau-activity' => array(
+			'file'         => 'js/buddypress-activity%s.js',
+			'dependencies' => array( 'bp-nouveau' ),
+			'footer'       => true,
+		),
+		'bp-nouveau-activity-post-form' => array(
+			'file'         => 'js/buddypress-activity-post-form%s.js',
+			'dependencies' => array( 'bp-nouveau', 'bp-nouveau-activity', 'json2', 'wp-backbone' ),
+			'footer'       => true,
+		),
+	) );
+}
+
+/**
+ * Enqueue the activity scripts
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_enqueue_scripts() {
+	if ( ! bp_is_activity_component() && ! bp_is_group_activity() ) {
+		return;
+	}
+
+	wp_enqueue_script( 'bp-nouveau-activity' );
+}
+
+/**
+ * Localize the strings needed for the Activity Post form UI
+ *
+ * @since 3.0.0
+ *
+ * @param array $params Associative array containing the JS Strings needed by scripts.
+ *
+ * @return array The same array with specific strings for the Activity Post form UI if needed.
+ */
+function bp_nouveau_activity_localize_scripts( $params = array() ) {
+	if ( ! bp_is_activity_component() && ! bp_is_group_activity() ) {
+		return $params;
+	}
+
+	$activity_params = array(
+		'user_id'     => bp_loggedin_user_id(),
+		'object'      => 'user',
+		'backcompat'  => (bool) has_action( 'bp_activity_post_form_options' ),
+		'post_nonce'  => wp_create_nonce( 'post_update', '_wpnonce_post_update' ),
+	);
+
+	$user_displayname = bp_get_loggedin_user_fullname();
+
+	if ( buddypress()->avatar->show_avatars ) {
+		$width  = bp_core_avatar_thumb_width();
+		$height = bp_core_avatar_thumb_height();
+		$activity_params = array_merge( $activity_params, array(
+			'avatar_url'    => bp_get_loggedin_user_avatar( array(
+				'width'  => $width,
+				'height' => $height,
+				'html'   => false,
+			) ),
+			'avatar_width'  => $width,
+			'avatar_height' => $height,
+			'user_domain'   => bp_loggedin_user_domain(),
+			'avatar_alt'    => sprintf(
+				/* translators: %s = member name */
+				__( 'Profile photo of %s', 'buddypress' ),
+				$user_displayname
+			),
+		) );
+	}
+
+	/**
+	 * Filters the included, specific, Action buttons.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value The array containing the button params. Must look like:
+	 * array( 'buttonid' => array(
+	 *  'id'      => 'buttonid',                            // Id for your action
+	 *  'caption' => __( 'Button caption', 'text-domain' ),
+	 *  'icon'    => 'dashicons-*',                         // The dashicon to use
+	 *  'order'   => 0,
+	 *  'handle'  => 'button-script-handle',                // The handle of the registered script to enqueue
+	 * );
+	 */
+	$activity_buttons = apply_filters( 'bp_nouveau_activity_buttons', array() );
+
+	if ( ! empty( $activity_buttons ) ) {
+		$activity_params['buttons'] = bp_sort_by_key( $activity_buttons, 'order', 'num' );
+
+		// Enqueue Buttons scripts and styles
+		foreach ( $activity_params['buttons'] as $key_button => $buttons ) {
+			if ( empty( $buttons['handle'] ) ) {
+				continue;
+			}
+
+			if ( wp_style_is( $buttons['handle'], 'registered' ) ) {
+				wp_enqueue_style( $buttons['handle'] );
+			}
+
+			if ( wp_script_is( $buttons['handle'], 'registered' ) ) {
+				wp_enqueue_script( $buttons['handle'] );
+			}
+
+			unset( $activity_params['buttons'][ $key_button ]['handle'] );
+		}
+	}
+
+	// Activity Objects
+	if ( ! bp_is_single_item() && ! bp_is_user() ) {
+		$activity_objects = array(
+			'profile' => array(
+				'text'                     => __( 'Post in: Profile', 'buddypress' ),
+				'autocomplete_placeholder' => '',
+				'priority'                 => 5,
+			),
+		);
+
+		// the groups component is active & the current user is at least a member of 1 group
+		if ( bp_is_active( 'groups' ) && bp_has_groups( array( 'user_id' => bp_loggedin_user_id(), 'max' => 1 ) ) ) {
+			$activity_objects['group'] = array(
+				'text'                     => __( 'Post in: Group', 'buddypress' ),
+				'autocomplete_placeholder' => __( 'Start typing the group name...', 'buddypress' ),
+				'priority'                 => 10,
+			);
+		}
+
+		/**
+		 * Filters the activity objects to apply for localized javascript data.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $activity_objects Array of activity objects.
+		 */
+		$activity_params['objects'] = apply_filters( 'bp_nouveau_activity_objects', $activity_objects );
+	}
+
+	$activity_strings = array(
+		'whatsnewPlaceholder' => sprintf( __( "What's new, %s?", 'buddypress' ), bp_get_user_firstname( $user_displayname ) ),
+		'whatsnewLabel'       => __( 'Post what\'s new', 'buddypress' ),
+		'whatsnewpostinLabel' => __( 'Post in', 'buddypress' ),
+		'postUpdateButton'    => __( 'Post Update', 'buddypress' ),
+		'cancelButton'        => __( 'Cancel', 'buddypress' ),
+	);
+
+	if ( bp_is_group() ) {
+		$activity_params = array_merge(
+			$activity_params,
+			array(
+				'object'  => 'group',
+				'item_id' => bp_get_current_group_id(),
+			)
+		);
+	}
+
+	$params['activity'] = array(
+		'params'  => $activity_params,
+		'strings' => $activity_strings,
+	);
+
+	return $params;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_get_activity_directory_nav_items() {
+	$nav_items = array();
+
+	$nav_items['all'] = array(
+		'component' => 'activity',
+		'slug'      => 'all', // slug is used because BP_Core_Nav requires it, but it's the scope
+		'li_class'  => array( 'dynamic' ),
+		'link'      => bp_get_activity_directory_permalink(),
+		'text'      => __( 'All Members', 'buddypress' ),
+		'count'     => '',
+		'position'  => 5,
+	);
+
+	// deprecated hooks
+	$deprecated_hooks = array(
+		array( 'bp_before_activity_type_tab_all', 'activity', 0 ),
+		array( 'bp_activity_type_tabs', 'activity', 46 ),
+	);
+
+	if ( is_user_logged_in() ) {
+		$deprecated_hooks = array_merge(
+			$deprecated_hooks,
+			array(
+				array( 'bp_before_activity_type_tab_friends', 'activity', 6 ),
+				array( 'bp_before_activity_type_tab_groups', 'activity', 16 ),
+				array( 'bp_before_activity_type_tab_favorites', 'activity', 26 ),
+			)
+		);
+
+		// If the user has favorite create a nav item
+		if ( bp_get_total_favorite_count_for_user( bp_loggedin_user_id() ) ) {
+			$nav_items['favorites'] = array(
+				'component' => 'activity',
+				'slug'      => 'favorites', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array(),
+				'link'      => bp_loggedin_user_domain() . bp_get_activity_slug() . '/favorites/',
+				'text'      => __( 'My Favorites', 'buddypress' ),
+				'count'     => false,
+				'position'  => 35,
+			);
+		}
+
+		// The friends component is active and user has friends
+		if ( bp_is_active( 'friends' ) && bp_get_total_friend_count( bp_loggedin_user_id() ) ) {
+			$nav_items['friends'] = array(
+				'component' => 'activity',
+				'slug'      => 'friends', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array( 'dynamic' ),
+				'link'      => bp_loggedin_user_domain() . bp_get_activity_slug() . '/' . bp_get_friends_slug() . '/',
+				'text'      => __( 'My Friends', 'buddypress' ),
+				'count'     => '',
+				'position'  => 15,
+			);
+		}
+
+		// The groups component is active and user has groups
+		if ( bp_is_active( 'groups' ) && bp_get_total_group_count_for_user( bp_loggedin_user_id() ) ) {
+			$nav_items['groups'] = array(
+				'component' => 'activity',
+				'slug'      => 'groups', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array( 'dynamic' ),
+				'link'      => bp_loggedin_user_domain() . bp_get_activity_slug() . '/' . bp_get_groups_slug() . '/',
+				'text'      => __( 'My Groups', 'buddypress' ),
+				'count'     => '',
+				'position'  => 25,
+			);
+		}
+
+		// Mentions are allowed
+		if ( bp_activity_do_mentions() ) {
+			$deprecated_hooks[] = array( 'bp_before_activity_type_tab_mentions', 'activity', 36 );
+
+			$count = '';
+			if ( bp_get_total_mention_count_for_user( bp_loggedin_user_id() ) ) {
+				$count = bp_get_total_mention_count_for_user( bp_loggedin_user_id() );
+			}
+
+			$nav_items['mentions'] = array(
+				'component' => 'activity',
+				'slug'      => 'mentions', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array( 'dynamic' ),
+				'link'      => bp_loggedin_user_domain() . bp_get_activity_slug() . '/mentions/',
+				'text'      => __( 'Mentions', 'buddypress' ),
+				'count'     => $count,
+				'position'  => 45,
+			);
+		}
+	}
+
+	// Check for deprecated hooks :
+	foreach ( $deprecated_hooks as $deprectated_hook ) {
+		list( $hook, $component, $position ) = $deprectated_hook;
+
+		$extra_nav_items = bp_nouveau_parse_hooked_dir_nav( $hook, $component, $position );
+
+		if ( ! empty( $extra_nav_items ) ) {
+			$nav_items = array_merge( $nav_items, $extra_nav_items );
+		}
+	}
+
+	/**
+	 * Filters the activity directory navigation items.
+	 *
+	 * Use this filter to introduce your custom nav items for the activity directory.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $nav_items The list of the activity directory nav items.
+	 */
+	return apply_filters( 'bp_nouveau_get_activity_directory_nav_items', $nav_items );
+}
+
+/**
+ * Make sure bp_get_activity_show_filters() will return the filters and the context
+ * instead of the output.
+ *
+ * @since 3.0.0
+ *
+ * @param string $output  HTML output
+ * @param array  $filters Optional.
+ * @param string $context
+ *
+ * @return array
+ */
+function bp_nouveau_get_activity_filters_array( $output = '', $filters = array(), $context = '' ) {
+	return array(
+		'filters' => $filters,
+		'context' => $context,
+	);
+}
+
+/**
+ * Get Dropdown filters of the activity component
+ *
+ * @since 3.0.0
+ *
+ * @return array the filters
+ */
+function bp_nouveau_get_activity_filters() {
+	add_filter( 'bp_get_activity_show_filters', 'bp_nouveau_get_activity_filters_array', 10, 3 );
+
+	$filters_data = bp_get_activity_show_filters();
+
+	remove_filter( 'bp_get_activity_show_filters', 'bp_nouveau_get_activity_filters_array', 10, 3 );
+
+	$action = '';
+	if ( 'group' === $filters_data['context'] ) {
+		$action = 'bp_group_activity_filter_options';
+	} elseif ( 'member' === $filters_data['context'] || 'member_groups' === $filters_data['context'] ) {
+		$action = 'bp_member_activity_filter_options';
+	} else {
+		$action = 'bp_activity_filter_options';
+	}
+
+	$filters = $filters_data['filters'];
+
+	if ( $action ) {
+		return bp_nouveau_parse_hooked_options( $action, $filters );
+	}
+
+	return $filters;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_secondary_avatars( $action, $activity ) {
+	switch ( $activity->component ) {
+		case 'groups':
+		case 'friends':
+			// Only insert avatar if one exists.
+			if ( $secondary_avatar = bp_get_activity_secondary_avatar() ) {
+				$reverse_content = strrev( $action );
+				$position        = strpos( $reverse_content, 'a<' );
+				$action          = substr_replace( $action, $secondary_avatar, -$position - 2, 0 );
+			}
+			break;
+	}
+
+	return $action;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_scope_newest_class( $classes = '' ) {
+	if ( ! is_user_logged_in() ) {
+		return $classes;
+	}
+
+	$user_id    = bp_loggedin_user_id();
+	$my_classes = array();
+
+	/*
+	 * HeartBeat requests will transport the scope.
+	 * See bp_nouveau_ajax_querystring().
+	 */
+	$scope = '';
+
+	if ( ! empty( $_POST['data']['bp_heartbeat']['scope'] ) ) {
+		$scope = sanitize_key( $_POST['data']['bp_heartbeat']['scope'] );
+	}
+
+	// Add specific classes to perform specific actions on the client side.
+	if ( $scope && bp_is_activity_directory() ) {
+		$component = bp_get_activity_object_name();
+
+		/*
+		 * These classes will be used to count the number of newest activities for
+		 * the 'Mentions', 'My Groups' & 'My Friends' tabs
+		 */
+		if ( 'all' === $scope ) {
+			if ( 'groups' === $component && bp_is_active( $component ) ) {
+				// Is the current user a member of the group the activity is attached to?
+				if ( groups_is_user_member( $user_id, bp_get_activity_item_id() ) ) {
+					$my_classes[] = 'bp-my-groups';
+				}
+			}
+
+			// Friends can post in groups the user is a member of
+			if ( bp_is_active( 'friends' ) && (int) $user_id !== (int) bp_get_activity_user_id() ) {
+				if ( friends_check_friendship( $user_id, bp_get_activity_user_id() ) ) {
+					$my_classes[] = 'bp-my-friends';
+				}
+			}
+
+			// A mention can be posted by a friend within a group
+			if ( true === bp_activity_do_mentions() ) {
+				$new_mentions = bp_get_user_meta( $user_id, 'bp_new_mentions', true );
+
+				// The current activity is one of the new mentions
+				if ( is_array( $new_mentions ) && in_array( bp_get_activity_id(), $new_mentions, true ) ) {
+					$my_classes[] = 'bp-my-mentions';
+				}
+			}
+
+		/*
+		 * This class will be used to highlight the newest activities when
+		 * viewing the 'Mentions', 'My Groups' or the 'My Friends' tabs
+		 */
+		} elseif ( 'friends' === $scope || 'groups' === $scope || 'mentions' === $scope ) {
+			$my_classes[] = 'newest_' . $scope . '_activity';
+		}
+
+		// Leave other components do their specific stuff if needed.
+		/**
+		 * Filters the classes to be applied to the newest activity item.
+		 *
+		 * Leave other components do their specific stuff if needed.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $my_classes Array of classes to output to class attribute.
+		 * @param string $scope      Current scope for the activity type.
+		 */
+		$my_classes = (array) apply_filters( 'bp_nouveau_activity_scope_newest_class', $my_classes, $scope );
+
+		if ( ! empty( $my_classes ) ) {
+			$classes .= ' ' . join( ' ', $my_classes );
+		}
+	}
+
+	return $classes;
+}
+
+/**
+ * Get the activity query args for the widget.
+ *
+ * @since 3.0.0
+ *
+ * @return array The activity arguments.
+ */
+function bp_nouveau_activity_widget_query() {
+	$args       = array();
+	$bp_nouveau = bp_nouveau();
+
+	if ( isset( $bp_nouveau->activity->widget_args ) ) {
+		$args = $bp_nouveau->activity->widget_args;
+	}
+
+	/**
+	 * Filter to edit the activity widget arguments.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args The activity arguments.
+	 */
+	return apply_filters( 'bp_nouveau_activity_widget_query', $args );
+}
+
+/**
+ * Register notifications filters for the activity component.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_notification_filters() {
+	$notifications = array(
+		array(
+			'id'       => 'new_at_mention',
+			'label'    => __( 'New mentions', 'buddypress' ),
+			'position' => 5,
+		),
+		array(
+			'id'       => 'update_reply',
+			'label'    => __( 'New update replies', 'buddypress' ),
+			'position' => 15,
+		),
+		array(
+			'id'       => 'comment_reply',
+			'label'    => __( 'New update comment replies', 'buddypress' ),
+			'position' => 25,
+		),
+	);
+
+	foreach ( $notifications as $notification ) {
+		bp_nouveau_notifications_register_filter( $notification );
+	}
+}
+
+/**
+ * Add controls for the settings of the customizer for the activity component.
+ *
+ * @since 3.0.0
+ *
+ * @param array $controls Optional. The controls to add.
+ *
+ * @return array the controls to add.
+ */
+function bp_nouveau_activity_customizer_controls( $controls = array() ) {
+	return array_merge( $controls, array(
+		'act_dir_layout' => array(
+			'label'      => __( 'Use column navigation for the Activity directory.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[activity_dir_layout]',
+			'type'       => 'checkbox',
+		),
+		'act_dir_tabs' => array(
+			'label'      => __( 'Use tab styling for Activity directory navigation.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[activity_dir_tabs]',
+			'type'       => 'checkbox',
+		),
+	) );
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/loader.php	(revision 12265)
@@ -0,0 +1,117 @@
+<?php
+/**
+ * BP Nouveau Activity
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Activity Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Activity {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir = trailingslashit( dirname( __FILE__ ) );
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require $this->dir . 'functions.php';
+		require $this->dir . 'template-tags.php';
+		require $this->dir . 'widgets.php';
+
+		// Test suite requires the AJAX functions early.
+		if ( function_exists( 'tests_add_filter' ) ) {
+			require $this->dir . 'ajax.php';
+
+		// Load AJAX code only on AJAX requests.
+		} else {
+			add_action( 'admin_init', function() {
+				// AJAX condtion.
+				if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX &&
+					// Check to see if action is activity-specific.
+					( false !== strpos( $_REQUEST['action'], 'activity' ) || ( 'post_update' === $_REQUEST['action'] ) )
+				) {
+					require bp_nouveau()->activity->dir . 'ajax.php';
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		add_action( 'bp_nouveau_enqueue_scripts', 'bp_nouveau_activity_enqueue_scripts' );
+		add_action( 'bp_widgets_init', array( 'BP_Latest_Activities', 'register_widget' ) );
+		add_action( 'bp_nouveau_notifications_init_filters', 'bp_nouveau_activity_notification_filters' );
+
+		$bp = buddypress();
+
+		if ( bp_is_akismet_active() && isset( $bp->activity->akismet ) ) {
+			remove_action( 'bp_activity_entry_meta', array( $bp->activity->akismet, 'add_activity_spam_button' ) );
+			remove_action( 'bp_activity_comment_options', array( $bp->activity->akismet, 'add_activity_comment_spam_button' ) );
+		}
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		// Register customizer controls.
+		add_filter( 'bp_nouveau_customizer_controls', 'bp_nouveau_activity_customizer_controls', 10, 1 );
+
+		// Register activity scripts
+		add_filter( 'bp_nouveau_register_scripts', 'bp_nouveau_activity_register_scripts', 10, 1 );
+
+		// Localize Scripts
+		add_filter( 'bp_core_get_js_strings', 'bp_nouveau_activity_localize_scripts', 10, 1 );
+
+		add_filter( 'bp_get_activity_action_pre_meta', 'bp_nouveau_activity_secondary_avatars', 10, 2 );
+		add_filter( 'bp_get_activity_css_class', 'bp_nouveau_activity_scope_newest_class', 10, 1 );
+	}
+}
+
+/**
+ * Launch the Activity loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->activity = new BP_Nouveau_Activity();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_activity', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/template-tags.php	(revision 12265)
@@ -0,0 +1,920 @@
+<?php
+/**
+ * Activity Template tags
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Before Activity's directory content legacy do_action hooks wrapper
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_before_activity_directory_content() {
+	/**
+	 * Fires at the begining of the templates BP injected content.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_before_directory_activity' );
+
+	/**
+	 * Fires before the activity directory display content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_directory_activity_content' );
+}
+
+/**
+ * After Activity's directory content legacy do_action hooks wrapper
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_after_activity_directory_content() {
+	/**
+	 * Fires after the display of the activity list.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_activity_list' );
+
+	/**
+	 * Fires inside and displays the activity directory display content.
+	 */
+	do_action( 'bp_directory_activity_content' );
+
+	/**
+	 * Fires after the activity directory display content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_directory_activity_content' );
+
+	/**
+	 * Fires after the activity directory listing.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_activity' );
+}
+
+/**
+ * Enqueue needed scripts for the Activity Post Form
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_before_activity_post_form() {
+	if ( bp_nouveau_current_user_can( 'publish_activity' ) ) {
+		wp_enqueue_script( 'bp-nouveau-activity-post-form' );
+	}
+
+	/**
+	 * Fires before the activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_activity_post_form' );
+}
+
+/**
+ * Load JS Templates for the Activity Post Form
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_after_activity_post_form() {
+	if ( bp_nouveau_current_user_can( 'publish_activity' ) ) {
+		bp_get_template_part( 'common/js-templates/activity/form' );
+	}
+
+	/**
+	 * Fires after the activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_activity_post_form' );
+}
+
+/**
+ * Display the displayed user activity post form if needed
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML.
+ */
+function bp_nouveau_activity_member_post_form() {
+
+	/**
+	 * Fires before the display of the member activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_activity_post_form' );
+
+	if ( is_user_logged_in() && bp_is_my_profile() && ( ! bp_current_action() || bp_is_current_action( 'just-me' ) ) ) {
+		bp_get_template_part( 'activity/post-form' );
+	}
+
+	/**
+	 * Fires after the display of the member activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_activity_post_form' );
+}
+
+/**
+ * Fire specific hooks into the activity entry template
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   Optional. Either 'before' or 'after'.
+ * @param string $suffix Optional. Use it to add terms at the end of the hook name.
+ */
+function bp_nouveau_activity_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a activity entry hook
+	$hook[] = 'activity';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Checks if an activity of the loop has some content.
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if the activity has some content. False Otherwise.
+ */
+function bp_nouveau_activity_has_content() {
+	return bp_activity_has_content() || (bool) has_action( 'bp_activity_entry_content' );
+}
+
+/**
+ * Output the Activity content into the loop.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_content() {
+	if ( bp_activity_has_content() ) {
+		bp_activity_content_body();
+	}
+
+	/**
+	 * Fires after the display of an activity entry content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_activity_entry_content' );
+}
+
+/**
+ * Output the Activity timestamp into the bp-timestamp attribute.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_timestamp() {
+	echo esc_attr( bp_nouveau_get_activity_timestamp() );
+}
+
+	/**
+	 * Get the Activity timestamp.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return integer The Activity timestamp.
+	 */
+	function bp_nouveau_get_activity_timestamp() {
+		/**
+		 * Filter here to edit the activity timestamp.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param integer $value The Activity timestamp.
+		 */
+		return apply_filters( 'bp_nouveau_get_activity_timestamp', strtotime( bp_get_activity_date_recorded() ) );
+	}
+
+/**
+ * Output the action buttons inside an Activity Loop.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_activity_entry_buttons( $args = array() ) {
+	$output = join( ' ', bp_nouveau_get_activity_entry_buttons( $args ) );
+
+	ob_start();
+
+	/**
+	 * Fires at the end of the activity entry meta data area.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_activity_entry_meta' );
+
+	$output .= ob_get_clean();
+
+	$has_content = trim( $output, ' ' );
+	if ( ! $has_content ) {
+		return;
+	}
+
+	if ( ! $args ) {
+		$args = array( 'classes' => array( 'activity-meta' ) );
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+	/**
+	 * Get the action buttons inside an Activity Loop,
+	 *
+	 * @todo This function is too large and needs refactoring and reviewing.
+	 * @since 3.0.0
+	 */
+	function bp_nouveau_get_activity_entry_buttons( $args ) {
+		$buttons = array();
+
+		if ( ! isset( $GLOBALS['activities_template'] ) ) {
+			return $buttons;
+		}
+
+		$activity_id    = bp_get_activity_id();
+		$activity_type  = bp_get_activity_type();
+		$parent_element = '';
+		$button_element = 'a';
+
+		if ( ! $activity_id ) {
+			return $buttons;
+		}
+
+		/*
+		 * If the container is set to 'ul' force the $parent_element to 'li',
+		 * else use parent_element args if set.
+		 *
+		 * This will render li elements around anchors/buttons.
+		 */
+		if ( isset( $args['container'] ) && 'ul' === $args['container'] ) {
+			$parent_element = 'li';
+		} elseif ( ! empty( $args['parent_element'] ) ) {
+			$parent_element = $args['parent_element'];
+		}
+
+		$parent_attr = ( ! empty( $args['parent_attr'] ) ) ? $args['parent_attr'] : array();
+
+		/*
+		 * If we have a arg value for $button_element passed through
+		 * use it to default all the $buttons['button_element'] values
+		 * otherwise default to 'a' (anchor)
+		 * Or override & hardcode the 'element' string on $buttons array.
+		 *
+		 */
+		if ( ! empty( $args['button_element'] ) ) {
+			$button_element = $args['button_element'];
+		}
+
+		/*
+		 * The view conversation button and the comment one are sharing
+		 * the same id because when display_comments is on stream mode,
+		 * it is not possible to comment an activity comment and as we
+		 * are updating the links to avoid sorting the activity buttons
+		 * for each entry of the loop, it's a convenient way to make
+		 * sure the right button will be displayed.
+		 */
+		if ( $activity_type === 'activity_comment' ) {
+			$buttons['activity_conversation'] = array(
+				'id'                => 'activity_conversation',
+				'position'          => 5,
+				'component'         => 'activity',
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'must_be_logged_in' => false,
+				'button_element'    => $button_element,
+				'button_attr'       => array(
+					'class'           => 'button view bp-secondary-action bp-tooltip',
+					'data-bp-tooltip' => __( 'View Conversation', 'buddypress' ),
+					),
+				'link_text' => sprintf(
+					'<span class="bp-screen-reader-text">%1$s</span>',
+					__( 'View Conversation', 'buddypress' )
+				),
+			);
+
+			// If button element set add url link to data-attr
+			if ( 'button' === $button_element ) {
+				$buttons['activity_conversation']['button_attr']['data-bp-url'] = bp_get_activity_thread_permalink();
+			} else {
+				$buttons['activity_conversation']['button_attr']['href'] = bp_get_activity_thread_permalink();
+				$buttons['activity_conversation']['button_attr']['role'] = 'button';
+			}
+
+		/*
+		 * We always create the Button to make sure we always have the right numbers of buttons,
+		 * no matter the previous activity had less.
+		 */
+		} else {
+			$buttons['activity_conversation'] =  array(
+				'id'                => 'activity_conversation',
+				'position'          => 5,
+				'component'         => 'activity',
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'must_be_logged_in' => true,
+				'button_element'    => $button_element,
+				'button_attr'       => array(
+					'id'              => 'acomment-comment-' . $activity_id,
+					'class'           => 'button acomment-reply bp-primary-action bp-tooltip',
+					'data-bp-tooltip' => _x( 'Comment', 'button', 'buddypress' ),
+					'aria-expanded'   => 'false',
+				),
+				'link_text'  => sprintf(
+					'<span class="bp-screen-reader-text">%1$s</span> <span class="comment-count">%2$s</span>',
+					_x( 'Comment', 'link', 'buddypress' ),
+					esc_html( bp_activity_get_comment_count() )
+				),
+			);
+
+			// If button element set add href link to data-attr
+			if ( 'button' === $button_element ) {
+				$buttons['activity_conversation']['button_attr']['data-bp-url'] = bp_get_activity_comment_link();
+			} else {
+				$buttons['activity_conversation']['button_attr']['href'] = bp_get_activity_comment_link();
+				$buttons['activity_conversation']['button_attr']['role'] = 'button';
+			}
+
+		}
+
+		if ( bp_activity_can_favorite() ) {
+
+			// If button element set attr needs to be data-* else 'href'
+			if ( 'button' === $button_element ) {
+				$key = 'data-bp-nonce';
+			} else {
+				$key = 'href';
+			}
+
+			if ( ! bp_get_activity_is_favorite() ) {
+				$fav_args = array(
+					'parent_element'   => $parent_element,
+					'parent_attr'      => $parent_attr,
+					'button_element'   => $button_element,
+					'link_class'       => 'button fav bp-secondary-action bp-tooltip',
+					'data_bp_tooltip'  => __( 'Mark as Favorite', 'buddypress' ),
+					'link_text'        => __( 'Favorite', 'buddypress' ),
+					'aria-pressed'     => 'false',
+					'link_attr'        => bp_get_activity_favorite_link(),
+				);
+
+			} else {
+				$fav_args = array(
+					'parent_element'  => $parent_element,
+					'parent_attr'     => $parent_attr,
+					'button_element'  => $button_element,
+					'link_class'      => 'button unfav bp-secondary-action bp-tooltip',
+					'data_bp_tooltip' => __( 'Remove Favorite', 'buddypress' ),
+					'link_text'       => __( 'Remove Favorite', 'buddypress' ),
+					'aria-pressed'    => 'true',
+					'link_attr'       => bp_get_activity_unfavorite_link(),
+				);
+			}
+
+			$buttons['activity_favorite'] =  array(
+				'id'                => 'activity_favorite',
+				'position'          => 15,
+				'component'         => 'activity',
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'must_be_logged_in' => true,
+				'button_element'    => $fav_args['button_element'],
+				'link_text'         => sprintf( '<span class="bp-screen-reader-text">%1$s</span>', esc_html( $fav_args['link_text'] ) ),
+				'button_attr'       => array(
+					$key              => $fav_args['link_attr'],
+					'class'           => $fav_args['link_class'],
+					'data-bp-tooltip' => $fav_args['data_bp_tooltip'],
+					'aria-pressed'    => $fav_args['aria-pressed'],
+				),
+			);
+		}
+
+		// The delete button is always created, and removed later on if needed.
+		$delete_args = array();
+
+		/*
+		 * As the delete link is filterable we need this workaround
+		 * to try to intercept the edits the filter made and build
+		 * a button out of it.
+		 */
+		if ( has_filter( 'bp_get_activity_delete_link' ) ) {
+			preg_match( '/<a\s[^>]*>(.*)<\/a>/siU', bp_get_activity_delete_link(), $link );
+
+			if ( ! empty( $link[0] ) && ! empty( $link[1] ) ) {
+				$delete_args['link_text'] = $link[1];
+				$subject = str_replace( $delete_args['link_text'], '', $link[0] );
+			}
+
+			preg_match_all( '/([\w\-]+)=([^"\'> ]+|([\'"]?)(?:[^\3]|\3+)+?\3)/', $subject, $attrs );
+
+			if ( ! empty( $attrs[1] ) && ! empty( $attrs[2] ) ) {
+				foreach ( $attrs[1] as $key_attr => $key_value ) {
+					$delete_args[ 'link_'. $key_value ] = trim( $attrs[2][$key_attr], '"' );
+				}
+			}
+
+			$delete_args = bp_parse_args(
+				$delete_args,
+				array(
+					'link_text'   => '',
+					'button_attr' => array(
+						'link_id'         => '',
+						'link_href'       => '',
+						'link_class'      => '',
+						'link_rel'        => 'nofollow',
+						'data_bp_tooltip' => '',
+					),
+				),
+				'nouveau_get_activity_entry_buttons'
+			);
+		}
+
+		if ( empty( $delete_args['link_href'] ) ) {
+			$delete_args = array(
+				'button_element'  => $button_element,
+				'link_id'         => '',
+				'link_class'      => 'button item-button bp-secondary-action bp-tooltip delete-activity confirm',
+				'link_rel'        => 'nofollow',
+				'data_bp_tooltip' => _x( 'Delete', 'button', 'buddypress' ),
+				'link_text'       => _x( 'Delete', 'button', 'buddypress' ),
+				'link_href'       => bp_get_activity_delete_url(),
+			);
+
+			// If button element set add nonce link to data-attr attr
+			if ( 'button' === $button_element ) {
+				$delete_args['data-attr'] = bp_get_activity_delete_url();
+				$delete_args['link_href'] = '';
+			} else {
+				$delete_args['link_href'] = bp_get_activity_delete_url();
+				$delete_args['data-attr'] = '';
+			}
+		}
+
+		$buttons['activity_delete'] = array(
+			'id'                => 'activity_delete',
+			'position'          => 35,
+			'component'         => 'activity',
+			'parent_element'    => $parent_element,
+			'parent_attr'       => $parent_attr,
+			'must_be_logged_in' => true,
+			'button_element'    => $button_element,
+			'button_attr'       => array(
+				'id'              => $delete_args['link_id'],
+				'href'            => $delete_args['link_href'],
+				'class'           => $delete_args['link_class'],
+				'data-bp-tooltip' => $delete_args['data_bp_tooltip'],
+				'data-bp-nonce'   => $delete_args['data-attr'] ,
+			),
+			'link_text'  => sprintf( '<span class="bp-screen-reader-text">%s</span>', esc_html( $delete_args['data_bp_tooltip'] ) ),
+		);
+
+		// Add the Spam Button if supported
+		if ( bp_is_akismet_active() && isset( buddypress()->activity->akismet ) && bp_activity_user_can_mark_spam() ) {
+			$buttons['activity_spam'] = array(
+				'id'                => 'activity_spam',
+				'position'          => 45,
+				'component'         => 'activity',
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'must_be_logged_in' => true,
+				'button_element'    => $button_element,
+				'button_attr'       => array(
+					'class'           => 'bp-secondary-action spam-activity confirm button item-button bp-tooltip',
+					'id'              => 'activity_make_spam_' . $activity_id,
+					'data-bp-tooltip' => _x( 'Spam', 'button', 'buddypress' ),
+					),
+				'link_text'  => sprintf(
+					/** @todo: use a specific css rule for this *************************************************************/
+					'<span class="dashicons dashicons-flag" style="color:#a00;vertical-align:baseline;width:18px;height:18px" aria-hidden="true"></span><span class="bp-screen-reader-text">%s</span>',
+					esc_html_x( 'Spam', 'button', 'buddypress' )
+				),
+			);
+
+			// If button element, add nonce link to data attribute.
+			if ( 'button' === $button_element ) {
+				$data_element = 'data-bp-nonce';
+			} else {
+				$data_element = 'href';
+			}
+
+			$buttons['activity_spam']['button_attr'][ $data_element ] = wp_nonce_url(
+				bp_get_root_domain() . '/' . bp_get_activity_slug() . '/spam/' . $activity_id . '/',
+				'bp_activity_akismet_spam_' . $activity_id
+			);
+		}
+
+		/**
+		 * Filter to add your buttons, use the position argument to choose where to insert it.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $buttons     The list of buttons.
+		 * @param int   $activity_id The current activity ID.
+		 */
+		$buttons_group = apply_filters( 'bp_nouveau_get_activity_entry_buttons', $buttons, $activity_id );
+
+		if ( ! $buttons_group ) {
+			return $buttons;
+		}
+
+		// It's the first entry of the loop, so build the Group and sort it
+		if ( ! isset( bp_nouveau()->activity->entry_buttons ) || ! is_a( bp_nouveau()->activity->entry_buttons, 'BP_Buttons_Group' ) ) {
+			$sort = true;
+			bp_nouveau()->activity->entry_buttons = new BP_Buttons_Group( $buttons_group );
+
+		// It's not the first entry, the order is set, we simply need to update the Buttons Group
+		} else {
+			$sort = false;
+			bp_nouveau()->activity->entry_buttons->update( $buttons_group );
+		}
+
+		$return = bp_nouveau()->activity->entry_buttons->get( $sort );
+
+		if ( ! $return ) {
+			return array();
+		}
+
+		// Remove the Comment button if the user can't comment
+		if ( ! bp_activity_can_comment() && $activity_type !== 'activity_comment' ) {
+			unset( $return['activity_conversation'] );
+		}
+
+		// Remove the Delete button if the user can't delete
+		if ( ! bp_activity_user_can_delete() ) {
+			unset( $return['activity_delete'] );
+		}
+
+		if ( isset( $return['activity_spam'] ) && ! in_array( $activity_type, BP_Akismet::get_activity_types(), true ) ) {
+			unset( $return['activity_spam'] );
+		}
+
+		/**
+		 * Leave a chance to adjust the $return
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $return      The list of buttons ordered.
+		 * @param int   $activity_id The current activity ID.
+		 */
+		do_action_ref_array( 'bp_nouveau_return_activity_entry_buttons', array( &$return, $activity_id ) );
+
+		return $return;
+	}
+
+/**
+ * Output Activity Comments if any
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_comments() {
+	global $activities_template;
+
+	if ( empty( $activities_template->activity->children ) ) {
+		return;
+	}
+
+	bp_nouveau_activity_recurse_comments( $activities_template->activity );
+}
+
+/**
+ * Loops through a level of activity comments and loads the template for each.
+ *
+ * Note: This is an adaptation of the bp_activity_recurse_comments() BuddyPress core function
+ *
+ * @since 3.0.0
+ *
+ * @param object $comment The activity object currently being recursed.
+ */
+function bp_nouveau_activity_recurse_comments( $comment ) {
+	global $activities_template;
+
+	if ( empty( $comment->children ) ) {
+		return;
+	}
+
+	/**
+	 * Filters the opening tag for the template that lists activity comments.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value Opening tag for the HTML markup to use.
+	 */
+	echo apply_filters( 'bp_activity_recurse_comments_start_ul', '<ul>' );
+
+	foreach ( (array) $comment->children as $comment_child ) {
+
+		// Put the comment into the global so it's available to filters.
+		$activities_template->activity->current_comment = $comment_child;
+
+		/**
+		 * Fires before the display of an activity comment.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_before_activity_comment' );
+
+		bp_get_template_part( 'activity/comment' );
+
+		/**
+		 * Fires after the display of an activity comment.
+		 *
+		 * @since 1.5.0
+		 */
+		do_action( 'bp_after_activity_comment' );
+
+		unset( $activities_template->activity->current_comment );
+	}
+
+	/**
+	 * Filters the closing tag for the template that list activity comments.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param string $value Closing tag for the HTML markup to use.
+	 */
+	echo apply_filters( 'bp_activity_recurse_comments_end_ul', '</ul>' );
+}
+
+/**
+ * Ouptut the Activity comment action string
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_comment_action() {
+	echo bp_nouveau_get_activity_comment_action();
+}
+
+	/**
+	 * Get the Activity comment action string
+	 *
+	 * @since 3.0.0
+	 */
+	function bp_nouveau_get_activity_comment_action() {
+
+		/**
+		 * Filter to edit the activity comment action.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $value HTML Output
+		 */
+		return apply_filters( 'bp_nouveau_get_activity_comment_action',
+			/* translators: 1: user profile link, 2: user name, 3: activity permalink, 4: activity recorded date, 5: activity timestamp, 6: activity human time since */
+			sprintf( __( '<a href="%1$s">%2$s</a> replied <a href="%3$s" class="activity-time-since"><time class="time-since" datetime="%4$s" data-bp-timestamp="%5$d">%6$s</time></a>', 'buddypress' ),
+				esc_url( bp_get_activity_comment_user_link() ),
+				esc_html( bp_get_activity_comment_name() ),
+				esc_url( bp_get_activity_comment_permalink() ),
+				esc_attr( bp_get_activity_comment_date_recorded_raw() ),
+				esc_attr( strtotime( bp_get_activity_comment_date_recorded_raw() ) ),
+				esc_attr( bp_get_activity_comment_date_recorded() )
+		) );
+	}
+
+/**
+ * Load the Activity comment form
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_activity_comment_form() {
+	bp_get_template_part( 'activity/comment-form' );
+}
+
+/**
+ * Output the action buttons for the activity comments
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_activity_comment_buttons( $args = array() ) {
+	$output = join( ' ', bp_nouveau_get_activity_comment_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires after the defualt comment action options display.
+	 *
+	 * @since 1.6.0
+	 */
+	do_action( 'bp_activity_comment_options' );
+
+	$output     .= ob_get_clean();
+	$has_content = trim( $output, ' ' );
+	if ( ! $has_content ) {
+		return;
+	}
+
+	if ( ! $args ) {
+		$args = array( 'classes' => array( 'acomment-options' ) );
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+	/**
+	 * Get the action buttons for the activity comments
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+	 *
+	 * @return array
+	 */
+	function bp_nouveau_get_activity_comment_buttons($args) {
+		$buttons = array();
+
+		if ( ! isset( $GLOBALS['activities_template'] ) ) {
+			return $buttons;
+		}
+
+		$activity_comment_id = bp_get_activity_comment_id();
+		$activity_id         = bp_get_activity_id();
+
+		if ( ! $activity_comment_id || ! $activity_id ) {
+			return $buttons;
+		}
+
+		/*
+		 * If the 'container' is set to 'ul'
+		 * set a var $parent_element to li
+		 * otherwise simply pass any value found in args
+		 * or set var false.
+		 */
+		if ( 'ul' === $args['container']  ) {
+			$parent_element = 'li';
+		} elseif ( ! empty( $args['parent_element'] ) ) {
+			$parent_element = $args['parent_element'];
+		} else {
+			$parent_element = false;
+		}
+
+		$parent_attr = ( ! empty( $args['parent_attr'] ) ) ? $args['parent_attr'] : array();
+
+		/*
+		 * If we have a arg value for $button_element passed through
+		 * use it to default all the $buttons['button_element'] values
+		 * otherwise default to 'a' (anchor).
+		 */
+		if ( ! empty( $args['button_element'] ) ) {
+			$button_element = $args['button_element'] ;
+		} else {
+			$button_element = 'a';
+		}
+
+		$buttons = array(
+			'activity_comment_reply' => array(
+				'id'                => 'activity_comment_reply',
+				'position'          => 5,
+				'component'         => 'activity',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'button_element'    => $button_element,
+				'link_text'         => _x( 'Reply', 'link', 'buddypress' ),
+				'button_attr'       => array(
+					'class' => "acomment-reply bp-primary-action",
+					'id'    => sprintf( 'acomment-reply-%1$s-from-%2$s', $activity_id, $activity_comment_id ),
+				),
+			),
+			'activity_comment_delete' => array(
+				'id'                => 'activity_comment_delete',
+				'position'          => 15,
+				'component'         => 'activity',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'button_element'    => $button_element,
+				'link_text'         => _x( 'Delete', 'link', 'buddypress' ),
+				'button_attr'       => array(
+					'class' => 'delete acomment-delete confirm bp-secondary-action',
+					'rel'   => 'nofollow',
+				),
+			),
+		);
+
+		// If button element set add nonce link to data-attr attr
+		if ( 'button' === $button_element ) {
+			$buttons['activity_comment_reply']['button_attr']['data-bp-act-reply-nonce'] = sprintf( '#acomment-%s', $activity_comment_id );
+			$buttons['activity_comment_delete']['button_attr']['data-bp-act-reply-delete-nonce'] = bp_get_activity_comment_delete_link();
+		} else {
+			$buttons['activity_comment_reply']['button_attr']['href'] = sprintf( '#acomment-%s', $activity_comment_id );
+			$buttons['activity_comment_delete']['button_attr']['href'] = bp_get_activity_comment_delete_link();
+		}
+
+		// Add the Spam Button if supported
+		if ( bp_is_akismet_active() && isset( buddypress()->activity->akismet ) && bp_activity_user_can_mark_spam() ) {
+			$buttons['activity_comment_spam'] = array(
+				'id'                => 'activity_comment_spam',
+				'position'          => 25,
+				'component'         => 'activity',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'parent_attr'       => $parent_attr,
+				'button_element'    => $button_element,
+				'link_text'         => _x( 'Spam', 'link', 'buddypress' ),
+				'button_attr'       => array(
+					'id'     => "activity_make_spam_{$activity_comment_id}",
+					'class'  => 'bp-secondary-action spam-activity-comment confirm',
+					'rel'    => 'nofollow',
+				),
+			);
+
+			// If button element set add nonce link to data-attr attr
+			if ( 'button' === $button_element ) {
+				$data_element = 'data-bp-act-spam-nonce';
+			} else {
+				$data_element = 'href';
+			}
+
+			$buttons['activity_comment_spam']['button_attr'][ $data_element ] = wp_nonce_url(
+				bp_get_root_domain() . '/' . bp_get_activity_slug() . '/spam/' . $activity_comment_id . '/?cid=' . $activity_comment_id,
+				'bp_activity_akismet_spam_' . $activity_comment_id
+			);
+		}
+
+		/**
+		 * Filter to add your buttons, use the position argument to choose where to insert it.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $buttons             The list of buttons.
+		 * @param int   $activity_comment_id The current activity comment ID.
+		 * @param int   $activity_id         The current activity ID.
+		 */
+		$buttons_group = apply_filters( 'bp_nouveau_get_activity_comment_buttons', $buttons, $activity_comment_id, $activity_id );
+
+		if ( ! $buttons_group ) {
+			return $buttons;
+		}
+
+		// It's the first comment of the loop, so build the Group and sort it
+		if ( ! isset( bp_nouveau()->activity->comment_buttons ) || ! is_a( bp_nouveau()->activity->comment_buttons, 'BP_Buttons_Group' ) ) {
+			$sort = true;
+			bp_nouveau()->activity->comment_buttons = new BP_Buttons_Group( $buttons_group );
+
+		// It's not the first comment, the order is set, we simply need to update the Buttons Group
+		} else {
+			$sort = false;
+			bp_nouveau()->activity->comment_buttons->update( $buttons_group );
+		}
+
+		$return = bp_nouveau()->activity->comment_buttons->get( $sort );
+
+		if ( ! $return ) {
+			return array();
+		}
+
+		/**
+		 * If post comment / Activity comment sync is on, it's safer
+		 * to unset the comment button just before returning it.
+		 */
+		if ( ! bp_activity_can_comment_reply( bp_activity_current_comment() ) ) {
+			unset( $return['activity_comment_reply'] );
+		}
+
+		/**
+		 * If there was an activity of the user before one af another
+		 * user as we're updating buttons, we need to unset the delete link
+		 */
+		if ( ! bp_activity_user_can_delete() ) {
+			unset( $return['activity_comment_delete'] );
+		}
+
+		if ( isset( $return['activity_comment_spam'] ) && ( ! bp_activity_current_comment() || ! in_array( bp_activity_current_comment()->type, BP_Akismet::get_activity_types(), true ) ) ) {
+			unset( $return['activity_comment_spam'] );
+		}
+
+		/**
+		 * Leave a chance to adjust the $return
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $return              The list of buttons ordered.
+		 * @param int   $activity_comment_id The current activity comment ID.
+		 * @param int   $activity_id         The current activity ID.
+		 */
+		do_action_ref_array( 'bp_nouveau_return_activity_comment_buttons', array( &$return, $activity_comment_id, $activity_id ) );
+
+		return $return;
+	}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/widgets.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/widgets.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/activity/widgets.php	(revision 12265)
@@ -0,0 +1,205 @@
+<?php
+/**
+ * BP Nouveau Activity widgets
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * A widget to display the latest activities of your community!
+ *
+ * @since 3.0.0
+ */
+class BP_Latest_Activities extends WP_Widget {
+	/**
+	 * Construct the widget.
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+
+		/**
+		 * Filters the widget options for the BP_Latest_Activities widget.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $value Array of widget options.
+		 */
+		$widget_ops = apply_filters(
+			'bp_latest_activities', array(
+				'classname'                   => 'bp-latest-activities buddypress',
+				'description'                 => __( 'Display the latest updates of your community having the types of your choice.', 'buddypress' ),
+				'customize_selective_refresh' => true,
+			)
+		);
+
+		parent::__construct( false, __( '(BuddyPress) Latest Activities', 'buddypress' ), $widget_ops );
+	}
+
+	/**
+	 * Register the widget.
+	 *
+	 * @since 3.0.0
+	 */
+	public static function register_widget() {
+		register_widget( 'BP_Latest_Activities' );
+	}
+
+	/**
+	 * Display the widget content.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args     Widget arguments.
+	 * @param array $instance Widget settings, as saved by the user.
+	 */
+	public function widget( $args, $instance ) {
+		// Default values
+		$title      = __( 'Latest updates', 'buddypress' );
+		$type       = array( 'activity_update' );
+		$max        = 5;
+		$bp_nouveau = bp_nouveau();
+
+		// Check instance for a custom title
+		if ( ! empty( $instance['title'] ) ) {
+			$title = $instance['title'];
+		}
+
+		/**
+		 * Filters the BP_Latest_Activities widget title.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
+
+		// Check instance for custom max number of activities to display
+		if ( ! empty( $instance['max'] ) ) {
+			$max = (int) $instance['max'];
+		}
+
+		// Check instance for custom activity types
+		if ( ! empty( $instance['type'] ) ) {
+			$type    = maybe_unserialize( $instance['type'] );
+			$classes = array_map( 'sanitize_html_class', array_merge( $type, array( 'bp-latest-activities' ) ) );
+
+			// Add classes to the container
+			$args['before_widget'] = str_replace( 'bp-latest-activities', join( ' ', $classes ), $args['before_widget'] );
+		}
+
+		echo $args['before_widget'];
+
+		if ( $title ) {
+			echo $args['before_title'] . $title . $args['after_title'];
+		}
+
+		$reset_activities_template = null;
+		if ( ! empty( $GLOBALS['activities_template'] ) ) {
+			$reset_activities_template = $GLOBALS['activities_template'];
+		}
+
+		/**
+		 * Globalize the activity widget arguments.
+		 * @see bp_nouveau_activity_widget_query() to override
+		 */
+		$bp_nouveau->activity->widget_args = array(
+			'max'          => $max,
+			'scope'        => 'all',
+			'user_id'      => 0,
+			'object'       => false,
+			'action'       => join( ',', $type ),
+			'primary_id'   => 0,
+			'secondary_id' => 0,
+		);
+
+		bp_get_template_part( 'activity/widget' );
+
+		// Reset the globals
+		$GLOBALS['activities_template']    = $reset_activities_template;
+		$bp_nouveau->activity->widget_args = array();
+
+		echo $args['after_widget'];
+	}
+
+	/**
+	 * Update the widget settings.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $new_instance The new instance settings.
+	 * @param array $old_instance The old instance settings.
+	 *
+	 * @return array The widget settings.
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance = $old_instance;
+
+		$instance['title'] = strip_tags( $new_instance['title'] );
+		$instance['max']   = 5;
+		if ( ! empty( $new_instance['max'] ) ) {
+			$instance['max'] = $new_instance['max'];
+		}
+
+		$instance['type'] = maybe_serialize( array( 'activity_update' ) );
+		if ( ! empty( $new_instance['type'] ) ) {
+			$instance['type'] = maybe_serialize( $new_instance['type'] );
+		}
+
+		return $instance;
+	}
+
+	/**
+	 * Display the form to set the widget settings.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $instance Settings for this widget.
+	 *
+	 * @return string HTML output.
+	 */
+	public function form( $instance ) {
+		$instance = bp_parse_args(
+			(array) $instance,
+			array(
+				'title' => __( 'Latest updates', 'buddypress' ),
+				'max'   => 5,
+				'type'  => '',
+			),
+			'widget_latest_activities'
+		);
+
+		$title = esc_attr( $instance['title'] );
+		$max   = (int) $instance['max'];
+
+		$type = array( 'activity_update' );
+		if ( ! empty( $instance['type'] ) ) {
+			$type = maybe_unserialize( $instance['type'] );
+		}
+		?>
+		<p>
+			<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:', 'buddypress' ); ?></label>
+			<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+		</p>
+
+		<p>
+			<label for="<?php echo $this->get_field_id( 'max' ); ?>"><?php _e( 'Maximum amount to display:', 'buddypress' ); ?></label>
+			<input type="number" class="widefat" id="<?php echo $this->get_field_id( 'max' ); ?>" name="<?php echo $this->get_field_name( 'max' ); ?>" value="<?php echo intval( $max ); ?>" step="1" min="1" max="20" />
+		</p>
+		<p>
+			<label for="<?php echo $this->get_field_id( 'type' ); ?>"><?php esc_html_e( 'Type:', 'buddypress' ); ?></label>
+			<select class="widefat" multiple="multiple" id="<?php echo $this->get_field_id( 'type' ); ?>" name="<?php echo $this->get_field_name( 'type' ); ?>[]">
+				<?php foreach ( bp_nouveau_get_activity_filters() as $key => $name ) : ?>
+					<option value="<?php echo esc_attr( $key ); ?>" <?php selected( in_array( $key, $type, true ) ); ?>><?php echo esc_html( $name ); ?></option>
+				<?php endforeach; ?>
+			</select>
+		</p>
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/ajax.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/ajax.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/ajax.php	(revision 12265)
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Common functions only loaded on AJAX requests.
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Load the template loop for the current object.
+ *
+ * @since 3.0.0
+ *
+ * @return string Template loop for the specified object
+ */
+function bp_nouveau_ajax_object_template_loader() {
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error();
+	}
+
+	if ( empty( $_POST['object'] ) ) {
+		wp_send_json_error();
+	}
+
+	$object = sanitize_title( $_POST['object'] );
+
+	// Bail if object is not an active component to prevent arbitrary file inclusion.
+	if ( ! bp_is_active( $object ) ) {
+		wp_send_json_error();
+	}
+
+	// Nonce check!
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_' . $object ) ) {
+		wp_send_json_error();
+	}
+
+	$result = array();
+
+	if ( 'activity' === $object ) {
+		$scope = '';
+		if ( ! empty( $_POST['scope'] ) ) {
+			$scope = sanitize_text_field( $_POST['scope'] );
+		}
+
+		// We need to calculate and return the feed URL for each scope.
+		switch ( $scope ) {
+			case 'friends':
+				$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/friends/feed/';
+				break;
+			case 'groups':
+				$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/groups/feed/';
+				break;
+			case 'favorites':
+				$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/favorites/feed/';
+				break;
+			case 'mentions':
+				$feed_url = bp_loggedin_user_domain() . bp_get_activity_slug() . '/mentions/feed/';
+
+				// Get user new mentions
+				$new_mentions = bp_get_user_meta( bp_loggedin_user_id(), 'bp_new_mentions', true );
+
+				// If we have some, include them into the returned json before deleting them
+				if ( is_array( $new_mentions ) ) {
+					$result['new_mentions'] = $new_mentions;
+
+					// Clear new mentions
+					bp_activity_clear_new_mentions( bp_loggedin_user_id() );
+				}
+
+				break;
+			default:
+				$feed_url = bp_get_sitewide_activity_feed_link();
+				break;
+		}
+
+		/**
+		 * Filters the browser URL for the template loader.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $feed_url Template feed url.
+		 * @param string $scope    Current component scope.
+		 */
+		$result['feed_url'] = apply_filters( 'bp_nouveau_ajax_object_template_loader', $feed_url, $scope );
+	}
+
+	/*
+	 * AJAX requests happen too early to be seen by bp_update_is_directory()
+	 * so we do it manually here to ensure templates load with the correct
+	 * context. Without this check, templates will load the 'single' version
+	 * of themselves rather than the directory version.
+	 */
+	if ( ! bp_current_action() ) {
+		bp_update_is_directory( true, bp_current_component() );
+	}
+
+	// Get the template path based on the 'template' variable via the AJAX request.
+	$template = isset( $_POST['template'] ) ? wp_unslash( $_POST['template'] ) : '';
+
+	switch ( $template ) {
+		case 'group_members' :
+		case 'groups/single/members' :
+			$template_part = 'groups/single/members-loop.php';
+		break;
+
+		case 'group_requests' :
+			$template_part = 'groups/single/requests-loop.php';
+		break;
+
+		case 'member_notifications' :
+			$template_part = 'members/single/notifications/notifications-loop.php';
+		break;
+
+		default :
+			$template_part = $object . '/' . $object . '-loop.php';
+		break;
+	}
+
+	ob_start();
+
+	$template_path = bp_locate_template( array( $template_part ), false );
+
+	/**
+	 * Filters the server path for the template loader.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string Template file path.
+	 */
+	$template_path = apply_filters( 'bp_nouveau_object_template_path', $template_path );
+
+	load_template( $template_path );
+	$result['contents'] = ob_get_contents();
+	ob_end_clean();
+
+	// Locate the object template.
+	wp_send_json_success( $result );
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/ajax.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/ajax.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/ajax.php	(revision 12265)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Blogs Ajax functions
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_action( 'admin_init', function() {
+	$ajax_actions = array(
+		array(
+			'blogs_filter' => array(
+				'function' => 'bp_nouveau_ajax_object_template_loader',
+				'nopriv'   => true,
+			),
+		),
+	);
+
+	foreach ( $ajax_actions as $ajax_action ) {
+		$action = key( $ajax_action );
+
+		add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+		if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+			add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+		}
+	}
+}, 12 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/functions.php	(revision 12265)
@@ -0,0 +1,234 @@
+<?php
+/**
+ * Blogs functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_get_blogs_directory_nav_items() {
+	$nav_items = array();
+
+	$nav_items['all'] = array(
+		'component' => 'blogs',
+		'slug'      => 'all', // slug is used because BP_Core_Nav requires it, but it's the scope
+		'li_class'  => array( 'selected' ),
+		'link'      => bp_get_root_domain() . '/' . bp_get_blogs_root_slug(),
+		'text'      => __( 'All Sites', 'buddypress' ),
+		'count'     => bp_get_total_blog_count(),
+		'position'  => 5,
+	);
+
+	if ( is_user_logged_in() ) {
+		$my_blogs_count = bp_get_total_blog_count_for_user( bp_loggedin_user_id() );
+
+		// If the user has blogs create a nav item
+		if ( $my_blogs_count ) {
+			$nav_items['personal'] = array(
+				'component' => 'blogs',
+				'slug'      => 'personal', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array(),
+				'link'      => bp_loggedin_user_domain() . bp_get_blogs_slug(),
+				'text'      => __( 'My Sites', 'buddypress' ),
+				'count'     => $my_blogs_count,
+				'position'  => 15,
+			);
+		}
+
+		// If the user can create blogs, add the create nav
+		if ( bp_blog_signup_enabled() ) {
+			$nav_items['create'] = array(
+				'component' => 'blogs',
+				'slug'      => 'create', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array( 'no-ajax', 'site-create', 'create-button' ),
+				'link'      => trailingslashit( bp_get_blogs_directory_permalink() . 'create' ),
+				'text'      => __( 'Create a Site', 'buddypress' ),
+				'count'     => false,
+				'position'  => 999,
+			);
+		}
+	}
+
+	// Check for the deprecated hook :
+	$extra_nav_items = bp_nouveau_parse_hooked_dir_nav( 'bp_blogs_directory_blog_types', 'blogs', 20 );
+
+	if ( ! empty( $extra_nav_items ) ) {
+		$nav_items = array_merge( $nav_items, $extra_nav_items );
+	}
+
+	/**
+	 * Use this filter to introduce your custom nav items for the blogs directory.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param  array $nav_items The list of the blogs directory nav items.
+	 */
+	return apply_filters( 'bp_nouveau_get_blogs_directory_nav_items', $nav_items );
+}
+
+/**
+ * Get Dropdown filters for the blogs component
+ *
+ * @since 3.0.0
+ *
+ * @param string $context 'directory' or 'user'
+ *
+ * @return array the filters
+ */
+function bp_nouveau_get_blogs_filters( $context = '' ) {
+	if ( empty( $context ) ) {
+		return array();
+	}
+
+	$action = '';
+	if ( 'user' === $context ) {
+		$action = 'bp_member_blog_order_options';
+	} elseif ( 'directory' === $context ) {
+		$action = 'bp_blogs_directory_order_options';
+	}
+
+	/**
+	 * Recommended, filter here instead of adding an action to 'bp_member_blog_order_options'
+	 * or 'bp_blogs_directory_order_options'
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array  the blogs filters.
+	 * @param string the context.
+	 */
+	$filters = apply_filters( 'bp_nouveau_get_blogs_filters', array(
+		'active'       => __( 'Last Active', 'buddypress' ),
+		'newest'       => __( 'Newest', 'buddypress' ),
+		'alphabetical' => __( 'Alphabetical', 'buddypress' ),
+	), $context );
+
+	if ( $action ) {
+		return bp_nouveau_parse_hooked_options( $action, $filters );
+	}
+
+	return $filters;
+}
+
+/**
+ * Catch the arguments for buttons
+ *
+ * @since 3.0.0
+ *
+ * @param array $buttons The arguments of the button that BuddyPress is about to create.
+ *
+ * @return array An empty array to stop the button creation process.
+ */
+function bp_nouveau_blogs_catch_button_args( $button = array() ) {
+	// Globalize the arguments so that we can use it  in bp_nouveau_get_blogs_buttons().
+	bp_nouveau()->blogs->button_args = $button;
+
+	// return an empty array to stop the button creation process
+	return array();
+}
+
+/**
+ * Add settings to the customizer for the blogs component.
+ *
+ * @since 3.0.0
+ *
+ * @param array $settings the settings to add.
+ *
+ * @return array the settings to add.
+ */
+function bp_nouveau_blogs_customizer_settings( $settings = array() ) {
+	return array_merge( $settings, array(
+		'bp_nouveau_appearance[blogs_layout]' => array(
+			'index'             => 'blogs_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+	) );
+}
+
+/**
+ * Add controls for the settings of the customizer for the blogs component.
+ *
+ * @since 3.0.0
+ *
+ * @param array $controls the controls to add.
+ *
+ * @return array the controls to add.
+ */
+function bp_nouveau_blogs_customizer_controls( $controls = array() ) {
+	return array_merge( $controls, array(
+		'blogs_layout' => array(
+			'label'      => __( 'Sites loop:', 'buddypress' ),
+			'section'    => 'bp_nouveau_loops_layout',
+			'settings'   => 'bp_nouveau_appearance[blogs_layout]',
+			'type'       => 'select',
+			'choices'    => bp_nouveau_customizer_grid_choices(),
+		),
+		'sites_dir_layout' => array(
+			'label'      => __( 'Use column navigation for the Sites directory.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[sites_dir_layout]',
+			'type'       => 'checkbox',
+		),
+		'sites_dir_tabs' => array(
+			'label'      => __( 'Use tab styling for Sites directory navigation.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[sites_dir_tabs]',
+			'type'       => 'checkbox',
+		),
+	) );
+}
+
+/**
+ * Inline script to toggle the signup blog form
+ *
+ * @since 3.0.0
+ *
+ * @return string Javascript output
+ */
+function bp_nouveau_get_blog_signup_inline_script() {
+	return '
+		( function( $ ) {
+			if ( $( \'body\' ).hasClass( \'register\' ) ) {
+				var blog_checked = $( \'#signup_with_blog\' );
+
+				// hide "Blog Details" block if not checked by default
+				if ( ! blog_checked.prop( \'checked\' ) ) {
+					$( \'#blog-details\' ).toggle();
+				}
+
+				// toggle "Blog Details" block whenever checkbox is checked
+				blog_checked.change( function( event ) {
+					// Toggle HTML5 required attribute.
+					$.each( $( \'#blog-details\' ).find( \'[aria-required]\' ), function( i, input ) {
+						$( input ).prop( \'required\',  $( event.target ).prop( \'checked\' ) );
+					} );
+
+					$( \'#blog-details\' ).toggle();
+				} );
+			}
+		} )( jQuery );
+	';
+}
+
+/**
+ * Filter bp_get_blog_class().
+ * Adds a class if blog item has a latest post.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_blog_loop_item_has_lastest_post( $classes ) {
+	if ( bp_get_blog_latest_post_title() ) {
+		$classes[] = 'has-latest-post';
+	}
+
+	return $classes;
+}
+add_filter( 'bp_get_blog_class', 'bp_nouveau_blog_loop_item_has_lastest_post' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/loader.php	(revision 12265)
@@ -0,0 +1,107 @@
+<?php
+/**
+ * BP Nouveau Blogs
+ *
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Blogs Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Blogs {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir = trailingslashit( dirname( __FILE__ ) );
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require $this->dir . 'functions.php';
+		require $this->dir . 'template-tags.php';
+
+		// Test suite requires the AJAX functions early.
+		if ( function_exists( 'tests_add_filter' ) ) {
+			require $this->dir . 'ajax.php';
+
+		// Load AJAX code only on AJAX requests.
+		} else {
+			add_action( 'admin_init', function() {
+				if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && 0 === strpos( $_REQUEST['action'], 'blogs_' ) ) {
+					require bp_nouveau()->blogs->dir . 'ajax.php';
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		if ( ! is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+			// Avoid Notices for BuddyPress Legacy Backcompat
+			remove_action( 'bp_blogs_directory_blog_types', 'bp_blog_backcompat_create_nav_item', 1000 );
+		}
+
+		add_action( 'bp_nouveau_enqueue_scripts', function() {
+			if ( bp_get_blog_signup_allowed() && bp_is_register_page() ) {
+				wp_add_inline_script( 'bp-nouveau', bp_nouveau_get_blog_signup_inline_script() );
+			}
+		} );
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		if ( is_multisite() ) {
+			// Add settings into the Blogs sections of the customizer.
+			add_filter( 'bp_nouveau_customizer_settings', 'bp_nouveau_blogs_customizer_settings', 11, 1 );
+
+			// Add controls into the Blogs sections of the customizer.
+			add_filter( 'bp_nouveau_customizer_controls', 'bp_nouveau_blogs_customizer_controls', 11, 1 );
+		}
+	}
+}
+
+/**
+ * Launch the Blogs loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_blogs( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->blogs = new BP_Nouveau_Blogs();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_blogs', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/blogs/template-tags.php	(revision 12265)
@@ -0,0 +1,311 @@
+<?php
+/**
+ * Blogs Template tags
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Template tag to wrap all Legacy actions that was used
+ * before the blogs directory content
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_before_blogs_directory_content() {
+	/**
+	 * Fires at the begining of the templates BP injected content.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_before_directory_blogs_page' );
+
+	/**
+	 * Fires before the display of the blogs.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_before_directory_blogs' );
+
+	/**
+	 * Fires before the display of the blogs listing content.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_before_directory_blogs_content' );
+
+	/**
+	 * Fires before the display of the blogs list tabs.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_before_directory_blogs_tabs' );
+}
+
+/**
+ * Template tag to wrap all Legacy actions that was used after the blogs directory content
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_after_blogs_directory_content() {
+	/**
+	 * Fires inside and displays the blogs content.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_directory_blogs_content' );
+
+	/**
+	 * Fires after the display of the blogs listing content.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_after_directory_blogs_content' );
+
+	/**
+	 * Fires at the bottom of the blogs directory template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_blogs' );
+
+	/**
+	 * Fires at the bottom of the blogs directory template file.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_after_directory_blogs_page' );
+}
+
+/**
+ * Fire specific hooks into the blogs create template
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   Optional. Either 'before' or 'after'.
+ * @param string $suffix Optional. Use it to add terms at the end of the hook name.
+ */
+function bp_nouveau_blogs_create_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a create a blog hook
+	$hook[] = 'create_blog';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Fire an isolated hook inside the blogs loop
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_blogs_loop_item() {
+	/**
+	 * Fires after the listing of a blog item in the blogs loop.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_directory_blogs_item' );
+}
+
+/**
+ * Output the action buttons inside the blogs loop.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_blogs_loop_buttons( $args = array() ) {
+	if ( empty( $GLOBALS['blogs_template'] ) ) {
+		return;
+	}
+
+	$args['type'] = 'loop';
+
+	$output = join( ' ', bp_nouveau_get_blogs_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires inside the blogs action listing area.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_directory_blogs_actions' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+	/**
+	 * Get the action buttons for the current blog in the loop.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $type Type of Group of buttons to get.
+	 *
+	 * @return array
+	 */
+	function bp_nouveau_get_blogs_buttons( $args ) {
+		$type = ( ! empty( $args['type'] ) ) ? $args['type'] : 'loop';
+
+		// @todo Not really sure why BP Legacy needed to do this...
+		if ( 'loop' !== $type && is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+			return array();
+		}
+
+		$buttons = array();
+
+		if ( isset( $GLOBALS['blogs_template']->blog ) ) {
+			$blog = $GLOBALS['blogs_template']->blog;
+		}
+
+		if ( empty( $blog->blog_id ) ) {
+			return $buttons;
+		}
+
+		/*
+		 * If the 'container' is set to 'ul', set a var $parent_element to li,
+		 * otherwise simply pass any value found in args or set var false.
+		 */
+		if ( ! empty( $args['container'] ) && 'ul' === $args['container'] ) {
+			$parent_element = 'li';
+		} elseif ( ! empty( $args['parent_element'] ) ) {
+			$parent_element = $args['parent_element'];
+		} else {
+			$parent_element = false;
+		}
+
+		/*
+		 * If we have a arg value for $button_element passed through
+		 * use it to default all the $buttons['button_element'] values
+		 * otherwise default to 'a' (anchor)
+		 * Or override & hardcode the 'element' string on $buttons array.
+		 *
+		 * Icons sets a class for icon display if not using the button element
+		 */
+		$icons = '';
+		if ( ! empty( $args['button_element'] ) ) {
+			$button_element = $args['button_element'] ;
+		} else {
+			$button_element = 'a';
+			$icons = ' icons';
+		}
+
+		/*
+		 * This filter workaround is waiting for a core adaptation
+		 * so that we can directly get the groups button arguments
+		 * instead of the button.
+		 *
+		 * See https://buddypress.trac.wordpress.org/ticket/7126
+		 */
+		add_filter( 'bp_get_blogs_visit_blog_button', 'bp_nouveau_blogs_catch_button_args', 100, 1 );
+
+		bp_get_blogs_visit_blog_button();
+
+		remove_filter( 'bp_get_blogs_visit_blog_button', 'bp_nouveau_blogs_catch_button_args', 100, 1 );
+
+		if ( ! empty( bp_nouveau()->blogs->button_args ) ) {
+			$button_args = bp_nouveau()->blogs->button_args ;
+
+			// If we pass through parent classes add them to $button array
+			$parent_class = '';
+			if ( ! empty( $args['parent_attr']['class'] ) ) {
+				$parent_class = $args['parent_attr']['class'];
+			}
+
+			$buttons['visit_blog'] = array(
+				'id'                => 'visit_blog',
+				'position'          => 5,
+				'component'         => $button_args['component'],
+				'must_be_logged_in' => $button_args['must_be_logged_in'],
+				'block_self'        => $button_args['block_self'],
+				'parent_element'    => $parent_element,
+				'button_element'    => $button_element,
+				'link_text'         => $button_args['link_text'],
+				'parent_attr'       => array(
+					'id'              => $button_args['wrapper_id'],
+					'class'           => $parent_class,
+				),
+				'button_attr'       => array(
+					'href'             => $button_args['link_href'],
+					'id'               => $button_args['link_id'],
+					'class'            => $button_args['link_class'] . ' button',
+					'rel'              => $button_args['link_rel'],
+					'title'            => '',
+				),
+			);
+
+			unset( bp_nouveau()->blogs->button_args );
+		}
+
+		/**
+		 * Filter to add your buttons, use the position argument to choose where to insert it.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $buttons The list of buttons.
+		 * @param object $blog    The current blog object.
+		 * @param string $type    Whether we're displaying a blogs loop or a the blogs single item (in the future!).
+		 */
+		$buttons_group = apply_filters( 'bp_nouveau_get_blogs_buttons', $buttons, $blog, $type );
+
+		if ( ! $buttons_group ) {
+			return $buttons;
+		}
+
+		// It's the first entry of the loop, so build the Group and sort it
+		if ( ! isset( bp_nouveau()->blogs->group_buttons ) || ! is_a( bp_nouveau()->blogs->group_buttons, 'BP_Buttons_Group' ) ) {
+			$sort = true;
+			bp_nouveau()->blogs->group_buttons = new BP_Buttons_Group( $buttons_group );
+
+		// It's not the first entry, the order is set, we simply need to update the Buttons Group
+		} else {
+			$sort = false;
+			bp_nouveau()->blogs->group_buttons->update( $buttons_group );
+		}
+
+		$return = bp_nouveau()->blogs->group_buttons->get( $sort );
+
+		if ( ! $return ) {
+			return array();
+		}
+
+		/**
+		 * Leave a chance to adjust the $return
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $return  The list of buttons ordered.
+		 * @param object $blog    The current blog object.
+		 * @param string $type    Whether we're displaying a blogs loop or a the blogs single item (in the future!).
+		 */
+		do_action_ref_array( 'bp_nouveau_return_blogs_buttons', array( &$return, $blog, $type ) );
+
+		return $return;
+	}
+
+/**
+ * Check if the Sites has a latest post
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if the sites has a latest post. False otherwise.
+ */
+function bp_nouveau_blog_has_latest_post() {
+	return (bool) bp_get_blog_latest_post_title();
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/classes.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/classes.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/classes.php	(revision 12265)
@@ -0,0 +1,335 @@
+<?php
+/**
+ * Common Classes
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Builds a group of BP_Button
+ *
+ * @since 3.0.0
+ */
+class BP_Buttons_Group {
+
+	/**
+	 * The parameters of the Group of buttons
+	 *
+	 * @var array
+	 */
+	protected $group = array();
+
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args Optional array having the following parameters {
+	 *     @type string $id                A string to use as the unique ID for the button. Required.
+	 *     @type int    $position          Where to insert the Button. Defaults to 99.
+	 *     @type string $component         The Component's the button is build for (eg: Activity, Groups..). Required.
+	 *     @type bool   $must_be_logged_in Whether the button should only be displayed to logged in users. Defaults to True.
+	 *     @type bool   $block_self        Optional. True if the button should be hidden when a user is viewing his own profile.
+	 *                                     Defaults to False.
+	 *     @type string $parent_element    Whether to use a wrapper. Defaults to false.
+	 *     @type string $parent_attr       set an array of attributes for the parent element.
+	 *     @type string $button_element    Set this to 'button', 'img', or 'a', defaults to anchor.
+	 *     @type string $button_attr       Any attributes required for the button_element
+	 *     @type string $link_text         The text of the link. Required.
+	 * }
+	 */
+	public function __construct( $args = array() ) {
+		foreach ( $args as $arg ) {
+			$r = bp_parse_args(
+				(array) $arg,
+				array(
+					'id'                => '',
+					'position'          => 99,
+					'component'         => '',
+					'must_be_logged_in' => true,
+					'block_self'        => false,
+					'parent_element'    => false,
+					'parent_attr'       => array(),
+					'button_element'    => 'a',
+					'button_attr'       => array(),
+					'link_text'         => '',
+				),
+				'buttons_group_constructor'
+			);
+
+			// Just don't set the button if a param is missing
+			if ( empty( $r['id'] ) || empty( $r['component'] ) || empty( $r['link_text'] ) ) {
+				continue;
+			}
+
+			$r['id'] = sanitize_key( $r['id'] );
+
+			// If the button already exist don't add it
+			if ( isset( $this->group[ $r['id'] ] ) ) {
+				continue;
+			}
+
+			/*
+			 * If, in bp_nouveau_get_*_buttons(), we pass through a false value for 'parent_element'
+			 * but we have attributtes for it in the array, let's default to setting a div.
+			 *
+			 * Otherwise, the original false value will be passed through to BP buttons.
+			 * @todo: this needs review, probably trying to be too clever
+			 */
+			if ( ( ! empty( $r['parent_attr'] ) ) && false === $r['parent_element'] ) {
+				$r['parent_element'] = 'div';
+			}
+
+			$this->group[ $r['id'] ] = $r;
+		}
+	}
+
+
+	/**
+	 * Sort the Buttons of the group according to their position attribute
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array the list of buttons to sort.
+	 *
+	 * @return array the list of buttons sorted.
+	 */
+	public function sort( $buttons ) {
+		$sorted = array();
+
+		foreach ( $buttons as $button ) {
+			$position = 99;
+
+			if ( isset( $button['position'] ) ) {
+				$position = (int) $button['position'];
+			}
+
+			// If position is already taken, move to the first next available
+			if ( isset( $sorted[ $position ] ) ) {
+				$sorted_keys = array_keys( $sorted );
+
+				do {
+					$position += 1;
+				} while ( in_array( $position, $sorted_keys, true ) );
+			}
+
+			$sorted[ $position ] = $button;
+		}
+
+		ksort( $sorted );
+		return $sorted;
+	}
+
+	/**
+	 * Get the BuddyPress buttons for the group
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param bool $sort whether to sort the buttons or not.
+	 *
+	 * @return array An array of HTML links.
+	 */
+	public function get( $sort = true ) {
+		$buttons = array();
+
+		if ( empty( $this->group ) ) {
+			return $buttons;
+		}
+
+		if ( true === $sort ) {
+			$this->group = $this->sort( $this->group );
+		}
+
+		foreach ( $this->group as $key_button => $button ) {
+			// Reindex with ids.
+			if ( true === $sort ) {
+				$this->group[ $button['id'] ] = $button;
+				unset( $this->group[ $key_button ] );
+			}
+
+			$buttons[ $button['id'] ] = bp_get_button( $button );
+		}
+
+		return $buttons;
+	}
+
+	/**
+	 * Update the group of buttons
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args Optional. See the __constructor for a description of this argument.
+	 */
+	public function update( $args = array() ) {
+		foreach ( $args as $id => $params ) {
+			if ( isset( $this->group[ $id ] ) ) {
+				$this->group[ $id ] = bp_parse_args(
+					$params,
+					$this->group[ $id ],
+					'buttons_group_update'
+				);
+			}
+		}
+	}
+}
+
+/**
+ * BP Sidebar Item Nav_Widget
+ *
+ * Adds a widget to move avatar/item nav into the sidebar
+ *
+ * @since 3.0.0
+ *
+ * @uses WP_Widget
+ */
+class BP_Nouveau_Object_Nav_Widget extends WP_Widget {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$widget_ops = array(
+			'description' => __( 'Displays BuddyPress primary nav in the sidebar of your site. Make sure to use it as the first widget of the sidebar and only once.', 'buddypress' ),
+			'classname'   => 'widget_nav_menu buddypress_object_nav',
+		);
+
+		parent::__construct(
+			'bp_nouveau_sidebar_object_nav_widget',
+			__( '(BuddyPress) Primary navigation', 'buddypress' ),
+			$widget_ops
+		);
+	}
+
+	/**
+	 * Register the widget
+	 *
+	 * @since 3.0.0
+	 */
+	public static function register_widget() {
+		register_widget( 'BP_Nouveau_Object_Nav_Widget' );
+	}
+
+	/**
+	 * Displays the output, the button to post new support topics
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param mixed   $args     Arguments
+	 * @param unknown $instance
+	 */
+	public function widget( $args, $instance ) {
+		if ( ! is_buddypress() || bp_is_group_create() ) {
+			return;
+		}
+
+		/**
+		 * Filters the nav widget args for the BP_Nouveau_Object_Nav_Widget widget.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $value Array of arguments {
+		 *     @param bool $bp_nouveau_widget_title Whether or not to assign a title for the widget.
+		 * }
+		 */
+		$item_nav_args = bp_parse_args(
+			$instance,
+			apply_filters(
+				'bp_nouveau_object_nav_widget_args',
+				array( 'bp_nouveau_widget_title' => true )
+			),
+			'widget_object_nav'
+		);
+
+		$title = '';
+
+		if ( ! empty( $item_nav_args['bp_nouveau_widget_title'] ) ) {
+			if ( bp_is_group() ) {
+				$title = bp_get_current_group_name();
+			} elseif ( bp_is_user() ) {
+				$title = bp_get_displayed_user_fullname();
+			} elseif ( bp_get_directory_title( bp_current_component() ) ) {
+				$title = bp_get_directory_title( bp_current_component() );
+			}
+		}
+
+		/**
+		 * Filters the BP_Nouveau_Object_Nav_Widget widget title.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $title    The widget title.
+		 * @param array  $instance The settings for the particular instance of the widget.
+		 * @param string $id_base  Root ID for all widgets of this type.
+		 */
+		$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
+
+		echo $args['before_widget'];
+
+		if ( ! empty( $title ) ) {
+			echo $args['before_title'] . $title . $args['after_title'];
+		}
+
+		if ( bp_is_user() ) {
+			bp_get_template_part( 'members/single/parts/item-nav' );
+		} elseif ( bp_is_group() ) {
+			bp_get_template_part( 'groups/single/parts/item-nav' );
+		} elseif ( bp_is_directory() ) {
+			bp_get_template_part( 'common/nav/directory-nav' );
+		}
+
+		echo $args['after_widget'];
+	}
+
+	/**
+	 * Update the new support topic widget options (title)
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $new_instance The new instance options
+	 * @param array $old_instance The old instance options
+	 *
+	 * @return array the instance
+	 */
+	public function update( $new_instance, $old_instance ) {
+		$instance                            = $old_instance;
+		$instance['bp_nouveau_widget_title'] = (bool) $new_instance['bp_nouveau_widget_title'];
+
+		return $instance;
+	}
+
+	/**
+	 * Output the new support topic widget options form
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param $instance Instance
+	 *
+	 * @return string HTML Output
+	 */
+	public function form( $instance ) {
+		$defaults = array(
+			'bp_nouveau_widget_title' => true,
+		);
+
+		$instance = bp_parse_args(
+			(array) $instance,
+			$defaults,
+			'widget_object_nav_form'
+		);
+
+		$bp_nouveau_widget_title = (bool) $instance['bp_nouveau_widget_title'];
+		?>
+
+		<p>
+			<input class="checkbox" type="checkbox" <?php checked( $bp_nouveau_widget_title, true ); ?> id="<?php echo $this->get_field_id( 'bp_nouveau_widget_title' ); ?>" name="<?php echo $this->get_field_name( 'bp_nouveau_widget_title' ); ?>" />
+			<label for="<?php echo $this->get_field_id( 'bp_nouveau_widget_title' ); ?>"><?php esc_html_e( 'Include navigation title', 'buddypress' ); ?></label>
+		</p>
+
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/customizer-controls.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/customizer-controls.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/customizer-controls.php	(revision 12265)
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Customizer controls
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * This control let users change the order of the BuddyPress
+ * single items navigation items.
+ *
+ * NB: this is a first pass to improve by using Javascript templating as explained here:
+ * https://developer.wordpress.org/themes/advanced-topics/customizer-api/#putting-the-pieces-together
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Nav_Customize_Control extends WP_Customize_Control {
+	/**
+	 * @var string
+	 */
+	public $type = '';
+
+	/**
+	 * Render the control's content.
+	 *
+	 * @since 3.0.0
+	 */
+	public function render_content() {
+		$id       = 'customize-control-' . str_replace( '[', '-', str_replace( ']', '', $this->id ) );
+		$class    = 'customize-control customize-control-' . $this->type;
+		$setting  = "bp_nouveau_appearance[{$this->type}_nav_order]";
+		$item_nav = array();
+
+		// It's a group
+		if ( 'group' === $this->type ) {
+			$guide = __( 'Customizing the Groups navigation order needs you create at least one group first.', 'buddypress' );
+
+			// Try to fetch any random group:
+			$random = groups_get_groups(
+				array(
+					'type'        => 'random',
+					'per_page'    => 1,
+					'show_hidden' => true,
+				)
+			);
+
+			if ( ! empty( $random['groups'] ) ) {
+				$group    = reset( $random['groups'] );
+				$nav      = new BP_Nouveau_Customizer_Group_Nav( $group->id );
+				$item_nav = $nav->get_group_nav();
+			}
+
+			if ( $item_nav ) {
+				$guide = __( 'Drag each possible group navigation items that are listed below into the order you prefer, in some groups some of these navigation items might not be active.', 'buddypress' );
+			}
+
+		// It's a user!
+		} else {
+			$item_nav = bp_nouveau_member_customizer_nav();
+
+			$guide = __( 'Drag each possible member navigation items that are listed below into the order you prefer.', 'buddypress' );
+		}
+		?>
+
+		<?php if ( isset( $guide ) ) : ?>
+			<p class="description">
+				<?php echo esc_html( $guide ); ?>
+			</p>
+		<?php endif; ?>
+
+		<?php if ( ! empty( $item_nav ) ) : ?>
+			<ul id="<?php echo esc_attr( $id ); ?>" class="ui-sortable" style="margin-top: 0px; height: 500px;" data-bp-type="<?php echo esc_attr( $this->type ); ?>">
+
+				<?php
+				$i = 0;
+				foreach ( $item_nav as $item ) :
+					$i += 1;
+				?>
+					<li data-bp-nav="<?php echo esc_attr( $item->slug ); ?>">
+						<div class="menu-item-bar">
+							<div class="menu-item-handle ui-sortable-handle">
+								<span class="item-title" aria-hidden="true">
+									<span class="menu-item-title"><?php echo esc_html( _bp_strip_spans_from_title( $item->name ) ); ?></span>
+								</span>
+							</div>
+						</div>
+					</li>
+				<?php endforeach; ?>
+
+			</ul>
+		<?php endif; ?>
+
+			<input id="<?php echo esc_attr( 'bp_item_' . $this->type ); ?>" type="hidden" value="" data-customize-setting-link="<?php echo esc_attr( $setting ); ?>" />
+
+		<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/customizer.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/customizer.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/customizer.php	(revision 12265)
@@ -0,0 +1,333 @@
+<?php
+/**
+ * Code to hook into the WP Customizer
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+/**
+ * Add a specific panel for the BP Nouveau Template Pack.
+ *
+ * @since 3.0.0
+ *
+ * @param WP_Customize_Manager $wp_customize WordPress customizer.
+ */
+function bp_nouveau_customize_register( WP_Customize_Manager $wp_customize ) {
+	if ( ! bp_is_root_blog() ) {
+		return;
+	}
+
+	require_once( trailingslashit( bp_nouveau()->includes_dir ) . 'customizer-controls.php' );
+	$wp_customize->register_control_type( 'BP_Nouveau_Nav_Customize_Control' );
+	$bp_nouveau_options = bp_nouveau_get_appearance_settings();
+
+	$wp_customize->add_panel( 'bp_nouveau_panel', array(
+		'description' => __( 'Customize the appearance of BuddyPress Nouveau Template pack.', 'buddypress' ),
+		'title'       => _x( 'BuddyPress Nouveau', 'Customizer Panel', 'buddypress' ),
+		'priority'    => 200,
+	) );
+
+	/**
+	 * Filters the BuddyPress Nouveau customizer sections and their arguments.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value Array of Customizer sections.
+	 */
+	$sections = apply_filters( 'bp_nouveau_customizer_sections', array(
+		'bp_nouveau_general_settings' => array(
+			'title'       => __( 'General BP Settings', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 10,
+			'description' => __( 'Configure general BuddyPress appearance options.', 'buddypress' ),
+		),
+		'bp_nouveau_user_front_page' => array(
+			'title'       => __( 'Member front page', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 30,
+			'description' => __( 'Configure the default front page for members.', 'buddypress' ),
+		),
+		'bp_nouveau_user_primary_nav' => array(
+			'title'       => __( 'Member navigation', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 50,
+			'description' => __( 'Customize the navigation menu for members. In the preview window, navigate to a user to preview your changes.', 'buddypress' ),
+		),
+		'bp_nouveau_loops_layout' => array(
+			'title'       => __( 'Loop layouts', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 70,
+			'description' => __( 'Set the number of columns to use for BuddyPress loops.', 'buddypress' ),
+		),
+		'bp_nouveau_dir_layout' => array(
+			'title'       => __( 'Directory layouts', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 80,
+			'description' => __( 'Select the layout style for directory content &amp; navigation.', 'buddypress' ),
+		),
+	) );
+
+	// Add the sections to the customizer
+	foreach ( $sections as $id_section => $section_args ) {
+		$wp_customize->add_section( $id_section, $section_args );
+	}
+
+	/**
+	 * Filters the BuddyPress Nouveau customizer settings and their arguments.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value Array of Customizer settings.
+	 */
+	$settings = apply_filters( 'bp_nouveau_customizer_settings', array(
+		'bp_nouveau_appearance[avatar_style]' => array(
+			'index'             => 'avatar_style',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[user_front_page]' => array(
+			'index'             => 'user_front_page',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[user_front_bio]' => array(
+			'index'             => 'user_front_bio',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[user_nav_display]' => array(
+			'index'             => 'user_nav_display',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[user_nav_tabs]' => array(
+			'index'             => 'user_nav_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[user_subnav_tabs]' => array(
+			'index'             => 'user_subnav_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[user_nav_order]' => array(
+			'index'             => 'user_nav_order',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'bp_nouveau_sanitize_nav_order',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[members_layout]' => array(
+			'index'             => 'members_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[members_group_layout]' => array(
+			'index'             => 'members_group_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[members_friends_layout]' => array(
+			'index'             => 'members_friends_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[activity_dir_layout]' => array(
+			'index'             => 'activity_dir_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[activity_dir_tabs]' => array(
+			'index'             => 'activity_dir_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[members_dir_layout]' => array(
+			'index'             => 'members_dir_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[members_dir_tabs]' => array(
+			'index'             => 'members_dir_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[groups_dir_layout]' => array(
+			'index'             => 'groups_dir_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[sites_dir_layout]' => array(
+			'index'             => 'sites_dir_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[sites_dir_tabs]' => array(
+			'index'             => 'sites_dir_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+	) );
+
+	// Add the settings
+	foreach ( $settings as $id_setting => $setting_args ) {
+		$args = array();
+
+		if ( empty( $setting_args['index'] ) || ! isset( $bp_nouveau_options[ $setting_args['index'] ] ) ) {
+			continue;
+		}
+
+		$args = array_merge( $setting_args, array( 'default' => $bp_nouveau_options[ $setting_args['index'] ] ) );
+
+		$wp_customize->add_setting( $id_setting, $args );
+	}
+
+	$controls = array(
+		'bp_site_avatars' => array(
+			'label'      => __( 'Use the round style for member and group avatars.', 'buddypress' ),
+			'section'    => 'bp_nouveau_general_settings',
+			'settings'   => 'bp_nouveau_appearance[avatar_style]',
+			'type'       => 'checkbox',
+		),
+		'user_front_page' => array(
+			'label'      => __( 'Enable default front page for member profiles.', 'buddypress' ),
+			'section'    => 'bp_nouveau_user_front_page',
+			'settings'   => 'bp_nouveau_appearance[user_front_page]',
+			'type'       => 'checkbox',
+		),
+		'user_front_bio' => array(
+			'label'      => __( 'Display the biographical info from the member\'s WordPress profile.', 'buddypress' ),
+			'section'    => 'bp_nouveau_user_front_page',
+			'settings'   => 'bp_nouveau_appearance[user_front_bio]',
+			'type'       => 'checkbox',
+		),
+		'user_nav_display' => array(
+			'label'      => __( 'Display the member navigation vertically.', 'buddypress' ),
+			'section'    => 'bp_nouveau_user_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[user_nav_display]',
+			'type'       => 'checkbox',
+		),
+		'user_nav_tabs' => array(
+			'label'      => __( 'Use tab styling for primary nav.', 'buddypress' ),
+			'section'    => 'bp_nouveau_user_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[user_nav_tabs]',
+			'type'       => 'checkbox',
+		),
+		'user_subnav_tabs' => array(
+			'label'      => __( 'Use tab styling for secondary nav.', 'buddypress' ),
+			'section'    => 'bp_nouveau_user_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[user_subnav_tabs]',
+			'type'       => 'checkbox',
+		),
+		'user_nav_order' => array(
+			'class'      => 'BP_Nouveau_Nav_Customize_Control',
+			'label'      => __( 'Reorder the primary navigation for a user.', 'buddypress' ),
+			'section'    => 'bp_nouveau_user_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[user_nav_order]',
+			'type'       => 'user',
+		),
+		'members_layout' => array(
+			'label'      => __( 'Members', 'buddypress' ),
+			'section'    => 'bp_nouveau_loops_layout',
+			'settings'   => 'bp_nouveau_appearance[members_layout]',
+			'type'       => 'select',
+			'choices'    => bp_nouveau_customizer_grid_choices(),
+		),
+		'members_friends_layout' => array(
+			'label'      => __( 'Member > Friends', 'buddypress' ),
+			'section'    => 'bp_nouveau_loops_layout',
+			'settings'   => 'bp_nouveau_appearance[members_friends_layout]',
+			'type'       => 'select',
+			'choices'    => bp_nouveau_customizer_grid_choices(),
+		),
+		'members_dir_layout' => array(
+			'label'      => __( 'Use column navigation for the Members directory.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[members_dir_layout]',
+			'type'       => 'checkbox',
+		),
+		'members_dir_tabs' => array(
+			'label'      => __( 'Use tab styling for Members directory navigation.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[members_dir_tabs]',
+			'type'       => 'checkbox',
+		),
+	);
+
+	/**
+	 * Filters the BuddyPress Nouveau customizer controls and their arguments.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value Array of Customizer controls.
+	 */
+	$controls = apply_filters( 'bp_nouveau_customizer_controls', $controls );
+
+	// Add the controls to the customizer's section
+	foreach ( $controls as $id_control => $control_args ) {
+		if ( empty( $control_args['class'] ) ) {
+			$wp_customize->add_control( $id_control, $control_args );
+		} else {
+			$wp_customize->add_control( new $control_args['class']( $wp_customize, $id_control, $control_args ) );
+		}
+	}
+}
+add_action( 'bp_customize_register', 'bp_nouveau_customize_register', 10, 1 );
+
+/**
+ * Enqueue needed JS for our customizer Settings & Controls
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_customizer_enqueue_scripts() {
+	$min = bp_core_get_minified_asset_suffix();
+
+	wp_enqueue_script(
+		'bp-nouveau-customizer',
+		trailingslashit( bp_get_theme_compat_url() ) . "js/customizer{$min}.js",
+		array( 'jquery', 'jquery-ui-sortable', 'customize-controls', 'iris', 'underscore', 'wp-util' ),
+		bp_nouveau()->version,
+		true
+	);
+
+	/**
+	 * Fires after Nouveau enqueues its required javascript.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_nouveau_customizer_enqueue_scripts' );
+}
+add_action( 'customize_controls_enqueue_scripts', 'bp_nouveau_customizer_enqueue_scripts' );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/friends/ajax.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/friends/ajax.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/friends/ajax.php	(revision 12265)
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Friends Ajax functions
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_action( 'admin_init', function() {
+	$ajax_actions = array(
+		array(
+			'friends_remove_friend' => array(
+				'function' => 'bp_nouveau_ajax_addremove_friend',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'friends_add_friend' => array(
+				'function' => 'bp_nouveau_ajax_addremove_friend',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'friends_withdraw_friendship' => array(
+				'function' => 'bp_nouveau_ajax_addremove_friend',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'friends_accept_friendship' => array(
+				'function' => 'bp_nouveau_ajax_addremove_friend',
+				'nopriv'   => false,
+			),
+		),
+		array(
+			'friends_reject_friendship' => array(
+				'function' => 'bp_nouveau_ajax_addremove_friend',
+				'nopriv'   => false,
+			),
+		),
+	);
+
+	foreach ( $ajax_actions as $ajax_action ) {
+		$action = key( $ajax_action );
+
+		add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+		if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+			add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+		}
+	}
+}, 12 );
+
+/**
+ * Friend/un-friend a user via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML
+ */
+function bp_nouveau_ajax_addremove_friend() {
+	$response = array(
+		'feedback' => sprintf(
+			'<div class="bp-feedback error bp-ajax-message"><p>%s</p></div>',
+			esc_html__( 'There was a problem performing this action. Please try again.', 'buddypress' )
+		),
+	);
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['nonce'] ) || empty( $_POST['item_id'] ) || ! bp_is_active( 'friends' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Use default nonce
+	$nonce = $_POST['nonce'];
+	$check = 'bp_nouveau_friends';
+
+	// Use a specific one for actions needed it
+	if ( ! empty( $_POST['_wpnonce'] ) && ! empty( $_POST['action'] ) ) {
+		$nonce = $_POST['_wpnonce'];
+		$check = $_POST['action'];
+	}
+
+	// Nonce check!
+	if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $check ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Cast fid as an integer.
+	$friend_id = (int) $_POST['item_id'];
+
+	// Check if the user exists only when the Friend ID is not a Frienship ID.
+	if ( isset( $_POST['action'] ) && $_POST['action'] !== 'friends_accept_friendship' && $_POST['action'] !== 'friends_reject_friendship' ) {
+		$user = get_user_by( 'id', $friend_id );
+		if ( ! $user ) {
+			wp_send_json_error(
+				array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback error">%s</div>',
+						esc_html__( 'No member found by that ID.', 'buddypress' )
+					),
+				)
+			);
+		}
+	}
+
+	// In the 2 first cases the $friend_id is a friendship id.
+	if ( ! empty( $_POST['action'] ) && 'friends_accept_friendship' === $_POST['action'] ) {
+		if ( ! friends_accept_friendship( $friend_id ) ) {
+			wp_send_json_error(
+				array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback error">%s</div>',
+						esc_html__( 'There was a problem accepting that request. Please try again.', 'buddypress' )
+					),
+				)
+			);
+		} else {
+			wp_send_json_success(
+				array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback success">%s</div>',
+						esc_html__( 'Friendship accepted.', 'buddypress' )
+					),
+					'type'     => 'success',
+					'is_user'  => true,
+				)
+			);
+		}
+
+	// Rejecting a friendship
+	} elseif ( ! empty( $_POST['action'] ) && 'friends_reject_friendship' === $_POST['action'] ) {
+		if ( ! friends_reject_friendship( $friend_id ) ) {
+			wp_send_json_error(
+				array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback error">%s</div>',
+						esc_html__( 'There was a problem rejecting that request. Please try again.', 'buddypress' )
+					),
+				)
+			);
+		} else {
+			wp_send_json_success(
+				array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback success">%s</div>',
+						esc_html__( 'Friendship rejected.', 'buddypress' )
+					),
+					'type'     => 'success',
+					'is_user'  => true,
+				)
+			);
+		}
+
+	// Trying to cancel friendship.
+	} elseif ( 'is_friend' === BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $friend_id ) ) {
+		if ( ! friends_remove_friend( bp_loggedin_user_id(), $friend_id ) ) {
+			$response['feedback'] = sprintf(
+				'<div class="bp-feedback error">%s</div>',
+				esc_html__( 'Friendship could not be cancelled.', 'buddypress' )
+			);
+
+			wp_send_json_error( $response );
+		} else {
+			$is_user = bp_is_my_profile();
+
+			if ( ! $is_user ) {
+				$response = array( 'contents' => bp_get_add_friend_button( $friend_id ) );
+			} else {
+				$response = array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback success">%s</div>',
+						esc_html__( 'Friendship cancelled.', 'buddypress' )
+					),
+					'type'     => 'success',
+					'is_user'  => $is_user,
+				);
+			}
+
+			wp_send_json_success( $response );
+		}
+
+	// Trying to request friendship.
+	} elseif ( 'not_friends' === BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $friend_id ) ) {
+		if ( ! friends_add_friend( bp_loggedin_user_id(), $friend_id ) ) {
+			$response['feedback'] = sprintf(
+				'<div class="bp-feedback error">%s</div>',
+				esc_html__( 'Friendship could not be requested.', 'buddypress' )
+			);
+
+			wp_send_json_error( $response );
+		} else {
+			wp_send_json_success( array( 'contents' => bp_get_add_friend_button( $friend_id ) ) );
+		}
+
+	// Trying to cancel pending request.
+	} elseif ( 'pending' === BP_Friends_Friendship::check_is_friend( bp_loggedin_user_id(), $friend_id ) ) {
+		if ( friends_withdraw_friendship( bp_loggedin_user_id(), $friend_id ) ) {
+			wp_send_json_success( array( 'contents' => bp_get_add_friend_button( $friend_id ) ) );
+		} else {
+			$response['feedback'] = sprintf(
+				'<div class="bp-feedback error">%s</div>',
+				esc_html__( 'Friendship request could not be cancelled.', 'buddypress' )
+			);
+
+			wp_send_json_error( $response );
+		}
+
+	// Request already pending.
+	} else {
+		$response['feedback'] = sprintf(
+			'<div class="bp-feedback error">%s</div>',
+			esc_html__( 'Request Pending', 'buddypress' )
+		);
+
+		wp_send_json_error( $response );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/friends/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/friends/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/friends/loader.php	(revision 12265)
@@ -0,0 +1,129 @@
+<?php
+/**
+ * BP Nouveau Friends
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Friends Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Friends {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir = trailingslashit( dirname( __FILE__ ) );
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		// Test suite requires the AJAX functions early.
+		if ( function_exists( 'tests_add_filter' ) ) {
+			require $this->dir . 'ajax.php';
+
+		// Load AJAX code only on AJAX requests.
+		} else {
+			add_action( 'admin_init', function() {
+				if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && 0 === strpos( $_REQUEST['action'], 'friends_' ) ) {
+					require bp_nouveau()->friends->dir . 'ajax.php';
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		// Remove BuddyPress action for the members loop
+		remove_action( 'bp_directory_members_actions', 'bp_member_add_friend_button' );
+
+		// Register the friends Notifications filters
+		add_action( 'bp_nouveau_notifications_init_filters', array( $this, 'notification_filters' ) );
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		$buttons = array(
+			'friends_pending',
+			'friends_is_friend',
+			'friends_not_friends',
+			'friends_member_friendship',
+			'friends_accept_friendship',
+			'friends_reject_friendship',
+		);
+
+		foreach ( $buttons as $button ) {
+			add_filter( 'bp_button_' . $button, 'bp_nouveau_ajax_button', 10, 5 );
+		}
+	}
+
+	/**
+	 * Register notifications filters for the friends component.
+	 *
+	 * @since 3.0.0
+	 */
+	public function notification_filters() {
+		$notifications = array(
+			array(
+				'id'       => 'friendship_accepted',
+				'label'    => __( 'Accepted friendship requests', 'buddypress' ),
+				'position' => 35,
+			),
+			array(
+				'id'       => 'friendship_request',
+				'label'    => __( 'Pending friendship requests', 'buddypress' ),
+				'position' => 45,
+			),
+		);
+
+		foreach ( $notifications as $notification ) {
+			bp_nouveau_notifications_register_filter( $notification );
+		}
+	}
+}
+
+/**
+ * Launch the Friends loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_friends( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->friends = new BP_Nouveau_Friends();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_friends', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/functions.php	(revision 12265)
@@ -0,0 +1,1393 @@
+<?php
+/**
+ * Common functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * This function looks scarier than it actually is. :)
+ * Each object loop (activity/members/groups/blogs/forums) contains default
+ * parameters to show specific information based on the page we are currently
+ * looking at.
+ *
+ * The following function will take into account any cookies set in the JS and
+ * allow us to override the parameters sent. That way we can change the results
+ * returned without reloading the page.
+ *
+ * By using cookies we can also make sure that user settings are retained
+ * across page loads.
+ *
+ * @since 3.0.0
+ *
+ * @param string $query_string Query string for the current request.
+ * @param string $object       Object for cookie.
+ *
+ * @return string Query string for the component loops
+ */
+function bp_nouveau_ajax_querystring( $query_string, $object ) {
+	if ( empty( $object ) ) {
+		return '';
+	}
+
+	// Default query
+	$post_query = array(
+		'filter'       => '',
+		'scope'        => 'all',
+		'page'         => 1,
+		'search_terms' => '',
+		'extras'       => '',
+	);
+
+	if ( ! empty( $_POST ) ) {
+		$post_query = bp_parse_args(
+			$_POST,
+			$post_query,
+			'nouveau_ajax_querystring'
+		);
+
+		// Make sure to transport the scope, filter etc.. in HeartBeat Requests
+		if ( ! empty( $post_query['data']['bp_heartbeat'] ) ) {
+			$bp_heartbeat = $post_query['data']['bp_heartbeat'];
+
+			// Remove heartbeat specific vars
+			$post_query = array_diff_key(
+				bp_parse_args(
+					$bp_heartbeat,
+					$post_query,
+					'nouveau_ajax_querystring_heartbeat'
+				),
+				array(
+					'data'      => false,
+					'interval'  => false,
+					'_nonce'    => false,
+					'action'    => false,
+					'screen_id' => false,
+					'has_focus' => false,
+				)
+			);
+		}
+	}
+
+	// Init the query string
+	$qs = array();
+
+	// Activity stream filtering on action.
+	if ( ! empty( $post_query['filter'] ) && '-1' !== $post_query['filter'] ) {
+		if ( 'notifications' === $object ) {
+			$qs[] = 'component_action=' . $post_query['filter'];
+		} else {
+			$qs[] = 'type=' . $post_query['filter'];
+			$qs[] = 'action=' . $post_query['filter'];
+		}
+	}
+
+	// Sort the notifications if needed
+	if ( ! empty( $post_query['extras'] ) && 'notifications' === $object ) {
+		$qs[] = 'sort_order=' . $post_query['extras'];
+	}
+
+	if ( 'personal' === $post_query['scope'] ) {
+		$user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
+		$qs[]    = 'user_id=' . $user_id;
+	}
+
+	// Activity stream scope only on activity directory.
+	if ( 'all' !== $post_query['scope'] && ! bp_displayed_user_id() && ! bp_is_single_item() ) {
+		$qs[] = 'scope=' . $post_query['scope'];
+	}
+
+	// If page have been passed via the AJAX post request, use those.
+	if ( '-1' != $post_query['page'] ) {
+		$qs[] = 'page=' . absint( $post_query['page'] );
+	}
+
+	// Excludes activity just posted and avoids duplicate ids.
+	if ( ! empty( $post_query['exclude_just_posted'] ) ) {
+		$just_posted = wp_parse_id_list( $post_query['exclude_just_posted'] );
+		$qs[]        = 'exclude=' . implode( ',', $just_posted );
+	}
+
+	// To get newest activities.
+	if ( ! empty( $post_query['offset'] ) ) {
+		$qs[] = 'offset=' . intval( $post_query['offset'] );
+	}
+
+	$object_search_text = bp_get_search_default_text( $object );
+	if ( ! empty( $post_query['search_terms'] ) && $object_search_text != $post_query['search_terms'] && 'false' != $post_query['search_terms'] && 'undefined' != $post_query['search_terms'] ) {
+		$qs[] = 'search_terms=' . urlencode( $_POST['search_terms'] );
+	}
+
+	// Specific to messages
+	if ( 'messages' === $object ) {
+		if ( ! empty( $post_query['box'] ) ) {
+			$qs[] = 'box=' . $post_query['box'];
+		}
+	}
+
+	// Single activity.
+	if ( bp_is_single_activity() ) {
+		$qs = array(
+			'display_comments=threaded',
+			'show_hidden=true',
+			'include=' . bp_current_action(),
+		);
+	}
+
+	// Now pass the querystring to override default values.
+	$query_string = empty( $qs ) ? '' : join( '&', (array) $qs );
+
+	// List the variables for the filter
+	list( $filter, $scope, $page, $search_terms, $extras ) = array_values( $post_query );
+
+	/**
+	 * Filters the AJAX query string for the component loops.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $query_string The query string we are working with.
+	 * @param string $object       The type of page we are on.
+	 * @param string $filter       The current object filter.
+	 * @param string $scope        The current object scope.
+	 * @param string $page         The current object page.
+	 * @param string $search_terms The current object search terms.
+	 * @param string $extras       The current object extras.
+	 */
+	return apply_filters( 'bp_nouveau_ajax_querystring', $query_string, $object, $filter, $scope, $page, $search_terms, $extras );
+}
+
+/**
+ * @since 3.0.0
+ *
+ * @return string
+ */
+function bp_nouveau_ajax_button( $output = '', $button = null, $before = '', $after = '', $r = array() ) {
+	if ( empty( $button->component ) ) {
+		return $output;
+	}
+
+	// Custom data attribute.
+	$r['button_attr']['data-bp-btn-action'] = $button->id;
+
+	$reset_ids = array(
+		'member_friendship' => true,
+		'group_membership'  => true,
+	);
+
+	if ( ! empty( $reset_ids[ $button->id ] ) )  {
+		$parse_class = array_map( 'sanitize_html_class', explode( ' ', $r['button_attr']['class'] ) );
+		if ( false === $parse_class ) {
+			return $output;
+		}
+
+		$find_id = array_intersect( $parse_class, array(
+			'pending_friend',
+			'is_friend',
+			'not_friends',
+			'leave-group',
+			'join-group',
+			'accept-invite',
+			'membership-requested',
+			'request-membership',
+		) );
+
+		if ( 1 !== count( $find_id ) ) {
+			return $output;
+		}
+
+		$data_attribute = reset( $find_id );
+		if ( 'pending_friend' === $data_attribute ) {
+			$data_attribute = str_replace( '_friend', '', $data_attribute );
+		} elseif ( 'group_membership' === $button->id ) {
+			$data_attribute = str_replace( '-', '_', $data_attribute );
+		}
+
+		$r['button_attr']['data-bp-btn-action'] = $data_attribute;
+	}
+
+	// Re-render the button with our custom data attribute.
+	$output = new BP_Core_HTML_Element( array(
+		'element'    => $r['button_element'],
+		'attr'       => $r['button_attr'],
+		'inner_html' => ! empty( $r['link_text'] ) ? $r['link_text'] : ''
+	) );
+	$output = $output->contents();
+
+	// Add span bp-screen-reader-text class
+	return $before . $output . $after;
+}
+
+/**
+ * Output HTML content into a wrapper.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args {
+ *     Optional arguments.
+ *
+ *     @type string $container         String HTML container type that should wrap
+ *                                     the items as a group: 'div', 'ul', or 'p'. Required.
+ *     @type string $container_id      The group wrapping container element ID
+ *     @type string $container_classes The group wrapping container elements class
+ *     @type string $output            The HTML to output. Required.
+ * }
+ */
+function bp_nouveau_wrapper( $args = array() ) {
+ /**
+	* Classes need to be determined & set by component to a certain degree
+	*
+	* Check the component to find a default container_class to add
+	*/
+	$current_component_class = bp_current_component() . '-meta';
+
+	if ( bp_is_group_activity() ) {
+		$generic_class = ' activity-meta ';
+	} else {
+		$generic_class = '';
+	}
+
+	$r = bp_parse_args(
+		$args,
+		array(
+			'container'         => 'div',
+			'container_id'      => '',
+			'container_classes' => array( $generic_class, $current_component_class   ),
+			'output'            => '',
+		),
+		'nouveau_wrapper'
+	);
+
+	$valid_containers = array(
+		'div'  => true,
+		'ul'   => true,
+		'ol'   => true,
+		'span' => true,
+		'p'    => true,
+	);
+
+	// Actually merge some classes defaults and $args
+	// @todo This is temp, we need certain classes but maybe improve this approach.
+	$default_classes        = array( 'action' );
+	$r['container_classes'] = array_merge( $r['container_classes'], $default_classes );
+
+	if ( empty( $r['container'] ) || ! isset( $valid_containers[ $r['container'] ] ) || empty( $r['output'] ) ) {
+		return;
+	}
+
+	$container         = $r['container'];
+	$container_id      = '';
+	$container_classes = '';
+	$output            = $r['output'];
+
+	if ( ! empty( $r['container_id'] ) ) {
+		$container_id = ' id="' . esc_attr( $r['container_id'] ) . '"';
+	}
+
+	if ( ! empty( $r['container_classes'] ) && is_array( $r['container_classes'] ) ) {
+		$container_classes = ' class="' . join( ' ', array_map( 'sanitize_html_class', $r['container_classes'] ) ) . '"';
+	}
+
+	// Print the wrapper and its content.
+	printf( '<%1$s%2$s%3$s>%4$s</%1$s>', $container, $container_id, $container_classes, $output );
+}
+
+/**
+ * Register the 2 sidebars for the Group & User default front page
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_register_sidebars() {
+	$default_fronts      = bp_nouveau_get_appearance_settings();
+	$default_user_front  = 0;
+	$default_group_front = 0;
+	$is_active_groups    = bp_is_active( 'groups' );
+
+	if ( isset( $default_fronts['user_front_page'] ) ) {
+		$default_user_front = $default_fronts['user_front_page'];
+	}
+
+	if ( $is_active_groups ) {
+		if ( isset( $default_fronts['group_front_page'] ) ) {
+			$default_group_front = $default_fronts['group_front_page'];
+		}
+	}
+
+	// Setting the front template happens too early, so we need this!
+	if ( is_customize_preview() ) {
+		$default_user_front = bp_nouveau_get_temporary_setting( 'user_front_page', $default_user_front );
+
+		if ( $is_active_groups ) {
+			$default_group_front = bp_nouveau_get_temporary_setting( 'group_front_page', $default_group_front );
+		}
+	}
+
+	$sidebars = array();
+	if ( $default_user_front ) {
+		$sidebars[] = array(
+			'name'          => __( 'BuddyPress Member\'s Home', 'buddypress' ),
+			'id'            => 'sidebar-buddypress-members',
+			'description'   => __( 'Add widgets here to appear in the front page of each member of your community.', 'buddypress' ),
+			'before_widget' => '<div id="%1$s" class="widget %2$s">',
+			'after_widget'  => '</div>',
+			'before_title'  => '<h2 class="widget-title">',
+			'after_title'   => '</h2>',
+		);
+	}
+
+	if ( $default_group_front ) {
+		$sidebars[] = array(
+			'name'          => __( 'BuddyPress Group\'s Home', 'buddypress' ),
+			'id'            => 'sidebar-buddypress-groups',
+			'description'   => __( 'Add widgets here to appear in the front page of each group of your community.', 'buddypress' ),
+			'before_widget' => '<div id="%1$s" class="widget %2$s">',
+			'after_widget'  => '</div>',
+			'before_title'  => '<h2 class="widget-title">',
+			'after_title'   => '</h2>',
+		);
+	}
+
+	if ( empty( $sidebars ) ) {
+		return;
+	}
+
+	// Register the sidebars if needed.
+	foreach ( $sidebars as $sidebar ) {
+		register_sidebar( $sidebar );
+	}
+}
+
+/**
+ * @since 3.0.0
+ *
+ * @return bool
+ */
+function bp_nouveau_is_object_nav_in_sidebar() {
+	return is_active_widget( false, false, 'bp_nouveau_sidebar_object_nav_widget', true );
+}
+
+/**
+ * @since 3.0.0
+ *
+ * @return bool
+ */
+function bp_nouveau_current_user_can( $capability = '' ) {
+	/**
+	 * Filters whether or not the current user can perform an action for BuddyPress Nouveau.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param bool   $value      Whether or not the user is logged in.
+	 * @param string $capability Current capability being checked.
+	 * @param int    $value      Current logged in user ID.
+	 */
+	return apply_filters( 'bp_nouveau_current_user_can', is_user_logged_in(), $capability, bp_loggedin_user_id() );
+}
+
+/**
+ * Parse an html output to a list of component's directory nav item.
+ *
+ * @since 3.0.0
+ *
+ * @param string $hook      The hook to fire.
+ * @param string $component The component nav belongs to.
+ * @param int    $position  The position of the nav item.
+ *
+ * @return array A list of component's dir nav items
+ */
+function bp_nouveau_parse_hooked_dir_nav( $hook = '', $component = '', $position = 99 ) {
+	$extra_nav_items = array();
+
+	if ( empty( $hook ) || empty( $component ) || ! has_action( $hook ) ) {
+		return $extra_nav_items;
+	}
+
+	// Get the hook output.
+	ob_start();
+
+	/**
+	 * Fires at the start of the output for `bp_nouveau_parse_hooked_dir_nav()`.
+	 *
+	 * This hook is variable and depends on the hook parameter passed in.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( $hook );
+	$output = ob_get_clean();
+
+	if ( empty( $output ) ) {
+		return $extra_nav_items;
+	}
+
+	preg_match_all( "/<li\sid=\"{$component}\-(.*)\"[^>]*>/siU", $output, $lis );
+	if ( empty( $lis[1] ) ) {
+		return $extra_nav_items;
+	}
+
+	$extra_nav_items = array_fill_keys( $lis[1], array( 'component' => $component, 'position' => $position ) );
+	preg_match_all( '/<a\s[^>]*>(.*)<\/a>/siU', $output, $as );
+
+	if ( ! empty( $as[0] ) ) {
+		foreach ( $as[0] as $ka => $a ) {
+			$extra_nav_items[ $lis[1][ $ka ] ]['slug'] = $lis[1][ $ka ];
+			$extra_nav_items[ $lis[1][ $ka ] ]['text'] = $as[1][ $ka ];
+			preg_match_all( '/([\w\-]+)=([^"\'> ]+|([\'"]?)(?:[^\3]|\3+)+?\3)/', $a, $attrs );
+
+			if ( ! empty( $attrs[1] ) ) {
+				foreach ( $attrs[1] as $katt => $att ) {
+					if ( 'href' === $att ) {
+						$extra_nav_items[ $lis[1][ $ka ] ]['link'] = trim( $attrs[2][ $katt ], '"' );
+					} else {
+						$extra_nav_items[ $lis[1][ $ka ] ][ $att ] = trim( $attrs[2][ $katt ], '"' );
+					}
+				}
+			}
+		}
+	}
+
+	if ( ! empty( $as[1] ) ) {
+		foreach ( $as[1] as $ks => $s ) {
+			preg_match_all( '/<span>(.*)<\/span>/siU', $s, $spans );
+
+			if ( empty( $spans[0] ) ) {
+				$extra_nav_items[ $lis[1][ $ks ] ]['count'] = false;
+			} elseif ( ! empty( $spans[1][0] ) ) {
+				$extra_nav_items[ $lis[1][ $ks ] ]['count'] = (int) $spans[1][0];
+			} else {
+				$extra_nav_items[ $lis[1][ $ks ] ]['count'] = '';
+			}
+		}
+	}
+
+	return $extra_nav_items;
+}
+
+/**
+ * Run specific "select filter" hooks to catch the options and build an array out of them
+ *
+ * @since 3.0.0
+ *
+ * @param string $hook
+ * @param array  $filters
+ *
+ * @return array
+ */
+function bp_nouveau_parse_hooked_options( $hook = '', $filters = array() ) {
+	if ( empty( $hook ) ) {
+		return $filters;
+	}
+
+	ob_start();
+
+	/**
+	 * Fires at the start of the output for `bp_nouveau_parse_hooked_options()`.
+	 *
+	 * This hook is variable and depends on the hook parameter passed in.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( $hook );
+
+	$output = ob_get_clean();
+
+	preg_match_all( '/<option value="(.*?)"\s*>(.*?)<\/option>/', $output, $matches );
+
+	if ( ! empty( $matches[1] ) && ! empty( $matches[2] ) ) {
+		foreach ( $matches[1] as $ik => $key_action ) {
+			if ( ! empty( $matches[2][ $ik ] ) && ! isset( $filters[ $key_action ] ) ) {
+				$filters[ $key_action ] = $matches[2][ $ik ];
+			}
+		}
+	}
+
+	return $filters;
+}
+
+/**
+ * Get Dropdawn filters for the current component of the one passed in params
+ *
+ * @since 3.0.0
+ *
+ * @param string $context   'directory', 'user' or 'group'
+ * @param string $component The BuddyPress component ID
+ *
+ * @return array the dropdown filters
+ */
+function bp_nouveau_get_component_filters( $context = '', $component = '' ) {
+	$filters = array();
+
+	if ( empty( $context ) ) {
+		if ( bp_is_user() ) {
+			$context = 'user';
+		} elseif ( bp_is_group() ) {
+			$context = 'group';
+
+		// Defaults to directory
+		} else {
+			$context = 'directory';
+		}
+	}
+
+	if ( empty( $component ) ) {
+		if ( 'directory' === $context || 'user' === $context ) {
+			$component = bp_current_component();
+
+			if ( 'friends' === $component ) {
+				$context   = 'friends';
+				$component = 'members';
+			}
+		} elseif ( 'group' === $context && bp_is_group_activity() ) {
+			$component = 'activity';
+		} elseif ( 'group' === $context && bp_is_group_members() ) {
+			$component = 'members';
+		}
+	}
+
+	if ( ! bp_is_active( $component ) ) {
+		return $filters;
+	}
+
+	if ( 'members' === $component ) {
+		$filters = bp_nouveau_get_members_filters( $context );
+	} elseif ( 'activity' === $component ) {
+		$filters = bp_nouveau_get_activity_filters();
+
+		// Specific case for the activity dropdown
+		$filters = array_merge( array( '-1' => __( '&mdash; Everything &mdash;', 'buddypress' ) ), $filters );
+	} elseif ( 'groups' === $component ) {
+		$filters = bp_nouveau_get_groups_filters( $context );
+	} elseif ( 'blogs' === $component ) {
+		$filters = bp_nouveau_get_blogs_filters( $context );
+	}
+
+	return $filters;
+}
+
+/**
+ * When previewing make sure to get the temporary setting of the customizer.
+ * This is necessary when we need to get these very early.
+ *
+ * @since 3.0.0
+ *
+ * @param string $option the index of the setting to get.
+ * @param mixed  $retval the value to use as default.
+ *
+ * @return mixed The value for the requested option.
+ */
+function bp_nouveau_get_temporary_setting( $option = '', $retval = false ) {
+	if ( empty( $option ) || ! isset( $_POST['customized'] ) ) {
+		return $retval;
+	}
+
+	$temporary_setting = wp_unslash( $_POST['customized'] );
+	if ( ! is_array( $temporary_setting ) ) {
+		$temporary_setting = json_decode( $temporary_setting, true );
+	}
+
+	// This is used to transport the customizer settings into Ajax requests.
+	if ( 'any' === $option ) {
+		$retval = array();
+
+		foreach ( $temporary_setting as $key => $setting ) {
+			if ( 0 !== strpos( $key, 'bp_nouveau_appearance' ) ) {
+				continue;
+			}
+
+			$k            = str_replace( array( '[', ']' ), array( '_', '' ), $key );
+			$retval[ $k ] = $setting;
+		}
+
+	// Used when it's an early regular request
+	} elseif ( isset( $temporary_setting[ 'bp_nouveau_appearance[' . $option . ']' ] ) ) {
+		$retval = $temporary_setting[ 'bp_nouveau_appearance[' . $option . ']' ];
+
+	// Used when it's an ajax request
+	} elseif ( isset( $_POST['customized'][ 'bp_nouveau_appearance_' . $option ] ) ) {
+		$retval = $_POST['customized'][ 'bp_nouveau_appearance_' . $option ];
+	}
+
+	return $retval;
+}
+
+/**
+ * Get the BP Nouveau Appearance settings.
+ *
+ * @since 3.0.0
+ *
+ * @param string $option Leave empty to get all settings, specify a value for a specific one.
+ * @param mixed          An array of settings, the value of the requested setting.
+ *
+ * @return array|false|mixed
+ */
+function bp_nouveau_get_appearance_settings( $option = '' ) {
+	$default_args = array(
+		'avatar_style'       => 0,
+		'user_front_page'    => 1,
+		'user_front_bio'     => 0,
+		'user_nav_display'   => 0, // O is default (horizontally). 1 is vertically.
+		'user_nav_tabs'      => 0,
+		'user_subnav_tabs'   => 0,
+		'user_nav_order'     => array(),
+		'members_layout'     => 1,
+		'members_dir_tabs'   => 0,
+		'members_dir_layout' => 0,
+	);
+
+	if ( bp_is_active( 'friends' ) ) {
+		$default_args['members_friends_layout'] = 1;
+	}
+
+	if ( bp_is_active( 'activity' ) ) {
+		$default_args['activity_dir_layout'] = 0;
+		$default_args['activity_dir_tabs']   = 0; // default = no tabs
+	}
+
+	if ( bp_is_active( 'groups' ) ) {
+		$default_args = array_merge( $default_args, array(
+			'group_front_page'        => 1,
+			'group_front_boxes'       => 1,
+			'group_front_description' => 0,
+			'group_nav_display'       => 0,       // O is default (horizontally). 1 is vertically.
+			'group_nav_order'         => array(),
+			'group_nav_tabs'          => 0,
+			'group_subnav_tabs'       => 0,
+			'groups_create_tabs'      => 1,
+			'groups_layout'           => 1,
+			'members_group_layout'    => 1,
+			'groups_dir_layout'       => 0,
+			'groups_dir_tabs'         => 0,
+		) );
+	}
+
+	if ( is_multisite() && bp_is_active( 'blogs' ) ) {
+		$default_args = array_merge( $default_args, array(
+			'sites_dir_layout' => 0,
+			'sites_dir_tabs'   => 0,
+		) );
+	}
+
+	$settings = bp_parse_args(
+		bp_get_option( 'bp_nouveau_appearance', array() ),
+		$default_args,
+		'nouveau_appearance_settings'
+	);
+
+	if ( ! empty( $option ) ) {
+		if ( isset( $settings[ $option ] ) ) {
+			return $settings[ $option ];
+		} else {
+			return false;
+		}
+	}
+
+	return $settings;
+}
+
+/**
+ * Returns the choices for the Layout option of the customizer
+ * or the list of corresponding css classes.
+ *
+ * @since 3.0.0
+ *
+ * @param string $type 'option' to get the labels, 'classes' to get the classes
+ *
+ * @return array The list of labels or classes preserving keys.
+ */
+function bp_nouveau_customizer_grid_choices( $type = 'option' ) {
+	$columns = array(
+		array( 'key' => '1', 'label' => __( 'One column', 'buddypress'    ), 'class' => ''      ),
+		array( 'key' => '2', 'label' => __( 'Two columns', 'buddypress'   ), 'class' => 'two'   ),
+		array( 'key' => '3', 'label' => __( 'Three columns', 'buddypress' ), 'class' => 'three' ),
+		array( 'key' => '4', 'label' => __( 'Four columns', 'buddypress'  ), 'class' => 'four'  ),
+	);
+
+	if ( 'option' === $type ) {
+		return wp_list_pluck( $columns, 'label', 'key' );
+	}
+
+	return wp_list_pluck( $columns, 'class', 'key' );
+}
+
+/**
+ * Sanitize a list of slugs to save it as an array
+ *
+ * @since 3.0.0
+ *
+ * @param  string $option A comma separated list of nav items slugs.
+ *
+ * @return array An array of nav items slugs.
+ */
+function bp_nouveau_sanitize_nav_order( $option = '' ) {
+	$option = explode( ',', $option );
+	return array_map( 'sanitize_key', $option );
+}
+
+/**
+ * BP Nouveau's callback for the cover image feature.
+ *
+ * @since 3.0.0
+ *
+ * @param array $params Optional. The current component's feature parameters.
+ *
+ * @return string
+ */
+function bp_nouveau_theme_cover_image( $params = array() ) {
+	if ( empty( $params ) ) {
+		return '';
+	}
+
+	// Avatar height - padding - 1/2 avatar height.
+	$avatar_offset = $params['height'] - 5 - round( (int) bp_core_avatar_full_height() / 2 );
+
+	// Header content offset + spacing.
+	$top_offset  = bp_core_avatar_full_height() - 10;
+	$left_offset = bp_core_avatar_full_width() + 20;
+
+	$cover_image = isset( $params['cover_image'] ) ? 'background-image: url( ' . $params['cover_image'] . ' );' : '';
+	$hide_avatar_style = '';
+
+	// Adjust the cover image header, in case avatars are completely disabled.
+	if ( ! buddypress()->avatar->show_avatars ) {
+		$hide_avatar_style = '
+			#buddypress #item-header-cover-image #item-header-avatar {
+				display:  none;
+			}
+		';
+
+		if ( bp_is_user() ) {
+			$hide_avatar_style = '
+				#buddypress #item-header-cover-image #item-header-avatar a {
+					display: block;
+					height: ' . $top_offset . 'px;
+					margin: 0 15px 19px 0;
+				}
+
+				#buddypress div#item-header #item-header-cover-image #item-header-content {
+					margin-left:auto;
+				}
+			';
+		}
+	}
+
+	return '
+		/* Cover image */
+		#buddypress #item-header-cover-image {
+			min-height: ' . $params['height'] . 'px;
+			margin-bottom: 1em;
+		}
+
+		#buddypress #item-header-cover-image:after {
+			clear: both;
+			content: "";
+			display: table;
+		}
+
+		#buddypress #header-cover-image {
+			height: ' . $params['height'] . 'px;
+			' . $cover_image . '
+		}
+
+		#buddypress #create-group-form #header-cover-image {
+			position: relative;
+			margin: 1em 0;
+		}
+
+		.bp-user #buddypress #item-header {
+			padding-top: 0;
+		}
+
+		#buddypress #item-header-cover-image #item-header-avatar {
+			margin-top: ' . $avatar_offset . 'px;
+			float: left;
+			overflow: visible;
+			width:auto;
+		}
+
+		#buddypress div#item-header #item-header-cover-image #item-header-content {
+			clear: both;
+			float: left;
+			margin-left: ' . $left_offset . 'px;
+			margin-top: -' . $top_offset . 'px;
+			width:auto;
+		}
+
+		body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content,
+		body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+			margin-top: ' . $params['height'] . 'px;
+			margin-left: 0;
+			clear: none;
+			max-width: 50%;
+		}
+
+		body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+			padding-top: 20px;
+			max-width: 20%;
+		}
+
+		' . $hide_avatar_style . '
+
+		#buddypress div#item-header-cover-image h2 a,
+		#buddypress div#item-header-cover-image h2 {
+			color: #FFF;
+			text-rendering: optimizelegibility;
+			text-shadow: 0px 0px 3px rgba( 0, 0, 0, 0.8 );
+			margin: 0 0 .6em;
+			font-size:200%;
+		}
+
+		#buddypress #item-header-cover-image #item-header-avatar img.avatar {
+			border: solid 2px #FFF;
+			background: rgba( 255, 255, 255, 0.8 );
+		}
+
+		#buddypress #item-header-cover-image #item-header-avatar a {
+			border: none;
+			text-decoration: none;
+		}
+
+		#buddypress #item-header-cover-image #item-buttons {
+			margin: 0 0 10px;
+			padding: 0 0 5px;
+		}
+
+		#buddypress #item-header-cover-image #item-buttons:after {
+			clear: both;
+			content: "";
+			display: table;
+		}
+
+		@media screen and (max-width: 782px) {
+			#buddypress #item-header-cover-image #item-header-avatar,
+			.bp-user #buddypress #item-header #item-header-cover-image #item-header-avatar,
+			#buddypress div#item-header #item-header-cover-image #item-header-content {
+				width:100%;
+				text-align:center;
+			}
+
+			#buddypress #item-header-cover-image #item-header-avatar a {
+				display:inline-block;
+			}
+
+			#buddypress #item-header-cover-image #item-header-avatar img {
+				margin:0;
+			}
+
+			#buddypress div#item-header #item-header-cover-image #item-header-content,
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content,
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+				margin:0;
+			}
+
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-header-content,
+			body.single-item.groups #buddypress div#item-header #item-header-cover-image #item-actions {
+				max-width: 100%;
+			}
+
+			#buddypress div#item-header-cover-image h2 a,
+			#buddypress div#item-header-cover-image h2 {
+				color: inherit;
+				text-shadow: none;
+				margin:25px 0 0;
+				font-size:200%;
+			}
+
+			#buddypress #item-header-cover-image #item-buttons div {
+				float:none;
+				display:inline-block;
+			}
+
+			#buddypress #item-header-cover-image #item-buttons:before {
+				content:"";
+			}
+
+			#buddypress #item-header-cover-image #item-buttons {
+				margin: 5px 0;
+			}
+		}
+	';
+}
+
+/**
+ * All user feedback messages are available here
+ *
+ * @since 3.0.0
+ *
+ * @param string $feedback_id The ID of the message.
+ *
+ * @return string|false The list of parameters for the message
+ */
+function bp_nouveau_get_user_feedback( $feedback_id = '' ) {
+	/**
+	 * Filters the BuddyPress Nouveau feedback messages.
+	 *
+	 * Use this filter to add your custom feedback messages.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value The list of feedback messages.
+	 */
+	$feedback_messages = apply_filters( 'bp_nouveau_feedback_messages', array(
+		'registration-disabled' => array(
+			'type'    => 'info',
+			'message' => __( 'Member registration is currently not allowed.', 'buddypress' ),
+			'before'  => 'bp_before_registration_disabled',
+			'after'   => 'bp_after_registration_disabled'
+		),
+		'request-details' => array(
+			'type'    => 'info',
+			'message' => __( 'Registering for this site is easy. Just fill in the fields below, and we\'ll get a new account set up for you in no time.', 'buddypress' ),
+			'before'  => false,
+			'after'   => false,
+		),
+		'completed-confirmation' => array(
+			'type'    => 'info',
+			'message' => __( 'You have successfully created your account! Please log in using the username and password you have just created.', 'buddypress' ),
+			'before'  => 'bp_before_registration_confirmed',
+			'after'   => 'bp_after_registration_confirmed',
+		),
+		'directory-activity-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the community updates. Please wait.', 'buddypress' ),
+		),
+		'single-activity-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the update. Please wait.', 'buddypress' ),
+		),
+		'activity-loop-none' => array(
+			'type'    => 'info',
+			'message' => __( 'Sorry, there was no activity found. Please try a different filter.', 'buddypress' ),
+		),
+		'blogs-loop-none' => array(
+			'type'    => 'info',
+			'message' => __( 'Sorry, there were no sites found.', 'buddypress' ),
+		),
+		'blogs-no-signup' => array(
+			'type'    => 'info',
+			'message' => __( 'Site registration is currently disabled.', 'buddypress' ),
+		),
+		'directory-blogs-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the sites of the network. Please wait.', 'buddypress' ),
+		),
+		'directory-groups-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the groups of the community. Please wait.', 'buddypress' ),
+		),
+		'groups-loop-none' => array(
+			'type'    => 'info',
+			'message' => __( 'Sorry, there were no groups found.', 'buddypress' ),
+		),
+		'group-activity-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the group updates. Please wait.', 'buddypress' ),
+		),
+		'group-members-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Requesting the group members. Please wait.', 'buddypress' ),
+		),
+		'group-members-none' => array(
+			'type'    => 'info',
+			'message' => __( 'Sorry, there were no group members found.', 'buddypress' ),
+		),
+		'group-members-search-none' => array(
+			'type'    => 'info',
+			'message' => __( 'Sorry, there was no member of that name found in this group.', 'buddypress' ),
+		),
+		'group-manage-members-none' => array(
+			'type'    => 'info',
+			'message' => __( 'This group has no members.', 'buddypress' ),
+		),
+		'group-requests-none' => array(
+			'type'    => 'info',
+			'message' => __( 'There are no pending membership requests.', 'buddypress' ),
+		),
+		'group-requests-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the members who requested to join the group. Please wait.', 'buddypress' ),
+		),
+		'group-delete-warning' => array(
+			'type'    => 'warning',
+			'message' => __( 'WARNING: Deleting this group will completely remove ALL content associated with it. There is no way back. Please be careful with this option.', 'buddypress' ),
+		),
+		'group-avatar-delete-info' => array(
+			'type'    => 'info',
+			'message' => __( 'If you\'d like to remove the existing group profile photo but not upload a new one, please use the delete group profile photo button.', 'buddypress' ),
+		),
+		'directory-members-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the members of your community. Please wait.', 'buddypress' ),
+		),
+		'members-loop-none' => array(
+			'type'    => 'info',
+			'message' => __( 'Sorry, no members were found.', 'buddypress' ),
+		),
+		'member-requests-none' => array(
+			'type'    => 'info',
+			'message' => __( 'You have no pending friendship requests.', 'buddypress' ),
+		),
+		'member-invites-none' => array(
+			'type'    => 'info',
+			'message' => __( 'You have no outstanding group invites.', 'buddypress' ),
+		),
+		'member-notifications-none' => array(
+			'type'    => 'info',
+			'message' => __( 'This member has no notifications.', 'buddypress' ),
+		),
+		'member-wp-profile-none' => array(
+			'type'    => 'info',
+			'message' => __( '%s did not save any profile information yet.', 'buddypress' ),
+		),
+		'member-delete-account' => array(
+			'type'    => 'warning',
+			'message' => __( 'Deleting this account will delete all of the content it has created. It will be completely unrecoverable.', 'buddypress' ),
+		),
+		'member-activity-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the member\'s updates. Please wait.', 'buddypress' ),
+		),
+		'member-blogs-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the member\'s blogs. Please wait.', 'buddypress' ),
+		),
+		'member-friends-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the member\'s friends. Please wait.', 'buddypress' ),
+		),
+		'member-groups-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading the member\'s groups. Please wait.', 'buddypress' ),
+		),
+		'member-notifications-loading' => array(
+			'type'    => 'loading',
+			'message' => __( 'Loading notifications. Please wait.', 'buddypress' ),
+		),
+		'member-group-invites-all' => array(
+			'type'    => 'info',
+			'message' => __( 'Currently every member of the community can invite you to join their groups. If you are not comfortable with it, you can always restrict group invites to your friends only.', 'buddypress' ),
+		),
+		'member-group-invites-friends-only' => array(
+			'type'    => 'info',
+			'message' => __( 'Currently only your friends can invite you to groups. Uncheck the box to allow any member to send invites.', 'buddypress' ),
+		),
+	) );
+
+	if ( ! isset( $feedback_messages[ $feedback_id ] ) ) {
+		return false;
+	}
+
+	/*
+	 * Adjust some messages to the context.
+	 */
+	if ( 'completed-confirmation' === $feedback_id && bp_registration_needs_activation() ) {
+		$feedback_messages['completed-confirmation']['message'] = __( 'You have successfully created your account! To begin using this site you will need to activate your account via the email we have just sent to your address.', 'buddypress' );
+	} elseif ( 'member-notifications-none' === $feedback_id ) {
+		$is_myprofile = bp_is_my_profile();
+
+		if ( bp_is_current_action( 'unread' ) ) {
+			$feedback_messages['member-notifications-none']['message'] = __( 'This member has no unread notifications.', 'buddypress' );
+
+			if ( $is_myprofile ) {
+				$feedback_messages['member-notifications-none']['message'] = __( 'You have no unread notifications.', 'buddypress' );
+			}
+		} elseif ( $is_myprofile ) {
+			$feedback_messages['member-notifications-none']['message'] = __( 'You have no notifications.', 'buddypress' );
+		}
+	} elseif ( 'member-wp-profile-none' === $feedback_id && bp_is_user_profile() ) {
+		$feedback_messages['member-wp-profile-none']['message'] = sprintf( $feedback_messages['member-wp-profile-none']['message'], bp_get_displayed_user_fullname() );
+	} elseif ( 'member-delete-account' === $feedback_id && bp_is_my_profile() ) {
+		$feedback_messages['member-delete-account']['message'] = __( 'Deleting your account will delete all of the content you have created. It will be completely irrecoverable.', 'buddypress' );
+	} elseif ( 'member-activity-loading' === $feedback_id && bp_is_my_profile() ) {
+		$feedback_messages['member-activity-loading']['message'] = __( 'Loading your updates. Please wait.', 'buddypress' );
+	} elseif ( 'member-blogs-loading' === $feedback_id && bp_is_my_profile() ) {
+		$feedback_messages['member-blogs-loading']['message'] = __( 'Loading your blogs. Please wait.', 'buddypress' );
+	} elseif ( 'member-friends-loading' === $feedback_id && bp_is_my_profile() ) {
+		$feedback_messages['member-friends-loading']['message'] = __( 'Loading your friends. Please wait.', 'buddypress' );
+	} elseif ( 'member-groups-loading' === $feedback_id && bp_is_my_profile() ) {
+		$feedback_messages['member-groups-loading']['message'] = __( 'Loading your groups. Please wait.', 'buddypress' );
+	}
+
+	/**
+	 * Filter here if you wish to edit the message just before being displayed
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $feedback_messages
+	 */
+	return apply_filters( 'bp_nouveau_get_user_feedback', $feedback_messages[ $feedback_id ] );
+}
+
+/**
+ * Get the signup fields for the requested section
+ *
+ * @since 3.0.0
+ *
+ * @param string $section Optional. The section of fields to get 'account_details' or 'blog_details'.
+ *
+ * @return array|false The list of signup fields for the requested section. False if not found.
+ */
+function bp_nouveau_get_signup_fields( $section = '' ) {
+	if ( empty( $section ) ) {
+		return false;
+	}
+
+	/**
+	 * Filter to add your specific 'text' or 'password' inputs
+	 *
+	 * If you need to use other types of field, please use the
+	 * do_action( 'bp_account_details_fields' ) or do_action( 'blog_details' ) hooks instead.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value The list of fields organized into sections.
+	 */
+	$fields = apply_filters( 'bp_nouveau_get_signup_fields', array(
+		'account_details' => array(
+			'signup_username' => array(
+				'label'          => __( 'Username', 'buddypress' ),
+				'required'       => true,
+				'value'          => 'bp_get_signup_username_value',
+				'attribute_type' => 'username',
+				'type'           => 'text',
+				'class'          => '',
+			),
+			'signup_email' => array(
+				'label'          => __( 'Email Address', 'buddypress' ),
+				'required'       => true,
+				'value'          => 'bp_get_signup_email_value',
+				'attribute_type' => 'email',
+				'type'           => 'email',
+				'class'          => '',
+			),
+			'signup_password' => array(
+				'label'          => __( 'Choose a Password', 'buddypress' ),
+				'required'       => true,
+				'value'          => '',
+				'attribute_type' => 'password',
+				'type'           => 'password',
+				'class'          => 'password-entry',
+			),
+			'signup_password_confirm' => array(
+				'label'          => __( 'Confirm Password', 'buddypress' ),
+				'required'       => true,
+				'value'          => '',
+				'attribute_type' => 'password',
+				'type'           => 'password',
+				'class'          => 'password-entry-confirm',
+			),
+		),
+		'blog_details' => array(
+			'signup_blog_url' => array(
+				'label'          => __( 'Site URL', 'buddypress' ),
+				'required'       => true,
+				'value'          => 'bp_get_signup_blog_url_value',
+				'attribute_type' => 'slug',
+				'type'           => 'text',
+				'class'          => '',
+			),
+			'signup_blog_title' => array(
+				'label'          => __( 'Site Title', 'buddypress' ),
+				'required'       => true,
+				'value'          => 'bp_get_signup_blog_title_value',
+				'attribute_type' => 'title',
+				'type'           => 'text',
+				'class'          => '',
+			),
+			'signup_blog_privacy_public' => array(
+				'label'          => __( 'Yes', 'buddypress' ),
+				'required'       => false,
+				'value'          => 'public',
+				'attribute_type' => '',
+				'type'           => 'radio',
+				'class'          => '',
+			),
+			'signup_blog_privacy_private' => array(
+				'label'          => __( 'No', 'buddypress' ),
+				'required'       => false,
+				'value'          => 'private',
+				'attribute_type' => '',
+				'type'           => 'radio',
+				'class'          => '',
+			),
+		),
+	) );
+
+	if ( ! bp_get_blog_signup_allowed() ) {
+		unset( $fields['blog_details'] );
+	}
+
+	if ( isset( $fields[ $section ] ) ) {
+		return $fields[ $section ];
+	}
+
+	return false;
+}
+
+/**
+ * Get Some submit buttons data.
+ *
+ * @since 3.0.0
+ *
+ * @param string $action The action requested.
+ *
+ * @return array|false The list of the submit button parameters for the requested action
+ *                     False if no actions were found.
+ */
+function bp_nouveau_get_submit_button( $action = '' ) {
+	if ( empty( $action ) ) {
+		return false;
+	}
+
+	/**
+	 * Filter the Submit buttons to add your own.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value The list of submit buttons.
+	 *
+	 * @return array|false
+	 */
+	$actions = apply_filters( 'bp_nouveau_get_submit_button', array(
+		'register' => array(
+			'before'     => 'bp_before_registration_submit_buttons',
+			'after'      => 'bp_after_registration_submit_buttons',
+			'nonce'      => 'bp_new_signup',
+			'attributes' => array(
+				'name'  => 'signup_submit',
+				'id'    => 'signup_submit',
+				'value' => __( 'Complete Sign Up', 'buddypress' ),
+			),
+		),
+		'member-profile-edit' => array(
+			'before' => '',
+			'after'  => '',
+			'nonce'  => 'bp_xprofile_edit',
+			'attributes' => array(
+				'name'  => 'profile-group-edit-submit',
+				'id'    => 'profile-group-edit-submit',
+				'value' => __( 'Save Changes', 'buddypress' ),
+			),
+		),
+		'member-capabilities' => array(
+			'before' => 'bp_members_capabilities_account_before_submit',
+			'after'  => 'bp_members_capabilities_account_after_submit',
+			'nonce'  => 'capabilities',
+			'attributes' => array(
+				'name'  => 'capabilities-submit',
+				'id'    => 'capabilities-submit',
+				'value' => __( 'Save', 'buddypress' ),
+			),
+		),
+		'member-delete-account' => array(
+			'before' => 'bp_members_delete_account_before_submit',
+			'after'  => 'bp_members_delete_account_after_submit',
+			'nonce'  => 'delete-account',
+			'attributes' => array(
+				'disabled' => 'disabled',
+				'name'     => 'delete-account-button',
+				'id'       => 'delete-account-button',
+				'value'    => __( 'Delete Account', 'buddypress' ),
+			),
+		),
+		'members-general-settings' => array(
+			'before' => 'bp_core_general_settings_before_submit',
+			'after'  => 'bp_core_general_settings_after_submit',
+			'nonce'  => 'bp_settings_general',
+			'attributes' => array(
+				'name'  => 'submit',
+				'id'    => 'submit',
+				'value' => __( 'Save Changes', 'buddypress' ),
+				'class' => 'auto',
+			),
+		),
+		'member-notifications-settings' => array(
+			'before' => 'bp_members_notification_settings_before_submit',
+			'after'  => 'bp_members_notification_settings_after_submit',
+			'nonce'  => 'bp_settings_notifications',
+			'attributes' => array(
+				'name'  => 'submit',
+				'id'    => 'submit',
+				'value' => __( 'Save Changes', 'buddypress' ),
+				'class' => 'auto',
+			),
+		),
+		'members-profile-settings' => array(
+			'before' => 'bp_core_xprofile_settings_before_submit',
+			'after'  => 'bp_core_xprofile_settings_after_submit',
+			'nonce'  => 'bp_xprofile_settings',
+			'attributes' => array(
+				'name'  => 'xprofile-settings-submit',
+				'id'    => 'submit',
+				'value' => __( 'Save Changes', 'buddypress' ),
+				'class' => 'auto',
+			),
+		),
+		'member-group-invites' => array(
+			'nonce'  => 'bp_nouveau_group_invites_settings',
+			'attributes' => array(
+				'name'  => 'member-group-invites-submit',
+				'id'    => 'submit',
+				'value' => __( 'Save', 'buddypress' ),
+				'class' => 'auto',
+			),
+		),
+		'activity-new-comment' => array(
+			'after'     => 'bp_activity_entry_comments',
+			'nonce'     => 'new_activity_comment',
+			'nonce_key' => '_wpnonce_new_activity_comment',
+			'wrapper'   => false,
+			'attributes' => array(
+				'name'  => 'ac_form_submit',
+				'value' => _x( 'Post', 'button', 'buddypress' ),
+			),
+		),
+	) );
+
+	if ( isset( $actions[ $action ] ) ) {
+		return $actions[ $action ];
+	}
+
+	return false;
+}
+
+/**
+ * Reorder a BuddyPress item nav according to a given list of nav item slugs
+ *
+ * @since 3.0.0
+ *
+ * @param object $nav         The BuddyPress Item Nav object to reorder
+ * @param array  $order       A list of slugs ordered (eg: array( 'profile', 'activity', etc..) )
+ * @param string $parent_slug A parent slug if it's a secondary nav we are reordering (case of the Groups single item)
+ *
+ * @return bool True on success. False otherwise.
+ */
+function bp_nouveau_set_nav_item_order( $nav = null, $order = array(), $parent_slug = '' ) {
+	if ( ! is_object( $nav ) || empty( $order ) || ! is_array( $order ) ) {
+		return false;
+	}
+
+	$position = 0;
+
+	foreach ( $order as $slug ) {
+		$position += 10;
+
+		$key = $slug;
+		if ( ! empty( $parent_slug ) ) {
+			$key = $parent_slug . '/' . $key;
+		}
+
+		$item_nav = $nav->get( $key );
+
+		if ( ! $item_nav ) {
+			continue;
+		}
+
+		if ( (int) $item_nav->position !== (int) $position ) {
+			$nav->edit_nav( array( 'position' => $position ), $slug, $parent_slug );
+		}
+	}
+
+	return true;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/ajax.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/ajax.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/ajax.php	(revision 12265)
@@ -0,0 +1,485 @@
+<?php
+/**
+ * Groups Ajax functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_action( 'admin_init', function() {
+	$ajax_actions = array(
+		array( 'groups_filter'                      => array( 'function' => 'bp_nouveau_ajax_object_template_loader', 'nopriv' => true  ) ),
+		array( 'groups_join_group'                  => array( 'function' => 'bp_nouveau_ajax_joinleave_group', 'nopriv' => false ) ),
+		array( 'groups_leave_group'                 => array( 'function' => 'bp_nouveau_ajax_joinleave_group', 'nopriv' => false ) ),
+		array( 'groups_accept_invite'               => array( 'function' => 'bp_nouveau_ajax_joinleave_group', 'nopriv' => false ) ),
+		array( 'groups_reject_invite'               => array( 'function' => 'bp_nouveau_ajax_joinleave_group', 'nopriv' => false ) ),
+		array( 'groups_request_membership'          => array( 'function' => 'bp_nouveau_ajax_joinleave_group', 'nopriv' => false ) ),
+		array( 'groups_get_group_potential_invites' => array( 'function' => 'bp_nouveau_ajax_get_users_to_invite', 'nopriv' => false ) ),
+		array( 'groups_send_group_invites'          => array( 'function' => 'bp_nouveau_ajax_send_group_invites', 'nopriv' => false ) ),
+		array( 'groups_delete_group_invite'         => array( 'function' => 'bp_nouveau_ajax_remove_group_invite', 'nopriv' => false ) ),
+	);
+
+	foreach ( $ajax_actions as $ajax_action ) {
+		$action = key( $ajax_action );
+
+		add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+		if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+			add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+		}
+	}
+}, 12 );
+
+/**
+ * Join or leave a group when clicking the "join/leave" button via a POST request.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML
+ */
+function bp_nouveau_ajax_joinleave_group() {
+	$response = array(
+		'feedback' => sprintf(
+			'<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+			esc_html__( 'There was a problem performing this action. Please try again.', 'buddypress' )
+		),
+	);
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() || empty( $_POST['action'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['nonce'] ) || empty( $_POST['item_id'] ) || ! bp_is_active( 'groups' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Use default nonce
+	$nonce = $_POST['nonce'];
+	$check = 'bp_nouveau_groups';
+
+	// Use a specific one for actions needed it
+	if ( ! empty( $_POST['_wpnonce'] ) && ! empty( $_POST['action'] ) ) {
+		$nonce = $_POST['_wpnonce'];
+		$check = $_POST['action'];
+	}
+
+	// Nonce check!
+	if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $check ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Cast gid as integer.
+	$group_id = (int) $_POST['item_id'];
+
+	$errors = array(
+		'cannot' => sprintf( '<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>', esc_html__( 'You cannot join this group.', 'buddypress' ) ),
+		'member' => sprintf( '<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>', esc_html__( 'You are already a member of the group.', 'buddypress' ) ),
+	);
+
+	if ( groups_is_user_banned( bp_loggedin_user_id(), $group_id ) ) {
+		$response['feedback'] = $errors['cannot'];
+
+		wp_send_json_error( $response );
+	}
+
+	// Validate and get the group
+	$group = groups_get_group( array( 'group_id' => $group_id ) );
+
+	if ( empty( $group->id ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Manage all button's possible actions here.
+	switch ( $_POST['action'] ) {
+
+		case 'groups_accept_invite':
+			if ( ! groups_accept_invite( bp_loggedin_user_id(), $group_id ) ) {
+				$response = array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+						esc_html__( 'Group invitation could not be accepted.', 'buddypress' )
+					),
+					'type'     => 'error',
+				);
+
+			} else {
+				if ( bp_is_active( 'activity' ) ) {
+					groups_record_activity(
+						array(
+							'type'    => 'joined_group',
+							'item_id' => $group->id,
+						)
+					);
+				}
+
+				// User is now a member of the group
+				$group->is_member = '1';
+
+				$response = array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback success"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+						esc_html__( 'Group invite accepted.', 'buddypress' )
+					),
+					'type'     => 'success',
+					'is_user'  => bp_is_user(),
+					'contents' => bp_get_group_join_button( $group ),
+					'is_group' => bp_is_group(),
+				);
+			}
+			break;
+
+		case 'groups_reject_invite':
+			if ( ! groups_reject_invite( bp_loggedin_user_id(), $group_id ) ) {
+				$response = array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+						esc_html__( 'Group invite could not be rejected', 'buddypress' )
+					),
+					'type'     => 'error',
+				);
+			} else {
+				$response = array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback success"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+						esc_html__( 'Group invite rejected', 'buddypress' )
+					),
+					'type'     => 'success',
+					'is_user'  => bp_is_user(),
+				);
+			}
+			break;
+
+		case 'groups_join_group':
+			if ( groups_is_user_member( bp_loggedin_user_id(), $group->id ) ) {
+				$response = array(
+					'feedback' => $errors['member'],
+					'type'     => 'error',
+				);
+			} elseif ( 'public' !== $group->status ) {
+				$response = array(
+					'feedback' => $errors['cannot'],
+					'type'     => 'error',
+				);
+			} elseif ( ! groups_join_group( $group->id ) ) {
+				$response = array(
+					'feedback' => sprintf(
+						'<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+						esc_html__( 'Error joining this group.', 'buddypress' )
+					),
+					'type'     => 'error',
+				);
+			} else {
+				// User is now a member of the group
+				$group->is_member = '1';
+
+				$response = array(
+					'contents' => bp_get_group_join_button( $group ),
+					'is_group' => bp_is_group(),
+					'type'     => 'success',
+				);
+			}
+			break;
+
+			case 'groups_request_membership' :
+				if ( ! groups_send_membership_request( bp_loggedin_user_id(), $group->id ) ) {
+					$response = array(
+						'feedback' => sprintf(
+							'<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+							esc_html__( 'Error requesting membership.', 'buddypress' )
+						),
+						'type'     => 'error',
+					);
+				} else {
+					// Request is pending
+					$group->is_pending = '1';
+
+					$response = array(
+						'contents' => bp_get_group_join_button( $group ),
+						'is_group' => bp_is_group(),
+						'type'     => 'success',
+					);
+				}
+				break;
+
+			case 'groups_leave_group' :
+				if (  ! groups_leave_group( $group->id ) ) {
+					$response = array(
+						'feedback' => sprintf(
+							'<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>%s</p></div>',
+							esc_html__( 'Error leaving group.', 'buddypress' )
+						),
+						'type'     => 'error',
+					);
+				} else {
+					// User is no more a member of the group
+					$group->is_member = '0';
+					$bp               = buddypress();
+
+					/**
+					 * When inside the group or in the loggedin user's group memberships screen
+					 * we need to reload the page.
+					 */
+					$bp_is_group = bp_is_group() || ( bp_is_user_groups() && bp_is_my_profile() );
+
+					$response = array(
+						'contents' => bp_get_group_join_button( $group ),
+						'is_group' => $bp_is_group,
+						'type'     => 'success',
+					);
+
+					// Reset the message if not in a Group or in a loggedin user's group memberships one!
+					if ( ! $bp_is_group && isset( $bp->template_message ) && isset( $bp->template_message_type ) ) {
+						unset( $bp->template_message, $bp->template_message_type );
+
+						@setcookie( 'bp-message', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+						@setcookie( 'bp-message-type', false, time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+					}
+				}
+				break;
+	}
+
+	if ( 'error' === $response['type'] ) {
+		wp_send_json_error( $response );
+	}
+
+	wp_send_json_success( $response );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_get_users_to_invite() {
+	$bp = buddypress();
+
+	$response = array(
+		'feedback' => __( 'There was a problem performing this action. Please try again.', 'buddypress' ),
+		'type'     => 'error',
+	);
+
+	if ( empty( $_POST['nonce'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Use default nonce
+	$nonce = $_POST['nonce'];
+	$check = 'bp_nouveau_groups';
+
+	// Use a specific one for actions needed it
+	if ( ! empty( $_POST['_wpnonce'] ) && ! empty( $_POST['action'] ) ) {
+		$nonce = $_POST['_wpnonce'];
+		$check = $_POST['action'];
+	}
+
+	// Nonce check!
+	if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $check ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$request = bp_parse_args(
+		$_POST,
+		array(
+			'scope' => 'members',
+		),
+		'nouveau_ajax_get_users_to_invite'
+	);
+
+	$bp->groups->invites_scope = 'members';
+	$message = __( 'Select members to invite by clicking the + button. Once you\'ve made your selection, use the "Send Invites" navigation item to continue.', 'buddypress' );
+
+	if ( 'friends' === $request['scope'] ) {
+		$request['user_id'] = bp_loggedin_user_id();
+		$bp->groups->invites_scope = 'friends';
+		$message = __( 'Select friends to invite by clicking the + button. Once you\'ve made your selection, use the "Send Invites" navigation item to continue.', 'buddypress' );
+	}
+
+	if ( 'invited' === $request['scope'] ) {
+
+		if ( ! bp_group_has_invites( array( 'user_id' => 'any' ) ) ) {
+			wp_send_json_error( array(
+				'feedback' => __( 'No pending group invitations found.', 'buddypress' ),
+				'type'     => 'info',
+			) );
+		}
+
+		$request['is_confirmed'] = false;
+		$bp->groups->invites_scope = 'invited';
+		$message = __( 'You can view the group\'s pending invitations from this screen.', 'buddypress' );
+	}
+
+	$potential_invites = bp_nouveau_get_group_potential_invites( $request );
+
+	if ( empty( $potential_invites->users ) ) {
+		$error = array(
+			'feedback' => __( 'No members were found. Try another filter.', 'buddypress' ),
+			'type'     => 'info',
+		);
+
+		if ( 'friends' === $bp->groups->invites_scope ) {
+			$error = array(
+				'feedback' => __( 'All your friends are already members of this group, or have already received an invite to join this group, or have requested to join it.', 'buddypress' ),
+				'type'     => 'info',
+			);
+
+			if ( 0 === (int) bp_get_total_friend_count( bp_loggedin_user_id() ) ) {
+				$error = array(
+					'feedback' => __( 'You have no friends!', 'buddypress' ),
+					'type'     => 'info',
+				);
+			}
+		}
+
+		unset( $bp->groups->invites_scope );
+
+		wp_send_json_error( $error );
+	}
+
+	$potential_invites->users = array_map( 'bp_nouveau_prepare_group_potential_invites_for_js', array_values( $potential_invites->users ) );
+	$potential_invites->users = array_filter( $potential_invites->users );
+
+	// Set a message to explain use of the current scope
+	$potential_invites->feedback = $message;
+
+	unset( $bp->groups->invites_scope );
+
+	wp_send_json_success( $potential_invites );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_send_group_invites() {
+	$bp = buddypress();
+
+	$response = array(
+		'feedback' => __( 'Invites could not be sent. Please try again.', 'buddypress' ),
+		'type'     => 'error',
+	);
+
+	// Verify nonce
+	if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'groups_send_invites' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$group_id = bp_get_current_group_id();
+
+	if ( bp_is_group_create() && ! empty( $_POST['group_id'] ) ) {
+		$group_id = (int) $_POST['group_id'];
+	}
+
+	if ( ! bp_groups_user_can_send_invites( $group_id ) ) {
+		$response['feedback'] = __( 'You are not allowed to send invitations for this group.', 'buddypress' );
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['users'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// For feedback
+	$invited = array();
+
+	foreach ( (array) $_POST['users'] as $user_id ) {
+		$invited[ (int) $user_id ] = groups_invite_user(
+			array(
+				'user_id'  => $user_id,
+				'group_id' => $group_id,
+			)
+		);
+	}
+
+	if ( ! empty( $_POST['message'] ) ) {
+		$bp->groups->invites_message = wp_kses( wp_unslash( $_POST['message'] ), array() );
+
+		add_filter( 'groups_notification_group_invites_message', 'bp_nouveau_groups_invites_custom_message', 10, 1 );
+	}
+
+	// Send the invites.
+	groups_send_invites( bp_loggedin_user_id(), $group_id );
+
+	if ( ! empty( $_POST['message'] ) ) {
+		unset( $bp->groups->invites_message );
+
+		remove_filter( 'groups_notification_group_invites_message', 'bp_nouveau_groups_invites_custom_message', 10, 1 );
+	}
+
+	if ( array_search( false, $invited ) ) {
+		$errors = array_keys( $invited, false );
+
+		$error_count   = count( $errors );
+		$error_message = sprintf(
+			/* translators: count of users affected */
+			_n(
+				'Invitation failed for %s user.',
+				'Invitation failed for %s users.',
+				$error_count, 'buddypress'
+			),
+			number_format_i18n( $error_count )
+		);
+
+		wp_send_json_error(
+			array(
+				'feedback' => $error_message,
+				'users'    => $errors,
+				'type'     => 'error',
+			)
+		);
+	}
+
+	wp_send_json_success(
+		array(
+			'feedback' => __( 'Invitations sent.', 'buddypress' ),
+			'type'     => 'success',
+		)
+	);
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_remove_group_invite() {
+	$user_id  = (int) $_POST['user'];
+	$group_id = bp_get_current_group_id();
+
+	// Verify nonce
+	if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'groups_invite_uninvite_user' ) ) {
+		wp_send_json_error(
+			array(
+				'feedback' => __( 'Group invitation could not be removed.', 'buddypress' ),
+				'type'     => 'error',
+			)
+		);
+	}
+
+	if ( BP_Groups_Member::check_for_membership_request( $user_id, $group_id ) ) {
+		wp_send_json_error(
+			array(
+				'feedback' => __( 'The member is already a member of the group.', 'buddypress' ),
+				'type'     => 'warning',
+				'code'     => 1,
+			)
+		);
+	}
+
+	// Remove the unsent invitation.
+	if ( ! groups_uninvite_user( $user_id, $group_id ) ) {
+		wp_send_json_error(
+			array(
+				'feedback' => __( 'Group invitation could not be removed.', 'buddypress' ),
+				'type'     => 'error',
+				'code'     => 0,
+			)
+		);
+	}
+
+	wp_send_json_success(
+		array(
+			'feedback'    => __( 'There are no more pending invitations for the group.', 'buddypress' ),
+			'type'        => 'info',
+			'has_invites' => bp_group_has_invites( array( 'user_id' => 'any' ) ),
+		)
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/classes.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/classes.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/classes.php	(revision 12265)
@@ -0,0 +1,361 @@
+<?php
+/**
+ * Groups classes
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Query to get members that are not already members of the group
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Group_Invite_Query extends BP_User_Query {
+	/**
+	 * Array of group member ids, cached to prevent redundant lookups
+	 *
+	 * @var null|array Null if not yet defined, otherwise an array of ints
+	 * @since 3.0.0
+	 */
+	protected $group_member_ids;
+
+	/**
+	 * Set up action hooks
+	 *
+	 * @since 3.0.0
+	 */
+	public function setup_hooks() {
+		add_action( 'bp_pre_user_query_construct', array( $this, 'build_exclude_args' ) );
+		add_action( 'bp_pre_user_query', array( $this, 'build_meta_query' ) );
+	}
+
+	/**
+	 * Exclude group members from the user query as it's not needed to invite members to join the group.
+	 *
+	 * @since 3.0.0
+	 */
+	public function build_exclude_args() {
+		$this->query_vars = bp_parse_args(
+			$this->query_vars,
+			array(
+				'group_id'     => 0,
+				'is_confirmed' => true,
+			),
+			'nouveau_group_invite_query_exlude_args'
+		);
+
+		$group_member_ids = $this->get_group_member_ids();
+
+		// We want to get users that are already members of the group
+		$type = 'exclude';
+
+		// We want to get invited users who did not confirmed yet
+		if ( false === $this->query_vars['is_confirmed'] ) {
+			$type = 'include';
+		}
+
+		if ( ! empty( $group_member_ids ) ) {
+			$this->query_vars[ $type ] = $group_member_ids;
+		}
+	}
+
+	/**
+	 * Get the members of the queried group
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array $ids User IDs of relevant group member ids
+	 */
+	protected function get_group_member_ids() {
+		global $wpdb;
+
+		if ( is_array( $this->group_member_ids ) ) {
+			return $this->group_member_ids;
+		}
+
+		$bp  = buddypress();
+		$sql = array(
+			'select'  => "SELECT user_id FROM {$bp->groups->table_name_members}",
+			'where'   => array(),
+			'orderby' => '',
+			'order'   => '',
+			'limit'   => '',
+		);
+
+		/** WHERE clauses *****************************************************/
+
+		// Group id
+		$sql['where'][] = $wpdb->prepare( 'group_id = %d', $this->query_vars['group_id'] );
+
+		if ( false === $this->query_vars['is_confirmed'] ) {
+			$sql['where'][] = $wpdb->prepare( 'is_confirmed = %d', (int) $this->query_vars['is_confirmed'] );
+			$sql['where'][] = 'inviter_id != 0';
+		}
+
+		// Join the query part
+		$sql['where'] = ! empty( $sql['where'] ) ? 'WHERE ' . implode( ' AND ', $sql['where'] ) : '';
+
+		/** ORDER BY clause ***************************************************/
+		$sql['orderby'] = 'ORDER BY date_modified';
+		$sql['order']   = 'DESC';
+
+		/** LIMIT clause ******************************************************/
+		$this->group_member_ids = $wpdb->get_col( "{$sql['select']} {$sql['where']} {$sql['orderby']} {$sql['order']} {$sql['limit']}" );
+
+		return $this->group_member_ids;
+	}
+
+	/**
+	 * @since 3.0.0
+	 */
+	public function build_meta_query( BP_User_Query $bp_user_query ) {
+		if ( isset( $this->query_vars['scope'] ) && 'members' === $this->query_vars['scope'] && isset( $this->query_vars['meta_query'] ) ) {
+
+			$invites_meta_query = new WP_Meta_Query( $this->query_vars['meta_query'] );
+			$meta_sql           = $invites_meta_query->get_sql( 'user', 'u', 'ID' );
+
+			if ( empty( $meta_sql['join'] ) || empty( $meta_sql['where'] ) ) {
+				return;
+			}
+
+			$bp_user_query->uid_clauses['select'] .= ' ' . $meta_sql['join'];
+			$bp_user_query->uid_clauses['where']  .= ' ' . $meta_sql['where'];
+		}
+	}
+
+	/**
+	 * @since 3.0.0
+	 */
+	public static function get_inviter_ids( $user_id = 0, $group_id = 0 ) {
+		global $wpdb;
+
+		if ( empty( $group_id ) || empty( $user_id ) ) {
+			return array();
+		}
+
+		$bp = buddypress();
+
+		return $wpdb->get_col( $wpdb->prepare( "SELECT inviter_id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $user_id, $group_id ) );
+	}
+}
+
+/**
+ * A specific Group Nav class to make it possible to set new positions for
+ * buddypress()->groups->nav.
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Customizer_Group_Nav extends BP_Core_Nav {
+	/**
+	 * Constructor
+	 *
+	 * @param int $object_id Optional. The random group ID used to generate the nav.
+	 */
+	public function __construct( $object_id = 0 ) {
+		$error = new WP_Error( 'missing_parameter' );
+
+		if ( empty( $object_id ) || ! bp_current_user_can( 'bp_moderate' ) || ! did_action( 'admin_init' ) ) {
+			return $error;
+		}
+
+		$group = groups_get_group( array( 'group_id' => $object_id ) );
+		if ( empty( $group->id ) ) {
+			return $error;
+		}
+
+		$this->group = $group;
+
+		parent::__construct( $group->id );
+		$this->setup_nav();
+	}
+
+	/**
+	 * Checks whether a property is set.
+	 *
+	 * Overrides BP_Core_Nav::__isset() to avoid looking into its nav property.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $key The property.
+	 *
+	 * @return bool True if the property is set, false otherwise.
+	 */
+	public function __isset( $key ) {
+		return isset( $this->{$key} );
+	}
+
+	/**
+	 * Gets a property.
+	 *
+	 * Overrides BP_Core_Nav::__isset() to avoid looking into its nav property.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $key The property.
+	 *
+	 * @return mixed The value corresponding to the property.
+	 */
+	public function __get( $key ) {
+		if ( ! isset( $this->{$key} ) ) {
+			$this->{$key} = null;
+		}
+
+		return $this->{$key};
+	}
+
+	/**
+	 * Sets a property.
+	 *
+	 * Overrides BP_Core_Nav::__isset() to avoid adding a value to its nav property.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $key The property.
+	 *
+	 * @param mixed $value The value of the property.
+	 */
+	public function __set( $key, $value ) {
+		$this->{$key} = $value;
+	}
+
+	/**
+	 * Setup a temporary nav with only the needed parameters.
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_nav() {
+		$nav_items = array(
+			'root'    => array(
+				'name'                => __( 'Memberships', 'buddypress' ),
+				'slug'                => $this->group->slug,
+				'position'            => -1,
+				/** This filter is documented in bp-groups/classes/class-bp-groups-component.php. */
+				'default_subnav_slug' => apply_filters( 'bp_groups_default_extension', defined( 'BP_GROUPS_DEFAULT_EXTENSION' ) ? BP_GROUPS_DEFAULT_EXTENSION : 'home' ),
+			),
+			'home'    => array(
+				'name'        => _x( 'Home', 'Group screen navigation title', 'buddypress' ),
+				'slug'        => 'home',
+				'parent_slug' => $this->group->slug,
+				'position'    => 10,
+			),
+			'invites' => array(
+				'name'        => _x( 'Invite', 'My Group screen nav', 'buddypress' ),
+				'slug'        => 'send-invites',
+				'parent_slug' => $this->group->slug,
+				'position'    => 70,
+			),
+			'manage'  => array(
+				'name'        => _x( 'Manage', 'My Group screen nav', 'buddypress' ),
+				'slug'        => 'admin',
+				'parent_slug' => $this->group->slug,
+				'position'    => 1000,
+			),
+		);
+
+		// Make sure only global front.php will be checked.
+		add_filter( '_bp_nouveau_group_reset_front_template', array( $this, 'all_groups_fronts' ), 10, 1 );
+
+		$front_template = bp_groups_get_front_template( $this->group );
+
+		remove_filter( '_bp_nouveau_group_reset_front_template', array( $this, 'all_groups_fronts' ), 10, 1 );
+
+		if ( ! $front_template ) {
+			if ( bp_is_active( 'activity' ) ) {
+				$nav_items['home']['name'] = _x( 'Home (Activity)', 'Group screen navigation title', 'buddypress' );
+			} else {
+				$nav_items['home']['name'] = _x( 'Home (Members)', 'Group screen navigation title', 'buddypress' );
+			}
+		} else {
+			if ( bp_is_active( 'activity' ) ) {
+				$nav_items['activity'] = array(
+					'name'        => _x( 'Activity', 'My Group screen nav', 'buddypress' ),
+					'slug'        => 'activity',
+					'parent_slug' => $this->group->slug,
+					'position'    => 11,
+				);
+			}
+
+			// Add the members one
+			$nav_items['members'] = array(
+				'name'        => _x( 'Members', 'My Group screen nav', 'buddypress' ),
+				'slug'        => 'members',
+				'parent_slug' => $this->group->slug,
+				'position'    => 60,
+			);
+		}
+
+		// Required params
+		$required_params = array(
+			'slug'              => true,
+			'name'              => true,
+			'nav_item_position' => true,
+		);
+
+		// Now find nav items plugins are creating within their Group extensions!
+		foreach ( get_declared_classes() as $class ) {
+			if ( is_subclass_of( $class, 'BP_Group_Extension' ) ) {
+				$extension = new $class;
+
+				if ( ! empty( $extension->params ) && ! array_diff_key( $required_params, $extension->params ) ) {
+					$nav_items[ $extension->params['slug'] ] = array(
+						'name'        => $extension->params['name'],
+						'slug'        => $extension->params['slug'],
+						'parent_slug' => $this->group->slug,
+						'position'    => $extension->params['nav_item_position'],
+					);
+				}
+			}
+		}
+
+		// Now we got all, create the temporary nav.
+		foreach ( $nav_items as $nav_item ) {
+			$this->add_nav( $nav_item );
+		}
+	}
+
+	/**
+	 * Front template: do not look into group's template hierarchy.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $templates The list of possible group front templates.
+	 *
+	 * @return array The list of "global" group front templates.
+	 */
+	public function all_groups_fronts( $templates = array() ) {
+		return array_intersect( array(
+			'groups/single/front.php',
+			'groups/single/default-front.php',
+		), $templates );
+	}
+
+	/**
+	 * Get the original order for the group navigation.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array a list of nav items slugs ordered.
+	 */
+	public function get_default_value() {
+		$default_nav = $this->get_secondary( array( 'parent_slug' => $this->group->slug ) );
+		return wp_list_pluck( $default_nav, 'slug' );
+	}
+
+	/**
+	 * Get the list of nav items ordered according to the Site owner preferences.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array the nav items ordered.
+	 */
+	public function get_group_nav() {
+		// Eventually reset the order
+		bp_nouveau_set_nav_item_order( $this, bp_nouveau_get_appearance_settings( 'group_nav_order' ), $this->group->slug );
+
+		return $this->get_secondary( array( 'parent_slug' => $this->group->slug ) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/functions.php	(revision 12265)
@@ -0,0 +1,1217 @@
+<?php
+/**
+ * Groups functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Provide a convenience function to add markup wrapper for message strings
+ *
+ * @param string $message The message text string
+ * @param string $type    The message type - 'error, 'info', 'warning', success'
+ *
+ * @return string
+ *
+ * @since 3.0
+ */
+function bp_nouveau_message_markup_wrapper( $message, $type ) {
+	if ( ! $message ) {
+		return false;
+	}
+
+	$message = '<div class=" ' . esc_attr( "bp-feedback {$type}" ) . '"><span class="bp-icon" aria-hidden="true"></span><p>' . esc_html( $message ) . '</p></div>';
+
+	return $message;
+}
+
+/**
+ * Register Scripts for the Groups component
+ *
+ * @since 3.0.0
+ *
+ * @param array $scripts Optional. The array of scripts to register.
+ *
+ * @return array The same array with the specific groups scripts.
+ */
+function bp_nouveau_groups_register_scripts( $scripts = array() ) {
+	if ( ! isset( $scripts['bp-nouveau'] ) ) {
+		return $scripts;
+	}
+
+	return array_merge( $scripts, array(
+		'bp-nouveau-group-invites' => array(
+			'file'         => 'js/buddypress-group-invites%s.js',
+			'dependencies' => array( 'bp-nouveau', 'json2', 'wp-backbone' ),
+			'footer'       => true,
+		),
+	) );
+}
+
+/**
+ * Enqueue the groups scripts
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_enqueue_scripts() {
+	// Neutralize Ajax when using BuddyPress Groups & member widgets on default front page
+	if ( bp_is_group_home() && bp_nouveau_get_appearance_settings( 'group_front_page' ) ) {
+		wp_add_inline_style( 'bp-nouveau', '
+			#group-front-widgets #groups-list-options,
+			#group-front-widgets #members-list-options {
+				display: none;
+			}
+		' );
+	}
+
+	if ( ! bp_is_group_invites() && ! ( bp_is_group_create() && bp_is_group_creation_step( 'group-invites' ) ) ) {
+		return;
+	}
+
+	wp_enqueue_script( 'bp-nouveau-group-invites' );
+}
+
+/**
+ * Can all members be invited to join any group?
+ *
+ * @since 3.0.0
+ *
+ * @param bool $default False to allow. True to disallow.
+ *
+ * @return bool
+ */
+function bp_nouveau_groups_disallow_all_members_invites( $default = false ) {
+	/**
+	 * Filter to remove the All members nav, returning true
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param bool $default True to disable the nav. False otherwise.
+	 */
+	return apply_filters( 'bp_nouveau_groups_disallow_all_members_invites', $default );
+}
+
+/**
+ * Localize the strings needed for the Group's Invite UI
+ *
+ * @since 3.0.0
+ *
+ * @param array $params Associative array containing the JS Strings needed by scripts
+ *
+ * @return array The same array with specific strings for the Group's Invite UI if needed.
+ */
+function bp_nouveau_groups_localize_scripts( $params = array() ) {
+	if ( ! bp_is_group_invites() && ! ( bp_is_group_create() && bp_is_group_creation_step( 'group-invites' ) ) ) {
+		return $params;
+	}
+
+	$show_pending = bp_group_has_invites( array( 'user_id' => 'any' ) ) && ! bp_is_group_create();
+
+	// Init the Group invites nav
+	$invites_nav = array(
+		'members' => array(
+			'id'      => 'members',
+			'caption' => __( 'All Members', 'buddypress' ),
+			'order'   => 5,
+		),
+		'invited' => array(
+			'id'      => 'invited',
+			'caption' => __( 'Pending Invites', 'buddypress' ),
+			'order'   => 90,
+			'hide'    => (int) ! $show_pending,
+		),
+		'invites' => array(
+			'id'      => 'invites',
+			'caption' => __( 'Send Invites', 'buddypress' ),
+			'order'   => 100,
+			'hide'    => 1,
+			'href'    => '#send-invites-editor',
+		),
+	);
+
+	if ( bp_is_active( 'friends' ) ) {
+		$invites_nav['friends'] = array(
+			'id'      => 'friends',
+			'caption' => __( 'My Friends', 'buddypress' ),
+			'order'   => 0,
+		);
+
+		if ( true === bp_nouveau_groups_disallow_all_members_invites() ) {
+			unset( $invites_nav['members'] );
+		}
+	}
+
+	$params['group_invites'] = array(
+		'nav'                => bp_sort_by_key( $invites_nav, 'order', 'num' ),
+		'loading'            => __( 'Loading members. Please wait.', 'buddypress' ),
+		'invites_form'       => __( 'Use the "Send" button to send your invite or the "Cancel" button to abort.', 'buddypress' ),
+		'invites_form_reset' => __( 'Group invitations cleared. Please use one of the available tabs to select members to invite.', 'buddypress' ),
+		'invites_sending'    => __( 'Sending group invitations. Please wait.', 'buddypress' ),
+		'removeUserInvite'   => __( 'Cancel invitation %s', 'buddypress' ),
+		'group_id'           => ! bp_get_current_group_id() ? bp_get_new_group_id() : bp_get_current_group_id(),
+		'is_group_create'    => bp_is_group_create(),
+		'nonces'             => array(
+			'uninvite'     => wp_create_nonce( 'groups_invite_uninvite_user' ),
+			'send_invites' => wp_create_nonce( 'groups_send_invites' )
+		),
+	);
+
+	return $params;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_get_inviter_ids( $user_id, $group_id ) {
+	if ( empty( $user_id ) || empty( $group_id ) ) {
+		return false;
+	}
+
+	return BP_Nouveau_Group_Invite_Query::get_inviter_ids( $user_id, $group_id );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_prepare_group_potential_invites_for_js( $user ) {
+	$bp = buddypress();
+
+	$response = array(
+		'id'           => intval( $user->ID ),
+		'name'         => $user->display_name,
+		'avatar'       => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+			'item_id' => $user->ID,
+			'object'  => 'user',
+			'type'    => 'thumb',
+			'width'   => 50,
+			'height'  => 50,
+			'html'    => false )
+		) ),
+	);
+
+	// Do extra queries only if needed
+	if ( ! empty( $bp->groups->invites_scope ) && 'invited' === $bp->groups->invites_scope ) {
+		$response['is_sent']  = (bool) groups_check_user_has_invite( $user->ID, bp_get_current_group_id() );
+
+		$inviter_ids = bp_nouveau_groups_get_inviter_ids( $user->ID, bp_get_current_group_id() );
+
+		foreach ( $inviter_ids as $inviter_id ) {
+			$class = false;
+
+			if ( bp_loggedin_user_id() === (int) $inviter_id ) {
+				$class = 'group-self-inviter';
+			}
+
+			$response['invited_by'][] = array(
+				'avatar' => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+					'item_id' => $inviter_id,
+					'object'  => 'user',
+					'type'    => 'thumb',
+					'width'   => 50,
+					'height'  => 50,
+					'html'    => false,
+					'class'   => $class,
+				) ) ),
+				'user_link' => bp_core_get_userlink( $inviter_id, false, true ),
+				'user_name' => bp_core_get_username( $inviter_id ),
+			);
+		}
+
+		if ( bp_is_item_admin() ) {
+			$response['can_edit'] = true;
+		} else {
+			$response['can_edit'] = in_array( bp_loggedin_user_id(), $inviter_ids, true );
+		}
+	}
+
+	/**
+	 * Filters the response value for potential group invite data for use with javascript.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array   $response Array of invite data.
+	 * @param WP_User $user User object.
+	 */
+	return apply_filters( 'bp_nouveau_prepare_group_potential_invites_for_js', $response, $user );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_get_group_potential_invites( $args = array() ) {
+	$r = bp_parse_args( $args, array(
+		'group_id'     => bp_get_current_group_id(),
+		'type'         => 'alphabetical',
+		'per_page'     => 20,
+		'page'         => 1,
+		'search_terms' => false,
+		'member_type'  => false,
+		'user_id'      => 0,
+		'is_confirmed' => true,
+	) );
+
+	if ( empty( $r['group_id'] ) ) {
+		return false;
+	}
+
+	/*
+	 * If it's not a friend request and users can restrict invites to friends,
+	 * make sure they are not displayed in results.
+	 */
+	if ( ! $r['user_id'] && bp_is_active( 'friends' ) && bp_is_active( 'settings' ) && ! bp_nouveau_groups_disallow_all_members_invites() ) {
+		$r['meta_query'] = array(
+			array(
+				'key'     => '_bp_nouveau_restrict_invites_to_friends',
+				'compare' => 'NOT EXISTS',
+			),
+		);
+	}
+
+	$query = new BP_Nouveau_Group_Invite_Query( $r );
+
+	$response = new stdClass();
+
+	$response->meta = array( 'total_page' => 0, 'current_page' => 0 );
+	$response->users = array();
+
+	if ( ! empty( $query->results ) ) {
+		$response->users = $query->results;
+
+		if ( ! empty( $r['per_page'] ) ) {
+			$response->meta = array(
+				'total_page' => ceil( (int) $query->total_users / (int) $r['per_page'] ),
+				'page'       => (int) $r['page'],
+			);
+		}
+	}
+
+	return $response;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_group_invites_create_steps( $steps = array() ) {
+	if ( bp_is_active( 'friends' ) && isset( $steps['group-invites'] ) ) {
+		// Simply change the name
+		$steps['group-invites']['name'] = _x( 'Invite', 'Group invitations menu title', 'buddypress' );
+		return $steps;
+	}
+
+	// Add the create step if friends component is not active
+	$steps['group-invites'] = array(
+		'name'     => _x( 'Invite', 'Group invitations menu title', 'buddypress' ),
+		'position' => 30,
+	);
+
+	return $steps;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_group_setup_nav() {
+	if ( ! bp_is_group() || ! bp_groups_user_can_send_invites() ) {
+		return;
+	}
+
+	// Simply change the name
+	if ( bp_is_active( 'friends' ) ) {
+		$bp = buddypress();
+
+		$bp->groups->nav->edit_nav(
+			array( 'name' => _x( 'Invite', 'Group invitations menu title', 'buddypress' ) ),
+			'send-invites',
+			bp_get_current_group_slug()
+		);
+
+	// Create the Subnav item for the group
+	} else {
+		$current_group = groups_get_current_group();
+		$group_link    = bp_get_group_permalink( $current_group );
+
+		bp_core_new_subnav_item( array(
+			'name'            => _x( 'Invite', 'Group invitations menu title', 'buddypress' ),
+			'slug'            => 'send-invites',
+			'parent_url'      => $group_link,
+			'parent_slug'     => $current_group->slug,
+			'screen_function' => 'groups_screen_group_invite',
+			'item_css_id'     => 'invite',
+			'position'        => 70,
+			'user_has_access' => $current_group->user_has_access,
+			'no_access_url'   => $group_link,
+		) );
+	}
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_invites_custom_message( $message = '' ) {
+	if ( empty( $message ) ) {
+		return $message;
+	}
+
+	$bp = buddypress();
+
+	if ( empty( $bp->groups->invites_message ) ) {
+		return $message;
+	}
+
+	$message = str_replace( '---------------------', "
+---------------------\n
+" . $bp->groups->invites_message . "\n
+---------------------
+	", $message );
+
+	return $message;
+}
+
+/**
+ * Format a Group for a json reply
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_prepare_group_for_js( $item ) {
+	if ( empty( $item->id ) ) {
+		return array();
+	}
+
+	$item_avatar_url = bp_core_fetch_avatar( array(
+		'item_id'    => $item->id,
+		'object'     => 'group',
+		'type'       => 'thumb',
+		'html'       => false
+	) );
+
+	return array(
+		'id'          => $item->id,
+		'name'        => $item->name,
+		'avatar_url'  => $item_avatar_url,
+		'object_type' => 'group',
+		'is_public'   => ( 'public' === $item->status ),
+	);
+}
+
+/**
+ * Group invites restriction settings navigation.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_invites_restriction_nav() {
+	$slug        = bp_get_settings_slug();
+	$user_domain = bp_loggedin_user_domain();
+
+	if ( bp_displayed_user_domain() ) {
+		$user_domain = bp_displayed_user_domain();
+	}
+
+	bp_core_new_subnav_item( array(
+		'name'            => _x( 'Group Invites', 'Group invitations main menu title', 'buddypress' ),
+		'slug'            => 'invites',
+		'parent_url'      => trailingslashit( $user_domain . $slug ),
+		'parent_slug'     => $slug,
+		'screen_function' => 'bp_nouveau_groups_screen_invites_restriction',
+		'item_css_id'     => 'invites',
+		'position'        => 70,
+		'user_has_access' => bp_core_can_edit_settings(),
+	), 'members' );
+}
+
+/**
+ * Group invites restriction settings Admin Bar navigation.
+ *
+ * @since 3.0.0
+ *
+ * @param array $wp_admin_nav The list of settings admin subnav items.
+ *
+ * @return array The list of settings admin subnav items.
+ */
+function bp_nouveau_groups_invites_restriction_admin_nav( $wp_admin_nav ) {
+	// Setup the logged in user variables.
+	$settings_link = trailingslashit( bp_loggedin_user_domain() . bp_get_settings_slug() );
+
+	// Add the "Group Invites" subnav item.
+	$wp_admin_nav[] = array(
+		'parent' => 'my-account-' . buddypress()->settings->id,
+		'id'     => 'my-account-' . buddypress()->settings->id . '-invites',
+		'title'  => _x( 'Group Invites', 'Group invitations main menu title', 'buddypress' ),
+		'href'   => trailingslashit( $settings_link . 'invites/' ),
+	);
+
+	return $wp_admin_nav;
+}
+
+/**
+ * Group invites restriction screen.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_screen_invites_restriction() {
+	// Redirect if no invites restriction settings page is accessible.
+	if ( 'invites' !== bp_current_action() || ! bp_is_active( 'friends' ) ) {
+		bp_do_404();
+		return;
+	}
+
+	if ( isset( $_POST['member-group-invites-submit'] ) ) {
+		// Nonce check.
+		check_admin_referer( 'bp_nouveau_group_invites_settings' );
+
+		if ( bp_is_my_profile() || bp_current_user_can( 'bp_moderate' ) ) {
+			if ( empty( $_POST['account-group-invites-preferences'] ) ) {
+				bp_delete_user_meta( bp_displayed_user_id(), '_bp_nouveau_restrict_invites_to_friends' );
+			} else {
+				bp_update_user_meta( bp_displayed_user_id(), '_bp_nouveau_restrict_invites_to_friends', (int) $_POST['account-group-invites-preferences'] );
+			}
+
+			bp_core_add_message( __( 'Group invites preferences saved.', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'You are not allowed to perform this action.', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_settings_slug() ) . 'invites/' );
+	}
+
+	/**
+	 * Filters the template to load for the Group Invites settings screen.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $template Path to the Group Invites settings screen template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_nouveau_groups_screen_invites_restriction', 'members/single/settings/group-invites' ) );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_get_groups_directory_nav_items() {
+	$nav_items = array();
+
+	$nav_items['all'] = array(
+		'component' => 'groups',
+		'slug'      => 'all', // slug is used because BP_Core_Nav requires it, but it's the scope
+		'li_class'  => array( 'selected' ),
+		'link'      => bp_get_groups_directory_permalink(),
+		'text'      => __( 'All Groups', 'buddypress' ),
+		'count'     => bp_get_total_group_count(),
+		'position'  => 5,
+	);
+
+	if ( is_user_logged_in() ) {
+
+		$my_groups_count = bp_get_total_group_count_for_user( bp_loggedin_user_id() );
+
+		// If the user has groups create a nav item
+		if ( $my_groups_count ) {
+			$nav_items['personal'] = array(
+				'component' => 'groups',
+				'slug'      => 'personal', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array(),
+				'link'      => bp_loggedin_user_domain() . bp_get_groups_slug() . '/my-groups/',
+				'text'      => __( 'My Groups', 'buddypress' ),
+				'count'     => $my_groups_count,
+				'position'  => 15,
+			);
+		}
+
+		// If the user can create groups, add the create nav
+		if ( bp_user_can_create_groups() ) {
+			$nav_items['create'] = array(
+				'component' => 'groups',
+				'slug'      => 'create', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array( 'no-ajax', 'group-create', 'create-button' ),
+				'link'      => trailingslashit( bp_get_groups_directory_permalink() . 'create' ),
+				'text'      => __( 'Create a Group', 'buddypress' ),
+				'count'     => false,
+				'position'  => 999,
+			);
+		}
+	}
+
+	// Check for the deprecated hook :
+	$extra_nav_items = bp_nouveau_parse_hooked_dir_nav( 'bp_groups_directory_group_filter', 'groups', 20 );
+
+	if ( ! empty( $extra_nav_items ) ) {
+		$nav_items = array_merge( $nav_items, $extra_nav_items );
+	}
+
+	/**
+	 * Use this filter to introduce your custom nav items for the groups directory.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param  array $nav_items The list of the groups directory nav items.
+	 */
+	return apply_filters( 'bp_nouveau_get_groups_directory_nav_items', $nav_items );
+}
+
+/**
+ * Get Dropdown filters for the groups component
+ *
+ * @since 3.0.0
+ *
+ * @param string $context 'directory' or 'user'
+ *
+ * @return array the filters
+ */
+function bp_nouveau_get_groups_filters( $context = '' ) {
+	if ( empty( $context ) ) {
+		return array();
+	}
+
+	$action = '';
+	if ( 'user' === $context ) {
+		$action = 'bp_member_group_order_options';
+	} elseif ( 'directory' === $context ) {
+		$action = 'bp_groups_directory_order_options';
+	}
+
+	/**
+	 * Recommended, filter here instead of adding an action to 'bp_member_group_order_options'
+	 * or 'bp_groups_directory_order_options'
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array  the members filters.
+	 * @param string the context.
+	 */
+	$filters = apply_filters( 'bp_nouveau_get_groups_filters', array(
+		'active'       => __( 'Last Active', 'buddypress' ),
+		'popular'      => __( 'Most Members', 'buddypress' ),
+		'newest'       => __( 'Newly Created', 'buddypress' ),
+		'alphabetical' => __( 'Alphabetical', 'buddypress' ),
+	), $context );
+
+	if ( $action ) {
+		return bp_nouveau_parse_hooked_options( $action, $filters );
+	}
+
+	return $filters;
+}
+
+/**
+ * Catch the arguments for buttons
+ *
+ * @since 3.0.0
+ *
+ * @param array $button The arguments of the button that BuddyPress is about to create.
+ *
+ * @return array An empty array to stop the button creation process.
+ */
+function bp_nouveau_groups_catch_button_args( $button = array() ) {
+	/**
+	 * Globalize the arguments so that we can use it
+	 * in bp_nouveau_get_groups_buttons().
+	 */
+	bp_nouveau()->groups->button_args = $button;
+
+	// return an empty array to stop the button creation process
+	return array();
+}
+
+/**
+ * Catch the content hooked to the 'bp_group_header_meta' action
+ *
+ * @since 3.0.0
+ *
+ * @return string|bool HTML Output if hooked. False otherwise.
+ */
+function bp_nouveau_get_hooked_group_meta() {
+	ob_start();
+
+	/**
+	 * Fires after inside the group header item meta section.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_group_header_meta' );
+
+	$output = ob_get_clean();
+
+	if ( ! empty( $output ) ) {
+		return $output;
+	}
+
+	return false;
+}
+
+/**
+ * Display the Widgets of Group extensions into the default front page?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True to display. False otherwise.
+ */
+function bp_nouveau_groups_do_group_boxes() {
+	$group_settings = bp_nouveau_get_appearance_settings();
+
+	return ! empty( $group_settings['group_front_page'] ) && ! empty( $group_settings['group_front_boxes'] );
+}
+
+/**
+ * Display description of the Group into the default front page?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True to display. False otherwise.
+ */
+function bp_nouveau_groups_front_page_description() {
+	$group_settings = bp_nouveau_get_appearance_settings();
+
+	// This check is a problem it needs to be used in templates but returns true even if not on the front page
+	// return false on this if we are not displaying the front page 'bp_is_group_home()'
+	// This may well be a bad approach to re-think ~hnla.
+	// @todo
+	return ! empty( $group_settings['group_front_page'] ) && ! empty( $group_settings['group_front_description'] ) && bp_is_group_home();
+}
+
+/**
+ * Add sections to the customizer for the groups component.
+ *
+ * @since 3.0.0
+ *
+ * @param array $sections the Customizer sections to add.
+ *
+ * @return array the Customizer sections to add.
+ */
+function bp_nouveau_groups_customizer_sections( $sections = array() ) {
+	return array_merge( $sections, array(
+		'bp_nouveau_group_front_page' => array(
+			'title'       => __( 'Group front page', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 20,
+			'description' => __( 'Configure the default front page for groups.', 'buddypress' ),
+		),
+		'bp_nouveau_group_primary_nav' => array(
+			'title'       => __( 'Group navigation', 'buddypress' ),
+			'panel'       => 'bp_nouveau_panel',
+			'priority'    => 40,
+			'description' => __( 'Customize the navigation menu for groups. See your changes by navigating to a group in the live-preview window.', 'buddypress' ),
+		),
+	) );
+}
+
+/**
+ * Add settings to the customizer for the groups component.
+ *
+ * @since 3.0.0
+ *
+ * @param array $settings Optional. The settings to add.
+ *
+ * @return array the settings to add.
+ */
+function bp_nouveau_groups_customizer_settings( $settings = array() ) {
+	return array_merge( $settings, array(
+		'bp_nouveau_appearance[group_front_page]' => array(
+			'index'             => 'group_front_page',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[group_front_boxes]' => array(
+			'index'             => 'group_front_boxes',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[group_front_description]' => array(
+			'index'             => 'group_front_description',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[group_nav_display]' => array(
+			'index'             => 'group_nav_display',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[group_nav_tabs]' => array(
+			'index'             => 'group_nav_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[group_subnav_tabs]' => array(
+			'index'             => 'group_subnav_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[groups_create_tabs]' => array(
+			'index'             => 'groups_create_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[group_nav_order]' => array(
+			'index'             => 'group_nav_order',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'bp_nouveau_sanitize_nav_order',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[groups_layout]' => array(
+			'index'             => 'groups_layout',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+		'bp_nouveau_appearance[groups_dir_tabs]' => array(
+			'index'             => 'groups_dir_tabs',
+			'capability'        => 'bp_moderate',
+			'sanitize_callback' => 'absint',
+			'transport'         => 'refresh',
+			'type'              => 'option',
+		),
+	) );
+}
+
+/**
+ * Add controls for the settings of the customizer for the groups component.
+ *
+ * @since 3.0.0
+ *
+ * @param array $controls Optional. The controls to add.
+ *
+ * @return array the controls to add.
+ */
+function bp_nouveau_groups_customizer_controls( $controls = array() ) {
+	return array_merge( $controls, array(
+		'group_front_page' => array(
+			'label'      => __( 'Enable custom front pages for groups.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_front_page',
+			'settings'   => 'bp_nouveau_appearance[group_front_page]',
+			'type'       => 'checkbox',
+		),
+		'group_front_boxes' => array(
+			'label'      => __( 'Enable widget region for group homepages. When enabled, the site admin can add widgets to group pages via the Widgets panel.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_front_page',
+			'settings'   => 'bp_nouveau_appearance[group_front_boxes]',
+			'type'       => 'checkbox',
+		),
+		'group_front_description' => array(
+			'label'      => __( "Display the group description in the body of the group's front page.", 'buddypress' ),
+			'section'    => 'bp_nouveau_group_front_page',
+			'settings'   => 'bp_nouveau_appearance[group_front_description]',
+			'type'       => 'checkbox',
+		),
+		'group_nav_display' => array(
+			'label'      => __( 'Display the group navigation vertically.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[group_nav_display]',
+			'type'       => 'checkbox',
+		),
+		'group_nav_tabs' => array(
+			'label'      => __( 'Use tab styling for primary navigation.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[group_nav_tabs]',
+			'type'       => 'checkbox',
+		),
+		'group_subnav_tabs' => array(
+			'label'      => __( 'Use tab styling for secondary navigation.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[group_subnav_tabs]',
+			'type'       => 'checkbox',
+		),
+		'groups_create_tabs' => array(
+			'label'      => __( 'Use tab styling for the group creation process.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[groups_create_tabs]',
+			'type'       => 'checkbox',
+		),
+		'group_nav_order' => array(
+			'class'       => 'BP_Nouveau_Nav_Customize_Control',
+			'label'      => __( 'Reorder the primary navigation for a group.', 'buddypress' ),
+			'section'    => 'bp_nouveau_group_primary_nav',
+			'settings'   => 'bp_nouveau_appearance[group_nav_order]',
+			'type'       => 'group',
+		),
+		'groups_layout' => array(
+			'label'      => _x( 'Groups', 'Customizer control label', 'buddypress' ),
+			'section'    => 'bp_nouveau_loops_layout',
+			'settings'   => 'bp_nouveau_appearance[groups_layout]',
+			'type'       => 'select',
+			'choices'    => bp_nouveau_customizer_grid_choices(),
+		),
+		'members_group_layout' => array(
+			'label'      => __( 'Group > Members', 'buddypress' ),
+			'section'    => 'bp_nouveau_loops_layout',
+			'settings'   => 'bp_nouveau_appearance[members_group_layout]',
+			'type'       => 'select',
+			'choices'    => bp_nouveau_customizer_grid_choices(),
+		),
+		'group_dir_layout' => array(
+			'label'      => __( 'Use column navigation for the Groups directory.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[groups_dir_layout]',
+			'type'       => 'checkbox',
+		),
+		'group_dir_tabs' => array(
+			'label'      => __( 'Use tab styling for Groups directory navigation.', 'buddypress' ),
+			'section'    => 'bp_nouveau_dir_layout',
+			'settings'   => 'bp_nouveau_appearance[groups_dir_tabs]',
+			'type'       => 'checkbox',
+		),
+	) );
+}
+
+/**
+ * Add the default group front template to the front template hierarchy.
+ *
+ * @since 3.0.0
+ *
+ * @param array           $templates Optional. The list of templates for the front.php template part.
+ * @param BP_Groups_Group $group Optional. The group object.
+ *
+ * @return array The same list with the default front template if needed.
+ */
+function bp_nouveau_group_reset_front_template( $templates = array(), $group = null ) {
+	if ( empty( $group->id ) ) {
+		return $templates;
+	}
+
+	$use_default_front = bp_nouveau_get_appearance_settings( 'group_front_page' );
+
+	// Setting the front template happens too early, so we need this!
+	if ( is_customize_preview() ) {
+		$use_default_front = bp_nouveau_get_temporary_setting( 'group_front_page', $use_default_front );
+	}
+
+	if ( ! empty( $use_default_front ) ) {
+		array_push( $templates, 'groups/single/default-front.php' );
+	}
+
+	/**
+	 * Filters the BuddyPress Nouveau template hierarchy after resetting front template for groups.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $templates Array of templates.
+	 */
+	return apply_filters( '_bp_nouveau_group_reset_front_template', $templates );
+}
+
+/**
+ * Locate a single group template into a specific hierarchy.
+ *
+ * @since 3.0.0
+ *
+ * @param string $template Optional. The template part to get (eg: activity, members...).
+ *
+ * @return string The located template.
+ */
+function bp_nouveau_group_locate_template_part( $template = '' ) {
+	$current_group = groups_get_current_group();
+	$bp_nouveau    = bp_nouveau();
+
+	if ( ! $template || empty( $current_group->id ) ) {
+		return '';
+	}
+
+	// Use a global to avoid requesting the hierarchy for each template
+	if ( ! isset( $bp_nouveau->groups->current_group_hierarchy ) ) {
+		$bp_nouveau->groups->current_group_hierarchy = array(
+			'groups/single/%s-id-' . sanitize_file_name( $current_group->id ) . '.php',
+			'groups/single/%s-slug-' . sanitize_file_name( $current_group->slug ) . '.php',
+		);
+
+		/**
+		 * Check for group types and add it to the hierarchy
+		 */
+		if ( bp_groups_get_group_types() ) {
+			$current_group_type = bp_groups_get_group_type( $current_group->id );
+			if ( ! $current_group_type ) {
+				$current_group_type = 'none';
+			}
+
+			$bp_nouveau->groups->current_group_hierarchy[] = 'groups/single/%s-group-type-' . sanitize_file_name( $current_group_type ) . '.php';
+		}
+
+		$bp_nouveau->groups->current_group_hierarchy = array_merge( $bp_nouveau->groups->current_group_hierarchy, array(
+			'groups/single/%s-status-' . sanitize_file_name( $current_group->status ) . '.php',
+			'groups/single/%s.php'
+		) );
+	}
+
+	// Init the templates
+	$templates = array();
+
+	// Loop in the hierarchy to fill it for the requested template part
+	foreach ( $bp_nouveau->groups->current_group_hierarchy as $part ) {
+		$templates[] = sprintf( $part, sanitize_file_name( $template ) );
+	}
+
+	/**
+	 * Filters the found template parts for the group template part locating functionality.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $templates Array of found templates.
+	 */
+	return bp_locate_template( apply_filters( 'bp_nouveau_group_locate_template_part', $templates ), false, true );
+}
+
+/**
+ * Load a single group template part
+ *
+ * @since 3.0.0
+ *
+ * @param string $template Optional. The template part to get (eg: activity, members...).
+ *
+ * @return string HTML output.
+ */
+function bp_nouveau_group_get_template_part( $template = '' ) {
+	$located = bp_nouveau_group_locate_template_part( $template );
+
+	if ( false !== $located ) {
+		$slug = str_replace( '.php', '', $located );
+		$name = null;
+
+		/**
+		 * Let plugins adding an action to bp_get_template_part get it from here.
+		 *
+		 * This is a variable hook that is dependent on the template part slug.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $slug Template part slug requested.
+		 * @param string $name Template part name requested.
+		 */
+		do_action( 'get_template_part_' . $slug, $slug, $name );
+
+		load_template( $located, true );
+	}
+
+	return $located;
+}
+
+/**
+ * Are we inside the Current group's default front page sidebar?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if in the group's home sidebar. False otherwise.
+ */
+function bp_nouveau_group_is_home_widgets() {
+	return ( true === bp_nouveau()->groups->is_group_home_sidebar );
+}
+
+/**
+ * Filter the Latest activities Widget to only keep the one of the group displayed
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. The Activities Template arguments.
+ *
+ * @return array The Activities Template arguments.
+ */
+function bp_nouveau_group_activity_widget_overrides( $args = array() ) {
+	return array_merge( $args, array(
+		'object'     => 'groups',
+		'primary_id' => bp_get_current_group_id(),
+	) );
+}
+
+/**
+ * Filter the Groups widget to only keep the displayed group.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. The Groups Template arguments.
+ *
+ * @return array The Groups Template arguments.
+ */
+function bp_nouveau_group_groups_widget_overrides( $args = array() ) {
+	return array_merge( $args, array(
+		'include' => bp_get_current_group_id(),
+	) );
+}
+
+/**
+ * Filter the Members widgets to only keep members of the displayed group.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. The Members Template arguments.
+ *
+ * @return array The Members Template arguments.
+ */
+function bp_nouveau_group_members_widget_overrides( $args = array() ) {
+	$group_members = groups_get_group_members( array( 'exclude_admins_mods' => false ) );
+
+	if ( empty( $group_members['members'] ) ) {
+		return $args;
+	}
+
+	return array_merge( $args, array(
+		'include' => wp_list_pluck( $group_members['members'], 'ID' ),
+	) );
+}
+
+/**
+ * Init the Group's default front page filters as we're in the sidebar
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_add_home_widget_filters() {
+	add_filter( 'bp_nouveau_activity_widget_query', 'bp_nouveau_group_activity_widget_overrides', 10, 1 );
+	add_filter( 'bp_before_has_groups_parse_args', 'bp_nouveau_group_groups_widget_overrides', 10, 1 );
+	add_filter( 'bp_before_has_members_parse_args', 'bp_nouveau_group_members_widget_overrides', 10, 1 );
+
+	/**
+	 * Fires after BuddyPress Nouveau groups have added their home widget filters.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_nouveau_groups_add_home_widget_filters' );
+}
+
+/**
+ * Remove the Group's default front page filters as we're no more in the sidebar
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_remove_home_widget_filters() {
+	remove_filter( 'bp_nouveau_activity_widget_query', 'bp_nouveau_group_activity_widget_overrides', 10, 1 );
+	remove_filter( 'bp_before_has_groups_parse_args', 'bp_nouveau_group_groups_widget_overrides', 10, 1 );
+	remove_filter( 'bp_before_has_members_parse_args', 'bp_nouveau_group_members_widget_overrides', 10, 1 );
+
+	/**
+	 * Fires after BuddyPress Nouveau groups have removed their home widget filters.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_nouveau_groups_remove_home_widget_filters' );
+}
+
+/**
+ * Get the hook, nonce, and eventually a specific template for Core Group's create screens.
+ *
+ * @since 3.0.0
+ *
+ * @param string $id Optional. The screen id
+ *
+ * @return mixed An array containing the hook dynamic part, the nonce, and eventually a specific template.
+ *               False if it's not a core create screen.
+ */
+function bp_nouveau_group_get_core_create_screens( $id = '' ) {
+	// screen id => dynamic part of the hooks, nonce & specific template to use.
+	$screens = array(
+		'group-details' => array(
+			'hook'     => 'group_details_creation_step',
+			'nonce'    => 'groups_create_save_group-details',
+			'template' => 'groups/single/admin/edit-details',
+		),
+		'group-settings' => array(
+			'hook'  => 'group_settings_creation_step',
+			'nonce' => 'groups_create_save_group-settings',
+		),
+		'group-avatar' => array(
+			'hook'  => 'group_avatar_creation_step',
+			'nonce' => 'groups_create_save_group-avatar',
+		),
+		'group-cover-image' => array(
+			'hook'  => 'group_cover_image_creation_step',
+			'nonce' => 'groups_create_save_group-cover-image',
+		),
+		'group-invites' => array(
+			'hook'     => 'group_invites_creation_step',
+			'nonce'    => 'groups_create_save_group-invites',
+			'template' => 'common/js-templates/invites/index',
+		),
+	);
+
+	if ( isset( $screens[ $id ] ) ) {
+		return $screens[ $id ];
+	}
+
+	return false;
+}
+
+/**
+ * Get the hook and nonce for Core Group's manage screens.
+ *
+ * @since 3.0.0
+ *
+ * @param string $id Optional. The screen id
+ *
+ * @return mixed An array containing the hook dynamic part and the nonce.
+ *               False if it's not a core manage screen.
+ */
+function bp_nouveau_group_get_core_manage_screens( $id = '' ) {
+	// screen id => dynamic part of the hooks & nonce.
+	$screens = array(
+		'edit-details'        => array( 'hook' => 'group_details_admin',             'nonce' => 'groups_edit_group_details'  ),
+		'group-settings'      => array( 'hook' => 'group_settings_admin',            'nonce' => 'groups_edit_group_settings' ),
+		'group-avatar'        => array(),
+		'group-cover-image'   => array( 'hook' => 'group_settings_cover_image',      'nonce' => ''                           ),
+		'manage-members'      => array( 'hook' => 'group_manage_members_admin',      'nonce' => ''                           ),
+		'membership-requests' => array( 'hook' => 'group_membership_requests_admin', 'nonce' => ''                           ),
+		'delete-group'        => array( 'hook' => 'group_delete_admin',              'nonce' => 'groups_delete_group'        ),
+	);
+
+	if ( isset( $screens[ $id ] ) ) {
+		return $screens[ $id ];
+	}
+
+	return false;
+}
+
+/**
+ * Register notifications filters for the groups component.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_notification_filters() {
+	$notifications = array(
+		array(
+			'id'       => 'new_membership_request',
+			'label'    => __( 'Pending Group membership requests', 'buddypress' ),
+			'position' => 55,
+		),
+		array(
+			'id'       => 'membership_request_accepted',
+			'label'    => __( 'Accepted Group membership requests', 'buddypress' ),
+			'position' => 65,
+		),
+		array(
+			'id'       => 'membership_request_rejected',
+			'label'    => __( 'Rejected Group membership requests', 'buddypress' ),
+			'position' => 75,
+		),
+		array(
+			'id'       => 'member_promoted_to_admin',
+			'label'    => __( 'Group Administrator promotions', 'buddypress' ),
+			'position' => 85,
+		),
+		array(
+			'id'       => 'member_promoted_to_mod',
+			'label'    => __( 'Group Moderator promotions', 'buddypress' ),
+			'position' => 95,
+		),
+		array(
+			'id'       => 'group_invite',
+			'label'    => __( 'Group invitations', 'buddypress' ),
+			'position' => 105,
+		),
+	);
+
+	foreach ( $notifications as $notification ) {
+		bp_nouveau_notifications_register_filter( $notification );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/loader.php	(revision 12265)
@@ -0,0 +1,185 @@
+<?php
+/**
+ * BP Nouveau Groups
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Groups Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Groups {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir                   = trailingslashit( dirname( __FILE__ ) );
+		$this->is_group_home_sidebar = false;
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require $this->dir . 'functions.php';
+		require $this->dir . 'classes.php';
+		require $this->dir . 'template-tags.php';
+
+		// Test suite requires the AJAX functions early.
+		if ( function_exists( 'tests_add_filter' ) ) {
+			require $this->dir . 'ajax.php';
+
+		// Load AJAX code only on AJAX requests.
+		} else {
+			add_action( 'admin_init', function() {
+				if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && 0 === strpos( $_REQUEST['action'], 'groups_' ) ) {
+					require bp_nouveau()->groups->dir . 'ajax.php';
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		if ( ! is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+			add_action( 'groups_setup_nav', 'bp_nouveau_group_setup_nav' );
+		}
+
+		add_action( 'bp_nouveau_enqueue_scripts', 'bp_nouveau_groups_enqueue_scripts' );
+
+		// Avoid Notices for BuddyPress Legacy Backcompat
+		remove_action( 'bp_groups_directory_group_filter', 'bp_group_backcompat_create_nav_item', 1000 );
+
+		// Register the Groups Notifications filters
+		add_action( 'bp_nouveau_notifications_init_filters', 'bp_nouveau_groups_notification_filters' );
+
+		// Actions to check whether we are in the Group's default front page sidebar
+		add_action( 'dynamic_sidebar_before', array( $this, 'group_home_sidebar_set' ), 10, 1 );
+		add_action( 'dynamic_sidebar_after', array( $this, 'group_home_sidebar_unset' ), 10, 1 );
+
+		// Add a new nav item to settings to let users choose their group invites preferences
+		if ( bp_is_active( 'friends' ) && ! bp_nouveau_groups_disallow_all_members_invites() ) {
+			add_action( 'bp_settings_setup_nav', 'bp_nouveau_groups_invites_restriction_nav' );
+		}
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		add_filter( 'bp_nouveau_register_scripts', 'bp_nouveau_groups_register_scripts', 10, 1 );
+		add_filter( 'bp_core_get_js_strings', 'bp_nouveau_groups_localize_scripts', 10, 1 );
+		add_filter( 'groups_create_group_steps', 'bp_nouveau_group_invites_create_steps', 10, 1 );
+
+		$buttons = array(
+			'groups_leave_group',
+			'groups_join_group',
+			'groups_accept_invite',
+			'groups_reject_invite',
+			'groups_membership_requested',
+			'groups_request_membership',
+			'groups_group_membership',
+		);
+
+		foreach ( $buttons as $button ) {
+			add_filter( 'bp_button_' . $button, 'bp_nouveau_ajax_button', 10, 5 );
+		}
+
+		// Add sections in the BP Template Pack panel of the customizer.
+		add_filter( 'bp_nouveau_customizer_sections', 'bp_nouveau_groups_customizer_sections', 10, 1 );
+
+		// Add settings into the Groups sections of the customizer.
+		add_filter( 'bp_nouveau_customizer_settings', 'bp_nouveau_groups_customizer_settings', 10, 1 );
+
+		// Add controls into the Groups sections of the customizer.
+		add_filter( 'bp_nouveau_customizer_controls', 'bp_nouveau_groups_customizer_controls', 10, 1 );
+
+		// Add the group's default front template to hieararchy if user enabled it (Enabled by default).
+		add_filter( 'bp_groups_get_front_template', 'bp_nouveau_group_reset_front_template', 10, 2 );
+
+		// Add a new nav item to settings to let users choose their group invites preferences
+		if ( bp_is_active( 'friends' ) && ! bp_nouveau_groups_disallow_all_members_invites() ) {
+			add_filter( 'bp_settings_admin_nav', 'bp_nouveau_groups_invites_restriction_admin_nav', 10, 1 );
+		}
+	}
+
+	/**
+	 * Add filters to be sure the (BuddyPress) widgets display will be consistent
+	 * with the current group's default front page.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $sidebar_index The Sidebar identifier.
+	 */
+	public function group_home_sidebar_set( $sidebar_index = '' ) {
+		if ( 'sidebar-buddypress-groups' !== $sidebar_index ) {
+			return;
+		}
+
+		$this->is_group_home_sidebar = true;
+
+		// Add needed filters.
+		bp_nouveau_groups_add_home_widget_filters();
+	}
+
+	/**
+	 * Remove filters to be sure the (BuddyPress) widgets display will no more take
+	 * the current group displayed in account.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $sidebar_index The Sidebar identifier.
+	 */
+	public function group_home_sidebar_unset( $sidebar_index = '' ) {
+		if ( 'sidebar-buddypress-groups' !== $sidebar_index ) {
+			return;
+		}
+
+		$this->is_group_home_sidebar = false;
+
+		// Remove no more needed filters.
+		bp_nouveau_groups_remove_home_widget_filters();
+	}
+}
+
+/**
+ * Launch the Groups loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->groups = new BP_Nouveau_Groups();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_groups', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/groups/template-tags.php	(revision 12265)
@@ -0,0 +1,1390 @@
+<?php
+/**
+ * Groups Template tags
+ *
+ * @since 3.0.0
+ * @version 3.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Template tag to wrap all Legacy actions that was used
+ * before the groups directory content
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_before_groups_directory_content() {
+	/**
+	 * Fires at the begining of the templates BP injected content.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_before_directory_groups_page' );
+
+	/**
+	 * Fires before the display of the groups.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_groups' );
+
+	/**
+	 * Fires before the display of the groups content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_groups_content' );
+}
+
+/**
+ * Template tag to wrap all Legacy actions that was used
+ * after the groups directory content
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_after_groups_directory_content() {
+	/**
+	 * Fires and displays the group content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_directory_groups_content' );
+
+	/**
+	 * Fires after the display of the groups content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_groups_content' );
+
+	/**
+	 * Fires after the display of the groups.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_groups' );
+
+	/**
+	 * Fires at the bottom of the groups directory template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_groups_page' );
+}
+
+/**
+ * Fire specific hooks into the groups create template.
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   Optional. Either 'before' or 'after'.
+ * @param string $suffix Optional. Use it to add terms at the end of the hook name.
+ */
+function bp_nouveau_groups_create_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a create group hook
+	$hook[] = 'create_group';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Fire specific hooks into the single groups templates.
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   Optional. Either 'before' or 'after'.
+ * @param string $suffix Optional. Use it to add terms at the end of the hook name.
+ */
+function bp_nouveau_group_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a group hook
+	$hook[] = 'group';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Fire an isolated hook inside the groups loop
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_loop_item() {
+	/**
+	 * Fires inside the listing of an individual group listing item.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_directory_groups_item' );
+}
+
+/**
+ * Display the current group activity post form if needed
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_groups_activity_post_form() {
+	/**
+	 * Fires before the display of the group activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_group_activity_post_form' );
+
+	if ( is_user_logged_in() && bp_group_is_member() ) {
+		bp_get_template_part( 'activity/post-form' );
+	}
+
+	/**
+	 * Fires after the display of the group activity post form.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_group_activity_post_form' );
+}
+
+/**
+ * Load the Group Invites UI.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output.
+ */
+function bp_nouveau_group_invites_interface() {
+	/**
+	 * Fires before the send invites content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_group_send_invites_content' );
+
+	bp_get_template_part( 'common/js-templates/invites/index' );
+
+	/**
+	 * Fires after the send invites content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_group_send_invites_content' );
+}
+
+/**
+ * Gets the displayed user group invites preferences
+ *
+ * @since 3.0.0
+ *
+ * @return int Returns 1 if user chose to restrict to friends, 0 otherwise.
+ */
+function bp_nouveau_groups_get_group_invites_setting() {
+	return (int) bp_get_user_meta( bp_displayed_user_id(), '_bp_nouveau_restrict_invites_to_friends' );
+}
+
+/**
+ * Outputs the group creation numbered steps navbar
+ *
+ * @since 3.0.0
+ *
+ * @todo This output isn't localised correctly.
+ */
+function bp_nouveau_group_creation_tabs() {
+	$bp = buddypress();
+
+	if ( ! is_array( $bp->groups->group_creation_steps ) ) {
+		return;
+	}
+
+	if ( ! bp_get_groups_current_create_step() ) {
+		$keys                            = array_keys( $bp->groups->group_creation_steps );
+		$bp->groups->current_create_step = array_shift( $keys );
+	}
+
+	$counter = 1;
+
+	foreach ( (array) $bp->groups->group_creation_steps as $slug => $step ) {
+		$is_enabled = bp_are_previous_group_creation_steps_complete( $slug ); ?>
+
+		<li<?php if ( bp_get_groups_current_create_step() === $slug ) : ?> class="current"<?php endif; ?>>
+			<?php if ( $is_enabled ) : ?>
+				<a href="<?php echo esc_url( bp_groups_directory_permalink() . 'create/step/' . $slug . '/' ); ?>">
+					<?php echo (int) $counter; ?> <?php echo esc_html( $step['name'] ); ?>
+				</a>
+			<?php else : ?>
+				<?php echo (int) $counter; ?>. <?php echo esc_html( $step['name'] ); ?>
+			<?php endif ?>
+		</li>
+			<?php
+		$counter++;
+	}
+
+	unset( $is_enabled );
+
+	/**
+	 * Fires at the end of the creation of the group tabs.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'groups_creation_tabs' );
+}
+
+/**
+ * Load the requested Create Screen for the new group.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_group_creation_screen() {
+	return bp_nouveau_group_manage_screen();
+}
+
+/**
+ * Load the requested Manage Screen for the current group.
+ *
+ * @since 3.0.0
+ */
+
+function bp_nouveau_group_manage_screen() {
+	$action          = bp_action_variable( 0 );
+	$is_group_create = bp_is_group_create();
+	$output          = '';
+
+	if ( $is_group_create ) {
+		$action = bp_action_variable( 1 );
+	}
+
+	$screen_id = sanitize_file_name( $action );
+	if ( ! bp_is_group_admin_screen( $screen_id ) && ! bp_is_group_creation_step( $screen_id ) ) {
+		return;
+	}
+
+	if ( ! $is_group_create ) {
+		/**
+		 * Fires inside the group admin form and before the content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_group_admin_content' );
+
+		$core_screen = bp_nouveau_group_get_core_manage_screens( $screen_id );
+
+	// It's a group step, get the creation screens.
+	} else {
+		$core_screen = bp_nouveau_group_get_core_create_screens( $screen_id );
+	}
+
+	if ( ! $core_screen ) {
+		if ( ! $is_group_create ) {
+			/**
+			 * Fires inside the group admin template.
+			 *
+			 * Allows plugins to add custom group edit screens.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'groups_custom_edit_steps' );
+
+		// Else use the group create hook
+		} else {
+			/**
+			 * Fires inside the group admin template.
+			 *
+			 * Allows plugins to add custom group creation steps.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'groups_custom_create_steps' );
+		}
+
+	// Else we load the core screen.
+	} else {
+		if ( ! empty( $core_screen['hook'] ) ) {
+			/**
+			 * Fires before the display of group delete admin.
+			 *
+			 * @since 1.1.0 For most hooks.
+			 * @since 2.4.0 For the cover image hook.
+			 */
+			do_action( 'bp_before_' . $core_screen['hook'] );
+		}
+
+		$template = 'groups/single/admin/' . $screen_id;
+
+		if ( ! empty( $core_screen['template'] ) ) {
+			$template = $core_screen['template'];
+		}
+
+		bp_get_template_part( $template );
+
+		if ( ! empty( $core_screen['hook'] ) ) {
+			/**
+			 * Fires before the display of group delete admin.
+			 *
+			 * @since 1.1.0 For most hooks.
+			 * @since 2.4.0 For the cover image hook.
+			 */
+			do_action( 'bp_after_' . $core_screen['hook'] );
+		}
+
+		if ( ! empty( $core_screen['nonce'] ) ) {
+			if ( ! $is_group_create ) {
+				$output = sprintf( '<p><input type="submit" value="%s" id="save" name="save" /></p>', esc_attr__( 'Save Changes', 'buddypress' ) );
+
+				// Specific case for the delete group screen
+				if ( 'delete-group' === $screen_id ) {
+					$output = sprintf(
+						'<div class="submit">
+							<input type="submit" disabled="disabled" value="%s" id="delete-group-button" name="delete-group-button" />
+						</div>',
+						esc_attr__( 'Delete Group', 'buddypress' )
+					);
+				}
+			}
+		}
+	}
+
+	if ( $is_group_create ) {
+		/**
+		 * Fires before the display of the group creation step buttons.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_group_creation_step_buttons' );
+
+		if ( 'crop-image' !== bp_get_avatar_admin_step() ) {
+			$creation_step_buttons = '';
+
+			if ( ! bp_is_first_group_creation_step() ) {
+				$creation_step_buttons .= sprintf(
+					'<input type="button" value="%1$s" id="group-creation-previous" name="previous" onclick="%2$s" />',
+					esc_attr__( 'Back to Previous Step', 'buddypress' ),
+					"location.href='" . esc_js( esc_url_raw( bp_get_group_creation_previous_link() ) ) . "'"
+				);
+			}
+
+			if ( ! bp_is_last_group_creation_step() && ! bp_is_first_group_creation_step() ) {
+				$creation_step_buttons .= sprintf(
+					'<input type="submit" value="%s" id="group-creation-next" name="save" />',
+					esc_attr__( 'Next Step', 'buddypress' )
+				);
+			}
+
+			if ( bp_is_first_group_creation_step() ) {
+				$creation_step_buttons .= sprintf(
+					'<input type="submit" value="%s" id="group-creation-create" name="save" />',
+					esc_attr__( 'Create Group and Continue', 'buddypress' )
+				);
+			}
+
+			if ( bp_is_last_group_creation_step() ) {
+				$creation_step_buttons .= sprintf(
+					'<input type="submit" value="%s" id="group-creation-finish" name="save" />',
+					esc_attr__( 'Finish', 'buddypress' )
+				);
+			}
+
+			// Set the output for the buttons
+			$output = sprintf( '<div class="submit" id="previous-next">%s</div>', $creation_step_buttons );
+		}
+
+		/**
+		 * Fires after the display of the group creation step buttons.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_group_creation_step_buttons' );
+	}
+
+	/**
+	 * Avoid nested forms with the Backbone views for the group invites step.
+	 */
+	if ( 'group-invites' === bp_get_groups_current_create_step() ) {
+		printf(
+			'<form action="%s" method="post" enctype="multipart/form-data">',
+			bp_get_group_creation_form_action()
+		);
+	}
+
+	if ( ! empty( $core_screen['nonce'] ) ) {
+		wp_nonce_field( $core_screen['nonce'] );
+	}
+
+	printf(
+		'<input type="hidden" name="group-id" id="group-id" value="%s" />',
+		$is_group_create ? esc_attr( bp_get_new_group_id() ) : esc_attr( bp_get_group_id() )
+	);
+
+	// The submit actions
+	echo $output;
+
+	if ( ! $is_group_create ) {
+		/**
+		 * Fires inside the group admin form and after the content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_group_admin_content' );
+
+	} else {
+		/**
+		 * Fires and displays the groups directory content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_directory_groups_content' );
+	}
+
+	/**
+	 * Avoid nested forms with the Backbone views for the group invites step.
+	 */
+	if ( 'group-invites' === bp_get_groups_current_create_step() ) {
+		echo '</form>';
+	}
+}
+
+/**
+ * Output the action buttons for the displayed group
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_group_header_buttons( $args = array() ) {
+	$bp_nouveau = bp_nouveau();
+
+	$output = join( ' ', bp_nouveau_get_groups_buttons( $args ) );
+
+	// On the group's header we need to reset the group button's global.
+	if ( ! empty( $bp_nouveau->groups->group_buttons ) ) {
+		unset( $bp_nouveau->groups->group_buttons );
+	}
+
+	ob_start();
+	/**
+	 * Fires in the group header actions section.
+	 *
+	 * @since 1.2.6
+	 */
+	do_action( 'bp_group_header_actions' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	if ( ! $args ) {
+		$args = array( 'classes' => array( 'item-buttons' ) );
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+/**
+ * Output the action buttons inside the groups loop.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_groups_loop_buttons( $args = array() ) {
+	if ( empty( $GLOBALS['groups_template'] ) ) {
+		return;
+	}
+
+	$args['type'] = 'loop';
+
+	$output = join( ' ', bp_nouveau_get_groups_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires inside the action section of an individual group listing item.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_directory_groups_actions' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+/**
+ * Output the action buttons inside the invites loop of the displayed user.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_groups_invite_buttons( $args = array() ) {
+	if ( empty( $GLOBALS['groups_template'] ) ) {
+		return;
+	}
+
+	$args['type'] = 'invite';
+
+	$output = join( ' ', bp_nouveau_get_groups_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires inside the member group item action markup.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_group_invites_item_action' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+/**
+ * Output the action buttons inside the requests loop of the group's manage screen.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_groups_request_buttons( $args = array() ) {
+	if ( empty( $GLOBALS['requests_template'] ) ) {
+		return;
+	}
+
+	$args['type'] = 'request';
+
+	$output = join( ' ', bp_nouveau_get_groups_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires inside the list of membership request actions.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_group_membership_requests_admin_item_action' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+/**
+ * Output the action buttons inside the manage members loop of the group's manage screen.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_groups_manage_members_buttons( $args = array() ) {
+	if ( empty( $GLOBALS['members_template'] ) ) {
+		return;
+	}
+
+	$args['type'] = 'manage_members';
+
+	$output = join( ' ', bp_nouveau_get_groups_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires inside the display of a member admin item in group management area.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_group_manage_members_admin_item' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	if ( ! $args ) {
+		$args = array(
+			'wrapper' => 'span',
+			'classes' => array( 'small' ),
+		);
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+	/**
+	 * Get the action buttons for the current group in the loop,
+	 * or the current displayed group.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args Optional. See bp_nouveau_wrapper() for the description of parameters.
+	 */
+	function bp_nouveau_get_groups_buttons( $args = array() ) {
+		$type = ( ! empty( $args['type'] ) ) ? $args['type'] : 'group';
+
+		// @todo Not really sure why BP Legacy needed to do this...
+		if ( 'group' === $type && is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+			return;
+		}
+
+		$buttons = array();
+
+		if ( ( 'loop' === $type || 'invite' === $type ) && isset( $GLOBALS['groups_template']->group ) ) {
+			$group = $GLOBALS['groups_template']->group;
+		} else {
+			$group = groups_get_current_group();
+		}
+
+		if ( empty( $group->id ) ) {
+			return $buttons;
+		}
+
+		/*
+		 * If the 'container' is set to 'ul' set $parent_element to li,
+		 * otherwise simply pass any value found in $args or set var false.
+		 */
+		if ( ! empty( $args['container'] ) && 'ul' === $args['container']  ) {
+			$parent_element = 'li';
+		} elseif ( ! empty( $args['parent_element'] ) ) {
+			$parent_element = $args['parent_element'];
+		} else {
+			$parent_element = false;
+		}
+
+		/*
+		 * If we have a arg value for $button_element passed through
+		 * use it to default all the $buttons['button_element'] values
+		 * otherwise default to 'a' (anchor) o override & hardcode the
+		 * 'element' string on $buttons array.
+		 *
+		 * Icons sets a class for icon display if not using the button element
+		 */
+		$icons = '';
+		if ( ! empty( $args['button_element'] ) ) {
+			$button_element = $args['button_element'] ;
+		} else {
+			$button_element = 'a';
+			$icons = ' icons';
+		}
+
+		// If we pass through parent classes add them to $button array
+		$parent_class = '';
+		if ( ! empty( $args['parent_attr']['class'] ) ) {
+			$parent_class = $args['parent_attr']['class'];
+		}
+
+		// Invite buttons on member's invites screen
+		if ( 'invite' === $type ) {
+			// Don't show button if not logged in or previously banned
+			if ( ! is_user_logged_in() || bp_group_is_user_banned( $group ) || empty( $group->status ) ) {
+				return $buttons;
+			}
+
+			// Setup Accept button attributes
+			$buttons['accept_invite'] =  array(
+				'id'                => 'accept_invite',
+				'position'          => 5,
+				'component'         => 'groups',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'link_text'         => esc_html__( 'Accept', 'buddypress' ),
+				'button_element'    => $button_element,
+				'parent_attr'       => array(
+					'id'    => '',
+					'class' => $parent_class . ' ' . 'accept',
+				),
+				'button_attr'       => array(
+					'id'    => '',
+					'class' => 'button accept group-button accept-invite',
+					'rel'   => '',
+				),
+			);
+
+			// If button element set add nonce link to data-attr attr
+			if ( 'button' === $button_element ) {
+				$buttons['accept_invite']['button_attr']['data-bp-nonce'] = esc_url( bp_get_group_accept_invite_link() );
+			} else {
+				$buttons['accept_invite']['button_attr']['href'] = esc_url( bp_get_group_accept_invite_link() );
+			}
+
+			// Setup Reject button attributes
+			$buttons['reject_invite'] = array(
+				'id'                => 'reject_invite',
+				'position'          => 15,
+				'component'         => 'groups',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'link_text'         => __( 'Reject', 'buddypress' ),
+				'parent_attr'       => array(
+					'id'    => '',
+					'class' => $parent_class . ' ' . 'reject',
+				),
+				'button_element'    => $button_element,
+				'button_attr'       => array(
+					'id'    => '',
+					'class' => 'button reject group-button reject-invite',
+					'rel'   => '',
+				),
+			);
+
+			// If button element set add nonce link to formaction attr
+			if ( 'button' === $button_element ) {
+				$buttons['reject_invite']['button_attr']['data-bp-nonce'] = esc_url( bp_get_group_reject_invite_link() );
+			} else {
+				$buttons['reject_invite']['button_attr']['href'] = esc_url( bp_get_group_reject_invite_link() );
+			}
+
+		// Request button for the group's manage screen
+		} elseif ( 'request' === $type ) {
+			// Setup Accept button attributes
+			$buttons['group_membership_accept'] =  array(
+				'id'                => 'group_membership_accept',
+				'position'          => 5,
+				'component'         => 'groups',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'link_text'         => esc_html__( 'Accept', 'buddypress' ),
+				'button_element'    => $button_element,
+				'parent_attr'       => array(
+					'id'    => '',
+					'class' => $parent_class,
+				),
+				'button_attr'       => array(
+					'id'    => '',
+					'class' => 'button accept',
+					'rel'   => '',
+				),
+			);
+
+			// If button element set add nonce link to data-attr attr
+			if ( 'button' === $button_element ) {
+				$buttons['group_membership_accept']['button_attr']['data-bp-nonce'] = esc_url( bp_get_group_request_accept_link() );
+			} else {
+				$buttons['group_membership_accept']['button_attr']['href'] = esc_url( bp_get_group_request_accept_link() );
+			}
+
+			$buttons['group_membership_reject'] = array(
+				'id'                => 'group_membership_reject',
+				'position'          => 15,
+				'component'         => 'groups',
+				'must_be_logged_in' => true,
+				'parent_element'    => $parent_element,
+				'button_element'    => $button_element,
+				'link_text'         => __( 'Reject', 'buddypress' ),
+				'parent_attr'       => array(
+					'id'    => '',
+					'class' => $parent_class,
+				),
+				'button_attr'       => array(
+					'id'    => '',
+					'class' => 'button reject',
+					'rel'   => '',
+				),
+			);
+
+			// If button element set add nonce link to data-attr attr
+			if ( 'button' === $button_element ) {
+				$buttons['group_membership_reject']['button_attr']['data-bp-nonce'] = esc_url( bp_get_group_request_reject_link() );
+			} else {
+				$buttons['group_membership_reject']['button_attr']['href'] = esc_url( bp_get_group_request_reject_link() );
+			}
+
+		/*
+		 * Manage group members for the group's manage screen.
+		 * The 'button_attr' keys 'href' & 'formaction' are set at the end of this array block
+		 */
+		} elseif ( 'manage_members' === $type && isset( $GLOBALS['members_template']->member->user_id ) ) {
+			$user_id = $GLOBALS['members_template']->member->user_id;
+
+			$buttons = array(
+				'unban_member' => array(
+					'id'                => 'unban_member',
+					'position'          => 5,
+					'component'         => 'groups',
+					'must_be_logged_in' => true,
+					'parent_element'    => $parent_element,
+					'button_element'    => $button_element,
+					'link_text'         => __( 'Remove Ban', 'buddypress' ),
+					'parent_attr'       => array(
+						'id'    => '',
+						'class' => $parent_class,
+					),
+					'button_attr'       => array(
+						'id'    => '',
+						'class' => 'button confirm member-unban',
+						'rel'   => '',
+						'title' => '',
+					),
+				),
+				'ban_member' => array(
+					'id'                => 'ban_member',
+					'position'          => 15,
+					'component'         => 'groups',
+					'must_be_logged_in' => true,
+					'parent_element'    => $parent_element,
+					'button_element'    => $button_element,
+					'link_text'         => __( 'Kick &amp; Ban', 'buddypress' ),
+					'parent_attr'       => array(
+						'id'    => '',
+						'class' => $parent_class,
+					),
+					'button_attr'       => array(
+						'id'    => '',
+						'class' => 'button confirm member-ban',
+						'rel'   => '',
+						'title' => '',
+					),
+				),
+				'promote_mod' => array(
+					'id'                => 'promote_mod',
+					'position'          => 25,
+					'component'         => 'groups',
+					'must_be_logged_in' => true,
+					'parent_element'    => $parent_element,
+					'parent_attr'       => array(
+						'id'    => '',
+						'class' => $parent_class,
+					),
+					'button_element'    => $button_element,
+					'button_attr'       => array(
+						'id'               => '',
+						'class'            => 'button confirm member-promote-to-mod',
+						'rel'              => '',
+						'title'            => '',
+					),
+					'link_text'         => __( 'Promote to Mod', 'buddypress' ),
+				),
+				'promote_admin' => array(
+					'id'                => 'promote_admin',
+					'position'          => 35,
+					'component'         => 'groups',
+					'must_be_logged_in' => true,
+					'parent_element'    => $parent_element,
+					'button_element'    => $button_element,
+					'link_text'         => __( 'Promote to Admin', 'buddypress' ),
+					'parent_attr'       => array(
+						'id'    => '',
+						'class' => $parent_class,
+					),
+					'button_attr'       => array(
+						'href'  => esc_url( bp_get_group_member_promote_admin_link() ),
+						'id'    => '',
+						'class' => 'button confirm member-promote-to-admin',
+						'rel'   => '',
+						'title' => '',
+					),
+				),
+				'remove_member' => array(
+					'id'                => 'remove_member',
+					'position'          => 45,
+					'component'         => 'groups',
+					'must_be_logged_in' => true,
+					'parent_element'    => $parent_element,
+					'button_element'    => $button_element,
+					'link_text'         => __( 'Remove from group', 'buddypress' ),
+					'parent_attr'       => array(
+						'id'    => '',
+						'class' => $parent_class,
+					),
+					'button_attr'       => array(
+						'id'    => '',
+						'class' => 'button confirm',
+						'rel'   => '',
+						'title' => '',
+					),
+				),
+			);
+
+			// If 'button' element is set add the nonce link to data-attr attr, else add it to the href.
+			if ( 'button' === $button_element ) {
+				$buttons['unban_member']['button_attr']['data-bp-nonce'] = bp_get_group_member_unban_link( $user_id );
+				$buttons['ban_member']['button_attr']['data-bp-nonce'] = bp_get_group_member_ban_link( $user_id );
+				$buttons['promote_mod']['button_attr']['data-bp-nonce'] = bp_get_group_member_promote_mod_link();
+				$buttons['promote_admin']['button_attr']['data-bp-nonce'] = bp_get_group_member_promote_admin_link();
+				$buttons['remove_member']['button_attr']['data-bp-nonce'] = bp_get_group_member_remove_link( $user_id );
+			} else {
+				$buttons['unban_member']['button_attr']['href'] = bp_get_group_member_unban_link( $user_id );
+				$buttons['ban_member']['button_attr']['href'] = bp_get_group_member_ban_link( $user_id );
+				$buttons['promote_mod']['button_attr']['href'] = bp_get_group_member_promote_mod_link();
+				$buttons['promote_admin']['button_attr']['href'] = bp_get_group_member_promote_admin_link();
+				$buttons['remove_member']['button_attr']['href'] = bp_get_group_member_remove_link( $user_id );
+			}
+
+		// Membership button on groups loop or single group's header
+		} else {
+			/*
+			 * This filter workaround is waiting for a core adaptation
+			 * so that we can directly get the groups button arguments
+			 * instead of the button.
+			 *
+			 * See https://buddypress.trac.wordpress.org/ticket/7126
+			 */
+			add_filter( 'bp_get_group_join_button', 'bp_nouveau_groups_catch_button_args', 100, 1 );
+
+			bp_get_group_join_button( $group );
+
+			remove_filter( 'bp_get_group_join_button', 'bp_nouveau_groups_catch_button_args', 100, 1 );
+
+			if ( ! empty( bp_nouveau()->groups->button_args ) ) {
+				$button_args = bp_nouveau()->groups->button_args;
+
+				// If we pass through parent classes merge those into the existing ones
+				if ( $parent_class ) {
+					$parent_class .= ' ' . $button_args['wrapper_class'];
+				}
+
+				// The join or leave group header button should default to 'button'
+				// Reverse the earler button var to set default as 'button' not 'a'
+				if ( empty( $args['button_element'] ) ) {
+					$button_element = 'button';
+				}
+
+				$buttons['group_membership'] = array(
+					'id'                => 'group_membership',
+					'position'          => 5,
+					'component'         => $button_args['component'],
+					'must_be_logged_in' => $button_args['must_be_logged_in'],
+					'block_self'        => $button_args['block_self'],
+					'parent_element'    => $parent_element,
+					'button_element'    => $button_element,
+					'link_text'         => $button_args['link_text'],
+					'parent_attr'       => array(
+							'id'    => $button_args['wrapper_id'],
+							'class' => $parent_class,
+					),
+					'button_attr'       => array(
+						'id'    => ! empty( $button_args['link_id'] ) ? $button_args['link_id'] : '',
+						'class' => $button_args['link_class'] . ' button',
+						'rel'   => ! empty( $button_args['link_rel'] ) ? $button_args['link_rel'] : '',
+						'title' => '',
+					),
+				);
+
+			// If button element set add nonce 'href' link to data-attr attr.
+			if ( 'button' === $button_element ) {
+				$buttons['group_membership']['button_attr']['data-bp-nonce'] = $button_args['link_href'];
+			} else {
+			// Else this is an anchor so use an 'href' attr.
+				$buttons['group_membership']['button_attr']['href'] = $button_args['link_href'];
+			}
+
+				unset( bp_nouveau()->groups->button_args );
+			}
+		}
+
+		/**
+		 * Filter to add your buttons, use the position argument to choose where to insert it.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $buttons The list of buttons.
+		 * @param int    $group   The current group object.
+		 * @param string $type    Whether we're displaying a groups loop or a groups single item.
+		 */
+		$buttons_group = apply_filters( 'bp_nouveau_get_groups_buttons', $buttons, $group, $type );
+
+		if ( ! $buttons_group ) {
+			return $buttons;
+		}
+
+		// It's the first entry of the loop, so build the Group and sort it
+		if ( ! isset( bp_nouveau()->groups->group_buttons ) || ! is_a( bp_nouveau()->groups->group_buttons, 'BP_Buttons_Group' ) ) {
+			$sort = true;
+			bp_nouveau()->groups->group_buttons = new BP_Buttons_Group( $buttons_group );
+
+		// It's not the first entry, the order is set, we simply need to update the Buttons Group
+		} else {
+			$sort = false;
+			bp_nouveau()->groups->group_buttons->update( $buttons_group );
+		}
+
+		$return = bp_nouveau()->groups->group_buttons->get( $sort );
+
+		if ( ! $return ) {
+			return array();
+		}
+
+		// Remove buttons according to the user's membership type.
+		if ( 'manage_members' === $type && isset( $GLOBALS['members_template'] ) ) {
+			if ( bp_get_group_member_is_banned() ) {
+				unset( $return['ban_member'], $return['promote_mod'], $return['promote_admin'] );
+			} else {
+				unset( $return['unban_member'] );
+			}
+		}
+
+		/**
+		 * Leave a chance to adjust the $return
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $return  The list of buttons.
+		 * @param int    $group   The current group object.
+		 * @parem string $type    Whether we're displaying a groups loop or a groups single item.
+		 */
+		do_action_ref_array( 'bp_nouveau_return_groups_buttons', array( &$return, $group, $type ) );
+
+		return $return;
+	}
+
+/**
+ * Does the group has metas or a specific meta value.
+ *
+ * @since 3.0.0
+ * @since 3.2.0 Adds the $meta_key argument.
+ *
+ * @param  string $meta_key The key of the meta to check the value for.
+ * @return bool             True if the group has meta. False otherwise.
+ */
+function bp_nouveau_group_has_meta( $meta_key = '' ) {
+	$group_meta = bp_nouveau_get_group_meta();
+
+	if ( ! $meta_key ) {
+		return (bool) $group_meta;
+	}
+
+	return ! empty( $group_meta[ $meta_key ] );
+}
+
+/**
+ * Does the group have extra meta?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if the group has meta. False otherwise.
+ */
+function bp_nouveau_group_has_meta_extra() {
+	return (bool) bp_nouveau_get_hooked_group_meta();
+}
+
+/**
+ * Display the group meta.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output.
+ */
+function bp_nouveau_group_meta() {
+	$meta = bp_nouveau_get_group_meta();
+
+	if ( ! bp_is_group() ) {
+		echo join( ' / ', array_map( 'esc_html', (array) $meta ) );
+	} else {
+
+		/*
+		 * Lets return an object not echo an array here for the single groups,
+		 * more flexible for the template!!?? ~hnla
+		 *
+		 * @todo Paul says that a function that prints and/or returns a value,
+		 * depending on global state, is madness. This needs changing.
+		 */
+		return (object) bp_nouveau_get_group_meta();
+	}
+}
+
+	/**
+	 * Get the group meta.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array The group meta.
+	 */
+	function bp_nouveau_get_group_meta() {
+		/*
+		 * @todo For brevity required approapriate markup is added here as strings
+		 * this needs to be either filterable or the function needs to be able to accept
+		 * & parse args!
+		 */
+		$meta     = array();
+		$is_group = bp_is_group();
+
+		if ( ! empty( $GLOBALS['groups_template']->group ) ) {
+			$group = $GLOBALS['groups_template']->group;
+		}
+
+		if ( empty( $group->id ) ) {
+			return $meta;
+		}
+
+		if ( empty( $group->template_meta ) ) {
+			// It's a single group
+			if ( $is_group ) {
+					$meta = array(
+						'status'          =>  bp_get_group_type(),
+						'group_type_list' =>  bp_get_group_type_list(),
+						'description'     =>  bp_get_group_description(),
+					);
+
+				// Make sure to include hooked meta.
+				$extra_meta = bp_nouveau_get_hooked_group_meta();
+
+				if ( $extra_meta ) {
+					$meta['extra'] = $extra_meta;
+				}
+
+			// We're in the groups loop
+			} else {
+				$meta = array(
+					'status' => bp_get_group_type(),
+					'count'  => bp_get_group_member_count(),
+				);
+			}
+
+			/**
+			 * Filter to add/remove Group meta.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param array  $meta     The list of meta to output.
+			 * @param object $group    The current Group of the loop object.
+			 * @param bool   $is_group True if a single group is displayed. False otherwise.
+			 */
+			$group->template_meta = apply_filters( 'bp_nouveau_get_group_meta', $meta, $group, $is_group );
+		}
+
+		return $group->template_meta;
+	}
+
+/**
+ * Load the appropriate content for the single group pages
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_group_template_part() {
+	/**
+	 * Fires before the display of the group home body.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_group_body' );
+
+	$bp_is_group_home = bp_is_group_home();
+
+	if ( $bp_is_group_home && ! bp_current_user_can( 'groups_access_group' ) ) {
+		/**
+		 * Fires before the display of the group status message.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_group_status_message' );
+		?>
+
+		<div id="message" class="info">
+			<p><?php bp_group_status_message(); ?></p>
+		</div>
+
+		<?php
+
+		/**
+		 * Fires after the display of the group status message.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_group_status_message' );
+
+	// We have a front template, Use BuddyPress function to load it.
+	} elseif ( $bp_is_group_home && false !== bp_groups_get_front_template() ) {
+		bp_groups_front_template_part();
+
+	// Otherwise use BP_Nouveau template hierarchy
+	} else {
+		$template = 'plugins';
+
+		// the home page
+		if ( $bp_is_group_home ) {
+			if ( bp_is_active( 'activity' ) ) {
+				$template = 'activity';
+			} else {
+				$template = 'members';
+			}
+
+		// Not the home page
+		} elseif ( bp_is_group_admin_page() ) {
+			$template = 'admin';
+		} elseif ( bp_is_group_activity() ) {
+			$template = 'activity';
+		} elseif ( bp_is_group_members() ) {
+			$template = 'members';
+		} elseif ( bp_is_group_invites() ) {
+			$template = 'send-invites';
+		} elseif ( bp_is_group_membership_request() ) {
+			$template = 'request-membership';
+		}
+
+		bp_nouveau_group_get_template_part( $template );
+	}
+
+	/**
+	 * Fires after the display of the group home body.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_group_body' );
+}
+
+/**
+ * Use the appropriate Group header and enjoy a template hierarchy
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_group_header_template_part() {
+	$template = 'group-header';
+
+	if ( bp_group_use_cover_image_header() ) {
+		$template = 'cover-image-header';
+	}
+
+	/**
+	 * Fires before the display of a group's header.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_group_header' );
+
+	// Get the template part for the header
+	bp_nouveau_group_get_template_part( $template );
+
+	/**
+	 * Fires after the display of a group's header.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_group_header' );
+
+	bp_nouveau_template_notices();
+}
+
+/**
+ * Get a link to set the Group's default front page and directly
+ * reach the Customizer section where it's possible to do it.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output
+ */
+function bp_nouveau_groups_get_customizer_option_link() {
+	return bp_nouveau_get_customizer_link(
+		array(
+			'object'    => 'group',
+			'autofocus' => 'bp_nouveau_group_front_page',
+			'text'      => __( 'Groups default front page', 'buddypress' ),
+		)
+	);
+}
+
+/**
+ * Get a link to set the Group's front page widgets and directly
+ * reach the Customizer section where it's possible to do it.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output
+ */
+function bp_nouveau_groups_get_customizer_widgets_link() {
+	return bp_nouveau_get_customizer_link(
+		array(
+			'object'    => 'group',
+			'autofocus' => 'sidebar-widgets-sidebar-buddypress-groups',
+			'text'      => __( '(BuddyPress) Widgets', 'buddypress' ),
+		)
+	);
+}
+
+/**
+ * Output the group description excerpt
+ *
+ * @since 3.0.0
+ *
+ * @param object $group Optional. The group being referenced.
+ *                      Defaults to the group currently being iterated on in the groups loop.
+ * @param int $length   Optional. Length of returned string, including ellipsis. Default: 100.
+ *
+ * @return string Excerpt.
+ */
+function bp_nouveau_group_description_excerpt( $group = null, $length = null ) {
+	echo bp_nouveau_get_group_description_excerpt( $group, $length );
+}
+
+/**
+ * Filters the excerpt of a group description.
+ *
+ * Checks if the group loop is set as a 'Grid' layout and returns a reduced excerpt.
+ *
+ * @since 3.0.0
+ *
+ * @param object $group Optional. The group being referenced. Defaults to the group currently being
+ *                      iterated on in the groups loop.
+ * @param int $length   Optional. Length of returned string, including ellipsis. Default: 100.
+ *
+ * @return string Excerpt.
+ */
+function bp_nouveau_get_group_description_excerpt( $group = null, $length = null ) {
+	global $groups_template;
+
+	if ( ! $group ) {
+		$group =& $groups_template->group;
+	}
+
+	/**
+	 * If this is a grid layout but no length is passed in set a shorter
+	 * default value otherwise use the passed in value.
+	 * If not a grid then the BP core default is used or passed in value.
+	 */
+	if ( bp_nouveau_loop_is_grid() && 'groups' === bp_current_component() ) {
+		if ( ! $length ) {
+			$length = 100;
+		} else {
+			$length = $length;
+		}
+	}
+
+	/**
+	 * Filters the excerpt of a group description.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $value Excerpt of a group description.
+	 * @param object $group Object for group whose description is made into an excerpt.
+	 */
+	return apply_filters( 'bp_nouveau_get_group_description_excerpt', bp_create_excerpt( $group->description, $length ), $group );
+}
+
+/**
+ * Output "checked" attribute to determine if the group type should be checked.
+ *
+ * @since 3.2.0
+ *
+ * @param object $type Group type object. See bp_groups_get_group_type_object().
+ */
+function bp_nouveau_group_type_checked( $type = null ) {
+	if ( ! is_object( $type ) ) {
+		return;
+	}
+
+	// Group creation screen requires a different check.
+	if ( bp_is_group_create() ) {
+		checked( true, ! empty( $type->create_screen_checked ) );
+	} elseif ( bp_is_group() ) {
+		checked( bp_groups_has_group_type( bp_get_current_group_id(), $type->name ) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/functions.php	(revision 12265)
@@ -0,0 +1,506 @@
+<?php
+/**
+ * Members functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Enqueue the members scripts
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_members_enqueue_scripts() {
+	// Neutralize Ajax when using BuddyPress Groups & member widgets on default front page
+	if ( ! bp_is_user_front() || ! bp_nouveau_get_appearance_settings( 'user_front_page' ) ) {
+		return;
+	}
+
+	wp_add_inline_style(
+		'bp-nouveau', '
+		#member-front-widgets #groups-list-options,
+		#member-front-widgets #members-list-options,
+		#member-front-widgets #friends-list-options {
+			display: none;
+		}
+	'
+	);
+}
+
+/**
+ * Get the nav items for the Members directory
+ *
+ * @since 3.0.0
+ *
+ * @return array An associative array of nav items.
+ */
+function bp_nouveau_get_members_directory_nav_items() {
+	$nav_items = array();
+
+	$nav_items['all'] = array(
+		'component' => 'members',
+		'slug'      => 'all', // slug is used because BP_Core_Nav requires it, but it's the scope
+		'li_class'  => array(),
+		'link'      => bp_get_members_directory_permalink(),
+		'text'      => __( 'All Members', 'buddypress' ),
+		'count'     => bp_get_total_member_count(),
+		'position'  => 5,
+	);
+
+	if ( is_user_logged_in() ) {
+		// If friends component is active and the user has friends
+		if ( bp_is_active( 'friends' ) && bp_get_total_friend_count( bp_loggedin_user_id() ) ) {
+			$nav_items['personal'] = array(
+				'component' => 'members',
+				'slug'      => 'personal', // slug is used because BP_Core_Nav requires it, but it's the scope
+				'li_class'  => array(),
+				'link'      => bp_loggedin_user_domain() . bp_get_friends_slug() . '/my-friends/',
+				'text'      => __( 'My Friends', 'buddypress' ),
+				'count'     => bp_get_total_friend_count( bp_loggedin_user_id() ),
+				'position'  => 15,
+			);
+		}
+	}
+
+	// Check for the deprecated hook :
+	$extra_nav_items = bp_nouveau_parse_hooked_dir_nav( 'bp_members_directory_member_types', 'members', 20 );
+	if ( ! empty( $extra_nav_items ) ) {
+		$nav_items = array_merge( $nav_items, $extra_nav_items );
+	}
+
+	/**
+	 * Use this filter to introduce your custom nav items for the members directory.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $nav_items The list of the members directory nav items.
+	 */
+	return apply_filters( 'bp_nouveau_get_members_directory_nav_items', $nav_items );
+}
+
+/**
+ * Get Dropdown filters for the members component
+ *
+ * @since 3.0.0
+ *
+ * @param string $context Optional.
+ *
+ * @return array the filters
+ */
+function bp_nouveau_get_members_filters( $context = '' ) {
+	if ( 'group' !== $context ) {
+		$filters = array(
+			'active' => __( 'Last Active', 'buddypress' ),
+			'newest' => __( 'Newest Registered', 'buddypress' ),
+		);
+
+		if ( bp_is_active( 'xprofile' ) ) {
+			$filters['alphabetical'] = __( 'Alphabetical', 'buddypress' );
+		}
+
+		$action = 'bp_members_directory_order_options';
+
+		if ( 'friends' === $context ) {
+			$action = 'bp_member_friends_order_options';
+		}
+	} else {
+		$filters = array(
+			'last_joined'  => __( 'Newest', 'buddypress' ),
+			'first_joined' => __( 'Oldest', 'buddypress' ),
+		);
+
+		if ( bp_is_active( 'activity' ) ) {
+			$filters['group_activity'] = __( 'Group Activity', 'buddypress' );
+		}
+
+		$filters['alphabetical'] = __( 'Alphabetical', 'buddypress' );
+		$action                  = 'bp_groups_members_order_options';
+	}
+
+	/**
+	 * Recommended, filter here instead of adding an action to 'bp_members_directory_order_options'
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array  the members filters.
+	 * @param string the context.
+	 */
+	$filters = apply_filters( 'bp_nouveau_get_members_filters', $filters, $context );
+
+	return bp_nouveau_parse_hooked_options( $action, $filters );
+}
+
+/**
+ * Catch the arguments for buttons
+ *
+ * @since 3.0.0
+ *
+ * @param array $buttons The arguments of the button that BuddyPress is about to create.
+ *
+ * @return array An empty array to stop the button creation process.
+ */
+function bp_nouveau_members_catch_button_args( $button = array() ) {
+	/*
+	 * Globalize the arguments so that we can use it
+	 * in bp_nouveau_get_member_header_buttons().
+	 */
+	bp_nouveau()->members->button_args = $button;
+
+	// return an empty array to stop the button creation process
+	return array();
+}
+
+/**
+ * Catch the content hooked to the do_action hooks in single member header
+ * and in the members loop.
+ *
+ * @since 3.0.0
+ *
+ * @return string|false HTML Output if hooked. False otherwise.
+ */
+function bp_nouveau_get_hooked_member_meta() {
+	ob_start();
+
+	if ( ! empty( $GLOBALS['members_template'] ) ) {
+		/**
+		 * Fires inside the display of a directory member item.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_directory_members_item' );
+
+	// It's the user's header
+	} else {
+		/**
+		 * Fires after the group header actions section.
+		 *
+		 * If you'd like to show specific profile fields here use:
+		 * bp_member_profile_data( 'field=About Me' ); -- Pass the name of the field
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_profile_header_meta' );
+	}
+
+	$output = ob_get_clean();
+
+	if ( ! empty( $output ) ) {
+		return $output;
+	}
+
+	return false;
+}
+
+/**
+ * Add the default user front template to the front template hierarchy
+ *
+ * @since 3.0.0
+ *
+ * @param array $templates The list of templates for the front.php template part.
+ *
+ * @return array The same list with the default front template if needed.
+ */
+function bp_nouveau_member_reset_front_template( $templates = array() ) {
+	$use_default_front = bp_nouveau_get_appearance_settings( 'user_front_page' );
+
+	// Setting the front template happens too early, so we need this!
+	if ( is_customize_preview() ) {
+		$use_default_front = bp_nouveau_get_temporary_setting( 'user_front_page', $use_default_front );
+	}
+
+	if ( ! empty( $use_default_front ) ) {
+		array_push( $templates, 'members/single/default-front.php' );
+	}
+
+	/**
+	 * Filters the BuddyPress Nouveau template hierarchy after resetting front template for members.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $templates Array of templates.
+	 */
+	return apply_filters( '_bp_nouveau_member_reset_front_template', $templates );
+}
+
+/**
+ * Only locate global user's front templates
+ *
+ * @since 3.0.0
+ *
+ * @param array $templates The User's front template hierarchy.
+ *
+ * @return array Only the global front templates.
+ */
+function bp_nouveau_member_restrict_user_front_templates( $templates = array() ) {
+	return array_intersect( array(
+		'members/single/front.php',
+		'members/single/default-front.php',
+	), $templates );
+}
+
+/**
+ * Locate a single member template into a specific hierarchy.
+ *
+ * @since 3.0.0
+ *
+ * @param string $template The template part to get (eg: activity, groups...).
+ *
+ * @return string The located template.
+ */
+function bp_nouveau_member_locate_template_part( $template = '' ) {
+	$displayed_user = bp_get_displayed_user();
+	$bp_nouveau     = bp_nouveau();
+
+	if ( ! $template || empty( $displayed_user->id ) ) {
+		return '';
+	}
+
+	// Use a global to avoid requesting the hierarchy for each template
+	if ( ! isset( $bp_nouveau->members->displayed_user_hierarchy ) ) {
+		$bp_nouveau->members->displayed_user_hierarchy = array(
+			'members/single/%s-id-' . sanitize_file_name( $displayed_user->id ) . '.php',
+			'members/single/%s-nicename-' . sanitize_file_name( $displayed_user->userdata->user_nicename ) . '.php',
+		);
+
+		/*
+		 * Check for member types and add it to the hierarchy
+		 *
+		 * Make sure to register your member
+		 * type using the hook 'bp_register_member_types'
+		 */
+		if ( bp_get_member_types() ) {
+			$displayed_user_member_type = bp_get_member_type( $displayed_user->id );
+			if ( ! $displayed_user_member_type ) {
+				$displayed_user_member_type = 'none';
+			}
+
+			$bp_nouveau->members->displayed_user_hierarchy[] = 'members/single/%s-member-type-' . sanitize_file_name( $displayed_user_member_type ) . '.php';
+		}
+
+		// And the regular one
+		$bp_nouveau->members->displayed_user_hierarchy[] = 'members/single/%s.php';
+	}
+
+	$templates = array();
+
+	// Loop in the hierarchy to fill it for the requested template part
+	foreach ( $bp_nouveau->members->displayed_user_hierarchy as $part ) {
+		$templates[] = sprintf( $part, $template );
+	}
+
+	/**
+	 * Filters the found template parts for the member template part locating functionality.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $templates Array of found templates.
+	 */
+	return bp_locate_template( apply_filters( 'bp_nouveau_member_locate_template_part', $templates ), false, true );
+}
+
+/**
+ * Load a single member template part
+ *
+ * @since 3.0.0
+ *
+ * @param string $template The template part to get (eg: activity, groups...).
+ *
+ * @return string HTML output.
+ */
+function bp_nouveau_member_get_template_part( $template = '' ) {
+	$located = bp_nouveau_member_locate_template_part( $template );
+
+	if ( false !== $located ) {
+		$slug = str_replace( '.php', '', $located );
+		$name = null;
+
+		/**
+		 * Let plugins adding an action to bp_get_template_part get it from here.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $slug Template part slug requested.
+		 * @param string $name Template part name requested.
+		 */
+		do_action( 'get_template_part_' . $slug, $slug, $name );
+
+		load_template( $located, true );
+	}
+
+	return $located;
+}
+
+/**
+ * Display the User's WordPress bio info into the default front page?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True to display. False otherwise.
+ */
+function bp_nouveau_members_wp_bio_info() {
+	$user_settings = bp_nouveau_get_appearance_settings();
+
+	return ! empty( $user_settings['user_front_page'] ) && ! empty( $user_settings['user_front_bio'] );
+}
+
+/**
+ * Are we inside the Current user's default front page sidebar?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if in the group's home sidebar. False otherwise.
+ */
+function bp_nouveau_member_is_home_widgets() {
+	return ( true === bp_nouveau()->members->is_user_home_sidebar );
+}
+
+/**
+ * Filter the Latest activities Widget to only keep the one of displayed user
+ *
+ * @since 3.0.0
+ *
+ * @param array $args The Activities Template arguments.
+ *
+ * @return array The Activities Template arguments.
+ */
+function bp_nouveau_member_activity_widget_overrides( $args = array() ) {
+	return array_merge( $args, array(
+		'user_id' => bp_displayed_user_id(),
+	) );
+}
+
+/**
+ * Filter the Groups widget to only keep the groups the displayed user is a member of.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args The Groups Template arguments.
+ *
+ * @return array The Groups Template arguments.
+ */
+function bp_nouveau_member_groups_widget_overrides( $args = array() ) {
+	return array_merge( $args, array(
+		'user_id' => bp_displayed_user_id(),
+	) );
+}
+
+/**
+ * Filter the Members widgets to only keep members of the displayed group.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args The Members Template arguments.
+ *
+ * @return array The Members Template arguments.
+ */
+function bp_nouveau_member_members_widget_overrides( $args = array() ) {
+	// Do nothing for the friends widget
+	if ( ! empty( $args['user_id'] ) && (int) $args['user_id'] === (int) bp_displayed_user_id() ) {
+		return $args;
+	}
+
+	return array_merge( $args, array(
+		'include' => bp_displayed_user_id(),
+	) );
+}
+
+/**
+ * Init the Member's default front page filters as we're in the sidebar
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_members_add_home_widget_filters() {
+	add_filter( 'bp_nouveau_activity_widget_query', 'bp_nouveau_member_activity_widget_overrides', 10, 1 );
+	add_filter( 'bp_before_has_groups_parse_args', 'bp_nouveau_member_groups_widget_overrides', 10, 1 );
+	add_filter( 'bp_before_has_members_parse_args', 'bp_nouveau_member_members_widget_overrides', 10, 1 );
+
+	/**
+	 * Fires after Nouveau adds its members home widget filters.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_nouveau_members_add_home_widget_filters' );
+}
+
+/**
+ * Remove the Member's default front page filters as we're no more in the sidebar
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_members_remove_home_widget_filters() {
+	remove_filter( 'bp_nouveau_activity_widget_query', 'bp_nouveau_member_activity_widget_overrides', 10, 1 );
+	remove_filter( 'bp_before_has_groups_parse_args', 'bp_nouveau_member_groups_widget_overrides', 10, 1 );
+	remove_filter( 'bp_before_has_members_parse_args', 'bp_nouveau_member_members_widget_overrides', 10, 1 );
+
+	/**
+	 * Fires after Nouveau removes its members home widget filters.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_nouveau_members_remove_home_widget_filters' );
+}
+
+/**
+ * Get the WP Profile fields for all or a specific user
+ *
+ * @since 3.0.0
+ *
+ * @param WP_User $user The user object. Optional.
+ *
+ * @return array The list of WP Profile fields
+ */
+function bp_nouveau_get_wp_profile_fields( $user = null ) {
+	/**
+	 * Filters the contact methods to be included in the WP Profile fields for a specific user.
+	 *
+	 * Provide a chance for plugins to avoid showing the contact methods they're adding on front end.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array   $value Array of user contact methods.
+	 * @param WP_User $user  WordPress user to get contact methods for.
+	 */
+	$contact_methods = (array) apply_filters( 'bp_nouveau_get_wp_profile_field', wp_get_user_contact_methods( $user ), $user );
+
+	$wp_fields = array(
+		'display_name'     => __( 'Name', 'buddypress' ),
+		'user_description' => __( 'About Me', 'buddypress' ),
+		'user_url'         => __( 'Website', 'buddypress' ),
+	);
+
+	return array_merge( $wp_fields, $contact_methods );
+}
+
+/**
+ * Build the Member's nav for the our customizer control.
+ *
+ * @since 3.0.0
+ *
+ * @return array The Members single item primary nav ordered.
+ */
+function bp_nouveau_member_customizer_nav() {
+	add_filter( '_bp_nouveau_member_reset_front_template', 'bp_nouveau_member_restrict_user_front_templates', 10, 1 );
+
+	if ( bp_displayed_user_get_front_template( buddypress()->loggedin_user ) ) {
+		buddypress()->members->nav->add_nav(
+			array(
+				'name'     => _x( 'Home', 'Member Home page', 'buddypress' ),
+				'slug'     => 'front',
+				'position' => 5,
+			)
+		);
+	}
+
+	remove_filter( '_bp_nouveau_member_reset_front_template', 'bp_nouveau_member_restrict_user_front_templates', 10, 1 );
+
+	$nav = buddypress()->members->nav;
+
+	// Eventually reset the order.
+	bp_nouveau_set_nav_item_order( $nav, bp_nouveau_get_appearance_settings( 'user_nav_order' ) );
+
+	return $nav->get_primary();
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/loader.php	(revision 12265)
@@ -0,0 +1,143 @@
+<?php
+/**
+ * BP Nouveau Members
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Members Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Members {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir                  = dirname( __FILE__ );
+		$this->is_user_home_sidebar = false;
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require( trailingslashit( $this->dir ) . 'functions.php' );
+		require( trailingslashit( $this->dir ) . 'template-tags.php' );
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		$ajax_actions = array(
+			array(
+				'members_filter' => array(
+					'function' => 'bp_nouveau_ajax_object_template_loader',
+					'nopriv'   => true,
+				),
+			),
+		);
+
+		foreach ( $ajax_actions as $ajax_action ) {
+			$action = key( $ajax_action );
+
+			add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+			if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+				add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+			}
+		}
+
+		add_action( 'bp_nouveau_enqueue_scripts', 'bp_nouveau_members_enqueue_scripts' );
+
+		// Actions to check whether we are in the member's default front page sidebar
+		add_action( 'dynamic_sidebar_before', array( $this, 'user_home_sidebar_set' ), 10, 1 );
+		add_action( 'dynamic_sidebar_after', array( $this, 'user_home_sidebar_unset' ), 10, 1 );
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		// Add the default-front to User's front hierarchy if user enabled it (Enabled by default).
+		add_filter( 'bp_displayed_user_get_front_template', 'bp_nouveau_member_reset_front_template', 10, 1 );
+	}
+
+	/**
+	 * Add filters to be sure the (BuddyPress) widgets display will be consistent
+	 * with the displayed user's default front page.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $sidebar_index The Sidebar identifier.
+	 */
+	public function user_home_sidebar_set( $sidebar_index = '' ) {
+		if ( 'sidebar-buddypress-members' !== $sidebar_index ) {
+			return;
+		}
+
+		$this->is_user_home_sidebar = true;
+
+		// Add needed filters.
+		bp_nouveau_members_add_home_widget_filters();
+	}
+
+	/**
+	 * Remove filters to be sure the (BuddyPress) widgets display will no more take
+	 * the displayed user in account.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param  string $sidebar_index The Sidebar identifier.
+	 */
+	public function user_home_sidebar_unset( $sidebar_index = '' ) {
+		if ( 'sidebar-buddypress-members' !== $sidebar_index ) {
+			return;
+		}
+
+		$this->is_user_home_sidebar = false;
+
+		// Remove no more needed filters.
+		bp_nouveau_members_remove_home_widget_filters();
+	}
+}
+
+/**
+ * Launch the Members loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_members( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->members = new BP_Nouveau_Members();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_members', 5, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/members/template-tags.php	(revision 12265)
@@ -0,0 +1,1007 @@
+<?php
+/**
+ * Members template tags
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Template tag to wrap all Legacy actions that was used
+ * before the members directory content
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_before_members_directory_content() {
+	/**
+	 * Fires at the begining of the templates BP injected content.
+	 *
+	 * @since 2.3.0
+	 */
+	do_action( 'bp_before_directory_members_page' );
+
+	/**
+	 * Fires before the display of the members.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_members' );
+
+	/**
+	 * Fires before the display of the members content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_before_directory_members_content' );
+
+	/**
+	 * Fires before the display of the members list tabs.
+	 *
+	 * @since 1.8.0
+	 */
+	do_action( 'bp_before_directory_members_tabs' );
+}
+
+/**
+ * Template tag to wrap all Legacy actions that was used
+ * after the members directory content
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_after_members_directory_content() {
+	/**
+	 * Fires and displays the members content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_directory_members_content' );
+
+	/**
+	 * Fires after the display of the members content.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_members_content' );
+
+	/**
+	 * Fires after the display of the members.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'bp_after_directory_members' );
+
+	/**
+	 * Fires at the bottom of the members directory template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_after_directory_members_page' );
+}
+
+/**
+ * Fire specific hooks into the single members templates
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   'before' or 'after'
+ * @param string $suffix Use it to add terms at the end of the hook name
+ */
+function bp_nouveau_member_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a member hook
+	$hook[] = 'member';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Template tag to wrap the notification settings hook
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_member_email_notice_settings() {
+	/**
+	 * Fires at the top of the member template notification settings form.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'bp_notification_settings' );
+}
+
+/**
+ * Output the action buttons for the displayed user profile
+ *
+ * @since 3.0.0
+ *
+ * @param array $args See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_member_header_buttons( $args = array() ) {
+	$bp_nouveau = bp_nouveau();
+
+	if ( bp_is_user() ) {
+		$args['type'] = 'profile';
+	} else {
+		$args['type'] = 'header';// we have no real need for this 'type' on header actions
+	}
+
+	$output = join( ' ', bp_nouveau_get_members_buttons( $args ) );
+
+	/**
+	 * On the member's header we need to reset the group button's global
+	 * once displayed as the friends component will use the member's loop
+	 */
+	if ( ! empty( $bp_nouveau->members->member_buttons ) ) {
+		unset( $bp_nouveau->members->member_buttons );
+	}
+
+	ob_start();
+	/**
+	 * Fires in the member header actions section.
+	 *
+	 * @since 1.2.6
+	 */
+	do_action( 'bp_member_header_actions' );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	if ( ! $args ) {
+		$args = array(
+			'id'      => 'item-buttons',
+			'classes' => false,
+		);
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+/**
+ * Output the action buttons in member loops
+ *
+ * @since 3.0.0
+ *
+ * @param array $args See bp_nouveau_wrapper() for the description of parameters.
+ */
+function bp_nouveau_members_loop_buttons( $args = array() ) {
+	if ( empty( $GLOBALS['members_template'] ) ) {
+		return;
+	}
+
+	$args['type'] = 'loop';
+	$action       = 'bp_directory_members_actions';
+
+	// Specific case for group members.
+	if ( bp_is_active( 'groups' ) && bp_is_group_members() ) {
+		$args['type'] = 'group_member';
+		$action       = 'bp_group_members_list_item_action';
+
+	} elseif ( bp_is_active( 'friends' ) && bp_is_user_friend_requests() ) {
+		$args['type'] = 'friendship_request';
+		$action       = 'bp_friend_requests_item_action';
+	}
+
+	$output = join( ' ', bp_nouveau_get_members_buttons( $args ) );
+
+	ob_start();
+	/**
+	 * Fires inside the members action HTML markup to display actions.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( $action );
+	$output .= ob_get_clean();
+
+	if ( ! $output ) {
+		return;
+	}
+
+	bp_nouveau_wrapper( array_merge( $args, array( 'output' => $output ) ) );
+}
+
+	/**
+	 * Get the action buttons for the displayed user profile
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array
+	 */
+	function bp_nouveau_get_members_buttons( $args ) {
+		$buttons = array();
+		$type = ( ! empty( $args['type'] ) ) ? $args['type'] : '';
+
+		// @todo Not really sure why BP Legacy needed to do this...
+		if ( 'profile' === $type && is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+			return $buttons;
+		}
+
+		$user_id = bp_displayed_user_id();
+
+		if ( 'loop' === $type || 'friendship_request' === $type ) {
+			$user_id = bp_get_member_user_id();
+		} elseif ( 'group_member' === $type ) {
+			$user_id = bp_get_group_member_id();
+		}
+
+		if ( ! $user_id ) {
+			return $buttons;
+		}
+
+		/*
+		 * If the 'container' is set to 'ul'
+		 * set a var $parent_element to li
+		 * otherwise simply pass any value found in args
+		 * or set var false.
+		 */
+		$parent_element = false;
+
+		if ( ! empty( $args['container'] ) && 'ul' === $args['container']  ) {
+			$parent_element = 'li';
+		} elseif ( ! empty( $args['parent_element'] ) ) {
+			$parent_element = $args['parent_element'];
+		}
+
+		/*
+		 * If we have a arg value for $button_element passed through
+		 * use it to default all the $buttons['button_element'] values
+		 * otherwise default to 'a' (anchor)
+		 * Or override & hardcode the 'element' string on $buttons array.
+		 *
+		 * Icons sets a class for icon display if not using the button element
+		 */
+		$icons = '';
+		if ( ! empty( $args['button_element'] ) ) {
+			$button_element = $args['button_element'] ;
+		} else {
+			$button_element = 'button';
+			$icons = ' icons';
+		}
+
+		// If we pass through parent classes add them to $button array
+		$parent_class = '';
+		if ( ! empty( $args['parent_attr']['class'] ) ) {
+			$parent_class = $args['parent_attr']['class'];
+		}
+
+		if ( bp_is_active( 'friends' ) ) {
+			// It's the member's friendship requests screen
+			if ( 'friendship_request' === $type ) {
+				$buttons = array(
+					'accept_friendship' => array(
+						'id'                => 'accept_friendship',
+						'position'          => 5,
+						'component'         => 'friends',
+						'must_be_logged_in' => true,
+						'parent_element'    => $parent_element,
+						'link_text'         => _x( 'Accept', 'button', 'buddypress' ),
+						'parent_attr'       => array(
+							'id'    => '',
+							'class' => $parent_class ,
+						),
+						'button_element'    => $button_element,
+						'button_attr'       => array(
+							'class'           => 'button accept',
+							'rel'             => '',
+						),
+					), 'reject_friendship' => array(
+						'id'                => 'reject_friendship',
+						'position'          => 15,
+						'component'         => 'friends',
+						'must_be_logged_in' => true,
+						'parent_element'    => $parent_element,
+						'link_text'         => _x( 'Reject', 'button', 'buddypress' ),
+						'parent_attr'       => array(
+							'id'    => '',
+							'class' => $parent_class,
+						),
+						'button_element'    => $button_element,
+						'button_attr'       => array (
+							'class'           => 'button reject',
+							'rel'             => '',
+						),
+					),
+				);
+
+				// If button element set add nonce link to data attr
+				if ( 'button' === $button_element ) {
+					$buttons['accept_friendship']['button_attr']['data-bp-nonce'] = bp_get_friend_accept_request_link();
+					$buttons['reject_friendship']['button_attr']['data-bp-nonce'] = bp_get_friend_reject_request_link();
+				} else {
+					$buttons['accept_friendship']['button_attr']['href'] = bp_get_friend_accept_request_link();
+					$buttons['reject_friendship']['button_attr']['href'] = bp_get_friend_reject_request_link();
+				}
+
+			// It's any other members screen
+			} else {
+				/*
+				 * This filter workaround is waiting for a core adaptation
+				 * so that we can directly get the friends button arguments
+				 * instead of the button.
+				 *
+				 * See https://buddypress.trac.wordpress.org/ticket/7126
+				 */
+				add_filter( 'bp_get_add_friend_button', 'bp_nouveau_members_catch_button_args', 100, 1 );
+
+				bp_get_add_friend_button( $user_id );
+
+				remove_filter( 'bp_get_add_friend_button', 'bp_nouveau_members_catch_button_args', 100, 1 );
+
+				if ( ! empty( bp_nouveau()->members->button_args ) ) {
+					$button_args = bp_nouveau()->members->button_args;
+
+					$buttons['member_friendship'] = array(
+						'id'                => 'member_friendship',
+						'position'          => 5,
+						'component'         => $button_args['component'],
+						'must_be_logged_in' => $button_args['must_be_logged_in'],
+						'block_self'        => $button_args['block_self'],
+						'parent_element'    => $parent_element,
+						'link_text'         => $button_args['link_text'],
+						'parent_attr'       => array(
+							'id'    => $button_args['wrapper_id'],
+							'class' => $parent_class . ' ' . $button_args['wrapper_class'],
+						),
+						'button_element'    => $button_element,
+						'button_attr'       => array(
+							'id'    => $button_args['link_id'],
+							'class' => $button_args['link_class'],
+							'rel'   => $button_args['link_rel'],
+							'title' => '',
+						),
+					);
+
+					// If button element set add nonce link to data attr
+					if ( 'button' === $button_element && 'awaiting_response' !== $button_args['id'] ) {
+						$buttons['member_friendship']['button_attr']['data-bp-nonce'] = $button_args['link_href'];
+					} else {
+						$buttons['member_friendship']['button_element'] = 'a';
+						$buttons['member_friendship']['button_attr']['href'] = $button_args['link_href'];
+					}
+
+					unset( bp_nouveau()->members->button_args );
+				}
+			}
+		}
+
+		// Only add The public and private messages when not in a loop
+		if ( 'profile' === $type ) {
+			if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) {
+				/*
+				 * This filter workaround is waiting for a core adaptation
+				 * so that we can directly get the public message button arguments
+				 * instead of the button.
+				 *
+				 * See https://buddypress.trac.wordpress.org/ticket/7126
+				 */
+				add_filter( 'bp_get_send_public_message_button', 'bp_nouveau_members_catch_button_args', 100, 1 );
+
+				bp_get_send_public_message_button();
+
+				remove_filter( 'bp_get_send_public_message_button', 'bp_nouveau_members_catch_button_args', 100, 1 );
+
+				if ( ! empty( bp_nouveau()->members->button_args ) ) {
+					$button_args = bp_nouveau()->members->button_args;
+
+					/*
+					 * This button should remain as an anchor link.
+					 * Hardcode the use of anchor elements if button arg passed in for other elements.
+					 */
+					$buttons['public_message'] = array(
+						'id'                => $button_args['id'],
+						'position'          => 15,
+						'component'         => $button_args['component'],
+						'must_be_logged_in' => $button_args['must_be_logged_in'],
+						'block_self'        => $button_args['block_self'],
+						'parent_element'    => $parent_element,
+						'button_element'    => 'a',
+						'link_text'         => $button_args['link_text'],
+						'parent_attr'       => array(
+							'id'    => $button_args['wrapper_id'],
+							'class' => $parent_class,
+						),
+						'button_attr'       => array(
+							'href'             => $button_args['link_href'],
+							'id'               => '',
+							'class'            => $button_args['link_class'],
+						),
+					);
+					unset( bp_nouveau()->members->button_args );
+				}
+			}
+
+			if ( bp_is_active( 'messages' ) ) {
+				/**
+				 * This filter workaround is waiting for a core adaptation
+				 * so that we can directly get the private messages button arguments
+				 * instead of the button.
+				 * @see https://buddypress.trac.wordpress.org/ticket/7126
+				 */
+				add_filter( 'bp_get_send_message_button_args', 'bp_nouveau_members_catch_button_args', 100, 1 );
+
+				bp_get_send_message_button();
+
+				remove_filter( 'bp_get_send_message_button_args', 'bp_nouveau_members_catch_button_args', 100, 1 );
+
+				if ( ! empty( bp_nouveau()->members->button_args ) ) {
+					$button_args = bp_nouveau()->members->button_args;
+
+					/*
+					 * This button should remain as an anchor link.
+					 * Hardcode the use of anchor elements if button arg passed in for other elements.
+					 */
+					$buttons['private_message'] = array(
+						'id'                => $button_args['id'],
+						'position'          => 25,
+						'component'         => $button_args['component'],
+						'must_be_logged_in' => $button_args['must_be_logged_in'],
+						'block_self'        => $button_args['block_self'],
+						'parent_element'    => $parent_element,
+						'button_element'    => 'a',
+						'link_text'         => $button_args['link_text'],
+						'parent_attr'       => array(
+							'id'    => $button_args['wrapper_id'],
+							'class' => $parent_class,
+						),
+						'button_attr'       => array(
+							'href'  => trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() ) . 'compose?r=' . bp_core_get_username( $user_id ),
+							'id'    => false,
+							'class' => $button_args['link_class'],
+							'rel'   => '',
+							'title' => '',
+							),
+					);
+
+					unset( bp_nouveau()->members->button_args );
+				}
+			}
+		}
+
+		/**
+		 * Filter to add your buttons, use the position argument to choose where to insert it.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $buttons The list of buttons.
+		 * @param int    $user_id The displayed user ID.
+		 * @param string $type    Whether we're displaying a members loop or a user's page
+		 */
+		$buttons_group = apply_filters( 'bp_nouveau_get_members_buttons', $buttons, $user_id, $type );
+		if ( ! $buttons_group ) {
+			return $buttons;
+		}
+
+		// It's the first entry of the loop, so build the Group and sort it
+		if ( ! isset( bp_nouveau()->members->member_buttons ) || ! is_a( bp_nouveau()->members->member_buttons, 'BP_Buttons_Group' ) ) {
+			$sort = true;
+			bp_nouveau()->members->member_buttons = new BP_Buttons_Group( $buttons_group );
+
+		// It's not the first entry, the order is set, we simply need to update the Buttons Group
+		} else {
+			$sort = false;
+			bp_nouveau()->members->member_buttons->update( $buttons_group );
+		}
+
+		$return = bp_nouveau()->members->member_buttons->get( $sort );
+
+		if ( ! $return ) {
+			return array();
+		}
+
+		/**
+		 * Leave a chance to adjust the $return
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $return  The list of buttons ordered.
+		 * @param int    $user_id The displayed user ID.
+		 * @param string $type    Whether we're displaying a members loop or a user's page
+		 */
+		do_action_ref_array( 'bp_nouveau_return_members_buttons', array( &$return, $user_id, $type ) );
+
+		return $return;
+	}
+
+/**
+ * Does the member has meta.
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if the member has meta. False otherwise.
+ */
+function bp_nouveau_member_has_meta() {
+	return (bool) bp_nouveau_get_member_meta();
+}
+
+/**
+ * Display the member meta.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output.
+ */
+function bp_nouveau_member_meta() {
+	echo join( "\n", bp_nouveau_get_member_meta() );
+}
+
+	/**
+	 * Get the member meta.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array The member meta.
+	 */
+	function bp_nouveau_get_member_meta() {
+		$meta    = array();
+		$is_loop = false;
+
+		if ( ! empty( $GLOBALS['members_template']->member ) ) {
+			$member  = $GLOBALS['members_template']->member;
+			$is_loop = true;
+		} else {
+			$member = bp_get_displayed_user();
+		}
+
+		if ( empty( $member->id ) ) {
+			return $meta;
+		}
+
+		if ( empty( $member->template_meta ) ) {
+			// It's a single user's header
+			if ( ! $is_loop ) {
+				$meta['last_activity'] = sprintf(
+					'<span class="activity">%s</span>',
+					esc_html( bp_get_last_activity( bp_displayed_user_id() ) )
+				);
+
+			// We're in the members loop
+			} else {
+				$meta = array(
+					'last_activity' => sprintf( '%s', bp_get_member_last_active() ),
+				);
+			}
+
+			// Make sure to include hooked meta.
+			$extra_meta = bp_nouveau_get_hooked_member_meta();
+
+			if ( $extra_meta ) {
+				$meta['extra'] = $extra_meta;
+			}
+
+			/**
+			 * Filter to add/remove Member meta.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param array  $meta    The list of meta to output.
+			 * @param object $member  The member object
+			 * @param bool   $is_loop True if in the members loop. False otherwise.
+			 */
+			$member->template_meta = apply_filters( 'bp_nouveau_get_member_meta', $meta, $member, $is_loop );
+		}
+
+		return $member->template_meta;
+	}
+
+/**
+ * Load the appropriate content for the single member pages
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_member_template_part() {
+	/**
+	 * Fires before the display of member body content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_body' );
+
+	if ( bp_is_user_front() ) {
+		bp_displayed_user_front_template_part();
+
+	} else {
+		$template = 'plugins';
+
+		if ( bp_is_user_activity() ) {
+			$template = 'activity';
+		} elseif ( bp_is_user_blogs() ) {
+			$template = 'blogs';
+		} elseif ( bp_is_user_friends() ) {
+			$template = 'friends';
+		} elseif ( bp_is_user_groups() ) {
+			$template = 'groups';
+		} elseif ( bp_is_user_messages() ) {
+			$template = 'messages';
+		} elseif ( bp_is_user_profile() ) {
+			$template = 'profile';
+		} elseif ( bp_is_user_notifications() ) {
+			$template = 'notifications';
+		} elseif ( bp_is_user_settings() ) {
+			$template = 'settings';
+		}
+
+		bp_nouveau_member_get_template_part( $template );
+	}
+
+	/**
+	 * Fires after the display of member body content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_body' );
+}
+
+/**
+ * Use the appropriate Member header and enjoy a template hierarchy
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output
+ */
+function bp_nouveau_member_header_template_part() {
+	$template = 'member-header';
+
+	if ( bp_displayed_user_use_cover_image_header() ) {
+		$template = 'cover-image-header';
+	}
+
+	/**
+	 * Fires before the display of a member's header.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_header' );
+
+	// Get the template part for the header
+	bp_nouveau_member_get_template_part( $template );
+
+	/**
+	 * Fires after the display of a member's header.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_header' );
+
+	bp_nouveau_template_notices();
+}
+
+/**
+ * Get a link to set the Member's default front page and directly
+ * reach the Customizer section where it's possible to do it.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output
+ */
+function bp_nouveau_members_get_customizer_option_link() {
+	return bp_nouveau_get_customizer_link(
+		array(
+			'object'    => 'user',
+			'autofocus' => 'bp_nouveau_user_front_page',
+			'text'      => __( 'Members default front page', 'buddypress' ),
+		)
+	);
+}
+
+/**
+ * Get a link to set the Member's front page widgets and directly
+ * reach the Customizer section where it's possible to do it.
+ *
+ * @since 3.0.0
+ *
+ * @return string HTML Output
+ */
+function bp_nouveau_members_get_customizer_widgets_link() {
+	return bp_nouveau_get_customizer_link(
+		array(
+			'object'    => 'user',
+			'autofocus' => 'sidebar-widgets-sidebar-buddypress-members',
+			'text'      => __( '(BuddyPress) Widgets', 'buddypress' ),
+		)
+	);
+}
+
+/**
+ * Display the Member description making sure linefeeds are taking in account
+ *
+ * @since 3.0.0
+ *
+ * @param int $user_id Optional.
+ *
+ * @return string HTML output.
+ */
+function bp_nouveau_member_description( $user_id = 0 ) {
+	if ( ! $user_id ) {
+		$user_id = bp_loggedin_user_id();
+
+		if ( bp_displayed_user_id() ) {
+			$user_id = bp_displayed_user_id();
+		}
+	}
+
+	// @todo This hack is too brittle.
+	add_filter( 'the_author_description', 'make_clickable', 9 );
+	add_filter( 'the_author_description', 'wpautop' );
+	add_filter( 'the_author_description', 'wptexturize' );
+	add_filter( 'the_author_description', 'convert_smilies' );
+	add_filter( 'the_author_description', 'convert_chars' );
+	add_filter( 'the_author_description', 'stripslashes' );
+
+	the_author_meta( 'description', $user_id );
+
+	remove_filter( 'the_author_description', 'make_clickable', 9 );
+	remove_filter( 'the_author_description', 'wpautop' );
+	remove_filter( 'the_author_description', 'wptexturize' );
+	remove_filter( 'the_author_description', 'convert_smilies' );
+	remove_filter( 'the_author_description', 'convert_chars' );
+	remove_filter( 'the_author_description', 'stripslashes' );
+}
+
+/**
+ * Display the Edit profile link (temporary).
+ *
+ * @since 3.0.0
+ *
+ * @todo replace with Ajax feature
+ *
+ * @return string HTML Output
+ */
+function bp_nouveau_member_description_edit_link() {
+	echo bp_nouveau_member_get_description_edit_link();
+}
+
+	/**
+	 * Get the Edit profile link (temporary)
+	 * @todo  replace with Ajax featur
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string HTML Output
+	 */
+	function bp_nouveau_member_get_description_edit_link() {
+		remove_filter( 'edit_profile_url', 'bp_members_edit_profile_url', 10, 3 );
+
+		if ( is_multisite() && ! current_user_can( 'read' ) ) {
+			$link = get_dashboard_url( bp_displayed_user_id(), 'profile.php' );
+		} else {
+			$link = get_edit_profile_url( bp_displayed_user_id() );
+		}
+
+		add_filter( 'edit_profile_url', 'bp_members_edit_profile_url', 10, 3 );
+		$link .= '#description';
+
+		return sprintf( '<a href="%1$s">%2$s</a>', esc_url( $link ), esc_html__( 'Edit your bio', 'buddypress' ) );
+	}
+
+
+/** WP Profile tags **********************************************************/
+
+/**
+ * Template tag to wrap all Legacy actions that was used
+ * before and after the WP User's Profile.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_wp_profile_hooks( $type = 'before' ) {
+	if ( 'before' === $type ) {
+		/**
+		 * Fires before the display of member profile loop content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_before_profile_loop_content' );
+
+		/**
+		 * Fires before the display of member profile field content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_before_profile_field_content' );
+	} else {
+		/**
+		 * Fires after the display of member profile field content.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_after_profile_field_content' );
+
+		/**
+		 * Fires and displays the profile field buttons.
+		 *
+		 * @since 1.1.0
+		 */
+		do_action( 'bp_profile_field_buttons' );
+
+		/**
+		 * Fires after the display of member profile loop content.
+		 *
+		 * @since 1.2.0
+		 */
+		do_action( 'bp_after_profile_loop_content' );
+	}
+}
+
+/**
+ * Does the displayed user has WP profile fields?
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if user has profile fields. False otherwise.
+ */
+function bp_nouveau_has_wp_profile_fields() {
+	$user_id = bp_displayed_user_id();
+	if ( ! $user_id ) {
+		return false;
+	}
+
+	$user = get_userdata( $user_id );
+	if ( ! $user ) {
+		return false;
+	}
+
+	$fields              = bp_nouveau_get_wp_profile_fields( $user );
+	$user_profile_fields = array();
+
+	foreach ( $fields as $key => $field ) {
+		if ( empty( $user->{$key} ) ) {
+			continue;
+		}
+
+		$user_profile_fields[] = (object) array(
+			'id'    => 'wp_' . $key,
+			'label' => $field,
+			'data'  => $user->{$key},
+		);
+	}
+
+	if ( ! $user_profile_fields ) {
+		return false;
+	}
+
+	// Keep it for a later use.
+	$bp_nouveau                            = bp_nouveau();
+	$bp_nouveau->members->wp_profile       = $user_profile_fields;
+	$bp_nouveau->members->wp_profile_index = 0;
+
+	return true;
+}
+
+/**
+ * Check if there are still profile fields to output.
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if the profile field exists. False otherwise.
+ */
+function bp_nouveau_wp_profile_fields() {
+	$bp_nouveau = bp_nouveau();
+
+	if ( isset( $bp_nouveau->members->wp_profile[ $bp_nouveau->members->wp_profile_index ] ) ) {
+		return true;
+	}
+
+	$bp_nouveau->members->wp_profile_index = 0;
+	unset( $bp_nouveau->members->wp_profile_current );
+
+	return false;
+}
+
+/**
+ * Set the current profile field and iterate into the loop.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_wp_profile_field() {
+	$bp_nouveau = bp_nouveau();
+
+	$bp_nouveau->members->wp_profile_current = $bp_nouveau->members->wp_profile[ $bp_nouveau->members->wp_profile_index ];
+	$bp_nouveau->members->wp_profile_index  += 1;
+}
+
+/**
+ * Output the WP profile field ID.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_wp_profile_field_id() {
+	echo esc_attr( bp_nouveau_get_wp_profile_field_id() );
+}
+	/**
+	 * Get the WP profile field ID.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return int the profile field ID.
+	 */
+	function bp_nouveau_get_wp_profile_field_id() {
+		$field = bp_nouveau()->members->wp_profile_current;
+
+		/**
+		 * Filters the WP profile field ID used for BuddyPress Nouveau.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $id Field ID.
+		 */
+		return apply_filters( 'bp_nouveau_get_wp_profile_field_id', $field->id );
+	}
+
+/**
+ * Output the WP profile field label.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_wp_profile_field_label() {
+	echo esc_html( bp_nouveau_get_wp_profile_field_label() );
+}
+
+	/**
+	 * Get the WP profile label.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string the profile field label.
+	 */
+	function bp_nouveau_get_wp_profile_field_label() {
+		$field = bp_nouveau()->members->wp_profile_current;
+
+		/**
+		 * Filters the WP profile field label used for BuddyPress Nouveau.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $label Field label.
+		 */
+		return apply_filters( 'bp_nouveau_get_wp_profile_field_label', $field->label );
+	}
+
+/**
+ * Output the WP profile field data.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_wp_profile_field_data() {
+	$data = bp_nouveau_get_wp_profile_field_data();
+	$data = make_clickable( $data );
+
+	echo wp_kses(
+		/**
+		 * Filters a WP profile field value.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $data The profile field data value.
+		 */
+		apply_filters( 'bp_nouveau_get_wp_profile_field_data', $data ),
+		array(
+			'a' => array(
+				'href' => true,
+				'rel'  => true,
+			),
+		)
+	);
+}
+
+	/**
+	 * Get the WP profile field data.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string the profile field data.
+	 */
+	function bp_nouveau_get_wp_profile_field_data() {
+		$field = bp_nouveau()->members->wp_profile_current;
+		return $field->data;
+	}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/ajax.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/ajax.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/ajax.php	(revision 12265)
@@ -0,0 +1,750 @@
+<?php
+/**
+ * Messages Ajax functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_action( 'admin_init', function() {
+	$ajax_actions = array(
+		array( 'messages_send_message'             => array( 'function' => 'bp_nouveau_ajax_messages_send_message', 'nopriv' => false ) ),
+		array( 'messages_send_reply'               => array( 'function' => 'bp_nouveau_ajax_messages_send_reply', 'nopriv' => false ) ),
+		array( 'messages_get_user_message_threads' => array( 'function' => 'bp_nouveau_ajax_get_user_message_threads', 'nopriv' => false ) ),
+		array( 'messages_thread_read'              => array( 'function' => 'bp_nouveau_ajax_messages_thread_read', 'nopriv' => false ) ),
+		array( 'messages_get_thread_messages'      => array( 'function' => 'bp_nouveau_ajax_get_thread_messages', 'nopriv' => false ) ),
+		array( 'messages_delete'                   => array( 'function' => 'bp_nouveau_ajax_delete_thread_messages', 'nopriv' => false ) ),
+		array( 'messages_unstar'                   => array( 'function' => 'bp_nouveau_ajax_star_thread_messages', 'nopriv' => false ) ),
+		array( 'messages_star'                     => array( 'function' => 'bp_nouveau_ajax_star_thread_messages', 'nopriv' => false ) ),
+		array( 'messages_unread'                   => array( 'function' => 'bp_nouveau_ajax_readunread_thread_messages', 'nopriv' => false ) ),
+		array( 'messages_read'                     => array( 'function' => 'bp_nouveau_ajax_readunread_thread_messages', 'nopriv' => false ) ),
+		array( 'messages_dismiss_sitewide_notice'  => array( 'function' => 'bp_nouveau_ajax_dismiss_sitewide_notice', 'nopriv' => false ) ),
+	);
+
+	foreach ( $ajax_actions as $ajax_action ) {
+		$action = key( $ajax_action );
+
+		add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+		if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+			add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+		}
+	}
+}, 12 );
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_messages_send_message() {
+	$response = array(
+		'feedback' => __( 'Your message could not be sent. Please try again.', 'buddypress' ),
+		'type'     => 'error',
+	);
+
+	// Verify nonce
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'messages_send_message' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Validate subject and message content
+	if ( empty( $_POST['subject'] ) || empty( $_POST['message_content'] ) ) {
+		if ( empty( $_POST['subject'] ) ) {
+			$response['feedback'] = __( 'Your message was not sent. Please enter a subject line.', 'buddypress' );
+		} else {
+			$response['feedback'] = __( 'Your message was not sent. Please enter some content.', 'buddypress' );
+		}
+
+		wp_send_json_error( $response );
+	}
+
+	// Validate recipients
+	if ( empty( $_POST['send_to'] ) || ! is_array( $_POST['send_to'] ) ) {
+		$response['feedback'] = __( 'Your message was not sent. Please enter at least one username.', 'buddypress' );
+
+		wp_send_json_error( $response );
+	}
+
+	// Trim @ from usernames
+	/**
+	 * Filters the results of trimming of `@` characters from usernames for who is set to receive a message.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $value Array of trimmed usernames.
+	 * @param array $value Array of un-trimmed usernames submitted.
+	 */
+	$recipients = apply_filters( 'bp_messages_recipients', array_map( function( $username ) {
+		return trim( $username, '@' );
+	}, $_POST['send_to'] ) );
+
+	// Attempt to send the message.
+	$send = messages_new_message( array(
+		'recipients' => $recipients,
+		'subject'    => $_POST['subject'],
+		'content'    => $_POST['message_content'],
+		'error_type' => 'wp_error',
+	) );
+
+	// Send the message.
+	if ( true === is_int( $send ) ) {
+		wp_send_json_success( array(
+			'feedback' => __( 'Message successfully sent.', 'buddypress' ),
+			'type'     => 'success',
+		) );
+
+	// Message could not be sent.
+	} else {
+		$response['feedback'] = $send->get_error_message();
+
+		wp_send_json_error( $response );
+	}
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_messages_send_reply() {
+	$response = array(
+		'feedback' => __( 'There was a problem sending your reply. Please try again.', 'buddypress' ),
+		'type'     => 'error',
+	);
+
+	// Verify nonce
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'messages_send_message' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['content'] ) || empty( $_POST['thread_id'] ) ) {
+		$response['feedback'] = __( 'Your reply was not sent. Please enter some content.', 'buddypress' );
+
+		wp_send_json_error( $response );
+	}
+
+	$new_reply = messages_new_message( array(
+		'thread_id' => (int) $_POST['thread_id'],
+		'subject'   => ! empty( $_POST['subject'] ) ? $_POST['subject'] : false,
+		'content'   => $_POST['content']
+	) );
+
+	// Send the reply.
+	if ( empty( $new_reply ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Get the message by pretending we're in the message loop.
+	global $thread_template;
+
+	$bp           = buddypress();
+	$reset_action = $bp->current_action;
+
+	// Override bp_current_action().
+	$bp->current_action = 'view';
+
+	bp_thread_has_messages( array( 'thread_id' => (int) $_POST['thread_id'] ) );
+
+	// Set the current message to the 2nd last.
+	$thread_template->message = end( $thread_template->thread->messages );
+	$thread_template->message = prev( $thread_template->thread->messages );
+
+	// Set current message to current key.
+	$thread_template->current_message = key( $thread_template->thread->messages );
+
+	// Now manually iterate message like we're in the loop.
+	bp_thread_the_message();
+
+	// Manually call oEmbed
+	// this is needed because we're not at the beginning of the loop.
+	bp_messages_embed();
+
+	// Output single message template part.
+	$reply = array(
+		'id'            => bp_get_the_thread_message_id(),
+		'content'       => html_entity_decode( do_shortcode( bp_get_the_thread_message_content() ) ),
+		'sender_id'     => bp_get_the_thread_message_sender_id(),
+		'sender_name'   => esc_html( bp_get_the_thread_message_sender_name() ),
+		'sender_link'   => bp_get_the_thread_message_sender_link(),
+		'sender_avatar' => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+			'item_id' => bp_get_the_thread_message_sender_id(),
+			'object'  => 'user',
+			'type'    => 'thumb',
+			'width'   => 32,
+			'height'  => 32,
+			'html'    => false,
+		) ) ),
+		'date'          => bp_get_the_thread_message_date_sent() * 1000,
+		'display_date'  => bp_get_the_thread_message_time_since(),
+	);
+
+	if ( bp_is_active( 'messages', 'star' ) ) {
+		$star_link = bp_get_the_message_star_action_link( array(
+			'message_id' => bp_get_the_thread_message_id(),
+			'url_only'  => true,
+		) );
+
+		$reply['star_link']  = $star_link;
+		$reply['is_starred'] = array_search( 'unstar', explode( '/', $star_link ) );
+	}
+
+	$extra_content = bp_nouveau_messages_catch_hook_content( array(
+		'beforeMeta'    => 'bp_before_message_meta',
+		'afterMeta'     => 'bp_after_message_meta',
+		'beforeContent' => 'bp_before_message_content',
+		'afterContent'  => 'bp_after_message_content',
+	) );
+
+	if ( array_filter( $extra_content ) ) {
+		$reply = array_merge( $reply, $extra_content );
+	}
+
+	// Clean up the loop.
+	bp_thread_messages();
+
+	// Remove the bp_current_action() override.
+	$bp->current_action = $reset_action;
+
+	wp_send_json_success( array(
+		'messages' => array( $reply ),
+		'feedback' => __( 'Your reply was sent successfully', 'buddypress' ),
+		'type'     => 'success',
+	) );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_get_user_message_threads() {
+	global $messages_template;
+
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+		wp_send_json_error( array(
+			'feedback' => __( 'Unauthorized request.', 'buddypress' ),
+			'type'     => 'error'
+		) );
+	}
+
+	$bp           = buddypress();
+	$reset_action = $bp->current_action;
+
+	// Override bp_current_action().
+	if ( isset( $_POST['box'] ) ) {
+		$bp->current_action = $_POST['box'];
+	}
+
+	// Add the message thread filter.
+	if ( 'starred' === $bp->current_action ) {
+		add_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+	}
+
+	// Simulate the loop.
+	if ( ! bp_has_message_threads( bp_ajax_querystring( 'messages' ) ) ) {
+		// Remove the bp_current_action() override.
+		$bp->current_action = $reset_action;
+
+		wp_send_json_error( array(
+			'feedback' => __( 'Sorry, no messages were found.', 'buddypress' ),
+			'type'     => 'info'
+		) );
+	}
+
+	// remove the message thread filter.
+	if ( 'starred' === $bp->current_action ) {
+		remove_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+	}
+
+	$threads       = new stdClass;
+	$threads->meta = array(
+		'total_page' => ceil( (int) $messages_template->total_thread_count / (int) $messages_template->pag_num ),
+		'page'       => $messages_template->pag_page,
+	);
+
+	$threads->threads = array();
+	$i                = 0;
+
+	while ( bp_message_threads() ) : bp_message_thread();
+		$last_message_id = (int) $messages_template->thread->last_message_id;
+
+		$threads->threads[ $i ] = array(
+			'id'            => bp_get_message_thread_id(),
+			'message_id'    => (int) $last_message_id,
+			'subject'       => html_entity_decode( bp_get_message_thread_subject() ),
+			'excerpt'       => html_entity_decode( bp_get_message_thread_excerpt() ),
+			'content'       => html_entity_decode( do_shortcode( bp_get_message_thread_content() ) ),
+			'unread'        => bp_message_thread_has_unread(),
+			'sender_name'   => bp_core_get_user_displayname( $messages_template->thread->last_sender_id ),
+			'sender_link'   => bp_core_get_userlink( $messages_template->thread->last_sender_id, false, true ),
+			'sender_avatar' => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+				'item_id' => $messages_template->thread->last_sender_id,
+				'object'  => 'user',
+				'type'    => 'thumb',
+				'width'   => 32,
+				'height'  => 32,
+				'html'    => false,
+			) ) ),
+			'count'         => bp_get_message_thread_total_count(),
+			'date'          => strtotime( bp_get_message_thread_last_post_date_raw() ) * 1000,
+			'display_date'  => bp_nouveau_get_message_date( bp_get_message_thread_last_post_date_raw() ),
+		);
+
+		if ( is_array( $messages_template->thread->recipients ) ) {
+			foreach ( $messages_template->thread->recipients as $recipient ) {
+				$threads->threads[ $i ]['recipients'][] = array(
+					'avatar' => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+						'item_id' => $recipient->user_id,
+						'object'  => 'user',
+						'type'    => 'thumb',
+						'width'   => 28,
+						'height'  => 28,
+						'html'    => false,
+					) ) ),
+					'user_link' => bp_core_get_userlink( $recipient->user_id, false, true ),
+					'user_name' => bp_core_get_username( $recipient->user_id ),
+				);
+			}
+		}
+
+		if ( bp_is_active( 'messages', 'star' ) ) {
+			$star_link = bp_get_the_message_star_action_link( array(
+				'thread_id' => bp_get_message_thread_id(),
+				'url_only'  => true,
+			) );
+
+			$threads->threads[ $i ]['star_link']  = $star_link;
+
+			$star_link_data = explode( '/', $star_link );
+			$threads->threads[ $i ]['is_starred'] = array_search( 'unstar', $star_link_data );
+
+			// Defaults to last
+			$sm_id = $last_message_id;
+
+			if ( $threads->threads[ $i ]['is_starred'] ) {
+				$sm_id = (int) $star_link_data[ $threads->threads[ $i ]['is_starred'] + 1 ];
+			}
+
+			$threads->threads[ $i ]['star_nonce'] = wp_create_nonce( 'bp-messages-star-' . $sm_id );
+			$threads->threads[ $i ]['starred_id'] = $sm_id;
+		}
+
+		$thread_extra_content = bp_nouveau_messages_catch_hook_content( array(
+			'inboxListItem' => 'bp_messages_inbox_list_item',
+			'threadOptions' => 'bp_messages_thread_options',
+		) );
+
+		if ( array_filter( $thread_extra_content ) ) {
+			$threads->threads[ $i ] = array_merge( $threads->threads[ $i ], $thread_extra_content );
+		}
+
+		$i += 1;
+	endwhile;
+
+	$threads->threads = array_filter( $threads->threads );
+
+	$extra_content = bp_nouveau_messages_catch_hook_content( array(
+		'beforeLoop' => 'bp_before_member_messages_loop',
+		'afterLoop'  => 'bp_after_member_messages_loop',
+	) );
+
+	if ( array_filter( $extra_content ) ) {
+		$threads->extraContent = $extra_content;
+	}
+
+	// Remove the bp_current_action() override.
+	$bp->current_action = $reset_action;
+
+	// Return the successfull reply.
+	wp_send_json_success( $threads );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_messages_thread_read() {
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+		wp_send_json_error();
+	}
+
+	if ( empty( $_POST['id'] ) || empty( $_POST['message_id'] ) ) {
+		wp_send_json_error();
+	}
+
+	$thread_id  = (int) $_POST['id'];
+	$message_id = (int) $_POST['message_id'];
+
+	if ( ! messages_is_valid_thread( $thread_id ) || ( ! messages_check_thread_access( $thread_id ) && ! bp_current_user_can( 'bp_moderate' ) ) ) {
+		wp_send_json_error();
+	}
+
+	// Mark thread as read
+	messages_mark_thread_read( $thread_id );
+
+	// Mark latest message as read
+	if ( bp_is_active( 'notifications' ) ) {
+		bp_notifications_mark_notifications_by_item_id( bp_loggedin_user_id(), (int) $message_id, buddypress()->messages->id, 'new_message' );
+	}
+
+	wp_send_json_success();
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_get_thread_messages() {
+	global $thread_template;
+
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+		wp_send_json_error( array(
+			'feedback' => __( 'Unauthorized request.', 'buddypress' ),
+			'type'     => 'error'
+		) );
+	}
+
+	$response = array(
+		'feedback' => __( 'Sorry, no messages were found.', 'buddypress' ),
+		'type'     => 'info'
+	);
+
+	if ( empty( $_POST['id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$thread_id    = (int) $_POST['id'];
+	$bp           = buddypress();
+	$reset_action = $bp->current_action;
+
+	// Override bp_current_action().
+	$bp->current_action = 'view';
+
+	// Simulate the loop.
+	if ( ! bp_thread_has_messages( array( 'thread_id' => $thread_id ) ) ) {
+		// Remove the bp_current_action() override.
+		$bp->current_action = $reset_action;
+
+		wp_send_json_error( $response );
+	}
+
+	$thread = new stdClass;
+
+	if ( empty( $_POST['js_thread'] ) ) {
+		$thread->thread = array(
+			'id'      => bp_get_the_thread_id(),
+			'subject' => html_entity_decode( bp_get_the_thread_subject() ),
+		);
+
+		if ( is_array( $thread_template->thread->recipients ) ) {
+			foreach ( $thread_template->thread->recipients as $recipient ) {
+				$thread->thread['recipients'][] = array(
+					'avatar' => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+						'item_id' => $recipient->user_id,
+						'object'  => 'user',
+						'type'    => 'thumb',
+						'width'   => 28,
+						'height'  => 28,
+						'html'    => false,
+					) ) ),
+					'user_link' => bp_core_get_userlink( $recipient->user_id, false, true ),
+					'user_name' => bp_core_get_username( $recipient->user_id ),
+				);
+			}
+		}
+	}
+
+	$thread->messages = array();
+	$i = 0;
+
+	while ( bp_thread_messages() ) : bp_thread_the_message();
+		$thread->messages[ $i ] = array(
+			'id'            => bp_get_the_thread_message_id(),
+			'content'       => html_entity_decode( do_shortcode( bp_get_the_thread_message_content() ) ),
+			'sender_id'     => bp_get_the_thread_message_sender_id(),
+			'sender_name'   => esc_html( bp_get_the_thread_message_sender_name() ),
+			'sender_link'   => bp_get_the_thread_message_sender_link(),
+			'sender_avatar' => htmlspecialchars_decode( bp_core_fetch_avatar( array(
+				'item_id' => bp_get_the_thread_message_sender_id(),
+				'object'  => 'user',
+				'type'    => 'thumb',
+				'width'   => 32,
+				'height'  => 32,
+				'html'    => false,
+			) ) ),
+			'date'          => bp_get_the_thread_message_date_sent() * 1000,
+			'display_date'  => bp_get_the_thread_message_time_since(),
+		);
+
+		if ( bp_is_active( 'messages', 'star' ) ) {
+			$star_link = bp_get_the_message_star_action_link( array(
+				'message_id' => bp_get_the_thread_message_id(),
+				'url_only'  => true,
+			) );
+
+			$thread->messages[ $i ]['star_link']  = $star_link;
+			$thread->messages[ $i ]['is_starred'] = array_search( 'unstar', explode( '/', $star_link ) );
+			$thread->messages[ $i ]['star_nonce'] = wp_create_nonce( 'bp-messages-star-' . bp_get_the_thread_message_id() );
+		}
+
+		$extra_content = bp_nouveau_messages_catch_hook_content( array(
+			'beforeMeta'    => 'bp_before_message_meta',
+			'afterMeta'     => 'bp_after_message_meta',
+			'beforeContent' => 'bp_before_message_content',
+			'afterContent'  => 'bp_after_message_content',
+		) );
+
+		if ( array_filter( $extra_content ) ) {
+			$thread->messages[ $i ] = array_merge( $thread->messages[ $i ], $extra_content );
+		}
+
+		$i += 1;
+	endwhile;
+
+	$thread->messages = array_filter( $thread->messages );
+
+	// Remove the bp_current_action() override.
+	$bp->current_action = $reset_action;
+
+	wp_send_json_success( $thread );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_delete_thread_messages() {
+	$response = array(
+		'feedback' => __( 'There was a problem deleting your messages. Please try again.', 'buddypress' ),
+		'type'     => 'error',
+	);
+
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$thread_ids = wp_parse_id_list( $_POST['id'] );
+
+	foreach ( $thread_ids as $thread_id ) {
+		if ( ! messages_check_thread_access( $thread_id ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+			wp_send_json_error( $response );
+		}
+
+		messages_delete_thread( $thread_id );
+	}
+
+	wp_send_json_success( array(
+		'feedback' => __( 'Messages deleted', 'buddypress' ),
+		'type'     => 'success',
+	) );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_star_thread_messages() {
+	if ( empty( $_POST['action'] ) ) {
+		wp_send_json_error();
+	}
+
+	$action = str_replace( 'messages_', '', $_POST['action'] );
+
+	if ( 'star' === $action ) {
+		$error_message = __( 'There was a problem starring your messages. Please try again.', 'buddypress' );
+	} else {
+		$error_message = __( 'There was a problem unstarring your messages. Please try again.', 'buddypress' );
+	}
+
+	$response = array(
+		'feedback' => esc_html( $error_message ),
+		'type'     => 'error',
+	);
+
+	if ( false === bp_is_active( 'messages', 'star' ) || empty( $_POST['id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Check capability.
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		wp_send_json_error( $response );
+	}
+
+	$ids      = wp_parse_id_list( $_POST['id'] );
+	$messages = array();
+
+	// Use global nonce for bulk actions involving more than one id
+	if ( 1 !== count( $ids ) ) {
+		if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+			wp_send_json_error( $response );
+		}
+
+		foreach ( $ids as $mid ) {
+			if ( 'star' === $action ) {
+				bp_messages_star_set_action( array(
+					'action'     => 'star',
+					'message_id' => $mid,
+				) );
+			} else {
+				$thread_id = messages_get_message_thread_id( $mid );
+
+				bp_messages_star_set_action( array(
+					'action'    => 'unstar',
+					'thread_id' => $thread_id,
+					'bulk'      => true
+				) );
+			}
+
+			$messages[ $mid ] = array(
+				'star_link' => bp_get_the_message_star_action_link( array(
+					'message_id' => $mid,
+					'url_only'  => true,
+				) ),
+				'is_starred' => 'star' === $action,
+			);
+		}
+
+	// Use global star nonce for bulk actions involving one id or regular action
+	} else {
+		$id = reset( $ids );
+
+		if ( empty( $_POST['star_nonce'] ) || ! wp_verify_nonce( $_POST['star_nonce'], 'bp-messages-star-' . $id ) ) {
+			wp_send_json_error( $response );
+		}
+
+		bp_messages_star_set_action( array(
+			'action'     => $action,
+			'message_id' => $id,
+		) );
+
+		$messages[ $id ] = array(
+			'star_link' => bp_get_the_message_star_action_link( array(
+				'message_id' => $id,
+				'url_only'  => true,
+			) ),
+			'is_starred' => 'star' === $action,
+		);
+	}
+
+	if ( 'star' === $action ) {
+		$success_message = __( 'Messages successfully starred.', 'buddypress' );
+	} else {
+		$success_message = __( 'Messages successfully unstarred.', 'buddypress' );
+	}
+
+	wp_send_json_success( array(
+		'feedback' => esc_html( $success_message ),
+		'type'     => 'success',
+		'messages' => $messages,
+	) );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_readunread_thread_messages() {
+	if ( empty( $_POST['action'] ) ) {
+		wp_send_json_error();
+	}
+
+	$action = str_replace( 'messages_', '', $_POST['action'] );
+
+	$response = array(
+		'feedback' => __( 'There was a problem marking your messages as read. Please try again.', 'buddypress' ),
+		'type'     => 'error',
+	);
+
+	if ( 'unread' === $action ) {
+		$response = array(
+			'feedback' => __( 'There was a problem marking your messages as unread. Please try again.', 'buddypress' ),
+			'type'     => 'error',
+		);
+	}
+
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['id'] ) ) {
+		wp_send_json_error( $response );
+	}
+
+	$thread_ids = wp_parse_id_list( $_POST['id'] );
+
+	$response['messages'] = array();
+
+	if ( 'unread' === $action ) {
+		$response['feedback'] = __( 'Messages marked as unread.', 'buddypress' );
+	} else {
+		$response['feedback'] = __( 'Messages marked as read.', 'buddypress' );
+	}
+
+	foreach ( $thread_ids as $thread_id ) {
+		if ( ! messages_check_thread_access( $thread_id ) && ! bp_current_user_can( 'bp_moderate' ) ) {
+			wp_send_json_error( $response );
+		}
+
+		if ( 'unread' === $action ) {
+			// Mark unread
+			messages_mark_thread_unread( $thread_id );
+		} else {
+			// Mark read
+			messages_mark_thread_read( $thread_id );
+		}
+
+		$response['messages'][ $thread_id ] = array(
+			'unread' => 'unread' === $action,
+		);
+	}
+
+	$response['type'] = 'success';
+
+	wp_send_json_success( $response );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_ajax_dismiss_sitewide_notice() {
+	if ( empty( $_POST['action'] ) ) {
+		wp_send_json_error();
+	}
+
+	$response = array(
+		'feedback' => '<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>' . __( 'There was a problem dismissing the notice. Please try again.', 'buddypress' ) . '</p></div>',
+		'type'     => 'error',
+	);
+
+	if ( false === bp_is_active( 'messages' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bp_nouveau_messages' ) ) {
+		wp_send_json_error( $response );
+	}
+
+	// Check capability.
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		wp_send_json_error( $response );
+	}
+
+	// Mark the active notice as closed.
+	$notice = BP_Messages_Notice::get_active();
+
+	if ( ! empty( $notice->id ) ) {
+		$user_id = bp_loggedin_user_id();
+
+		$closed_notices = bp_get_user_meta( $user_id, 'closed_notices', true );
+
+		if ( empty( $closed_notices ) ) {
+			$closed_notices = array();
+		}
+
+		// Add the notice to the array of the user's closed notices.
+		$closed_notices[] = (int) $notice->id;
+		bp_update_user_meta( $user_id, 'closed_notices', array_map( 'absint', array_unique( $closed_notices ) ) );
+
+		wp_send_json_success( array(
+			'feedback' => '<div class="bp-feedback info"><span class="bp-icon" aria-hidden="true"></span><p>' . __( 'Sitewide notice dismissed', 'buddypress' ) . '</p></div>',
+			'type'     => 'success',
+		) );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/functions.php	(revision 12265)
@@ -0,0 +1,449 @@
+<?php
+/**
+ * Messages functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Enqueue styles for the Messages UI (mentions).
+ *
+ * @since 3.0.0
+ *
+ * @param array $styles Optional. The array of styles to enqueue.
+ *
+ * @return array The same array with the specific messages styles.
+ */
+function bp_nouveau_messages_enqueue_styles( $styles = array() ) {
+	if ( ! bp_is_user_messages() ) {
+		return $styles;
+	}
+
+	return array_merge( $styles, array(
+		'bp-nouveau-messages-at' => array(
+			'file'         => buddypress()->plugin_url . 'bp-activity/css/mentions%1$s%2$s.css',
+			'dependencies' => array( 'bp-nouveau' ),
+			'version'      => bp_get_version(),
+		),
+	) );
+}
+
+/**
+ * Register Scripts for the Messages component
+ *
+ * @since 3.0.0
+ *
+ * @param array $scripts The array of scripts to register
+ *
+ * @return array The same array with the specific messages scripts.
+ */
+function bp_nouveau_messages_register_scripts( $scripts = array() ) {
+	if ( ! isset( $scripts['bp-nouveau'] ) ) {
+		return $scripts;
+	}
+
+	return array_merge( $scripts, array(
+		'bp-nouveau-messages-at' => array(
+			'file'         => buddypress()->plugin_url . 'bp-activity/js/mentions%s.js',
+			'dependencies' => array( 'bp-nouveau', 'jquery', 'jquery-atwho' ),
+			'version'      => bp_get_version(),
+			'footer'       => true,
+		),
+		'bp-nouveau-messages' => array(
+			'file'         => 'js/buddypress-messages%s.js',
+			'dependencies' => array( 'bp-nouveau', 'json2', 'wp-backbone', 'bp-nouveau-messages-at' ),
+			'footer'       => true,
+		),
+	) );
+}
+
+/**
+ * Enqueue the messages scripts
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_enqueue_scripts() {
+	if ( ! bp_is_user_messages() ) {
+		return;
+	}
+
+	wp_enqueue_script( 'bp-nouveau-messages' );
+
+	// Add The tiny MCE init specific function.
+	add_filter( 'tiny_mce_before_init', 'bp_nouveau_messages_at_on_tinymce_init', 10, 2 );
+}
+
+/**
+ * Localize the strings needed for the messages UI
+ *
+ * @since 3.0.0
+ *
+ * @param  array $params Associative array containing the JS Strings needed by scripts
+ * @return array         The same array with specific strings for the messages UI if needed.
+ */
+function bp_nouveau_messages_localize_scripts( $params = array() ) {
+	if ( ! bp_is_user_messages() ) {
+		return $params;
+	}
+
+	$params['messages'] = array(
+		'errors' => array(
+			'send_to'         => __( 'Please add at least one recipient.', 'buddypress' ),
+			'subject'         => __( 'Please add a subject to your message.', 'buddypress' ),
+			'message_content' => __( 'Please add some content to your message.', 'buddypress' ),
+		),
+		'nonces' => array(
+			'send' => wp_create_nonce( 'messages_send_message' ),
+		),
+		'loading'       => __( 'Loading messages. Please wait.', 'buddypress' ),
+		'doingAction'   => array(
+			'read'   => __( 'Marking messages as read. Please wait.', 'buddypress' ),
+			'unread' => __( 'Marking messages as unread. Please wait.', 'buddypress' ),
+			'delete' => __( 'Deleting messages. Please wait.', 'buddypress' ),
+			'star'   => __( 'Starring messages. Please wait.', 'buddypress' ),
+			'unstar' => __( 'Unstarring messages. Please wait.', 'buddypress' ),
+		),
+		'bulk_actions'  => bp_nouveau_messages_get_bulk_actions(),
+		'howto'         => __( 'Click on the message title to preview it in the Active conversation box below.', 'buddypress' ),
+		'howtoBulk'     => __( 'Use the select box to define your bulk action and click on the &#10003; button to apply.', 'buddypress' ),
+		'toOthers'      => array(
+			'one'  => __( '(and 1 other)', 'buddypress' ),
+			'more' => __( '(and %d others)', 'buddypress' ),
+		),
+		'rootUrl' => parse_url( trailingslashit( bp_displayed_user_domain() . bp_get_messages_slug() ), PHP_URL_PATH ),
+	);
+
+	// Star private messages.
+	if ( bp_is_active( 'messages', 'star' ) ) {
+		$params['messages'] = array_merge( $params['messages'], array(
+			'strings' => array(
+				'text_unstar'  => __( 'Unstar', 'buddypress' ),
+				'text_star'    => __( 'Star', 'buddypress' ),
+				'title_unstar' => __( 'Starred', 'buddypress' ),
+				'title_star'   => __( 'Not starred', 'buddypress' ),
+				'title_unstar_thread' => __( 'Remove all starred messages in this thread', 'buddypress' ),
+				'title_star_thread'   => __( 'Star the first message in this thread', 'buddypress' ),
+			),
+			'is_single_thread' => (int) bp_is_messages_conversation(),
+			'star_counter'     => 0,
+			'unstar_counter'   => 0
+		) );
+	}
+
+	return $params;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_adjust_nav() {
+	$bp = buddypress();
+
+	$secondary_nav_items = $bp->members->nav->get_secondary( array( 'parent_slug' => bp_get_messages_slug() ), false );
+
+	if ( empty( $secondary_nav_items ) ) {
+		return;
+	}
+
+	foreach ( $secondary_nav_items as $secondary_nav_item ) {
+		if ( empty( $secondary_nav_item->slug ) ) {
+			continue;
+		}
+
+		if ( 'notices' === $secondary_nav_item->slug ) {
+			bp_core_remove_subnav_item( bp_get_messages_slug(), $secondary_nav_item->slug, 'members' );
+		} elseif ( 'compose' === $secondary_nav_item->slug ) {
+			$bp->members->nav->edit_nav( array(
+				'user_has_access' => bp_is_my_profile()
+			), $secondary_nav_item->slug, bp_get_messages_slug() );
+		}
+	}
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_adjust_admin_nav( $admin_nav ) {
+	if ( empty( $admin_nav ) ) {
+		return $admin_nav;
+	}
+
+	$user_messages_link = trailingslashit( bp_loggedin_user_domain() . bp_get_messages_slug() );
+
+	foreach ( $admin_nav as $nav_iterator => $nav ) {
+		$nav_id = str_replace( 'my-account-messages-', '', $nav['id'] );
+
+		if ( 'notices' === $nav_id ) {
+			$admin_nav[ $nav_iterator ]['href'] = esc_url( add_query_arg( array(
+				'page' => 'bp-notices'
+			), bp_get_admin_url( 'users.php' ) ) );
+		}
+	}
+
+	return $admin_nav;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_add_notice_notification_for_user( $notifications, $user_id ) {
+	if ( ! bp_is_active( 'messages' ) || ! doing_action( 'admin_bar_menu' ) ) {
+		return $notifications;
+	}
+
+	$notice = BP_Messages_Notice::get_active();
+	if ( empty( $notice->id ) ) {
+		return $notifications;
+	}
+
+	$closed_notices = bp_get_user_meta( $user_id, 'closed_notices', true );
+	if ( empty( $closed_notices ) ) {
+		$closed_notices = array();
+	}
+
+	if ( in_array( $notice->id, $closed_notices, true ) ) {
+		return $notifications;
+	}
+
+	$notice_notification                    = new stdClass;
+	$notice_notification->id                = 0;
+	$notice_notification->user_id           = $user_id;
+	$notice_notification->item_id           = $notice->id;
+	$notice_notification->secondary_item_id = '';
+	$notice_notification->component_name    = 'messages';
+	$notice_notification->component_action  = 'new_notice';
+	$notice_notification->date_notified     = $notice->date_sent;
+	$notice_notification->is_new            = '1';
+
+	return array_merge( $notifications, array( $notice_notification ) );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_format_notice_notification_for_user( $array ) {
+	if ( ! empty( $array['text'] ) || ! doing_action( 'admin_bar_menu' ) ) {
+		return $array;
+	}
+
+	return array(
+		'text' => __( 'New sitewide notice', 'buddypress' ),
+		'link' => bp_loggedin_user_domain(),
+	);
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_unregister_notices_widget() {
+	unregister_widget( 'BP_Messages_Sitewide_Notices_Widget' );
+}
+
+/**
+ * Add active sitewide notices to the BP template_message global.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_push_sitewide_notices() {
+	// Do not show notices if user is not logged in.
+	if ( ! is_user_logged_in() || ! bp_is_my_profile() ) {
+		return;
+	}
+
+	$notice = BP_Messages_Notice::get_active();
+	if ( empty( $notice ) ) {
+		return;
+	}
+
+	$user_id = bp_loggedin_user_id();
+
+	$closed_notices = bp_get_user_meta( $user_id, 'closed_notices', true );
+	if ( empty( $closed_notices ) ) {
+		$closed_notices = array();
+	}
+
+	if ( $notice->id && is_array( $closed_notices ) && ! in_array( $notice->id, $closed_notices, true ) ) {
+		// Inject the notice into the template_message if no other message has priority.
+		$bp = buddypress();
+
+		if ( empty( $bp->template_message ) ) {
+			$message = sprintf(
+				'<strong class="subject">%s</strong>
+				%s',
+				stripslashes( $notice->subject ),
+				stripslashes( $notice->message )
+			);
+			$bp->template_message      = $message;
+			$bp->template_message_type = 'bp-sitewide-notice';
+		}
+	}
+}
+
+/**
+ * Disable the WP Editor buttons not allowed in messages content.
+ *
+ * @since 3.0.0
+ *
+ * @param array $buttons The WP Editor buttons list.
+ * @param array          The filtered WP Editor buttons list.
+ */
+function bp_nouveau_messages_mce_buttons( $buttons = array() ) {
+	$remove_buttons = array(
+		'wp_more',
+		'spellchecker',
+		'wp_adv',
+		'fullscreen',
+		'alignleft',
+		'alignright',
+		'aligncenter',
+		'formatselect',
+	);
+
+	// Remove unused buttons
+	$buttons = array_diff( $buttons, $remove_buttons );
+
+	// Add the image button
+	array_push( $buttons, 'image' );
+
+	return $buttons;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_at_on_tinymce_init( $settings, $editor_id ) {
+	// We only apply the mentions init to the visual post editor in the WP dashboard.
+	if ( 'message_content' === $editor_id ) {
+		$settings['init_instance_callback'] = 'window.bp.Nouveau.Messages.tinyMCEinit';
+	}
+
+	return $settings;
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_get_message_date( $date ) {
+	$now  = bp_core_current_time( true, 'timestamp' );
+	$date = strtotime( $date );
+
+	$now_date    = getdate( $now );
+	$date_date   = getdate( $date );
+	$compare     = array_diff( $date_date, $now_date );
+	$date_format = 'Y/m/d';
+
+	// Use Timezone string if set.
+	$timezone_string = bp_get_option( 'timezone_string' );
+	if ( ! empty( $timezone_string ) ) {
+		$timezone_object = timezone_open( $timezone_string );
+		$datetime_object = date_create( "@{$date}" );
+		$timezone_offset = timezone_offset_get( $timezone_object, $datetime_object ) / HOUR_IN_SECONDS;
+
+	// Fall back on less reliable gmt_offset
+	} else {
+		$timezone_offset = bp_get_option( 'gmt_offset' );
+	}
+
+	// Calculate time based on the offset
+	$calculated_time = $date + ( $timezone_offset * HOUR_IN_SECONDS );
+
+	if ( empty( $compare['mday'] ) && empty( $compare['mon'] ) && empty( $compare['year'] ) ) {
+		$date_format = 'H:i';
+
+	} elseif ( empty( $compare['mon'] ) || empty( $compare['year'] ) ) {
+		$date_format = 'M j';
+	}
+
+	/**
+	 * Filters the message date for BuddyPress Nouveau display.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $value           Internationalization-ready formatted date value.
+	 * @param mixed  $calculated_time Calculated time.
+	 * @param string $date            Date value.
+	 * @param string $date_format     Format to convert the calcuated date to.
+	 */
+	return apply_filters( 'bp_nouveau_get_message_date', date_i18n( $date_format, $calculated_time, true ), $calculated_time, $date, $date_format );
+}
+
+/**
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_get_bulk_actions() {
+	ob_start();
+	bp_messages_bulk_management_dropdown();
+
+	$bulk_actions = array();
+	$bulk_options = ob_get_clean();
+
+	$matched = preg_match_all( '/<option value="(.*?)"\s*>(.*?)<\/option>/', $bulk_options, $matches, PREG_SET_ORDER );
+
+	if ( $matched && is_array( $matches ) ) {
+		foreach ( $matches as $i => $match ) {
+			if ( 0 === $i ) {
+				continue;
+			}
+
+			if ( isset( $match[1] ) && isset( $match[2] ) ) {
+				$bulk_actions[] = array(
+					'value' => trim( $match[1] ),
+					'label' => trim( $match[2] ),
+				);
+			}
+		}
+	}
+
+	return $bulk_actions;
+}
+
+/**
+ * Register notifications filters for the messages component.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_notification_filters() {
+	bp_nouveau_notifications_register_filter(
+		array(
+			'id'       => 'new_message',
+			'label'    => __( 'New private messages', 'buddypress' ),
+			'position' => 115,
+		)
+	);
+}
+
+/**
+ * Fires Messages Legacy hooks to catch the content and add them
+ * as extra keys to the JSON Messages UI reply.
+ *
+ * @since 3.0.1
+ *
+ * @param array $hooks The list of hooks to fire.
+ * @return array       An associative containing the caught content.
+ */
+function bp_nouveau_messages_catch_hook_content( $hooks = array() ) {
+	$content = array();
+
+	ob_start();
+	foreach ( $hooks as $js_key => $hook ) {
+		if ( ! has_action( $hook ) ) {
+			continue;
+		}
+
+		// Fire the hook.
+		do_action( $hook );
+
+		// Catch the sanitized content.
+		$content[ $js_key ] = bp_strip_script_and_style_tags( ob_get_contents() );
+
+		// Clean the buffer.
+		ob_clean();
+	}
+	ob_end_clean();
+
+	return $content;
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/loader.php	(revision 12265)
@@ -0,0 +1,121 @@
+<?php
+/**
+ * BP Nouveau Messages
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Messages Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Messages {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir = trailingslashit( dirname( __FILE__ ) );
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require $this->dir . 'functions.php';
+		require $this->dir . 'template-tags.php';
+
+		// Test suite requires the AJAX functions early.
+		if ( function_exists( 'tests_add_filter' ) ) {
+			require $this->dir . 'ajax.php';
+
+		// Load AJAX code only on AJAX requests.
+		} else {
+			add_action( 'admin_init', function() {
+				if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && 0 === strpos( $_REQUEST['action'], 'messages_' ) ) {
+					require bp_nouveau()->messages->dir . 'ajax.php';
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		// Notices
+		add_action( 'widgets_init', 'bp_nouveau_unregister_notices_widget' );
+		add_action( 'bp_init', 'bp_nouveau_push_sitewide_notices', 99 );
+
+		// Messages
+		add_action( 'bp_messages_setup_nav', 'bp_nouveau_messages_adjust_nav' );
+
+		// Remove deprecated scripts
+		remove_action( 'bp_enqueue_scripts', 'messages_add_autocomplete_js' );
+
+		// Enqueue the scripts for the new UI
+		add_action( 'bp_nouveau_enqueue_scripts', 'bp_nouveau_messages_enqueue_scripts' );
+
+		// Register the Messages Notifications filters
+		add_action( 'bp_nouveau_notifications_init_filters', 'bp_nouveau_messages_notification_filters' );
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		// Enqueue specific styles
+		add_filter( 'bp_nouveau_enqueue_styles', 'bp_nouveau_messages_enqueue_styles', 10, 1 );
+
+		// Register messages scripts
+		add_filter( 'bp_nouveau_register_scripts', 'bp_nouveau_messages_register_scripts', 10, 1 );
+
+		// Localize Scripts
+		add_filter( 'bp_core_get_js_strings', 'bp_nouveau_messages_localize_scripts', 10, 1 );
+
+		// Notices
+		add_filter( 'bp_messages_single_new_message_notification', 'bp_nouveau_format_notice_notification_for_user', 10, 1 );
+		add_filter( 'bp_notifications_get_all_notifications_for_user', 'bp_nouveau_add_notice_notification_for_user', 10, 2 );
+
+		// Messages
+		add_filter( 'bp_messages_admin_nav', 'bp_nouveau_messages_adjust_admin_nav', 10, 1 );
+	}
+}
+
+/**
+ * Launch the Messages loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_messages( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->messages = new BP_Nouveau_Messages();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_messages', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/messages/template-tags.php	(revision 12265)
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Messages template tags
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Fire specific hooks into the private messages template.
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   Optional. Either 'before' or 'after'.
+ * @param string $suffix Optional. Use it to add terms at the end of the hook name.
+ */
+function bp_nouveau_messages_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a message hook
+	$hook[] = 'message';
+
+	if ( $suffix ) {
+		if ( 'compose_content' === $suffix ) {
+			$hook[2] = 'messages';
+		}
+
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Load the new Messages User Interface
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_messages_member_interface() {
+	/**
+	 * Fires before the member messages content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_before_member_messages_content' );
+
+	// Load the Private messages UI
+	bp_get_template_part( 'common/js-templates/messages/index' );
+
+	/**
+	 * Fires after the member messages content.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( 'bp_after_member_messages_content' );
+}
+
+/**
+ * Output the Member's messages search form.
+ *
+ * @since  3.0.0
+ * @since  3.2.0 Move the function into Template Tags and use a template part.
+ */
+function bp_nouveau_message_search_form() {
+	$search_form_html = bp_buffer_template_part( 'common/js-templates/messages/search-form', null, false );
+
+	/**
+	 * Filters the private message component search form.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $search_form_html HTML markup for the message search form.
+	 */
+	echo apply_filters( 'bp_message_search_form', $search_form_html );
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/functions.php	(revision 12265)
@@ -0,0 +1,251 @@
+<?php
+/**
+ * Notifications functions
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register Scripts for the Notifications component
+ *
+ * @since 3.0.0
+ *
+ * @param  array  $scripts  The array of scripts to register
+ * @return array  The same array with the specific notifications scripts.
+ */
+function bp_nouveau_notifications_register_scripts( $scripts = array() ) {
+
+	if ( ! isset( $scripts['bp-nouveau'] ) ) {
+		return $scripts;
+	}
+
+	return array_merge( $scripts, array(
+		'bp-nouveau-notifications' => array(
+			'file'         => 'js/buddypress-notifications%s.js',
+			'dependencies' => array( 'bp-nouveau' ),
+			'footer'       => true,
+		),
+	) );
+}
+
+/**
+ * Enqueue the notifications scripts
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_notifications_enqueue_scripts() {
+
+	if ( ! bp_is_user_notifications() ) {
+		return;
+	}
+
+	wp_enqueue_script( 'bp-nouveau-notifications' );
+}
+
+/**
+ * Init Notifications filters and fire a hook to let
+ * plugins/components register their filters.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_notifications_init_filters() {
+	if ( ! bp_is_user_notifications() ) {
+		return;
+	}
+
+	bp_nouveau()->notifications->filters = array();
+
+	/**
+	 * Hook here to register your custom notification filters
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_nouveau_notifications_init_filters' );
+}
+
+/**
+ * Register new filters for the notifications screens.
+ *
+ * @since 3.0.0
+ *
+ * @param  array  $args {
+ *     Array of arguments.
+ *
+ *     @type string      $id         The unique string to identify your "component action". Required.
+ *     @type string      $label      The human readable notification type. Required.
+ *     @type int         $position   The position to output your filter. Optional.
+ * }
+ * @return bool True if the filter has been successfully registered. False otherwise.
+ */
+function bp_nouveau_notifications_register_filter( $args = array() ) {
+	$bp_nouveau = bp_nouveau();
+
+	$r = bp_parse_args(
+		$args,
+		array(
+			'id'       => '',
+			'label'    => '',
+			'position' => 99,
+		),
+		'nouveau_notifications_register_filter'
+	);
+
+	if ( empty( $r['id'] ) || empty( $r['label'] ) ) {
+		return false;
+	}
+
+	if ( isset( $bp_nouveau->notifications->filters[ $r['id'] ] ) ) {
+		return false;
+	}
+
+	$bp_nouveau->notifications->filters[ $r['id'] ] = $r;
+	return true;
+}
+
+/**
+ * Get one or all notifications filters.
+ *
+ * @since 3.0.0
+ *
+ * @param  string $id  The notificication component action to get the filter of.
+ *                     Leave empty to get all notifications filters.
+ * @return array|false All or a specific notifications parameters. False if no match are found.
+ */
+function bp_nouveau_notifications_get_filters( $id = '' ) {
+	$bp_nouveau = bp_nouveau();
+
+	// Get all filters
+	if ( empty( $id ) ) {
+		return $bp_nouveau->notifications->filters;
+
+	// Get a specific filter
+	} elseif ( ! empty( $id ) && isset( $bp_nouveau->notifications->filters[ $id ] ) ) {
+		return $bp_nouveau->notifications->filters[ $id ];
+
+	} else {
+		return false;
+	}
+}
+
+/**
+ * Sort Notifications according to their position arguments.
+ *
+ * @since 3.0.0
+ *
+ * @param  array  $filters The notifications filters to order.
+ * @return array  The sorted filters.
+ */
+function bp_nouveau_notifications_sort( $filters = array() ) {
+	$sorted = array();
+
+	if ( empty( $filters ) || ! is_array( $filters ) ) {
+		return $sorted;
+	}
+
+	foreach ( $filters as $filter ) {
+		$position = 99;
+
+		if ( isset( $filter['position'] ) ) {
+			$position = (int) $filter['position'];
+		}
+
+		// If position is already taken, move to the first next available
+		if ( isset( $sorted[ $position ] ) ) {
+			$sorted_keys = array_keys( $sorted );
+
+			do {
+				$position += 1;
+			} while ( in_array( $position, $sorted_keys, true ) );
+		}
+
+		$sorted[ $position ] = $filter;
+	}
+
+	ksort( $sorted );
+	return $sorted;
+}
+
+/**
+ * Add a dashicon to Notifications action links
+ *
+ * @since 3.0.0
+ *
+ * @param  string $link        The action link.
+ * @param  string $bp_tooltip  The data-bp-attribute of the link.
+ * @param  string $aria_label  The aria-label attribute of the link.
+ * @param  string $dashicon    The dashicon class.
+ * @return string              Link Output.
+ */
+function bp_nouveau_notifications_dashiconified_link( $link = '', $bp_tooltip = '', $dashicon = '' ) {
+	preg_match( '/<a\s[^>]*>(.*)<\/a>/siU', $link, $match );
+
+	if ( ! empty( $match[0] ) && ! empty( $match[1] ) && ! empty( $dashicon ) && ! empty( $bp_tooltip ) ) {
+		$link = str_replace(
+			'>' . $match[1] . '<',
+			sprintf(
+				' class="bp-tooltip" data-bp-tooltip="%1$s"><span class="dashicons %2$s" aria-hidden="true"></span><span class="bp-screen-reader-text">%3$s</span><',
+				esc_attr( $bp_tooltip ),
+				sanitize_html_class( $dashicon ),
+				$match[1]
+			),
+			$match[0]
+		);
+	}
+
+	return $link;
+}
+
+/**
+ * Edit the Mark Unread action link to include a dashicon
+ *
+ * @since 3.0.0
+ *
+ * @param string $link Optional. The Mark Unread action link.
+ *
+ * @return string Link Output.
+ */
+function bp_nouveau_notifications_mark_unread_link( $link = '' ) {
+	return bp_nouveau_notifications_dashiconified_link(
+		$link,
+		_x( 'Mark Unread', 'link', 'buddypress' ),
+		'dashicons-hidden'
+	);
+}
+
+/**
+ * Edit the Mark Read action link to include a dashicon
+ *
+ * @since 3.0.0
+ *
+ * @param string $link Optional. The Mark Read action link.
+ *
+ * @return string Link Output.
+ */
+function bp_nouveau_notifications_mark_read_link( $link = '' ) {
+	return bp_nouveau_notifications_dashiconified_link(
+		$link,
+		_x( 'Mark Read', 'link', 'buddypress' ),
+		'dashicons-visibility'
+	);
+}
+
+/**
+ * Edit the Delete action link to include a dashicon
+ *
+ * @since 3.0.0
+ *
+ * @param string $link Optional. The Delete action link.
+ *
+ * @return string Link Output.
+ */
+function bp_nouveau_notifications_delete_link( $link = '' ) {
+	return bp_nouveau_notifications_dashiconified_link(
+		$link,
+		_x( 'Delete', 'link', 'buddypress' ),
+		'dashicons-dismiss'
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/loader.php	(revision 12265)
@@ -0,0 +1,105 @@
+<?php
+/**
+ * BP Nouveau Notifications
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Notifications Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_Notifications {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir = dirname( __FILE__ );
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		$dir = trailingslashit( $this->dir );
+
+		require "{$dir}functions.php";
+		require "{$dir}template-tags.php";
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		add_action( 'bp_init', 'bp_nouveau_notifications_init_filters', 20 );
+		add_action( 'bp_nouveau_enqueue_scripts', 'bp_nouveau_notifications_enqueue_scripts' );
+
+		$ajax_actions = array(
+			array(
+				'notifications_filter' => array(
+					'function' => 'bp_nouveau_ajax_object_template_loader',
+					'nopriv'   => false,
+				),
+			),
+		);
+
+		foreach ( $ajax_actions as $ajax_action ) {
+			$action = key( $ajax_action );
+
+			add_action( 'wp_ajax_' . $action, $ajax_action[ $action ]['function'] );
+
+			if ( ! empty( $ajax_action[ $action ]['nopriv'] ) ) {
+				add_action( 'wp_ajax_nopriv_' . $action, $ajax_action[ $action ]['function'] );
+			}
+		}
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		add_filter( 'bp_nouveau_register_scripts', 'bp_nouveau_notifications_register_scripts', 10, 1 );
+		add_filter( 'bp_get_the_notification_mark_unread_link', 'bp_nouveau_notifications_mark_unread_link', 10, 1 );
+		add_filter( 'bp_get_the_notification_mark_read_link', 'bp_nouveau_notifications_mark_read_link', 10, 1 );
+		add_filter( 'bp_get_the_notification_delete_link', 'bp_nouveau_notifications_delete_link', 10, 1 );
+	}
+}
+
+/**
+ * Launch the Notifications loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_notifications( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->notifications = new BP_Nouveau_Notifications();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_notifications', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/notifications/template-tags.php	(revision 12265)
@@ -0,0 +1,123 @@
+<?php
+/**
+ * Notifications template tags
+ *
+ * @since 3.0.0
+ * @version 3.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Display the notifications filter options.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_notifications_filters() {
+	echo bp_nouveau_get_notifications_filters();
+}
+
+	/**
+	 * Get the notifications filter options.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string HTML output.
+	 */
+	function bp_nouveau_get_notifications_filters() {
+		$output   = '';
+		$filters  = bp_nouveau_notifications_sort( bp_nouveau_notifications_get_filters() );
+		$selected = 0;
+
+		if ( ! empty( $_REQUEST['type'] ) ) {
+			$selected = sanitize_key( $_REQUEST['type'] );
+		}
+
+		foreach ( $filters as $filter ) {
+			if ( empty( $filter['id'] ) || empty( $filter['label'] ) ) {
+				continue;
+			}
+
+			$output .= sprintf( '<option value="%1$s" %2$s>%3$s</option>',
+				esc_attr( sanitize_key( $filter['id'] ) ),
+				selected( $selected, $filter['id'], false ),
+				esc_html( $filter['label'] )
+			) . "\n";
+		}
+
+		if ( $output ) {
+			$output = sprintf( '<option value="%1$s" %2$s>%3$s</option>',
+				0,
+				selected( $selected, 0, false ),
+				esc_html__( '&mdash; Everything &mdash;', 'buddypress' )
+			) . "\n" . $output;
+		}
+
+		/**
+		 * Filter to edit the options output.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $output  The options output.
+		 * @param array  $filters The sorted notifications filters.
+		 */
+		return apply_filters( 'bp_nouveau_get_notifications_filters', $output, $filters );
+	}
+
+/**
+ * Outputs the order action links.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_notifications_sort_order_links() {
+	if ( 'unread' === bp_current_action() ) {
+		$link = bp_get_notifications_unread_permalink( bp_displayed_user_id() );
+	} else {
+		$link = bp_get_notifications_read_permalink( bp_displayed_user_id() );
+	}
+
+	$desc = add_query_arg( 'sort_order', 'DESC', $link );
+	$asc  = add_query_arg( 'sort_order', 'ASC', $link );
+	?>
+
+	<span class="notifications-order-actions">
+		<a href="<?php echo esc_url( $desc ); ?>" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( 'Newest First', 'buddypress' ); ?>" aria-label="<?php esc_attr_e( 'Newest First', 'buddypress' ); ?>" data-bp-notifications-order="DESC"><span class="dashicons dashicons-arrow-down" aria-hidden="true"></span></a>
+		<a href="<?php echo esc_url( $asc ); ?>" class="bp-tooltip" data-bp-tooltip="<?php esc_attr_e( 'Oldest First', 'buddypress' ); ?>" aria-label="<?php esc_attr_e( 'Oldest First', 'buddypress' ); ?>" data-bp-notifications-order="ASC"><span class="dashicons dashicons-arrow-up" aria-hidden="true"></span></a>
+	</span>
+
+	<?php
+}
+
+/**
+ * Output the dropdown for bulk management of notifications.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_notifications_bulk_management_dropdown() {
+?>
+
+	<div class="select-wrap">
+
+		<label class="bp-screen-reader-text" for="notification-select"><?php
+			esc_html_e( 'Select Bulk Action', 'buddypress' );
+		?></label>
+
+		<select name="notification_bulk_action" id="notification-select">
+			<option value="" selected="selected"><?php echo esc_html( 'Bulk Actions', 'buddypress' ); ?></option>
+
+			<?php if ( bp_is_current_action( 'unread' ) ) : ?>
+				<option value="read"><?php echo esc_html_x( 'Mark read', 'button', 'buddypress' ); ?></option>
+			<?php elseif ( bp_is_current_action( 'read' ) ) : ?>
+				<option value="unread"><?php echo esc_html_x( 'Mark unread', 'button', 'buddypress' ); ?></option>
+			<?php endif; ?>
+			<option value="delete"><?php echo esc_html_x( 'Delete', 'button', 'buddypress' ); ?></option>
+		</select>
+
+		<span class="select-arrow"></span>
+
+	</div><!-- // .select-wrap -->
+
+	<input type="submit" id="notification-bulk-manage" class="button action" value="<?php echo esc_attr_x( 'Apply', 'button', 'buddypress' ); ?>">
+	<?php
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/template-tags.php	(revision 12265)
@@ -0,0 +1,2570 @@
+<?php
+/**
+ * Common template tags
+ *
+ * @since 3.0.0
+ * @version 4.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Fire specific hooks at various places of templates
+ *
+ * @since 3.0.0
+ *
+ * @param array $pieces The list of terms of the hook to join.
+ */
+function bp_nouveau_hook( $pieces = array() ) {
+	if ( ! $pieces ) {
+		return;
+	}
+
+	$bp_prefix = reset( $pieces );
+	if ( 'bp' !== $bp_prefix ) {
+		array_unshift( $pieces, 'bp' );
+	}
+
+	$hook = join( '_', $pieces );
+
+	/**
+	 * Fires inside the `bp_nouveau_hook()` function.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( $hook );
+}
+
+/**
+ * Fire plugin hooks in the plugins.php template (Groups and Members single items)
+ *
+ * @since 3.0.0
+ *
+ * @param string The suffix of the hook.
+ */
+function bp_nouveau_plugin_hook( $suffix = '' ) {
+	if ( ! $suffix ) {
+		return;
+	}
+
+	bp_nouveau_hook(
+		array(
+			'bp',
+			'template',
+			$suffix,
+		)
+	);
+}
+
+/**
+ * Fire friend hooks
+ *
+ * @todo Move this into bp-nouveau/includes/friends/template-tags.php
+ *       once we'll need other friends template tags.
+ *
+ * @since 3.0.0
+ *
+ * @param string The suffix of the hook.
+ */
+function bp_nouveau_friend_hook( $suffix = '' ) {
+	if ( ! $suffix ) {
+		return;
+	}
+
+	bp_nouveau_hook(
+		array(
+			'bp',
+			'friend',
+			$suffix,
+		)
+	);
+}
+
+/**
+ * Add classes to style the template notice/feedback message
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_template_message_classes() {
+	$classes = array( 'bp-feedback', 'bp-messages' );
+
+	if ( ! empty( bp_nouveau()->template_message['message'] ) ) {
+		$classes[] = 'bp-template-notice';
+	}
+
+	$classes[] = bp_nouveau_get_template_message_type();
+	echo join( ' ', array_map( 'sanitize_html_class', $classes ) );
+}
+
+	/**
+	 * Get the template notice/feedback message type
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string The type of the notice. Defaults to error.
+	 */
+	function bp_nouveau_get_template_message_type() {
+		$bp_nouveau = bp_nouveau();
+		$type       = 'error';
+
+		if ( ! empty( $bp_nouveau->template_message['type'] ) ) {
+			$type = $bp_nouveau->template_message['type'];
+		} elseif ( ! empty( $bp_nouveau->user_feedback['type'] ) ) {
+			$type = $bp_nouveau->user_feedback['type'];
+		}
+
+		return $type;
+	}
+
+/**
+ * Checks if a template notice/feedback message is set
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if a template notice is set. False otherwise.
+ */
+function bp_nouveau_has_template_message() {
+	$bp_nouveau = bp_nouveau();
+
+	if ( empty( $bp_nouveau->template_message['message'] ) && empty( $bp_nouveau->user_feedback ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Checks if the template notice/feedback message needs a dismiss button
+ *
+ * @todo Dismiss button re-worked to try and prevent buttons on general
+ *       BP template notices - Nouveau user_feedback key needs review.
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if a template notice needs a dismiss button. False otherwise.
+ */
+function bp_nouveau_has_dismiss_button() {
+	$bp_nouveau = bp_nouveau();
+
+	// BP template notices - set 'dismiss' true for a type in `bp_nouveau_template_notices()`
+	if ( ! empty( $bp_nouveau->template_message['message'] ) && true === $bp_nouveau->template_message['dismiss'] ) {
+		return true;
+	}
+
+	// Test for isset as value can be falsey.
+	if ( isset( $bp_nouveau->user_feedback['dismiss'] ) ) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Ouptut the dismiss type.
+ *
+ * $type is used to set the data-attr for the button.
+ * 'clear' is tested for & used to remove cookies, if set, in buddypress-nouveau.js.
+ * Currently template_notices(BP) will take $type = 'clear' if button set to true.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_dismiss_button_type() {
+	$bp_nouveau = bp_nouveau();
+	$type       = 'clear';
+
+	if ( ! empty( $bp_nouveau->user_feedback['dismiss'] ) ) {
+		$type = $bp_nouveau->user_feedback['dismiss'];
+	}
+
+	echo esc_attr( $type );
+}
+
+/**
+ * Displays a template notice/feedback message.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_template_message() {
+	echo bp_nouveau_get_template_message();
+}
+
+	/**
+	 * Get the template notice/feedback message and make sure core filter is applied.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string HTML Output.
+	 */
+	function bp_nouveau_get_template_message() {
+		$bp_nouveau = bp_nouveau();
+
+		if ( ! empty( $bp_nouveau->user_feedback['message'] ) ) {
+			$user_feedback = $bp_nouveau->user_feedback['message'];
+
+			// @TODO: why is this treated differently?
+			foreach ( array( 'wp_kses_data', 'wp_unslash', 'wptexturize', 'convert_smilies', 'convert_chars' ) as $filter ) {
+				$user_feedback = call_user_func( $filter, $user_feedback );
+			}
+
+			return '<p>' . $user_feedback . '</p>';
+
+		} elseif ( ! empty( $bp_nouveau->template_message['message'] ) ) {
+			/**
+			 * Filters the 'template_notices' feedback message content.
+			 *
+			 * @since 1.5.5
+			 *
+			 * @param string $template_message Feedback message content.
+			 * @param string $type             The type of message being displayed.
+			 *                                 Either 'updated' or 'error'.
+			 */
+			return apply_filters( 'bp_core_render_message_content', $bp_nouveau->template_message['message'], bp_nouveau_get_template_message_type() );
+		}
+	}
+
+/**
+ * Template tag to display feedback notices to users, if there are to display
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_template_notices() {
+	$bp         = buddypress();
+	$bp_nouveau = bp_nouveau();
+
+	if ( ! empty( $bp->template_message ) ) {
+		// Clone BuddyPress template message to avoid altering it.
+		$template_message = array( 'message' => $bp->template_message );
+
+		if ( ! empty( $bp->template_message_type ) ) {
+			$template_message['type'] = $bp->template_message_type;
+		}
+
+		// Adds a 'dimiss' (button) key to array - set true/false.
+		$template_message['dismiss'] = false;
+
+		// Set dismiss button true for sitewide notices
+		if ( 'bp-sitewide-notice' == $template_message['type'] ) {
+			$template_message['dismiss'] = true;
+		}
+
+		$bp_nouveau->template_message = $template_message;
+		bp_get_template_part( 'common/notices/template-notices' );
+
+		// Reset just after rendering it.
+		$bp_nouveau->template_message = array();
+
+		/**
+		 * Fires after the display of any template_notices feedback messages.
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( 'bp_core_render_message' );
+	}
+
+	/**
+	 * Fires towards the top of template pages for notice display.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'template_notices' );
+}
+
+/**
+ * Displays a feedback message to the user.
+ *
+ * @since 3.0.0
+ *
+ * @param string $feedback_id The ID of the message to display
+ */
+function bp_nouveau_user_feedback( $feedback_id = '' ) {
+	if ( ! isset( $feedback_id ) ) {
+		return;
+	}
+
+	$bp_nouveau = bp_nouveau();
+	$feedback   = bp_nouveau_get_user_feedback( $feedback_id );
+
+	if ( ! $feedback ) {
+		return;
+	}
+
+	if ( ! empty( $feedback['before'] ) ) {
+
+		/**
+		 * Fires before display of a feedback message to the user.
+		 *
+		 * This is a dynamic filter that is dependent on the "before" value provided by bp_nouveau_get_user_feedback().
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( $feedback['before'] );
+	}
+
+	$bp_nouveau->user_feedback = $feedback;
+
+	bp_get_template_part(
+
+		/**
+		 * Filter here if you wish to use a different templates than the notice one.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string path to your template part.
+		 */
+		apply_filters( 'bp_nouveau_user_feedback_template', 'common/notices/template-notices' )
+	);
+
+	if ( ! empty( $feedback['after'] ) ) {
+
+		/**
+		 * Fires before display of a feedback message to the user.
+		 *
+		 * This is a dynamic filter that is dependent on the "after" value provided by bp_nouveau_get_user_feedback().
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( $feedback['after'] );
+	}
+
+	// Reset the feedback message.
+	$bp_nouveau->user_feedback = array();
+}
+
+/**
+ * Template tag to wrap the before component loop
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_before_loop() {
+	$component = bp_current_component();
+
+	if ( bp_is_group() ) {
+		$component = bp_current_action();
+	}
+
+	/**
+	 * Fires before the start of the component loop.
+	 *
+	 * This is a variable hook that is dependent on the current component.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( "bp_before_{$component}_loop" );
+}
+
+/**
+ * Template tag to wrap the after component loop
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_after_loop() {
+	$component = bp_current_component();
+
+	if ( bp_is_group() ) {
+		$component = bp_current_action();
+	}
+
+	/**
+	 * Fires after the finish of the component loop.
+	 *
+	 * This is a variable hook that is dependent on the current component.
+	 *
+	 * @since 1.2.0
+	 */
+	do_action( "bp_after_{$component}_loop" );
+}
+
+/**
+ * Pagination for loops
+ *
+ * @param string $position
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_pagination( $position ) {
+	$screen          = 'dir';
+	$pagination_type = bp_current_component();
+
+	if ( bp_is_user() ) {
+		$screen = 'user';
+
+	} elseif ( bp_is_group() ) {
+		$screen          = 'group';
+		$pagination_type = bp_current_action();
+
+		if ( bp_is_group_admin_page() ) {
+			$pagination_type = bp_action_variable( 0 );
+		}
+	}
+
+	switch ( $pagination_type ) {
+		case 'blogs':
+			$pag_count   = bp_get_blogs_pagination_count();
+			$pag_links   = bp_get_blogs_pagination_links();
+			$top_hook    = 'bp_before_directory_blogs_list';
+			$bottom_hook = 'bp_after_directory_blogs_list';
+			$page_arg    = $GLOBALS['blogs_template']->pag_arg;
+			break;
+
+		case 'members':
+		case 'friends':
+		case 'manage-members':
+			$pag_count = bp_get_members_pagination_count();
+			$pag_links = bp_get_members_pagination_links();
+
+			// Groups single items are not using these hooks
+			if ( ! bp_is_group() ) {
+				$top_hook    = 'bp_before_directory_members_list';
+				$bottom_hook = 'bp_after_directory_members_list';
+			}
+
+			$page_arg = $GLOBALS['members_template']->pag_arg;
+			break;
+
+		case 'groups':
+			$pag_count   = bp_get_groups_pagination_count();
+			$pag_links   = bp_get_groups_pagination_links();
+			$top_hook    = 'bp_before_directory_groups_list';
+			$bottom_hook = 'bp_after_directory_groups_list';
+			$page_arg    = $GLOBALS['groups_template']->pag_arg;
+			break;
+
+		case 'notifications':
+			$pag_count   = bp_get_notifications_pagination_count();
+			$pag_links   = bp_get_notifications_pagination_links();
+			$top_hook    = '';
+			$bottom_hook = '';
+			$page_arg    = buddypress()->notifications->query_loop->pag_arg;
+			break;
+
+		case 'membership-requests':
+			$pag_count   = bp_get_group_requests_pagination_count();
+			$pag_links   = bp_get_group_requests_pagination_links();
+			$top_hook    = '';
+			$bottom_hook = '';
+			$page_arg    = $GLOBALS['requests_template']->pag_arg;
+			break;
+	}
+
+	$count_class = sprintf( '%1$s-%2$s-count-%3$s', $pagination_type, $screen, $position );
+	$links_class = sprintf( '%1$s-%2$s-links-%3$s', $pagination_type, $screen, $position );
+	?>
+
+	<?php
+	if ( 'bottom' === $position && isset( $bottom_hook ) ) {
+		/**
+		 * Fires after the component directory list.
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( $bottom_hook );
+	};
+	?>
+
+	<div class="<?php echo esc_attr( 'bp-pagination ' . sanitize_html_class( $position ) ); ?>" data-bp-pagination="<?php echo esc_attr( $page_arg ); ?>">
+
+		<?php if ( $pag_count ) : ?>
+			<div class="<?php echo esc_attr( 'pag-count ' . sanitize_html_class( $position ) ); ?>">
+
+				<p class="pag-data">
+					<?php echo esc_html( $pag_count ); ?>
+				</p>
+
+			</div>
+		<?php endif; ?>
+
+		<?php if ( $pag_links ) : ?>
+			<div class="<?php echo esc_attr( 'bp-pagination-links ' . sanitize_html_class( $position ) ); ?>">
+
+				<p class="pag-data">
+					<?php echo wp_kses_post( $pag_links ); ?>
+				</p>
+
+			</div>
+		<?php endif; ?>
+
+	</div>
+
+	<?php
+	if ( 'top' === $position && isset( $top_hook ) ) {
+		/**
+		 * Fires before the component directory list.
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( $top_hook );
+	};
+}
+
+/**
+ * Display the component's loop classes
+ *
+ * @since 3.0.0
+ *
+ * @return string CSS class attributes (escaped).
+ */
+function bp_nouveau_loop_classes() {
+	echo esc_attr( bp_nouveau_get_loop_classes() );
+}
+
+	/**
+	 * Get the component's loop classes
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string space separated value of classes.
+	 */
+	function bp_nouveau_get_loop_classes() {
+		$bp_nouveau = bp_nouveau();
+
+		// @todo: this function could do with passing args so we can pass simple strings in or array of strings
+
+		// The $component is faked if it's the single group member loop
+		if ( ! bp_is_directory() && ( bp_is_group() && 'members' === bp_current_action() ) ) {
+			$component = 'members_group';
+		} elseif ( ! bp_is_directory() && ( bp_is_user() && 'my-friends' === bp_current_action() ) ) {
+			$component = 'members_friends';
+		} else {
+			$component = sanitize_key( bp_current_component() );
+		}
+
+		$classes = array(
+			'item-list',
+			sprintf( '%s-list', str_replace( '_', '-', $component ) ),
+			'bp-list',
+		);
+
+		if ( bp_is_user() && 'my-friends' === bp_current_action() ) {
+			$classes[] = 'members-list';
+		}
+
+		if ( bp_is_user() && 'requests' === bp_current_action() ) {
+			$classes[] = 'friends-request-list';
+		}
+
+		$available_components = array(
+			'members' => true,
+			'groups'  => true,
+			'blogs'   => true,
+
+			/*
+			 * Technically not a component but allows us to check the single group members loop as a seperate loop.
+			 */
+			'members_group'   => true,
+			'members_friends' => true,
+		);
+
+		// Only the available components supports custom layouts.
+		if ( ! empty( $available_components[ $component ] ) && ( bp_is_directory() || bp_is_group() || bp_is_user() ) ) {
+			$customizer_option = sprintf( '%s_layout', $component );
+			$layout_prefs      = bp_nouveau_get_temporary_setting(
+				$customizer_option,
+				bp_nouveau_get_appearance_settings( $customizer_option )
+			);
+
+			if ( $layout_prefs && (int) $layout_prefs > 1 ) {
+				$grid_classes = bp_nouveau_customizer_grid_choices( 'classes' );
+
+				if ( isset( $grid_classes[ $layout_prefs ] ) ) {
+					$classes = array_merge( $classes, array(
+						'grid',
+						$grid_classes[ $layout_prefs ],
+					) );
+				}
+
+				if ( ! isset( $bp_nouveau->{$component} ) ) {
+					$bp_nouveau->{$component} = new stdClass;
+				}
+
+				// Set the global for a later use.
+				$bp_nouveau->{$component}->loop_layout = $layout_prefs;
+			}
+		}
+
+		/**
+		 * Filter to edit/add classes.
+		 *
+		 * NB: you can also directly add classes into the template parts.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array  $classes   The list of classes.
+		 * @param string $component The current component's loop.
+		 */
+		$class_list = (array) apply_filters( 'bp_nouveau_get_loop_classes', $classes, $component );
+
+		return join( ' ', array_map( 'sanitize_html_class', $class_list ) );
+	}
+
+
+/**
+ * Checks if the layout preferences is set to grid (2 or more columns).
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if loop is displayed in grid mod. False otherwise.
+ */
+function bp_nouveau_loop_is_grid() {
+	$bp_nouveau = bp_nouveau();
+	$component  = sanitize_key( bp_current_component() );
+
+	return ! empty( $bp_nouveau->{$component}->loop_layout ) && $bp_nouveau->{$component}->loop_layout > 1;
+}
+
+/**
+ * Returns the number of columns of the layout preferences.
+ *
+ * @since 3.0.0
+ *
+ * @return int The number of columns.
+ */
+function bp_nouveau_loop_get_grid_columns() {
+	$bp_nouveau = bp_nouveau();
+	$component  = sanitize_key( bp_current_component() );
+
+	$columns = 1;
+
+	if ( ! empty( $bp_nouveau->{$component}->loop_layout ) ) {
+		$columns = (int) $bp_nouveau->{$component}->loop_layout;
+	}
+
+	/**
+	 * Filter number of columns for this grid.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param int $columns The number of columns.
+	 */
+	return (int) apply_filters( 'bp_nouveau_loop_get_grid_columns', $columns );
+}
+
+/**
+ * Return a bool check for component directory layout.
+ *
+ * Checks if activity, members, groups, blogs has the vert nav layout selected.
+ *
+ * @since 3.0.0
+ *
+ * @return bool
+ */
+function bp_dir_is_vert_layout() {
+	$bp_nouveau = bp_nouveau();
+	$component  = sanitize_key( bp_current_component() );
+
+	return (bool) $bp_nouveau->{$component}->directory_vertical_layout;
+}
+
+/**
+ * Get the full size avatar args.
+ *
+ * @since 3.0.0
+ *
+ * @return array The avatar arguments.
+ */
+function bp_nouveau_avatar_args() {
+	/**
+	 * Filter arguments for full-size avatars.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $args {
+	 *     @param string $type   Avatar type.
+	 *     @param int    $width  Avatar width value.
+	 *     @param int    $height Avatar height value.
+	 * }
+	 */
+	return apply_filters( 'bp_nouveau_avatar_args', array(
+		'type'   => 'full',
+		'width'  => bp_core_avatar_full_width(),
+		'height' => bp_core_avatar_full_height(),
+	) );
+}
+
+
+/** Template Tags for BuddyPress navigations **********************************/
+
+/*
+ * This is the BP Nouveau Navigation Loop.
+ *
+ * It can be used by any object using the
+ * BP_Core_Nav API introduced in BuddyPress 2.6.0.
+ */
+
+/**
+ * Init the Navigation Loop and check it has items.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args {
+ *     Array of arguments.
+ *
+ *     @type string $type                    The type of Nav to get (primary or secondary)
+ *                                           Default 'primary'. Required.
+ *     @type string $object                  The object to get the nav for (eg: 'directory', 'group_manage',
+ *                                           or any custom object). Default ''. Optional
+ *     @type bool   $user_has_access         Used by the secondary member's & group's nav. Default true. Optional.
+ *     @type bool   $show_for_displayed_user Used by the primary member's nav. Default true. Optional.
+ * }
+ *
+ * @return bool True if the Nav contains items. False otherwise.
+ */
+function bp_nouveau_has_nav( $args = array() ) {
+	$bp_nouveau = bp_nouveau();
+
+	$n = bp_parse_args(
+		$args,
+		array(
+			'type'                    => 'primary',
+			'object'                  => '',
+			'user_has_access'         => true,
+			'show_for_displayed_user' => true,
+		),
+		'nouveau_has_nav'
+	);
+
+	if ( empty( $n['type'] ) ) {
+		return false;
+	}
+
+	$nav                       = array();
+	$bp_nouveau->displayed_nav = '';
+	$bp_nouveau->object_nav    = $n['object'];
+
+	if ( bp_is_directory() || 'directory' === $bp_nouveau->object_nav ) {
+		$bp_nouveau->displayed_nav = 'directory';
+		$nav                       = $bp_nouveau->directory_nav->get_primary();
+
+	// So far it's only possible to build a Group nav when displaying it.
+	} elseif ( bp_is_group() ) {
+		$bp_nouveau->displayed_nav = 'groups';
+		$parent_slug               = bp_get_current_group_slug();
+		$group_nav                 = buddypress()->groups->nav;
+
+		if ( 'group_manage' === $bp_nouveau->object_nav && bp_is_group_admin_page() ) {
+			$parent_slug .= '_manage';
+
+		/**
+		 * If it's not the Admin tabs, reorder the Group's nav according to the
+		 * customizer setting.
+		 */
+		} else {
+			bp_nouveau_set_nav_item_order( $group_nav, bp_nouveau_get_appearance_settings( 'group_nav_order' ), $parent_slug );
+		}
+
+		$nav = $group_nav->get_secondary(
+			array(
+				'parent_slug'     => $parent_slug,
+				'user_has_access' => (bool) $n['user_has_access'],
+			)
+		);
+
+	// Build the nav for the displayed user
+	} elseif ( bp_is_user() ) {
+		$bp_nouveau->displayed_nav = 'personal';
+		$user_nav                  = buddypress()->members->nav;
+
+		if ( 'secondary' === $n['type'] ) {
+			$nav = $user_nav->get_secondary(
+				array(
+					'parent_slug'     => bp_current_component(),
+					'user_has_access' => (bool) $n['user_has_access'],
+				)
+			);
+
+		} else {
+			$args = array();
+
+			if ( true === (bool) $n['show_for_displayed_user'] && ! bp_is_my_profile() ) {
+				$args = array( 'show_for_displayed_user' => true );
+			}
+
+			// Reorder the user's primary nav according to the customizer setting.
+			bp_nouveau_set_nav_item_order( $user_nav, bp_nouveau_get_appearance_settings( 'user_nav_order' ) );
+
+			$nav = $user_nav->get_primary( $args );
+		}
+
+	} elseif ( ! empty( $bp_nouveau->object_nav ) ) {
+		$bp_nouveau->displayed_nav = $bp_nouveau->object_nav;
+
+		/**
+		 * Use the filter to use your specific Navigation.
+		 * Use the $n param to check for your custom object.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $nav The list of item navigations generated by the BP_Core_Nav API.
+		 * @param array $n   The arguments of the Navigation loop.
+		 */
+		$nav = apply_filters( 'bp_nouveau_get_nav', $nav, $n );
+
+	}
+
+	// The navigation can be empty.
+	if ( $nav === false ) {
+		$nav = array();
+	}
+
+	$bp_nouveau->sorted_nav = array_values( $nav );
+
+	if ( 0 === count( $bp_nouveau->sorted_nav ) || ! $bp_nouveau->displayed_nav ) {
+		unset( $bp_nouveau->sorted_nav, $bp_nouveau->displayed_nav, $bp_nouveau->object_nav );
+
+		return false;
+	}
+
+	$bp_nouveau->current_nav_index = 0;
+	return true;
+}
+
+/**
+ * Checks there are still nav items to display.
+ *
+ * @since 3.0.0
+ *
+ * @return bool True if there are still items to display. False otherwise.
+ */
+function bp_nouveau_nav_items() {
+	$bp_nouveau = bp_nouveau();
+
+	if ( isset( $bp_nouveau->sorted_nav[ $bp_nouveau->current_nav_index ] ) ) {
+		return true;
+	}
+
+	$bp_nouveau->current_nav_index = 0;
+	unset( $bp_nouveau->current_nav_item );
+
+	return false;
+}
+
+/**
+ * Sets the current nav item and prepare the navigation loop to iterate to next one.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_item() {
+	$bp_nouveau = bp_nouveau();
+
+	$bp_nouveau->current_nav_item   = $bp_nouveau->sorted_nav[ $bp_nouveau->current_nav_index ];
+	$bp_nouveau->current_nav_index += 1;
+}
+
+/**
+ * Displays the nav item ID.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_id() {
+	echo esc_attr( bp_nouveau_get_nav_id() );
+}
+
+	/**
+	 * Retrieve the ID attribute of the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string the ID attribute.
+	 */
+	function bp_nouveau_get_nav_id() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+
+		if ( 'directory' === $bp_nouveau->displayed_nav ) {
+			$id = sprintf( '%1$s-%2$s', $nav_item->component, $nav_item->slug );
+		} elseif ( 'groups' === $bp_nouveau->displayed_nav || 'personal' ===  $bp_nouveau->displayed_nav ) {
+			$id = sprintf( '%1$s-%2$s-li', $nav_item->css_id, $bp_nouveau->displayed_nav );
+		} else {
+			$id = $nav_item->slug;
+		}
+
+		/**
+		 * Filter to edit the ID attribute of the nav.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $id       The ID attribute of the nav.
+		 * @param object $nav_item The current nav item object.
+		 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		return apply_filters( 'bp_nouveau_get_nav_id', $id, $nav_item, $bp_nouveau->displayed_nav );
+	}
+
+/**
+ * Displays the nav item classes.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_classes() {
+	echo esc_attr( bp_nouveau_get_nav_classes() );
+}
+
+	/**
+	 * Retrieve a space separated list of classes for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string List of classes.
+	 */
+	function bp_nouveau_get_nav_classes() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+		$classes    = array();
+
+		if ( 'directory' === $bp_nouveau->displayed_nav ) {
+			if ( ! empty( $nav_item->li_class ) ) {
+				$classes = (array) $nav_item->li_class;
+			}
+
+			if ( bp_get_current_member_type() || ( bp_is_groups_directory() && bp_get_current_group_directory_type() ) ) {
+				$classes[] = 'no-ajax';
+			}
+		} elseif ( 'groups' === $bp_nouveau->displayed_nav || 'personal' === $bp_nouveau->displayed_nav ) {
+			$classes  = array( 'bp-' . $bp_nouveau->displayed_nav . '-tab' );
+			$selected = bp_current_action();
+
+			// User's primary nav
+			if ( ! empty( $nav_item->primary ) ) {
+				$selected = bp_current_component();
+
+			// Group Admin Tabs.
+			} elseif ( 'group_manage' === $bp_nouveau->object_nav ) {
+				$selected = bp_action_variable( 0 );
+				$classes  = array( 'bp-' . $bp_nouveau->displayed_nav . '-admin-tab' );
+
+			// If we are here, it's the member's subnav
+			} elseif ( 'personal' === $bp_nouveau->displayed_nav ) {
+				$classes  = array( 'bp-' . $bp_nouveau->displayed_nav . '-sub-tab' );
+			}
+
+			if ( $nav_item->slug === $selected ) {
+				$classes = array_merge( $classes, array( 'current', 'selected' ) );
+			}
+		}
+
+		if ( ! empty( $classes ) ) {
+			$classes = array_map( 'sanitize_html_class', $classes );
+		}
+
+		/**
+		 * Filter to edit/add classes.
+		 *
+		 * NB: you can also directly add classes into the template parts.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $value    A space separated list of classes.
+		 * @param array  $classes  The list of classes.
+		 * @param object $nav_item The current nav item object.
+		 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		$classes_list = apply_filters( 'bp_nouveau_get_classes', join( ' ', $classes ), $classes, $nav_item, $bp_nouveau->displayed_nav );
+		if ( ! $classes_list ) {
+			$classes_list = '';
+		}
+
+		return $classes_list;
+	}
+
+/**
+ * Displays the nav item scope.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_scope() {
+	echo bp_nouveau_get_nav_scope();  // Escaped by bp_get_form_field_attributes().
+}
+
+	/**
+	 * Retrieve the specific scope for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string the specific scope of the nav.
+	 */
+	function bp_nouveau_get_nav_scope() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+		$scope      = array();
+
+		if ( 'directory' === $bp_nouveau->displayed_nav ) {
+			$scope = array( 'data-bp-scope' => $nav_item->slug );
+
+		} elseif ( 'personal' === $bp_nouveau->displayed_nav && ! empty( $nav_item->secondary ) ) {
+			$scope = array( 'data-bp-user-scope' => $nav_item->slug );
+
+		} else {
+			/**
+			 * Filter to add your own scope.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param array $scope     Contains the key and the value for your scope.
+			 * @param object $nav_item The current nav item object.
+			 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+			 */
+			$scope = apply_filters( 'bp_nouveau_set_nav_scope', $scope, $nav_item, $bp_nouveau->displayed_nav );
+		}
+
+		if ( ! $scope ) {
+			return '';
+		}
+
+		return bp_get_form_field_attributes( 'scope', $scope );
+	}
+
+/**
+ * Displays the nav item URL.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_link() {
+	echo esc_url( bp_nouveau_get_nav_link() );
+}
+
+	/**
+	 * Retrieve the URL for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string The URL for the nav item.
+	 */
+	function bp_nouveau_get_nav_link() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+
+		$link = '#';
+		if ( ! empty( $nav_item->link ) ) {
+			$link = $nav_item->link;
+		}
+
+		if ( 'personal' === $bp_nouveau->displayed_nav && ! empty( $nav_item->primary ) ) {
+			if ( bp_loggedin_user_domain() ) {
+				$link = str_replace( bp_loggedin_user_domain(), bp_displayed_user_domain(), $link );
+			} else {
+				$link = trailingslashit( bp_displayed_user_domain() . $link );
+			}
+		}
+
+		/**
+		 * Filter to edit the URL of the nav item.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $link     The URL for the nav item.
+		 * @param object $nav_item The current nav item object.
+		 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		return apply_filters( 'bp_nouveau_get_nav_link', $link, $nav_item, $bp_nouveau->displayed_nav );
+	}
+
+/**
+ * Displays the nav item link ID.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_link_id() {
+	echo esc_attr( bp_nouveau_get_nav_link_id() );
+}
+
+	/**
+	 * Retrieve the id attribute of the link for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string The link id for the nav item.
+	 */
+	function bp_nouveau_get_nav_link_id() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+		$link_id   = '';
+
+		if ( ( 'groups' === $bp_nouveau->displayed_nav || 'personal' === $bp_nouveau->displayed_nav ) && ! empty( $nav_item->css_id ) ) {
+			$link_id = $nav_item->css_id;
+
+			if ( ! empty( $nav_item->primary ) && 'personal' === $bp_nouveau->displayed_nav ) {
+				$link_id = 'user-' . $link_id;
+			}
+		} else {
+			$link_id = $nav_item->slug;
+		}
+
+		/**
+		 * Filter to edit the link id attribute of the nav.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $link_id  The link id attribute for the nav item.
+		 * @param object $nav_item The current nav item object.
+		 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		return apply_filters( 'bp_nouveau_get_nav_link_id', $link_id, $nav_item, $bp_nouveau->displayed_nav );
+	}
+
+/**
+ * Displays the nav item link title.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_link_title() {
+	echo esc_attr( bp_nouveau_get_nav_link_title() );
+}
+
+	/**
+	 * Retrieve the title attribute of the link for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string The link title for the nav item.
+	 */
+	function bp_nouveau_get_nav_link_title() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+		$title      = '';
+
+		if ( 'directory' === $bp_nouveau->displayed_nav && ! empty( $nav_item->title ) ) {
+			$title = $nav_item->title;
+
+		} elseif (
+			( 'groups' === $bp_nouveau->displayed_nav || 'personal' === $bp_nouveau->displayed_nav )
+			&&
+			! empty( $nav_item->name )
+		) {
+			$title = $nav_item->name;
+		}
+
+		/**
+		 * Filter to edit the link title attribute of the nav.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $title    The link title attribute for the nav item.
+		 * @param object $nav_item The current nav item object.
+		 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		return apply_filters( 'bp_nouveau_get_nav_link_title', $title, $nav_item, $bp_nouveau->displayed_nav );
+	}
+
+/**
+ * Displays the nav item link html text.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_link_text() {
+	echo esc_html( bp_nouveau_get_nav_link_text() );
+}
+
+	/**
+	 * Retrieve the html text of the link for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string The html text for the nav item.
+	 */
+	function bp_nouveau_get_nav_link_text() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+		$link_text  = '';
+
+		if ( 'directory' === $bp_nouveau->displayed_nav && ! empty( $nav_item->text ) ) {
+			$link_text = _bp_strip_spans_from_title( $nav_item->text );
+
+		} elseif (
+			( 'groups' === $bp_nouveau->displayed_nav || 'personal' === $bp_nouveau->displayed_nav )
+			&&
+			! empty( $nav_item->name )
+		) {
+			$link_text = _bp_strip_spans_from_title( $nav_item->name );
+		}
+
+		/**
+		 * Filter to edit the html text of the nav.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $link_text The html text of the nav item.
+		 * @param object $nav_item  The current nav item object.
+		 * @param string $value     The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		return apply_filters( 'bp_nouveau_get_nav_link_text', $link_text, $nav_item, $bp_nouveau->displayed_nav );
+	}
+
+/**
+ * Checks if the nav item has a count attribute.
+ *
+ * @since 3.0.0
+ *
+ * @return bool
+ */
+function bp_nouveau_nav_has_count() {
+	$bp_nouveau = bp_nouveau();
+	$nav_item   = $bp_nouveau->current_nav_item;
+	$count      = false;
+
+	if ( 'directory' === $bp_nouveau->displayed_nav ) {
+		$count = $nav_item->count;
+	} elseif ( 'groups' === $bp_nouveau->displayed_nav && 'members' === $nav_item->slug ) {
+		$count = 0 !== (int) groups_get_current_group()->total_member_count;
+	} elseif ( 'personal' === $bp_nouveau->displayed_nav && ! empty( $nav_item->primary ) ) {
+		$count = (bool) strpos( $nav_item->name, '="count"' );
+	}
+
+	/**
+	 * Filter to edit whether the nav has a count attribute.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param bool   $value     True if the nav has a count attribute. False otherwise
+	 * @param object $nav_item  The current nav item object.
+	 * @param string $value     The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+	 */
+	return (bool) apply_filters( 'bp_nouveau_nav_has_count', false !== $count, $nav_item, $bp_nouveau->displayed_nav );
+}
+
+/**
+ * Displays the nav item count attribute.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_nav_count() {
+	echo esc_html( number_format_i18n( bp_nouveau_get_nav_count() ) );
+}
+
+	/**
+	 * Retrieve the count attribute for the current nav item.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return int The count attribute for the nav item.
+	 */
+	function bp_nouveau_get_nav_count() {
+		$bp_nouveau = bp_nouveau();
+		$nav_item   = $bp_nouveau->current_nav_item;
+		$count      = 0;
+
+		if ( 'directory' === $bp_nouveau->displayed_nav ) {
+			$count = (int) $nav_item->count;
+
+		} elseif ( 'groups' === $bp_nouveau->displayed_nav && 'members' === $nav_item->slug ) {
+			$count = groups_get_current_group()->total_member_count;
+
+		// @todo imho BuddyPress shouldn't add html tags inside Nav attributes...
+		} elseif ( 'personal' === $bp_nouveau->displayed_nav && ! empty( $nav_item->primary ) ) {
+			$span = strpos( $nav_item->name, '<span' );
+
+			// Grab count out of the <span> element.
+			if ( false !== $span ) {
+				$count_start = strpos( $nav_item->name, '>', $span ) + 1;
+				$count_end   = strpos( $nav_item->name, '<', $count_start );
+				$count       = (int) substr( $nav_item->name, $count_start, $count_end - $count_start );
+			}
+		}
+
+		/**
+		 * Filter to edit the count attribute for the nav item.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param int $count    The count attribute for the nav item.
+		 * @param object $nav_item The current nav item object.
+		 * @param string $value    The current nav in use (eg: 'directory', 'groups', 'personal', etc..).
+		 */
+		return (int) apply_filters( 'bp_nouveau_get_nav_count', $count, $nav_item, $bp_nouveau->displayed_nav );
+	}
+
+/** Template tags specific to the Directory navs ******************************/
+
+/**
+ * Displays the directory nav class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_directory_type_navs_class() {
+	echo esc_attr( bp_nouveau_get_directory_type_navs_class() );
+}
+
+	/**
+	 * Provides default nav wrapper classes.
+	 *
+	 * Gets the directory component nav class.
+	 * Gets user selection Customizer options.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string
+	 */
+	function bp_nouveau_get_directory_type_navs_class() {
+		$component  = sanitize_key( bp_current_component() );
+
+		// If component is 'blogs' we need to access options as 'Sites'.
+		if ('blogs' === $component) {
+			$component = 'sites';
+		};
+
+		$customizer_option = sprintf( '%s_dir_tabs', $component );
+		$nav_style  = bp_nouveau_get_temporary_setting( $customizer_option, bp_nouveau_get_appearance_settings( $customizer_option ) );
+		$tab_style = '';
+
+		if ( 1 === $nav_style ) {
+			$tab_style = $component . '-nav-tabs';
+		}
+
+		$nav_wrapper_classes = array(
+			sprintf( '%s-type-navs', $component ),
+			'main-navs',
+			'bp-navs',
+			'dir-navs',
+			$tab_style
+		);
+
+		/**
+		 * Filter to edit/add classes.
+		 *
+		 * NB: you can also directly add classes to the class attr.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $nav_wrapper_classes The list of classes.
+		 */
+		$nav_wrapper_classes = (array) apply_filters( 'bp_nouveau_get_directory_type_navs_class', $nav_wrapper_classes );
+
+		return join( ' ', array_map( 'sanitize_html_class', $nav_wrapper_classes ) );
+	}
+
+/**
+ * Displays the directory nav item list class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_directory_list_class() {
+	echo esc_attr( bp_nouveau_get_directory_list_class() );
+}
+
+	/**
+	 * Gets the directory nav item list class.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string
+	 */
+	function bp_nouveau_get_directory_list_class() {
+		return sanitize_html_class( sprintf( '%s-nav', bp_current_component() ) );
+	}
+
+/**
+ * Displays the directory nav item object (data-bp attribute).
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_directory_nav_object() {
+	$obj = bp_nouveau_get_directory_nav_object();
+
+	if ( ! is_null( $obj ) ) {
+		echo esc_attr( $obj );
+	}
+}
+
+	/**
+	 * Gets the directory nav item object.
+	 *
+	 * @see BP_Component::setup_nav().
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return array
+	 */
+	function bp_nouveau_get_directory_nav_object() {
+		$nav_item = bp_nouveau()->current_nav_item;
+
+		if ( ! $nav_item->component ) {
+			return null;
+		}
+
+		return $nav_item->component;
+	}
+
+
+// Template tags for the single item navs.
+
+/**
+ * Output main BuddyPress container classes.
+ *
+ * @since 3.0.0
+ *
+ * @return string CSS classes
+ */
+function bp_nouveau_container_classes() {
+	echo esc_attr( bp_nouveau_get_container_classes() );
+}
+
+	/**
+	 * Returns the main BuddyPress container classes.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string CSS classes
+	 */
+	function bp_nouveau_get_container_classes() {
+		$classes           = array( 'buddypress-wrap' );
+		$component         = bp_current_component();
+		$bp_nouveau        = bp_nouveau();
+		$member_type_class = '';
+
+		if ( bp_is_user() ) {
+			$customizer_option = 'user_nav_display';
+			$component         = 'members';
+			$user_type         = bp_get_member_type( bp_displayed_user_id() );
+			$member_type_class = ( $user_type )? $user_type : '';
+
+		} elseif ( bp_is_group() ) {
+			$customizer_option = 'group_nav_display';
+
+		} elseif ( bp_is_directory() ) {
+			switch ( $component ) {
+				case 'activity':
+					$customizer_option = 'activity_dir_layout';
+					break;
+
+				case 'members':
+					$customizer_option = 'members_dir_layout';
+					break;
+
+				case 'groups':
+					$customizer_option = 'groups_dir_layout';
+					break;
+
+				case 'blogs':
+					$customizer_option = 'sites_dir_layout';
+					break;
+
+				default:
+					$customizer_option = '';
+					break;
+			}
+
+		} else {
+			/**
+			 * Filters the BuddyPress Nouveau single item setting ID.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param string $value Setting ID.
+			 */
+			$customizer_option = apply_filters( 'bp_nouveau_single_item_display_settings_id', '' );
+		}
+
+		if ( $member_type_class ) {
+			$classes[] = $member_type_class;
+		}
+
+		// Provide a class token to acknowledge additional extended profile fields added to default account reg screen
+		if ( 'register' === bp_current_component() && bp_is_active( 'xprofile' ) && bp_nouveau_base_account_has_xprofile()) {
+			$classes[] = 'extended-default-reg';
+		}
+
+		// Add classes according to site owners preferences. These are options set via Customizer.
+
+		// These are general site wide Cust options falling outside component checks
+		$general_settings = bp_nouveau_get_temporary_setting( 'avatar_style', bp_nouveau_get_appearance_settings( 'avatar_style' ) );
+		if ( $general_settings ) {
+			$classes[] = 'round-avatars';
+		}
+
+		// Set via earlier switch for component check to provide correct option key.
+		if ( $customizer_option ) {
+			$layout_prefs  = bp_nouveau_get_temporary_setting( $customizer_option, bp_nouveau_get_appearance_settings( $customizer_option ) );
+
+			if ( $layout_prefs && (int) $layout_prefs === 1 && ( bp_is_user() || bp_is_group() ) ) {
+				$classes[] = 'bp-single-vert-nav';
+				$classes[] = 'bp-vertical-navs';
+			}
+
+			if ( $layout_prefs && bp_is_directory() ) {
+				$classes[] = 'bp-dir-vert-nav';
+				$classes[] = 'bp-vertical-navs';
+				$bp_nouveau->{$component}->directory_vertical_layout = $layout_prefs;
+			} else {
+				$classes[] = 'bp-dir-hori-nav';
+			}
+		}
+
+		$class = array_map( 'sanitize_html_class', $classes );
+
+		/**
+		 * Filters the final results for BuddyPress Nouveau container classes.
+		 *
+		 * This filter will return a single string of concatenated classes to be used.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $value   Concatenated classes.
+		 * @param array  $classes Array of classes that were concatenated.
+		 */
+		return apply_filters( 'bp_nouveau_get_container_classes', join( ' ', $class ), $classes );
+	}
+
+/**
+ * Output single item nav container classes
+ *
+ * @since 3.0.0
+ *
+ * @return string CSS classes
+ */
+function bp_nouveau_single_item_nav_classes() {
+	echo esc_attr( bp_nouveau_get_single_item_nav_classes() );
+}
+
+	/**
+	 * Returns the single item nav container classes
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string CSS classes
+	 */
+	function bp_nouveau_get_single_item_nav_classes() {
+		$classes    = array( 'main-navs', 'no-ajax', 'bp-navs', 'single-screen-navs' );
+		$component  = bp_current_component();
+		$bp_nouveau = bp_nouveau();
+
+		// @todo wasn't able to get $customizer_option to pass a string to get_settings
+		// this is a temp workaround but differs from earlier dir approach- bad!
+		if ( bp_is_group() ) {
+			$nav_tabs = (int) bp_nouveau_get_temporary_setting( 'group_nav_tabs', bp_nouveau_get_appearance_settings( 'group_nav_tabs' ) );
+
+		} elseif ( bp_is_user() ) {
+			$nav_tabs = (int) bp_nouveau_get_temporary_setting( 'user_nav_tabs', bp_nouveau_get_appearance_settings( 'user_nav_tabs' ) );
+		}
+
+		if ( bp_is_group() && 1 === $nav_tabs) {
+			$classes[] = 'group-nav-tabs';
+			$classes[] = 'tabbed-links';
+		} elseif ( bp_is_user() && 1 === $nav_tabs ) {
+			$classes[] = 'user-nav-tabs';
+			$classes[] = 'tabbed-links';
+		}
+
+		if ( bp_is_user() ) {
+			$component = 'members';
+			$menu_type = 'users-nav';
+		} else {
+			$menu_type = 'groups-nav';
+		}
+
+		$customizer_option = ( bp_is_user() )? 'user_nav_display' : 'group_nav_display';
+
+		$layout_prefs = (int) bp_nouveau_get_temporary_setting( $customizer_option, bp_nouveau_get_appearance_settings( $customizer_option ) );
+
+		// Set the global for a later use - this is moved from the `bp_nouveau_get_container_classes()
+		// But was set as a check for this array class addition.
+		$bp_nouveau->{$component}->single_primary_nav_layout = $layout_prefs;
+
+		if ( 1 === $layout_prefs ) {
+			$classes[] = 'vertical';
+		} else {
+			$classes[] = 'horizontal';
+		}
+
+		$classes[] = $menu_type;
+		$class = array_map( 'sanitize_html_class', $classes );
+
+		/**
+		 * Filters the final results for BuddyPress Nouveau single item nav classes.
+		 *
+		 * This filter will return a single string of concatenated classes to be used.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $value   Concatenated classes.
+		 * @param array  $classes Array of classes that were concatenated.
+		 */
+		return apply_filters( 'bp_nouveau_get_single_item_nav_classes', join( ' ', $class ), $classes );
+	}
+
+/**
+ * Output single item subnav container classes.
+ *
+ * @since 3.0.0
+ *
+ * @return string CSS classes
+ */
+function bp_nouveau_single_item_subnav_classes() {
+	echo esc_attr( bp_nouveau_get_single_item_subnav_classes() );
+}
+
+	/**
+	 * Returns the single item subnav container classes.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string CSS classes
+	 */
+	function bp_nouveau_get_single_item_subnav_classes() {
+		$classes = array( 'bp-navs', 'bp-subnavs', 'no-ajax' );
+
+		// Set user or group class string
+		if ( bp_is_user() ) {
+			$classes[] = 'user-subnav';
+		}
+
+		if ( bp_is_group() ) {
+			$classes[] = 'group-subnav';
+		}
+
+		if ( ( bp_is_group() && 'send-invites' === bp_current_action() ) || ( bp_is_group_create() && 'group-invites' === bp_get_groups_current_create_step() ) ) {
+			$classes[] = 'bp-invites-nav';
+		}
+
+		$customizer_option = ( bp_is_user() )? 'user_subnav_tabs' : 'group_subnav_tabs';
+		$nav_tabs = (int) bp_nouveau_get_temporary_setting( $customizer_option, bp_nouveau_get_appearance_settings( $customizer_option ) );
+
+		if ( bp_is_user() && 1 === $nav_tabs ) {
+			$classes[] = 'tabbed-links';
+		}
+
+		if ( bp_is_group() && 1 === $nav_tabs ) {
+			$classes[] = 'tabbed-links';
+		}
+
+		$class = array_map( 'sanitize_html_class', $classes );
+
+		/**
+		 * Filters the final results for BuddyPress Nouveau single item subnav classes.
+		 *
+		 * This filter will return a single string of concatenated classes to be used.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $value   Concatenated classes.
+		 * @param array  $classes Array of classes that were concatenated.
+		 */
+		return apply_filters( 'bp_nouveau_get_single_item_subnav_classes', join( ' ', $class ), $classes );
+	}
+
+/**
+ * Output the groups create steps classes.
+ *
+ * @since 3.0.0
+ *
+ * @return string CSS classes
+ */
+function bp_nouveau_groups_create_steps_classes() {
+	echo esc_attr( bp_nouveau_get_group_create_steps_classes() );
+}
+
+	/**
+	 * Returns the groups create steps customizer option choice class.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string CSS classes
+	 */
+	function bp_nouveau_get_group_create_steps_classes() {
+		$classes  = array( 'bp-navs', 'group-create-links', 'no-ajax' );
+		$nav_tabs = (int) bp_nouveau_get_temporary_setting( 'groups_create_tabs', bp_nouveau_get_appearance_settings( 'groups_create_tabs' ) );
+
+		if ( 1 === $nav_tabs ) {
+			$classes[] = 'tabbed-links';
+		}
+
+		$class = array_map( 'sanitize_html_class', $classes );
+
+		/**
+		 * Filters the final results for BuddyPress Nouveau group creation step classes.
+		 *
+		 * This filter will return a single string of concatenated classes to be used.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $value   Concatenated classes.
+		 * @param array  $classes Array of classes that were concatenated.
+		 */
+		return apply_filters( 'bp_nouveau_get_group_create_steps_classes', join( ' ', $class ), $classes );
+	}
+
+
+/** Template tags for the object search **************************************/
+
+/**
+ * Get the search primary object
+ *
+ * @since 3.0.0
+ *
+ * @param string $object Optional. The primary object.
+ *
+ * @return string The primary object.
+ */
+function bp_nouveau_get_search_primary_object( $object = '' ) {
+	if ( bp_is_user() ) {
+		$object = 'member';
+	} elseif ( bp_is_group() ) {
+		$object = 'group';
+	} elseif ( bp_is_directory() ) {
+		$object = 'dir';
+	} else {
+
+		/**
+		 * Filters the search primary object if no other was found.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $object Search object.
+		 */
+		$object = apply_filters( 'bp_nouveau_get_search_primary_object', $object );
+	}
+
+	return $object;
+}
+
+/**
+ * Get The list of search objects (primary + secondary).
+ *
+ * @since 3.0.0
+ *
+ * @param array $objects Optional. The list of objects.
+ *
+ * @return array The list of objects.
+ */
+function bp_nouveau_get_search_objects( $objects = array() ) {
+	$primary = bp_nouveau_get_search_primary_object();
+	if ( ! $primary ) {
+		return $objects;
+	}
+
+	$objects = array(
+		'primary' => $primary,
+	);
+
+	if ( 'member' === $primary || 'dir' === $primary ) {
+		$objects['secondary'] = bp_current_component();
+	} elseif ( 'group' === $primary ) {
+		$objects['secondary'] = bp_current_action();
+	} else {
+
+		/**
+		 * Filters the search objects if no others were found.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param array $objects Search objects.
+		 */
+		$objects = apply_filters( 'bp_nouveau_get_search_objects', $objects );
+	}
+
+	return $objects;
+}
+
+/**
+ * Output the search form container classes.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_search_container_class() {
+	$objects = bp_nouveau_get_search_objects();
+	$class   = join( '-search ', array_map( 'sanitize_html_class', $objects ) ) . '-search';
+
+	echo esc_attr( $class );
+}
+
+/**
+ * Output the search form data-bp attribute.
+ *
+ * @since 3.0.0
+ *
+ * @param  string $attr The data-bp attribute.
+ * @return string The data-bp attribute.
+ */
+function bp_nouveau_search_object_data_attr( $attr = '' ) {
+	$objects = bp_nouveau_get_search_objects();
+
+	if ( ! isset( $objects['secondary'] ) ) {
+		return $attr;
+	}
+
+	if ( bp_is_active( 'groups' ) && bp_is_group_members() ) {
+		$attr = join( '_', $objects );
+	} else {
+		$attr = $objects['secondary'];
+	}
+
+	echo esc_attr( $attr );
+}
+
+/**
+ * Output a selector ID.
+ *
+ * @since 3.0.0
+ *
+ * @param string $suffix Optional. A string to append at the end of the ID.
+ * @param string $sep    Optional. The separator to use between each token.
+ *
+ * @return string The selector ID.
+ */
+function bp_nouveau_search_selector_id( $suffix = '', $sep = '-' ) {
+	$id = join( $sep, array_merge( bp_nouveau_get_search_objects(), (array) $suffix ) );
+	echo esc_attr( $id );
+}
+
+/**
+ * Output the name attribute of a selector.
+ *
+ * @since 3.0.0
+ *
+ * @param  string $suffix Optional. A string to append at the end of the name.
+ * @param  string $sep    Optional. The separator to use between each token.
+ *
+ * @return string The name attribute of a selector.
+ */
+function bp_nouveau_search_selector_name( $suffix = '', $sep = '_' ) {
+	$objects = bp_nouveau_get_search_objects();
+
+	if ( isset( $objects['secondary'] ) && ! $suffix ) {
+		$name = bp_core_get_component_search_query_arg( $objects['secondary'] );
+	} else {
+		$name = join( $sep, array_merge( $objects, (array) $suffix ) );
+	}
+
+	echo esc_attr( $name );
+}
+
+/**
+ * Output the default search text for the search object
+ *
+ * @since 3.0.0
+ *
+ * @param  string $text    Optional. The default search text for the search object.
+ * @param  string $is_attr Optional. True if it's to be output inside an attribute. False Otherwise.
+ *
+ * @return string The default search text.
+ *
+ * @todo 28/09/17 added  'empty( $text )' check to $object query as it wasn't returning output as expected & not returning user set params
+ * This may require further examination - hnla
+ */
+function bp_nouveau_search_default_text( $text = '', $is_attr = true ) {
+	$objects = bp_nouveau_get_search_objects();
+
+	if ( ! empty( $objects['secondary'] ) && empty( $text ) ) {
+		$text = bp_get_search_default_text( $objects['secondary'] );
+	}
+
+	if ( $is_attr ) {
+		echo esc_attr( $text );
+	} else {
+		echo esc_html( $text );
+	}
+}
+
+/**
+ * Get the search form template part and fire some do_actions if needed.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_search_form() {
+	$search_form_html = bp_buffer_template_part( 'common/search/search-form', null, false );
+
+	$objects = bp_nouveau_get_search_objects();
+	if ( empty( $objects['primary'] ) || empty( $objects['secondary'] ) ) {
+		echo $search_form_html;
+		return;
+	}
+
+	if ( 'dir' === $objects['primary'] ) {
+		/**
+		 * Filter here to edit the HTML output of the directory search form.
+		 *
+		 * NB: This will take in charge the following BP Core Components filters
+		 *     - bp_directory_members_search_form
+		 *     - bp_directory_blogs_search_form
+		 *     - bp_directory_groups_search_form
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param string $search_form_html The HTML output for the directory search form.
+		 */
+		echo apply_filters( "bp_directory_{$objects['secondary']}_search_form", $search_form_html );
+
+		if ( 'activity' === $objects['secondary'] ) {
+			/**
+			 * Fires before the display of the activity syndication options.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_activity_syndication_options' );
+
+		} elseif ( 'blogs' === $objects['secondary'] ) {
+			/**
+			 * Fires inside the unordered list displaying blog sub-types.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_blogs_directory_blog_sub_types' );
+
+		} elseif ( 'groups' === $objects['secondary'] ) {
+			/**
+			 * Fires inside the groups directory group types.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_groups_directory_group_types' );
+
+		} elseif ( 'members' === $objects['secondary'] ) {
+			/**
+			 * Fires inside the members directory member sub-types.
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_members_directory_member_sub_types' );
+		}
+	} elseif ( 'group' === $objects['primary'] ) {
+		if ( 'members' !== $objects['secondary'] ) {
+			/**
+			 * Filter here to edit the HTML output of the displayed group search form.
+			 *
+			 * @since 3.2.0
+			 *
+			 * @param string $search_form_html The HTML output for the directory search form.
+			 */
+			echo apply_filters( "bp_group_{$objects['secondary']}_search_form", $search_form_html );
+
+		} else {
+			/**
+			 * Filters the Members component search form.
+			 *
+			 * @since 1.9.0
+			 *
+			 * @param string $search_form_html HTML markup for the member search form.
+			 */
+			echo apply_filters( 'bp_directory_members_search_form', $search_form_html );
+		}
+
+		if ( 'members' === $objects['secondary'] ) {
+			/**
+			 * Fires at the end of the group members search unordered list.
+			 *
+			 * Part of bp_groups_members_template_part().
+			 *
+			 * @since 1.5.0
+			 */
+			do_action( 'bp_members_directory_member_sub_types' );
+
+		} elseif ( 'activity' === $objects['secondary'] ) {
+			/**
+			 * Fires inside the syndication options list, after the RSS option.
+			 *
+			 * @since 1.2.0
+			 */
+			do_action( 'bp_group_activity_syndication_options' );
+		}
+	}
+}
+
+
+// Template tags for the directory & user/group screen filters.
+
+/**
+ * Get the current component or action.
+ *
+ * If on single group screens we need to switch from component to bp_current_action() to add the correct
+ * IDs/labels for group/activity & similar screens.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_current_object() {
+	/*
+	 * If we're looking at groups single screens we need to factor in current action
+	 * to avoid the component check adding the wrong id for the main dir e.g 'groups' instead of 'activity'.
+	 * We also need to check for group screens to adjust the id's for prefixes.
+	 */
+	$component = array();
+
+	if ( bp_is_group() ) {
+		$component['members_select']   = 'groups_members-order-select';
+		$component['members_order_by'] = 'groups_members-order-by';
+		$component['object']           = bp_current_action();
+		$component['data_filter']      = bp_current_action();
+
+		if ( 'activity' !== bp_current_action() ) {
+			$component['data_filter'] = 'group_' . bp_current_action();
+		}
+
+	} else {
+		$component['members_select']   = 'members-order-select';
+		$component['members_order_by'] = 'members-order-by';
+		$component['object']           = bp_current_component();
+		$component['data_filter']      = bp_current_component();
+	}
+
+	return $component;
+}
+
+/**
+ * Output data filter container's ID attribute value.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_filter_container_id() {
+	echo esc_attr( bp_nouveau_get_filter_container_id() );
+}
+
+	/**
+	 * Get data filter container's ID attribute value.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string
+	 */
+	function bp_nouveau_get_filter_container_id() {
+		$component = bp_nouveau_current_object();
+
+		$ids = array(
+			'members'       =>  $component['members_select'],
+			'friends'       => 'members-friends-select',
+			'notifications' => 'notifications-filter-select',
+			'activity'      => 'activity-filter-select',
+			'groups'        => 'groups-order-select',
+			'blogs'         => 'blogs-order-select',
+		);
+
+		if ( isset( $ids[ $component['object'] ] ) ) {
+
+			/**
+			 * Filters the container ID for BuddyPress Nouveau filters.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param string $value ID based on current component object.
+			 */
+			return apply_filters( 'bp_nouveau_get_filter_container_id', $ids[ $component['object'] ] );
+		}
+
+		return '';
+	}
+
+/**
+ * Output data filter's ID attribute value.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_filter_id() {
+	echo esc_attr( bp_nouveau_get_filter_id() );
+}
+
+	/**
+	 * Get data filter's ID attribute value.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string
+	 */
+	function bp_nouveau_get_filter_id() {
+		$component = bp_nouveau_current_object();
+
+		$ids = array(
+			'members'       => $component['members_order_by'],
+			'friends'       => 'members-friends',
+			'notifications' => 'notifications-filter-by',
+			'activity'      => 'activity-filter-by',
+			'groups'        => 'groups-order-by',
+			'blogs'         => 'blogs-order-by',
+		);
+
+		if ( isset( $ids[ $component['object'] ] ) ) {
+
+			/**
+			 * Filters the filter ID for BuddyPress Nouveau filters.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param string $value ID based on current component object.
+			 */
+			return apply_filters( 'bp_nouveau_get_filter_id', $ids[ $component['object'] ] );
+		}
+
+		return '';
+	}
+
+/**
+ * Output data filter's label.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_filter_label() {
+	echo esc_html( bp_nouveau_get_filter_label() );
+}
+
+	/**
+	 * Get data filter's label.
+ 	 *
+	 * @since 3.0.0
+	 *
+	 * @param string
+	 */
+	function bp_nouveau_get_filter_label() {
+		$component = bp_nouveau_current_object();
+		$label     = __( 'Order By:', 'buddypress' );
+
+		if ( 'activity' === $component['object'] || 'friends' === $component['object'] ) {
+			$label = __( 'Show:', 'buddypress' );
+		}
+
+		/**
+		 * Filters the label for BuddyPress Nouveau filters.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param string $label     Label for BuddyPress Nouveau filter.
+		 * @param array  $component The data filter's data-bp-filter attribute value.
+		 */
+		return apply_filters( 'bp_nouveau_get_filter_label', $label, $component );
+	}
+
+/**
+ * Output data filter's data-bp-filter attribute value.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_filter_component() {
+	$component = bp_nouveau_current_object();
+	echo esc_attr( $component['data_filter'] );
+}
+
+/**
+ * Output the <option> of the data filter's <select> element.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_filter_options() {
+	echo bp_nouveau_get_filter_options();  // Escaped in inner functions.
+}
+
+	/**
+	 * Get the <option> of the data filter's <select> element.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @return string
+	 */
+	function bp_nouveau_get_filter_options() {
+		$output = '';
+
+		if ( 'notifications' === bp_current_component() ) {
+			$output = bp_nouveau_get_notifications_filters();
+
+		} else {
+			$filters = bp_nouveau_get_component_filters();
+
+			foreach ( $filters as $key => $value ) {
+				$output .= sprintf( '<option value="%1$s">%2$s</option>%3$s',
+					esc_attr( $key ),
+					esc_html( $value ),
+					PHP_EOL
+				);
+			}
+		}
+
+		return $output;
+	}
+
+
+/** Template tags for the Customizer ******************************************/
+
+/**
+ * Get a link to reach a specific section into the customizer
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Optional. The argument to customize the Customizer link.
+ *
+ * @return string HTML.
+ */
+function bp_nouveau_get_customizer_link( $args = array() ) {
+	$r = bp_parse_args( $args, array(
+		'capability' => 'bp_moderate',
+		'object'     => 'user',
+		'item_id'    => 0,
+		'autofocus'  => '',
+		'text'       => '',
+	), 'nouveau_get_customizer_link' );
+
+	if ( empty( $r['capability'] ) || empty( $r['autofocus'] ) || empty( $r['text'] ) ) {
+		return '';
+	}
+
+	if ( ! bp_current_user_can( $r['capability'] ) ) {
+		return '';
+	}
+
+	$url = '';
+
+	if ( bp_is_user() ) {
+		$url = rawurlencode( bp_displayed_user_domain() );
+
+	} elseif ( bp_is_group() ) {
+		$url = rawurlencode( bp_get_group_permalink( groups_get_current_group() ) );
+
+	} elseif ( ! empty( $r['object'] ) && ! empty( $r['item_id'] ) ) {
+		if ( 'user' === $r['object'] ) {
+			$url = rawurlencode( bp_core_get_user_domain( $r['item_id'] ) );
+
+		} elseif ( 'group' === $r['object'] ) {
+			$group = groups_get_group( array( 'group_id' => $r['item_id'] ) );
+
+			if ( ! empty( $group->id ) ) {
+				$url = rawurlencode( bp_get_group_permalink( $group ) );
+			}
+		}
+	}
+
+	if ( ! $url ) {
+		return '';
+	}
+
+	$customizer_link = add_query_arg( array(
+		'autofocus[section]' => $r['autofocus'],
+		'url'                => $url,
+	), admin_url( 'customize.php' ) );
+
+	return sprintf( '<a href="%1$s">%2$s</a>', esc_url( $customizer_link ), esc_html( $r['text'] ) );
+}
+
+/** Template tags for signup forms *******************************************/
+
+/**
+ * Fire specific hooks into the register template
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   'before' or 'after'
+ * @param string $prefix Use it to add terms before the hook name
+ */
+function bp_nouveau_signup_hook( $when = '', $prefix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	if ( $prefix ) {
+		if ( 'page' === $prefix ) {
+			$hook[] = 'register';
+		} elseif ( 'steps' === $prefix ) {
+			$hook[] = 'signup';
+		}
+
+		$hook[] = $prefix;
+	}
+
+	if ( 'page' !== $prefix && 'steps' !== $prefix ) {
+		$hook[] = 'fields';
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Fire specific hooks into the activate template
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   'before' or 'after'
+ * @param string $prefix Use it to add terms before the hook name
+ */
+function bp_nouveau_activation_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	$hook[] = 'activate';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	if ( 'page' === $suffix ) {
+		$hook[2] = 'activation';
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Output the signup form for the requested section
+ *
+ * @since 3.0.0
+ *
+ * @param string $section Optional. The section of fields to get 'account_details' or 'blog_details'.
+ *                        Default: 'account_details'.
+ */
+function bp_nouveau_signup_form( $section = 'account_details' ) {
+	$fields = bp_nouveau_get_signup_fields( $section );
+	if ( ! $fields ) {
+		return;
+	}
+
+	foreach ( $fields as $name => $attributes ) {
+		list( $label, $required, $value, $attribute_type, $type, $class ) = array_values( $attributes );
+
+		// Text fields are using strings, radios are using their inputs
+		$label_output = '<label for="%1$s">%2$s</label>';
+		$id           = $name;
+		$classes      = '';
+
+		if ( $required ) {
+			/* translators: Do not translate placeholders. 2 = form field name, 3 = "(required)". */
+			$label_output = __( '<label for="%1$s">%2$s %3$s</label>', 'buddypress' );
+		}
+
+		// Output the label for regular fields
+		if ( 'radio' !== $type ) {
+			if ( $required ) {
+				printf( $label_output, esc_attr( $name ), esc_html( $label ), __( '(required)', 'buddypress' ) );
+			} else {
+				printf( $label_output, esc_attr( $name ), esc_html( $label ) );
+			}
+
+			if ( ! empty( $value ) && is_callable( $value ) ) {
+				$value = call_user_func( $value );
+			}
+
+		// Handle the specific case of Site's privacy differently
+		} elseif ( 'signup_blog_privacy_private' !== $name ) {
+			?>
+				<span class="label">
+					<?php esc_html_e( 'I would like my site to appear in search engines, and in public listings around this network.', 'buddypress' ); ?>
+				</span>
+			<?php
+		}
+
+		// Set the additional attributes
+		if ( $attribute_type ) {
+			$existing_attributes = array();
+
+			if ( ! empty( $required ) ) {
+				$existing_attributes = array( 'aria-required' => 'true' );
+
+				/**
+				 * The blog section is hidden, so let's avoid a browser warning
+				 * and deal with the Blog section in Javascript.
+				 */
+				if ( $section !== 'blog_details' ) {
+					$existing_attributes['required'] = 'required';
+				}
+			}
+
+			$attribute_type = ' ' . bp_get_form_field_attributes( $attribute_type, $existing_attributes );
+		}
+
+		// Specific case for Site's privacy
+		if ( 'signup_blog_privacy_public' === $name || 'signup_blog_privacy_private' === $name ) {
+			$name      = 'signup_blog_privacy';
+			$submitted = bp_get_signup_blog_privacy_value();
+
+			if ( ! $submitted ) {
+				$submitted = 'public';
+			}
+
+			$attribute_type = ' ' . checked( $value, $submitted, false );
+		}
+
+		// Do not run function to display errors for the private radio.
+		if ( 'private' !== $value ) {
+
+			/**
+			 * Fetch & display any BP member registration field errors.
+			 *
+			 * Passes BP signup errors to Nouveau's template function to
+			 * render suitable markup for error string.
+			 */
+			if ( isset( buddypress()->signup->errors[ $name ] ) ) {
+				nouveau_error_template( buddypress()->signup->errors[ $name ] );
+				$invalid = 'invalid';
+			}
+		}
+
+		if ( isset( $invalid ) && isset( buddypress()->signup->errors[ $name ] ) ) {
+			if ( ! empty( $class ) ) {
+				$class = $class . ' ' . $invalid;
+			} else {
+				$class = $invalid;
+			}
+		}
+
+		if ( $class ) {
+			$class = sprintf(
+				' class="%s"',
+				esc_attr( join( ' ', array_map( 'sanitize_html_class', explode( ' ', $class ) ) ) )
+			);
+		}
+
+		// Set the input.
+		$field_output = sprintf(
+			'<input type="%1$s" name="%2$s" id="%3$s" %4$s value="%5$s" %6$s />',
+			esc_attr( $type ),
+			esc_attr( $name ),
+			esc_attr( $id ),
+			$class,  // Constructed safely above.
+			esc_attr( $value ),
+			$attribute_type // Constructed safely above.
+		);
+
+		// Not a radio, let's output the field
+		if ( 'radio' !== $type ) {
+			if ( 'signup_blog_url' !== $name ) {
+				print( $field_output );  // Constructed safely above.
+
+			// If it's the signup blog url, it's specific to Multisite config.
+			} elseif ( is_subdomain_install() ) {
+				// Constructed safely above.
+				printf(
+					'%1$s %2$s . %3$s',
+					is_ssl() ? 'https://' : 'http://',
+					$field_output,
+					bp_signup_get_subdomain_base()
+				);
+
+			// Subfolders!
+			} else {
+				printf(
+					'%1$s %2$s',
+					home_url( '/' ),
+					$field_output  // Constructed safely above.
+				);
+			}
+
+		// It's a radio, let's output the field inside the label
+		} else {
+			// $label_output and $field_output are constructed safely above.
+			printf( $label_output, esc_attr( $name ), $field_output . ' ' . esc_html( $label ) );
+		}
+
+		// Password strength is restricted to the signup_password field
+		if ( 'signup_password' === $name ) :
+		?>
+			<div id="pass-strength-result"></div>
+		<?php
+		endif;
+	}
+
+	/**
+	 * Fires and displays any extra member registration details fields.
+	 *
+	 * This is a variable hook that depends on the current section.
+	 *
+	 * @since 1.9.0
+	 */
+	do_action( "bp_{$section}_fields" );
+}
+
+/**
+ * Outputs the Privacy Policy acceptance area on the registration page.
+ *
+ * @since 4.0.0
+ */
+function bp_nouveau_signup_privacy_policy_acceptance_section() {
+	$error = null;
+	if ( isset( buddypress()->signup->errors['signup_privacy_policy'] ) ) {
+		$error = buddypress()->signup->errors['signup_privacy_policy'];
+	}
+
+	?>
+
+	<div class="privacy-policy-accept">
+		<?php if ( $error ) : ?>
+			<?php nouveau_error_template( $error ); ?>
+		<?php endif; ?>
+
+		<label for="signup-privacy-policy-accept">
+			<input type="hidden" name="signup-privacy-policy-check" value="1" />
+
+			<?php /* translators: link to Privacy Policy */ ?>
+			<input type="checkbox" name="signup-privacy-policy-accept" id="signup-privacy-policy-accept" required /> <?php printf( esc_html__( 'I have read and agree to this site\'s %s.', 'buddypress' ), sprintf( '<a href="%s">%s</a>', esc_url( get_privacy_policy_url() ), esc_html__( 'Privacy Policy', 'buddypress' ) ) ); ?>
+		</label>
+	</div>
+
+	<?php
+}
+
+/**
+ * Output a submit button and the nonce for the requested action.
+ *
+ * @since 3.0.0
+ *
+ * @param string $action The action to get the submit button for. Required.
+ */
+function bp_nouveau_submit_button( $action ) {
+	$submit_data = bp_nouveau_get_submit_button( $action );
+	if ( empty( $submit_data['attributes'] ) || empty( $submit_data['nonce'] ) ) {
+		return;
+	}
+
+	if ( ! empty( $submit_data['before'] ) ) {
+
+		/**
+		 * Fires before display of the submit button.
+		 *
+		 * This is a dynamic filter that is dependent on the "before" value provided by bp_nouveau_get_submit_button().
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( $submit_data['before'] );
+	}
+
+	$submit_input = sprintf( '<input type="submit" %s/>',
+		bp_get_form_field_attributes( 'submit', $submit_data['attributes'] )  // Safe.
+	);
+
+	// Output the submit button.
+	if ( isset( $submit_data['wrapper'] ) && false === $submit_data['wrapper'] ) {
+		echo $submit_input;
+
+	// Output the submit button into a wrapper.
+	} else {
+		printf( '<div class="submit">%s</div>', $submit_input );
+	}
+
+	if ( empty( $submit_data['nonce_key'] ) ) {
+		wp_nonce_field( $submit_data['nonce'] );
+	} else {
+		wp_nonce_field( $submit_data['nonce'], $submit_data['nonce_key'] );
+	}
+
+	if ( ! empty( $submit_data['after'] ) ) {
+
+		/**
+		 * Fires before display of the submit button.
+		 *
+		 * This is a dynamic filter that is dependent on the "after" value provided by bp_nouveau_get_submit_button().
+		 *
+		 * @since 3.0.0
+		 */
+		do_action( $submit_data['after'] );
+	}
+}
+
+/**
+ * Display supplemental error or feedback messages.
+ *
+ * This template handles in page error or feedback messages e.g signup fields
+ * 'Username exists' type registration field error notices.
+ *
+ * @param string $message required: the message to display.
+ * @param string $type optional: the type of error message e.g 'error'.
+ *
+ * @since 3.0.0
+ */
+function nouveau_error_template( $message = '', $type = '' ) {
+	if ( ! $message ) {
+		return;
+	}
+
+	$type = ( $type ) ? $type : 'error';
+	?>
+
+	<div class="<?php echo esc_attr( 'bp-messages bp-feedback ' . $type ); ?>">
+		<span class="bp-icon" aria-hidden="true"></span>
+		<p><?php echo esc_html( $message ); ?></p>
+	</div>
+
+	<?php
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/functions.php	(revision 12265)
@@ -0,0 +1,46 @@
+<?php
+/**
+ * xProfile functions
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register Scripts for the xProfile component
+ *
+ * @since 3.0.0
+ *
+ * @param array $scripts The array of scripts to register
+ *
+ * @return array The same array with the specific groups scripts.
+ */
+function bp_nouveau_xprofile_register_scripts( $scripts = array() ) {
+	if ( ! isset( $scripts['bp-nouveau'] ) ) {
+		return $scripts;
+	}
+
+	return array_merge( $scripts, array(
+		'bp-nouveau-xprofile' => array(
+			'file'         => 'js/buddypress-xprofile%s.js',
+			'dependencies' => array( 'bp-nouveau' ),
+			'footer'       => true,
+		),
+	) );
+}
+
+/**
+ * Enqueue the xprofile scripts
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_xprofile_enqueue_scripts() {
+	if ( ! bp_is_user_profile_edit() && ! bp_is_register_page() ) {
+		return;
+	}
+
+	wp_enqueue_script( 'bp-nouveau-xprofile' );
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/loader.php	(revision 12265)
@@ -0,0 +1,80 @@
+<?php
+/**
+ * BP Nouveau xProfile
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * xProfile Loader class
+ *
+ * @since 3.0.0
+ */
+class BP_Nouveau_xProfile {
+	/**
+	 * Constructor
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		$this->setup_globals();
+		$this->includes();
+		$this->setup_actions();
+		$this->setup_filters();
+	}
+
+	/**
+	 * Globals
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_globals() {
+		$this->dir = dirname( __FILE__ );
+	}
+
+	/**
+	 * Include needed files
+	 *
+	 * @since 3.0.0
+	 */
+	protected function includes() {
+		require( trailingslashit( $this->dir ) . 'functions.php' );
+		require( trailingslashit( $this->dir ) . 'template-tags.php' );
+	}
+
+	/**
+	 * Register do_action() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_actions() {
+		add_action( 'bp_nouveau_enqueue_scripts', 'bp_nouveau_xprofile_enqueue_scripts' );
+	}
+
+	/**
+	 * Register add_filter() hooks
+	 *
+	 * @since 3.0.0
+	 */
+	protected function setup_filters() {
+		add_filter( 'bp_nouveau_register_scripts', 'bp_nouveau_xprofile_register_scripts', 10, 1 );
+	}
+}
+
+/**
+ * Launch the xProfile loader class.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_xprofile( $bp_nouveau = null ) {
+	if ( is_null( $bp_nouveau ) ) {
+		return;
+	}
+
+	$bp_nouveau->xprofile = new BP_Nouveau_xProfile();
+}
+add_action( 'bp_nouveau_includes', 'bp_nouveau_xprofile', 10, 1 );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/template-tags.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/template-tags.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/includes/xprofile/template-tags.php	(revision 12265)
@@ -0,0 +1,73 @@
+<?php
+/**
+ * xProfile Template tags
+ *
+ * @since 3.0.0
+ * @version 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Fire specific hooks into the single members xprofile templates.
+ *
+ * @since 3.0.0
+ *
+ * @param string $when   Optional. Either 'before' or 'after'.
+ * @param string $suffix Optional. Use it to add terms at the end of the hook name.
+ */
+function bp_nouveau_xprofile_hook( $when = '', $suffix = '' ) {
+	$hook = array( 'bp' );
+
+	if ( $when ) {
+		$hook[] = $when;
+	}
+
+	// It's a xprofile hook
+	$hook[] = 'profile';
+
+	if ( $suffix ) {
+		$hook[] = $suffix;
+	}
+
+	bp_nouveau_hook( $hook );
+}
+
+/**
+ * Template tag to output the field visibility markup in edit and signup screens.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_xprofile_edit_visibilty() {
+	/**
+	 * Fires before the display of visibility options for the field.
+	 *
+	 * @since 1.7.0
+	 */
+	do_action( 'bp_custom_profile_edit_fields_pre_visibility' );
+
+	bp_get_template_part( 'members/single/parts/profile-visibility' );
+
+	/**
+	 * Fires after the visibility options for a field.
+	 *
+	 * @since 3.0.0
+	 */
+	do_action( 'bp_custom_profile_edit_fields' );
+}
+
+/**
+ * Return a bool check to see whether the base re group has had extended
+ * profile fields added to it for the registration screen.
+ *
+ * @since 3.0.0
+ */
+function bp_nouveau_base_account_has_xprofile() {
+	return (bool) bp_has_profile(
+		array(
+			'profile_group_id' => 1,
+			'fetch_field_data' => false,
+		)
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-activity-post-form.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-activity-post-form.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-activity-post-form.js	(revision 12265)
@@ -0,0 +1,766 @@
+/* global bp, BP_Nouveau, _, Backbone */
+/* @version 3.1.0 */
+window.wp = window.wp || {};
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+	bp.Nouveau = bp.Nouveau || {};
+
+	// Bail if not set
+	if ( typeof bp.Nouveau.Activity === 'undefined' || typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	_.extend( bp, _.pick( wp, 'Backbone', 'ajax', 'template' ) );
+
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+
+	/**
+	 * [Activity description]
+	 * @type {Object}
+	 */
+	bp.Nouveau.Activity.postForm = {
+		start: function() {
+			this.views           = new Backbone.Collection();
+			this.ActivityObjects = new bp.Collections.ActivityObjects();
+			this.buttons         = new Backbone.Collection();
+
+			this.postFormView();
+		},
+
+		postFormView: function() {
+			// Do not carry on if the main element is not available.
+			if ( ! $( '#bp-nouveau-activity-form' ).length ) {
+				return;
+			}
+
+			// Create the BuddyPress Uploader
+			var postForm = new bp.Views.PostForm();
+
+			// Add it to views
+			this.views.add( { id: 'post_form', view: postForm } );
+
+			// Display it
+			postForm.inject( '#bp-nouveau-activity-form' );
+		}
+	};
+
+	if ( typeof bp.View === 'undefined' ) {
+		// Extend wp.Backbone.View with .prepare() and .inject()
+		bp.View = bp.Backbone.View.extend( {
+			inject: function( selector ) {
+				this.render();
+				$(selector).html( this.el );
+				this.views.ready();
+			},
+
+			prepare: function() {
+				if ( ! _.isUndefined( this.model ) && _.isFunction( this.model.toJSON ) ) {
+					return this.model.toJSON();
+				} else {
+					return {};
+				}
+			}
+		} );
+	}
+
+	/** Models ****************************************************************/
+
+	// The Activity to post
+	bp.Models.Activity = Backbone.Model.extend( {
+		defaults: {
+			user_id:   0,
+			item_id:   0,
+			object:   '',
+			content:  ''
+		}
+	} );
+
+	// Object, the activity is attached to (group or blog or any other)
+	bp.Models.ActivityObject = Backbone.Model.extend( {
+		defaults: {
+			id          : 0,
+			name        : '',
+			avatar_url  : '',
+			object_type : 'group'
+		}
+	} );
+
+	/** Collections ***********************************************************/
+
+	// Objects, the activity can be attached to (groups or blogs or any others)
+	bp.Collections.ActivityObjects = Backbone.Collection.extend( {
+		model: bp.Models.ActivityObject,
+
+		sync: function( method, model, options ) {
+
+			if ( 'read' === method ) {
+				options = options || {};
+				options.context = this;
+				options.data = _.extend( options.data || {}, {
+					action: 'bp_nouveau_get_activity_objects'
+				} );
+
+				return bp.ajax.send( options );
+			}
+		},
+
+		parse: function( resp ) {
+			if ( ! _.isArray( resp ) ) {
+				resp = [resp];
+			}
+
+			return resp;
+		}
+
+	} );
+
+	/** Views *****************************************************************/
+
+	// Feedback messages
+	bp.Views.activityFeedback = bp.View.extend( {
+		tagName  : 'div',
+		id       : 'message',
+		template : bp.template( 'activity-post-form-feedback' ),
+
+		initialize: function() {
+			this.model = new Backbone.Model();
+
+			if ( this.options.value ) {
+				this.model.set( 'message', this.options.value, { silent: true } );
+			}
+
+			this.type  = 'info';
+
+			if ( ! _.isUndefined( this.options.type ) && 'info' !== this.options.type ) {
+				this.type = this.options.type;
+			}
+
+			this.el.className = 'bp-messages bp-feedback ' + this.type ;
+		}
+	} );
+
+	// Regular input
+	bp.Views.ActivityInput = bp.View.extend( {
+		tagName  : 'input',
+
+		attributes: {
+			type : 'text'
+		},
+
+		initialize: function() {
+			if ( ! _.isObject( this.options ) ) {
+				return;
+			}
+
+			_.each( this.options, function( value, key ) {
+				this.$el.prop( key, value );
+			}, this );
+		}
+	} );
+
+	// The content of the activity
+	bp.Views.WhatsNew = bp.View.extend( {
+		tagName   : 'textarea',
+		className : 'bp-suggestions',
+		id        : 'whats-new',
+
+		attributes: {
+			name         : 'whats-new',
+			cols         : '50',
+			rows         : '4',
+			placeholder  : BP_Nouveau.activity.strings.whatsnewPlaceholder,
+			'aria-label' : BP_Nouveau.activity.strings.whatsnewLabel
+		},
+
+		initialize: function() {
+			this.on( 'ready', this.adjustContent, this );
+
+			this.options.activity.on( 'change:content', this.resetContent, this );
+		},
+
+		adjustContent: function() {
+
+			// First adjust layout
+			this.$el.css( {
+				resize: 'none',
+				height: '50px'
+			} );
+
+			// Check for mention
+			var	mention = bp.Nouveau.getLinkParams( null, 'r' ) || null;
+
+			if ( ! _.isNull( mention ) ) {
+				this.$el.text( '@' + _.escape( mention ) + ' ' );
+				this.$el.focus();
+			}
+		},
+
+		resetContent: function( activity ) {
+			if ( _.isUndefined( activity ) ) {
+				return;
+			}
+
+			this.$el.val( activity.get( 'content' ) );
+		}
+	} );
+
+	bp.Views.WhatsNewPostIn = bp.View.extend( {
+		tagName:   'select',
+		id:        'whats-new-post-in',
+
+		attributes: {
+			name         : 'whats-new-post-in',
+			'aria-label' : BP_Nouveau.activity.strings.whatsnewpostinLabel
+		},
+
+		events: {
+			change: 'change'
+		},
+
+		keys: [],
+
+		initialize: function() {
+			this.model = new Backbone.Model();
+
+			this.filters = this.options.filters || {};
+
+			// Build `<option>` elements.
+			this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
+				return {
+					el: $( '<option></option>' ).val( value ).html( filter.text )[0],
+					priority: filter.priority || 50
+				};
+			}, this ).sortBy( 'priority' ).pluck( 'el' ).value() );
+		},
+
+		change: function() {
+			var filter = this.filters[ this.el.value ];
+			if ( filter ) {
+				this.model.set( { 'selected': this.el.value, 'placeholder': filter.autocomplete_placeholder } );
+			}
+		}
+	} );
+
+	bp.Views.Item = bp.View.extend( {
+		tagName:   'li',
+		className: 'bp-activity-object',
+		template:  bp.template( 'activity-target-item' ),
+
+		attributes: {
+			role: 'checkbox'
+		},
+
+		initialize: function() {
+			if ( this.model.get( 'selected' ) ) {
+				this.el.className += ' selected';
+			}
+		},
+
+		events: {
+			click : 'setObject'
+		},
+
+		setObject:function( event ) {
+			event.preventDefault();
+
+			if ( true === this.model.get( 'selected' ) ) {
+				this.model.clear();
+			} else {
+				this.model.set( 'selected', true );
+			}
+		}
+	} );
+
+	bp.Views.AutoComplete = bp.View.extend( {
+		tagName : 'ul',
+		id      : 'whats-new-post-in-box-items',
+
+		events: {
+			keyup :  'autoComplete'
+		},
+
+		initialize: function() {
+			var autocomplete = new bp.Views.ActivityInput( {
+				type        : 'text',
+				id          : 'activity-autocomplete',
+				placeholder : this.options.placeholder || ''
+			} ).render();
+
+			this.$el.prepend( $( '<li></li>' ).html( autocomplete.$el ) );
+
+			this.on( 'ready', this.setFocus, this );
+			this.collection.on( 'add', this.addItemView, this );
+			this.collection.on( 'reset', this.cleanView, this );
+		},
+
+		setFocus: function() {
+			this.$el.find( '#activity-autocomplete' ).focus();
+		},
+
+		addItemView: function( item ) {
+			this.views.add( new bp.Views.Item( { model: item } ) );
+		},
+
+		autoComplete: function() {
+			var search = $( '#activity-autocomplete' ).val();
+
+			// Reset the collection before starting a new search
+			this.collection.reset();
+
+			if ( 2 > search.length ) {
+				return;
+			}
+
+			this.collection.fetch( {
+				data: {
+					type   : this.options.type,
+					search : search,
+					nonce  : BP_Nouveau.nonces.activity
+				},
+				success : _.bind( this.itemFetched, this ),
+				error   : _.bind( this.itemFetched, this )
+			} );
+		},
+
+		itemFetched: function( items ) {
+			if ( ! items.length ) {
+				this.cleanView();
+			}
+		},
+
+		cleanView: function() {
+			_.each( this.views._views[''], function( view ) {
+					view.remove();
+			} );
+		}
+	} );
+
+	bp.Views.FormAvatar = bp.View.extend( {
+		tagName  : 'div',
+		id       : 'whats-new-avatar',
+		template : bp.template( 'activity-post-form-avatar' ),
+
+		initialize: function() {
+			this.model = new Backbone.Model( _.pick( BP_Nouveau.activity.params, [
+				'user_id',
+				'avatar_url',
+				'avatar_width',
+				'avatar_height',
+				'avatar_alt',
+				'user_domain'
+			] ) );
+
+			if ( this.model.has( 'avatar_url' ) ) {
+				this.model.set( 'display_avatar', true );
+			}
+		}
+	} );
+
+	bp.Views.FormContent = bp.View.extend( {
+		tagName  : 'div',
+		id       : 'whats-new-content',
+
+		initialize: function() {
+			this.$el.html( $('<div></div>' ).prop( 'id', 'whats-new-textarea' ) );
+			this.views.set( '#whats-new-textarea', new bp.Views.WhatsNew( { activity: this.options.activity } ) );
+		}
+	} );
+
+	bp.Views.FormOptions = bp.View.extend( {
+		tagName  : 'div',
+		id       : 'whats-new-options',
+		template : bp.template( 'activity-post-form-options' )
+	} );
+
+	bp.Views.FormTarget = bp.View.extend( {
+		tagName   : 'div',
+		id        : 'whats-new-post-in-box',
+		className : 'in-profile',
+
+		initialize: function() {
+			var select = new bp.Views.WhatsNewPostIn( { filters: BP_Nouveau.activity.params.objects } );
+			this.views.add( select );
+
+			select.model.on( 'change', this.attachAutocomplete, this );
+			bp.Nouveau.Activity.postForm.ActivityObjects.on( 'change:selected', this.postIn, this );
+		},
+
+		attachAutocomplete: function( model ) {
+			if ( 0 !== bp.Nouveau.Activity.postForm.ActivityObjects.models.length ) {
+				bp.Nouveau.Activity.postForm.ActivityObjects.reset();
+			}
+
+			// Clean up views
+			_.each( this.views._views[''], function( view ) {
+				if ( ! _.isUndefined( view.collection ) ) {
+					view.remove();
+				}
+			} );
+
+			if ( 'profile' !== model.get( 'selected') ) {
+				this.views.add( new bp.Views.AutoComplete( {
+					collection:   bp.Nouveau.Activity.postForm.ActivityObjects,
+					type:         model.get( 'selected' ),
+					placeholder : model.get( 'placeholder' )
+				} ) );
+
+				// Set the object type
+				this.model.set( 'object', model.get( 'selected' ) );
+
+			} else {
+				this.model.set( { object: 'user', item_id: 0 } );
+			}
+
+			this.updateDisplay();
+		},
+
+		postIn: function( model ) {
+			if ( _.isUndefined( model.get( 'id' ) ) ) {
+				// Reset the item id
+				this.model.set( 'item_id', 0 );
+
+				// When the model has been cleared, Attach Autocomplete!
+				this.attachAutocomplete( new Backbone.Model( { selected: this.model.get( 'object' ) } ) );
+				return;
+			}
+
+			// Set the item id for the selected object
+			this.model.set( 'item_id', model.get( 'id' ) );
+
+			// Set the view to the selected object
+			this.views.set( '#whats-new-post-in-box-items', new bp.Views.Item( { model: model } ) );
+		},
+
+		updateDisplay: function() {
+			if ( 'user' !== this.model.get( 'object' ) ) {
+				this.$el.removeClass( );
+			} else if ( ! this.$el.hasClass( 'in-profile' ) ) {
+				this.$el.addClass( 'in-profile' );
+			}
+		}
+	} );
+
+	/**
+	 * Now build the buttons!
+	 * @type {[type]}
+	 */
+	bp.Views.FormButtons = bp.View.extend( {
+		tagName : 'div',
+		id      : 'whats-new-actions',
+
+		initialize: function() {
+			this.views.add( new bp.View( { tagName: 'ul', id: 'whats-new-buttons' } ) );
+
+			_.each( this.collection.models, function( button ) {
+				this.addItemView( button );
+			}, this );
+
+			this.collection.on( 'change:active', this.isActive, this );
+		},
+
+		addItemView: function( button ) {
+			this.views.add( '#whats-new-buttons', new bp.Views.FormButton( { model: button } ) );
+		},
+
+		isActive: function( button ) {
+			// Clean up views
+			_.each( this.views._views[''], function( view, index ) {
+				if ( 0 !== index ) {
+					view.remove();
+				}
+			} );
+
+			// Then loop threw all buttons to update their status
+			if ( true === button.get( 'active' ) ) {
+				_.each( this.views._views['#whats-new-buttons'], function( view ) {
+					if ( view.model.get( 'id') !== button.get( 'id' ) ) {
+						// Silently update the model
+						view.model.set( 'active', false, { silent: true } );
+
+						// Remove the active class
+						view.$el.removeClass( 'active' );
+
+						// Trigger an even to let Buttons reset
+						// their modifications to the activity model
+						this.collection.trigger( 'reset:' + view.model.get( 'id' ), this.model );
+					}
+				}, this );
+
+				// Tell the active Button to load its content
+				this.collection.trigger( 'display:' + button.get( 'id' ), this );
+
+			// Trigger an even to let Buttons reset
+			// their modifications to the activity model
+			} else {
+				this.collection.trigger( 'reset:' + button.get( 'id' ), this.model );
+			}
+		}
+	} );
+
+	bp.Views.FormButton = bp.View.extend( {
+		tagName   : 'li',
+		className : 'whats-new-button',
+		template  : bp.template( 'activity-post-form-buttons' ),
+
+		events: {
+			click : 'setActive'
+		},
+
+		setActive: function( event ) {
+			var isActive = this.model.get( 'active' ) || false;
+
+			// Stop event propagation
+			event.preventDefault();
+
+			if ( false === isActive ) {
+				this.$el.addClass( 'active' );
+				this.model.set( 'active', true );
+			} else {
+				this.$el.removeClass( 'active' );
+				this.model.set( 'active', false );
+			}
+		}
+	} );
+
+	bp.Views.FormSubmit = bp.View.extend( {
+		tagName   : 'div',
+		id        : 'whats-new-submit',
+		className : 'in-profile',
+
+		initialize: function() {
+			var reset = new bp.Views.ActivityInput( {
+				type  : 'reset',
+				id    : 'aw-whats-new-reset',
+				className : 'text-button small',
+				value : BP_Nouveau.activity.strings.cancelButton
+			} );
+
+			var submit = new bp.Views.ActivityInput( {
+				type  : 'submit',
+				id    : 'aw-whats-new-submit',
+				className : 'button',
+				name  : 'aw-whats-new-submit',
+				value : BP_Nouveau.activity.strings.postUpdateButton
+			} );
+
+			this.views.set( [ submit, reset ] );
+
+			this.model.on( 'change:object', this.updateDisplay, this );
+		},
+
+		updateDisplay: function( model ) {
+			if ( _.isUndefined( model ) ) {
+				return;
+			}
+
+			if ( 'user' !== model.get( 'object' ) ) {
+				this.$el.removeClass( 'in-profile' );
+			} else if ( ! this.$el.hasClass( 'in-profile' ) ) {
+				this.$el.addClass( 'in-profile' );
+			}
+		}
+	} );
+
+	bp.Views.PostForm = bp.View.extend( {
+		tagName   : 'form',
+		className : 'activity-form',
+		id        : 'whats-new-form',
+
+		attributes: {
+			name   : 'whats-new-form',
+			method : 'post'
+		},
+
+		events: {
+			'focus #whats-new' : 'displayFull',
+			'reset'            : 'resetForm',
+			'submit'           : 'postUpdate',
+			'keydown'          : 'postUpdate'
+		},
+
+		initialize: function() {
+			this.model = new bp.Models.Activity( _.pick(
+				BP_Nouveau.activity.params,
+				['user_id', 'item_id', 'object' ]
+			) );
+
+			// Clone the model to set the resetted one
+			this.resetModel = this.model.clone();
+
+			this.views.set( [
+				new bp.Views.FormAvatar(),
+				new bp.Views.FormContent( { activity: this.model } )
+			] );
+
+			this.model.on( 'change:errors', this.displayFeedback, this );
+		},
+
+		displayFull: function( event ) {
+
+			// Remove feedback.
+			this.cleanFeedback();
+
+			if ( 2 !== this.views._views[''].length ) {
+				return;
+			}
+
+			$( event.target ).css( {
+				resize : 'vertical',
+				height : 'auto'
+			} );
+
+			// Backcompat custom fields
+			if ( true === BP_Nouveau.activity.params.backcompat ) {
+				this.views.add( new bp.Views.FormOptions( { model: this.model } ) );
+			}
+
+			// Attach buttons
+			if ( ! _.isUndefined( BP_Nouveau.activity.params.buttons ) ) {
+				// Global
+				bp.Nouveau.Activity.postForm.buttons.set( BP_Nouveau.activity.params.buttons );
+				this.views.add( new bp.Views.FormButtons( { collection: bp.Nouveau.Activity.postForm.buttons, model: this.model } ) );
+			}
+
+			// Select box for the object
+			if ( ! _.isUndefined( BP_Nouveau.activity.params.objects ) && 1 < _.keys( BP_Nouveau.activity.params.objects ).length ) {
+				this.views.add( new bp.Views.FormTarget( { model: this.model } ) );
+			}
+
+			this.views.add( new bp.Views.FormSubmit( { model: this.model } ) );
+		},
+
+		resetForm: function() {
+			_.each( this.views._views[''], function( view, index ) {
+				if ( index > 1 ) {
+					view.remove();
+				}
+			} );
+
+			$( '#whats-new' ).css( {
+				resize : 'none',
+				height : '50px'
+			} );
+
+			// Reset the model
+			this.model.clear();
+			this.model.set( this.resetModel.attributes );
+		},
+
+		cleanFeedback: function() {
+			_.each( this.views._views[''], function( view ) {
+				if ( 'message' === view.$el.prop( 'id' ) ) {
+					view.remove();
+				}
+			} );
+		},
+
+		displayFeedback: function( model ) {
+			if ( _.isUndefined( this.model.get( 'errors' ) ) ) {
+				this.cleanFeedback();
+			} else {
+				this.views.add( new bp.Views.activityFeedback( model.get( 'errors' ) ) );
+			}
+		},
+
+		postUpdate: function( event ) {
+			var self = this,
+			    meta = {};
+
+			if ( event ) {
+				if ( 'keydown' === event.type && ( 13 !== event.keyCode || ! event.ctrlKey ) ) {
+					return event;
+				}
+
+				event.preventDefault();
+			}
+
+			// Set the content and meta
+			_.each( this.$el.serializeArray(), function( pair ) {
+				pair.name = pair.name.replace( '[]', '' );
+				if ( 'whats-new' === pair.name ) {
+					self.model.set( 'content', pair.value );
+				} else if ( -1 === _.indexOf( ['aw-whats-new-submit', 'whats-new-post-in'], pair.name ) ) {
+					if ( _.isUndefined( meta[ pair.name ] ) ) {
+						meta[ pair.name ] = pair.value;
+					} else {
+						if ( ! _.isArray( meta[ pair.name ] ) ) {
+							meta[ pair.name ] = [ meta[ pair.name ] ];
+						}
+
+						meta[ pair.name ].push( pair.value );
+					}
+				}
+			} );
+
+			// Silently add meta
+			this.model.set( meta, { silent: true } );
+
+			var data = {
+				'_wpnonce_post_update': BP_Nouveau.activity.params.post_nonce
+			};
+
+			// Add the Akismet nonce if it exists.
+			if ( $('#_bp_as_nonce').val() ) {
+				data._bp_as_nonce = $('#_bp_as_nonce').val();
+			}
+
+			bp.ajax.post( 'post_update', _.extend( data, this.model.attributes ) ).done( function( response ) {
+				var store       = bp.Nouveau.getStorage( 'bp-activity' ),
+					searchTerms = $( '[data-bp-search="activity"] input[type="search"]' ).val(), matches = {},
+					toPrepend = false;
+
+				// Look for matches if the stream displays search results.
+				if ( searchTerms ) {
+					searchTerms = new RegExp( searchTerms, 'im' );
+					matches = response.activity.match( searchTerms );
+				}
+
+				/**
+				 * Before injecting the activity into the stream, we need to check the filter
+				 * and search terms are consistent with it when posting from a single item or
+				 * from the Activity directory.
+				 */
+				if ( ( ! searchTerms || matches ) ) {
+					toPrepend = ! store.filter || 0 === parseInt( store.filter, 10 ) || 'activity_update' === store.filter;
+				}
+
+				/**
+				 * In the Activity directory, we also need to check the active scope.
+				 * eg: An update posted in a private group should only show when the
+				 * "My Groups" tab is active.
+				 */
+				if ( toPrepend && response.is_directory ) {
+					toPrepend = ( 'all' === store.scope && ( 'user' === self.model.get( 'object' ) || false === response.is_private ) ) || ( self.model.get( 'object' ) + 's'  === store.scope );
+				}
+
+				// Reset the form
+				self.resetForm();
+
+				// Display a successful feedback if the acticity is not consistent with the displayed stream.
+				if ( ! toPrepend ) {
+					self.views.add( new bp.Views.activityFeedback( { value: response.message, type: 'updated' } ) );
+
+				// Inject the activity into the stream only if it hasn't been done already (HeartBeat).
+				} else if ( ! $( '#activity-' + response.id  ).length ) {
+
+					// It's the very first activity, let's make sure the container can welcome it!
+					if ( ! $( '#activity-stream ul.activity-list').length ) {
+						$( '#activity-stream' ).html( $( '<ul></ul>').addClass( 'activity-list item-list bp-list' ) );
+					}
+
+					// Prepend the activity.
+					bp.Nouveau.inject( '#activity-stream ul.activity-list', response.activity, 'prepend' );
+				}
+			} ).fail( function( response ) {
+
+				self.model.set( 'errors', { type: 'error', value: response.message } );
+			} );
+		}
+	} );
+
+	bp.Nouveau.Activity.postForm.start();
+
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-activity.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-activity.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-activity.js	(revision 12265)
@@ -0,0 +1,837 @@
+/* jshint browser: true */
+/* global bp, BP_Nouveau */
+/* @version 3.1.0 */
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	bp.Nouveau = bp.Nouveau || {};
+
+	/**
+	 * [Activity description]
+	 * @type {Object}
+	 */
+	bp.Nouveau.Activity = {
+
+		/**
+		 * [start description]
+		 * @return {[type]} [description]
+		 */
+		start: function() {
+			this.setupGlobals();
+
+			// Listen to events ("Add hooks!")
+			this.addListeners();
+		},
+
+		/**
+		 * [setupGlobals description]
+		 * @return {[type]} [description]
+		 */
+		setupGlobals: function() {
+			// Init just posted activities
+			this.just_posted    = [];
+
+			// Init current page
+			this.current_page   = 1;
+
+			// Init mentions count
+			this.mentions_count = Number( $( bp.Nouveau.objectNavParent + ' [data-bp-scope="mentions"]' ).find( 'a span' ).html() ) || 0;
+
+			// HeartBeat Globals
+			this.heartbeat_data = {
+				newest         : '',
+				highlights     : {},
+				last_recorded  : 0,
+				first_recorded : 0,
+				document_title : $( document ).prop( 'title' )
+			};
+		},
+
+		/**
+		 * [addListeners description]
+		 */
+		addListeners: function() {
+			// HeartBeat listeners
+			$( '#buddypress' ).on( 'bp_heartbeat_send', this.heartbeatSend.bind( this ) );
+			$( '#buddypress' ).on( 'bp_heartbeat_tick', this.heartbeatTick.bind( this ) );
+
+			// Inject Activities
+			$( '#buddypress [data-bp-list="activity"]' ).on( 'click', 'li.load-newest, li.load-more', this.injectActivities.bind( this ) );
+
+			// Hightlight new activities & clean up the stream
+			$( '#buddypress' ).on( 'bp_ajax_request', '[data-bp-list="activity"]', this.scopeLoaded.bind( this ) );
+
+			// Activity comments effect
+			$( '#buddypress [data-bp-list="activity"]' ).on( 'bp_ajax_append', this.hideComments );
+			$( '#buddypress [data-bp-list="activity"]' ).on( 'click', '.show-all', this.showComments );
+
+			// Activity actions
+			$( '#buddypress [data-bp-list="activity"]' ).on( 'click', '.activity-item', bp.Nouveau, this.activityActions );
+			$( document ).keydown( this.commentFormAction );
+		},
+
+		/**
+		 * [heartbeatSend description]
+		 * @param  {[type]} event [description]
+		 * @param  {[type]} data  [description]
+		 * @return {[type]}       [description]
+		 */
+		heartbeatSend: function( event, data ) {
+			this.heartbeat_data.first_recorded = $( '#buddypress [data-bp-list] [data-bp-activity-id]' ).first().data( 'bp-timestamp' ) || 0;
+
+			if ( 0 === this.heartbeat_data.last_recorded || this.heartbeat_data.first_recorded > this.heartbeat_data.last_recorded ) {
+				this.heartbeat_data.last_recorded = this.heartbeat_data.first_recorded;
+			}
+
+			data.bp_activity_last_recorded = this.heartbeat_data.last_recorded;
+
+			if ( $( '#buddypress .dir-search input[type=search]' ).length ) {
+				data.bp_activity_last_recorded_search_terms = $( '#buddypress .dir-search input[type=search]' ).val();
+			}
+
+			$.extend( data, { bp_heartbeat: bp.Nouveau.getStorage( 'bp-activity' ) } );
+		},
+
+		/**
+		 * [heartbeatTick description]
+		 * @param  {[type]} event          [description]
+		 * @param  {[type]} data           [description]
+		 * @return {[type]}                [description]
+		 */
+		heartbeatTick: function( event, data ) {
+			var newest_activities_count, newest_activities, objects = bp.Nouveau.objects,
+				scope = bp.Nouveau.getStorage( 'bp-activity', 'scope' ), self = this;
+
+			// Only proceed if we have newest activities
+			if ( undefined === data || ! data.bp_activity_newest_activities ) {
+				return;
+			}
+
+			this.heartbeat_data.newest = $.trim( data.bp_activity_newest_activities.activities ) + this.heartbeat_data.newest;
+			this.heartbeat_data.last_recorded  = Number( data.bp_activity_newest_activities.last_recorded );
+
+			// Parse activities
+			newest_activities = $( this.heartbeat_data.newest ).filter( '.activity-item' );
+
+			// Count them
+			newest_activities_count = Number( newest_activities.length );
+
+			/**
+			 * It's not a regular object but we need it!
+			 * so let's add it temporarly..
+			 */
+			objects.push( 'mentions' );
+
+			/**
+			 * On the All Members tab, we need to know what these activities are about
+			 * in order to update all the other tabs dynamic span
+			 */
+			if ( 'all' === scope ) {
+
+				$.each( newest_activities, function( a, activity ) {
+					activity = $( activity );
+
+					$.each( objects, function( o, object ) {
+						if ( -1 !== $.inArray( 'bp-my-' + object, activity.get( 0 ).classList ) ) {
+							if ( undefined === self.heartbeat_data.highlights[ object ] ) {
+								self.heartbeat_data.highlights[ object ] = [ activity.data( 'bp-activity-id' ) ];
+							} else if ( -1 === $.inArray( activity.data( 'bp-activity-id' ), self.heartbeat_data.highlights[ object ] ) ) {
+								self.heartbeat_data.highlights[ object ].push( activity.data( 'bp-activity-id' ) );
+							}
+						}
+					} );
+				} );
+
+				// Remove the specific classes to count highligthts
+				var regexp = new RegExp( 'bp-my-(' + objects.join( '|' ) + ')', 'g' );
+				this.heartbeat_data.newest = this.heartbeat_data.newest.replace( regexp, '' );
+
+				/**
+				 * Deal with the 'All Members' dynamic span from here as HeartBeat is working even when
+				 * the user is not logged in
+				 */
+				 $( bp.Nouveau.objectNavParent + ' [data-bp-scope="all"]' ).find( 'a span' ).html( newest_activities_count );
+
+			// Set all activities to be highlighted for the current scope
+			} else {
+				// Init the array of highlighted activities
+				this.heartbeat_data.highlights[ scope ] = [];
+
+				$.each( newest_activities, function( a, activity ) {
+					self.heartbeat_data.highlights[ scope ].push( $( activity ).data( 'bp-activity-id' ) );
+				} );
+			}
+
+			$.each( objects, function( o, object ) {
+				if ( undefined !== self.heartbeat_data.highlights[ object ] && self.heartbeat_data.highlights[ object ].length ) {
+					var count = 0;
+
+					if ( 'mentions' === object ) {
+						count = self.mentions_count;
+					}
+
+					$( bp.Nouveau.objectNavParent + ' [data-bp-scope="' + object + '"]' ).find( 'a span' ).html( Number( self.heartbeat_data.highlights[ object ].length ) + count );
+				}
+			} );
+
+			/**
+			 * Let's remove the mentions from objects!
+			 */
+			objects.pop();
+
+			// Add an information about the number of newest activities inside the document's title
+			$( document ).prop( 'title', '(' + newest_activities_count + ') ' + this.heartbeat_data.document_title );
+
+			// Update the Load Newest li if it already exists.
+			if ( $( '#buddypress [data-bp-list="activity"] li' ).first().hasClass( 'load-newest' ) ) {
+				var newest_link = $( '#buddypress [data-bp-list="activity"] .load-newest a' ).html();
+				$( '#buddypress [data-bp-list="activity"] .load-newest a' ).html( newest_link.replace( /([0-9]+)/, newest_activities_count ) );
+
+			// Otherwise add it
+			} else {
+				$( '#buddypress [data-bp-list="activity"] ul.activity-list' ).prepend( '<li class="load-newest"><a href="#newest">' + BP_Nouveau.newest + ' (' + newest_activities_count + ')</a></li>' );
+			}
+
+			/**
+			 * Finally trigger a pending event containing the activity heartbeat data
+			 */
+			$( '#buddypress [data-bp-list="activity"]' ).trigger( 'bp_heartbeat_pending', this.heartbeat_data );
+		},
+
+		/**
+		 * [injectQuery description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		injectActivities: function( event ) {
+			var store = bp.Nouveau.getStorage( 'bp-activity' ),
+				scope = store.scope || null, filter = store.filter || null;
+
+			// Load newest activities
+			if ( $( event.currentTarget ).hasClass( 'load-newest' ) ) {
+				// Stop event propagation
+				event.preventDefault();
+
+				$( event.currentTarget ).remove();
+
+				/**
+				 * If a plugin is updating the recorded_date of an activity
+				 * it will be loaded as a new one. We need to look in the
+				 * stream and eventually remove similar ids to avoid "double".
+				 */
+				var activities = $.parseHTML( this.heartbeat_data.newest );
+
+				$.each( activities, function( a, activity ){
+					if( 'LI' === activity.nodeName && $( activity ).hasClass( 'just-posted' ) ) {
+						if( $( '#' + $( activity ).prop( 'id' ) ).length ) {
+							$( '#' + $( activity ).prop( 'id' ) ).remove();
+						}
+					}
+				} );
+
+				// Now the stream is cleaned, prepend newest
+				$( event.delegateTarget ).find( '.activity-list' ).prepend( this.heartbeat_data.newest ).trigger( 'bp_heartbeat_prepend', this.heartbeat_data );
+
+				// Reset the newest activities now they're displayed
+				this.heartbeat_data.newest = '';
+
+				// Reset the All members tab dynamic span id it's the current one
+				if ( 'all' === scope ) {
+					$( bp.Nouveau.objectNavParent + ' [data-bp-scope="all"]' ).find( 'a span' ).html( '' );
+				}
+
+				// Specific to mentions
+				if ( 'mentions' === scope ) {
+					// Now mentions are displayed, remove the user_metas
+					bp.Nouveau.ajax( { action: 'activity_clear_new_mentions' }, 'activity' );
+					this.mentions_count = 0;
+				}
+
+				// Activities are now displayed, clear the newest count for the scope
+				$( bp.Nouveau.objectNavParent + ' [data-bp-scope="' + scope + '"]' ).find( 'a span' ).html( '' );
+
+				// Activities are now displayed, clear the highlighted activities for the scope
+				if ( undefined !== this.heartbeat_data.highlights[ scope ] ) {
+					this.heartbeat_data.highlights[ scope ] = [];
+				}
+
+				// Remove highlighted for the current scope
+				setTimeout( function () {
+					$( event.delegateTarget ).find( '[data-bp-activity-id]' ).removeClass( 'newest_' + scope + '_activity' );
+				}, 3000 );
+
+				// Reset the document title
+				$( document ).prop( 'title', this.heartbeat_data.document_title );
+
+			// Load more activities
+			} else if ( $( event.currentTarget ).hasClass( 'load-more' ) ) {
+				var next_page = ( Number( this.current_page ) * 1 ) + 1, self = this, search_terms = '';
+
+				// Stop event propagation
+				event.preventDefault();
+
+				$( event.currentTarget ).find( 'a' ).first().addClass( 'loading' );
+
+				// reset the just posted
+				this.just_posted = [];
+
+				// Now set it
+				$( event.delegateTarget ).children( '.just-posted' ).each( function() {
+					self.just_posted.push( $( this ).data( 'bp-activity-id' ) );
+				} );
+
+				if ( $( '#buddypress .dir-search input[type=search]' ).length ) {
+					search_terms = $( '#buddypress .dir-search input[type=search]' ).val();
+				}
+
+				bp.Nouveau.objectRequest( {
+					object              : 'activity',
+					scope               : scope,
+					filter              : filter,
+					search_terms        : search_terms,
+					page                : next_page,
+					method              : 'append',
+					exclude_just_posted : this.just_posted.join( ',' ),
+					target              : '#buddypress [data-bp-list] ul.bp-list'
+				} ).done( function( response ) {
+					if ( true === response.success ) {
+						$( event.currentTarget ).remove();
+
+						// Update the current page
+						self.current_page = next_page;
+					}
+				} );
+			}
+		},
+
+		/**
+		 * [truncateComments description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		hideComments: function( event ) {
+			var comments = $( event.target ).find( '.activity-comments' ),
+				activity_item, comment_items, comment_count, comment_parents;
+
+			if ( ! comments.length ) {
+				return;
+			}
+
+			comments.each( function( c, comment ) {
+				comment_parents = $( comment ).children( 'ul' );
+				comment_items   = $( comment_parents ).find( 'li' );
+
+
+				if ( ! comment_items.length ) {
+					return;
+				}
+
+				// Get the activity id
+				activity_item = $( comment ).closest( '.activity-item' );
+
+				// Get the comment count
+				comment_count = $( '#acomment-comment-' + activity_item.data( 'bp-activity-id' ) + ' span.comment-count' ).html() || ' ';
+
+				// Keep latest 5 comments
+				comment_items.each( function( i, item ) {
+					if ( i < comment_items.length - 5 ) {
+						$( item ).addClass('bp-hidden').hide();
+
+						// Prepend a link to display all
+						if ( ! i ) {
+							$( item ).before( '<li class="show-all"><button class="text-button" type="button" data-bp-show-comments-id="#' + activity_item.prop( 'id' ) + '/show-all/"><span class="icon dashicons dashicons-visibility" aria-hidden="true"></span> ' + BP_Nouveau.show_x_comments.replace( '%d', comment_count ) + '</button></li>' );
+						}
+					}
+				} );
+
+				// If all parents are hidden, reveal at least one. It seems very risky to manipulate the DOM to keep exactly 5 comments!
+				if ( $( comment_parents ).children( '.bp-hidden' ).length === $( comment_parents ).children( 'li' ).length - 1 && $( comment_parents ).find( 'li.show-all' ).length ) {
+					$( comment_parents ).children( 'li' ).removeClass( 'bp-hidden' ).toggle();
+				}
+			} );
+		},
+
+		/**
+		 * [showComments description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		showComments: function( event ) {
+			// Stop event propagation
+			event.preventDefault();
+
+			$( event.target ).addClass( 'loading' );
+
+			setTimeout( function() {
+				$( event.target ).closest( 'ul' ).find( 'li' ).removeClass('bp-hidden').fadeIn( 300, function() {
+					$( event.target ).parent( 'li' ).remove();
+				} );
+			}, 600 );
+		},
+
+		/**
+		 * [scopeLoaded description]
+		 * @param  {[type]} event [description]
+		 * @param  {[type]} data  [description]
+		 * @return {[type]}       [description]
+		 */
+		scopeLoaded: function ( event, data ) {
+			// Make sure to only keep 5 root comments
+			this.hideComments( event );
+
+			// Reset the pagination for the scope.
+			this.current_page = 1;
+
+			// Mentions are specific
+			if ( 'mentions' === data.scope && undefined !== data.response.new_mentions ) {
+				$.each( data.response.new_mentions, function( i, id ) {
+					$( '#buddypress #activity-stream' ).find( '[data-bp-activity-id="' + id + '"]' ).addClass( 'newest_mentions_activity' );
+				} );
+
+				// Reset mentions count
+				this.mentions_count = 0;
+			} else if ( undefined !== this.heartbeat_data.highlights[data.scope] && this.heartbeat_data.highlights[data.scope].length ) {
+				$.each( this.heartbeat_data.highlights[data.scope], function( i, id ) {
+					if ( $( '#buddypress #activity-stream' ).find( '[data-bp-activity-id="' + id + '"]' ).length ) {
+						$( '#buddypress #activity-stream' ).find( '[data-bp-activity-id="' + id + '"]' ).addClass( 'newest_' + data.scope + '_activity' );
+					}
+				} );
+			}
+
+			// Reset the newest activities now they're displayed
+			this.heartbeat_data.newest = '';
+			$.each( $( bp.Nouveau.objectNavParent + ' [data-bp-scope]' ).find( 'a span' ), function( s, count ) {
+				if ( 0 === parseInt( $( count ).html(), 10 ) ) {
+					$( count ).html( '' );
+				}
+			} );
+
+			// Activities are now loaded, clear the highlighted activities for the scope
+			if ( undefined !== this.heartbeat_data.highlights[ data.scope ] ) {
+				this.heartbeat_data.highlights[ data.scope ] = [];
+			}
+
+			// Reset the document title
+			$( document ).prop( 'title', this.heartbeat_data.document_title );
+
+			setTimeout( function () {
+				$( '#buddypress #activity-stream .activity-item' ).removeClass( 'newest_' + data.scope +'_activity' );
+			}, 3000 );
+		},
+
+		/**
+		 * [activityActions description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		activityActions: function( event ) {
+			var parent = event.data, target = $( event.target ), activity_item = $( event.currentTarget ),
+				activity_id = activity_item.data( 'bp-activity-id' ), stream = $( event.delegateTarget ),
+				item_id, form;
+
+			// In case the target is set to a span inside the link.
+			if ( $( target ).is( 'span' ) ) {
+				target = $( target ).closest( 'a' );
+			}
+
+			// Favoriting
+			if ( target.hasClass( 'fav') || target.hasClass('unfav') ) {
+				var type = target.hasClass( 'fav' ) ? 'fav' : 'unfav';
+
+				// Stop event propagation
+				event.preventDefault();
+
+				target.addClass( 'loading' );
+
+				parent.ajax( { action: 'activity_mark_' + type, 'id': activity_id }, 'activity' ).done( function( response ) {
+					target.removeClass( 'loading' );
+
+					if ( false === response.success ) {
+						return;
+					} else {
+						target.fadeOut( 200, function() {
+							if ( $( this ).find( 'span' ).first().length ) {
+								$( this ).find( 'span' ).first().html( response.data.content );
+							} else {
+								$( this ).html( response.data.content );
+							}
+							$( this ).prop( 'title', response.data.content );
+
+							if ('false' === $(this).attr('aria-pressed') ) {
+								$( this ).attr('aria-pressed', 'true');
+							} else {
+								$( this ).attr('aria-pressed', 'false');
+							}
+
+							$( this ).fadeIn( 200 );
+						} );
+					}
+
+					if ( 'fav' === type ) {
+						if ( undefined !== response.data.directory_tab ) {
+							if ( ! $( parent.objectNavParent + ' [data-bp-scope="favorites"]' ).length ) {
+								$( parent.objectNavParent + ' [data-bp-scope="all"]' ).after( response.data.directory_tab );
+							}
+						}
+
+						target.removeClass( 'fav' );
+						target.addClass( 'unfav' );
+
+					} else if ( 'unfav' === type ) {
+						var favoriteScope = $( '[data-bp-user-scope="favorites"]' ).hasClass( 'selected' ) || $( parent.objectNavParent + ' [data-bp-scope="favorites"]' ).hasClass( 'selected' );
+
+						// If on user's profile or on the favorites directory tab, remove the entry
+						if ( favoriteScope ) {
+							activity_item.remove();
+						}
+
+						if ( undefined !== response.data.no_favorite ) {
+							// Remove the tab when on activity directory but not on the favorites tabs
+							if ( $( parent.objectNavParent + ' [data-bp-scope="all"]' ).length && $( parent.objectNavParent + ' [data-bp-scope="all"]' ).hasClass( 'selected' ) ) {
+								$( parent.objectNavParent + ' [data-bp-scope="favorites"]' ).remove();
+
+							// In all the other cases, append a message to the empty stream
+							} else if ( favoriteScope ) {
+								stream.append( response.data.no_favorite );
+							}
+						}
+
+						target.removeClass( 'unfav' );
+						target.addClass( 'fav' );
+					}
+				} );
+			}
+
+			// Deleting or spamming
+			if ( target.hasClass( 'delete-activity' ) || target.hasClass( 'acomment-delete' ) || target.hasClass( 'spam-activity' ) || target.hasClass( 'spam-activity-comment' ) ) {
+				var activity_comment_li = target.closest( '[data-bp-activity-comment-id]' ),
+				    activity_comment_id = activity_comment_li.data( 'bp-activity-comment-id' ),
+				    li_parent, comment_count_span, comment_count, show_all_a, deleted_comments_count = 0;
+
+				// Stop event propagation
+				event.preventDefault();
+
+				if ( undefined !== BP_Nouveau.confirm && false === window.confirm( BP_Nouveau.confirm ) ) {
+					return false;
+				}
+
+				target.addClass( 'loading' );
+
+				var ajaxData = {
+					action      : 'delete_activity',
+					'id'        : activity_id,
+					'_wpnonce'  : parent.getLinkParams( target.prop( 'href' ), '_wpnonce' ),
+					'is_single' : target.closest( '[data-bp-single]' ).length
+				};
+
+				// Only the action changes when spamming an activity or a comment.
+				if ( target.hasClass( 'spam-activity' ) || target.hasClass( 'spam-activity-comment' ) ) {
+					ajaxData.action = 'bp_spam_activity';
+				}
+
+				// Set defaults parent li to activity container
+				li_parent = activity_item;
+
+				// If it's a comment edit ajaxData.
+				if ( activity_comment_id ) {
+					delete ajaxData.is_single;
+
+					// Set comment data.
+					ajaxData.id         = activity_comment_id;
+					ajaxData.is_comment = true;
+
+					// Set parent li to activity comment container
+					li_parent = activity_comment_li;
+				}
+
+				parent.ajax( ajaxData, 'activity' ).done( function( response ) {
+					target.removeClass( 'loading' );
+
+					if ( false === response.success ) {
+						li_parent.prepend( response.data.feedback );
+						li_parent.find( '.bp-feedback' ).hide().fadeIn( 300 );
+					} else {
+						// Specific case of the single activity screen.
+						if ( response.data.redirect ) {
+							return window.location.href = response.data.redirect;
+						}
+
+						if ( activity_comment_id ) {
+							deleted_comments_count = 1;
+
+							// Move the form if needed
+							activity_item.append( activity_comment_li.find( 'form' ) );
+
+							// Count child comments if there are some
+							$.each( activity_comment_li.find( 'li' ), function() {
+								deleted_comments_count += 1;
+							} );
+
+							// Update the button count
+							comment_count_span = activity_item.find( '.acomment-reply span.comment-count' );
+							comment_count      = Number( comment_count_span.html() - deleted_comments_count );
+							comment_count_span.html( comment_count );
+
+							// Update the show all count
+							show_all_a = activity_item.find( 'li.show-all a' );
+							if ( show_all_a.length ) {
+								show_all_a.html( BP_Nouveau.show_x_comments.replace( '%d', comment_count ) );
+							}
+
+							// Clean up the parent activity classes.
+							if ( 0 === comment_count ) {
+								activity_item.removeClass( 'has-comments' );
+							}
+						}
+
+						// Remove the entry
+						li_parent.slideUp( 300, function() {
+							li_parent.remove();
+						} );
+
+						// reset vars to get newest activities when an activity is deleted
+						if ( ! activity_comment_id && activity_item.data( 'bp-timestamp' ) === parent.Activity.heartbeat_data.last_recorded ) {
+							parent.Activity.heartbeat_data.newest        = '';
+							parent.Activity.heartbeat_data.last_recorded  = 0;
+						}
+					}
+				} );
+			}
+
+			// Reading more
+			if ( target.closest( 'span' ).hasClass( 'activity-read-more' ) ) {
+				var content = target.closest( 'div' ), readMore = target.closest( 'span' );
+
+				item_id = null;
+
+				if ( $( content ).hasClass( 'activity-inner' ) ) {
+					item_id = activity_id;
+				} else if ( $( content ).hasClass( 'acomment-content' ) ) {
+					item_id = target.closest( 'li' ).data( 'bp-activity-comment-id' );
+				}
+
+				if ( ! item_id ) {
+					return event;
+				}
+
+				// Stop event propagation
+				event.preventDefault();
+
+				$( readMore ).addClass( 'loading' );
+
+				parent.ajax( {
+					action : 'get_single_activity_content',
+					id     : item_id
+				}, 'activity' ).done( function( response ) {
+					$( readMore ).removeClass( 'loading' );
+
+					if ( content.parent().find( '.bp-feedback' ).length ) {
+						content.parent().find( '.bp-feedback' ).remove();
+					}
+
+					if ( false === response.success ) {
+						content.after( response.data.feedback );
+						content.parent().find( '.bp-feedback' ).hide().fadeIn( 300 );
+					} else {
+						$( content ).slideUp( 300 ).html( response.data.contents ).slideDown( 300 );
+					}
+				} );
+			}
+
+			// Displaying the comment form
+			if ( target.hasClass( 'acomment-reply' ) || target.parent().hasClass( 'acomment-reply' ) ) {
+				var comment_link = target;
+
+				form = $( '#ac-form-' + activity_id );
+				item_id = activity_id;
+
+				// Stop event propagation
+				event.preventDefault();
+
+				// If the comment count span inside the link is clicked
+				if ( target.parent().hasClass( 'acomment-reply' ) ) {
+					comment_link = target.parent();
+				}
+
+				if ( target.closest( 'li' ).data( 'bp-activity-comment-id' ) ) {
+					item_id = target.closest( 'li' ).data( 'bp-activity-comment-id' );
+				}
+
+				// ?? hide and display none..
+				//form.css( 'display', 'none' );
+				form.removeClass( 'root' );
+				$('.ac-form').hide();
+
+				/* Remove any error messages */
+				$.each( form.children( 'div' ), function( e, err ) {
+					if ( $( err ).hasClass( 'error' ) ) {
+						$( err ).remove();
+					}
+				} );
+
+				// It's an activity we're commenting
+				if ( item_id === activity_id ) {
+					$( '[data-bp-activity-id="' + item_id + '"] .activity-comments' ).append( form );
+					form.addClass( 'root' );
+
+				// It's a comment we're replying to
+				} else {
+					$( '[data-bp-activity-comment-id="' + item_id + '"]' ).append( form );
+				}
+
+				form.slideDown( 200 );
+
+				// change the aria state from false to true
+				target.attr( 'aria-expanded', 'true' );
+
+				$.scrollTo( form, 500, {
+					offset:-100,
+					easing:'swing'
+				} );
+
+				$( '#ac-form-' + activity_id + ' textarea' ).focus();
+			}
+
+			// Removing the form
+			if ( target.hasClass( 'ac-reply-cancel' ) ) {
+
+				$( target ).closest( '.ac-form' ).slideUp( 200 );
+
+				// Change the aria state back to false on comment cancel
+				$( '.acomment-reply').attr( 'aria-expanded', 'false' );
+
+				// Stop event propagation
+				event.preventDefault();
+			}
+
+			// Submitting comments and replies
+			if ( 'ac_form_submit' === target.prop( 'name' ) ) {
+				var comment_content, comment_data;
+
+				form = target.closest( 'form' );
+				item_id = activity_id;
+
+				// Stop event propagation
+				event.preventDefault();
+
+				if ( target.closest( 'li' ).data( 'bp-activity-comment-id' ) ) {
+					item_id    = target.closest( 'li' ).data( 'bp-activity-comment-id' );
+				}
+
+				comment_content = $( form ).find( 'textarea' ).first();
+
+				target.addClass( 'loading' ).prop( 'disabled', true );
+				comment_content.addClass( 'loading' ).prop( 'disabled', true );
+
+				comment_data = {
+					action                        : 'new_activity_comment',
+					_wpnonce_new_activity_comment : $( '#_wpnonce_new_activity_comment' ).val(),
+					comment_id                    : item_id,
+					form_id                       : activity_id,
+					content                       : comment_content.val()
+				};
+
+				// Add the Akismet nonce if it exists
+				if ( $( '#_bp_as_nonce_' + activity_id ).val() ) {
+					comment_data['_bp_as_nonce_' + activity_id] = $( '#_bp_as_nonce_' + activity_id ).val();
+				}
+
+				parent.ajax( comment_data, 'activity' ).done( function( response ) {
+					target.removeClass( 'loading' );
+					comment_content.removeClass( 'loading' );
+					$( '.acomment-reply' ).attr( 'aria-expanded', 'false' );
+
+					if ( false === response.success ) {
+						form.append( $( response.data.feedback ).hide().fadeIn( 200 ) );
+					} else {
+						var activity_comments = form.parent();
+						var the_comment = $.trim( response.data.contents );
+
+						form.fadeOut( 200, function() {
+							if ( 0 === activity_comments.children( 'ul' ).length ) {
+								if ( activity_comments.hasClass( 'activity-comments' ) ) {
+									activity_comments.prepend( '<ul></ul>' );
+								} else {
+									activity_comments.append( '<ul></ul>' );
+								}
+							}
+
+							activity_comments.children( 'ul' ).append( $( the_comment ).hide().fadeIn( 200 ) );
+							$( form ).find( 'textarea' ).first().val( '' );
+
+							activity_comments.parent().addClass( 'has-comments' );
+						} );
+
+						// why, as it's already done a few lines ahead ???
+						//jq( '#' + form.attr('id') + ' textarea').val('');
+
+						// Set the new count
+						comment_count = Number( $( activity_item ).find( 'a span.comment-count' ).html() || 0 ) + 1;
+
+						// Increase the "Reply (X)" button count
+						$( activity_item ).find( 'a span.comment-count' ).html( comment_count );
+
+						// Increment the 'Show all x comments' string, if present
+						show_all_a = $( activity_item ).find( '.show-all a' );
+						if ( show_all_a ) {
+							show_all_a.html( BP_Nouveau.show_x_comments.replace( '%d', comment_count ) );
+						}
+					}
+
+					target.prop( 'disabled', false );
+					comment_content.prop( 'disabled', false );
+				} );
+			}
+		},
+
+		/**
+		 * [closeCommentForm description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		commentFormAction: function( event ) {
+			var element, keyCode;
+
+			event = event || window.event;
+
+			if ( event.target ) {
+				element = event.target;
+			} else if ( event.srcElement) {
+				element = event.srcElement;
+			}
+
+			if ( element.nodeType === 3 ) {
+				element = element.parentNode;
+			}
+
+			if ( event.altKey === true || event.metaKey === true ) {
+				return event;
+			}
+
+			// Not in a comment textarea, return
+			if ( element.tagName !== 'TEXTAREA' || ! $( element ).hasClass( 'ac-input' ) ) {
+				return event;
+			}
+
+			keyCode = ( event.keyCode) ? event.keyCode : event.which;
+
+			if ( 27 === keyCode && false === event.ctrlKey  ) {
+				if ( element.tagName === 'TEXTAREA' ) {
+					$( element ).closest( 'form' ).slideUp( 200 );
+				}
+			} else if ( event.ctrlKey && 13 === keyCode && $( element ).val() ) {
+				$( element ).closest( 'form' ).find( '[type=submit]' ).first().trigger( 'click' );
+			}
+		}
+	};
+
+	// Launch BP Nouveau Activity
+	bp.Nouveau.Activity.start();
+
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-group-invites.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-group-invites.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-group-invites.js	(revision 12265)
@@ -0,0 +1,847 @@
+/* global wp, bp, BP_Nouveau, _, Backbone */
+/* @version 4.0.0 */
+window.wp = window.wp || {};
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	_.extend( bp, _.pick( wp, 'Backbone', 'ajax', 'template' ) );
+
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+
+	bp.Nouveau = bp.Nouveau || {};
+
+	/**
+	 * [Nouveau description]
+	 * @type {Object}
+	 */
+	bp.Nouveau.GroupInvites = {
+		/**
+		 * [start description]
+		 * @return {[type]} [description]
+		 */
+		start: function() {
+			this.scope    = null;
+			this.views    = new Backbone.Collection();
+			this.navItems = new Backbone.Collection();
+			this.users    = new bp.Collections.Users();
+			this.invites  = this.users.clone();
+
+			// Add views
+			this.setupNav();
+			this.setupLoops();
+			this.displayFeedback( BP_Nouveau.group_invites.loading, 'loading' );
+
+			// Add an invite when a user is selected
+			this.users.on( 'change:selected', this.addInvite, this );
+
+			// Add an invite when a user is selected
+			this.invites.on( 'change:selected', this.manageInvite, this );
+
+			// And display the Invites nav
+			this.invites.on( 'add', this.invitesNav, this );
+			this.invites.on( 'reset', this.hideInviteNav, this );
+		},
+
+		setupNav: function() {
+			var activeView;
+
+			// Init the nav
+			this.nav = new bp.Views.invitesNav( { collection: this.navItems } );
+
+			// loop through available nav items to build it
+			_.each( BP_Nouveau.group_invites.nav, function( item, index ) {
+				if ( ! _.isObject( item ) ) {
+					return;
+				}
+
+				// Reset active View
+				activeView = 0;
+
+				if ( 0 === index ) {
+					this.scope = item.id;
+					activeView = 1;
+				}
+
+				this.navItems.add( {
+					id     : item.id,
+					name   : item.caption,
+					href   : item.href || '#members-list',
+					active : activeView,
+					hide   : _.isUndefined( item.hide ) ? 0 : item.hide
+				} );
+			}, this );
+
+			// Inject the nav into the DOM
+			this.nav.inject( '.bp-invites-nav' );
+
+			// Listen to the confirm event
+			this.nav.on( 'bp-invites:confirm', this.loadConfirmView, this );
+			this.nav.on( 'bp-invites:loops', this.setupLoops, this );
+		},
+
+		setupLoops: function( scope ) {
+			var users;
+
+			scope = scope || this.scope;
+
+			// Reset Views
+			this.clearViews();
+
+			// Only display the loading message if scope has changed
+			if ( scope !== this.scope ) {
+				// Loading
+				this.displayFeedback( BP_Nouveau.group_invites.loading, 'loading' );
+			}
+
+			// Set global scope to requested one
+			this.scope = scope;
+
+			// Create the loop view
+			users = new bp.Views.inviteUsers( { collection: this.users, scope: scope } );
+
+			this.views.add( { id: 'users', view: users } );
+
+			users.inject( '.bp-invites-content' );
+
+			this.displayFilters( this.users );
+		},
+
+		displayFilters: function( collection ) {
+			var filters_view;
+
+			// Create the model
+			this.filters = new Backbone.Model( {
+				page         : 1,
+				total_page   : 0,
+				search_terms : '',
+				scope        : this.scope
+			} );
+
+			// Use it in the filters viex
+			filters_view = new bp.Views.inviteFilters( { model: this.filters, users: collection } );
+
+			this.views.add( { id: 'filters', view: filters_view } );
+
+			filters_view.inject( '.bp-invites-filters' );
+		},
+
+		removeFeedback: function() {
+			var feedback;
+
+			if ( ! _.isUndefined( this.views.get( 'feedback' ) ) ) {
+				feedback = this.views.get( 'feedback' );
+				feedback.get( 'view' ).remove();
+				this.views.remove( { id: 'feedback', view: feedback } );
+			}
+		},
+
+		displayFeedback: function( message, type ) {
+			var feedback;
+
+			// Make sure to remove the feedbacks
+			this.removeFeedback();
+
+			if ( ! message ) {
+				return;
+			}
+
+			feedback = new bp.Views.Feedback( {
+				value : message,
+				type  : type || 'info'
+			} );
+
+			this.views.add( { id: 'feedback', view: feedback } );
+
+			feedback.inject( '.bp-invites-feedback' );
+		},
+
+		addInvite: function( user ) {
+			if ( true === user.get( 'selected' ) ) {
+				this.invites.add( user );
+			} else {
+				var invite = this.invites.get( user.get( 'id' ) );
+
+				if ( true === invite.get( 'selected' ) ) {
+					this.invites.remove( invite );
+				}
+			}
+		},
+
+		manageInvite: function( invite ) {
+			var user = this.users.get( invite.get( 'id' ) );
+
+			// Update the user
+			if ( user ) {
+				user.set( 'selected', false );
+			}
+
+			// remove the invite
+			this.invites.remove( invite );
+
+			// No more invites, reset the collection
+			if ( ! this.invites.length  ) {
+				this.invites.reset();
+			}
+		},
+
+		invitesNav: function() {
+			this.navItems.get( 'invites' ).set( { active: 0, hide: 0 } );
+		},
+
+		hideInviteNav: function() {
+			this.navItems.get( 'invites' ).set( { active: 0, hide: 1 } );
+		},
+
+		clearViews: function() {
+			// Clear views
+			if ( ! _.isUndefined( this.views.models ) ) {
+				_.each( this.views.models, function( model ) {
+					model.get( 'view' ).remove();
+				}, this );
+
+				this.views.reset();
+			}
+		},
+
+		loadConfirmView: function() {
+			this.clearViews();
+
+			this.displayFeedback( BP_Nouveau.group_invites.invites_form, 'help' );
+
+			// Activate the loop view
+			var invites = new bp.Views.invitesEditor( { collection: this.invites } );
+
+			this.views.add( { id: 'invites', view: invites } );
+
+			invites.inject( '.bp-invites-content' );
+		}
+	};
+
+	// Item (group or blog or any other)
+	bp.Models.User = Backbone.Model.extend( {
+		defaults : {
+			id       : 0,
+			avatar   : '',
+			name     : '',
+			selected : false
+        }
+	} );
+
+	/** Collections ***********************************************************/
+
+	// Items (groups or blogs or any others)
+	bp.Collections.Users = Backbone.Collection.extend( {
+		model: bp.Models.User,
+
+		initialize : function() {
+			this.options = { page: 1, total_page: 0, group_id: BP_Nouveau.group_invites.group_id };
+		},
+
+		sync: function( method, model, options ) {
+			options         = options || {};
+			options.context = this;
+			options.data    = options.data || {};
+
+			// Add generic nonce
+			options.data.nonce = BP_Nouveau.nonces.groups;
+
+			if ( this.options.group_id ) {
+				options.data.group_id = this.options.group_id;
+			}
+
+			if ( 'read' === method ) {
+				options.data = _.extend( options.data, {
+					action: 'groups_get_group_potential_invites'
+				} );
+
+				return bp.ajax.send( options );
+			}
+
+			if ( 'create' === method ) {
+				options.data = _.extend( options.data, {
+					action   : 'groups_send_group_invites',
+					_wpnonce : BP_Nouveau.group_invites.nonces.send_invites
+				} );
+
+				if ( model ) {
+					options.data.users = model;
+				}
+
+				return bp.ajax.send( options );
+			}
+
+			if ( 'delete' === method ) {
+				options.data = _.extend( options.data, {
+					action   : 'groups_delete_group_invite',
+					_wpnonce : BP_Nouveau.group_invites.nonces.uninvite
+				} );
+
+				if ( model ) {
+					options.data.user = model;
+				}
+
+				return bp.ajax.send( options );
+			}
+		},
+
+		parse: function( resp ) {
+
+			if ( ! _.isArray( resp.users ) ) {
+				resp.users = [resp.users];
+			}
+
+			_.each( resp.users, function( value, index ) {
+				if ( _.isNull( value ) ) {
+					return;
+				}
+
+				resp.users[index].id = value.id;
+				resp.users[index].avatar = value.avatar;
+				resp.users[index].name = value.name;
+			} );
+
+			if ( ! _.isUndefined( resp.meta ) ) {
+				this.options.page = resp.meta.page;
+				this.options.total_page = resp.meta.total_page;
+			}
+
+			return resp.users;
+		}
+
+	} );
+
+	// Extend wp.Backbone.View with .prepare() and .inject()
+	bp.Nouveau.GroupInvites.View = bp.Backbone.View.extend( {
+		inject: function( selector ) {
+			this.render();
+			$(selector).html( this.el );
+			this.views.ready();
+		},
+
+		prepare: function() {
+			if ( ! _.isUndefined( this.model ) && _.isFunction( this.model.toJSON ) ) {
+				return this.model.toJSON();
+			} else {
+				return {};
+			}
+		}
+	} );
+
+	// Feedback view
+	bp.Views.Feedback = bp.Nouveau.GroupInvites.View.extend( {
+		tagName   : 'div',
+		className : 'bp-invites-feedback',
+		template  : bp.template( 'bp-group-invites-feedback' ),
+
+		initialize: function() {
+			this.model = new Backbone.Model( {
+				type: this.options.type || 'info',
+				message: this.options.value
+			} );
+		}
+	} );
+
+	bp.Views.invitesNav = bp.Nouveau.GroupInvites.View.extend( {
+		tagName: 'ul',
+		className: 'subnav',
+
+		events: {
+			'click .bp-invites-nav-item' : 'toggleView'
+		},
+
+		initialize: function() {
+			this.collection.on( 'add', this.outputNav, this );
+			this.collection.on( 'change:hide', this.showHideNavItem, this );
+
+			window.onbeforeunload = _.bind( this.confirmQuit, this );
+		},
+
+		outputNav: function( nav ) {
+			/**
+			 * The delete nav is not added if no avatar
+			 * is set for the object
+			 */
+			if ( 1 === nav.get( 'hide' ) ) {
+				return;
+			}
+
+			this.views.add( new bp.Views.invitesNavItem( { model: nav } ) );
+		},
+
+		showHideNavItem: function( item ) {
+			var isRendered = null;
+
+			/**
+			 * Loop in views to show/hide the nav item
+			 * BuddyPress is only using this for the delete nav
+			 */
+			_.each( this.views._views[''], function( view ) {
+				if ( 1 === view.model.get( 'hide' ) ) {
+					view.remove();
+				}
+
+				// Check to see if the nav is not already rendered
+				if ( item.get( 'id' ) === view.model.get( 'id' ) ) {
+					isRendered = true;
+				}
+			} );
+
+			// Add the Delete nav if not rendered
+			if ( ! _.isBoolean( isRendered ) ) {
+				item.set( 'invites_count', bp.Nouveau.GroupInvites.invites.length );
+				this.outputNav( item );
+			}
+		},
+
+		toggleView: function( event ) {
+			var target = $( event.target );
+			event.preventDefault();
+
+			if ( ! target.data( 'nav' ) && 'SPAN' === event.target.tagName ) {
+				target = $( event.target ).parent();
+			}
+
+			var current_nav_id = target.data( 'nav' );
+
+			_.each( this.collection.models, function( nav ) {
+				if ( nav.id === current_nav_id ) {
+					nav.set( 'active', 1 );
+
+					// Specific to the invites view
+					if ( 'invites' === nav.id ) {
+						this.trigger( 'bp-invites:confirm' );
+					} else {
+						this.trigger( 'bp-invites:loops', nav.id );
+					}
+
+				} else if ( 1 !== nav.get( 'hide' ) ) {
+					nav.set( 'active', 0 );
+				}
+			}, this );
+		},
+
+		confirmQuit: function() {
+			if ( bp.Nouveau.GroupInvites.invites && bp.Nouveau.GroupInvites.invites.length ) {
+				$( '[data-nav="invites"]' ).focus();
+
+				return false;
+			}
+		}
+	} );
+
+	bp.Views.invitesNavItem = bp.Nouveau.GroupInvites.View.extend( {
+		tagName  : 'li',
+		template : bp.template( 'bp-invites-nav' ),
+
+		initialize: function() {
+			if ( 1 === this.model.get( 'active' ) ) {
+				this.el.className += ' current';
+			}
+
+			if ( 'invites' === this.model.get( 'id' ) ) {
+				this.el.className += ' dynamic';
+			}
+
+			if ( 'invited' === this.model.get( 'id' ) ) {
+				this.el.className += ' pending';
+			}
+
+			this.model.on( 'change:active', this.toggleClass, this );
+			this.on( 'ready', this.updateCount, this );
+
+			bp.Nouveau.GroupInvites.invites.on( 'add', this.updateCount, this );
+			bp.Nouveau.GroupInvites.invites.on( 'remove', this.updateCount, this );
+		},
+
+		updateCount: function( user, invite ) {
+			if ( 'invites' !== this.model.get( 'id' ) ) {
+				return;
+			}
+
+			var span_count = _.isUndefined( invite ) ? this.model.get( 'invites_count' ) : invite.models.length;
+
+			if ( $( this.el ).find( 'span' ).length ) {
+				$( this.el ).find( 'span' ).html( span_count );
+			} else {
+				$( this.el ).find( 'a' ).append( $( '<span class="count"></span>' ).html( span_count ) );
+			}
+		},
+
+		toggleClass: function( model ) {
+			if ( 0 === model.get( 'active' ) ) {
+				$( this.el ).removeClass( 'current' );
+			} else {
+				$( this.el ).addClass( 'current' );
+			}
+		}
+	} );
+
+	bp.Views.Pagination = bp.Nouveau.GroupInvites.View.extend( {
+		tagName   : 'div',
+		className : 'last',
+		template  :  bp.template( 'bp-invites-paginate' )
+	} );
+
+	bp.Views.inviteFilters = bp.Nouveau.GroupInvites.View.extend( {
+		tagName: 'div',
+		template:  bp.template( 'bp-invites-filters' ),
+
+		events : {
+			'search #group_invites_search'      : 'resetSearchTerms',
+			'submit #group_invites_search_form' : 'setSearchTerms',
+			'click #bp-invites-next-page'       : 'nextPage',
+			'click #bp-invites-prev-page'       : 'prevPage'
+		},
+
+		initialize: function() {
+			this.model.on( 'change', this.filterUsers, this );
+			this.options.users.on( 'sync', this.addPaginatation, this );
+		},
+
+		addPaginatation: function( collection ) {
+			_.each( this.views._views[''], function( view ) {
+				view.remove();
+			} );
+
+			if ( 1 === collection.options.total_page ) {
+				return;
+			}
+
+			this.views.add( new bp.Views.Pagination( { model: new Backbone.Model( collection.options ) } ) );
+		},
+
+		filterUsers: function() {
+			bp.Nouveau.GroupInvites.displayFeedback( BP_Nouveau.group_invites.loading, 'loading' );
+
+			this.options.users.reset();
+
+			this.options.users.fetch( {
+				data    : _.pick( this.model.attributes, ['scope', 'search_terms', 'page'] ),
+				success : this.usersFiltered,
+				error   : this.usersFilterError
+			} );
+		},
+
+		usersFiltered: function() {
+			bp.Nouveau.GroupInvites.removeFeedback();
+		},
+
+		usersFilterError: function( collection, response ) {
+			bp.Nouveau.GroupInvites.displayFeedback( response.feedback, 'error' );
+		},
+
+		resetSearchTerms: function( event ) {
+			event.preventDefault();
+
+			if ( ! $( event.target ).val() ) {
+				$( event.target ).closest( 'form' ).submit();
+			} else {
+				$( event.target ).closest( 'form' ).find( '[type=submit]' ).addClass('bp-show');
+			}
+		},
+
+		setSearchTerms: function( event ) {
+			event.preventDefault();
+
+			this.model.set( {
+				search_terms : $( event.target ).find( 'input[type=search]' ).val() || '',
+				page         : 1
+			} );
+		},
+
+		nextPage: function( event ) {
+			event.preventDefault();
+
+			this.model.set( 'page', this.model.get( 'page' ) + 1 );
+		},
+
+		prevPage: function( event ) {
+			event.preventDefault();
+
+			this.model.set( 'page', this.model.get( 'page' ) - 1 );
+		}
+	} );
+
+	bp.Views.inviteUsers = bp.Nouveau.GroupInvites.View.extend( {
+		tagName   : 'ul',
+		className : 'item-list bp-list',
+		id        : 'members-list',
+
+		initialize: function() {
+			// Load users for the active view
+			this.requestUsers();
+
+			this.collection.on( 'reset', this.cleanContent, this );
+			this.collection.on( 'add', this.addUser, this );
+		},
+
+		requestUsers: function() {
+			this.collection.reset();
+
+			this.collection.fetch( {
+				data    : _.pick( this.options, 'scope' ),
+				success : this.usersFetched,
+				error   : this.usersFetchError
+			} );
+		},
+
+		usersFetched: function( collection, response ) {
+			bp.Nouveau.GroupInvites.displayFeedback( response.feedback, 'help' );
+		},
+
+		usersFetchError: function( collection, response ) {
+			var type = response.type || 'help';
+
+			bp.Nouveau.GroupInvites.displayFeedback( response.feedback, type );
+		},
+
+		cleanContent: function() {
+			_.each( this.views._views[''], function( view ) {
+				view.remove();
+			} );
+		},
+
+		addUser: function( user ) {
+			this.views.add( new bp.Views.inviteUser( { model: user } ) );
+		}
+	} );
+
+	bp.Views.inviteUser = bp.Nouveau.GroupInvites.View.extend( {
+		tagName  : 'li',
+		template : bp.template( 'bp-invites-users' ),
+
+		events: {
+			'click .group-add-remove-invite-button'    : 'toggleUser',
+			'click .group-remove-invite-button'        : 'removeInvite'
+		},
+
+		initialize: function() {
+			var invite = bp.Nouveau.GroupInvites.invites.get( this.model.get( 'id' ) );
+
+			if ( invite ) {
+				this.model.set( 'selected', true, { silent: true } );
+			}
+		},
+
+		render: function() {
+			if ( this.model.get( 'selected' ) ) {
+				this.el.className = 'selected';
+			} else {
+				this.el.className = '';
+			}
+
+			bp.Nouveau.GroupInvites.View.prototype.render.apply( this, arguments );
+		},
+
+		toggleUser: function( event ) {
+			event.preventDefault();
+
+			var selected = this.model.get( 'selected' );
+
+			if ( false === selected ) {
+				this.model.set( 'selected', true );
+			} else {
+				this.model.set( 'selected', false );
+
+				if ( ! bp.Nouveau.GroupInvites.invites.length  ) {
+					bp.Nouveau.GroupInvites.invites.reset();
+				}
+			}
+
+			// Rerender to update buttons.
+			this.render();
+		},
+
+		removeInvite: function( event ) {
+			event.preventDefault();
+
+			var collection = this.model.collection;
+
+			if ( ! collection.length ) {
+				return;
+			}
+
+			collection.sync( 'delete', this.model.get( 'id' ), {
+				success : _.bind( this.inviteRemoved, this ),
+				error   : _.bind( this.uninviteError, this )
+			} );
+		},
+
+		inviteRemoved: function( response ) {
+			var collection = this.model.collection;
+
+			if ( ! collection.length ) {
+				return;
+			}
+
+			collection.remove( this.model );
+			this.remove();
+
+			bp.Nouveau.GroupInvites.removeFeedback();
+
+			if ( false === response.has_invites ) {
+				bp.Nouveau.GroupInvites.displayFeedback( response.feedback, 'success' );
+
+				// Hide the invited nav
+				bp.Nouveau.GroupInvites.navItems.get( 'invited' ).set( { active: 0, hide: 1 } );
+			}
+		},
+
+		uninviteError: function( response ) {
+			bp.Nouveau.GroupInvites.displayFeedback( response.feedback, 'error' );
+		}
+	} );
+
+	bp.Views.invitesEditor = bp.Nouveau.GroupInvites.View.extend( {
+		tagName : 'div',
+		id      : 'send-invites-editor',
+
+		events: {
+			'click #bp-invites-send'  : 'sendInvites',
+			'click #bp-invites-reset' : 'clearForm'
+		},
+
+		initialize: function() {
+			this.views.add( new bp.Views.selectedUsers( { collection: this.collection } ) );
+			this.views.add( new bp.Views.invitesForm() );
+
+			this.collection.on( 'reset', this.cleanViews, this );
+		},
+
+		sendInvites: function( event ) {
+			event.preventDefault();
+
+			$( this.el ).addClass( 'bp-hide' );
+
+			bp.Nouveau.GroupInvites.displayFeedback( BP_Nouveau.group_invites.invites_sending, 'info' );
+
+			this.collection.sync( 'create', _.pluck( this.collection.models, 'id' ), {
+				success : _.bind( this.invitesSent, this ),
+				error   : _.bind( this.invitesError, this ),
+				data    : {
+					message: $( this.el ).find( 'textarea' ).val()
+				}
+			} );
+		},
+
+		invitesSent: function( response ) {
+			this.collection.reset();
+
+			bp.Nouveau.GroupInvites.displayFeedback( response.feedback, 'success' );
+
+			// Display the pending invites
+			if ( 1 === bp.Nouveau.GroupInvites.navItems.get( 'invited' ).get( 'hide' ) && ! BP_Nouveau.group_invites.is_group_create ) {
+				bp.Nouveau.GroupInvites.navItems.get( 'invited' ).set( { active: 0, hide: 0 } );
+			}
+		},
+
+		invitesError: function( response ) {
+			var type = response.type || 'help';
+
+			$( this.el ).removeClass( 'bp-hide' );
+
+			bp.Nouveau.GroupInvites.displayFeedback( response.feedback, type );
+
+			if ( ! _.isUndefined( response.users ) ) {
+				// Display the pending invites
+				if ( 1 === bp.Nouveau.GroupInvites.navItems.get( 'invited' ).get( 'hide' ) && response.users.length < this.collection.length ) {
+					bp.Nouveau.GroupInvites.navItems.get( 'invited' ).set( { active: 0, hide: 0 } );
+				}
+
+				_.each( this.collection.models, function( invite ) {
+					// If not an error, remove from the selection
+					if ( -1 === _.indexOf( response.users, invite.get( 'id' ) ) ) {
+						invite.set( 'selected', false );
+					}
+				}, this );
+			}
+		},
+
+		clearForm: function( event ) {
+			event.preventDefault();
+
+			this.collection.reset();
+		},
+
+		cleanViews: function() {
+			_.each( this.views._views[''], function( view ) {
+				view.remove();
+			} );
+
+			bp.Nouveau.GroupInvites.displayFeedback( BP_Nouveau.group_invites.invites_form_reset, 'success' );
+		}
+	} );
+
+	bp.Views.invitesForm = bp.Nouveau.GroupInvites.View.extend( {
+		tagName  : 'div',
+		id       : 'bp-send-invites-form',
+		template :  bp.template( 'bp-invites-form' )
+	} );
+
+	bp.Views.selectedUsers = bp.Nouveau.GroupInvites.View.extend( {
+		tagName : 'ul',
+
+		initialize: function() {
+			this.cleanContent();
+
+			_.each( this.collection.models, function( invite ) {
+				this.views.add( new bp.Views.selectedUser( { model: invite } ) );
+			}, this );
+		},
+
+		cleanContent: function() {
+			_.each( this.views._views[''], function( view ) {
+				view.remove();
+			} );
+		}
+	} );
+
+	bp.Views.selectedUser = bp.Nouveau.GroupInvites.View.extend( {
+		tagName  : 'li',
+		template : bp.template( 'bp-invites-selection' ),
+
+		events: {
+			click : 'removeSelection'
+		},
+
+		initialize: function() {
+			this.model.on( 'change:selected', this.removeView, this );
+
+			// Build the BP Tooltip.
+			if ( ! this.model.get( 'uninviteTooltip' ) ) {
+				this.model.set( 'uninviteTooltip',
+					BP_Nouveau.group_invites.removeUserInvite.replace( '%s', this.model.get( 'name' ) ),
+					{ silent: true }
+				);
+			}
+
+			this.el.id = 'uninvite-user-' + this.model.get( 'id' );
+		},
+
+		removeSelection: function( event ) {
+			event.preventDefault();
+
+			this.model.set( 'selected', false );
+		},
+
+		removeView: function( model ) {
+			if ( false !== model.get( 'selected' ) ) {
+				return;
+			}
+
+			this.remove();
+		}
+	} );
+
+	// Launch BP Nouveau Groups
+	bp.Nouveau.GroupInvites.start();
+
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-messages.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-messages.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-messages.js	(revision 12265)
@@ -0,0 +1,1427 @@
+/* global wp, bp, BP_Nouveau, _, Backbone, tinymce, tinyMCE */
+/* jshint devel: true */
+/* @version 3.1.0 */
+window.wp = window.wp || {};
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	_.extend( bp, _.pick( wp, 'Backbone', 'ajax', 'template' ) );
+
+	bp.Models      = bp.Models || {};
+	bp.Collections = bp.Collections || {};
+	bp.Views       = bp.Views || {};
+
+	bp.Nouveau = bp.Nouveau || {};
+
+	/**
+	 * [Nouveau description]
+	 * @type {Object}
+	 */
+	bp.Nouveau.Messages = {
+		/**
+		 * [start description]
+		 * @return {[type]} [description]
+		 */
+		start: function() {
+			this.views    = new Backbone.Collection();
+			this.threads  = new bp.Collections.Threads();
+			this.messages = new bp.Collections.Messages();
+			this.router   = new bp.Nouveau.Messages.Router();
+			this.box      = 'inbox';
+
+			this.setupNav();
+
+			Backbone.history.start( {
+				pushState: true,
+				root: BP_Nouveau.messages.rootUrl
+			} );
+		},
+
+		setupNav: function() {
+			var self = this;
+
+			// First adapt the compose nav
+			$( '#compose-personal-li' ).addClass( 'last' );
+
+			// Then listen to nav click and load the appropriate view
+			$( '#subnav a' ).on( 'click', function( event ) {
+				event.preventDefault();
+
+				var view_id = $( event.target ).prop( 'id' );
+
+				// Remove the editor to be sure it will be added dynamically later
+				self.removeTinyMCE();
+
+				// The compose view is specific (toggle behavior)
+				if ( 'compose' === view_id ) {
+					// If it exists, it means the user wants to remove it
+					if ( ! _.isUndefined( self.views.get( 'compose' ) ) ) {
+						var form = self.views.get( 'compose' );
+						form.get( 'view' ).remove();
+						self.views.remove( { id: 'compose', view: form } );
+
+						// Back to inbox
+						if ( 'single' === self.box ) {
+							self.box = 'inbox';
+						}
+
+						// Navigate back to current box
+						self.router.navigate( self.box + '/', { trigger: true } );
+
+					// Otherwise load it
+					} else {
+						self.router.navigate( 'compose/', { trigger: true } );
+					}
+
+				// Other views are classic.
+				} else {
+
+					if ( self.box !== view_id || ! _.isUndefined( self.views.get( 'compose' ) ) ) {
+						self.clearViews();
+
+						self.router.navigate( view_id + '/', { trigger: true } );
+					}
+				}
+			} );
+		},
+
+		removeTinyMCE: function() {
+			if ( typeof tinymce !== 'undefined' ) {
+				var editor = tinymce.get( 'message_content' );
+
+				if ( editor !== null ) {
+					tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, 'message_content' );
+				}
+			}
+		},
+
+		tinyMCEinit: function() {
+			if ( typeof window.tinyMCE === 'undefined' || window.tinyMCE.activeEditor === null || typeof window.tinyMCE.activeEditor === 'undefined' ) {
+				return;
+			} else {
+				$( window.tinyMCE.activeEditor.contentDocument.activeElement )
+					.atwho( 'setIframe', $( '#message_content_ifr' )[0] )
+					.bp_mentions( {
+						data: [],
+						suffix: ' '
+					} );
+			}
+		},
+
+		removeFeedback: function() {
+			var feedback;
+
+			if ( ! _.isUndefined( this.views.get( 'feedback' ) ) ) {
+				feedback = this.views.get( 'feedback' );
+				feedback.get( 'view' ).remove();
+				this.views.remove( { id: 'feedback', view: feedback } );
+			}
+		},
+
+		displayFeedback: function( message, type ) {
+			var feedback;
+
+			// Make sure to remove the feedbacks
+			this.removeFeedback();
+
+			if ( ! message ) {
+				return;
+			}
+
+			feedback = new bp.Views.Feedback( {
+				value: message,
+				type:  type || 'info'
+			} );
+
+			this.views.add( { id: 'feedback', view: feedback } );
+
+			feedback.inject( '.bp-messages-feedback' );
+		},
+
+		clearViews: function() {
+			// Clear views
+			if ( ! _.isUndefined( this.views.models ) ) {
+				_.each( this.views.models, function( model ) {
+					model.get( 'view' ).remove();
+				}, this );
+
+				this.views.reset();
+			}
+		},
+
+		composeView: function() {
+			// Remove all existing views.
+			this.clearViews();
+
+			// Create the loop view
+			var form = new bp.Views.messageForm( {
+				model: new bp.Models.Message()
+			} );
+
+			this.views.add( { id: 'compose', view: form } );
+
+			form.inject( '.bp-messages-content' );
+		},
+
+		threadsView: function() {
+			// Activate the appropriate nav
+			$( '#subnav ul li' ).each( function( l, li ) {
+				$( li ).removeClass( 'current selected' );
+			} );
+			$( '#subnav a#' + this.box ).closest( 'li' ).addClass( 'current selected' );
+
+			// Create the loop view
+			var threads_list = new bp.Views.userThreads( { collection: this.threads, box: this.box } );
+
+			this.views.add( { id: 'threads', view: threads_list } );
+
+			threads_list.inject( '.bp-messages-content' );
+
+			// Attach filters
+			this.displayFilters( this.threads );
+		},
+
+		displayFilters: function( collection ) {
+			var filters_view;
+
+			// Create the model
+			this.filters = new Backbone.Model( {
+				'page'         : 1,
+				'total_page'   : 0,
+				'search_terms' : '',
+				'box'          : this.box
+			} );
+
+			// Use it in the filters viex
+			filters_view = new bp.Views.messageFilters( { model: this.filters, threads: collection } );
+
+			this.views.add( { id: 'filters', view: filters_view } );
+
+			filters_view.inject( '.bp-messages-filters' );
+		},
+
+		singleView: function( thread ) {
+			// Remove all existing views.
+			this.clearViews();
+
+			this.box = 'single';
+
+			// Create the single thread view
+			var single_thread = new bp.Views.userMessages( { collection: this.messages, thread: thread } );
+
+			this.views.add( { id: 'single', view: single_thread } );
+
+			single_thread.inject( '.bp-messages-content' );
+		}
+	};
+
+	bp.Models.Message = Backbone.Model.extend( {
+		defaults: {
+			send_to         : [],
+			subject         : '',
+			message_content : '',
+			meta            : {}
+		},
+
+		sendMessage: function() {
+			if ( true === this.get( 'sending' ) ) {
+				return;
+			}
+
+			this.set( 'sending', true, { silent: true } );
+
+			var sent = bp.ajax.post( 'messages_send_message', _.extend(
+				{
+					nonce: BP_Nouveau.messages.nonces.send
+				},
+				this.attributes
+			) );
+
+			this.set( 'sending', false, { silent: true } );
+
+			return sent;
+		}
+	} );
+
+	bp.Models.Thread = Backbone.Model.extend( {
+		defaults: {
+			id            : 0,
+			message_id    : 0,
+			subject       : '',
+			excerpt       : '',
+			content       : '',
+			unread        : true,
+			sender_name   : '',
+			sender_link   : '',
+			sender_avatar : '',
+			count         : 0,
+			date          : 0,
+			display_date  : '',
+			recipients    : []
+		},
+
+		updateReadState: function( options ) {
+			options = options || {};
+			options.data = _.extend(
+				_.pick( this.attributes, ['id', 'message_id'] ),
+				{
+					action : 'messages_thread_read',
+					nonce  : BP_Nouveau.nonces.messages
+				}
+			);
+
+			return bp.ajax.send( options );
+		}
+	} );
+
+	bp.Models.messageThread = Backbone.Model.extend( {
+		defaults: {
+			id            : 0,
+			content       : '',
+			sender_id     : 0,
+			sender_name   : '',
+			sender_link   : '',
+			sender_avatar : '',
+			date          : 0,
+			display_date  : ''
+		}
+	} );
+
+	bp.Collections.Threads = Backbone.Collection.extend( {
+		model: bp.Models.Thread,
+
+		initialize : function() {
+			this.options = { page: 1, total_page: 0 };
+		},
+
+		sync: function( method, model, options ) {
+			options         = options || {};
+			options.context = this;
+			options.data    = options.data || {};
+
+			// Add generic nonce
+			options.data.nonce = BP_Nouveau.nonces.messages;
+
+			if ( 'read' === method ) {
+				options.data = _.extend( options.data, {
+					action: 'messages_get_user_message_threads'
+				} );
+
+				return bp.ajax.send( options );
+			}
+		},
+
+		parse: function( resp ) {
+
+			if ( ! _.isArray( resp.threads ) ) {
+				resp.threads = [resp.threads];
+			}
+
+			_.each( resp.threads, function( value, index ) {
+				if ( _.isNull( value ) ) {
+					return;
+				}
+
+				resp.threads[index].id            = value.id;
+				resp.threads[index].message_id    = value.message_id;
+				resp.threads[index].subject       = value.subject;
+				resp.threads[index].excerpt       = value.excerpt;
+				resp.threads[index].content       = value.content;
+				resp.threads[index].unread        = value.unread;
+				resp.threads[index].sender_name   = value.sender_name;
+				resp.threads[index].sender_link   = value.sender_link;
+				resp.threads[index].sender_avatar = value.sender_avatar;
+				resp.threads[index].count         = value.count;
+				resp.threads[index].date          = new Date( value.date );
+				resp.threads[index].display_date  = value.display_date;
+				resp.threads[index].recipients    = value.recipients;
+				resp.threads[index].star_link     = value.star_link;
+				resp.threads[index].is_starred    = value.is_starred;
+			} );
+
+			if ( ! _.isUndefined( resp.meta ) ) {
+				this.options.page       = resp.meta.page;
+				this.options.total_page = resp.meta.total_page;
+			}
+
+			if ( bp.Nouveau.Messages.box ) {
+				this.options.box = bp.Nouveau.Messages.box;
+			}
+
+			if ( ! _.isUndefined( resp.extraContent ) ) {
+				_.extend( this.options, _.pick( resp.extraContent, [
+					'beforeLoop',
+					'afterLoop'
+				] ) );
+			}
+
+			return resp.threads;
+		},
+
+		doAction: function( action, ids, options ) {
+			options         = options || {};
+			options.context = this;
+			options.data    = options.data || {};
+
+			options.data = _.extend( options.data, {
+				action: 'messages_' + action,
+				nonce : BP_Nouveau.nonces.messages,
+				id    : ids
+			} );
+
+			return bp.ajax.send( options );
+		}
+	} );
+
+	bp.Collections.Messages = Backbone.Collection.extend( {
+		model: bp.Models.messageThread,
+		options: {},
+
+		sync: function( method, model, options ) {
+			options         = options || {};
+			options.context = this;
+			options.data    = options.data || {};
+
+			// Add generic nonce
+			options.data.nonce = BP_Nouveau.nonces.messages;
+
+			if ( 'read' === method ) {
+				options.data = _.extend( options.data, {
+					action: 'messages_get_thread_messages'
+				} );
+
+				return bp.ajax.send( options );
+			}
+
+			if ( 'create' === method ) {
+				options.data = _.extend( options.data, {
+					action : 'messages_send_reply',
+					nonce  : BP_Nouveau.messages.nonces.send
+				}, model || {} );
+
+				return bp.ajax.send( options );
+			}
+		},
+
+		parse: function( resp ) {
+
+			if ( ! _.isArray( resp.messages ) ) {
+				resp.messages = [resp.messages];
+			}
+
+			_.each( resp.messages, function( value, index ) {
+				if ( _.isNull( value ) ) {
+					return;
+				}
+
+				resp.messages[index].id            = value.id;
+				resp.messages[index].content       = value.content;
+				resp.messages[index].sender_id     = value.sender_id;
+				resp.messages[index].sender_name   = value.sender_name;
+				resp.messages[index].sender_link   = value.sender_link;
+				resp.messages[index].sender_avatar = value.sender_avatar;
+				resp.messages[index].date          = new Date( value.date );
+				resp.messages[index].display_date  = value.display_date;
+				resp.messages[index].star_link     = value.star_link;
+				resp.messages[index].is_starred    = value.is_starred;
+			} );
+
+			if ( ! _.isUndefined( resp.thread ) ) {
+				this.options.thread_id      = resp.thread.id;
+				this.options.thread_subject = resp.thread.subject;
+				this.options.recipients     = resp.thread.recipients;
+			}
+
+			return resp.messages;
+		}
+	} );
+
+	// Extend wp.Backbone.View with .prepare() and .inject()
+	bp.Nouveau.Messages.View = bp.Backbone.View.extend( {
+		inject: function( selector ) {
+			this.render();
+			$(selector).html( this.el );
+			this.views.ready();
+		},
+
+		prepare: function() {
+			if ( ! _.isUndefined( this.model ) && _.isFunction( this.model.toJSON ) ) {
+				return this.model.toJSON();
+			} else {
+				return {};
+			}
+		}
+	} );
+
+	// Feedback view
+	bp.Views.Feedback = bp.Nouveau.Messages.View.extend( {
+		tagName: 'div',
+		className: 'bp-messages bp-user-messages-feedback',
+		template  : bp.template( 'bp-messages-feedback' ),
+
+		initialize: function() {
+			this.model = new Backbone.Model( {
+				type: this.options.type || 'info',
+				message: this.options.value
+			} );
+		}
+	} );
+
+	// Hook view
+	bp.Views.Hook = bp.Nouveau.Messages.View.extend( {
+		tagName: 'div',
+		template  : bp.template( 'bp-messages-hook' ),
+
+		initialize: function() {
+			this.model = new Backbone.Model( {
+				extraContent: this.options.extraContent
+			} );
+
+			this.el.className = 'bp-messages-hook';
+
+			if ( this.options.className ) {
+				this.el.className += ' ' + this.options.className;
+			}
+		}
+	} );
+
+	bp.Views.messageEditor = bp.Nouveau.Messages.View.extend( {
+		template  : bp.template( 'bp-messages-editor' ),
+
+		initialize: function() {
+			this.on( 'ready', this.activateTinyMce, this );
+		},
+
+		activateTinyMce: function() {
+			if ( typeof tinymce !== 'undefined' ) {
+				tinymce.EditorManager.execCommand( 'mceAddEditor', true, 'message_content' );
+			}
+		}
+	} );
+
+	bp.Views.messageForm = bp.Nouveau.Messages.View.extend( {
+		tagName   : 'form',
+		id        : 'send_message_form',
+		className : 'standard-form',
+		template  : bp.template( 'bp-messages-form' ),
+
+		events: {
+			'click #bp-messages-send'  : 'sendMessage',
+			'click #bp-messages-reset' : 'resetForm'
+		},
+
+		initialize: function() {
+			// Clone the model to set the resetted one
+			this.resetModel = this.model.clone();
+
+			// Add the editor view
+			this.views.add( '#bp-message-content', new bp.Views.messageEditor() );
+
+			this.model.on( 'change', this.resetFields, this );
+
+			// Activate bp_mentions
+			this.on( 'ready', this.addMentions, this );
+		},
+
+		addMentions: function() {
+			var sendToInput = $( this.el ).find( '#send-to-input' ),
+			    mention = bp.Nouveau.getLinkParams( null, 'r' ) || null;
+
+			// Add autocomplete to send_to field
+			sendToInput.bp_mentions( {
+				data: [],
+				suffix: ' '
+			} );
+
+			// Check for mention
+			if ( ! _.isNull( mention ) ) {
+				sendToInput.val( '@' + _.escape( mention ) + ' ' );
+				sendToInput.focus();
+			}
+		},
+
+		resetFields: function( model ) {
+			// Clean inputs
+			_.each( model.previousAttributes(), function( value, input ) {
+				if ( 'message_content' === input ) {
+					// tinyMce
+					if ( undefined !== tinyMCE.activeEditor && null !== tinyMCE.activeEditor ) {
+						tinyMCE.activeEditor.setContent( '' );
+					}
+
+				// All except meta or empty value
+				} else if ( 'meta' !== input && false !== value ) {
+					$( 'input[name="' + input + '"]' ).val( '' );
+				}
+			} );
+
+			// Listen to this to eventually reset your custom inputs.
+			$( this.el ).trigger( 'message:reset', _.pick( model.previousAttributes(), 'meta' ) );
+		},
+
+		sendMessage: function( event ) {
+			var meta = {}, errors = [], self = this;
+			event.preventDefault();
+
+			bp.Nouveau.Messages.removeFeedback();
+
+			// Set the content and meta
+			_.each( this.$el.serializeArray(), function( pair ) {
+				pair.name = pair.name.replace( '[]', '' );
+
+				// Group extra fields in meta
+				if ( -1 === _.indexOf( ['send_to', 'subject', 'message_content'], pair.name ) ) {
+					if ( _.isUndefined( meta[ pair.name ] ) ) {
+						meta[ pair.name ] = pair.value;
+					} else {
+						if ( ! _.isArray( meta[ pair.name ] ) ) {
+							meta[ pair.name ] = [ meta[ pair.name ] ];
+						}
+
+						meta[ pair.name ].push( pair.value );
+					}
+
+				// Prepare the core model
+				} else {
+					// Send to
+					if ( 'send_to' === pair.name ) {
+						var usernames = pair.value.match( /(^|[^@\w\-])@([a-zA-Z0-9_\-]{1,50})\b/g );
+
+						if ( ! usernames ) {
+							errors.push( 'send_to' );
+						} else {
+							usernames = usernames.map( function( username ) {
+								username = $.trim( username );
+								return username;
+							} );
+
+							if ( ! usernames || ! $.isArray( usernames ) ) {
+								errors.push( 'send_to' );
+							}
+
+							this.model.set( 'send_to', usernames, { silent: true } );
+						}
+
+					// Subject and content
+					} else {
+						// Message content
+						if ( 'message_content' === pair.name && undefined !== tinyMCE.activeEditor ) {
+							pair.value = tinyMCE.activeEditor.getContent();
+						}
+
+						if ( ! pair.value ) {
+							errors.push( pair.name );
+						} else {
+							this.model.set( pair.name, pair.value, { silent: true } );
+						}
+					}
+				}
+
+			}, this );
+
+			if ( errors.length ) {
+				var feedback = '';
+				_.each( errors, function( e ) {
+					feedback += '<div class="bp-feedback error"><span class="bp-icon" aria-hidden="true"></span><p>'+BP_Nouveau.messages.errors[ e ] + '</p></div>';
+				} );
+
+				bp.Nouveau.Messages.displayFeedback( feedback, 'error' );
+				return;
+			}
+
+			// Set meta
+			this.model.set( 'meta', meta, { silent: true } );
+
+			// Send the message.
+			this.model.sendMessage().done( function( response ) {
+				// Reset the model
+				self.model.set( self.resetModel );
+
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+
+				// Remove tinyMCE
+				bp.Nouveau.Messages.removeTinyMCE();
+
+				// Remove the form view
+				var form = bp.Nouveau.Messages.views.get( 'compose' );
+				form.get( 'view' ).remove();
+				bp.Nouveau.Messages.views.remove( { id: 'compose', view: form } );
+
+				bp.Nouveau.Messages.router.navigate( 'sentbox/', { trigger: true } );
+			} ).fail( function( response ) {
+				if ( response.feedback ) {
+					bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+				}
+			} );
+		},
+
+		resetForm: function( event ) {
+			event.preventDefault();
+
+			this.model.set( this.resetModel );
+		}
+	} );
+
+	bp.Views.userThreads = bp.Nouveau.Messages.View.extend( {
+		tagName   : 'div',
+
+		events: {
+			'click .subject' : 'changePreview'
+		},
+
+		initialize: function() {
+			var Views = [
+				new bp.Nouveau.Messages.View( { tagName: 'ul', id: 'message-threads', className: 'message-lists' } ),
+				new bp.Views.previewThread( { collection: this.collection } )
+			];
+
+			_.each( Views, function( view ) {
+				this.views.add( view );
+			}, this );
+
+			// Load threads for the active view
+			this.requestThreads();
+
+			this.collection.on( 'reset', this.cleanContent, this );
+			this.collection.on( 'add', this.addThread, this );
+		},
+
+		requestThreads: function() {
+			this.collection.reset();
+
+			bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.loading, 'loading' );
+
+			this.collection.fetch( {
+				data    : _.pick( this.options, 'box' ),
+				success : _.bind( this.threadsFetched, this ),
+				error   : this.threadsFetchError
+			} );
+		},
+
+		threadsFetched: function() {
+			bp.Nouveau.Messages.removeFeedback();
+
+			// Display the bp_after_member_messages_loop hook.
+			if ( this.collection.options.afterLoop ) {
+				this.views.add( new bp.Views.Hook( { extraContent: this.collection.options.afterLoop, className: 'after-messages-loop' } ), { at: 1 } );
+			}
+
+			// Display the bp_before_member_messages_loop hook.
+			if ( this.collection.options.beforeLoop ) {
+				this.views.add( new bp.Views.Hook( { extraContent: this.collection.options.beforeLoop, className: 'before-messages-loop' } ), { at: 0 } );
+			}
+
+			// Inform the user about how to use the UI.
+			bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.howto, 'info' );
+		},
+
+		threadsFetchError: function( collection, response ) {
+			bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+		},
+
+		cleanContent: function() {
+			_.each( this.views._views['#message-threads'], function( view ) {
+				view.remove();
+			} );
+		},
+
+		addThread: function( thread ) {
+			var selected = this.collection.findWhere( { active: true } );
+
+			if ( _.isUndefined( selected ) ) {
+				thread.set( 'active', true );
+			}
+
+			this.views.add( '#message-threads', new bp.Views.userThread( { model: thread } ) );
+		},
+
+		setActiveThread: function( active ) {
+			if ( ! active ) {
+				return;
+			}
+
+			_.each( this.collection.models, function( thread ) {
+				if ( thread.id === active ) {
+					thread.set( 'active', true );
+				} else {
+					thread.unset( 'active' );
+				}
+			}, this );
+		},
+
+		changePreview: function( event ) {
+			var target = $( event.currentTarget );
+
+			event.preventDefault();
+			bp.Nouveau.Messages.removeFeedback();
+
+			// If the click is done on an active conversation, open it.
+			if ( target.closest( '.thread-item' ).hasClass( 'selected' ) ) {
+				bp.Nouveau.Messages.router.navigate(
+					'view/' + target.closest( '.thread-content' ).data( 'thread-id' ) + '/',
+					{ trigger: true }
+				);
+
+			// Otherwise activate the conversation and display its preview.
+			} else {
+				this.setActiveThread( target.closest( '.thread-content' ).data( 'thread-id' ) );
+
+				$( '.message-action-view' ).focus();
+			}
+		}
+	} );
+
+	bp.Views.userThread = bp.Nouveau.Messages.View.extend( {
+		tagName   : 'li',
+		template  : bp.template( 'bp-messages-thread' ),
+		className : 'thread-item',
+
+		events: {
+			'click .message-check' : 'singleSelect'
+		},
+
+		initialize: function() {
+			if ( this.model.get( 'active' ) ) {
+				this.el.className += ' selected';
+			}
+
+			if ( this.model.get( 'unread' ) ) {
+				this.el.className += ' unread';
+			}
+
+			if ( 'sentbox' === bp.Nouveau.Messages.box ) {
+				var recipientsCount = this.model.get( 'recipients' ).length, toOthers = '';
+
+				if ( 2 === recipientsCount ) {
+					toOthers = BP_Nouveau.messages.toOthers.one;
+				} else if ( 2 < recipientsCount ) {
+					toOthers = BP_Nouveau.messages.toOthers.more.replace( '%d', Number( recipientsCount - 1 ) );
+				}
+
+				this.model.set( {
+					recipientsCount: recipientsCount,
+					toOthers: toOthers
+				}, { silent: true } );
+			} else if ( this.model.get( 'recipientsCount' )  ) {
+				this.model.unset( 'recipientsCount', { silent: true } );
+			}
+
+			this.model.on( 'change:active', this.toggleClass, this );
+			this.model.on( 'change:unread', this.updateReadState, this );
+			this.model.on( 'change:checked', this.bulkSelect, this );
+			this.model.on( 'remove', this.cleanView, this );
+		},
+
+		toggleClass: function( model ) {
+			if ( true === model.get( 'active' ) ) {
+				$( this.el ).addClass( 'selected' );
+			} else {
+				$( this.el ).removeClass( 'selected' );
+			}
+		},
+
+		updateReadState: function( model, state ) {
+			if ( false === state ) {
+				$( this.el ).removeClass( 'unread' );
+			} else {
+				$( this.el ).addClass( 'unread' );
+			}
+		},
+
+		bulkSelect: function( model ) {
+			if ( $( '#bp-message-thread-' + model.get( 'id' ) ).length ) {
+				$( '#bp-message-thread-' + model.get( 'id' ) ).prop( 'checked',model.get( 'checked' ) );
+			}
+		},
+
+		singleSelect: function( event ) {
+			var isChecked = $( event.currentTarget ).prop( 'checked' );
+
+			// To avoid infinite loops
+			this.model.set( 'checked', isChecked, { silent: true } );
+
+			var hasChecked = false;
+
+			_.each( this.model.collection.models, function( model ) {
+				if ( true === model.get( 'checked' ) ) {
+					hasChecked = true;
+				}
+			} );
+
+			if ( hasChecked ) {
+				$( '#user-messages-bulk-actions' ).closest( '.bulk-actions-wrap' ).removeClass( 'bp-hide' );
+
+				// Inform the user about how to use the bulk actions.
+				bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.howtoBulk, 'info' );
+			} else {
+				$( '#user-messages-bulk-actions' ).closest( '.bulk-actions-wrap' ).addClass( 'bp-hide' );
+
+				bp.Nouveau.Messages.removeFeedback();
+			}
+		},
+
+		cleanView: function() {
+			this.views.view.remove();
+		}
+	} );
+
+	bp.Views.previewThread = bp.Nouveau.Messages.View.extend( {
+		tagName: 'div',
+		id: 'thread-preview',
+		template  : bp.template( 'bp-messages-preview' ),
+
+		events: {
+			'click .actions button' : 'doAction',
+			'click .actions a'      : 'doAction'
+		},
+
+		initialize: function() {
+			this.collection.on( 'change:active', this.setPreview, this );
+			this.collection.on( 'change:is_starred', this.updatePreview, this );
+			this.collection.on( 'reset', this.emptyPreview, this );
+			this.collection.on( 'remove', this.emptyPreview, this );
+		},
+
+		render: function() {
+			// Only render if we have some content to render
+			if ( _.isUndefined( this.model ) || true !== this.model.get( 'active' ) ) {
+				return;
+			}
+
+			bp.Nouveau.Messages.View.prototype.render.apply( this, arguments );
+		},
+
+		setPreview: function( model ) {
+			var self = this;
+
+			this.model = model;
+
+			if ( true === model.get( 'unread' ) ) {
+				this.model.updateReadState().done( function() {
+					self.model.set( 'unread', false );
+				} );
+			}
+
+			this.render();
+		},
+
+		updatePreview: function( model ) {
+			if ( true === model.get( 'active' ) ) {
+				this.render();
+			}
+		},
+
+		emptyPreview: function() {
+			$( this.el ).html( '' );
+		},
+
+		doAction: function( event ) {
+			var action = $( event.currentTarget ).data( 'bp-action' ), self = this, options = {}, mid,
+			    feedback = BP_Nouveau.messages.doingAction;
+
+			if ( ! action ) {
+				return event;
+			}
+
+			event.preventDefault();
+
+			var model = this.collection.findWhere( { active: true } );
+
+			if ( ! model.get( 'id' ) ) {
+				return;
+			}
+
+			mid = model.get( 'id' );
+
+			// Open the full conversation
+			if ( 'view' === action ) {
+				bp.Nouveau.Messages.router.navigate(
+					'view/' + mid + '/',
+					{ trigger: true }
+				);
+
+				return;
+
+			// Star/Unstar actions needs to use a specific id and nonce.
+			} else if ( 'star' === action || 'unstar' === action ) {
+				options.data = {
+					'star_nonce' : model.get( 'star_nonce' )
+				};
+
+				mid = model.get( 'starred_id' );
+			}
+
+			if ( ! _.isUndefined( feedback[ action ] ) ) {
+				bp.Nouveau.Messages.displayFeedback( feedback[ action ], 'loading' );
+			}
+
+			this.collection.doAction( action, mid, options ).done( function( response ) {
+				// Remove previous feedback.
+				bp.Nouveau.Messages.removeFeedback();
+
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+
+				if ( 'delete' === action || ( 'starred' === self.collection.options.box && 'unstar' === action ) ) {
+					// Remove from the list of messages
+					self.collection.remove( model.get( 'id' ) );
+
+					// And Requery
+					self.collection.fetch( {
+						data : _.pick( self.collection.options, ['box', 'search_terms', 'page'] )
+					} );
+				} else if ( 'unstar' === action || 'star' === action ) {
+					// Update the model attributes--updates the star icon.
+					_.each( response.messages, function( updated ) {
+						model.set( updated );
+					} );
+					model.set( _.first( response.messages ) );
+				} else if ( response.messages ) {
+					model.set( _.first( response.messages ) );
+				}
+			} ).fail( function( response ) {
+				// Remove previous feedback.
+				bp.Nouveau.Messages.removeFeedback();
+
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+			} );
+		}
+	} );
+
+	bp.Views.Pagination = bp.Nouveau.Messages.View.extend( {
+		tagName   : 'li',
+		className : 'last filter',
+		template  :  bp.template( 'bp-messages-paginate' )
+	} );
+
+	bp.Views.BulkActions = bp.Nouveau.Messages.View.extend( {
+		tagName   : 'div',
+		template  :  bp.template( 'bp-bulk-actions' ),
+
+		events : {
+			'click #user_messages_select_all' : 'bulkSelect',
+			'click .bulk-apply'               : 'doBulkAction'
+		},
+
+		bulkSelect: function( event ) {
+			var isChecked = $( event.currentTarget ).prop( 'checked' );
+
+			if ( isChecked ) {
+				$( this.el ).find( '.bulk-actions-wrap' ).removeClass( 'bp-hide' ).addClass( 'bp-show' );
+
+				// Inform the user about how to use the bulk actions.
+				bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.howtoBulk, 'info' );
+			} else {
+				$( this.el ).find( '.bulk-actions-wrap' ).addClass( 'bp-hide' );
+
+				bp.Nouveau.Messages.removeFeedback();
+			}
+
+			_.each( this.collection.models, function( model ) {
+				model.set( 'checked', isChecked );
+			} );
+		},
+
+		doBulkAction: function( event ) {
+			var self = this, options = {}, ids, attr = 'id',
+			    feedback = BP_Nouveau.messages.doingAction;
+
+			event.preventDefault();
+
+			var action = $( '#user-messages-bulk-actions' ).val();
+
+			if ( ! action ) {
+				return;
+			}
+
+			var threads    = this.collection.where( { checked: true } );
+			var thread_ids = _.map( threads, function( model ) {
+				return model.get( 'id' );
+			} );
+
+			// Default to thread ids
+			ids = thread_ids;
+
+			// We need to get the starred ids
+			if ( 'star' === action || 'unstar' === action ) {
+				ids = _.map( threads, function( model ) {
+					return model.get( 'starred_id' );
+				} );
+
+				if ( 1 === ids.length ) {
+					options.data = {
+						'star_nonce' : threads[0].get( 'star_nonce' )
+					};
+				}
+
+				// Map with first message starred in the thread
+				attr = 'starred_id';
+			}
+
+			// Message id to Thread id
+			var m_tid = _.object( _.map( threads, function (model) {
+			    return [model.get( attr ), model.get( 'id' )];
+			} ) );
+
+			if ( ! _.isUndefined( feedback[ action ] ) ) {
+				bp.Nouveau.Messages.displayFeedback( feedback[ action ], 'loading' );
+			}
+
+			this.collection.doAction( action, ids, options ).done( function( response ) {
+				// Remove previous feedback.
+				bp.Nouveau.Messages.removeFeedback();
+
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+
+				if ( 'delete' === action || ( 'starred' === self.collection.options.box && 'unstar' === action ) ) {
+					// Remove from the list of messages
+					self.collection.remove( thread_ids );
+
+					// And Requery
+					self.collection.fetch( {
+						data : _.pick( self.collection.options, ['box', 'search_terms', 'page'] )
+					} );
+				} else if ( response.messages ) {
+					// Update each model attributes
+					_.each( response.messages, function( updated, id ) {
+						var model = self.collection.get( m_tid[id] );
+						model.set( updated );
+					} );
+				}
+			} ).fail( function( response ) {
+				// Remove previous feedback.
+				bp.Nouveau.Messages.removeFeedback();
+
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+			} );
+		}
+	} );
+
+	bp.Views.messageFilters = bp.Nouveau.Messages.View.extend( {
+		tagName: 'ul',
+		template:  bp.template( 'bp-messages-filters' ),
+
+		events : {
+			'search #user_messages_search'      : 'resetSearchTerms',
+			'submit #user_messages_search_form' : 'setSearchTerms',
+			'click #bp-messages-next-page'      : 'nextPage',
+			'click #bp-messages-prev-page'      : 'prevPage'
+		},
+
+		initialize: function() {
+			this.model.on( 'change', this.filterThreads, this );
+			this.options.threads.on( 'sync', this.addPaginatation, this );
+		},
+
+		addPaginatation: function( collection ) {
+			_.each( this.views._views, function( view ) {
+				if ( ! _.isUndefined( view ) ) {
+					_.first( view ).remove();
+				}
+			} );
+
+			this.views.add( new bp.Views.Pagination( { model: new Backbone.Model( collection.options ) } ) );
+
+			this.views.add( '.user-messages-bulk-actions', new bp.Views.BulkActions( {
+				model: new Backbone.Model( BP_Nouveau.messages.bulk_actions ),
+				collection : collection
+			} ) );
+		},
+
+		filterThreads: function() {
+			bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.loading, 'loading' );
+
+			this.options.threads.reset();
+			_.extend( this.options.threads.options, _.pick( this.model.attributes, ['box', 'search_terms'] ) );
+
+			this.options.threads.fetch( {
+				data    : _.pick( this.model.attributes, ['box', 'search_terms', 'page'] ),
+				success : this.threadsFiltered,
+				error   : this.threadsFilterError
+			} );
+		},
+
+		threadsFiltered: function() {
+			bp.Nouveau.Messages.removeFeedback();
+		},
+
+		threadsFilterError: function( collection, response ) {
+			bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+		},
+
+		resetSearchTerms: function( event ) {
+			event.preventDefault();
+
+			if ( ! $( event.target ).val() ) {
+				$( event.target ).closest( 'form' ).submit();
+			} else {
+				$( event.target ).closest( 'form' ).find( '[type=submit]' ).addClass('bp-show').removeClass('bp-hide');
+			}
+		},
+
+		setSearchTerms: function( event ) {
+			event.preventDefault();
+
+			this.model.set( {
+				'search_terms': $( event.target ).find( 'input[type=search]' ).val() || '',
+				page: 1
+			} );
+		},
+
+		nextPage: function( event ) {
+			event.preventDefault();
+
+			this.model.set( 'page', this.model.get( 'page' ) + 1 );
+		},
+
+		prevPage: function( event ) {
+			event.preventDefault();
+
+			this.model.set( 'page', this.model.get( 'page' ) - 1 );
+		}
+	} );
+
+	bp.Views.userMessagesHeader = bp.Nouveau.Messages.View.extend( {
+		tagName  : 'div',
+		template : bp.template( 'bp-messages-single-header' ),
+
+		events: {
+			'click .actions a' : 'doAction',
+			'click .actions button' : 'doAction'
+		},
+
+		doAction: function( event ) {
+			var action = $( event.currentTarget ).data( 'bp-action' ), self = this, options = {},
+			    feedback = BP_Nouveau.messages.doingAction;
+
+			if ( ! action ) {
+				return event;
+			}
+
+			event.preventDefault();
+
+			if ( ! this.model.get( 'id' ) ) {
+				return;
+			}
+
+			if ( 'star' === action || 'unstar' === action ) {
+				var opposite = {
+					'star'  : 'unstar',
+					'unstar' : 'star'
+				};
+
+				options.data = {
+					'star_nonce' : this.model.get( 'star_nonce' )
+				};
+
+				$( event.currentTarget ).addClass( 'bp-hide' );
+				$( event.currentTarget ).parent().find( '[data-bp-action="' + opposite[ action ] + '"]' ).removeClass( 'bp-hide' );
+
+			}
+
+			if ( ! _.isUndefined( feedback[ action ] ) ) {
+				bp.Nouveau.Messages.displayFeedback( feedback[ action ], 'loading' );
+			}
+
+			bp.Nouveau.Messages.threads.doAction( action, this.model.get( 'id' ), options ).done( function( response ) {
+				// Remove all views
+				if ( 'delete' === action ) {
+					bp.Nouveau.Messages.clearViews();
+				} else if ( response.messages ) {
+					self.model.set( _.first( response.messages ) );
+				}
+
+				// Remove previous feedback.
+				bp.Nouveau.Messages.removeFeedback();
+
+				// Display the feedback
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+			} ).fail( function( response ) {
+				// Remove previous feedback.
+				bp.Nouveau.Messages.removeFeedback();
+
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+			} );
+		}
+	} );
+
+	bp.Views.userMessagesEntry = bp.Views.userMessagesHeader.extend( {
+		tagName  : 'li',
+		template : bp.template( 'bp-messages-single-list' ),
+
+		events: {
+			'click [data-bp-action]' : 'doAction'
+		},
+
+		initialize: function() {
+			this.model.on( 'change:is_starred', this.updateMessage, this );
+		},
+
+		updateMessage: function( model ) {
+			if ( this.model.get( 'id' ) !== model.get( 'id' ) ) {
+				return;
+			}
+
+			this.render();
+		}
+	} );
+
+	bp.Views.userMessages = bp.Nouveau.Messages.View.extend( {
+		tagName  : 'div',
+		template : bp.template( 'bp-messages-single' ),
+
+		initialize: function() {
+			// Load Messages
+			this.requestMessages();
+
+			// Init a reply
+			this.reply = new bp.Models.messageThread();
+
+			this.collection.on( 'add', this.addMessage, this );
+
+			// Add the editor view
+			this.views.add( '#bp-message-content', new bp.Views.messageEditor() );
+		},
+
+		events: {
+			'click #send_reply_button' : 'sendReply'
+		},
+
+		requestMessages: function() {
+			var data = {};
+
+			this.collection.reset();
+
+			bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.loading, 'loading' );
+
+			if ( _.isUndefined( this.options.thread.attributes ) ) {
+				data.id = this.options.thread.id;
+
+			} else {
+				data.id        = this.options.thread.get( 'id' );
+				data.js_thread = ! _.isEmpty( this.options.thread.get( 'subject' ) );
+			}
+
+			this.collection.fetch( {
+				data: data,
+				success : _.bind( this.messagesFetched, this ),
+				error   : this.messagesFetchError
+			} );
+		},
+
+		messagesFetched: function( collection, response ) {
+			if ( ! _.isUndefined( response.thread ) ) {
+				this.options.thread = new Backbone.Model( response.thread );
+			}
+
+			bp.Nouveau.Messages.removeFeedback();
+
+			this.views.add( '#bp-message-thread-header', new bp.Views.userMessagesHeader( { model: this.options.thread } ) );
+		},
+
+		messagesFetchError: function( collection, response ) {
+			if ( response.feedback && response.type ) {
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+			}
+		},
+
+		addMessage: function( message ) {
+			this.views.add( '#bp-message-thread-list', new bp.Views.userMessagesEntry( { model: message } ) );
+		},
+
+		addEditor: function() {
+			// Load the Editor
+			this.views.add( '#bp-message-content', new bp.Views.messageEditor() );
+		},
+
+		sendReply: function( event ) {
+			event.preventDefault();
+
+			if ( true === this.reply.get( 'sending' ) ) {
+				return;
+			}
+
+			this.reply.set ( {
+				thread_id : this.options.thread.get( 'id' ),
+				content   : tinyMCE.activeEditor.getContent(),
+				sending   : true
+			} );
+
+			this.collection.sync( 'create', _.pick( this.reply.attributes, ['thread_id', 'content' ] ), {
+				success : _.bind( this.replySent, this ),
+				error   : _.bind( this.replyError, this )
+			} );
+		},
+
+		replySent: function( response ) {
+			var reply = this.collection.parse( response );
+
+			// Reset the form
+			tinyMCE.activeEditor.setContent( '' );
+			this.reply.set( 'sending', false );
+
+			this.collection.add( _.first( reply ) );
+		},
+
+		replyError: function( response ) {
+			if ( response.feedback && response.type ) {
+				bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
+			}
+		}
+	} );
+
+	bp.Nouveau.Messages.Router = Backbone.Router.extend( {
+		routes: {
+			'compose/' : 'composeMessage',
+			'view/:id/': 'viewMessage',
+			'sentbox/' : 'sentboxView',
+			'starred/' : 'starredView',
+			'inbox/'   : 'inboxView',
+			''        : 'inboxView'
+		},
+
+		composeMessage: function() {
+			bp.Nouveau.Messages.composeView();
+		},
+
+		viewMessage: function( thread_id ) {
+			if ( ! thread_id ) {
+				return;
+			}
+
+			// Try to get the corresponding thread
+			var thread = bp.Nouveau.Messages.threads.get( thread_id );
+
+			if ( undefined === thread ) {
+				thread    = {};
+				thread.id = thread_id;
+			}
+
+			bp.Nouveau.Messages.singleView( thread );
+		},
+
+		sentboxView: function() {
+			bp.Nouveau.Messages.box = 'sentbox';
+			bp.Nouveau.Messages.threadsView();
+		},
+
+		starredView: function() {
+			bp.Nouveau.Messages.box = 'starred';
+			bp.Nouveau.Messages.threadsView();
+		},
+
+		inboxView: function() {
+			bp.Nouveau.Messages.box = 'inbox';
+			bp.Nouveau.Messages.threadsView();
+		}
+	} );
+
+	// Launch BP Nouveau Groups
+	bp.Nouveau.Messages.start();
+
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-notifications.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-notifications.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-notifications.js	(revision 12265)
@@ -0,0 +1,140 @@
+/* global bp, BP_Nouveau */
+/* @version 3.0.0 */
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	bp.Nouveau = bp.Nouveau || {};
+
+	/**
+	 * [Activity description]
+	 * @type {Object}
+	 */
+	bp.Nouveau.Notifications = {
+
+		/**
+		 * [start description]
+		 * @return {[type]} [description]
+		 */
+		start: function() {
+			this.setupGlobals();
+
+			// Listen to events ("Add hooks!")
+			this.addListeners();
+		},
+
+		/**
+		 * [setupGlobals description]
+		 * @return {[type]} [description]
+		 */
+		setupGlobals: function() {
+			// Always reset sort to Newest notifications
+			bp.Nouveau.setStorage( 'bp-notifications', 'extras', 'DESC' );
+		},
+
+		/**
+		 * [addListeners description]
+		 */
+		addListeners: function() {
+			// Change the Order actions visibility once the ajax request is done.
+			$( '#buddypress [data-bp-list="notifications"]' ).on( 'bp_ajax_request', this.prepareDocument );
+
+			// Trigger Notifications order request.
+			$( '#buddypress [data-bp-list="notifications"]' ).on( 'click', '[data-bp-notifications-order]', bp.Nouveau, this.sortNotifications );
+
+			// Enable the Apply Button once the bulk action is selected
+			$( '#buddypress [data-bp-list="notifications"]' ).on( 'change', '#notification-select', this.enableBulkSubmit );
+
+			// Select all displayed notifications
+			$( '#buddypress [data-bp-list="notifications"]' ).on( 'click', '#select-all-notifications', this.selectAll );
+
+			// Reset The filter before unload
+			$( window ).on( 'unload', this.resetFilter );
+		},
+
+		/**
+		 * [prepareDocument description]
+		 * @return {[type]} [description]
+		 */
+		prepareDocument: function() {
+			var store = bp.Nouveau.getStorage( 'bp-notifications' );
+
+			if ( 'ASC' === store.extras ) {
+				$( '[data-bp-notifications-order="DESC"]' ).show();
+				$( '[data-bp-notifications-order="ASC"]' ).hide();
+			} else {
+				$( '[data-bp-notifications-order="ASC"]' ).show();
+				$( '[data-bp-notifications-order="DESC"]' ).hide();
+			}
+
+			// Make sure a 'Bulk Action' is selected before submitting the form
+			$( '#notification-bulk-manage' ).prop( 'disabled', 'disabled' );
+		},
+
+		/**
+		 * [sortNotifications description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		sortNotifications: function( event ) {
+			var store = event.data.getStorage( 'bp-notifications' ),
+				scope = store.scope || null, filter = store.filter || null,
+				sort = store.extra || null, search_terms = '';
+
+			event.preventDefault();
+
+			sort = $( event.currentTarget ).data( 'bp-notifications-order' );
+			bp.Nouveau.setStorage( 'bp-notifications', 'extras', sort );
+
+			if ( $( '#buddypress [data-bp-search="notifications"] input[type=search]' ).length ) {
+				search_terms = $( '#buddypress [data-bp-search="notifications"] input[type=search]' ).val();
+			}
+
+			bp.Nouveau.objectRequest( {
+				object              : 'notifications',
+				scope               : scope,
+				filter              : filter,
+				search_terms        : search_terms,
+				extras              : sort,
+				page                : 1
+			} );
+		},
+
+		/**
+		 * [enableBulkSubmit description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		enableBulkSubmit: function( event ) {
+			$( '#notification-bulk-manage' ).prop( 'disabled', $( event.currentTarget ).val().length <= 0 );
+		},
+
+		/**
+		 * [selectAll description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		selectAll: function( event ) {
+			$.each( $( '.notification-check' ), function( cb, checkbox ) {
+				$( checkbox ).prop( 'checked', $( event.currentTarget ).prop( 'checked' ) );
+			} );
+		},
+
+		/**
+		 * [resetFilter description]
+		 * @return {[type]} [description]
+		 */
+		resetFilter: function() {
+			bp.Nouveau.setStorage( 'bp-notifications', 'filter', 0 );
+		}
+	};
+
+	// Launch BP Nouveau Notifications
+	bp.Nouveau.Notifications.start();
+
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-nouveau.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-nouveau.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-nouveau.js	(revision 12265)
@@ -0,0 +1,834 @@
+/* global wp, bp, BP_Nouveau, JSON */
+/* jshint devel: true */
+/* jshint browser: true */
+/* @version 3.2.0 */
+window.wp = window.wp || {};
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	/**
+	 * [Nouveau description]
+	 * @type {Object}
+	 */
+	bp.Nouveau = {
+		/**
+		 * [start description]
+		 * @return {[type]} [description]
+		 */
+		start: function() {
+
+			// Setup globals
+			this.setupGlobals();
+
+			// Adjust Document/Forms properties
+			this.prepareDocument();
+
+			// Init the BuddyPress objects
+			this.initObjects();
+
+			// Set BuddyPress HeartBeat
+			this.setHeartBeat();
+
+			// Listen to events ("Add hooks!")
+			this.addListeners();
+		},
+
+		/**
+		 * [setupGlobals description]
+		 * @return {[type]} [description]
+		 */
+		setupGlobals: function() {
+			this.ajax_request           = null;
+
+			// Object Globals
+			this.objects                = $.map( BP_Nouveau.objects, function( value ) { return value; } );
+			this.objectNavParent        = BP_Nouveau.object_nav_parent;
+
+			// HeartBeat Global
+			this.heartbeat              = wp.heartbeat || {};
+
+			// An object containing each query var
+			this.querystring            = this.getLinkParams();
+		},
+
+		/**
+		 * [prepareDocument description]
+		 * @return {[type]} [description]
+		 */
+		prepareDocument: function() {
+
+			// Remove the no-js class and add the js one
+			if ( $( 'body' ).hasClass( 'no-js' ) ) {
+				$('body').removeClass( 'no-js' ).addClass( 'js' );
+			}
+
+			// Log Warnings into the console instead of the screen
+			if ( BP_Nouveau.warnings && 'undefined' !== typeof console && console.warn ) {
+				$.each( BP_Nouveau.warnings, function( w, warning ) {
+					console.warn( warning );
+				} );
+			}
+
+			// Remove the directory title if there's a widget containing it
+			if ( $( '.buddypress_object_nav .widget-title' ).length ) {
+				var text = $( '.buddypress_object_nav .widget-title' ).html();
+
+				$( 'body' ).find( '*:contains("' + text + '")' ).each( function( e, element ) {
+					if ( ! $( element ).hasClass( 'widget-title' ) && text === $( element ).html() && ! $( element ).is( 'a' ) ) {
+						$( element ).remove();
+					}
+				} );
+			}
+		},
+
+		/** Helpers *******************************************************************/
+
+		/**
+		 * [getStorage description]
+		 * @param  {[type]} type     [description]
+		 * @param  {[type]} property [description]
+		 * @return {[type]}          [description]
+		 */
+		getStorage: function( type, property ) {
+			var store = sessionStorage.getItem( type );
+
+			if ( store ) {
+				store = JSON.parse( store );
+			} else {
+				store = {};
+			}
+
+			if ( undefined !== property ) {
+				return store[property] || false;
+			}
+
+			return store;
+		},
+
+		/**
+		 * [setStorage description]
+		 * @param {[type]} type     [description]
+		 * @param {[type]} property [description]
+		 * @param {[type]} value    [description]
+		 */
+		setStorage: function( type, property, value ) {
+			var store = this.getStorage( type );
+
+			if ( undefined === value && undefined !== store[ property ] ) {
+				delete store[ property ];
+			} else {
+				// Set property
+				store[ property ] = value;
+			}
+
+			sessionStorage.setItem( type, JSON.stringify( store ) );
+
+			return sessionStorage.getItem( type ) !== null;
+		},
+
+		/**
+		 * [getLinkParams description]
+		 * @param  {[type]} url   [description]
+		 * @param  {[type]} param [description]
+		 * @return {[type]}       [description]
+		 */
+		getLinkParams: function( url, param ) {
+			var qs;
+			if ( url ) {
+				qs = ( -1 !== url.indexOf( '?' ) ) ? '?' + url.split( '?' )[1] : '';
+			} else {
+				qs = document.location.search;
+			}
+
+			if ( ! qs ) {
+				return null;
+			}
+
+			var params = qs.replace( /(^\?)/, '' ).split( '&' ).map( function( n ) {
+				return n = n.split( '=' ), this[n[0]] = n[1], this;
+			}.bind( {} ) )[0];
+
+			if ( param ) {
+				return params[param];
+			}
+
+			return params;
+		},
+
+		/**
+		 * URL Decode a query variable.
+		 *
+		 * @param  {string} qv    The query variable to decode.
+		 * @param  {object} chars The specific characters to use. Optionnal.
+		 * @return {string}       The URL decoded variable.
+		 */
+		urlDecode: function( qv, chars ) {
+			var specialChars = chars || {
+				amp: '&',
+				lt: '<',
+				gt: '>',
+				quot: '"',
+				'#039': '\''
+			};
+
+			return decodeURIComponent( qv.replace( /\+/g, ' ' ) ).replace( /&([^;]+);/g, function( v, q ) {
+				return specialChars[q] || '';
+			} );
+		},
+
+		/**
+		 * [ajax description]
+		 * @param  {[type]} post_data [description]
+		 * @param  {[type]} object    [description]
+		 * @return {[type]}           [description]
+		 */
+		ajax: function( post_data, object ) {
+			if ( this.ajax_request ) {
+				this.ajax_request.abort();
+			}
+
+			// Extend posted data with stored data and object nonce
+			var postData = $.extend( {}, bp.Nouveau.getStorage( 'bp-' + object ), { nonce: BP_Nouveau.nonces[object] }, post_data );
+
+			if ( undefined !== BP_Nouveau.customizer_settings ) {
+				postData.customized = BP_Nouveau.customizer_settings;
+			}
+
+			this.ajax_request = $.post( BP_Nouveau.ajaxurl, postData, 'json' );
+
+			return this.ajax_request;
+		},
+
+		inject: function( selector, content, method ) {
+			if ( ! $( selector ).length || ! content ) {
+				return;
+			}
+
+			/**
+			 * How the content should be injected in the selector
+			 *
+			 * possible methods are
+			 * - reset: the selector will be reset with the content
+			 * - append:  the content will be added after selector's content
+			 * - prepend: the content will be added before selector's content
+			 */
+			method = method || 'reset';
+
+			if ( 'append' === method ) {
+				$( selector ).append( content );
+			} else if ( 'prepend' === method ) {
+				$( selector ).prepend( content );
+			} else {
+				$( selector ).html( content );
+			}
+
+			if ( 'undefined' !== typeof bp_mentions || 'undefined' !== typeof bp.mentions ) {
+				$( '.bp-suggestions' ).bp_mentions( bp.mentions.users );
+			}
+		},
+
+		/**
+		 * [objectRequest description]
+		 * @param  {[type]} data [description]
+		 * @return {[type]}      [description]
+		 */
+		objectRequest: function( data ) {
+			var postdata = {}, self = this;
+
+			data = $.extend( {
+				object       : '',
+				scope        : null,
+				filter       : null,
+				target       : '#buddypress [data-bp-list]',
+				search_terms : '',
+				page         : 1,
+				extras       : null,
+				caller       : null,
+				template     : null,
+				method       : 'reset'
+			}, data );
+
+			// Do not request if we don't have the object or the target to inject results into
+			if ( ! data.object || ! data.target ) {
+				return;
+			}
+
+			// Prepare the search terms for the request
+			if ( data.search_terms ) {
+				data.search_terms = data.search_terms.replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
+			}
+
+			// Set session's data
+			if ( null !== data.scope ) {
+				this.setStorage( 'bp-' + data.object, 'scope', data.scope );
+			}
+
+			if ( null !== data.filter ) {
+				this.setStorage( 'bp-' + data.object, 'filter', data.filter );
+			}
+
+			if ( null !== data.extras ) {
+				this.setStorage( 'bp-' + data.object, 'extras', data.extras );
+			}
+
+			/* Set the correct selected nav and filter */
+			$( this.objectNavParent + ' [data-bp-object]' ).each( function() {
+				$( this ).removeClass( 'selected loading' );
+			} );
+
+			$( this.objectNavParent + ' [data-bp-scope="' + data.scope + '"], #object-nav li.current' ).addClass( 'selected loading' );
+			$( '#buddypress [data-bp-filter="' + data.object + '"] option[value="' + data.filter + '"]' ).prop( 'selected', true );
+
+			if ( 'friends' === data.object || 'group_members' === data.object ) {
+				data.template = data.object;
+				data.object   = 'members';
+			} else if ( 'group_requests' === data.object ) {
+				data.object = 'groups';
+				data.template = 'group_requests';
+			} else if ( 'notifications' === data.object ) {
+				data.object = 'members';
+				data.template = 'member_notifications';
+			}
+
+			postdata = $.extend( {
+				action: data.object + '_filter'
+			}, data );
+
+			return this.ajax( postdata, data.object ).done( function( response ) {
+				if ( false === response.success ) {
+					return;
+				}
+
+				$( self.objectNavParent + ' [data-bp-scope="' + data.scope + '"]' ).removeClass( 'loading' );
+
+				if ( 'reset' !== data.method ) {
+					self.inject( data.target, response.data.contents, data.method );
+
+					$( data.target ).trigger( 'bp_ajax_' + data.method, $.extend( data, { response: response.data } ) );
+				} else {
+					/* animate to top if called from bottom pagination */
+					if ( data.caller === 'pag-bottom' && $( '#subnav' ).length ) {
+						var top = $('#subnav').parent();
+						$( 'html,body' ).animate( { scrollTop: top.offset().top }, 'slow', function() {
+							$( data.target ).fadeOut( 100, function() {
+								self.inject( this, response.data.contents, data.method );
+								$( this ).fadeIn( 100 );
+
+								// Inform other scripts the list of objects has been refreshed.
+								$( data.target ).trigger( 'bp_ajax_request', $.extend( data, { response: response.data } ) );
+							} );
+						} );
+
+					} else {
+						$( data.target ).fadeOut( 100, function() {
+							self.inject( this, response.data.contents, data.method );
+							$( this ).fadeIn( 100 );
+
+							// Inform other scripts the list of objects has been refreshed.
+							$( data.target ).trigger( 'bp_ajax_request', $.extend( data, { response: response.data } ) );
+						} );
+					}
+				}
+			} );
+		},
+
+		/**
+		 * [initObjects description]
+		 * @return {[type]} [description]
+		 */
+		initObjects: function() {
+			var self = this, objectData = {}, queryData = {}, scope = 'all', search_terms = '', extras = null, filter = null;
+
+			$.each( this.objects, function( o, object ) {
+				objectData = self.getStorage( 'bp-' + object );
+
+				if ( undefined !== objectData.scope ) {
+					scope = objectData.scope;
+				}
+
+				// Notifications always need to start with Newest ones
+				if ( undefined !== objectData.extras && 'notifications' !== object ) {
+					extras = objectData.extras;
+				}
+
+				if (  $( '#buddypress [data-bp-filter="' + object + '"]' ).length ) {
+					if ( '-1' !== $( '#buddypress [data-bp-filter="' + object + '"]' ).val() && '0' !== $( '#buddypress [data-bp-filter="' + object + '"]' ).val() ) {
+						filter = $( '#buddypress [data-bp-filter="' + object + '"]' ).val();
+					} else if ( undefined !== objectData.filter ) {
+						filter = objectData.filter,
+						$( '#buddypress [data-bp-filter="' + object + '"] option[value="' + filter + '"]' ).prop( 'selected', true );
+					}
+				}
+
+				if ( $( this.objectNavParent + ' [data-bp-object="' + object + '"]' ).length ) {
+					$( this.objectNavParent + ' [data-bp-object="' + object + '"]' ).each( function() {
+						$( this ).removeClass( 'selected' );
+					} );
+
+					$( this.objectNavParent + ' [data-bp-scope="' + object + '"], #object-nav li.current' ).addClass( 'selected' );
+				}
+
+				// Check the querystring to eventually include the search terms
+				if ( null !== self.querystring ) {
+					if ( undefined !== self.querystring[ object + '_search'] ) {
+						search_terms = self.querystring[ object + '_search'];
+					} else if ( undefined !== self.querystring.s ) {
+						search_terms = self.querystring.s;
+					}
+
+					if ( search_terms ) {
+						search_terms = self.urlDecode( search_terms );
+						$( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).val( search_terms );
+					}
+				}
+
+				if ( $( '#buddypress [data-bp-list="' + object + '"]' ).length ) {
+					queryData =  {
+						object       : object,
+						scope        : scope,
+						filter       : filter,
+						search_terms : search_terms,
+						extras       : extras
+					};
+
+					// Populate the object list
+					self.objectRequest( queryData );
+				}
+			} );
+		},
+
+		/**
+		 * [setHeartBeat description]
+		 */
+		setHeartBeat: function() {
+			if ( typeof BP_Nouveau.pulse === 'undefined' || ! this.heartbeat ) {
+				return;
+			}
+
+			this.heartbeat.interval( Number( BP_Nouveau.pulse ) );
+
+			// Extend "send" with BuddyPress namespace
+			$.fn.extend( {
+				'heartbeat-send': function() {
+					return this.bind( 'heartbeat-send.buddypress' );
+				}
+			} );
+
+			// Extend "tick" with BuddyPress namespace
+			$.fn.extend( {
+				'heartbeat-tick': function() {
+					return this.bind( 'heartbeat-tick.buddypress' );
+				}
+			} );
+		},
+
+		/** Event Listeners ***********************************************************/
+
+		/**
+		 * [addListeners description]
+		 */
+		addListeners: function() {
+			// Disabled inputs
+			$( '[data-bp-disable-input]' ).on( 'change', this.toggleDisabledInput );
+
+			// HeartBeat Send and Receive
+			$( document ).on( 'heartbeat-send.buddypress', this.heartbeatSend );
+			$( document ).on( 'heartbeat-tick.buddypress', this.heartbeatTick );
+
+			// Refreshing
+			$( this.objectNavParent + ' .bp-navs' ).on( 'click', 'a', this, this.scopeQuery );
+
+			// Filtering
+			$( '#buddypress [data-bp-filter]' ).on( 'change', this, this.filterQuery );
+
+			// Searching
+			$( '#buddypress [data-bp-search]' ).on( 'submit', 'form', this, this.searchQuery );
+			$( '#buddypress [data-bp-search] form' ).on( 'search', 'input[type=search]', this.resetSearch );
+
+			// Buttons
+			$( '#buddypress [data-bp-list], #buddypress #item-header' ).on( 'click', '[data-bp-btn-action]', this, this.buttonAction );
+
+			// Close notice
+			$( '#buddypress [data-bp-close]' ).on( 'click', this, this.closeNotice );
+
+			// Pagination
+			$( '#buddypress [data-bp-list]' ).on( 'click', '[data-bp-pagination] a', this, this.paginateAction );
+		},
+
+		/** Event Callbacks ***********************************************************/
+
+		/**
+		 * [enableDisabledInput description]
+		 * @param  {[type]} event [description]
+		 * @param  {[type]} data  [description]
+		 * @return {[type]}       [description]
+		 */
+		toggleDisabledInput: function() {
+
+			// Fetch the data attr value (id)
+			// This a pro tem approach due to current conditions see
+			// https://github.com/buddypress/next-template-packs/issues/180.
+			var disabledControl = $(this).attr('data-bp-disable-input');
+
+			if ( $( disabledControl ).prop( 'disabled', true ) && !$(this).hasClass('enabled') ) {
+				$(this).addClass('enabled').removeClass('disabled');
+				$( disabledControl ).removeProp( 'disabled' );
+
+			} else if( $( disabledControl ).prop( 'disabled', false ) && $(this).hasClass('enabled') ) {
+				$(this).removeClass('enabled').addClass('disabled');
+				// Set using attr not .prop else DOM renders as 'disable=""' CSS needs 'disable="disable"'.
+				$( disabledControl ).attr( 'disabled', 'disabled' );
+			}
+		},
+
+		/**
+		 * [heartbeatSend description]
+		 * @param  {[type]} event [description]
+		 * @param  {[type]} data  [description]
+		 * @return {[type]}       [description]
+		 */
+		heartbeatSend: function( event, data ) {
+			// Add an heartbeat send event to possibly any BuddyPress pages
+			$( '#buddypress' ).trigger( 'bp_heartbeat_send', data );
+		},
+
+		/**
+		 * [heartbeatTick description]
+		 * @param  {[type]} event [description]
+		 * @param  {[type]} data  [description]
+		 * @return {[type]}       [description]
+		 */
+		heartbeatTick: function( event, data ) {
+			// Add an heartbeat send event to possibly any BuddyPress pages
+			$( '#buddypress' ).trigger( 'bp_heartbeat_tick', data );
+		},
+
+		/**
+		 * [queryScope description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		scopeQuery: function( event ) {
+			var self = event.data, target = $( event.currentTarget ).parent(),
+				scope = 'all', object, filter = null, search_terms = '';
+
+			if ( target.hasClass( 'no-ajax' ) || $( event.currentTarget ).hasClass( 'no-ajax' ) || ! target.attr( 'data-bp-scope' ) ) {
+				return event;
+			}
+
+			scope  = target.data( 'bp-scope' );
+			object = target.data( 'bp-object' );
+
+			if ( ! scope || ! object ) {
+				return event;
+			}
+
+			// Stop event propagation
+			event.preventDefault();
+
+			filter = $( '#buddypress' ).find( '[data-bp-filter="' + object + '"]' ).first().val();
+
+			if ( $( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).length ) {
+				search_terms = $( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).val();
+			}
+
+			// Remove the New count on dynamic tabs
+			if ( target.hasClass( 'dynamic' ) ) {
+				target.find( 'a span' ).html('');
+			}
+
+			self.objectRequest( {
+				object       : object,
+				scope        : scope,
+				filter       : filter,
+				search_terms : search_terms,
+				page         : 1
+			} );
+		},
+
+		/**
+		 * [filterQuery description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		filterQuery: function( event ) {
+			var self = event.data, object = $( event.target ).data( 'bp-filter' ),
+				scope = 'all', filter = $( event.target ).val(),
+				search_terms = '', template = null;
+
+			if ( ! object ) {
+				return event;
+			}
+
+			if ( $( self.objectNavParent + ' [data-bp-object].selected' ).length ) {
+				scope = $( self.objectNavParent + ' [data-bp-object].selected' ).data( 'bp-scope' );
+			}
+
+			if ( $( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).length ) {
+				search_terms = $( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).val();
+			}
+
+			if ( 'friends' === object ) {
+				object = 'members';
+			}
+
+			self.objectRequest( {
+				object       : object,
+				scope        : scope,
+				filter       : filter,
+				search_terms : search_terms,
+				page         : 1,
+				template     : template
+			} );
+		},
+
+		/**
+		 * [searchQuery description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		searchQuery: function( event ) {
+			var self = event.data, object, scope = 'all', filter = null, template = null, search_terms = '';
+
+			if ( $( event.delegateTarget ).hasClass( 'no-ajax' ) || undefined === $( event.delegateTarget ).data( 'bp-search' ) ) {
+				return event;
+			}
+
+			// Stop event propagation
+			event.preventDefault();
+
+			object       = $( event.delegateTarget ).data( 'bp-search' );
+			filter       = $( '#buddypress' ).find( '[data-bp-filter="' + object + '"]' ).first().val();
+			search_terms = $( event.delegateTarget ).find( 'input[type=search]' ).first().val();
+
+			if ( $( self.objectNavParent + ' [data-bp-object]' ).length ) {
+				scope = $( self.objectNavParent + ' [data-bp-object="' + object + '"].selected' ).data( 'bp-scope' );
+			}
+
+			self.objectRequest( {
+				object       : object,
+				scope        : scope,
+				filter       : filter,
+				search_terms : search_terms,
+				page         : 1,
+				template     : template
+			} );
+		},
+
+		/**
+		 * [showSearchSubmit description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		showSearchSubmit: function( event ) {
+			$( event.delegateTarget ).find( '[type=submit]' ).addClass( 'bp-show' );
+			if( $('[type=submit]').hasClass( 'bp-hide' ) ) {
+				$( '[type=submit]' ).removeClass( 'bp-hide' );
+			}
+		},
+
+		/**
+		 * [resetSearch description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		resetSearch: function( event ) {
+			if ( ! $( event.target ).val() ) {
+				$( event.delegateTarget ).submit();
+			} else {
+				$( event.delegateTarget ).find( '[type=submit]' ).show();
+			}
+		},
+
+		/**
+		 * [buttonAction description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		buttonAction: function( event ) {
+			var self = event.data, target = $( event.currentTarget ), action = target.data( 'bp-btn-action' ), nonceUrl = target.data( 'bp-nonce' ),
+				item = target.closest( '[data-bp-item-id]' ), item_id = item.data( 'bp-item-id' ), item_inner = target.closest('.list-wrap'),
+				object = item.data( 'bp-item-component' ), nonce = '';
+
+			// Simply let the event fire if we don't have needed values
+			if ( ! action || ! item_id || ! object ) {
+				return event;
+			}
+
+			// Stop event propagation
+			event.preventDefault();
+
+			if ( ( undefined !== BP_Nouveau[ action + '_confirm'] && false === window.confirm( BP_Nouveau[ action + '_confirm'] ) ) || target.hasClass( 'pending' ) ) {
+				return false;
+			}
+
+			// Find the required wpnonce string.
+			// if  button element set we'll have our nonce set on a data attr
+			// Check the value & if exists split the string to obtain the nonce string
+			// if no value, i.e false, null then the href attr is used.
+			if ( nonceUrl ) {
+				nonce = nonceUrl.split('?_wpnonce=');
+				nonce = nonce[1];
+			} else {
+				nonce = self.getLinkParams( target.prop( 'href' ), '_wpnonce' );
+			}
+
+			// Unfortunately unlike groups
+			// Friends actions does not match the wpnonce
+			var friends_actions_map = {
+				is_friend         : 'remove_friend',
+				not_friends       : 'add_friend',
+				pending           : 'withdraw_friendship',
+				accept_friendship : 'accept_friendship',
+				reject_friendship : 'reject_friendship'
+			};
+
+			if ( 'members' === object && undefined !== friends_actions_map[ action ] ) {
+				action = friends_actions_map[ action ];
+				object = 'friends';
+			}
+
+			// Add a pending class to prevent queries while we're processing the action
+			target.addClass( 'pending loading' );
+
+			self.ajax( {
+				action   : object + '_' + action,
+				item_id  : item_id,
+				_wpnonce : nonce
+			}, object ).done( function( response ) {
+				if ( false === response.success ) {
+					item_inner.prepend( response.data.feedback );
+					target.removeClass( 'pending loading' );
+					item.find( '.bp-feedback' ).fadeOut( 6000 );
+				} else {
+					// Specific cases for groups
+					if ( 'groups' === object ) {
+
+						// Group's header button
+						if ( undefined !== response.data.is_group && response.data.is_group ) {
+							return window.location.reload();
+						}
+					}
+
+					// User's groups invitations screen & User's friend screens
+					if ( undefined !== response.data.is_user && response.data.is_user ) {
+						target.parent().html( response.data.feedback );
+						item.fadeOut( 1500 );
+						return;
+					}
+
+					// Update count
+					if ( $( self.objectNavParent + ' [data-bp-scope="personal"]' ).length ) {
+						var personal_count = Number( $( self.objectNavParent + ' [data-bp-scope="personal"] span' ).html() ) || 0;
+
+						if ( -1 !== $.inArray( action, ['leave_group', 'remove_friend'] ) ) {
+							personal_count -= 1;
+						} else if ( -1 !== $.inArray( action, ['join_group'] ) ) {
+							personal_count += 1;
+						}
+
+						if ( personal_count < 0 ) {
+							personal_count = 0;
+						}
+
+						$( self.objectNavParent + ' [data-bp-scope="personal"] span' ).html( personal_count );
+					}
+
+					target.parent().replaceWith( response.data.contents );
+				}
+			} );
+		},
+
+		/**
+		 * [closeNotice description]
+		 * @param  {[type]} event [description]
+		 * @return {[type]}       [description]
+		 */
+		closeNotice: function( event ) {
+			var closeBtn = $( event.currentTarget );
+
+			event.preventDefault();
+
+			// Make sure cookies are removed
+			if ( 'clear' === closeBtn.data( 'bp-close' ) ) {
+				if ( undefined !== $.cookie( 'bp-message' ) ) {
+					$.removeCookie( 'bp-message' );
+				}
+
+				if ( undefined !== $.cookie( 'bp-message-type' ) ) {
+					$.removeCookie( 'bp-message-type' );
+				}
+			}
+
+			// @todo other cases...
+			// Dismissing site-wide notices.
+			if ( closeBtn.closest( '.bp-feedback' ).hasClass( 'bp-sitewide-notice' ) ) {
+				bp.Nouveau.ajax( {
+					action : 'messages_dismiss_sitewide_notice'
+				}, 'messages' );
+			}
+
+			// Remove the notice
+			closeBtn.closest( '.bp-feedback' ).remove();
+		},
+
+		paginateAction: function( event ) {
+			var self  = event.data, navLink = $( event.currentTarget ), pagArg,
+			    scope = null, object, objectData, filter = null, search_terms = null, extras = null;
+
+			pagArg = navLink.closest( '[data-bp-pagination]' ).data( 'bp-pagination' ) || null;
+
+			if ( null === pagArg ) {
+				return event;
+			}
+
+			event.preventDefault();
+
+			object = $( event.delegateTarget ).data( 'bp-list' ) || null;
+
+			// Set the scope & filter
+			if ( null !== object ) {
+				objectData = self.getStorage( 'bp-' + object );
+
+				if ( undefined !== objectData.scope ) {
+					scope = objectData.scope;
+				}
+
+				if ( undefined !== objectData.filter ) {
+					filter = objectData.filter;
+				}
+
+				if ( undefined !== objectData.extras ) {
+					extras = objectData.extras;
+				}
+			}
+
+			// Set the search terms
+			if ( $( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).length ) {
+				search_terms = $( '#buddypress [data-bp-search="' + object + '"] input[type=search]' ).val();
+			}
+
+			var queryData = {
+				object       : object,
+				scope        : scope,
+				filter       : filter,
+				search_terms : search_terms,
+				extras       : extras,
+				page         : self.getLinkParams( navLink.prop( 'href' ), pagArg ) || 1
+			};
+
+			// Request the page
+			self.objectRequest( queryData );
+		}
+	};
+
+	// Launch BP Nouveau
+	bp.Nouveau.start();
+
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-xprofile.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-xprofile.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/buddypress-xprofile.js	(revision 12265)
@@ -0,0 +1,79 @@
+/* global bp, BP_Nouveau */
+/* @version 3.0.0 */
+window.bp = window.bp || {};
+
+( function( exports, $ ) {
+
+	// Bail if not set
+	if ( typeof BP_Nouveau === 'undefined' ) {
+		return;
+	}
+
+	/**
+	 * This an ugly copy from Legacy's buddypress.js for now
+	 *
+	 * This really needs to be improved !
+	 */
+
+	/** Profile Visibility Settings *********************************/
+
+	// Initially hide the 'field-visibility-settings' block
+	$( '.field-visibility-settings' ).addClass( 'bp-hide' );
+	// Add initial aria state to button
+	$( '.visibility-toggle-link' ).attr( 'aria-expanded', 'false' );
+
+	$( '.visibility-toggle-link' ).on( 'click', function( event ) {
+		event.preventDefault();
+
+		$( this ).attr('aria-expanded', 'true');
+
+		$( this ).parent().addClass( 'field-visibility-settings-hide bp-hide' )
+
+			.siblings( '.field-visibility-settings' ).removeClass( 'bp-hide' ).addClass( 'field-visibility-settings-open' );
+	} );
+
+	$( '.field-visibility-settings-close' ).on( 'click', function( event ) {
+		event.preventDefault();
+
+		var settings_div = $( this ).parent(),
+			vis_setting_text = settings_div.find( 'input:checked' ).parent().text();
+
+		settings_div.removeClass( 'field-visibility-settings-open' ).addClass( 'bp-hide' )
+			.siblings( '.field-visibility-settings-toggle' )
+				.children( '.current-visibility-level' ).text( vis_setting_text ).end()
+				.addClass( 'bp-show' ).removeClass( 'field-visibility-settings-hide bp-hide' );
+				$( '.visibility-toggle-link').attr( 'aria-expanded', 'false' );
+	} );
+
+	$( '#profile-edit-form input:not(:submit), #profile-edit-form textarea, #profile-edit-form select, #signup_form input:not(:submit), #signup_form textarea, #signup_form select' ).change( function() {
+		var shouldconfirm = true;
+
+		$( '#profile-edit-form input:submit, #signup_form input:submit' ).on( 'click', function() {
+			shouldconfirm = false;
+		} );
+
+		window.onbeforeunload = function() {
+			if ( shouldconfirm ) {
+				return BP_Nouveau.unsaved_changes;
+			}
+		};
+	} );
+
+	window.clear = function( container ) {
+		if ( ! container ) {
+			return;
+		}
+
+		container = container.replace( '[', '\\[' ).replace( ']', '\\]' );
+
+		if ( $( '#' + container + ' option' ).length ) {
+			$.each( $( '#' + container + ' option' ), function( c, option ) {
+				$( option ).prop( 'selected', false );
+			} );
+		} else if ( $( '#' + container + ' [type=radio]' ).length ) {
+			$.each( $( '#' + container + ' [type=radio]' ), function( c, checkbox ) {
+				$( checkbox ).prop( 'checked', false );
+			} );
+		}
+	};
+} )( bp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/customizer.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/customizer.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/customizer.js	(revision 12265)
@@ -0,0 +1,70 @@
+/* global _ */
+/* @version 3.0.0 */
+window.wp = window.wp || {};
+
+( function( wp, $ ) {
+
+	if ( 'undefined' === typeof wp.customize ) {
+		return;
+	}
+
+	wp.customize.bind( 'ready', function() {
+		var groupFrontPage = wp.customize.control( 'group_front_page' ),
+		    userFrontPage  = wp.customize.control( 'user_front_page' );
+
+		// If the Main Group setting is disabled, hide all others
+		if ( 'undefined' !== typeof groupFrontPage ) {
+			$( groupFrontPage.selector ).on( 'click', 'input[type=checkbox]', function( event ) {
+				var checked = $( event.currentTarget ).prop( 'checked' ), controller = $( event.delegateTarget ).prop( 'id' );
+
+				_.each( wp.customize.section( 'bp_nouveau_group_front_page' ).controls(), function( control ) {
+					if ( control.selector !== '#' + controller ) {
+						if ( true === checked ) {
+							$( control.selector ).show();
+						} else {
+							$( control.selector ).hide();
+						}
+					}
+				} );
+			} );
+		}
+
+		// If the Main User setting is disabled, hide all others
+		if ( 'undefined' !== typeof userFrontPage ) {
+			$( userFrontPage.selector ).on( 'click', 'input[type=checkbox]', function( event ) {
+				var checked = $( event.currentTarget ).prop( 'checked' ), controller = $( event.delegateTarget ).prop( 'id' );
+
+				_.each( wp.customize.section( 'bp_nouveau_user_front_page' ).controls(), function( control ) {
+					if ( control.selector !== '#' + controller ) {
+						if ( true === checked ) {
+							$( control.selector ).show();
+						} else {
+							$( control.selector ).hide();
+						}
+					}
+				} );
+			} );
+		}
+
+		$( 'ul#customize-control-group_nav_order, ul#customize-control-user_nav_order' ).sortable( {
+			cursor    : 'move',
+			axis      : 'y',
+			opacity   : 1,
+			items     : 'li:not(.ui-sortable-disabled)',
+			tolerance : 'intersect',
+
+			update: function() {
+				var order = [];
+
+				$( this ).find( '[data-bp-nav]' ).each( function( s, slug ) {
+					order.push( $( slug ).data( 'bp-nav' ) );
+				} );
+
+				if ( order.length ) {
+					$( '#bp_item_' + $( this ).data( 'bp-type' ) ).val( order.join() ).trigger( 'change' );
+				}
+			}
+		} ).disableSelection();
+	} );
+
+} )( window.wp, jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/password-verify.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/password-verify.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/js/password-verify.js	(revision 12265)
@@ -0,0 +1,45 @@
+/* jshint undef: false */
+/* Password Verify */
+/* global pwsL10n */
+/* @version 3.0.0 */
+( function( $ ){
+	function check_pass_strength() {
+		var pass1 = $( '.password-entry' ).val(),
+		    pass2 = $( '.password-entry-confirm' ).val(),
+		    strength;
+
+		// Reset classes and result text
+		$( '#pass-strength-result' ).removeClass( 'show mismatch short bad good strong' );
+		if ( ! pass1 ) {
+			$( '#pass-strength-result' ).html( pwsL10n.empty );
+			return;
+		}
+
+		strength = wp.passwordStrength.meter( pass1, wp.passwordStrength.userInputBlacklist(), pass2 );
+
+		switch ( strength ) {
+			case 2:
+				$( '#pass-strength-result' ).addClass( 'show bad' ).html( pwsL10n.bad );
+				break;
+			case 3:
+				$( '#pass-strength-result' ).addClass( 'show good' ).html( pwsL10n.good );
+				break;
+			case 4:
+				$( '#pass-strength-result' ).addClass( 'show strong' ).html( pwsL10n.strong );
+				break;
+			case 5:
+				$( '#pass-strength-result' ).addClass( 'show mismatch' ).html( pwsL10n.mismatch );
+				break;
+			default:
+				$( '#pass-strength-result' ).addClass( 'show short' ).html( pwsL10n['short'] );
+				break;
+		}
+	}
+
+	// Bind check_pass_strength to keyup events in the password fields
+	$( document ).ready( function() {
+		$( '.password-entry' ).val( '' ).keyup( check_pass_strength );
+		$( '.password-entry-confirm' ).val( '' ).keyup( check_pass_strength );
+	} );
+
+} )( jQuery );
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/_nouveau_invites.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/_nouveau_invites.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/_nouveau_invites.scss	(revision 12265)
@@ -0,0 +1,180 @@
+/*
+ * bp-nouveau styling: invite members, sent invites
+ * @version 3.0.0
+ */
+.buddypress {
+
+	.bp-invites-content {
+
+		ul.item-list {
+			border-top: 0;
+
+			li {
+				border: 1px solid $light-grey;
+				margin: 0 0 1%;
+				padding-left: 5px;
+				padding-right: 5px;
+				position: relative;
+				width: auto;
+
+				.list-title {
+					margin: 0 auto;
+					width: 80%;
+				}
+
+				.action {
+					position: absolute;
+					top: 10px;
+					right: 10px;
+
+					a.button.invite-button {
+						border: 0;
+
+						&:focus,
+						&:hover {
+							color: #1fb3dd;
+						}
+					}
+				}
+
+			} // close li
+
+			li.selected {
+
+				@include box-shadow(inset 0 0 12px 0 rgba($golden, 0.2) );
+			}
+
+		} // close ul
+
+		.item-list .item-meta span,
+		.group-inviters li {
+			color: $light-text;
+		}
+
+		li {
+
+			ul.group-inviters {
+				clear: both;
+				margin: 0;
+				overflow: hidden;
+
+				li {
+					border: 0;
+					float: left;
+
+					@include font-size(20) ;
+					width: inherit;
+				}
+			} // close .group-inviters
+
+			.status {
+
+				@include font-size(20);
+				font-style: italic;
+				clear: both;
+				color: $bp-text;
+				margin: $marg-sml 0;
+			}
+
+		} // close li
+
+		@include clearfix-element("#send-invites-editor ul");
+
+		#send-invites-editor {
+
+			textarea {
+				width: 100%;
+			}
+
+			ul {
+				clear: both;
+				list-style: none;
+				margin: $marg-sml 0;
+
+				li {
+					float: left;
+					margin: 0.5%;
+					max-height: 50px;
+					max-width: 50px;
+				}
+			} // ul
+
+			#bp-send-invites-form {
+				clear: both;
+				margin-top: $marg-sml;
+			}
+
+			.action {
+				margin-top: $marg-sml;
+				padding-top: 10px;
+			}
+
+		} // close #send-invites-editor
+
+		#send-invites-editor.bp-hide {
+			display: none;
+		}
+
+		@include medium-up() {
+
+			ul.item-list {
+
+				> li {
+
+					@include box-model();
+					border: 1px solid #eaeaea;
+					float: left;
+					padding-left: $pad-sml;
+					padding-right: $pad-sml;
+					width: 49.5%;
+
+					&:nth-child(odd) {
+						margin-right: 0.5%;
+					}
+
+					&:nth-child(even) {
+						margin-left: 0.5%;
+					}
+				} // close  li
+
+				ul.group-inviters {
+					float: left;
+					width: auto;
+				}
+
+			} // ul
+
+		} // close @media
+
+	} // close bp-invites-content
+}
+
+@include medium-up() {
+
+	:not(.vertical) + .item-body #group-invites-container {
+
+		display: -ms-grid;
+		display: grid;
+		-ms-grid-columns: 25% auto;
+		grid-template-columns: 25% auto;
+		grid-template-areas: "group-invites-nav group-invites-column";
+
+		.bp-invites-nav {
+			-ms-grid-row: 1;
+			-ms-grid-column: 1;
+			grid-area: group-invites-nav;
+
+			li {
+				display: block;
+				float: none;
+			}
+		}
+
+		.group-invites-column {
+			-ms-grid-row: 1;
+			-ms-grid-column: 2;
+			grid-area: group-invites-column;
+		}
+	}
+}
+
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/_nouveau_messages.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/_nouveau_messages.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/_nouveau_messages.scss	(revision 12265)
@@ -0,0 +1,409 @@
+// Nouveau Messages styles.
+// @version 3.0.0
+// Icon additions to default action star
+
+
+.message-action-star:before,
+.message-action-unstar:before,
+.message-action-view:before,
+.message-action-delete:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+
+.message-action-star:before {
+	color: #aaa;
+	content: "\f154";
+}
+
+.message-action-unstar:before {
+	color: #fcdd77;
+	content: "\f155";
+}
+
+.message-action-view:before {
+	content: "\f473";
+}
+
+.message-action-delete:before {
+	content: "\f153";
+}
+
+.message-action-delete:hover:before {
+	color: #a00;
+}
+
+.preview-content {
+
+	.actions {
+
+		a {
+			text-decoration: none;
+		}
+	}
+}
+
+// Visual message threads & preview pane styles
+
+.bp-messages-content {
+	margin: $marg-med 0;
+
+	.avatar {
+
+		@include box-shadow-none();
+	}
+
+	.thread-participants {
+		list-style: none;
+
+		dd {
+			margin-left: 0;
+		}
+	}
+
+	time {
+		color: $meta-text;
+
+		@include font-size(12);
+	}
+}
+
+// The actual messages list
+#message-threads {
+	border-top: 1px solid $light-grey;
+	clear: both;
+	list-style: none;
+	margin: 0;
+	max-height: 220px;
+	overflow-x: hidden;
+	overflow-y: auto;
+	padding: 0;
+	width: 100%;
+
+	// The primary list elements
+	li {
+		border-bottom: 1px solid $light-grey;
+
+		@include flex-box-dir();
+		margin: 0;
+		overflow: hidden;
+		padding: $pad-sml 0;
+
+		// the child items
+		.thread-cb {
+
+			@include center-vert();
+			@include box-item-size(1, 2, 5%);
+		}
+
+		.thread-from,
+		.thread-to {
+
+			@include box-item-size(1, 2, 20%);
+
+			img.avatar {
+				float: left;
+				margin: 0 $marg-sml 0 0;
+			}
+
+			.user-name {
+				display: inline-block;
+				line-height: 1.1;
+			}
+
+			.num-recipients {
+				color: $meta-text;
+				font-weight: 400;
+
+				@include font-size(12);
+				margin: 0;
+			}
+		}
+
+		.thread-content {
+
+			@include box-item-size(1, 2, 60%);
+		}
+
+		.thread-date {
+
+			@include box-item-size(1, 2, 15%);
+		}
+
+		// the selected parent li elements & message details
+		&.selected {
+			background-color: $off-white;
+
+			.thread-subject {
+
+				.subject {
+					color: $blue;
+				}
+			}
+		}
+
+		// the unread parent li
+		&.unread {
+			font-weight: 700;
+		}
+
+		.thread-content {
+
+			.excerpt {
+				color: $meta-text;
+
+				@include font-size(12);
+				margin: 0;
+			}
+
+			.thread-from,
+			.thread-to,
+			.thread-subject {
+
+				@include responsive-font(16);
+			}
+
+			.thread-subject {
+				vertical-align: top;
+
+				.excerpt {
+					font-weight: 400;
+				}
+
+			}
+		} // close thread-content
+
+		.thread-date {
+			padding-right: 5px;
+			text-align: right;
+		}
+
+	} // close li
+
+} // close message-threads
+
+.bp-messages-content {
+
+	.actions {
+		float: right;
+		max-width: 30%;
+
+		.bp-icons:not(.bp-hide) {
+			display: inline-block;
+			margin: 0;
+			padding: $pad-xsml $pad-sml;
+
+			&:before {
+
+				@include font-size(26);
+			}
+		}
+	}
+
+	// preview pane on main inbox view all message entries
+	#thread-preview {
+		border: 1px solid $light-grey;
+		margin-top: $marg-lrg;
+
+		.preview-message {
+			overflow: hidden;
+		}
+
+		.preview-content {
+			margin: 0.5em;
+
+			.preview-message {
+				background: $off-white;
+				margin: $marg-sml 0;
+				padding: $pad-med $pad-xsml $pad-xsml;
+			}
+
+		} // close .preview-content
+
+	} // close .thread-preview
+
+	// The single view of messages in a conversation thread
+	#bp-message-thread-list {
+		border-top: 1px solid $light-grey;
+		clear: both;
+		list-style: none;
+		padding: $pad-med 0 $pad-xsml;
+
+		li {
+			padding: $pad-sml;
+		}
+
+		li:nth-child(2n) {
+
+			.message-content {
+				background: $off-white;
+			}
+		}
+
+		.message-metadata {
+			border-bottom: 1px solid $bp-border-dark;
+
+			@include box-shadow(-2px 1px 9px 0 #eee);
+			display: table;
+			padding: 0.2em;
+			width: 100%;
+
+			.avatar {
+				width: 30px;
+			}
+
+			.user-link {
+				display: block;
+
+				@include responsive-font(16);
+				float: left;
+			}
+
+			time {
+				color: $meta-text;
+
+				@include font-size(12);
+				padding: 0 $pad-sml;
+			}
+
+			button {
+				padding: 0 $pad-xsml;
+			}
+
+			button:before {
+
+				@include font-size(20);
+			}
+		}
+
+		.message-content {
+			overflow: hidden;
+			margin: 1em auto 0;
+			width: 90%;
+		}
+
+		img.avatar {
+			float: left;
+			margin: 0 $marg-sml 0 0;
+		}
+
+		.actions {
+
+			a:before {
+				font-size: 18px;
+			}
+		}
+	} // close #bp-message-thread-list
+
+	form.send-reply {
+
+		.avatar-box {
+			padding: $pad-sml 0;
+		}
+	}
+
+	// Grouped rules for both inbox all messages lists &
+	// for single view conversation thread
+
+	.preview-pane-header,
+	.single-message-thread-header {
+		border-bottom: 1px solid $light-grey;
+
+		&:after {
+			clear: both;
+			content: "";
+			display: table;
+		}
+	}
+
+	.preview-thread-title,
+	.single-thread-title {
+
+		@include font-size(16);
+
+		.messages-title {
+			padding-left: $pad-lrg;
+		}
+	}
+
+	.thread-participants {
+		float: left;
+		margin: $marg-xsml 0;
+		width: 70%;
+
+		dd,
+		ul {
+			margin-bottom: $marg-sml;
+		}
+
+		ul {
+			list-style: none;
+
+			&:after {
+
+				clear: both;
+				content: "";
+				display: table;
+			}
+		}
+
+		li {
+			float: left;
+			margin-left: 5px;
+		}
+
+		img {
+			width: 30px;
+		}
+	}
+
+	#thread-preview .preview-message,
+	#bp-message-thread-list li .message-content {
+
+		ul,
+		ol,
+		blockquote {
+			list-style-position: inside;
+			margin-left: 0;
+		}
+	}
+
+	ul#message-threads:empty,
+	#thread-preview:empty {
+		display: none;
+	}
+
+	#thread-preview h2:first-child,
+	#bp-message-thread-header h2:first-child {
+		background-color: $light-grey;
+		color: $bp-text;
+		font-weight: 700;
+		margin: 0;
+		padding: 0.5em;
+	}
+
+	#message-threads .thread-content a,
+	#bp-message-thread-list li a.user-link {
+		border: 0;
+		text-decoration: none;
+	}
+
+	// The general form elements for composing messages
+	.standard-form {
+
+		#subject {
+			margin-bottom: $marg-lrg;
+		}
+	}
+
+} // close .bp-messages-content
+
+// Bulk Message styles
+
+div.bp-navs#subsubnav.bp-messages-filters {
+
+	.user-messages-bulk-actions {
+		margin-right: 15px;
+		max-width: 42.5%;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/buddypress.scss
===================================================================
--- /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/buddypress.scss	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-templates/bp-nouveau/sass/buddypress.scss	(revision 12265)
@@ -0,0 +1,444 @@
+//
+//
+// Buddypress scss primary scss stylesheet
+//
+//
+// This file compiles to buddypress.css.
+//
+// Imported into this file are the building block partial files
+// that form the base level styles plus any files from the new
+// theme/template pack.
+
+
+// Import our partials mixins & variables files
+
+@import "../common-styles/_bp-variables";
+@import "../common-styles/_bp-mixins";
+
+
+// Sectional structuring allows us to locate elements
+// & to keep a degree of clarity in mind as to BP
+// components. Please note that sections do NOT have to
+// be used, default modular styles imported may provide
+// all the rules needed for the section.
+//
+
+/*--------------------------------------------------------------
+Hello, this is the BuddyPress Nouveau stylesheet.
+
+@version 4.0.0
+
+----------------------------------------------------------------
+>>> TABLE OF CONTENTS:
+----------------------------------------------------------------
+1.0 - BP Generic, Typography & Imagery
+
+2.0 - Navigation - General
+	2.1 - Navs - Object Nav / Sub Nav (item-list-tabs)
+	2.2 - Pagination
+
+3.0 - BP Lists / Loops Generic & filters
+	3.1 - Activity Loop
+		3.1.1 Whats New Activity
+		3.1.2 - Activity Entries
+		3.1.3 - Activity Comments
+	3.2 - Blogs Loop
+	3.3 - Groups Loop
+	3.4 - Members Loop
+
+4.0 - Directories - Members, Groups, Blogs, Register, Activation
+	4.1 - Groups Creation Steps Screens
+5.0 - Single Item screens: User Account & Single Group Screens
+	5.1 - Item Headers: Global
+		5.1.1 - item-header: Groups
+		5.1.2 - item-header: User Accounts
+	5.2 - Item Body: Global
+		5.2.1 - item-body: Groups
+			5.2.1.1 - Management settings screens
+			5.2.1.2 - Group Members list
+			5.2.1.3 - Group Invite list
+			5.2.1.4 - Group Activity
+		5.2.2 - item-body: User Accounts
+			5.2.2.1 - classes, pag, filters
+			5.2.2.2 - Extended Profiles
+			5.2.2.3 - Groups
+			5.2.2.4 - friends
+			5.2.2.5 - Private Messaging Threads
+			5.2.2.6 - Settings
+
+6.0 - Forms  -  General
+	6.1 - Dir Search
+
+7.0 - Tables -  General
+
+8.0 - Classes - Messages, Ajax, Widgets, Buttons, Tooltips
+
+9.0 - Layout Classes.
+--------------------------------------------------------------*/
+
+/**
+*-------------------------------------------------------------------------------
+* @section 1.0 - BP Generic, Typography & Imagery
+*-------------------------------------------------------------------------------
+*/
+
+// Import our generic elements & typography styles
+
+@import "../common-styles/_bp_generic_and_typography";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 2.0 - Navigation - General
+*-------------------------------------------------------------------------------
+*/
+
+
+/**
+*----------------------------------------------------------
+* @section 2.1 - Navs Object Nav / Sub Nav (bp-list)
+*
+* The main navigational elements for all BP screens
+*----------------------------------------------------------
+*/
+
+// Import our generic navigation styles
+
+@import "../common-styles/_bp_navigation";
+
+// Import our Filter styles
+
+@import "../common-styles/_bp_filters";
+
+/**
+*----------------------------------------------------------
+* @section 2.2 - Pagination
+*----------------------------------------------------------
+*/
+
+// Import our generic pagination styles
+
+@import "../common-styles/_bp_pagination";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 3.0 - BP Lists / Loops Generic
+*-------------------------------------------------------------------------------
+*/
+
+// Import our generic list styles
+
+@import "../common-styles/_bp_lists";
+
+/**
+*----------------------------------------------------------
+* @section 3.1 - Activity Loop
+*----------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------
+* @section 3.1.1 - Activity Whats New
+*-----------------------------------------------------
+*/
+
+@import "../common-styles/_bp_update_form";
+
+/**
+*-----------------------------------------------------
+* @section 3.1.2 - Activity Entries
+*-----------------------------------------------------
+*/
+
+@import "../common-styles/_bp_activity_entries";
+
+/**
+*-----------------------------------------------------
+* @section 3.1.3 - Activity Comments
+*-----------------------------------------------------
+*/
+
+@import "../common-styles/_bp_activity_comments";
+@import "../common-styles/_bp_comment_form";
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Blogs Loop
+*----------------------------------------------------------
+*/
+
+// Blogs loop
+
+@import "../common-styles/_bp_blogs_loop";
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Groups Loop
+*----------------------------------------------------------
+*/
+
+// Groups loop
+
+@import "../common-styles/_bp_groups_loop";
+
+
+/**
+*----------------------------------------------------------
+* @section 3.2 - Members Loop
+*----------------------------------------------------------
+*/
+
+// Members loop
+
+@import "../common-styles/_bp_members_loop";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 4.0 - Directories
+*-------------------------------------------------------------------------------
+*/
+
+// Registration index screen (directory)
+
+@import "../common-styles/_bp_registration.scss";
+
+/**
+*----------------------------------------------------------
+* @section 4.1 - Groups Creation Steps
+*----------------------------------------------------------
+*/
+
+// Registration index screen (directory)
+
+@import "../common-styles/_bp_groups_create.scss";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 5.0 - Single Item screens: Groups, Users
+*-------------------------------------------------------------------------------
+*/
+
+/**
+*-----------------------------------------------------------
+* @subsection 5.1 - Item Header Global
+*-----------------------------------------------------------
+*/
+
+// Import general global styles for single screen headers
+
+@import "../common-styles/_bp_item_header_general";
+
+// Import Cover Image container defaults
+
+@import "../common-styles/_bp_cover_image";
+
+/**
+*-----------------------------------------------------
+* @subsection 5.1.1 - item-header Groups
+*
+* Group Specific Item Header
+*-----------------------------------------------------
+*/
+
+// Single Group specific header rules
+
+@import "../common-styles/_bp_group_header";
+
+/**
+*-----------------------------------------------------
+* @subsection 5.1.2 - Item Header User Accounts
+*
+* User Accounts Specific Item Header
+*-----------------------------------------------------
+*/
+
+// Single User specific header rules
+
+@import "../common-styles/_bp_user_header";
+
+/**
+*-----------------------------------------------------------
+* @subsection 5.2 - Item Body: Global
+*-----------------------------------------------------------
+*/
+
+// Import general global styles for single screen item-body
+
+@import "../common-styles/_bp_item_body_general";
+
+/**
+*----------------------------------------------------
+* @subsection 5.2.1 - Item Body Groups
+*
+* Groups specific item body rules -  screens
+*----------------------------------------------------
+*/
+
+// Import general global styles for single screen item-body
+
+@import "../common-styles/_bp_groups_item_body";
+
+/**
+*-----------------------------------------
+* @subsection 5.2.1.1 - Management Settings Screens
+*-----------------------------------------
+*/
+
+// Import general global styles for single screen item-body
+
+@import "../common-styles/_bp_groups_management";
+
+/**
+*-----------------------------------------
+* @subsection 5.2.1.2 - Group Members List
+*-----------------------------------------
+*/
+
+/*
+*-----------------------------------------
+* @subsection 5.2.1.3 - Group Invites List
+*-----------------------------------------
+*/
+
+// Import Group Invites screens
+
+//@import "../common-styles/_bp_groups_invites";
+
+// Import Nouveau style Group Invites screens
+
+@import "_nouveau_invites";
+
+/*
+*-----------------------------------------
+* @subsection 5.2.1.4 - Group Activity
+*-----------------------------------------
+*/
+
+@import "../common-styles/_bp_groups_activity";
+
+/**
+*-----------------------------------------------------
+* @subsection 5.2.2 - Item Body User Accounts
+*
+* User Account specific item body rules
+*-----------------------------------------------------
+*/
+
+/**
+*--------------------------------------------
+* @subsection 5.2.2.1 - classes, pag, filters
+*--------------------------------------------
+*/
+
+/**
+*-------------------------------------------
+* @subsection 5.2.2.2 - Extended Profiles
+*-------------------------------------------
+*/
+
+// Import User Profile
+
+@import "../common-styles/_bp_user_profile";
+
+/**
+*-------------------------------------------
+* @subsection 5.2.2.3 - Groups
+*-------------------------------------------
+*/
+
+/**
+*-------------------------------------------
+* @subsection 5.2.2.5 - Private Messaging
+*-------------------------------------------
+*/
+
+// Import Messages module
+// Nouveau is using custom markup and only needs to load
+// it's own partial file for styles.
+
+// @import "../common-styles/_bp_messages";
+
+// Import Nouveau specific Messages module styles
+
+@import "_nouveau_messages";
+
+/**
+*------------------------------------------
+* @subsection 5.2.2.6 - Settings
+*------------------------------------------
+*/
+
+// Import settings screens
+
+@import "../common-styles/_bp_user_settings";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 6.0 - Forms  - General
+*-------------------------------------------------------------------------------
+*/
+
+// Import general forms rules
+
+@import "../common-styles/_bp_forms";
+
+
+/**
+*----------------------------------------------------------
+* @section 6.1 - Directory Search
+*
+* The Search form & controls in directory pages
+*----------------------------------------------------------
+*/
+
+// Import our generic list styles
+
+@import "../common-styles/_bp_search";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 7.0 - Tables - General
+*-------------------------------------------------------------------------------
+*/
+
+// Import Table defaults rules
+
+@import "../common-styles/_bp_tables";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 8.0 - Classes - Messages, Ajax, Widgets, Buttons
+*-------------------------------------------------------------------------------
+*/
+// Import General useful classes: clearfix, screen-reader etc
+
+@import "../common-styles/_bp_general_classes";
+
+// Import Buttons styles
+
+@import "../common-styles/_bp_buttons";
+
+// Import Messages/info dialogues
+
+@import "../common-styles/_bp_info_messages";
+
+// Import BP Widget styles
+
+@import "../common-styles/_bp_widgets";
+
+// Import BP Animations & Transitions - fluffy effects :)
+
+@import "../common-styles/_bp_animations";
+
+// Import BP Tooltips
+
+@import "../common-styles/_bp_tooltips";
+
+/**
+*-------------------------------------------------------------------------------
+* @section 9.0 - Layout classes
+*-------------------------------------------------------------------------------
+*/
+// Import Layout classes
+
+@import "../common-styles/_bp_layouts";
+
Index: /tags/4.0.0-beta1/src/bp-xprofile/actions/delete-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/actions/delete-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/actions/delete-avatar.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+/**
+ * XProfile: Avatar deletion action handler
+ *
+ * @package BuddyPress
+ * @subpackage XProfileActions
+ * @since 3.0.0
+ */
+
+/**
+ * This function runs when an action is set for a screen:
+ * example.com/members/andy/profile/change-avatar/ [delete-avatar]
+ *
+ * The function will delete the active avatar for a user.
+ *
+ * @since 1.0.0
+ *
+ */
+function xprofile_action_delete_avatar() {
+
+	if ( ! bp_is_user_change_avatar() || ! bp_is_action_variable( 'delete-avatar', 0 ) ) {
+		return false;
+	}
+
+	// Check the nonce.
+	check_admin_referer( 'bp_delete_avatar_link' );
+
+	if ( ! bp_is_my_profile() && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	if ( bp_core_delete_existing_avatar( array( 'item_id' => bp_displayed_user_id() ) ) ) {
+		bp_core_add_message( __( 'Your profile photo was deleted successfully!', 'buddypress' ) );
+	} else {
+		bp_core_add_message( __( 'There was a problem deleting your profile photo. Please try again.', 'buddypress' ), 'error' );
+	}
+
+	bp_core_redirect( wp_get_referer() );
+}
+add_action( 'bp_actions', 'xprofile_action_delete_avatar' );
Index: /tags/4.0.0-beta1/src/bp-xprofile/admin/css/admin-rtl.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/admin/css/admin-rtl.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/admin/css/admin-rtl.css	(revision 12265)
@@ -0,0 +1,365 @@
+/* Profile field group main admin page */
+
+#profile-field-form {
+	margin-top: 12px;
+}
+
+#tabs {
+	position: relative;
+}
+
+p.nofields {
+	margin: 20px 0 0;
+}
+
+/* Field group tabs */
+ul#field-group-tabs {
+	float: right;
+	padding: 0 15px 0 0;
+	margin: 0;
+	display: none;
+}
+
+ul#field-group-tabs li {
+	float: right;
+	margin-left: 8px;
+	margin-bottom: -1px;
+}
+
+ul#field-group-tabs li.ui-state-hover a.ui-tab {
+	color: #d54e21;
+}
+
+ul#field-group-tabs li.ui-state-acceptable:not(.ui-state-active) a.ui-tab {
+	animation: tab-throb 2s infinite;
+}
+
+ul#field-group-tabs li.drop-candidate.ui-state-acceptable:not(.ui-state-active) a.ui-tab {
+	-o-box-shadow: 0 0 10px #0f0;
+	box-shadow: 0 0 10px #0f0;
+	animation: none;
+	color: #000;
+}
+
+ul#field-group-tabs li a.ui-tab {
+	font-size: 14px;
+	display: block;
+	margin-top: 3px;
+	padding: 5px 10px 6px;
+	border: 1px solid #e5e5e5;
+	background-color: #f5f5f5;
+	text-decoration: none;
+	color: #000;
+	outline: none;
+}
+
+/* Selected tab */
+ul#field-group-tabs li.ui-state-active a.ui-tab {
+	background-color: #fff;
+	margin-top: 0;
+	padding: 7px 10px;
+	border-bottom: 1px solid transparent;
+	color: #000;
+}
+
+/* Toolbar */
+.tab-toolbar {
+	clear: right;
+	background: #fff;
+	border-width: 1px;
+	border-color: #e5e5e5;
+	border-style: solid;
+	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+	padding: 10px;
+	margin-bottom: 10px;
+}
+
+.bp-option a.delete,
+.field-wrapper a.deletion,
+.tab-toolbar a.deletion {
+	color: #a00;
+	border-bottom: 1px solid #a00;
+	text-decoration: none;
+	padding: 0;
+}
+
+div.delete-button {
+	float: left;
+	line-height: 28px;
+}
+
+div.field-group {
+	clear: right;
+}
+
+.field-group {
+	cursor: default;
+	border: 1px solid #e5e5e5;
+	background: #fff;
+	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+	margin-top: 10px;
+	padding: 0 20px 20px;
+}
+
+.field-group .xprofile-field {
+	position: relative;
+}
+
+.field-group fieldset {
+	position: relative;
+	border: 1px solid #ddd;
+	margin: 20px 0 0;
+	cursor: move;
+	background: #fafafa;
+	padding: 10px 0 0;
+}
+
+.field-group fieldset legend {
+	position: absolute;
+	top: 10px;
+	right: 10px;
+	padding: 0;
+}
+
+fieldset.radio div div label,
+fieldset.checkbox div label {
+	margin-left: 20px;
+}
+
+fieldset.clear-value {
+	margin-right: 10px;
+}
+
+fieldset div.field-wrapper {
+	padding: 25px 10px 10px;
+}
+
+.field-group div.actions {
+	float: none;
+	border-top: 1px solid #ddd;
+	margin: 10px 0 0;
+	padding-top: 10px;
+}
+
+.field-group div.actions a,
+.field-group div.actions button,
+.field-group div.actions input {
+	float: none;
+}
+
+.field-group fieldset:hover {
+	border-color: #999;
+}
+
+.field-group fieldset:hover div.actions {
+	display: block;
+}
+
+.bp-options-box .ui-sortable-placeholder,
+#field-group-tabs .ui-sortable-placeholder,
+.field-group fieldset.ui-sortable-placeholder {
+	border: 1px dashed #999;
+	background-color: #fff;
+	visibility: visible !important;
+}
+
+#field-group-tabs .ui-sortable-placeholder {
+	background: transparent;
+	border-bottom: none;
+	margin: -1px 0 -1px 6px;
+}
+
+ul.forTab {
+	list-style: none;
+	padding: 0;
+	margin: 0 1em 0 0;
+}
+
+ul.forTab li {
+	margin: 0 0 1em;
+}
+
+ul.forTab li label {
+	display: block;
+}
+
+ul.forTab li input {
+	font-size: 1.4em;
+}
+
+p.success {
+	background: #0f0;
+}
+
+p.err {
+	border-top: 2px solid #f00;
+	border-bottom: 2px solid #f00;
+	color: #f00;
+	padding: 5px 0;
+	width: 40%;
+}
+
+span.desc,
+span.signup-description {
+	display: block;
+	font-size: 11px;
+	color: #555;
+}
+
+ul.multi-checkbox {
+	margin: 0 0 0 5px;
+	padding: 0.5em 0.9em;
+	height: 10em;
+	overflow: auto;
+	list-style: none;
+	border: solid 1px #ccc;
+	width: 90%;
+}
+
+ul.multi-checkbox li {
+	padding: 0;
+	margin: 0;
+}
+
+div.bp-option {
+	background: #fafafa;
+	border: 1px solid #dfdfdf;
+	margin: 10px 0;
+	padding: 10px 5px;
+}
+
+div.bp-option:hover {
+	border: 1px solid #999;
+}
+
+.bp-option-icon {
+	font: 400 20px/1 dashicons;
+	speak: none;
+	display: inline-block;
+	padding: 0 2px 0 0;
+	top: 5px;
+	position: relative;
+	vertical-align: top;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	text-decoration: none !important;
+	color: #888;
+}
+
+div.bp-option .grabber:before {
+	content: "\f329";
+}
+
+th a {
+	background: #fff;
+	padding: 2px 5px;
+	top: -2px;
+}
+
+textarea#description,
+textarea#group_description {
+	border: 1px solid #ddd;
+	width: 100%;
+}
+
+#bp-xprofile-add-field .titlewrap,
+#bp-xprofile-add-field-group .titlewrap {
+	margin-bottom: 20px;
+}
+
+#bp-xprofile-add-field #post-body-content {
+	margin-bottom: 0;
+}
+
+span.member-type-none-notice,
+p.member-type-none-notice {
+	color: #f00;
+}
+
+.hide {
+	display: none;
+	visibility: hidden;
+}
+
+/* xProfile Field Type - Date */
+
+.form-table.bp-date-options {
+	margin: 0 8px;
+}
+
+.form-table.bp-date-options th {
+	width: 150px;
+}
+
+.form-table td .date-format-section select {
+	margin-top: -2px;
+}
+
+.date-format-label {
+	display: inline-block;
+	width: 150px;
+}
+
+.date-format-custom-value,
+.date-range-numeric {
+	width: 56px;
+}
+
+.bp-date-range-type-label {
+	float: right;
+	font-style: italic;
+	width: auto;
+}
+
+.bp-date-range-type-values {
+	margin-left: 25px;
+	padding-right: 100px;
+}
+
+#bp-xprofile-add-field select#fieldtype {
+	max-width: 100%;
+	word-wrap: break-word;
+	word-break: break-all; /* e.g. Chinese, Japanese, Korean chars */
+}
+
+@media screen and (max-width: 782px) {
+
+	.bp-date-formats .bp-date-format-option:first-of-type {
+		margin-top: 1em;
+	}
+
+	.bp-range-types .bp-date-format-option {
+		margin-bottom: 1em;
+		margin-top: 1em;
+	}
+
+	.form-table td input.date-format-custom-value,
+	.form-table td input.date-range-numeric {
+		display: inline-block;
+		margin-left: 10px;
+		width: 56px;
+	}
+
+	.form-table td .bp-date-range-type-values select {
+		display: inline-block;
+		width: auto;
+	}
+
+	.bp-date-range-type-label {
+		float: none;
+		width: auto;
+	}
+
+	.bp-date-range-type-values {
+		padding-right: 0;
+		margin-right: 35px;
+	}
+}
+
+@media screen and (max-width: 480px) {
+
+	.form-table .bp-range-types label {
+		clear: both;
+		display: block;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/admin/css/admin.css
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/admin/css/admin.css	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/admin/css/admin.css	(revision 12265)
@@ -0,0 +1,365 @@
+/* Profile field group main admin page */
+
+#profile-field-form {
+	margin-top: 12px;
+}
+
+#tabs {
+	position: relative;
+}
+
+p.nofields {
+	margin: 20px 0 0;
+}
+
+/* Field group tabs */
+ul#field-group-tabs {
+	float: left;
+	padding: 0 0 0 15px;
+	margin: 0;
+	display: none;
+}
+
+ul#field-group-tabs li {
+	float: left;
+	margin-right: 8px;
+	margin-bottom: -1px;
+}
+
+ul#field-group-tabs li.ui-state-hover a.ui-tab {
+	color: #d54e21;
+}
+
+ul#field-group-tabs li.ui-state-acceptable:not(.ui-state-active) a.ui-tab {
+	animation: tab-throb 2s infinite;
+}
+
+ul#field-group-tabs li.drop-candidate.ui-state-acceptable:not(.ui-state-active) a.ui-tab {
+	-o-box-shadow: 0 0 10px #0f0;
+	box-shadow: 0 0 10px #0f0;
+	animation: none;
+	color: #000;
+}
+
+ul#field-group-tabs li a.ui-tab {
+	font-size: 14px;
+	display: block;
+	margin-top: 3px;
+	padding: 5px 10px 6px;
+	border: 1px solid #e5e5e5;
+	background-color: #f5f5f5;
+	text-decoration: none;
+	color: #000;
+	outline: none;
+}
+
+/* Selected tab */
+ul#field-group-tabs li.ui-state-active a.ui-tab {
+	background-color: #fff;
+	margin-top: 0;
+	padding: 7px 10px;
+	border-bottom: 1px solid transparent;
+	color: #000;
+}
+
+/* Toolbar */
+.tab-toolbar {
+	clear: left;
+	background: #fff;
+	border-width: 1px;
+	border-color: #e5e5e5;
+	border-style: solid;
+	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+	padding: 10px;
+	margin-bottom: 10px;
+}
+
+.bp-option a.delete,
+.field-wrapper a.deletion,
+.tab-toolbar a.deletion {
+	color: #a00;
+	border-bottom: 1px solid #a00;
+	text-decoration: none;
+	padding: 0;
+}
+
+div.delete-button {
+	float: right;
+	line-height: 28px;
+}
+
+div.field-group {
+	clear: left;
+}
+
+.field-group {
+	cursor: default;
+	border: 1px solid #e5e5e5;
+	background: #fff;
+	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+	margin-top: 10px;
+	padding: 0 20px 20px;
+}
+
+.field-group .xprofile-field {
+	position: relative;
+}
+
+.field-group fieldset {
+	position: relative;
+	border: 1px solid #ddd;
+	margin: 20px 0 0;
+	cursor: move;
+	background: #fafafa;
+	padding: 10px 0 0;
+}
+
+.field-group fieldset legend {
+	position: absolute;
+	top: 10px;
+	left: 10px;
+	padding: 0;
+}
+
+fieldset.radio div div label,
+fieldset.checkbox div label {
+	margin-right: 20px;
+}
+
+fieldset.clear-value {
+	margin-left: 10px;
+}
+
+fieldset div.field-wrapper {
+	padding: 25px 10px 10px;
+}
+
+.field-group div.actions {
+	float: none;
+	border-top: 1px solid #ddd;
+	margin: 10px 0 0;
+	padding-top: 10px;
+}
+
+.field-group div.actions a,
+.field-group div.actions button,
+.field-group div.actions input {
+	float: none;
+}
+
+.field-group fieldset:hover {
+	border-color: #999;
+}
+
+.field-group fieldset:hover div.actions {
+	display: block;
+}
+
+.bp-options-box .ui-sortable-placeholder,
+#field-group-tabs .ui-sortable-placeholder,
+.field-group fieldset.ui-sortable-placeholder {
+	border: 1px dashed #999;
+	background-color: #fff;
+	visibility: visible !important;
+}
+
+#field-group-tabs .ui-sortable-placeholder {
+	background: transparent;
+	border-bottom: none;
+	margin: -1px 6px -1px 0;
+}
+
+ul.forTab {
+	list-style: none;
+	padding: 0;
+	margin: 0 0 0 1em;
+}
+
+ul.forTab li {
+	margin: 0 0 1em;
+}
+
+ul.forTab li label {
+	display: block;
+}
+
+ul.forTab li input {
+	font-size: 1.4em;
+}
+
+p.success {
+	background: #0f0;
+}
+
+p.err {
+	border-top: 2px solid #f00;
+	border-bottom: 2px solid #f00;
+	color: #f00;
+	padding: 5px 0;
+	width: 40%;
+}
+
+span.desc,
+span.signup-description {
+	display: block;
+	font-size: 11px;
+	color: #555;
+}
+
+ul.multi-checkbox {
+	margin: 0 5px 0 0;
+	padding: 0.5em 0.9em;
+	height: 10em;
+	overflow: auto;
+	list-style: none;
+	border: solid 1px #ccc;
+	width: 90%;
+}
+
+ul.multi-checkbox li {
+	padding: 0;
+	margin: 0;
+}
+
+div.bp-option {
+	background: #fafafa;
+	border: 1px solid #dfdfdf;
+	margin: 10px 0;
+	padding: 10px 5px;
+}
+
+div.bp-option:hover {
+	border: 1px solid #999;
+}
+
+.bp-option-icon {
+	font: 400 20px/1 dashicons;
+	speak: none;
+	display: inline-block;
+	padding: 0 0 0 2px;
+	top: 5px;
+	position: relative;
+	vertical-align: top;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	text-decoration: none !important;
+	color: #888;
+}
+
+div.bp-option .grabber:before {
+	content: "\f329";
+}
+
+th a {
+	background: #fff;
+	padding: 2px 5px;
+	top: -2px;
+}
+
+textarea#description,
+textarea#group_description {
+	border: 1px solid #ddd;
+	width: 100%;
+}
+
+#bp-xprofile-add-field .titlewrap,
+#bp-xprofile-add-field-group .titlewrap {
+	margin-bottom: 20px;
+}
+
+#bp-xprofile-add-field #post-body-content {
+	margin-bottom: 0;
+}
+
+span.member-type-none-notice,
+p.member-type-none-notice {
+	color: #f00;
+}
+
+.hide {
+	display: none;
+	visibility: hidden;
+}
+
+/* xProfile Field Type - Date */
+
+.form-table.bp-date-options {
+	margin: 0 8px;
+}
+
+.form-table.bp-date-options th {
+	width: 150px;
+}
+
+.form-table td .date-format-section select {
+	margin-top: -2px;
+}
+
+.date-format-label {
+	display: inline-block;
+	width: 150px;
+}
+
+.date-format-custom-value,
+.date-range-numeric {
+	width: 56px;
+}
+
+.bp-date-range-type-label {
+	float: left;
+	font-style: italic;
+	width: auto;
+}
+
+.bp-date-range-type-values {
+	margin-right: 25px;
+	padding-left: 100px;
+}
+
+#bp-xprofile-add-field select#fieldtype {
+	max-width: 100%;
+	word-wrap: break-word;
+	word-break: break-all; /* e.g. Chinese, Japanese, Korean chars */
+}
+
+@media screen and (max-width: 782px) {
+
+	.bp-date-formats .bp-date-format-option:first-of-type {
+		margin-top: 1em;
+	}
+
+	.bp-range-types .bp-date-format-option {
+		margin-bottom: 1em;
+		margin-top: 1em;
+	}
+
+	.form-table td input.date-format-custom-value,
+	.form-table td input.date-range-numeric {
+		display: inline-block;
+		margin-right: 10px;
+		width: 56px;
+	}
+
+	.form-table td .bp-date-range-type-values select {
+		display: inline-block;
+		width: auto;
+	}
+
+	.bp-date-range-type-label {
+		float: none;
+		width: auto;
+	}
+
+	.bp-date-range-type-values {
+		padding-left: 0;
+		margin-left: 35px;
+	}
+}
+
+@media screen and (max-width: 480px) {
+
+	.form-table .bp-range-types label {
+		clear: both;
+		display: block;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/admin/js/admin.js
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/admin/js/admin.js	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/admin/js/admin.js	(revision 12265)
@@ -0,0 +1,370 @@
+/* exported add_option, show_options, hide, fixHelper */
+/* jshint scripturl: true */
+/* global XProfileAdmin, ajaxurl */
+
+/**
+ * Add option for the forWhat type.
+ *
+ * @param {string} forWhat Value of the field to show options for
+ */
+function add_option(forWhat) {
+	var holder       = document.getElementById(forWhat + '_more'),
+		theId        = document.getElementById(forWhat + '_option_number').value,
+		newDiv       = document.createElement( 'div' ),
+		grabber      = document.createElement( 'span' ),
+		newOption    = document.createElement( 'input' ),
+		label        = document.createElement( 'label' ),
+		isDefault    = document.createElement( 'input' ),
+		txt1         = document.createTextNode( 'Default Value' ),
+		toDeleteText = document.createTextNode( 'Delete' ),
+		toDeleteWrap = document.createElement( 'div' ),
+		toDelete     = document.createElement( 'a' );
+
+	newDiv.setAttribute('id', forWhat + '_div' + theId);
+	newDiv.setAttribute('class', 'bp-option sortable');
+
+	grabber.setAttribute( 'class', 'bp-option-icon grabber');
+
+	newOption.setAttribute( 'type', 'text' );
+	newOption.setAttribute( 'name', forWhat + '_option[' + theId + ']' );
+	newOption.setAttribute( 'id', forWhat + '_option' + theId );
+
+	if ( forWhat === 'checkbox' || forWhat === 'multiselectbox' ) {
+		isDefault.setAttribute( 'type', 'checkbox' );
+		isDefault.setAttribute( 'name', 'isDefault_' + forWhat + '_option[' + theId + ']' );
+	} else {
+		isDefault.setAttribute( 'type', 'radio' );
+		isDefault.setAttribute( 'name', 'isDefault_' + forWhat + '_option' );
+	}
+
+	isDefault.setAttribute( 'value', theId );
+
+	toDelete.setAttribute( 'href', 'javascript:hide("' + forWhat + '_div' + theId + '")' );
+	toDelete.setAttribute( 'class', 'delete' );
+	toDelete.appendChild( toDeleteText );
+
+	toDeleteWrap.setAttribute( 'class', 'delete-button' );
+	toDeleteWrap.appendChild( toDelete );
+
+	label.appendChild( document.createTextNode( ' ' ) );
+	label.appendChild( isDefault );
+	label.appendChild( document.createTextNode( ' ' ) );
+	label.appendChild( txt1 );
+	label.appendChild( document.createTextNode( ' ' ) );
+
+	newDiv.appendChild( grabber );
+	newDiv.appendChild( document.createTextNode( ' ' ) );
+	newDiv.appendChild( newOption );
+	newDiv.appendChild( label );
+	newDiv.appendChild( toDeleteWrap );
+	holder.appendChild( newDiv );
+
+	// re-initialize the sortable ui
+	enableSortableFieldOptions( forWhat );
+
+	// set focus on newly created element
+	document.getElementById(forWhat + '_option' + theId).focus();
+
+	theId++;
+
+	document.getElementById(forWhat + '_option_number').value = theId;
+}
+
+/**
+ * Hide all "options" sections, and show the options section for the forWhat type.
+ *
+ * @param {string} forWhat Value of the field to show options for
+ */
+function show_options( forWhat ) {
+	var do_autolink;
+
+	for ( var i = 0; i < XProfileAdmin.do_settings_section_field_types.length; i++ ) {
+		document.getElementById( XProfileAdmin.do_settings_section_field_types[i] ).style.display = 'none';
+	}
+
+	if ( XProfileAdmin.do_settings_section_field_types.indexOf( forWhat ) >= 0 ) {
+		document.getElementById( forWhat ).style.display = '';
+		do_autolink = 'on';
+	} else {
+		jQuery( '#do-autolink' ).val( '' );
+		do_autolink = '';
+	}
+
+	// Only overwrite the do_autolink setting if no setting is saved in the database.
+	if ( '' === XProfileAdmin.do_autolink ) {
+		jQuery( '#do-autolink' ).val( do_autolink );
+	}
+
+	jQuery( document ).trigger( 'bp-xprofile-show-options', forWhat );
+}
+
+function hide( id ) {
+	if ( !document.getElementById( id ) ) {
+		return false;
+	}
+
+	document.getElementById( id ).style.display = 'none';
+	// the field id is [fieldtype]option[iterator] and not [fieldtype]div[iterator]
+	var field_id = id.replace( 'div', 'option' );
+	document.getElementById( field_id ).value = '';
+}
+
+/**
+ * @summary Toggles "no member type" notice.
+ *
+ * @since 2.4.0
+ */
+function toggle_no_member_type_notice() {
+	var $member_type_checkboxes = jQuery( 'input.member-type-selector' );
+
+	// No checkboxes? Nothing to do.
+	if ( ! $member_type_checkboxes.length ) {
+		return;
+	}
+
+	var has_checked = false;
+	$member_type_checkboxes.each( function() {
+		if ( jQuery( this ).is( ':checked' ) ) {
+			has_checked = true;
+			return false;
+		}
+	} );
+
+	if ( has_checked ) {
+		jQuery( 'p.member-type-none-notice' ).addClass( 'hide' );
+	} else {
+		jQuery( 'p.member-type-none-notice' ).removeClass( 'hide' );
+	}
+}
+
+var fixHelper = function(e, ui) {
+	ui.children().each(function() {
+		jQuery(this).width( jQuery(this).width() );
+	});
+	return ui;
+};
+
+function enableSortableFieldOptions() {
+	jQuery( '.bp-options-box' ).sortable( {
+		cursor: 'move',
+		items: 'div.sortable',
+		tolerance: 'intersect',
+		axis: 'y'
+	});
+
+	jQuery( '.sortable, .sortable span' ).css( 'cursor', 'move' );
+}
+
+function destroySortableFieldOptions() {
+	jQuery( '.bp-options-box' ).sortable( 'destroy' );
+	jQuery( '.sortable, .sortable span' ).css( 'cursor', 'default' );
+}
+
+function titleHint( id ) {
+	id = id || 'title';
+
+	var title = jQuery('#' + id), titleprompt = jQuery('#' + id + '-prompt-text');
+
+	if ( '' === title.val() ) {
+		titleprompt.removeClass('screen-reader-text');
+	} else {
+		titleprompt.addClass('screen-reader-text');
+	}
+
+	titleprompt.click(function(){
+		jQuery(this).addClass('screen-reader-text');
+		title.focus();
+	});
+
+	title.blur(function(){
+		if ( '' === this.value ) {
+			titleprompt.removeClass('screen-reader-text');
+		}
+	}).focus(function(){
+		titleprompt.addClass('screen-reader-text');
+	}).keydown(function(e){
+		titleprompt.addClass('screen-reader-text');
+		jQuery(this).unbind(e);
+	});
+}
+
+jQuery( document ).ready( function() {
+
+	// Set focus in Field Title, if we're on the right page
+	jQuery( '#bp-xprofile-add-field #title' ).focus();
+
+	// Set up the notice that shows when no member types are selected for a field.
+	toggle_no_member_type_notice();
+	jQuery( 'input.member-type-selector' ).on( 'change', function() {
+		toggle_no_member_type_notice();
+	} );
+
+	// Set up deleting options ajax
+	jQuery( 'a.ajax-option-delete' ).on( 'click', function() {
+		var theId = this.id.split( '-' );
+		theId = theId[1];
+
+		jQuery.post( ajaxurl, {
+			action: 'xprofile_delete_option',
+			'cookie': encodeURIComponent( document.cookie ),
+			'_wpnonce': jQuery('input#_wpnonce').val(),
+			'option_id': theId
+		},
+		function() {} );
+	} );
+
+	// Set up the sort order change actions
+	jQuery( '[id^="sort_order_"]' ).change(function() {
+		if ( jQuery( this ).val() !== 'custom' ) {
+			destroySortableFieldOptions();
+		} else {
+			enableSortableFieldOptions( jQuery('#fieldtype :selected').val() );
+		}
+	});
+
+	// Show object if JS is enabled
+	jQuery( 'ul#field-group-tabs' ).show();
+
+	// Allow reordering of field group tabs
+	jQuery( 'ul#field-group-tabs' ).sortable( {
+		cursor: 'move',
+		axis: 'x',
+		opacity: 1,
+		items: 'li',
+		tolerance: 'intersect',
+
+		update: function() {
+			jQuery.post( ajaxurl, {
+				action: 'xprofile_reorder_groups',
+				'cookie': encodeURIComponent( document.cookie ),
+				'_wpnonce_reorder_groups': jQuery( 'input#_wpnonce_reorder_groups' ).val(),
+				'group_order': jQuery( this ).sortable( 'serialize' )
+			},
+			function() {} );
+		}
+	}).disableSelection();
+
+	// Allow reordering of fields within groups
+	jQuery( 'fieldset.field-group' ).sortable({
+		cursor: 'move',
+		opacity: 0.7,
+		items: 'fieldset',
+		tolerance: 'pointer',
+
+		update: function() {
+			jQuery.post( ajaxurl, {
+				action: 'xprofile_reorder_fields',
+				'cookie': encodeURIComponent(document.cookie),
+				'_wpnonce_reorder_fields': jQuery( 'input#_wpnonce_reorder_fields' ).val(),
+				'field_order': jQuery(this).sortable( 'serialize' ),
+				'field_group_id': jQuery(this).attr( 'id' )
+			},
+			function() {} );
+		}
+	})
+
+	// Disallow text selection
+	.disableSelection();
+
+	// Allow reordering of field options
+	enableSortableFieldOptions( jQuery('#fieldtype :selected').val() );
+
+	// Handle title placeholder text the WordPress way
+	titleHint( 'title' );
+
+	// On Date fields, selecting a date_format radio button should change the Custom value.
+	var $date_format = jQuery( 'input[name="date_format"]' );
+	var $date_format_custom_value = jQuery( '#date-format-custom-value' );
+	var $date_format_sample = jQuery( '#date-format-custom-sample' );
+	$date_format.click( function( e ) {
+		switch ( e.target.value ) {
+			case 'elapsed' :
+				$date_format_custom_value.val( '' );
+				$date_format_sample.html( '' );
+			break;
+
+			case 'custom' :
+			break;
+
+			default :
+				$date_format_custom_value.val( e.target.value );
+				$date_format_sample.html( jQuery( e.target ).siblings( '.date-format-label' ).html() );
+			break;
+		}
+	} );
+
+	// Clicking into the custom date format field should select the Custom radio button.
+	var $date_format_custom = jQuery( '#date-format-custom' );
+	$date_format_custom_value.focus( function() {
+		$date_format_custom.prop( 'checked', 'checked' );
+	} );
+
+	// Validate custom date field.
+	var $date_format_spinner = jQuery( '#date-format-custom-spinner' );
+	$date_format_custom_value.change( function( e ) {
+		$date_format_spinner.addClass( 'is-active' );
+		jQuery.post( ajaxurl, {
+			action: 'date_format',
+			date: e.target.value
+		},
+		function( response ) {
+			$date_format_spinner.removeClass( 'is-active' );
+			$date_format_sample.html( response );
+		} );
+	} );
+
+	// tabs init with a custom tab template and an "add" callback filling in the content
+	var $tab_items,
+		$tabs = jQuery( '#tabs' ).tabs();
+
+	set_tab_items( $tabs );
+
+	function set_tab_items( $tabs ) {
+		$tab_items = jQuery( 'ul:first li', $tabs ).droppable({
+			accept: '.connectedSortable fieldset',
+			hoverClass: 'ui-state-hover',
+			activeClass: 'ui-state-acceptable',
+			touch: 'pointer',
+			tolerance: 'pointer',
+
+			// When field is dropped on tab
+			drop: function( ev, ui ) {
+				var $item = jQuery(this), // The tab
+					$list = jQuery( $item.find( 'a' ).attr( 'href' ) ).find( '.connectedSortable' ); // The tab body
+
+				// Remove helper class
+				jQuery($item).removeClass( 'drop-candidate' );
+
+				// Hide field, change selected tab, and show new placement
+				ui.draggable.hide( 'slow', function() {
+
+					// Select new tab as current
+					$tabs.tabs( 'option', 'active', $tab_items.index( $item ) );
+
+					// Show new placement
+					jQuery(this).appendTo($list).show( 'slow' ).animate( {opacity: '1'}, 500 );
+
+					// Refresh $list variable
+					$list = jQuery( $item.find( 'a' ).attr( 'href' ) ).find( '.connectedSortable' );
+					jQuery($list).find( 'p.nofields' ).hide( 'slow' );
+
+					// Ajax update field locations and orders
+					jQuery.post( ajaxurl, {
+						action: 'xprofile_reorder_fields',
+						'cookie': encodeURIComponent(document.cookie),
+						'_wpnonce_reorder_fields': jQuery( 'input#_wpnonce_reorder_fields' ).val(),
+						'field_order': jQuery( $list ).sortable( 'serialize' ),
+						'field_group_id': jQuery( $list ).attr( 'id' )
+					},
+					function() {} );
+				});
+			},
+			over: function() {
+				jQuery(this).addClass( 'drop-candidate' );
+			},
+			out: function() {
+				jQuery(this).removeClass( 'drop-candidate' );
+			}
+		});
+	}
+});
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-activity.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-activity.php	(revision 12265)
@@ -0,0 +1,342 @@
+<?php
+/**
+ * BuddyPress XProfile Activity & Notification Functions.
+ *
+ * These functions handle the recording, deleting and formatting of activity
+ * items and notifications for the user and for this specific component.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileActivity
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Register the activity actions for the Extended Profile component.
+ *
+ * @since 1.0.0
+ *
+ */
+function xprofile_register_activity_actions() {
+
+	// Register the activity stream actions for this component.
+	bp_activity_set_action(
+		// Older avatar activity items use 'profile' for component. See r4273.
+		'profile',
+		'new_avatar',
+		__( 'Member changed profile picture', 'buddypress' ),
+		'bp_xprofile_format_activity_action_new_avatar',
+		__( 'Updated Profile Photos', 'buddypress' )
+	);
+
+	bp_activity_set_action(
+		buddypress()->profile->id,
+		'updated_profile',
+		__( 'Updated Profile', 'buddypress' ),
+		'bp_xprofile_format_activity_action_updated_profile',
+		__( 'Profile Updates', 'buddypress' ),
+		array( 'activity' )
+	);
+
+	/**
+	 * Fires after the registration of default activity actions for Extended Profile component.
+	 *
+	 * @since 1.1.0
+	 */
+	do_action( 'xprofile_register_activity_actions' );
+}
+add_action( 'bp_register_activity_actions', 'xprofile_register_activity_actions' );
+
+/**
+ * Format 'new_avatar' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity object.
+ * @return string
+ */
+function bp_xprofile_format_activity_action_new_avatar( $action, $activity ) {
+	$userlink = bp_core_get_userlink( $activity->user_id );
+	$action   = sprintf( __( '%s changed their profile picture', 'buddypress' ), $userlink );
+
+	// Legacy filter - pass $user_id instead of $activity.
+	if ( has_filter( 'bp_xprofile_new_avatar_action' ) ) {
+		$action = apply_filters( 'bp_xprofile_new_avatar_action', $action, $activity->user_id );
+	}
+
+	/**
+	 * Filters the formatted 'new_avatar' activity stream action.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   Formatted action for activity stream.
+	 * @param object $activity Activity object.
+	 */
+	return apply_filters( 'bp_xprofile_format_activity_action_new_avatar', $action, $activity );
+}
+
+/**
+ * Format 'updated_profile' activity actions.
+ *
+ * @since 2.0.0
+ *
+ * @param string $action   Static activity action.
+ * @param object $activity Activity object.
+ * @return string
+ */
+function bp_xprofile_format_activity_action_updated_profile( $action, $activity ) {
+
+	// Note for translators: The natural phrasing in English, "Joe updated
+	// his profile", requires that we know Joe's gender, which we don't. If
+	// your language doesn't have this restriction, feel free to use a more
+	// natural translation.
+	$profile_link = trailingslashit( bp_core_get_user_domain( $activity->user_id ) . bp_get_profile_slug() );
+	$action	      = sprintf( __( "%s's profile was updated", 'buddypress' ), '<a href="' . $profile_link . '">' . bp_core_get_user_displayname( $activity->user_id ) . '</a>' );
+
+	/**
+	 * Filters the formatted 'updated_profile' activity stream action.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $action   Formatted action for activity stream.
+	 * @param object $activity Activity object.
+	 */
+	return apply_filters( 'bp_xprofile_format_activity_action_updated_profile', $action, $activity );
+}
+
+/**
+ * Records activity for the logged in user within the profile component so that
+ * it will show in the users activity stream (if installed).
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args String containing all variables used after bp_parse_args() call.
+ * @return WP_Error|bool|int
+ */
+function xprofile_record_activity( $args = '' ) {
+
+	// Bail if activity component is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	// Parse the arguments.
+	$r = bp_parse_args( $args, array(
+		'user_id'           => bp_loggedin_user_id(),
+		'action'            => '',
+		'content'           => '',
+		'primary_link'      => '',
+		'component'         => buddypress()->profile->id,
+		'type'              => false,
+		'item_id'           => false,
+		'secondary_item_id' => false,
+		'recorded_time'     => bp_core_current_time(),
+		'hide_sitewide'     => false
+	) );
+
+	return bp_activity_add( $r );
+}
+
+/**
+ * Deletes activity for a user within the profile component so that it will be
+ * removed from the users activity stream and sitewide stream (if installed).
+ *
+ * @since 1.0.0
+ *
+ *                            for a specific activity.
+ *
+ * @param array|string $args Containing all variables used after bp_parse_args() call.
+ * @return bool
+ */
+function xprofile_delete_activity( $args = '' ) {
+
+	// Bail if activity component is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	// Parse the arguments.
+	$r = bp_parse_args( $args, array(
+		'component' => buddypress()->profile->id
+	), 'xprofile_delete_activity' );
+
+	// Delete the activity item.
+	bp_activity_delete_by_item_id( $r );
+}
+
+/**
+ * Register an activity action for the Extended Profiles component.
+ *
+ * @since 1.0.0
+ *
+ * @param string $key Key.
+ * @param string $value Value.
+ * @return bool True if success, false on failure.
+ */
+function xprofile_register_activity_action( $key, $value ) {
+
+	// Bail if activity component is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters the return value of bp_activity_set_action.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param bool   $value Whether or not an action was successfully registered.
+	 * @param string $key   Key used for the registered action.
+	 * @param string $value Value used for the registered action.
+	 */
+	return apply_filters( 'xprofile_register_activity_action', bp_activity_set_action( buddypress()->profile->id, $key, $value ), $key, $value );
+}
+
+/**
+ * Adds an activity stream item when a user has uploaded a new avatar.
+ *
+ * @since 1.0.0
+ * @since 2.3.4 Add new parameter to get the user id the avatar was set for.
+ *
+ *                         specific activity
+ *
+ * @param int $user_id The user id the avatar was set for.
+ * @return bool
+ */
+function bp_xprofile_new_avatar_activity( $user_id = 0 ) {
+
+	// Bail if activity component is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	/**
+	 * Filters the user ID when a user has uploaded a new avatar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int $user_id ID of the user the avatar was set for.
+	 */
+	$user_id = apply_filters( 'bp_xprofile_new_avatar_user_id', $user_id );
+
+	// Add the activity.
+	bp_activity_add( array(
+		'user_id'   => $user_id,
+		'component' => 'profile',
+		'type'      => 'new_avatar'
+	) );
+}
+add_action( 'xprofile_avatar_uploaded', 'bp_xprofile_new_avatar_activity' );
+
+/**
+ * Add an activity item when a user has updated his profile.
+ *
+ * @since 2.0.0
+ *
+ * @param int   $user_id    ID of the user who has updated his profile.
+ * @param array $field_ids  IDs of the fields submitted.
+ * @param bool  $errors     True if validation or saving errors occurred, otherwise false.
+ * @param array $old_values Pre-save xprofile field values and visibility levels.
+ * @param array $new_values Post-save xprofile field values and visibility levels.
+ * @return bool True on success, false on failure.
+ */
+function bp_xprofile_updated_profile_activity( $user_id, $field_ids = array(), $errors = false, $old_values = array(), $new_values = array() ) {
+
+	// If there were errors, don't post.
+	if ( ! empty( $errors ) ) {
+		return false;
+	}
+
+	// Bail if activity component is not active.
+	if ( ! bp_is_active( 'activity' ) ) {
+		return false;
+	}
+
+	// Don't post if there have been no changes, or if the changes are
+	// related solely to non-public fields.
+	$public_changes = false;
+	foreach ( $new_values as $field_id => $new_value ) {
+		$old_value = isset( $old_values[ $field_id ] ) ? $old_values[ $field_id ] : '';
+
+		// Don't register changes to private fields.
+		if ( empty( $new_value['visibility'] ) || ( 'public' !== $new_value['visibility'] ) ) {
+			continue;
+		}
+
+		// Don't register if there have been no changes.
+		if ( $new_value === $old_value ) {
+			continue;
+		}
+
+		// Looks like we have public changes - no need to keep checking.
+		$public_changes = true;
+		break;
+	}
+
+	// Bail if no public changes.
+	if ( empty( $public_changes ) ) {
+		return false;
+	}
+
+	// Throttle to one activity of this type per 2 hours.
+	$existing = bp_activity_get( array(
+		'max'    => 1,
+		'filter' => array(
+			'user_id' => $user_id,
+			'object'  => buddypress()->profile->id,
+			'action'  => 'updated_profile',
+		),
+	) );
+
+	// Default throttle time is 2 hours. Filter to change (in seconds).
+	if ( ! empty( $existing['activities'] ) ) {
+
+		/**
+		 * Filters the throttle time, in seconds, used to prevent excessive activity posting.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param int $value Throttle time, in seconds.
+		 */
+		$throttle_period = apply_filters( 'bp_xprofile_updated_profile_activity_throttle_time', HOUR_IN_SECONDS * 2 );
+		$then            = strtotime( $existing['activities'][0]->date_recorded );
+		$now             = bp_core_current_time( true, 'timestamp' );
+
+		// Bail if throttled.
+		if ( ( $now - $then ) < $throttle_period ) {
+			return false;
+		}
+	}
+
+	// If we've reached this point, assemble and post the activity item.
+	$profile_link = trailingslashit( bp_core_get_user_domain( $user_id ) . bp_get_profile_slug() );
+
+	return (bool) xprofile_record_activity( array(
+		'user_id'      => $user_id,
+		'primary_link' => $profile_link,
+		'component'    => buddypress()->profile->id,
+		'type'         => 'updated_profile',
+	) );
+}
+add_action( 'xprofile_updated_profile', 'bp_xprofile_updated_profile_activity', 10, 5 );
+
+/**
+ * Add filters for xprofile activity types to Show dropdowns.
+ *
+ * @since 2.0.0
+ * @todo Mark as deprecated
+ */
+function xprofile_activity_filter_options() {
+	?>
+
+	<option value="updated_profile"><?php _e( 'Profile Updates', 'buddypress' ) ?></option>
+
+	<?php
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-admin.php	(revision 12265)
@@ -0,0 +1,806 @@
+<?php
+/**
+ * BuddyPress XProfile Admin.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileAdmin
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Creates the administration interface menus and checks to see if the DB
+ * tables are set up.
+ *
+ * @since 1.0.0
+ *
+ * @return bool
+ */
+function xprofile_add_admin_menu() {
+
+	// Bail if current user cannot moderate community.
+	if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	add_users_page( _x( 'Profile Fields', 'xProfile admin page title', 'buddypress' ), _x( 'Profile Fields', 'Admin Users menu', 'buddypress' ), 'manage_options', 'bp-profile-setup', 'xprofile_admin' );
+}
+add_action( bp_core_admin_hook(), 'xprofile_add_admin_menu' );
+
+/**
+ * Handles all actions for the admin area for creating, editing and deleting
+ * profile groups and fields.
+ *
+ * @since 1.0.0
+ *
+ * @param string $message Message to display.
+ * @param string $type    Type of action to be displayed.
+ */
+function xprofile_admin( $message = '', $type = 'error' ) {
+
+	// What mode?
+	$mode = ! empty( $_GET['mode'] )
+		? sanitize_key( $_GET['mode'] )
+		: false;
+
+	// Group ID
+	$group_id = ! empty( $_GET['group_id'] )
+		? intval( $_GET['group_id'] )
+		: false;
+
+	// Field ID
+	$field_id = ! empty( $_GET['field_id'] )
+		? intval( $_GET['field_id'] )
+		: false;
+
+	// Option ID
+	$option_id = ! empty( $_GET['option_id'] )
+		? intval( $_GET['option_id'] )
+		: false;
+
+	// Allowed modes
+	$allowed_modes = array(
+		'add_group',
+		'edit_group',
+		'delete_group',
+		'add_field',
+		'edit_field',
+		'delete_field',
+		'delete_option'
+	);
+
+	// Is an allowed mode
+	if ( in_array( $mode, $allowed_modes, true ) ) {
+
+		// All group actions
+		if ( false !== $group_id ) {
+
+			// Add field to group
+			if ( 'add_field' == $mode ) {
+				xprofile_admin_manage_field( $group_id );
+
+			// Edit field of group
+			} elseif ( ! empty( $field_id ) && 'edit_field' === $mode ) {
+				xprofile_admin_manage_field( $group_id, $field_id );
+
+			// Delete group
+			} elseif ( 'delete_group' === $mode ) {
+				xprofile_admin_delete_group( $group_id );
+
+			// Edit group
+			} elseif ( 'edit_group' === $mode ) {
+				xprofile_admin_manage_group( $group_id );
+			}
+
+		// Delete field
+		} elseif ( ( false !== $field_id ) && ( 'delete_field' === $mode ) ) {
+			xprofile_admin_delete_field( $field_id, 'field');
+
+		// Delete option
+		} elseif ( ! empty( $option_id ) && 'delete_option' === $mode ) {
+			xprofile_admin_delete_field( $option_id, 'option' );
+
+		// Add group
+		} elseif ( 'add_group' == $mode ) {
+			xprofile_admin_manage_group();
+		}
+
+	} else {
+		xprofile_admin_screen( $message, $type );
+	}
+}
+
+/**
+ * Output the main XProfile management screen.
+ *
+ * @since 2.3.0
+ *
+ * @param string $message Feedback message.
+ * @param string $type    Feedback type.
+ *
+ * @todo Improve error message output
+ */
+function xprofile_admin_screen( $message = '', $type = 'error' ) {
+
+	// Users admin URL
+	$url = bp_get_admin_url( 'users.php' );
+
+	// Add Group
+	$add_group_url = add_query_arg( array(
+		'page' => 'bp-profile-setup',
+		'mode' => 'add_group'
+	), $url );
+
+	// Validate type.
+	$type = preg_replace( '|[^a-z]|i', '', $type );
+
+	// Get all of the profile groups & fields.
+	$groups = bp_xprofile_get_groups( array(
+		'fetch_fields' => true
+	) ); ?>
+
+	<div class="wrap">
+		<?php if ( version_compare( $GLOBALS['wp_version'], '4.8', '>=' ) ) : ?>
+
+			<h1 class="wp-heading-inline"><?php _ex( 'Profile Fields', 'Settings page header', 'buddypress'); ?></h1>
+
+				<a id="add_group" class="page-title-action" href="<?php echo esc_url( $add_group_url ); ?>"><?php _e( 'Add New Field Group', 'buddypress' ); ?></a>
+
+			<hr class="wp-header-end">
+
+		<?php else : ?>
+
+			<h1>
+				<?php _ex( 'Profile Fields', 'Settings page header', 'buddypress'); ?>
+				<a id="add_group" class="add-new-h2" href="<?php echo esc_url( $add_group_url ); ?>"><?php _e( 'Add New Field Group', 'buddypress' ); ?></a>
+			</h1>
+
+		<?php endif; ?>
+
+		<form action="" id="profile-field-form" method="post">
+
+			<?php
+
+			wp_nonce_field( 'bp_reorder_fields', '_wpnonce_reorder_fields'        );
+			wp_nonce_field( 'bp_reorder_groups', '_wpnonce_reorder_groups', false );
+
+			if ( ! empty( $message ) ) :
+				$type = ( $type == 'error' ) ? 'error' : 'updated'; ?>
+
+				<div id="message" class="<?php echo $type; ?> fade">
+					<p><?php echo esc_html( $message ); ?></p>
+				</div>
+
+			<?php endif; ?>
+
+			<div id="tabs" aria-live="polite" aria-atomic="true" aria-relevant="all">
+				<ul id="field-group-tabs">
+
+					<?php if ( !empty( $groups ) ) : foreach ( $groups as $group ) : ?>
+
+						<li id="group_<?php echo esc_attr( $group->id ); ?>">
+							<a href="#tabs-<?php echo esc_attr( $group->id ); ?>" class="ui-tab">
+								<?php
+								/** This filter is documented in bp-xprofile/bp-xprofile-template.php */
+								echo esc_html( apply_filters( 'bp_get_the_profile_group_name', $group->name ) );
+								?>
+
+								<?php if ( !$group->can_delete ) : ?>
+									<?php _e( '(Primary)', 'buddypress'); ?>
+								<?php endif; ?>
+
+							</a>
+						</li>
+
+					<?php endforeach; endif; ?>
+
+				</ul>
+
+				<?php if ( !empty( $groups ) ) : foreach ( $groups as $group ) :
+
+					// Add Field to Group URL
+					$add_field_url = add_query_arg( array(
+						'page'     => 'bp-profile-setup',
+						'mode'     => 'add_field',
+						'group_id' => (int) $group->id
+					), $url );
+
+					// Edit Group URL
+					$edit_group_url = add_query_arg( array(
+						'page'     => 'bp-profile-setup',
+						'mode'     => 'edit_group',
+						'group_id' => (int) $group->id
+					), $url );
+
+					// Delete Group URL
+					$delete_group_url = wp_nonce_url( add_query_arg( array(
+						'page'     => 'bp-profile-setup',
+						'mode'     => 'delete_group',
+						'group_id' => (int) $group->id
+					), $url ), 'bp_xprofile_delete_group' ); ?>
+
+					<noscript>
+						<h3><?php
+						/** This filter is documented in bp-xprofile/bp-xprofile-template.php */
+						echo esc_html( apply_filters( 'bp_get_the_profile_group_name', $group->name ) );
+						?></h3>
+					</noscript>
+
+					<div id="tabs-<?php echo esc_attr( $group->id ); ?>" class="tab-wrapper">
+						<div class="tab-toolbar">
+							<div class="tab-toolbar-left">
+								<a class="button-primary" href="<?php echo esc_url( $add_field_url ); ?>"><?php _e( 'Add New Field', 'buddypress' ); ?></a>
+								<a class="button edit" href="<?php echo esc_url( $edit_group_url ); ?>"><?php _ex( 'Edit Group', 'Edit Profile Fields Group', 'buddypress' ); ?></a>
+
+								<?php if ( $group->can_delete ) : ?>
+
+									<div class="delete-button">
+										<a class="confirm submitdelete deletion ajax-option-delete" href="<?php echo esc_url( $delete_group_url ); ?>"><?php _ex( 'Delete Group', 'Delete Profile Fields Group', 'buddypress' ); ?></a>
+									</div>
+
+								<?php endif; ?>
+
+								<?php
+
+								/**
+								 * Fires at end of action buttons in xprofile management admin.
+								 *
+								 * @since 2.2.0
+								 *
+								 * @param BP_XProfile_Group $group BP_XProfile_Group object
+								 *                                 for the current group.
+								 */
+								do_action( 'xprofile_admin_group_action', $group ); ?>
+
+							</div>
+						</div>
+
+						<?php if ( ! empty( $group->description ) ) : ?>
+
+							<p><?php
+							/** This filter is documented in bp-xprofile/bp-xprofile-template.php */
+							echo esc_html( apply_filters( 'bp_get_the_profile_group_description', $group->description ) );
+							?></p>
+
+						<?php endif; ?>
+
+						<fieldset id="<?php echo esc_attr( $group->id ); ?>" class="connectedSortable field-group" aria-live="polite" aria-atomic="true" aria-relevant="all">
+							<legend class="screen-reader-text"><?php
+							/** This filter is documented in bp-xprofile/bp-xprofile-template.php */
+							/* translators: accessibility text */
+							printf( esc_html__( 'Fields for "%s" Group', 'buddypress' ), apply_filters( 'bp_get_the_profile_group_name', $group->name ) );
+							?></legend>
+
+							<?php
+
+							if ( !empty( $group->fields ) ) :
+								foreach ( $group->fields as $field ) {
+
+									// Load the field.
+									$field = xprofile_get_field( $field->id );
+
+									$class = '';
+									if ( empty( $field->can_delete ) ) {
+										$class = ' core nodrag';
+									}
+
+									/**
+									 * This function handles the WYSIWYG profile field
+									 * display for the xprofile admin setup screen.
+									 */
+									xprofile_admin_field( $field, $group, $class );
+
+								} // end for
+
+							else : // !$group->fields ?>
+
+								<p class="nodrag nofields"><?php _e( 'There are no fields in this group.', 'buddypress' ); ?></p>
+
+							<?php endif; // End $group->fields. ?>
+
+						</fieldset>
+
+						<?php if ( empty( $group->can_delete ) ) : ?>
+
+							<p><?php esc_html_e( '* Fields in this group appear on the signup page.', 'buddypress' ); ?></p>
+
+						<?php endif; ?>
+
+					</div>
+
+				<?php endforeach; else : ?>
+
+					<div id="message" class="error"><p><?php _ex( 'You have no groups.', 'You have no profile fields groups.', 'buddypress' ); ?></p></div>
+					<p><a href="<?php echo esc_url( $add_group_url ); ?>"><?php _ex( 'Add New Group', 'Add New Profile Fields Group', 'buddypress' ); ?></a></p>
+
+				<?php endif; ?>
+
+			</div>
+		</form>
+	</div>
+
+<?php
+}
+
+/**
+ * Handles the adding or editing of groups.
+ *
+ * @since 1.0.0
+ *
+ * @param int|null $group_id Group ID to manage.
+ */
+function xprofile_admin_manage_group( $group_id = null ) {
+	global $message, $type;
+
+	// Get the field group.
+	$group = new BP_XProfile_Group( $group_id );
+
+	// Updating.
+	if ( isset( $_POST['save_group'] ) ) {
+
+		// Check nonce
+		check_admin_referer( 'bp_xprofile_admin_group', 'bp_xprofile_admin_group' );
+
+		// Validate $_POSTed data.
+		if ( BP_XProfile_Group::admin_validate() ) {
+
+			// Set the group name.
+			$group->name = $_POST['group_name'];
+
+			// Set the group description.
+			if ( ! empty( $_POST['group_description'] ) ) {
+				$group->description = $_POST['group_description'];
+			} else {
+				$group->description = '';
+			}
+
+			// Attempt to save the field group.
+			if ( false === $group->save() ) {
+				$message = __( 'There was an error saving the group. Please try again.', 'buddypress' );
+				$type    = 'error';
+
+			// Save successful.
+			} else {
+				$message = __( 'The group was saved successfully.', 'buddypress' );
+				$type    = 'success';
+
+				// @todo remove these old options
+				if ( 1 == $group_id ) {
+					bp_update_option( 'bp-xprofile-base-group-name', $group->name );
+				}
+
+				/**
+				 * Fires at the end of the group adding/saving process, if successful.
+				 *
+				 * @since 1.0.0
+				 *
+				 * @param BP_XProfile_Group $group Current BP_XProfile_Group object.
+				 */
+				do_action( 'xprofile_groups_saved_group', $group );
+			}
+
+			xprofile_admin_screen( $message, $type );
+
+		} else {
+			$group->render_admin_form( $message );
+		}
+	} else {
+		$group->render_admin_form();
+	}
+}
+
+/**
+ * Handles the deletion of profile data groups.
+ *
+ * @since 1.0.0
+ *
+ * @param int $group_id ID of the group to delete.
+ */
+function xprofile_admin_delete_group( $group_id ) {
+	global $message, $type;
+
+	check_admin_referer( 'bp_xprofile_delete_group' );
+
+	$group = new BP_XProfile_Group( $group_id );
+
+	if ( !$group->delete() ) {
+		$message = _x( 'There was an error deleting the group. Please try again.', 'Error when deleting profile fields group', 'buddypress' );
+		$type    = 'error';
+	} else {
+		$message = _x( 'The group was deleted successfully.', 'Profile fields group was deleted successfully', 'buddypress' );
+		$type    = 'success';
+
+		/**
+		 * Fires at the end of group deletion process, if successful.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_XProfile_Group $group Current BP_XProfile_Group object.
+		 */
+		do_action( 'xprofile_groups_deleted_group', $group );
+	}
+
+	xprofile_admin_screen( $message, $type );
+}
+
+/**
+ * Handles the adding or editing of profile field data for a user.
+ *
+ * @since 1.0.0
+ *
+ * @param int      $group_id ID of the group.
+ * @param int|null $field_id ID of the field being managed.
+ */
+function xprofile_admin_manage_field( $group_id, $field_id = null ) {
+	global $wpdb, $message, $groups;
+
+	$bp = buddypress();
+
+	if ( is_null( $field_id ) ) {
+		$field = new BP_XProfile_Field();
+	} else {
+		$field = xprofile_get_field( $field_id );
+	}
+
+	$field->group_id = $group_id;
+
+	if ( isset( $_POST['saveField'] ) ) {
+
+		// Check nonce
+		check_admin_referer( 'bp_xprofile_admin_field', 'bp_xprofile_admin_field' );
+
+		if ( BP_XProfile_Field::admin_validate() ) {
+			$field->is_required = $_POST['required'];
+			$field->type        = $_POST['fieldtype'];
+			$field->name        = $_POST['title'];
+
+			if ( ! empty( $_POST['description'] ) ) {
+				$field->description = $_POST['description'];
+			} else {
+				$field->description = '';
+			}
+
+			if ( ! empty( $_POST["sort_order_{$field->type}"] ) ) {
+				$field->order_by = $_POST["sort_order_{$field->type}"];
+			}
+
+			$field->field_order = $wpdb->get_var( $wpdb->prepare( "SELECT field_order FROM {$bp->profile->table_name_fields} WHERE id = %d", $field_id ) );
+			if ( ! is_numeric( $field->field_order ) || is_wp_error( $field->field_order ) ) {
+				$field->field_order = (int) $wpdb->get_var( $wpdb->prepare( "SELECT max(field_order) FROM {$bp->profile->table_name_fields} WHERE group_id = %d", $group_id ) );
+				$field->field_order++;
+			}
+
+			// For new profile fields, set the $field_id. For existing profile
+			// fields, this will overwrite $field_id with the same value.
+			$field_id = $field->save();
+
+			if ( empty( $field_id ) ) {
+				$message = __( 'There was an error saving the field. Please try again.', 'buddypress' );
+				$type    = 'error';
+			} else {
+				$message = __( 'The field was saved successfully.', 'buddypress' );
+				$type    = 'success';
+
+				// @todo remove these old options
+				if ( 1 == $field_id ) {
+					bp_update_option( 'bp-xprofile-fullname-field-name', $field->name );
+				}
+
+				// Set member types.
+				if ( isset( $_POST['has-member-types'] ) ) {
+					$member_types = array();
+					if ( isset( $_POST['member-types'] ) ) {
+						$member_types = stripslashes_deep( $_POST['member-types'] );
+					}
+
+					$field->set_member_types( $member_types );
+				}
+
+				// Validate default visibility.
+				if ( ! empty( $_POST['default-visibility'] ) && in_array( $_POST['default-visibility'], wp_list_pluck( bp_xprofile_get_visibility_levels(), 'id' ) ) ) {
+					bp_xprofile_update_field_meta( $field_id, 'default_visibility', $_POST['default-visibility'] );
+				}
+
+				// Validate custom visibility.
+				if ( ! empty( $_POST['allow-custom-visibility'] ) && in_array( $_POST['allow-custom-visibility'], array( 'allowed', 'disabled' ) ) ) {
+					bp_xprofile_update_field_meta( $field_id, 'allow_custom_visibility', $_POST['allow-custom-visibility'] );
+				}
+
+				// Validate signup.
+				if ( ! empty( $_POST['signup-position'] ) ) {
+					bp_xprofile_update_field_meta( $field_id, 'signup_position', (int) $_POST['signup-position'] );
+				} else {
+					bp_xprofile_delete_meta( $field_id, 'field', 'signup_position' );
+				}
+
+				// Save autolink settings.
+				if ( isset( $_POST['do_autolink'] ) && 'on' === wp_unslash( $_POST['do_autolink'] ) ) {
+					bp_xprofile_update_field_meta( $field_id, 'do_autolink', 'on' );
+				} else {
+					bp_xprofile_update_field_meta( $field_id, 'do_autolink', 'off' );
+				}
+
+				if ( $field->type_obj->do_settings_section() ) {
+					$settings = isset( $_POST['field-settings'] ) ? wp_unslash( $_POST['field-settings'] ) : array();
+					$field->admin_save_settings( $settings );
+				}
+
+				/**
+				 * Fires at the end of the process to save a field for a user, if successful.
+				 *
+				 * @since 1.0.0
+				 *
+				 * @param BP_XProfile_Field $field Current BP_XProfile_Field object.
+				 */
+				do_action( 'xprofile_fields_saved_field', $field );
+
+				$groups = bp_xprofile_get_groups();
+			}
+
+			xprofile_admin_screen( $message, $type );
+
+		} else {
+			$field->render_admin_form( $message );
+		}
+	} else {
+		$field->render_admin_form();
+	}
+}
+
+/**
+ * Handles the deletion of a profile field (or field option).
+ *
+ * @since 1.0.0
+ *
+ * @global string $message The feedback message to show.
+ * @global string $type The type of feedback message to show.
+ *
+ * @param int    $field_id    The field to delete.
+ * @param string $field_type  The type of field being deleted.
+ * @param bool   $delete_data Should the field data be deleted too.
+ */
+function xprofile_admin_delete_field( $field_id, $field_type = 'field', $delete_data = false ) {
+	global $message, $type;
+
+	// Switch type to 'option' if type is not 'field'.
+	// @todo trust this param.
+	$field_type  = ( 'field' == $field_type ) ? __( 'field', 'buddypress' ) : __( 'option', 'buddypress' );
+	$field       = xprofile_get_field( $field_id );
+
+	if ( !$field->delete( (bool) $delete_data ) ) {
+		$message = sprintf( __( 'There was an error deleting the %s. Please try again.', 'buddypress' ), $field_type );
+		$type    = 'error';
+	} else {
+		$message = sprintf( __( 'The %s was deleted successfully!', 'buddypress' ), $field_type );
+		$type    = 'success';
+
+		/**
+		 * Fires at the end of the field deletion process, if successful.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_XProfile_Field $field Current BP_XProfile_Field object.
+		 */
+		do_action( 'xprofile_fields_deleted_field', $field );
+	}
+
+	xprofile_admin_screen( $message, $type );
+}
+
+/**
+ * Handles the ajax reordering of fields within a group.
+ *
+ * @since 1.0.0
+ */
+function xprofile_ajax_reorder_fields() {
+
+	// Check the nonce.
+	check_admin_referer( 'bp_reorder_fields', '_wpnonce_reorder_fields' );
+
+	if ( empty( $_POST['field_order'] ) ) {
+		return false;
+	}
+
+	parse_str( $_POST['field_order'], $order );
+
+	$field_group_id = $_POST['field_group_id'];
+
+	foreach ( (array) $order['draggable_field'] as $position => $field_id ) {
+		xprofile_update_field_position( (int) $field_id, (int) $position, (int) $field_group_id );
+	}
+}
+add_action( 'wp_ajax_xprofile_reorder_fields', 'xprofile_ajax_reorder_fields' );
+
+/**
+ * Handles the reordering of field groups.
+ *
+ * @since 1.5.0
+ */
+function xprofile_ajax_reorder_field_groups() {
+
+	// Check the nonce.
+	check_admin_referer( 'bp_reorder_groups', '_wpnonce_reorder_groups' );
+
+	if ( empty( $_POST['group_order'] ) ) {
+		return false;
+	}
+
+	parse_str( $_POST['group_order'], $order );
+
+	foreach ( (array) $order['group'] as $position => $field_group_id ) {
+		xprofile_update_field_group_position( (int) $field_group_id, (int) $position );
+	}
+}
+add_action( 'wp_ajax_xprofile_reorder_groups', 'xprofile_ajax_reorder_field_groups' );
+
+/**
+ * Handles the WYSIWYG display of each profile field on the edit screen.
+ *
+ * @since 1.5.0
+ *
+ * @param BP_XProfile_Field   $admin_field Admin field.
+ * @param object $admin_group Admin group object.
+ * @param string $class       Classes to append to output.
+ */
+function xprofile_admin_field( $admin_field, $admin_group, $class = '' ) {
+	global $field;
+
+	$field = $admin_field;
+
+	// Users admin URL
+	$url = bp_get_admin_url( 'users.php' );
+
+	// Edit
+	$field_edit_url = add_query_arg( array(
+		'page'     => 'bp-profile-setup',
+		'mode'     => 'edit_field',
+		'group_id' => (int) $field->group_id,
+		'field_id' => (int) $field->id
+	), $url );
+
+	// Delete
+	if ( $field->can_delete ) {
+		$field_delete_url = add_query_arg( array(
+			'page'     => 'bp-profile-setup',
+			'mode'     => 'delete_field',
+			'field_id' => (int) $field->id
+		), $url . '#tabs-' . (int) $field->group_id );
+	} ?>
+
+	<fieldset id="draggable_field_<?php echo esc_attr( $field->id ); ?>" class="sortable<?php echo ' ' . $field->type; if ( !empty( $class ) ) echo ' ' . $class; ?>">
+		<legend>
+			<span>
+				<?php bp_the_profile_field_name(); ?>
+
+				<?php if ( empty( $field->can_delete )                                    ) : ?><?php esc_html_e( '(Primary)', 'buddypress' ); endif; ?>
+				<?php bp_the_profile_field_required_label(); ?>
+				<?php if ( bp_xprofile_get_meta( $field->id, 'field', 'signup_position' ) ) : ?><?php esc_html_e( '(Sign-up)', 'buddypress' ); endif; ?>
+				<?php if ( bp_get_member_types() ) : echo $field->get_member_type_label(); endif; ?>
+
+				<?php
+
+				/**
+				 * Fires at end of legend above the name field in base xprofile group.
+				 *
+				 * @since 2.2.0
+				 *
+				 * @param BP_XProfile_Field $field Current BP_XProfile_Field
+				 *                                 object being rendered.
+				 */
+				do_action( 'xprofile_admin_field_name_legend', $field ); ?>
+			</span>
+		</legend>
+		<div class="field-wrapper">
+
+			<?php
+			if ( in_array( $field->type, array_keys( bp_xprofile_get_field_types() ) ) ) {
+				$field_type = bp_xprofile_create_field_type( $field->type );
+				$field_type->admin_field_html();
+			} else {
+
+				/**
+				 * Fires after the input if the current field is not in default field types.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param BP_XProfile_Field $field Current BP_XProfile_Field
+				 *                                 object being rendered.
+				 * @param int               $value Integer 1.
+				 */
+				do_action( 'xprofile_admin_field', $field, 1 );
+			}
+			?>
+
+			<?php if ( $field->description ) : ?>
+
+				<p class="description"><?php echo esc_attr( $field->description ); ?></p>
+
+			<?php endif; ?>
+
+			<div class="actions">
+				<a class="button edit" href="<?php echo esc_url( $field_edit_url ); ?>"><?php _ex( 'Edit', 'Edit field link', 'buddypress' ); ?></a>
+
+				<?php if ( $field->can_delete ) : ?>
+
+					<div class="delete-button">
+						<a class="confirm submit-delete deletion" href="<?php echo esc_url( $field_delete_url ); ?>"><?php _ex( 'Delete', 'Delete field link', 'buddypress' ); ?></a>
+					</div>
+
+				<?php endif; ?>
+
+				<?php
+
+				/**
+				 * Fires at end of field management links in xprofile management admin.
+				 *
+				 * @since 2.2.0
+				 *
+				 * @param BP_XProfile_Group $group BP_XProfile_Group object
+				 *                                 for the current group.
+				 */
+				do_action( 'xprofile_admin_field_action', $field ); ?>
+
+			</div>
+		</div>
+	</fieldset>
+
+<?php
+}
+
+/**
+ * Print <option> elements containing the xprofile field types.
+ *
+ * @since 2.0.0
+ *
+ * @param string $select_field_type The name of the field type that should be selected.
+ *                                  Will defaults to "textbox" if NULL is passed.
+ */
+function bp_xprofile_admin_form_field_types( $select_field_type ) {
+	$categories = array();
+
+	if ( is_null( $select_field_type ) ) {
+		$select_field_type = 'textbox';
+	}
+
+	// Sort each field type into its category.
+	foreach ( bp_xprofile_get_field_types() as $field_name => $field_class ) {
+		$field_type_obj = new $field_class;
+		$the_category   = $field_type_obj->category;
+
+		// Fallback to a catch-all if category not set.
+		if ( ! $the_category ) {
+			$the_category = _x( 'Other', 'xprofile field type category', 'buddypress' );
+		}
+
+		if ( isset( $categories[$the_category] ) ) {
+			$categories[$the_category][] = array( $field_name, $field_type_obj );
+		} else {
+			$categories[$the_category] = array( array( $field_name, $field_type_obj ) );
+		}
+	}
+
+	// Sort the categories alphabetically. ksort()'s SORT_NATURAL is only in PHP >= 5.4 :((.
+	uksort( $categories, 'strnatcmp' );
+
+	// Loop through each category and output form <options>.
+	foreach ( $categories as $category => $fields ) {
+		printf( '<optgroup label="%1$s">', esc_attr( $category ) );  // Already i18n'd in each profile type class.
+
+		// Sort these fields types alphabetically.
+		uasort( $fields, function( $a, $b ) { return strnatcmp( $a[1]->name, $b[1]->name ); } );
+
+		foreach ( $fields as $field_type_obj ) {
+			$field_name     = $field_type_obj[0];
+			$field_type_obj = $field_type_obj[1];
+
+			printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $field_name ), selected( $select_field_type, $field_name, false ), esc_html( $field_type_obj->name ) );
+		}
+
+		printf( '</optgroup>' );
+	}
+}
+
+// Load the xprofile user admin.
+add_action( 'bp_init', array( 'BP_XProfile_User_Admin', 'register_xprofile_user_admin' ), 11 );
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-cache.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-cache.php	(revision 12265)
@@ -0,0 +1,291 @@
+<?php
+/**
+ * BuddyPress XProfile Caching Functions.
+ *
+ * Caching functions handle the clearing of cached objects and pages on specific
+ * actions throughout BuddyPress.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileCache
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Determine which xprofile fields do not have cached values for a user.
+ *
+ * @since 2.2.0
+ *
+ * @param int   $user_id   User ID to check.
+ * @param array $field_ids XProfile field IDs.
+ * @return array
+ */
+function bp_xprofile_get_non_cached_field_ids( $user_id = 0, $field_ids = array() ) {
+	$uncached_fields = array();
+
+	foreach ( $field_ids as $field_id ) {
+		$field_id  = (int) $field_id;
+		$cache_key = "{$user_id}:{$field_id}";
+		if ( false === wp_cache_get( $cache_key, 'bp_xprofile_data' ) ) {
+			$uncached_fields[] = $field_id;
+		}
+	}
+
+	return $uncached_fields;
+}
+
+/**
+ * Slurp up xprofilemeta for a specified set of profile objects.
+ *
+ * We do not use bp_update_meta_cache() for the xprofile component. This is
+ * because the xprofile component has three separate object types (group,
+ * field, and data) and three corresponding cache groups. Using the technique
+ * in bp_update_meta_cache(), pre-fetching would take three separate database
+ * queries. By grouping them together, we can reduce the required queries to
+ * one.
+ *
+ * This function is called within a bp_has_profile() loop.
+ *
+ * @since 2.0.0
+ *
+ * @param array $object_ids Multi-dimensional array of object_ids, keyed by
+ *                          object type ('group', 'field', 'data').
+ * @return bool
+ */
+function bp_xprofile_update_meta_cache( $object_ids = array() ) {
+	global $wpdb;
+
+	// Bail if no objects.
+	if ( empty( $object_ids ) ) {
+		return false;
+	}
+
+	$bp = buddypress();
+
+	// Define the array where uncached object IDs will be stored.
+	$uncached_object_ids = array(
+		'group',
+		'field',
+		'data'
+	);
+
+	// Define the cache groups for the 3 types of XProfile metadata.
+	$cache_groups = array(
+		'group' => 'xprofile_group_meta',
+		'field' => 'xprofile_field_meta',
+		'data'  => 'xprofile_data_meta',
+	);
+
+	// No reason to query yet.
+	$do_query = false;
+
+	// Loop through object types and look for uncached data.
+	foreach ( $uncached_object_ids as $object_type ) {
+
+		// Skip if empty object type.
+		if ( empty( $object_ids[ $object_type ] ) ) {
+			continue;
+		}
+
+		// Sanitize $object_ids passed to the function.
+		$object_type_ids = wp_parse_id_list( $object_ids[ $object_type ] );
+
+		// Get non-cached IDs for each object type.
+		$uncached_object_ids[ $object_type ] = bp_get_non_cached_ids( $object_type_ids, $cache_groups[ $object_type ] );
+
+		// Set the flag to do the meta query.
+		if ( ! empty( $uncached_object_ids[ $object_type ] ) && ( false === $do_query ) ) {
+			$do_query = true;
+		}
+	}
+
+	// Bail if no uncached items.
+	if ( false === $do_query ) {
+		return;
+	}
+
+	// Setup where conditions for query.
+	$where_sql        = '';
+	$where_conditions = array();
+
+	// Loop through uncached objects and prepare to query for them.
+	foreach ( $uncached_object_ids as $otype => $oids ) {
+
+		// Skip empty object IDs.
+		if ( empty( $oids ) ) {
+			continue;
+		}
+
+		// Compile WHERE query conditions for uncached metadata.
+		$oids_sql           = implode( ',', wp_parse_id_list( $oids ) );
+		$where_conditions[] = $wpdb->prepare( "( object_type = %s AND object_id IN ({$oids_sql}) )", $otype );
+	}
+
+	// Bail if no where conditions.
+	if ( empty( $where_conditions ) ) {
+		return;
+	}
+
+	// Setup the WHERE query part.
+	$where_sql = implode( " OR ", $where_conditions );
+
+	// Attempt to query meta values.
+	$meta_list = $wpdb->get_results( "SELECT object_id, object_type, meta_key, meta_value FROM {$bp->profile->table_name_meta} WHERE {$where_sql}" );
+
+	// Bail if no results found.
+	if ( empty( $meta_list ) || is_wp_error( $meta_list ) ) {
+		return;
+	}
+
+	// Setup empty cache array.
+	$cache = array();
+
+	// Loop through metas.
+	foreach ( $meta_list as $meta ) {
+		$oid    = $meta->object_id;
+		$otype  = $meta->object_type;
+		$okey   = $meta->meta_key;
+		$ovalue = $meta->meta_value;
+
+		// Force subkeys to be array type.
+		if ( ! isset( $cache[ $otype ][ $oid ] ) || ! is_array( $cache[ $otype ][ $oid ] ) ) {
+			$cache[ $otype ][ $oid ] = array();
+		}
+
+		if ( ! isset( $cache[ $otype ][ $oid ][ $okey ] ) || ! is_array( $cache[ $otype ][ $oid ][ $okey ] ) ) {
+			$cache[ $otype ][ $oid ][ $okey ] = array();
+		}
+
+		// Add to the cache array.
+		$cache[ $otype ][ $oid ][ $okey ][] = maybe_unserialize( $ovalue );
+	}
+
+	// Loop through data and cache to the appropriate object.
+	foreach ( $cache as $object_type => $object_caches ) {
+
+		// Determine the cache group for this data.
+		$cache_group = $cache_groups[ $object_type ];
+
+		// Loop through objects and cache appropriately.
+		foreach ( $object_caches as $object_id => $object_cache ) {
+			wp_cache_set( $object_id, $object_cache, $cache_group );
+		}
+	}
+}
+
+/**
+ * Clear cached XProfile field group data.
+ *
+ * @since 2.1.0
+ *
+ * @param object $group_obj Groub object to clear.
+ */
+function xprofile_clear_profile_groups_object_cache( $group_obj ) {
+	wp_cache_delete( 'all',          'bp_xprofile_groups' );
+	wp_cache_delete( $group_obj->id, 'bp_xprofile_groups' );
+}
+add_action( 'xprofile_group_after_delete', 'xprofile_clear_profile_groups_object_cache' );
+add_action( 'xprofile_group_after_save',   'xprofile_clear_profile_groups_object_cache' );
+
+/**
+ * Clear caches when a field object is modified.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_XProfile_Field $field_obj Field object cache to delete.
+ */
+function xprofile_clear_profile_field_object_cache( $field_obj ) {
+
+	// Clear default visibility level cache.
+	wp_cache_delete( 'default_visibility_levels', 'bp_xprofile' );
+
+	// Modified fields can alter parent group status, in particular when
+	// the group goes from empty to non-empty. Bust its cache, as well as
+	// the global 'all' cache.
+	wp_cache_delete( 'all',                'bp_xprofile_groups' );
+	wp_cache_delete( $field_obj->group_id, 'bp_xprofile_groups' );
+}
+add_action( 'xprofile_fields_saved_field',   'xprofile_clear_profile_field_object_cache' );
+add_action( 'xprofile_fields_deleted_field', 'xprofile_clear_profile_field_object_cache' );
+
+/**
+ * Clears member_type cache when a field's member types are updated.
+ *
+ * @since 2.4.0
+ */
+function bp_xprofile_clear_member_type_cache() {
+	wp_cache_delete( 'field_member_types', 'bp_xprofile' );
+}
+add_action( 'bp_xprofile_field_set_member_type', 'bp_xprofile_clear_member_type_cache' );
+
+/**
+ * Clear caches when a user's updates a field data object.
+ *
+ * @since 2.0.0
+ *
+ * @param BP_XProfile_ProfileData $data_obj Field data object to delete.
+ */
+function xprofile_clear_profiledata_object_cache( $data_obj ) {
+	wp_cache_delete( "{$data_obj->user_id}:{$data_obj->field_id}", 'bp_xprofile_data' );
+}
+add_action( 'xprofile_data_after_save',   'xprofile_clear_profiledata_object_cache' );
+add_action( 'xprofile_data_after_delete', 'xprofile_clear_profiledata_object_cache' );
+
+/**
+ * Clear fullname_field_id cache when bp-xprofile-fullname-field-name is updated.
+ *
+ * Note for future developers: Dating from an early version of BuddyPress where
+ * the fullname field (field #1) did not have a title that was editable in the
+ * normal Profile Fields admin interface, we have the bp-xprofile-fullname-field-name
+ * option. In many places throughout BuddyPress, the ID of the fullname field
+ * is queried using this setting. However, this is no longer strictly necessary,
+ * because we essentially hardcode (in the xprofile admin save routine, as well
+ * as the xprofile schema definition) that the fullname field will be 1. The
+ * presence of the non-hardcoded versions (and thus this bit of cache
+ * invalidation) is thus for backward compatibility only.
+ *
+ * @since 2.0.0
+ */
+function xprofile_clear_fullname_field_id_cache() {
+	wp_cache_delete( 'fullname_field_id', 'bp_xprofile' );
+}
+add_action( 'update_option_bp-xprofile-fullname-field-name', 'xprofile_clear_fullname_field_id_cache' );
+
+/**
+ * Clear a field's caches.
+ *
+ * @since 2.4.0
+ *
+ * @param int|BP_XProfile_Field $field A field ID or a field object.
+ * @return bool False on failure.
+ */
+function bp_xprofile_clear_field_cache( $field ) {
+	if ( is_numeric( $field ) ) {
+		$field_id = (int) $field;
+	} elseif ( $field instanceof BP_XProfile_Field ) {
+		$field_id = (int) $field->id;
+	}
+
+	if ( ! isset( $field_id ) ) {
+		return false;
+	}
+
+	wp_cache_delete( $field_id, 'bp_xprofile_fields' );
+	wp_cache_delete( $field_id, 'xprofile_meta' );
+}
+add_action( 'xprofile_field_after_save', 'bp_xprofile_clear_field_cache' );
+
+/**
+ * Clear the field-name cache.
+ *
+ * @since 2.8.0
+ */
+function bp_xprofile_reset_fields_by_name_cache_incrementor() {
+	bp_core_reset_incrementor( 'bp_xprofile_fields_by_name' );
+}
+add_action( 'xprofile_field_before_save', 'bp_xprofile_reset_fields_by_name_cache_incrementor' );
+
+// List actions to clear super cached pages on, if super cache is installed.
+add_action( 'xprofile_updated_profile', 'bp_core_clear_cache' );
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-caps.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-caps.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-caps.php	(revision 12265)
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Roles and capabilities logic for the XProfile component.
+ *
+ * @package BuddyPress
+ * @subpackage XPRofileCaps
+ * @since 1.6.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Maps XProfile caps to built in WordPress caps.
+ *
+ * @since 1.6.0
+ *
+ * @param array  $caps    Capabilities for meta capability.
+ * @param string $cap     Capability name.
+ * @param int    $user_id User id.
+ * @param mixed  $args    Arguments.
+ *
+ * @return array Actual capabilities for meta capability.
+ */
+function bp_xprofile_map_meta_caps( $caps, $cap, $user_id, $args ) {
+	switch ( $cap ) {
+		case 'bp_xprofile_change_field_visibility' :
+			$caps = array( 'exist' );
+
+			// You may pass args manually: $field_id, $profile_user_id.
+			$field_id        = 0;
+			$profile_user_id = isset( $args[1] ) ? (int) $args[1] : bp_displayed_user_id();
+
+			if ( ! empty( $args[0] ) ) {
+				$field_id = (int) $args[0];
+			} elseif ( isset( $GLOBALS['profile_template'] ) && $GLOBALS['profile_template']->in_the_loop ) {
+				$field_id = bp_get_the_profile_field_id();
+			}
+
+			// Visibility on the fullname field is not editable.
+			if ( 1 == $field_id ) {
+				$caps[] = 'do_not_allow';
+				break;
+			}
+
+			// Has the admin disabled visibility modification for this field?
+			if ( 'disabled' == bp_xprofile_get_meta( $field_id, 'field', 'allow_custom_visibility' ) ) {
+				$caps[] = 'do_not_allow';
+				break;
+			}
+
+			// Friends don't let friends edit each other's visibility.
+			if ( $profile_user_id != bp_displayed_user_id() && !bp_current_user_can( 'bp_moderate' ) ) {
+				$caps[] = 'do_not_allow';
+				break;
+			}
+
+			break;
+	}
+
+	/**
+	 * Filters the XProfile caps to built in WordPress caps.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array  $caps    Capabilities for meta capability.
+	 * @param string $cap     Capability name.
+	 * @param int    $user_id User ID being mapped.
+	 * @param mixed  $args    Capability arguments.
+	 */
+	return apply_filters( 'bp_xprofile_map_meta_caps', $caps, $cap, $user_id, $args );
+}
+add_filter( 'bp_map_meta_caps', 'bp_xprofile_map_meta_caps', 10, 4 );
+
+/**
+ * Grant the 'bp_xprofile_change_field_visibility' cap to logged-out users.
+ *
+ * @since 2.7.1
+ *
+ * @param bool   $user_can
+ * @param int    $user_id
+ * @param string $capability
+ * @return bool
+ */
+function bp_xprofile_grant_bp_xprofile_change_field_visibility_for_logged_out_users( $user_can, $user_id, $capability ) {
+	if ( 'bp_xprofile_change_field_visibility' === $capability && 0 === $user_id ) {
+		$field_id = bp_get_the_profile_field_id();
+		if ( $field_id && $field = xprofile_get_field( $field_id ) ) {
+			$user_can = 'allowed' === $field->allow_custom_visibility;
+		}
+	}
+
+	return $user_can;
+}
+add_filter( 'bp_user_can', 'bp_xprofile_grant_bp_xprofile_change_field_visibility_for_logged_out_users', 10, 3 );
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-cssjs.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-cssjs.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-cssjs.php	(revision 12265)
@@ -0,0 +1,76 @@
+<?php
+/**
+ * BuddyPress XProfile CSS and JS.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileScripts
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Enqueue the CSS for XProfile admin styling.
+ *
+ * @since 1.1.0
+ */
+function xprofile_add_admin_css() {
+	if ( !empty( $_GET['page'] ) && strpos( $_GET['page'], 'bp-profile-setup' ) !== false ) {
+		$min = bp_core_get_minified_asset_suffix();
+
+		wp_enqueue_style( 'xprofile-admin-css', buddypress()->plugin_url . "bp-xprofile/admin/css/admin{$min}.css", array(), bp_get_version() );
+
+		wp_style_add_data( 'xprofile-admin-css', 'rtl', true );
+		if ( $min ) {
+			wp_style_add_data( 'xprofile-admin-css', 'suffix', $min );
+		}
+	}
+}
+add_action( 'bp_admin_enqueue_scripts', 'xprofile_add_admin_css' );
+
+/**
+ * Enqueue the jQuery libraries for handling drag/drop/sort.
+ *
+ * @since 1.5.0
+ */
+function xprofile_add_admin_js() {
+	if ( !empty( $_GET['page'] ) && strpos( $_GET['page'], 'bp-profile-setup' ) !== false ) {
+		wp_enqueue_script( 'jquery-ui-core'      );
+		wp_enqueue_script( 'jquery-ui-tabs'      );
+		wp_enqueue_script( 'jquery-ui-mouse'     );
+		wp_enqueue_script( 'jquery-ui-draggable' );
+		wp_enqueue_script( 'jquery-ui-droppable' );
+		wp_enqueue_script( 'jquery-ui-sortable'  );
+
+		$min = bp_core_get_minified_asset_suffix();
+		wp_enqueue_script( 'xprofile-admin-js', buddypress()->plugin_url . "bp-xprofile/admin/js/admin{$min}.js", array( 'jquery', 'jquery-ui-sortable' ), bp_get_version() );
+
+		// Localize strings.
+		// supports_options_field_types is a dynamic list of field
+		// types that support options, for use in showing/hiding the
+		// "please enter options for this field" section.
+		$strings = array(
+			'do_settings_section_field_types' => array(),
+			'do_autolink' => '',
+		);
+
+		foreach ( bp_xprofile_get_field_types() as $field_type => $field_type_class ) {
+			$field = new $field_type_class();
+			if ( $field->do_settings_section() ) {
+				$strings['do_settings_section_field_types'][] = $field_type;
+			}
+		}
+
+		// Load 'autolink' setting into JS so that we can provide smart defaults when switching field type.
+		if ( ! empty( $_GET['field_id'] ) ) {
+			$field_id = intval( $_GET['field_id'] );
+
+			// Pull the raw data from the DB so we can tell whether the admin has saved a value yet.
+			$strings['do_autolink'] = bp_xprofile_get_meta( $field_id, 'field', 'do_autolink' );
+		}
+
+		wp_localize_script( 'xprofile-admin-js', 'XProfileAdmin', $strings );
+	}
+}
+add_action( 'bp_admin_enqueue_scripts', 'xprofile_add_admin_js', 1 );
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-filters.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-filters.php	(revision 12265)
@@ -0,0 +1,645 @@
+<?php
+/**
+ * BuddyPress XProfile Filters.
+ *
+ * Apply WordPress defined filters.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileFilters
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+add_filter( 'bp_get_the_profile_group_name',            'wp_filter_kses',       1 );
+add_filter( 'bp_get_the_profile_group_description',     'wp_filter_kses',       1 );
+add_filter( 'bp_get_the_profile_field_value',           'xprofile_filter_kses', 1 );
+add_filter( 'bp_get_the_profile_field_name',            'wp_filter_kses',       1 );
+add_filter( 'bp_get_the_profile_field_edit_value',      'wp_filter_kses',       1 );
+add_filter( 'bp_get_the_profile_field_description',     'wp_filter_kses',       1 );
+
+add_filter( 'bp_get_the_profile_field_value',           'wptexturize'        );
+add_filter( 'bp_get_the_profile_field_value',           'convert_chars'      );
+add_filter( 'bp_get_the_profile_field_value',           'wpautop'            );
+add_filter( 'bp_get_the_profile_field_value',           'force_balance_tags' );
+add_filter( 'bp_get_the_profile_field_value',           'make_clickable'     );
+add_filter( 'bp_get_the_profile_field_value',           'bp_xprofile_escape_field_data', 8, 3 );
+add_filter( 'bp_get_the_profile_field_value',           'convert_smilies', 9 );
+add_filter( 'bp_get_the_profile_field_value',           'xprofile_filter_format_field_value',         1, 2 );
+add_filter( 'bp_get_the_profile_field_value',           'xprofile_filter_format_field_value_by_type', 8, 3 );
+add_filter( 'bp_get_the_profile_field_value',           'xprofile_filter_link_profile_data',          9, 3 );
+
+add_filter( 'bp_get_the_profile_field_edit_value',      'force_balance_tags' );
+add_filter( 'bp_get_the_profile_field_edit_value',      'bp_xprofile_escape_field_data', 10, 3 );
+
+add_filter( 'bp_get_the_profile_group_name',            'stripslashes' );
+add_filter( 'bp_get_the_profile_group_description',     'stripslashes' );
+add_filter( 'bp_get_the_profile_field_value',           'stripslashes' );
+add_filter( 'bp_get_the_profile_field_edit_value',      'stripslashes' );
+add_filter( 'bp_get_the_profile_field_name',            'stripslashes' );
+add_filter( 'bp_get_the_profile_field_description',     'stripslashes' );
+
+add_filter( 'xprofile_get_field_data',                  'xprofile_filter_kses', 1 );
+add_filter( 'xprofile_field_name_before_save',          'wp_filter_kses', 1 );
+add_filter( 'xprofile_field_description_before_save',   'wp_filter_kses', 1 );
+
+add_filter( 'xprofile_get_field_data',                  'force_balance_tags' );
+add_filter( 'xprofile_field_name_before_save',          'force_balance_tags' );
+add_filter( 'xprofile_field_description_before_save',   'force_balance_tags' );
+
+add_filter( 'xprofile_get_field_data',                  'stripslashes' );
+add_filter( 'xprofile_get_field_data',                  'xprofile_filter_format_field_value_by_field_id', 5, 2 );
+
+add_filter( 'bp_xprofile_set_field_data_pre_validate',  'xprofile_filter_pre_validate_value_by_field_type', 10, 3 );
+add_filter( 'xprofile_data_value_before_save',          'xprofile_sanitize_data_value_before_save', 1, 4 );
+add_filter( 'xprofile_filtered_data_value_before_save', 'trim', 2 );
+
+// Save field groups.
+add_filter( 'xprofile_group_name_before_save',        'wp_filter_kses' );
+add_filter( 'xprofile_group_description_before_save', 'wp_filter_kses' );
+
+add_filter( 'xprofile_group_name_before_save',         'stripslashes' );
+add_filter( 'xprofile_group_description_before_save',  'stripslashes' );
+
+// Save fields.
+add_filter( 'xprofile_field_name_before_save',         'wp_filter_kses' );
+add_filter( 'xprofile_field_type_before_save',         'wp_filter_kses' );
+add_filter( 'xprofile_field_description_before_save',  'wp_filter_kses' );
+add_filter( 'xprofile_field_order_by_before_save',     'wp_filter_kses' );
+
+add_filter( 'xprofile_field_is_required_before_save',  'absint' );
+add_filter( 'xprofile_field_field_order_before_save',  'absint' );
+add_filter( 'xprofile_field_option_order_before_save', 'absint' );
+add_filter( 'xprofile_field_can_delete_before_save',   'absint' );
+
+// Save field options.
+add_filter( 'xprofile_field_options_before_save', 'bp_xprofile_sanitize_field_options' );
+add_filter( 'xprofile_field_default_before_save', 'bp_xprofile_sanitize_field_default' );
+
+// Personal data export.
+add_filter( 'wp_privacy_personal_data_exporters', 'bp_xprofile_register_personal_data_exporter' );
+
+/**
+ * Sanitize each field option name for saving to the database.
+ *
+ * @since 2.3.0
+ *
+ * @param mixed $field_options Options to sanitize.
+ * @return mixed
+ */
+function bp_xprofile_sanitize_field_options( $field_options = '' ) {
+	if ( is_array( $field_options ) ) {
+		return array_map( 'sanitize_text_field', $field_options );
+	} else {
+		return sanitize_text_field( $field_options );
+	}
+}
+
+/**
+ * Sanitize each field option default for saving to the database.
+ *
+ * @since 2.3.0
+ *
+ * @param mixed $field_default Field defaults to sanitize.
+ * @return array|int
+ */
+function bp_xprofile_sanitize_field_default( $field_default = '' ) {
+	if ( is_array( $field_default ) ) {
+		return array_map( 'intval', $field_default );
+	} else {
+		return intval( $field_default );
+	}
+}
+
+/**
+ * Run profile field values through kses with filterable allowed tags.
+ *
+ * @since 1.5.0
+ *
+ * @param string      $content  Content to filter.
+ * @param object|null $data_obj The BP_XProfile_ProfileData object.
+ * @return string $content
+ */
+function xprofile_filter_kses( $content, $data_obj = null ) {
+	global $allowedtags;
+
+	$xprofile_allowedtags             = $allowedtags;
+	$xprofile_allowedtags['a']['rel'] = array();
+
+	// If the field supports rich text, we must allow tags that appear in wp_editor().
+	if ( $data_obj instanceof BP_XProfile_ProfileData && bp_xprofile_is_richtext_enabled_for_field( $data_obj->field_id ) ) {
+		$richtext_tags = array(
+			'img'  => array( 'id' => 1, 'class' => 1, 'src' => 1, 'alt' => 1, 'width' => 1, 'height' => 1 ),
+			'ul'   => array( 'id' => 1, 'class' => 1 ),
+			'ol'   => array( 'id' => 1, 'class' => 1 ),
+			'li'   => array( 'id' => 1, 'class' => 1 ),
+			'span' => array( 'style' => 1 ),
+			'p'    => array( 'style' => 1 ),
+		);
+
+		$xprofile_allowedtags = array_merge( $allowedtags, $richtext_tags );
+	}
+
+	/**
+	 * Filters the allowed tags for use within xprofile_filter_kses().
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array                   $xprofile_allowedtags Array of allowed tags for profile field values.
+	 * @param BP_XProfile_ProfileData $data_obj             The BP_XProfile_ProfileData object.
+	 */
+	$xprofile_allowedtags = apply_filters( 'xprofile_allowed_tags', $xprofile_allowedtags, $data_obj );
+	return wp_kses( $content, $xprofile_allowedtags );
+}
+
+/**
+ * Safely runs profile field data through kses and force_balance_tags.
+ *
+ * @since 1.2.6
+ *
+ * @param string      $field_value Field value being santized.
+ * @param int         $field_id    Field ID being sanitized.
+ * @param bool        $reserialize Whether to reserialize arrays before returning. Defaults to true.
+ * @param object|null $data_obj    The BP_XProfile_ProfileData object.
+ * @return string
+ */
+function xprofile_sanitize_data_value_before_save( $field_value, $field_id = 0, $reserialize = true, $data_obj = null ) {
+
+	// Return if empty.
+	if ( empty( $field_value ) ) {
+		return $field_value;
+	}
+
+	// Force reserialization if serialized (avoids mutation, retains integrity)
+	if ( is_serialized( $field_value ) && ( false === $reserialize ) ) {
+		$reserialize = true;
+	}
+
+	// Value might be a serialized array of options.
+	$field_value = maybe_unserialize( $field_value );
+
+	// Sanitize single field value.
+	if ( ! is_array( $field_value ) ) {
+		$kses_field_value     = xprofile_filter_kses( $field_value, $data_obj );
+		$filtered_field_value = wp_rel_nofollow( force_balance_tags( $kses_field_value ) );
+
+		/**
+		 * Filters the kses-filtered data before saving to database.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param string                  $filtered_field_value The filtered value.
+		 * @param string                  $field_value          The original value before filtering.
+		 * @param BP_XProfile_ProfileData $data_obj             The BP_XProfile_ProfileData object.
+		 */
+		$filtered_field_value = apply_filters( 'xprofile_filtered_data_value_before_save', $filtered_field_value, $field_value, $data_obj );
+
+	// Sanitize multiple individual option values.
+	} else {
+		$filtered_values = array();
+		foreach ( (array) $field_value as $value ) {
+			$kses_field_value = xprofile_filter_kses( $value, $data_obj );
+			$filtered_value   = wp_rel_nofollow( force_balance_tags( $kses_field_value ) );
+
+			/** This filter is documented in bp-xprofile/bp-xprofile-filters.php */
+			$filtered_values[] = apply_filters( 'xprofile_filtered_data_value_before_save', $filtered_value, $value, $data_obj );
+		}
+
+		if ( !empty( $reserialize ) ) {
+			$filtered_field_value = serialize( $filtered_values );
+		} else {
+			$filtered_field_value = $filtered_values;
+		}
+	}
+
+	return $filtered_field_value;
+}
+
+/**
+ * Runs stripslashes on XProfile fields.
+ *
+ * @since 1.0.0
+ *
+ * @param string $field_value XProfile field_value to be filtered.
+ * @param string $field_type  XProfile field_type to be filtered.
+ * @return false|string $field_value Filtered XProfile field_value. False on failure.
+ */
+function xprofile_filter_format_field_value( $field_value, $field_type = '' ) {
+
+	// Valid field values of 0 or '0' get caught by empty(), so we have an extra check for these. See #BP5731.
+	if ( ! isset( $field_value ) || empty( $field_value ) && ( '0' !== $field_value ) ) {
+		return false;
+	}
+
+	if ( 'datebox' !== $field_type ) {
+		$field_value = str_replace( ']]>', ']]&gt;', $field_value );
+	}
+
+	return stripslashes( $field_value );
+}
+
+/**
+ * Apply display_filter() filters as defined by BP_XProfile_Field_Type classes, when inside a bp_has_profile() loop.
+ *
+ * @since 2.1.0
+ * @since 2.4.0 Added `$field_id` parameter.
+ *
+ * @param mixed      $field_value Field value.
+ * @param string     $field_type  Field type.
+ * @param string|int $field_id    Optional. ID of the field.
+ * @return mixed
+ */
+function xprofile_filter_format_field_value_by_type( $field_value, $field_type = '', $field_id = '' ) {
+	foreach ( bp_xprofile_get_field_types() as $type => $class ) {
+		if ( $type !== $field_type ) {
+			continue;
+		}
+
+		if ( method_exists( $class, 'display_filter' ) ) {
+			$field_value = call_user_func( array( $class, 'display_filter' ), $field_value, $field_id );
+		}
+	}
+
+	return $field_value;
+}
+
+/**
+ * Apply display_filter() filters as defined by the BP_XProfile_Field_Type classes, when fetched
+ * by xprofile_get_field_data().
+ *
+ * @since 2.1.0
+ *
+ * @param mixed $field_value Field value.
+ * @param int   $field_id    Field type.
+ * @return string
+ */
+function xprofile_filter_format_field_value_by_field_id( $field_value, $field_id ) {
+	$field = xprofile_get_field( $field_id );
+	return xprofile_filter_format_field_value_by_type( $field_value, $field->type, $field_id );
+}
+
+/**
+ * Apply pre_validate_filter() filters as defined by the BP_XProfile_Field_Type classes before validating.
+ *
+ * @since 2.1.0
+ *
+ * @param mixed                  $value          Value passed to the bp_xprofile_set_field_data_pre_validate filter.
+ * @param BP_XProfile_Field      $field          Field object.
+ * @param BP_XProfile_Field_Type $field_type_obj Field type object.
+ * @return mixed
+ */
+function xprofile_filter_pre_validate_value_by_field_type( $value, $field, $field_type_obj ) {
+	if ( method_exists( $field_type_obj, 'pre_validate_filter' ) ) {
+		$value = call_user_func( array( $field_type_obj, 'pre_validate_filter' ), $value, $field->id );
+	}
+
+	return $value;
+}
+
+/**
+ * Escape field value for display.
+ *
+ * Most field values are simply run through esc_html(). Those that support rich text (by default, `textarea` only)
+ * are sanitized using kses, which allows a whitelist of HTML tags.
+ *
+ * @since 2.4.0
+ *
+ * @param string $value      Field value.
+ * @param string $field_type Field type.
+ * @param int    $field_id   Field ID.
+ * @return string
+ */
+function bp_xprofile_escape_field_data( $value, $field_type, $field_id ) {
+	if ( bp_xprofile_is_richtext_enabled_for_field( $field_id ) ) {
+		// The xprofile_filter_kses() expects a BP_XProfile_ProfileData object.
+		$data_obj = null;
+		if ( bp_is_user() ) {
+			$data_obj = new BP_XProfile_ProfileData( $field_id, bp_displayed_user_id() );
+		}
+
+		$value = xprofile_filter_kses( $value, $data_obj );
+	} else {
+		$value = esc_html( $value );
+	}
+
+	return $value;
+}
+
+/**
+ * Filter an Extended Profile field value, and attempt to make clickable links
+ * to members search results out of them.
+ *
+ * - Not run on datebox field types.
+ * - Not run on values without commas with less than 5 words.
+ * - URL's are made clickable.
+ *
+ * To disable globally:
+ *     remove_filter( 'bp_get_the_profile_field_value', 'xprofile_filter_link_profile_data', 9, 3 );
+ *
+ * To disable for a single field, use the 'Autolink' settings in Dashboard > Users > Profile Fields.
+ *
+ * @since 1.1.0
+ *
+ * @param string $field_value Profile field data value.
+ * @param string $field_type  Profile field type.
+ * @return string|array
+ */
+function xprofile_filter_link_profile_data( $field_value, $field_type = 'textbox' ) {
+	global $field;
+
+	if ( ! $field->get_do_autolink() ) {
+		return $field_value;
+	}
+
+	if ( 'datebox' === $field_type ) {
+		return $field_value;
+	}
+
+	if ( strpos( $field_value, ',' ) === false && strpos( $field_value, ';' ) === false && ( count( explode( ' ', $field_value ) ) > 5 ) ) {
+		return $field_value;
+	}
+
+	if ( strpos( $field_value, ',' ) !== false ) {
+		// Comma-separated lists.
+		$list_type = 'comma';
+		$values    = explode( ',', $field_value );
+	} else {
+		/*
+		 * Semicolon-separated lists.
+		 *
+		 * bp_xprofile_escape_field_data() runs before this function, which often runs esc_html().
+		 * In turn, that encodes HTML entities in the string (";" becomes "&#039;").
+		 *
+		 * Before splitting on the ";" character, decode the HTML entities, and re-encode after.
+		 * This prevents input like "O'Hara" rendering as "O&#039; Hara" (with each of those parts
+		 * having a seperate HTML link).
+		 */
+		$list_type   = 'semicolon';
+		$field_value = wp_specialchars_decode( $field_value, ENT_QUOTES );
+		$values      = explode( ';', $field_value );
+
+		array_walk( $values, function( &$value, $key ) use ( $field_type, $field ) {
+			$value = bp_xprofile_escape_field_data( $value, $field_type, $field->id );
+		} );
+	}
+
+	if ( ! empty( $values ) ) {
+		foreach ( (array) $values as $value ) {
+			$value = trim( $value );
+
+			// If the value is a URL, skip it and just make it clickable.
+			if ( preg_match( '@(https?://([-\w\.]+)+(:\d+)?(/([\w/_\.]*(\?\S+)?)?)?)@', $value ) ) {
+				$new_values[] = make_clickable( $value );
+
+			// Is not clickable.
+			} else {
+
+				// More than 5 spaces.
+				if ( count( explode( ' ', $value ) ) > 5 ) {
+					$new_values[] = $value;
+
+				// Less than 5 spaces.
+				} else {
+					$query_arg    = bp_core_get_component_search_query_arg( 'members' );
+					$search_url   = add_query_arg( array( $query_arg => urlencode( $value ) ), bp_get_members_directory_permalink() );
+					$new_values[] = '<a href="' . esc_url( $search_url ) . '" rel="nofollow">' . $value . '</a>';
+				}
+			}
+		}
+
+		if ( 'comma' === $list_type ) {
+			$values = implode( ', ', $new_values );
+		} else {
+			$values = implode( '; ', $new_values );
+		}
+	}
+
+	return $values;
+}
+
+/**
+ * Ensures that BP data appears in comments array.
+ *
+ * This filter loops through the comments return by a normal WordPress request
+ * and swaps out user data with BP xprofile data, where available.
+ *
+ * @since 1.2.0
+ *
+ * @param array $comments Comments to filter in.
+ * @param int   $post_id  Post ID the comments are for.
+ * @return array $comments
+ */
+function xprofile_filter_comments( $comments, $post_id = 0 ) {
+
+	// Locate comment authors with WP accounts.
+	foreach( (array) $comments as $comment ) {
+		if ( $comment->user_id ) {
+			$user_ids[] = $comment->user_id;
+		}
+	}
+
+	// If none are found, just return the comments array.
+	if ( empty( $user_ids ) ) {
+		return $comments;
+	}
+
+	// Pull up the xprofile fullname of each commenter.
+	if ( $fullnames = bp_core_get_user_displaynames( $user_ids ) ) {
+		foreach( (array) $fullnames as $user_id => $user_fullname ) {
+			$users[ $user_id ] = trim( stripslashes( $user_fullname ) );
+		}
+	}
+
+	// Loop through and match xprofile fullname with commenters.
+	foreach( (array) $comments as $i => $comment ) {
+		if ( ! empty( $comment->user_id ) ) {
+			if ( ! empty( $users[ $comment->user_id ] ) ) {
+				$comments[ $i ]->comment_author = $users[ $comment->user_id ];
+			}
+		}
+	}
+
+	return $comments;
+}
+add_filter( 'comments_array', 'xprofile_filter_comments', 10, 2 );
+
+/**
+ * Filter BP_User_Query::populate_extras to override each queries users fullname.
+ *
+ * @since 1.7.0
+ *
+ * @param BP_User_Query $user_query   User query to filter.
+ * @param string        $user_ids_sql SQL statement to use.
+ */
+function bp_xprofile_filter_user_query_populate_extras( BP_User_Query $user_query, $user_ids_sql = '' ) {
+
+	if ( ! bp_is_active( 'xprofile' ) ) {
+		return;
+	}
+
+	$user_id_names = bp_core_get_user_displaynames( $user_query->user_ids );
+
+	// Loop through names and override each user's fullname.
+	foreach ( $user_id_names as $user_id => $user_fullname ) {
+		if ( isset( $user_query->results[ $user_id ] ) ) {
+			$user_query->results[ $user_id ]->fullname = $user_fullname;
+		}
+	}
+}
+add_filter( 'bp_user_query_populate_extras', 'bp_xprofile_filter_user_query_populate_extras', 2, 2 );
+
+/**
+ * Parse 'xprofile_query' argument passed to BP_User_Query.
+ *
+ * @since 2.2.0
+ *
+ * @param BP_User_Query $q User query object.
+ */
+function bp_xprofile_add_xprofile_query_to_user_query( BP_User_Query $q ) {
+
+	// Bail if no `xprofile_query` clause.
+	if ( empty( $q->query_vars['xprofile_query'] ) ) {
+		return;
+	}
+
+	$xprofile_query = new BP_XProfile_Query( $q->query_vars['xprofile_query'] );
+	$sql            = $xprofile_query->get_sql( 'u', $q->uid_name );
+
+	if ( ! empty( $sql['join'] ) ) {
+		$q->uid_clauses['select'] .= $sql['join'];
+		$q->uid_clauses['where'] .= $sql['where'];
+	}
+}
+add_action( 'bp_pre_user_query', 'bp_xprofile_add_xprofile_query_to_user_query' );
+
+/**
+ * Filter meta queries to modify for the xprofile data schema.
+ *
+ * @since 2.0.0
+ *
+ * @access private Do not use.
+ *
+ * @param string $q SQL query.
+ * @return string
+ */
+function bp_xprofile_filter_meta_query( $q ) {
+	global $wpdb;
+
+	$raw_q = $q;
+
+	/*
+	 * Replace quoted content with __QUOTE__ to avoid false positives.
+	 * This regular expression will match nested quotes.
+	 */
+	$quoted_regex = "/'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'/s";
+	preg_match_all( $quoted_regex, $q, $quoted_matches );
+	$q = preg_replace( $quoted_regex, '__QUOTE__', $q );
+
+	// Get the first word of the command.
+	preg_match( '/^(\S+)/', $q, $first_word_matches );
+
+	if ( empty( $first_word_matches[0] ) ) {
+		return $raw_q;
+	}
+
+	// Get the field type.
+	preg_match( '/xprofile_(group|field|data)_id/', $q, $matches );
+
+	if ( empty( $matches[0] ) || empty( $matches[1] ) ) {
+		return $raw_q;
+	}
+
+	switch ( $first_word_matches[0] ) {
+
+		/**
+		 * SELECT:
+		 * - replace 'xprofile_{fieldtype}_id' with 'object_id'
+		 * - ensure that 'object_id' is aliased to 'xprofile_{fieldtype}_id',
+		 *   because update_meta_cache() needs the column name to parse
+		 *   the query results
+		 * - append the 'object type' WHERE clause
+		 */
+		case 'SELECT' :
+			$q = str_replace(
+				array(
+					$matches[0],
+					'SELECT object_id',
+					'WHERE ',
+				),
+				array(
+					'object_id',
+					'SELECT object_id AS ' . $matches[0],
+					$wpdb->prepare( 'WHERE object_type = %s AND ', $matches[1] ),
+				),
+				$q
+			);
+			break;
+
+		/**
+		 * UPDATE and DELETE:
+		 * - replace 'xprofile_{fieldtype}_id' with 'object_id'
+		 * - append the 'object type' WHERE clause
+		 */
+		case 'UPDATE' :
+		case 'DELETE' :
+			$q = str_replace(
+				array(
+					$matches[0],
+					'WHERE ',
+				),
+				array(
+					'object_id',
+					$wpdb->prepare( 'WHERE object_type = %s AND ', $matches[1] ),
+				),
+				$q
+			);
+			break;
+
+		/**
+		 * UPDATE and DELETE:
+		 * - replace 'xprofile_{fieldtype}_id' with 'object_id'
+		 * - ensure that the object_type field gets filled in
+		 */
+		case 'INSERT' :
+			$q = str_replace(
+				array(
+					'`' . $matches[0] . '`',
+					'VALUES (',
+				),
+				array(
+					'`object_type`,`object_id`',
+					$wpdb->prepare( "VALUES (%s,", $matches[1] ),
+				),
+				$q
+			);
+			break;
+	}
+
+	// Put quoted content back into the string.
+	if ( ! empty( $quoted_matches[0] ) ) {
+		for ( $i = 0; $i < count( $quoted_matches[0] ); $i++ ) {
+			$quote_pos = strpos( $q, '__QUOTE__' );
+			$q = substr_replace( $q, $quoted_matches[0][ $i ], $quote_pos, 9 );
+		}
+	}
+
+	return $q;
+}
+
+/**
+ * Register XProfile personal data exporter.
+ *
+ * @since 4.0.0
+ *
+ * @param array $exporters  An array of personal data exporters.
+ * @return array An array of personal data exporters.
+ */
+function bp_xprofile_register_personal_data_exporter( $exporters ) {
+	$exporters['buddypress-xprofile'] = array(
+		'exporter_friendly_name' => __( 'BuddyPress XProfile Data', 'buddypress' ),
+		'callback'               => 'bp_xprofile_personal_data_exporter',
+	);
+
+	return $exporters;
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-functions.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-functions.php	(revision 12265)
@@ -0,0 +1,1390 @@
+<?php
+/**
+ * BuddyPress XProfile Filters.
+ *
+ * Business functions are where all the magic happens in BuddyPress. They will
+ * handle the actual saving or manipulation of information. Usually they will
+ * hand off to a database class for data access, then return
+ * true or false on success or failure.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileFunctions
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/*** Field Group Management **************************************************/
+
+/**
+ * Fetch a set of field groups, populated with fields and field data.
+ *
+ * Procedural wrapper for BP_XProfile_Group::get() method.
+ *
+ * @since 2.1.0
+ *
+ * @param array $args See {@link BP_XProfile_Group::get()} for description of arguments.
+ * @return array $groups
+ */
+function bp_xprofile_get_groups( $args = array() ) {
+
+	$groups = BP_XProfile_Group::get( $args );
+
+	/**
+	 * Filters a set of field groups, populated with fields and field data.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $groups Array of field groups and field data.
+	 * @param array $args   Array of arguments used to query for groups.
+	 */
+	return apply_filters( 'bp_xprofile_get_groups', $groups, $args );
+}
+
+/**
+ * Insert a new profile field group.
+ *
+ * @since 1.0.0
+ *
+ * @param array|string $args {
+ *    Array of arguments for field group insertion.
+ *
+ *    @type int|bool    $field_group_id ID of the field group to insert into.
+ *    @type string|bool $name           Name of the group.
+ *    @type string      $description    Field group description.
+ *    @type bool        $can_delete     Whether or not the field group can be deleted.
+ * }
+ * @return boolean
+ */
+function xprofile_insert_field_group( $args = '' ) {
+
+	// Parse the arguments.
+	$r = bp_parse_args( $args, array(
+		'field_group_id' => false,
+		'name'           => false,
+		'description'    => '',
+		'can_delete'     => true
+	), 'xprofile_insert_field_group' );
+
+	// Bail if no group name.
+	if ( empty( $r['name'] ) ) {
+		return false;
+	}
+
+	// Create new field group object, maybe using an existing ID.
+	$field_group              = new BP_XProfile_Group( $r['field_group_id'] );
+	$field_group->name        = $r['name'];
+	$field_group->description = $r['description'];
+	$field_group->can_delete  = $r['can_delete'];
+
+	return $field_group->save();
+}
+
+/**
+ * Get a specific profile field group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $field_group_id Field group ID to fetch.
+ * @return false|BP_XProfile_Group
+ */
+function xprofile_get_field_group( $field_group_id = 0 ) {
+
+	// Try to get a specific field group by ID.
+	$field_group = new BP_XProfile_Group( $field_group_id );
+
+	// Bail if group was not found.
+	if ( empty( $field_group->id ) ) {
+		return false;
+	}
+
+	// Return field group.
+	return $field_group;
+}
+
+/**
+ * Delete a specific profile field group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $field_group_id Field group ID to delete.
+ * @return boolean
+ */
+function xprofile_delete_field_group( $field_group_id = 0 ) {
+
+	// Try to get a specific field group by ID.
+	$field_group = xprofile_get_field_group( $field_group_id );
+
+	// Bail if group was not found.
+	if ( false === $field_group ) {
+		return false;
+	}
+
+	// Return the results of trying to delete the field group.
+	return $field_group->delete();
+}
+
+/**
+ * Update the position of a specific profile field group.
+ *
+ * @since 1.0.0
+ *
+ * @param int $field_group_id Field group ID to update.
+ * @param int $position       Field group position to update to.
+ * @return boolean
+ */
+function xprofile_update_field_group_position( $field_group_id = 0, $position = 0 ) {
+	return BP_XProfile_Group::update_position( $field_group_id, $position );
+}
+
+/*** Field Management *********************************************************/
+
+/**
+ * Get details of all xprofile field types.
+ *
+ * @since 2.0.0
+ *
+ * @return array Key/value pairs (field type => class name).
+ */
+function bp_xprofile_get_field_types() {
+	$fields = array(
+		'checkbox'       => 'BP_XProfile_Field_Type_Checkbox',
+		'datebox'        => 'BP_XProfile_Field_Type_Datebox',
+		'multiselectbox' => 'BP_XProfile_Field_Type_Multiselectbox',
+		'number'         => 'BP_XProfile_Field_Type_Number',
+		'url'            => 'BP_XProfile_Field_Type_URL',
+		'radio'          => 'BP_XProfile_Field_Type_Radiobutton',
+		'selectbox'      => 'BP_XProfile_Field_Type_Selectbox',
+		'textarea'       => 'BP_XProfile_Field_Type_Textarea',
+		'textbox'        => 'BP_XProfile_Field_Type_Textbox',
+		'telephone'      => 'BP_XProfile_Field_Type_Telephone',
+	);
+
+	/**
+	 * Filters the list of all xprofile field types.
+	 *
+	 * If you've added a custom field type in a plugin, register it with this filter.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $fields Array of field type/class name pairings.
+	 */
+	return apply_filters( 'bp_xprofile_get_field_types', $fields );
+}
+
+/**
+ * Creates the specified field type object; used for validation and templating.
+ *
+ * @since 2.0.0
+ *
+ * @param string $type Type of profile field to create. See {@link bp_xprofile_get_field_types()} for default core values.
+ * @return object $value If field type unknown, returns BP_XProfile_Field_Type_Textarea.
+ *                       Otherwise returns an instance of the relevant child class of BP_XProfile_Field_Type.
+ */
+function bp_xprofile_create_field_type( $type ) {
+
+	$field = bp_xprofile_get_field_types();
+	$class = isset( $field[$type] ) ? $field[$type] : '';
+
+	/**
+	 * To handle (missing) field types, fallback to a placeholder field object if a type is unknown.
+	 */
+	if ( $class && class_exists( $class ) ) {
+		return new $class;
+	} else {
+		return new BP_XProfile_Field_Type_Placeholder;
+	}
+}
+
+/**
+ * Insert or update an xprofile field.
+ *
+ * @since 1.1.0
+ *
+ * @param array|string $args {
+ *     Array of arguments.
+ *     @type int    $field_id          Optional. Pass the ID of an existing field to edit that field.
+ *     @type int    $field_group_id    ID of the associated field group.
+ *     @type int    $parent_id         Optional. ID of the parent field.
+ *     @type string $type              Field type. Checked against a field_types whitelist.
+ *     @type string $name              Name of the new field.
+ *     @type string $description       Optional. Descriptive text for the field.
+ *     @type bool   $is_required       Optional. Whether users must provide a value for the field. Default: false.
+ *     @type bool   $can_delete        Optional. Whether admins can delete this field in the Dashboard interface.
+ *                                     Generally this is false only for the Name field, which is required throughout BP.
+ *                                     Default: true.
+ *     @type string $order_by          Optional. For field types that support options (such as 'radio'), this flag
+ *                                     determines whether the sort order of the options will be 'default'
+ *                                     (order created) or 'custom'.
+ *     @type bool   $is_default_option Optional. For the 'option' field type, setting this value to true means that
+ *                                     it'll be the default value for the parent field when the user has not yet
+ *                                     overridden. Default: true.
+ *     @type int    $option_order      Optional. For the 'option' field type, this determines the order in which the
+ *                                     options appear.
+ * }
+ * @return bool|int False on failure, ID of new field on success.
+ */
+function xprofile_insert_field( $args = '' ) {
+
+	$r = wp_parse_args( $args, array(
+		'field_id'          => null,
+		'field_group_id'    => null,
+		'parent_id'         => null,
+		'type'              => '',
+		'name'              => '',
+		'description'       => '',
+		'is_required'       => false,
+		'can_delete'        => true,
+		'order_by'          => '',
+		'is_default_option' => false,
+		'option_order'      => null,
+		'field_order'       => null,
+	) );
+
+	// Field_group_id is required.
+	if ( empty( $r['field_group_id'] ) ) {
+		return false;
+	}
+
+	// Check this is a non-empty, valid field type.
+	if ( ! in_array( $r['type'], (array) buddypress()->profile->field_types ) ) {
+		return false;
+	}
+
+	// Instantiate a new field object.
+	if ( ! empty( $r['field_id'] ) ) {
+		$field = xprofile_get_field( $r['field_id'] );
+	} else {
+		$field = new BP_XProfile_Field;
+	}
+
+	$field->group_id = $r['field_group_id'];
+	$field->type     = $r['type'];
+
+	// The 'name' field cannot be empty.
+	if ( ! empty( $r['name'] ) ) {
+		$field->name = $r['name'];
+	}
+
+	$field->description       = $r['description'];
+	$field->order_by          = $r['order_by'];
+	$field->parent_id         = (int) $r['parent_id'];
+	$field->field_order       = (int) $r['field_order'];
+	$field->option_order      = (int) $r['option_order'];
+	$field->is_required       = (bool) $r['is_required'];
+	$field->can_delete        = (bool) $r['can_delete'];
+	$field->is_default_option = (bool) $r['is_default_option'];
+
+	return $field->save();
+}
+
+/**
+ * Get a profile field object.
+ *
+ * @since 1.1.0
+ * @since 2.8.0 Added `$user_id` and `$get_data` parameters.
+ *
+ * @param int|object $field    ID of the field or object representing field data.
+ * @param int|null   $user_id  Optional. ID of the user associated with the field.
+ *                             Ignored if `$get_data` is false. If `$get_data` is
+ *                             true, but no `$user_id` is provided, defaults to
+ *                             logged-in user ID.
+ * @param bool       $get_data Whether to fetch data for the specified `$user_id`.
+ * @return BP_XProfile_Field|null Field object if found, otherwise null.
+ */
+function xprofile_get_field( $field, $user_id = null, $get_data = true ) {
+	if ( $field instanceof BP_XProfile_Field ) {
+		$_field = $field;
+	} elseif ( is_object( $field ) ) {
+		$_field = new BP_XProfile_Field();
+		$_field->fill_data( $field );
+	} else {
+		$_field = BP_XProfile_Field::get_instance( $field, $user_id, $get_data );
+	}
+
+	if ( ! $_field ) {
+		return null;
+	}
+
+	return $_field;
+}
+
+/**
+ * Delete a profile field object.
+ *
+ * @since 1.1.0
+ *
+ * @param int|object $field_id ID of the field or object representing field data.
+ * @return bool Whether or not the field was deleted.
+ */
+function xprofile_delete_field( $field_id ) {
+	$field = new BP_XProfile_Field( $field_id );
+	return $field->delete();
+}
+
+/*** Field Data Management *****************************************************/
+
+
+/**
+ * Fetches profile data for a specific field for the user.
+ *
+ * When the field value is serialized, this function unserializes and filters
+ * each item in the array.
+ *
+ * @since 1.0.0
+ *
+ * @param mixed  $field        The ID of the field, or the $name of the field.
+ * @param int    $user_id      The ID of the user.
+ * @param string $multi_format How should array data be returned? 'comma' if you want a
+ *                             comma-separated string; 'array' if you want an array.
+ * @return mixed The profile field data.
+ */
+function xprofile_get_field_data( $field, $user_id = 0, $multi_format = 'array' ) {
+
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	if ( is_numeric( $field ) ) {
+		$field_id = $field;
+	} else {
+		$field_id = xprofile_get_field_id_from_name( $field );
+	}
+
+	if ( empty( $field_id ) ) {
+		return false;
+	}
+
+	$values = maybe_unserialize( BP_XProfile_ProfileData::get_value_byid( $field_id, $user_id ) );
+
+	if ( is_array( $values ) ) {
+		$data = array();
+		foreach( (array) $values as $value ) {
+
+			/**
+			 * Filters the field data value for a specific field for the user.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param string $value    Value saved for the field.
+			 * @param int    $field_id ID of the field being displayed.
+			 * @param int    $user_id  ID of the user being displayed.
+			 */
+			$data[] = apply_filters( 'xprofile_get_field_data', $value, $field_id, $user_id );
+		}
+
+		if ( 'comma' == $multi_format ) {
+			$data = implode( ', ', $data );
+		}
+	} else {
+		/** This filter is documented in bp-xprofile/bp-xprofile-functions.php */
+		$data = apply_filters( 'xprofile_get_field_data', $values, $field_id, $user_id );
+	}
+
+	return $data;
+}
+
+/**
+ * A simple function to set profile data for a specific field for a specific user.
+ *
+ * @since 1.0.0
+ *
+ * @param int|string $field       The ID of the field, or the $name of the field.
+ * @param int        $user_id     The ID of the user.
+ * @param mixed      $value       The value for the field you want to set for the user.
+ * @param bool       $is_required Whether or not the field is required.
+ * @return bool True on success, false on failure.
+ */
+function xprofile_set_field_data( $field, $user_id, $value, $is_required = false ) {
+
+	if ( is_numeric( $field ) ) {
+		$field_id = $field;
+	} else {
+		$field_id = xprofile_get_field_id_from_name( $field );
+	}
+
+	if ( empty( $field_id ) ) {
+		return false;
+	}
+
+	$field          = xprofile_get_field( $field_id );
+	$field_type     = BP_XProfile_Field::get_type( $field_id );
+	$field_type_obj = bp_xprofile_create_field_type( $field_type );
+
+	/**
+	 * Filter the raw submitted profile field value.
+	 *
+	 * Use this filter to modify the values submitted by users before
+	 * doing field-type-specific validation.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param mixed                  $value          Value passed to xprofile_set_field_data().
+	 * @param BP_XProfile_Field      $field          Field object.
+	 * @param BP_XProfile_Field_Type $field_type_obj Field type object.
+	 */
+	$value = apply_filters( 'bp_xprofile_set_field_data_pre_validate', $value, $field, $field_type_obj );
+
+	// Special-case support for integer 0 for the number field type.
+	if ( $is_required && ! is_integer( $value ) && $value !== '0' && ( empty( $value ) || ! is_array( $value ) && ! strlen( trim( $value ) ) ) ) {
+		return false;
+	}
+
+	/**
+	 * Certain types of fields (checkboxes, multiselects) may come through empty.
+	 * Save as empty array so this isn't overwritten by the default on next edit.
+	 *
+	 * Special-case support for integer 0 for the number field type
+	 */
+	if ( empty( $value ) && ! is_integer( $value ) && $value !== '0' && $field_type_obj->accepts_null_value ) {
+		$value = array();
+	}
+
+	// If the value is empty, then delete any field data that exists, unless the field is of a type
+	// where null values are semantically meaningful.
+	if ( empty( $value ) && ! is_integer( $value ) && $value !== '0' && ! $field_type_obj->accepts_null_value ) {
+		xprofile_delete_field_data( $field_id, $user_id );
+		return true;
+	}
+
+	// For certain fields, only certain parameters are acceptable, so add them to the whitelist.
+	if ( $field_type_obj->supports_options ) {
+		$field_type_obj->set_whitelist_values( wp_list_pluck( $field->get_children(), 'name' ) );
+	}
+
+	// Check the value is in an accepted format for this form field.
+	if ( ! $field_type_obj->is_valid( $value ) ) {
+		return false;
+	}
+
+	$field           = new BP_XProfile_ProfileData();
+	$field->field_id = $field_id;
+	$field->user_id  = $user_id;
+
+	// Gets un/reserialized via xprofile_sanitize_data_value_before_save()
+	$field->value    = maybe_serialize( $value );
+
+	return $field->save();
+}
+
+/**
+ * Set the visibility level for this field.
+ *
+ * @since 1.6.0
+ *
+ * @param int    $field_id         The ID of the xprofile field.
+ * @param int    $user_id          The ID of the user to whom the data belongs.
+ * @param string $visibility_level What the visibity setting should be.
+ * @return bool True on success
+ */
+function xprofile_set_field_visibility_level( $field_id = 0, $user_id = 0, $visibility_level = '' ) {
+	if ( empty( $field_id ) || empty( $user_id ) || empty( $visibility_level ) ) {
+		return false;
+	}
+
+	// Check against a whitelist.
+	$allowed_values = bp_xprofile_get_visibility_levels();
+	if ( !array_key_exists( $visibility_level, $allowed_values ) ) {
+		return false;
+	}
+
+	// Stored in an array in usermeta.
+	$current_visibility_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
+
+	if ( !$current_visibility_levels ) {
+		$current_visibility_levels = array();
+	}
+
+	$current_visibility_levels[$field_id] = $visibility_level;
+
+	return bp_update_user_meta( $user_id, 'bp_xprofile_visibility_levels', $current_visibility_levels );
+}
+
+/**
+ * Get the visibility level for a field.
+ *
+ * @since 2.0.0
+ *
+ * @param int $field_id The ID of the xprofile field.
+ * @param int $user_id The ID of the user to whom the data belongs.
+ * @return string
+ */
+function xprofile_get_field_visibility_level( $field_id = 0, $user_id = 0 ) {
+	$current_level = '';
+
+	if ( empty( $field_id ) || empty( $user_id ) ) {
+		return $current_level;
+	}
+
+	$current_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
+	$current_level  = isset( $current_levels[ $field_id ] ) ? $current_levels[ $field_id ] : '';
+
+	// Use the user's stored level, unless custom visibility is disabled.
+	$field = xprofile_get_field( $field_id );
+	if ( isset( $field->allow_custom_visibility ) && 'disabled' === $field->allow_custom_visibility ) {
+		$current_level = $field->default_visibility;
+	}
+
+	// If we're still empty, it means that overrides are permitted, but the
+	// user has not provided a value. Use the default value.
+	if ( empty( $current_level ) ) {
+		$current_level = $field->default_visibility;
+	}
+
+	return $current_level;
+}
+
+/**
+ * Delete XProfile field data.
+ *
+ * @since 1.1.0
+ *
+ * @param string $field   Field to delete.
+ * @param int    $user_id User ID to delete field from.
+ * @return bool Whether or not the field was deleted.
+ */
+function xprofile_delete_field_data( $field = '', $user_id = 0 ) {
+
+	// Get the field ID.
+	if ( is_numeric( $field ) ) {
+		$field_id = (int) $field;
+	} else {
+		$field_id = xprofile_get_field_id_from_name( $field );
+	}
+
+	// Bail if field or user ID are empty.
+	if ( empty( $field_id ) || empty( $user_id ) ) {
+		return false;
+	}
+
+	// Get the profile field data to delete.
+	$field = new BP_XProfile_ProfileData( $field_id, $user_id );
+
+	// Delete the field data.
+	return $field->delete();
+}
+
+/**
+ * Check if field is a required field.
+ *
+ * @since 1.1.0
+ *
+ * @param int $field_id ID of the field to check for.
+ * @return bool Whether or not field is required.
+ */
+function xprofile_check_is_required_field( $field_id ) {
+	$field  = new BP_XProfile_Field( $field_id );
+	$retval = false;
+
+	if ( isset( $field->is_required ) ) {
+		$retval = $field->is_required;
+	}
+
+	return (bool) $retval;
+}
+
+/**
+ * Returns the ID for the field based on the field name.
+ *
+ * @since 1.0.0
+ *
+ * @param string $field_name The name of the field to get the ID for.
+ * @return int|null $field_id on success, false on failure.
+ */
+function xprofile_get_field_id_from_name( $field_name ) {
+	return BP_XProfile_Field::get_id_from_name( $field_name );
+}
+
+/**
+ * Fetches a random piece of profile data for the user.
+ *
+ * @since 1.0.0
+ *
+ * @global BuddyPress $bp           The one true BuddyPress instance.
+ * @global object     $wpdb         WordPress DB access object.
+ * @global object     $current_user WordPress global variable containing current logged in user information.
+ *
+ * @param int  $user_id          User ID of the user to get random data for.
+ * @param bool $exclude_fullname Optional; whether or not to exclude the full name field as random data.
+ *                               Defaults to true.
+ * @return string|bool The fetched random data for the user, or false if no data or no match.
+ */
+function xprofile_get_random_profile_data( $user_id, $exclude_fullname = true ) {
+	$field_data = BP_XProfile_ProfileData::get_random( $user_id, $exclude_fullname );
+
+	if ( empty( $field_data ) ) {
+		return false;
+	}
+
+	$field_data[0]->value = xprofile_format_profile_field( $field_data[0]->type, $field_data[0]->value );
+
+	if ( empty( $field_data[0]->value ) ) {
+		return false;
+	}
+
+	/**
+	 * Filters a random piece of profile data for the user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array $field_data Array holding random profile data.
+	 */
+	return apply_filters( 'xprofile_get_random_profile_data', $field_data );
+}
+
+/**
+ * Formats a profile field according to its type. [ TODO: Should really be moved to filters ]
+ *
+ * @since 1.0.0
+ *
+ * @param string $field_type  The type of field: datebox, selectbox, textbox etc.
+ * @param string $field_value The actual value.
+ * @return string|bool The formatted value, or false if value is empty.
+ */
+function xprofile_format_profile_field( $field_type, $field_value ) {
+
+	if ( empty( $field_value ) ) {
+		return false;
+	}
+
+	$field_value = bp_unserialize_profile_field( $field_value );
+
+	if ( 'datebox' != $field_type ) {
+		$content = $field_value;
+		$field_value = str_replace( ']]>', ']]&gt;', $content );
+	}
+
+	return xprofile_filter_format_field_value_by_type( stripslashes_deep( $field_value ), $field_type );
+}
+
+/**
+ * Update the field position for a provided field.
+ *
+ * @since 1.1.0
+ *
+ * @param int $field_id       ID of the field to update.
+ * @param int $position       Position to update the field to.
+ * @param int $field_group_id Group ID for group the field is in.
+ * @return bool
+ */
+function xprofile_update_field_position( $field_id, $position, $field_group_id ) {
+	return BP_XProfile_Field::update_position( $field_id, $position, $field_group_id );
+}
+
+/**
+ * Replace the displayed and logged-in users fullnames with the xprofile name, if required.
+ *
+ * The Members component uses the logged-in user's display_name to set the
+ * value of buddypress()->loggedin_user->fullname. However, in cases where
+ * profile sync is disabled, display_name may diverge from the xprofile
+ * fullname field value, and the xprofile field should take precedence.
+ *
+ * Runs at bp_setup_globals:100 to ensure that all components have loaded their
+ * globals before attempting any overrides.
+ *
+ * @since 2.0.0
+ */
+function xprofile_override_user_fullnames() {
+	// If sync is enabled, the two names will match. No need to continue.
+	if ( ! bp_disable_profile_sync() ) {
+		return;
+	}
+
+	if ( bp_loggedin_user_id() ) {
+		buddypress()->loggedin_user->fullname = bp_core_get_user_displayname( bp_loggedin_user_id() );
+	}
+
+	if ( bp_displayed_user_id() ) {
+		buddypress()->displayed_user->fullname = bp_core_get_user_displayname( bp_displayed_user_id() );
+	}
+}
+add_action( 'bp_setup_globals', 'xprofile_override_user_fullnames', 100 );
+
+/**
+ * Setup the avatar upload directory for a user.
+ *
+ * @since 1.0.0
+ *
+ * @package BuddyPress Core
+ *
+ * @param string $directory The root directory name. Optional.
+ * @param int    $user_id   The user ID. Optional.
+ * @return array Array containing the path, URL, and other helpful settings.
+ */
+function xprofile_avatar_upload_dir( $directory = 'avatars', $user_id = 0 ) {
+
+	// Use displayed user if no user ID was passed.
+	if ( empty( $user_id ) ) {
+		$user_id = bp_displayed_user_id();
+	}
+
+	// Failsafe against accidentally nooped $directory parameter.
+	if ( empty( $directory ) ) {
+		$directory = 'avatars';
+	}
+
+	$path      = bp_core_avatar_upload_path() . '/' . $directory. '/' . $user_id;
+	$newbdir   = $path;
+	$newurl    = bp_core_avatar_url() . '/' . $directory. '/' . $user_id;
+	$newburl   = $newurl;
+	$newsubdir = '/' . $directory. '/' . $user_id;
+
+	/**
+	 * Filters the avatar upload directory for a user.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param array $value Array containing the path, URL, and other helpful settings.
+	 */
+	return apply_filters( 'xprofile_avatar_upload_dir', array(
+		'path'    => $path,
+		'url'     => $newurl,
+		'subdir'  => $newsubdir,
+		'basedir' => $newbdir,
+		'baseurl' => $newburl,
+		'error'   => false
+	) );
+}
+
+/**
+ * When search_terms are passed to BP_User_Query, search against xprofile fields.
+ *
+ * @since 2.0.0
+ *
+ * @param array         $sql   Clauses in the user_id SQL query.
+ * @param BP_User_Query $query User query object.
+ * @return array
+ */
+function bp_xprofile_bp_user_query_search( $sql, BP_User_Query $query ) {
+	global $wpdb;
+
+	if ( empty( $query->query_vars['search_terms'] ) || empty( $sql['where']['search'] ) ) {
+		return $sql;
+	}
+
+	$bp = buddypress();
+
+	$search_terms_clean = bp_esc_like( wp_kses_normalize_entities( $query->query_vars['search_terms'] ) );
+
+	if ( $query->query_vars['search_wildcard'] === 'left' ) {
+		$search_terms_nospace = '%' . $search_terms_clean;
+		$search_terms_space   = '%' . $search_terms_clean . ' %';
+	} elseif ( $query->query_vars['search_wildcard'] === 'right' ) {
+		$search_terms_nospace =        $search_terms_clean . '%';
+		$search_terms_space   = '% ' . $search_terms_clean . '%';
+	} else {
+		$search_terms_nospace = '%' . $search_terms_clean . '%';
+		$search_terms_space   = '%' . $search_terms_clean . '%';
+	}
+
+	// Combine the core search (against wp_users) into a single OR clause
+	// with the xprofile_data search.
+	$matched_user_ids = $wpdb->get_col( $wpdb->prepare(
+		"SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE %s OR value LIKE %s",
+		$search_terms_nospace,
+		$search_terms_space
+	) );
+
+	if ( ! empty( $matched_user_ids ) ) {
+		$search_core     = $sql['where']['search'];
+		$search_combined = " ( u.{$query->uid_name} IN (" . implode(',', $matched_user_ids) . ") OR {$search_core} )";
+		$sql['where']['search'] = $search_combined;
+	}
+
+	return $sql;
+}
+add_action( 'bp_user_query_uid_clauses', 'bp_xprofile_bp_user_query_search', 10, 2 );
+
+/**
+ * Syncs Xprofile data to the standard built in WordPress profile data.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id ID of the user to sync.
+ * @return bool
+ */
+function xprofile_sync_wp_profile( $user_id = 0 ) {
+
+	// Bail if profile syncing is disabled.
+	if ( bp_disable_profile_sync() ) {
+		return true;
+	}
+
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	if ( empty( $user_id ) ) {
+		return false;
+	}
+
+	$fullname = xprofile_get_field_data( bp_xprofile_fullname_field_id(), $user_id );
+	$space    = strpos( $fullname, ' ' );
+
+	if ( false === $space ) {
+		$firstname = $fullname;
+		$lastname = '';
+	} else {
+		$firstname = substr( $fullname, 0, $space );
+		$lastname = trim( substr( $fullname, $space, strlen( $fullname ) ) );
+	}
+
+	bp_update_user_meta( $user_id, 'nickname',   $fullname  );
+	bp_update_user_meta( $user_id, 'first_name', $firstname );
+	bp_update_user_meta( $user_id, 'last_name',  $lastname  );
+
+	wp_update_user( array( 'ID' => $user_id, 'display_name' => $fullname ) );
+}
+add_action( 'bp_core_signup_user',      'xprofile_sync_wp_profile' );
+add_action( 'bp_core_activated_user',   'xprofile_sync_wp_profile' );
+
+/**
+ * Syncs the standard built in WordPress profile data to XProfile.
+ *
+ * @since 1.2.4
+ *
+ * @param object $errors Array of errors. Passed by reference.
+ * @param bool   $update Whether or not being upated.
+ * @param object $user   User object whose profile is being synced. Passed by reference.
+ */
+function xprofile_sync_bp_profile( &$errors, $update, &$user ) {
+
+	// Bail if profile syncing is disabled.
+	if ( bp_disable_profile_sync() || ! $update || $errors->get_error_codes() ) {
+		return;
+	}
+
+	xprofile_set_field_data( bp_xprofile_fullname_field_id(), $user->ID, $user->display_name );
+}
+add_action( 'user_profile_update_errors', 'xprofile_sync_bp_profile', 10, 3 );
+
+/**
+ * Update the WP display, last, and first name fields when the xprofile display name field is updated.
+ *
+ * @since 3.0.0
+ *
+ * @param BP_XProfile_ProfileData $data Current instance of the profile data being saved.
+ */
+function xprofile_sync_wp_profile_on_single_field_set( $data ) {
+
+	if ( bp_xprofile_fullname_field_id() !== $data->field_id ) {
+		return;
+	}
+
+	xprofile_sync_wp_profile( $data->user_id );
+}
+add_action( 'xprofile_data_after_save', 'xprofile_sync_wp_profile_on_single_field_set' );
+
+/**
+ * When a user is deleted, we need to clean up the database and remove all the
+ * profile data from each table. Also we need to clean anything up in the
+ * usermeta table that this component uses.
+ *
+ * @since 1.0.0
+ *
+ * @param int $user_id The ID of the deleted user.
+ */
+function xprofile_remove_data( $user_id ) {
+	BP_XProfile_ProfileData::delete_data_for_user( $user_id );
+}
+add_action( 'wpmu_delete_user',  'xprofile_remove_data' );
+add_action( 'delete_user',       'xprofile_remove_data' );
+add_action( 'bp_make_spam_user', 'xprofile_remove_data' );
+
+/*** XProfile Meta ****************************************************/
+
+/**
+ * Delete a piece of xprofile metadata.
+ *
+ * @since 1.5.0
+ *
+ * @param int         $object_id   ID of the object the metadata belongs to.
+ * @param string      $object_type Type of object. 'group', 'field', or 'data'.
+ * @param string|bool $meta_key    Key of the metadata being deleted. If omitted, all
+ *                                 metadata for the object will be deleted.
+ * @param mixed       $meta_value  Optional. If provided, only metadata that matches
+ *                                 the value will be permitted.
+ * @param bool        $delete_all  Optional. If true, delete matching metadata entries
+ *                                 for all objects, ignoring the specified object_id. Otherwise, only
+ *                                 delete matching metadata entries for the specified object.
+ *                                 Default: false.
+ * @return bool True on success, false on failure.
+ */
+function bp_xprofile_delete_meta( $object_id, $object_type, $meta_key = false, $meta_value = false, $delete_all = false ) {
+	global $wpdb;
+
+	// Sanitize object type.
+	if ( ! in_array( $object_type, array( 'group', 'field', 'data' ) ) ) {
+		return false;
+	}
+
+	// Legacy - if no meta_key is passed, delete all for the item.
+	if ( empty( $meta_key ) ) {
+		$table_key  = 'xprofile_' . $object_type . 'meta';
+		$table_name = $wpdb->{$table_key};
+		$keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$table_name} WHERE object_type = %s AND object_id = %d", $object_type, $object_id ) );
+
+		// Force delete_all to false if deleting all for object.
+		$delete_all = false;
+	} else {
+		$keys = array( $meta_key );
+	}
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'query', 'bp_xprofile_filter_meta_query' );
+
+	$retval = false;
+	foreach ( $keys as $key ) {
+		$retval = delete_metadata( 'xprofile_' . $object_type, $object_id, $key, $meta_value, $delete_all );
+	}
+
+	remove_filter( 'query', 'bp_xprofile_filter_meta_query' );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Get a piece of xprofile metadata.
+ *
+ * Note that the default value of $single is true, unlike in the case of the
+ * underlying get_metadata() function. This is for backward compatibility.
+ *
+ * @since 1.5.0
+ *
+ * @param int    $object_id   ID of the object the metadata belongs to.
+ * @param string $object_type Type of object. 'group', 'field', or 'data'.
+ * @param string $meta_key    Key of the metadata being fetched. If omitted, all
+ *                            metadata for the object will be retrieved.
+ * @param bool   $single      Optional. If true, return only the first value of the
+ *                            specified meta_key. This parameter has no effect if meta_key is not
+ *                            specified. Default: true.
+ * @return mixed Meta value if found. False on failure.
+ */
+function bp_xprofile_get_meta( $object_id, $object_type, $meta_key = '', $single = true ) {
+	// Sanitize object type.
+	if ( ! in_array( $object_type, array( 'group', 'field', 'data' ) ) ) {
+		return false;
+	}
+
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'query', 'bp_xprofile_filter_meta_query' );
+	$retval = get_metadata( 'xprofile_' . $object_type, $object_id, $meta_key, $single );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+	remove_filter( 'query', 'bp_xprofile_filter_meta_query' );
+
+	return $retval;
+}
+
+/**
+ * Update a piece of xprofile metadata.
+ *
+ * @since 1.5.0
+ *
+ * @param int    $object_id   ID of the object the metadata belongs to.
+ * @param string $object_type Type of object. 'group', 'field', or 'data'.
+ * @param string $meta_key    Key of the metadata being updated.
+ * @param string $meta_value  Value of the metadata being updated.
+ * @param mixed  $prev_value  Optional. If specified, only update existing
+ *                            metadata entries with the specified value.
+ *                            Otherwise update all entries.
+ * @return bool|int Returns false on failure. On successful update of existing
+ *                  metadata, returns true. On successful creation of new metadata,
+ *                  returns the integer ID of the new metadata row.
+ */
+function bp_xprofile_update_meta( $object_id, $object_type, $meta_key, $meta_value, $prev_value = '' ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'query', 'bp_xprofile_filter_meta_query' );
+	$retval = update_metadata( 'xprofile_' . $object_type, $object_id, $meta_key, $meta_value, $prev_value );
+	remove_filter( 'query', 'bp_xprofile_filter_meta_query' );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+
+	return $retval;
+}
+
+/**
+ * Add a piece of xprofile metadata.
+ *
+ * @since 2.0.0
+ *
+ * @param int    $object_id   ID of the object the metadata belongs to.
+ * @param string $object_type Type of object. 'group', 'field', or 'data'.
+ * @param string $meta_key    Metadata key.
+ * @param mixed  $meta_value  Metadata value.
+ * @param bool   $unique      Optional. Whether to enforce a single metadata value
+ *                            for the given key. If true, and the object already
+ *                            has a value for the key, no change will be made.
+ *                            Default false.
+ * @return int|bool The meta ID on successful update, false on failure.
+ */
+function bp_xprofile_add_meta( $object_id, $object_type, $meta_key, $meta_value, $unique = false ) {
+	add_filter( 'query', 'bp_filter_metaid_column_name' );
+	add_filter( 'query', 'bp_xprofile_filter_meta_query' );
+	$retval = add_metadata( 'xprofile_' . $object_type , $object_id, $meta_key, $meta_value, $unique );
+	remove_filter( 'query', 'bp_filter_metaid_column_name' );
+	remove_filter( 'query', 'bp_xprofile_filter_meta_query' );
+
+	return $retval;
+}
+
+/**
+ * Updates the fieldgroup metadata.
+ *
+ * @since 1.5.0
+ *
+ * @param int    $field_group_id Group ID for the group field belongs to.
+ * @param string $meta_key       Meta key to update.
+ * @param string $meta_value     Meta value to update to.
+ * @return bool|int
+ */
+function bp_xprofile_update_fieldgroup_meta( $field_group_id, $meta_key, $meta_value ) {
+	return bp_xprofile_update_meta( $field_group_id, 'group', $meta_key, $meta_value );
+}
+
+/**
+ * Updates the field metadata.
+ *
+ * @since 1.5.0
+ *
+ * @param int    $field_id   Field ID to update.
+ * @param string $meta_key   Meta key to update.
+ * @param string $meta_value Meta value to update to.
+ * @return bool|int
+ */
+function bp_xprofile_update_field_meta( $field_id, $meta_key, $meta_value ) {
+	return bp_xprofile_update_meta( $field_id, 'field', $meta_key, $meta_value );
+}
+
+/**
+ * Updates the fielddata metadata.
+ *
+ * @since 1.5.0
+ *
+ * @param int    $field_data_id Field ID to update.
+ * @param string $meta_key      Meta key to update.
+ * @param string $meta_value    Meta value to update to.
+ * @return bool|int
+ */
+function bp_xprofile_update_fielddata_meta( $field_data_id, $meta_key, $meta_value ) {
+	return bp_xprofile_update_meta( $field_data_id, 'data', $meta_key, $meta_value );
+}
+
+/**
+ * Return the field ID for the Full Name xprofile field.
+ *
+ * @since 2.0.0
+ *
+ * @return int Field ID.
+ */
+function bp_xprofile_fullname_field_id() {
+	$id = wp_cache_get( 'fullname_field_id', 'bp_xprofile' );
+
+	if ( false === $id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+		$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE name = %s", addslashes( bp_xprofile_fullname_field_name() ) ) );
+
+		wp_cache_set( 'fullname_field_id', $id, 'bp_xprofile' );
+	}
+
+	return absint( $id );
+}
+
+/**
+ * Return the field name for the Full Name xprofile field.
+ *
+ * @since 1.5.0
+ *
+ * @return string The field name.
+ */
+function bp_xprofile_fullname_field_name() {
+
+	/**
+	 * Filters the field name for the Full Name xprofile field.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $value BP_XPROFILE_FULLNAME_FIELD_NAME Full name field constant.
+	 */
+	return apply_filters( 'bp_xprofile_fullname_field_name', BP_XPROFILE_FULLNAME_FIELD_NAME );
+}
+
+/**
+ * Is rich text enabled for this profile field?
+ *
+ * By default, rich text is enabled for textarea fields and disabled for all other field types.
+ *
+ * @since 2.4.0
+ *
+ * @param int|null $field_id Optional. Default current field ID.
+ * @return bool
+ */
+function bp_xprofile_is_richtext_enabled_for_field( $field_id = null ) {
+	if ( ! $field_id ) {
+		$field_id = bp_get_the_profile_field_id();
+	}
+
+	$field = xprofile_get_field( $field_id );
+
+	$enabled = false;
+	if ( $field instanceof BP_XProfile_Field ) {
+		$enabled = (bool) $field->type_obj->supports_richtext;
+	}
+
+	/**
+	 * Filters whether richtext is enabled for the given field.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param bool $enabled  True if richtext is enabled for the field, otherwise false.
+	 * @param int  $field_id ID of the field.
+	 */
+	return apply_filters( 'bp_xprofile_is_richtext_enabled_for_field', $enabled, $field_id );
+}
+
+/**
+ * Get visibility levels out of the $bp global.
+ *
+ * @since 1.6.0
+ *
+ * @return array
+ */
+function bp_xprofile_get_visibility_levels() {
+
+	/**
+	 * Filters the visibility levels out of the $bp global.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $visibility_levels Array of visibility levels.
+	 */
+	return apply_filters( 'bp_xprofile_get_visibility_levels', buddypress()->profile->visibility_levels );
+}
+
+/**
+ * Get the ids of fields that are hidden for this displayed/loggedin user pair.
+ *
+ * This is the function primarily responsible for profile field visibility. It works by determining
+ * the relationship between the displayed_user (ie the profile owner) and the current_user (ie the
+ * profile viewer). Then, based on that relationship, we query for the set of fields that should
+ * be excluded from the profile loop.
+ *
+ * @since 1.6.0
+ *
+ * @see BP_XProfile_Group::get()
+ *   or if you have added your own custom levels.
+ *
+ * @param int $displayed_user_id The id of the user the profile fields belong to.
+ * @param int $current_user_id   The id of the user viewing the profile.
+ * @return array An array of field ids that should be excluded from the profile query
+ */
+function bp_xprofile_get_hidden_fields_for_user( $displayed_user_id = 0, $current_user_id = 0 ) {
+	if ( !$displayed_user_id ) {
+		$displayed_user_id = bp_displayed_user_id();
+	}
+
+	if ( !$displayed_user_id ) {
+		return array();
+	}
+
+	if ( !$current_user_id ) {
+		$current_user_id = bp_loggedin_user_id();
+	}
+
+	// @todo - This is where you'd swap out for current_user_can() checks
+	$hidden_levels = bp_xprofile_get_hidden_field_types_for_user( $displayed_user_id, $current_user_id );
+	$hidden_fields = bp_xprofile_get_fields_by_visibility_levels( $displayed_user_id, $hidden_levels );
+
+	/**
+	 * Filters the ids of fields that are hidden for this displayed/loggedin user pair.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array $hidden_fields     Array of hidden fields for the displayed/logged in user.
+	 * @param int   $displayed_user_id ID of the displayed user.
+	 * @param int   $current_user_id   ID of the current user.
+	 */
+	return apply_filters( 'bp_xprofile_get_hidden_fields_for_user', $hidden_fields, $displayed_user_id, $current_user_id );
+}
+
+/**
+ * Get the visibility levels that should be hidden for this user pair.
+ *
+ * Field visibility is determined based on the relationship between the
+ * logged-in user, the displayed user, and the visibility setting for the
+ * current field. (See bp_xprofile_get_hidden_fields_for_user().) This
+ * utility function speeds up this matching by fetching the visibility levels
+ * that should be hidden for the current user pair.
+ *
+ * @since 1.8.2
+ *
+ * @see bp_xprofile_get_hidden_fields_for_user()
+ *
+ * @param int $displayed_user_id The id of the user the profile fields belong to.
+ * @param int $current_user_id   The id of the user viewing the profile.
+ * @return array An array of visibility levels hidden to the current user.
+ */
+function bp_xprofile_get_hidden_field_types_for_user( $displayed_user_id = 0, $current_user_id = 0 ) {
+
+	// Current user is logged in.
+	if ( ! empty( $current_user_id ) ) {
+
+		// Nothing's private when viewing your own profile, or when the
+		// current user is an admin.
+		if ( $displayed_user_id == $current_user_id || bp_current_user_can( 'bp_moderate' ) ) {
+			$hidden_levels = array();
+
+		// If the current user and displayed user are friends, show all.
+		} elseif ( bp_is_active( 'friends' ) && friends_check_friendship( $displayed_user_id, $current_user_id ) ) {
+			$hidden_levels = array( 'adminsonly', );
+
+		// Current user is logged in but not friends, so exclude friends-only.
+		} else {
+			$hidden_levels = array( 'friends', 'adminsonly', );
+		}
+
+	// Current user is not logged in, so exclude friends-only, loggedin, and adminsonly.
+	} else {
+		$hidden_levels = array( 'friends', 'loggedin', 'adminsonly', );
+	}
+
+	/**
+	 * Filters the visibility levels that should be hidden for this user pair.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $hidden_fields     Array of hidden fields for the displayed/logged in user.
+	 * @param int   $displayed_user_id ID of the displayed user.
+	 * @param int   $current_user_id   ID of the current user.
+	 */
+	return apply_filters( 'bp_xprofile_get_hidden_field_types_for_user', $hidden_levels, $displayed_user_id, $current_user_id );
+}
+
+/**
+ * Fetch an array of the xprofile fields that a given user has marked with certain visibility levels.
+ *
+ * @since 1.6.0
+ *
+ * @see bp_xprofile_get_hidden_fields_for_user()
+ *
+ * @param int   $user_id The id of the profile owner.
+ * @param array $levels  An array of visibility levels ('public', 'friends', 'loggedin', 'adminsonly' etc) to be
+ *                       checked against.
+ * @return array $field_ids The fields that match the requested visibility levels for the given user.
+ */
+function bp_xprofile_get_fields_by_visibility_levels( $user_id, $levels = array() ) {
+	if ( !is_array( $levels ) ) {
+		$levels = (array)$levels;
+	}
+
+	$user_visibility_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
+
+	// Parse the user-provided visibility levels with the default levels, which may take
+	// precedence.
+	$default_visibility_levels = BP_XProfile_Group::fetch_default_visibility_levels();
+
+	foreach( (array) $default_visibility_levels as $d_field_id => $defaults ) {
+		// If the admin has forbidden custom visibility levels for this field, replace
+		// the user-provided setting with the default specified by the admin.
+		if ( isset( $defaults['allow_custom'] ) && isset( $defaults['default'] ) && 'disabled' == $defaults['allow_custom'] ) {
+			$user_visibility_levels[$d_field_id] = $defaults['default'];
+		}
+	}
+
+	$field_ids = array();
+	foreach( (array) $user_visibility_levels as $field_id => $field_visibility ) {
+		if ( in_array( $field_visibility, $levels ) ) {
+			$field_ids[] = $field_id;
+		}
+	}
+
+	// Never allow the fullname field to be excluded.
+	if ( in_array( 1, $field_ids ) ) {
+		$key = array_search( 1, $field_ids );
+		unset( $field_ids[$key] );
+	}
+
+	return $field_ids;
+}
+
+/**
+ * Formats datebox field values passed through a POST request.
+ *
+ * @since 2.8.0
+ *
+ * @param int $field_id The id of the current field being looped through.
+ * @return void This function only changes the global $_POST that should contain
+ *              the datebox data.
+ */
+function bp_xprofile_maybe_format_datebox_post_data( $field_id ) {
+	if ( ! isset( $_POST['field_' . $field_id] ) ) {
+		if ( ! empty( $_POST['field_' . $field_id . '_day'] ) && ! empty( $_POST['field_' . $field_id . '_month'] ) && ! empty( $_POST['field_' . $field_id . '_year'] ) ) {
+			// Concatenate the values.
+			$date_value = $_POST['field_' . $field_id . '_day'] . ' ' . $_POST['field_' . $field_id . '_month'] . ' ' . $_POST['field_' . $field_id . '_year'];
+
+			// Check that the concatenated value can be turned into a timestamp.
+			if ( $timestamp = strtotime( $date_value ) ) {
+				// Add the timestamp to the global $_POST that should contain the datebox data.
+				$_POST['field_' . $field_id] = date( 'Y-m-d H:i:s', $timestamp );
+			}
+		}
+	}
+}
+
+/**
+ * Finds and exports personal data associated with an email address from the XProfile tables.
+ *
+ * @since 4.0.0
+ *
+ * @param string $email_address  The userss email address.
+ * @return array An array of personal data.
+ */
+function bp_xprofile_personal_data_exporter( $email_address ) {
+	$email_address = trim( $email_address );
+
+	$data_to_export = array();
+
+	$user = get_user_by( 'email', $email_address );
+
+	if ( ! $user ) {
+		return array(
+			'data' => array(),
+			'done' => true,
+		);
+	}
+
+	$user_data_to_export = array();
+
+	$user_profile_data = BP_XProfile_ProfileData::get_all_for_user( $user->ID );
+	foreach ( $user_profile_data as $field_name => $field ) {
+		// Skip non-array fields, which don't belong to XProfile.
+		if ( ! is_array( $field ) ) {
+			continue;
+		}
+
+		// Re-pull the data so that BuddyPress formats and sanitizes properly.
+		$value = xprofile_get_field_data( $field['field_id'], $user->ID, 'comma' );
+		$user_data_to_export[] = array(
+			'name'  => $field_name,
+			'value' => $value,
+		);
+	}
+
+	$data_to_export[] = array(
+		'group_id'    => 'bp_xprofile',
+		'group_label' => __( 'Extended Profile Data', 'buddypress' ),
+		'item_id'     => "bp-xprofile-{$user->ID}",
+		'data'        => $user_data_to_export,
+	);
+
+	return array(
+		'data' => $data_to_export,
+		'done' => true,
+	);
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-loader.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-loader.php	(revision 12265)
@@ -0,0 +1,28 @@
+<?php
+/**
+ * BuddyPress XProfile Loader.
+ *
+ * An extended profile component for users. This allows site admins to create
+ * groups of fields for users to enter information about themselves.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the bp-xprofile component.
+ *
+ * @since 1.6.0
+ */
+function bp_setup_xprofile() {
+	$bp = buddypress();
+
+	if ( ! isset( $bp->profile->id ) ) {
+		$bp->profile = new BP_XProfile_Component();
+	}
+}
+add_action( 'bp_setup_components', 'bp_setup_xprofile', 2 );
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-notifications.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-notifications.php	(revision 12265)
@@ -0,0 +1,55 @@
+<?php
+/**
+ * BuddyPress XProfile Notifications.
+ *
+ * @package    BuddyPress
+ * @subpackage XProfileSettings
+ * @since 1.0.0
+ */
+
+/** Notifications *************************************************************/
+
+/**
+ * Format notifications for the extended profile (Xprofile) component.
+ *
+ * @since 2.4.0
+ *
+ * @param string $action            The kind of notification being rendered.
+ * @param int    $item_id           The primary item ID.
+ * @param int    $secondary_item_id The secondary item ID.
+ * @param int    $total_items       The total number of messaging-related notifications
+ *                                  waiting for the user.
+ * @param string $format            'string' for BuddyBar-compatible notifications; 'array'
+ *                                  for WP Toolbar. Default: 'string'.
+ * @return string
+ */
+function xprofile_format_notifications( $action, $item_id, $secondary_item_id, $total_items, $format = 'string' ) {
+	switch ( $action ) {
+		default:
+
+			/**
+			 * Allows plugins to filter extended profile-related custom notifications.
+			 * Notifications must have a 'component_name' of 'xprofile' to be routed
+			 * to this function.
+			 *
+			 * @since 2.4.0
+			 *
+			 * @param string $notification      Null value.
+			 * @param int    $item_id           The primary item ID.
+			 * @param int    $secondary_item_id The secondary item ID.
+			 * @param int    $total_items       The total number of messaging-related notifications
+			 *                                  waiting for the user.
+			 * @param string $format            'string' for BuddyBar-compatible notifications;
+			 *                                  'array' for WP Toolbar.
+			 */
+			$custom_action_notification = apply_filters( 'bp_xprofile_' . $action . '_notification', null, $item_id, $secondary_item_id, $total_items, $format );
+
+			if ( ! is_null( $custom_action_notification ) ) {
+				return $custom_action_notification;
+			}
+
+			break;
+	}
+
+	return false;
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-settings.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-settings.php	(revision 12265)
@@ -0,0 +1,59 @@
+<?php
+/**
+ * BuddyPress XProfile Settings.
+ *
+ * @package    BuddyPress
+ * @subpackage XProfileSettings
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Query all profile fields and their visibility data for display in settings.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args Array of args for the settings fields.
+ * @return bool
+ */
+function bp_xprofile_get_settings_fields( $args = '' ) {
+
+	// Parse the possible arguments.
+	$r = bp_parse_args( $args, array(
+		'user_id'                => bp_displayed_user_id(),
+		'profile_group_id'       => false,
+		'hide_empty_groups'      => false,
+		'hide_empty_fields'      => false,
+		'fetch_fields'           => true,
+		'fetch_field_data'       => false,
+		'fetch_visibility_level' => true,
+		'exclude_groups'         => false,
+		'exclude_fields'         => false
+	), 'xprofile_get_settings_fields' );
+
+	return bp_has_profile( $r );
+}
+
+/**
+ * Adds feedback messages when successfully saving profile field settings.
+ *
+ * @since 2.0.0
+ *
+ */
+function bp_xprofile_settings_add_feedback_message() {
+
+	// Default message type is success.
+	$type    = 'success';
+	$message = __( 'Your profile settings have been saved.',        'buddypress' );
+
+	// Community moderator editing another user's settings.
+	if ( ! bp_is_my_profile() && bp_core_can_edit_settings() ) {
+		$message = __( "This member's profile settings have been saved.", 'buddypress' );
+	}
+
+	// Add the message.
+	bp_core_add_message( $message, $type );
+}
+add_action( 'bp_xprofile_settings_after_save', 'bp_xprofile_settings_add_feedback_message' );
Index: /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/bp-xprofile-template.php	(revision 12265)
@@ -0,0 +1,1472 @@
+<?php
+/**
+ * BuddyPress XProfile Template Tags.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileTemplate
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Query for XProfile groups and fields.
+ *
+ * @since 1.0.0
+ *
+ * @global object $profile_template
+ * @see BP_XProfile_Group::get() for full description of `$args` array.
+ *
+ * @param array|string $args {
+ *     Array of arguments. See BP_XProfile_Group::get() for full description. Those arguments whose defaults differ
+ *     from that method are described here:
+ *     @type int          $user_id                Default: ID of the displayed user.
+ *     @type string|array $member_type            Default: 'any'.
+ *     @type int|bool     $profile_group_id       Default: false.
+ *     @type bool         $hide_empty_groups      Default: true.
+ *     @type bool         $hide_empty_fields      Defaults to true on the Dashboard, on a user's Edit Profile page,
+ *                                                or during registration. Otherwise false.
+ *     @type bool         $fetch_fields           Default: true.
+ *     @type bool         $fetch_field_data       Default: true.
+ *     @type bool         $fetch_visibility_level Defaults to true when an admin is viewing a profile, or when a user is
+ *                                                viewing her own profile, or during registration. Otherwise false.
+ *     @type int|bool     $exclude_groups         Default: false.
+ *     @type int|bool     $exclude_fields         Default: false
+ *     @type bool         $update_meta_cache      Default: true.
+ * }
+ *
+ * @return bool
+ */
+function bp_has_profile( $args = '' ) {
+	global $profile_template;
+
+	// Only show empty fields if we're on the Dashboard, or we're on a user's
+	// profile edit page, or this is a registration page.
+	$hide_empty_fields_default = ( ! is_network_admin() && ! is_admin() && ! bp_is_user_profile_edit() && ! bp_is_register_page() );
+
+	// We only need to fetch visibility levels when viewing your own profile.
+	if ( bp_is_my_profile() || bp_current_user_can( 'bp_moderate' ) || bp_is_register_page() ) {
+		$fetch_visibility_level_default = true;
+	} else {
+		$fetch_visibility_level_default = false;
+	}
+
+	// Parse arguments.
+	$r = bp_parse_args( $args, array(
+		'user_id'                => bp_displayed_user_id(),
+		'member_type'            => 'any',
+		'profile_group_id'       => false,
+		'hide_empty_groups'      => true,
+		'hide_empty_fields'      => $hide_empty_fields_default,
+		'fetch_fields'           => true,
+		'fetch_field_data'       => true,
+		'fetch_visibility_level' => $fetch_visibility_level_default,
+		'exclude_groups'         => false, // Comma-separated list of profile field group IDs to exclude.
+		'exclude_fields'         => false, // Comma-separated list of profile field IDs to exclude.
+		'update_meta_cache'      => true,
+	), 'has_profile' );
+
+	// Populate the template loop global.
+	$profile_template = new BP_XProfile_Data_Template( $r );
+
+	/**
+	 * Filters whether or not a group has a profile to display.
+	 *
+	 * @since 1.1.0
+	 * @since 2.6.0 Added the `$r` parameter.
+	 *
+	 * @param bool   $has_groups       Whether or not there are group profiles to display.
+	 * @param string $profile_template Current profile template being used.
+	 * @param array  $r                Array of arguments passed into the BP_XProfile_Data_Template class.
+	 */
+	return apply_filters( 'bp_has_profile', $profile_template->has_groups(), $profile_template, $r );
+}
+
+/**
+ * Start off the profile groups.
+ *
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_profile_groups() {
+	global $profile_template;
+	return $profile_template->profile_groups();
+}
+
+/**
+ * Set up the profile groups.
+ *
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_the_profile_group() {
+	global $profile_template;
+	return $profile_template->the_profile_group();
+}
+
+/**
+ * Whether or not the group has fields to display.
+ *
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_profile_group_has_fields() {
+	global $profile_template;
+	return $profile_template->has_fields();
+}
+
+/**
+ * Output the class attribute for a field.
+ *
+ * @since 1.0.0
+ *
+ * @param mixed $class Extra classes to append to class attribute.
+ *                     Pass mutiple class names as an array or
+ *                     space-delimited string.
+ */
+function bp_field_css_class( $class = false ) {
+	echo bp_get_field_css_class( $class );
+}
+
+	/**
+	 * Return the class attribute for a field.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param string|bool $class Extra classes to append to class attribute.
+	 * @return string
+	 */
+	function bp_get_field_css_class( $class = false ) {
+		global $profile_template;
+
+		$css_classes = array();
+
+		if ( ! empty( $class ) ) {
+			if ( ! is_array( $class ) ) {
+				$class = preg_split( '#\s+#', $class );
+			}
+			$css_classes = array_map( 'sanitize_html_class', $class );
+		}
+
+		// Set a class with the field ID.
+		$css_classes[] = 'field_' . $profile_template->field->id;
+
+		// Set a class with the field name (sanitized).
+		$css_classes[] = 'field_' . sanitize_title( $profile_template->field->name );
+
+		// Set a class indicating whether the field is required or optional.
+		if ( ! empty( $profile_template->field->is_required ) ) {
+			$css_classes[] = 'required-field';
+		} else {
+			$css_classes[] = 'optional-field';
+		}
+
+		// Add the field visibility level.
+		$css_classes[] = 'visibility-' . esc_attr( bp_get_the_profile_field_visibility_level() );
+
+		if ( $profile_template->current_field % 2 == 1 ) {
+			$css_classes[] = 'alt';
+		}
+
+		$css_classes[] = 'field_type_' . sanitize_title( $profile_template->field->type );
+
+		/**
+		 * Filters the field classes to be applied to a field.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param array $css_classes Array of classes to be applied to field. Passed by reference.
+		 */
+		$css_classes = apply_filters_ref_array( 'bp_field_css_classes', array( &$css_classes ) );
+
+		/**
+		 * Filters the class HTML attribute to be used on a field.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value class HTML attribute with imploded classes.
+		 */
+		return apply_filters( 'bp_get_field_css_class', ' class="' . implode( ' ', $css_classes ) . '"' );
+	}
+
+/**
+ * Whether or not the XProfile field has data to display.
+ *
+ * @since 1.0.0
+ *
+ * @global object $profile_template
+ *
+ * @return mixed
+ */
+function bp_field_has_data() {
+	global $profile_template;
+
+	/**
+	 * Filters whether or not the XProfile field has data to display.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param bool   $value            Whether or not there is data to display.
+	 * @param object $profile_template Profile template object.
+	 * @param string $value            Profile field being displayed.
+	 * @param string $value            Profile field ID being displayed.
+	 */
+	return apply_filters( 'bp_field_has_data', $profile_template->field_has_data, $profile_template, $profile_template->field, $profile_template->field->id );
+}
+
+/**
+ * Whether or not the XProfile field has public data to display.
+ *
+ * @since 1.0.0
+ *
+ * @global object $profile_template
+ *
+ * @return bool
+ */
+function bp_field_has_public_data() {
+	global $profile_template;
+
+	/**
+	 * Filters whether or not the XProfile field has public data to display.
+	 *
+	 * @since 2.8.0
+	 *
+	 * @param bool   $value            Whether or not there is public data to display.
+	 * @param object $profile_template Profile template object.
+	 * @param string $value            Profile field being displayed.
+	 * @param string $value            Profile field ID being displayed.
+	 */
+	return apply_filters( 'bp_field_has_public_data', ( ! empty( $profile_template->field_has_data ) ), $profile_template, $profile_template->field, $profile_template->field->id );
+}
+
+/**
+ * Output the XProfile group ID.
+ *
+ * @since 1.0.0
+ */
+function bp_the_profile_group_id() {
+	echo bp_get_the_profile_group_id();
+}
+
+	/**
+	 * Return the XProfile group ID.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return int
+	 */
+	function bp_get_the_profile_group_id() {
+		global $group;
+
+		/**
+		 * Filters the XProfile group ID.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $id ID for the profile group.
+		 */
+		return (int) apply_filters( 'bp_get_the_profile_group_id', $group->id );
+	}
+
+/**
+ * Output the XProfile group name.
+ *
+ * @since 1.0.0
+ */
+function bp_the_profile_group_name() {
+	echo bp_get_the_profile_group_name();
+}
+
+	/**
+	 * Return the XProfile group name.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_group_name() {
+		global $group;
+
+		/**
+		 * Filters the XProfile group name.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $name Name for the profile group.
+		 */
+		return apply_filters( 'bp_get_the_profile_group_name', $group->name );
+	}
+
+/**
+ * Output the XProfile group slug.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_group_slug() {
+	echo bp_get_the_profile_group_slug();
+}
+
+	/**
+	 * Return the XProfile group slug.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_group_slug() {
+		global $group;
+
+		/**
+		 * Filters the XProfile group slug.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Slug for the profile group.
+		 */
+		return apply_filters( 'bp_get_the_profile_group_slug', sanitize_title( $group->name ) );
+	}
+
+/**
+ * Output the XProfile group description.
+ *
+ * @since 1.0.0
+ */
+function bp_the_profile_group_description() {
+	echo bp_get_the_profile_group_description();
+}
+
+	/**
+	 * Return the XProfile group description.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_group_description() {
+		global $group;
+
+		/**
+		 * Filters the XProfile group description.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $description Description for the profile group.
+		 */
+		return apply_filters( 'bp_get_the_profile_group_description', $group->description );
+	}
+
+/**
+ * Output the XProfile group edit form action.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_group_edit_form_action() {
+	echo bp_get_the_profile_group_edit_form_action();
+}
+
+	/**
+	 * Return the XProfile group edit form action.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_group_edit_form_action() {
+		global $group;
+
+		// Build the form action URL.
+		$form_action = trailingslashit( bp_displayed_user_domain() . bp_get_profile_slug() . '/edit/group/' . $group->id );
+
+		/**
+		 * Filters the action for the XProfile group edit form.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value URL for the action attribute on the
+		 *                      profile group edit form.
+		 */
+		return apply_filters( 'bp_get_the_profile_group_edit_form_action', $form_action );
+	}
+
+/**
+ * Output the XProfile group field IDs.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_group_field_ids() {
+	echo bp_get_the_profile_group_field_ids();
+}
+
+	/**
+	 * Return the XProfile group field IDs.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_group_field_ids() {
+		global $group;
+
+		$field_ids = '';
+
+		if ( !empty( $group->fields ) ) {
+			foreach ( (array) $group->fields as $field ) {
+				$field_ids .= $field->id . ',';
+			}
+		}
+
+		return substr( $field_ids, 0, -1 );
+	}
+
+/**
+ * Output a comma-separated list of field IDs that are to be submitted on profile edit.
+ *
+ * @since 2.1.0
+ */
+function bp_the_profile_field_ids() {
+	echo bp_get_the_profile_field_ids();
+}
+	/**
+	 * Generate a comma-separated list of field IDs that are to be submitted on profile edit.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_ids() {
+		global $profile_template;
+
+		$field_ids = array();
+		foreach ( $profile_template->groups as $group ) {
+			if ( ! empty( $group->fields ) ) {
+				$field_ids = array_merge( $field_ids, wp_list_pluck( $group->fields, 'id' ) );
+			}
+		}
+
+		$field_ids = implode( ',', wp_parse_id_list( $field_ids ) );
+
+		/**
+		 * Filters the comma-separated list of field IDs.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param string $field_ids Comma-separated field IDs.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_ids', $field_ids );
+	}
+
+/**
+ * Return the XProfile fields.
+ *
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_profile_fields() {
+	global $profile_template;
+	return $profile_template->profile_fields();
+}
+
+/**
+ * Sets up the XProfile field.
+ *
+ * @since 1.0.0
+ *
+ * @return mixed
+ */
+function bp_the_profile_field() {
+	global $profile_template;
+	return $profile_template->the_profile_field();
+}
+
+/**
+ * Output the XProfile field ID.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_field_id() {
+	echo bp_get_the_profile_field_id();
+}
+
+	/**
+	 * Return the XProfile field ID.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return int
+	 */
+	function bp_get_the_profile_field_id() {
+		global $field;
+
+		/**
+		 * Filters the XProfile field ID.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $id ID for the profile field.
+		 */
+		return (int) apply_filters( 'bp_get_the_profile_field_id', $field->id );
+	}
+
+/**
+ * Outputs the XProfile field name.
+ *
+ * @since 1.0.0
+ */
+function bp_the_profile_field_name() {
+	echo bp_get_the_profile_field_name();
+}
+
+	/**
+	 * Returns the XProfile field name.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_name() {
+		global $field;
+
+		/**
+		 * Filters the XProfile field name.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $name Name for the profile field.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_name', $field->name );
+	}
+
+/**
+ * Outputs the XProfile field value.
+ *
+ * @since 1.0.0
+ */
+function bp_the_profile_field_value() {
+	echo bp_get_the_profile_field_value();
+}
+
+	/**
+	 * Returns the XProfile field value.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_value() {
+		global $field;
+
+		$field->data->value = bp_unserialize_profile_field( $field->data->value );
+
+		/**
+		 * Filters the XProfile field value.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $value Value for the profile field.
+		 * @param string $type  Type for the profile field.
+		 * @param int    $id    ID for the profile field.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_value', $field->data->value, $field->type, $field->id );
+	}
+
+/**
+ * Outputs the XProfile field edit value.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_field_edit_value() {
+	echo bp_get_the_profile_field_edit_value();
+}
+
+	/**
+	 * Returns the XProfile field edit value.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_edit_value() {
+		global $field;
+
+		// Make sure field data object exists
+		if ( ! isset( $field->data ) ) {
+			$field->data = new stdClass;
+		}
+
+		// Default to empty value
+		if ( ! isset( $field->data->value ) ) {
+			$field->data->value = '';
+		}
+
+		// Was a new value posted? If so, use it instead.
+		if ( isset( $_POST['field_' . $field->id] ) ) {
+
+			// This is sanitized via the filter below (based on the field type)
+			$field->data->value = $_POST['field_' . $field->id];
+		}
+
+		/**
+		 * Filters the XProfile field edit value.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $field_value Current field edit value.
+		 * @param string $type        Type for the profile field.
+		 * @param int    $id          ID for the profile field.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_edit_value', $field->data->value, $field->type, $field->id );
+	}
+
+/**
+ * Outputs the XProfile field type.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_field_type() {
+	echo bp_get_the_profile_field_type();
+}
+
+	/**
+	 * Returns the XProfile field type.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_type() {
+		global $field;
+
+		/**
+		 * Filters the XProfile field type.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $type Type for the profile field.
+		 */
+		return apply_filters( 'bp_the_profile_field_type', $field->type );
+	}
+
+/**
+ * Outputs the XProfile field description.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_field_description() {
+	echo bp_get_the_profile_field_description();
+}
+
+	/**
+	 * Returns the XProfile field description.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_description() {
+		global $field;
+
+		/**
+		 * Filters the XProfile field description.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $description Description for the profile field.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_description', $field->description );
+	}
+
+/**
+ * Outputs the XProfile field input name.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_field_input_name() {
+	echo bp_get_the_profile_field_input_name();
+}
+
+	/**
+	 * Retursn the XProfile field input name.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_input_name() {
+		global $field;
+
+		/**
+		 * Filters the profile field input name.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Value used for the name attribute on an input.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_input_name', 'field_' . $field->id );
+	}
+
+/**
+ * Returns the action name for any signup errors related to this profile field.
+ *
+ * In the registration templates, signup errors are pulled from the global
+ * object and rendered at actions that look like 'bp_field_12_errors'. This
+ * function allows the action name to be easily concatenated and called in the
+ * following fashion:
+ *   do_action( bp_get_the_profile_field_errors_action() );
+ *
+ * @since 1.8.0
+ *
+ * @return string The _errors action name corresponding to this profile field.
+ */
+function bp_get_the_profile_field_errors_action() {
+	global $field;
+	return 'bp_field_' . $field->id . '_errors';
+}
+
+/**
+ * Displays field options HTML for field types of 'selectbox', 'multiselectbox',
+ * 'radio', 'checkbox', and 'datebox'.
+ *
+ * @since 1.1.0
+ *
+ * @param array $args Specify type for datebox. Allowed 'day', 'month', 'year'.
+ */
+function bp_the_profile_field_options( $args = array() ) {
+	echo bp_get_the_profile_field_options( $args );
+}
+	/**
+	 * Retrieves field options HTML for field types of 'selectbox', 'multiselectbox', 'radio', 'checkbox', and 'datebox'.
+	 *
+	 * @since 1.1.0
+	 *
+	 *
+	 * @param array $args {
+	 *     Array of optional arguments.
+	 *     @type string|bool $type    Type of datebox. False if it's not a
+	 *                                datebox, otherwise 'day, 'month', or 'year'. Default: false.
+	 *     @type int         $user_id ID of the user whose profile values should be
+	 *                                used when rendering options. Default: displayed user.
+	 * }
+	 *
+	 * @return string $vaue Field options markup.
+	 */
+	function bp_get_the_profile_field_options( $args = array() ) {
+		global $field;
+
+		$args = bp_parse_args( $args, array(
+			'type'    => false,
+			'user_id' => bp_displayed_user_id(),
+		), 'get_the_profile_field_options' );
+
+		/**
+		 * In some cases, the $field global is not an instantiation of the BP_XProfile_Field class.
+		 * However, we have to make sure that all data originally in $field gets merged back in, after reinstantiation.
+		 */
+		if ( ! method_exists( $field, 'get_children' ) ) {
+			$field_obj = xprofile_get_field( $field->id );
+
+			foreach ( $field as $field_prop => $field_prop_value ) {
+				if ( ! isset( $field_obj->{$field_prop} ) ) {
+					$field_obj->{$field_prop} = $field_prop_value;
+				}
+			}
+
+			$field = $field_obj;
+		}
+
+		ob_start();
+		$field->type_obj->edit_field_options_html( $args );
+		$html = ob_get_contents();
+		ob_end_clean();
+
+		return $html;
+	}
+
+/**
+ * Render whether or not a profile field is required.
+ *
+ * @since 1.1.0
+ */
+function bp_the_profile_field_is_required() {
+	echo bp_get_the_profile_field_is_required();
+}
+
+	/**
+	 * Return whether or not a profile field is required.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return bool
+	 */
+	function bp_get_the_profile_field_is_required() {
+		global $field;
+
+		$retval = false;
+
+		if ( isset( $field->is_required ) ) {
+			$retval = $field->is_required;
+		}
+
+		/**
+		 * Filters whether or not a profile field is required.
+		 *
+		 * @since 1.1.0
+		 * @since 2.8.0 Added field ID.
+		 *
+		 * @param bool   $retval Whether or not the field is required.
+		 * @param string $value  Field ID that may be required.
+		 */
+		return (bool) apply_filters( 'bp_get_the_profile_field_is_required', $retval, $field->id );
+	}
+
+/**
+ * Output the visibility level of this field.
+ *
+ * @since 1.6.0
+ */
+function bp_the_profile_field_visibility_level() {
+	echo bp_get_the_profile_field_visibility_level();
+}
+
+	/**
+	 * Return the visibility level of this field.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_visibility_level() {
+		global $field;
+
+		// On the registration page, values stored in POST should take
+		// precedence over default visibility, so that submitted values
+		// are not lost on failure.
+		if ( bp_is_register_page() && ! empty( $_POST['field_' . $field->id . '_visibility'] ) ) {
+			$retval = esc_attr( $_POST['field_' . $field->id . '_visibility'] );
+		} else {
+			$retval = ! empty( $field->visibility_level ) ? $field->visibility_level : 'public';
+		}
+
+		/**
+		 * Filters the profile field visibility level.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $retval Field visibility level.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_visibility_level', $retval );
+	}
+
+/**
+ * Echo the visibility level label of this field.
+ *
+ * @since 1.6.0
+ */
+function bp_the_profile_field_visibility_level_label() {
+	echo bp_get_the_profile_field_visibility_level_label();
+}
+
+	/**
+	 * Return the visibility level label of this field.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return string
+	 */
+	function bp_get_the_profile_field_visibility_level_label() {
+		global $field;
+
+		// On the registration page, values stored in POST should take
+		// precedence over default visibility, so that submitted values
+		// are not lost on failure.
+		if ( bp_is_register_page() && ! empty( $_POST['field_' . $field->id . '_visibility'] ) ) {
+			$level = esc_html( $_POST['field_' . $field->id . '_visibility'] );
+		} else {
+			$level = ! empty( $field->visibility_level ) ? $field->visibility_level : 'public';
+		}
+
+		$fields = bp_xprofile_get_visibility_levels();
+
+		/**
+		 * Filters the profile field visibility level label.
+		 *
+		 * @since 1.6.0
+		 * @since 2.6.0 Added the `$level` parameter.
+		 *
+		 * @param string $retval Field visibility level label.
+		 * @param string $level  Field visibility level.
+		 */
+		return apply_filters( 'bp_get_the_profile_field_visibility_level_label', $fields[ $level ]['label'], $level );
+	}
+
+/**
+ * Return unserialized profile field data, and combine any array items into a
+ * comma-separated string.
+ *
+ * @since 1.0.0
+ *
+ * @param string $value Content to maybe unserialize.
+ * @return string
+ */
+function bp_unserialize_profile_field( $value ) {
+	if ( is_serialized($value) ) {
+		$field_value = @unserialize($value);
+		$field_value = implode( ', ', $field_value );
+		return $field_value;
+	}
+
+	return $value;
+}
+
+/**
+ * Output XProfile field data.
+ *
+ * @since 1.2.0
+ *
+ * @param string|array $args Array of arguments for field data. See {@link bp_get_profile_field_data}
+ */
+function bp_profile_field_data( $args = '' ) {
+	echo bp_get_profile_field_data( $args );
+}
+
+	/**
+	 * Return XProfile field data.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param string|array $args {
+	 *    Array of arguments for field data.
+	 *
+	 *    @type string|int|bool $field   Field identifier.
+	 *    @type int             $user_id ID of the user to get field data for.
+	 * }
+	 * @return mixed
+	 */
+	function bp_get_profile_field_data( $args = '' ) {
+
+		$r = wp_parse_args( $args, array(
+			'field'   => false, // Field name or ID.
+			'user_id' => bp_displayed_user_id()
+		) );
+
+		/**
+		 * Filters the profile field data.
+		 *
+		 * @since 1.2.0
+		 * @since 2.6.0 Added the `$r` parameter.
+		 *
+		 * @param mixed $value Profile data for a specific field for the user.
+		 * @param array $r     Array of parsed arguments.
+		 */
+		return apply_filters( 'bp_get_profile_field_data', xprofile_get_field_data( $r['field'], $r['user_id'] ), $r );
+	}
+
+/**
+ * Get all profile field groups.
+ *
+ * @since 2.1.0
+ *
+ * @return array $groups
+ */
+function bp_profile_get_field_groups() {
+
+	$groups = wp_cache_get( 'all', 'bp_xprofile_groups' );
+	if ( false === $groups ) {
+		$groups = bp_xprofile_get_groups( array( 'fetch_fields' => true ) );
+		wp_cache_set( 'all', $groups, 'bp_xprofile_groups' );
+	}
+
+	/**
+	 * Filters all profile field groups.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $groups Array of available profile field groups.
+	 */
+	return apply_filters( 'bp_profile_get_field_groups', $groups );
+}
+
+/**
+ * Check if there is more than one group of fields for the profile being edited.
+ *
+ * @since 2.1.0
+ *
+ * @return bool True if there is more than one profile field group.
+ */
+function bp_profile_has_multiple_groups() {
+	$has_multiple_groups = count( (array) bp_profile_get_field_groups() ) > 1;
+
+	/**
+	 * Filters if there is more than one group of fields for the profile being edited.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param bool $has_multiple_groups Whether or not there are multiple groups.
+	 */
+	return (bool) apply_filters( 'bp_profile_has_multiple_groups', $has_multiple_groups );
+}
+
+/**
+ * Output the tabs to switch between profile field groups.
+ *
+ * @since 1.0.0
+ */
+function bp_profile_group_tabs() {
+	echo bp_get_profile_group_tabs();
+
+	/**
+	 * Fires at the end of the tab output for switching between profile field
+	 * groups. This action is in a strange place for legacy reasons.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'xprofile_profile_group_tabs' );
+}
+
+/**
+ * Return the XProfile group tabs.
+ *
+ * @since 2.3.0
+ *
+ * @return string
+ */
+function bp_get_profile_group_tabs() {
+
+	// Get field group data.
+	$groups     = bp_profile_get_field_groups();
+	$group_name = bp_get_profile_group_name();
+	$tabs       = array();
+
+	// Loop through field groups and put a tab-lst together.
+	for ( $i = 0, $count = count( $groups ); $i < $count; ++$i ) {
+
+		// Setup the selected class.
+		$selected = '';
+		if ( $group_name === $groups[ $i ]->name ) {
+			$selected = ' class="current"';
+		}
+
+		// Skip if group has no fields.
+		if ( empty( $groups[ $i ]->fields ) ) {
+			continue;
+		}
+
+		// Build the profile field group link.
+		$link   = trailingslashit( bp_displayed_user_domain() . bp_get_profile_slug() . '/edit/group/' . $groups[ $i ]->id );
+
+		// Add tab to end of tabs array.
+		$tabs[] = sprintf(
+			'<li %1$s><a href="%2$s">%3$s</a></li>',
+			$selected,
+			esc_url( $link ),
+			esc_html( apply_filters( 'bp_get_the_profile_group_name', $groups[ $i ]->name ) )
+		);
+	}
+
+	/**
+	 * Filters the tabs to display for profile field groups.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array  $tabs       Array of tabs to display.
+	 * @param array  $groups     Array of profile groups.
+	 * @param string $group_name Name of the current group displayed.
+	 */
+	$tabs = apply_filters( 'xprofile_filter_profile_group_tabs', $tabs, $groups, $group_name );
+
+	return join( '', $tabs );
+}
+
+/**
+ * Output the XProfile group name.
+ *
+ * @since 1.0.0
+ *
+ * @param bool $deprecated Deprecated boolean parameter.
+ *
+ * @return string|null
+ */
+function bp_profile_group_name( $deprecated = true ) {
+	if ( ! $deprecated ) {
+		return bp_get_profile_group_name();
+	} else {
+		echo bp_get_profile_group_name();
+	}
+}
+
+	/**
+	 * Return the XProfile group name.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string
+	 */
+	function bp_get_profile_group_name() {
+
+		// Check action variable.
+		$group_id = bp_action_variable( 1 );
+		if ( empty( $group_id ) || ! is_numeric( $group_id ) ) {
+			$group_id = 1;
+		}
+
+		// Check for cached group.
+		$group = new BP_XProfile_Group( $group_id );
+
+		/**
+		 * Filters the profile group name.
+		 *
+		 * @since 1.0.0
+		 * @since 2.6.0 Added the `$group_id` parameter
+		 *
+		 * @param string $name     Name of the profile group.
+		 * @param int    $group_id ID of the profile group.
+		 */
+		return apply_filters( 'bp_get_profile_group_name', $group->name, $group_id );
+	}
+
+/**
+ * Render a formatted string displaying when a profile was last updated.
+ *
+ * @since 1.0.0
+ */
+function bp_profile_last_updated() {
+
+	$last_updated = bp_get_profile_last_updated();
+
+	if ( empty( $last_updated ) ) {
+		_e( 'Profile not recently updated.', 'buddypress' );
+	} else {
+		echo $last_updated;
+	}
+}
+
+	/**
+	 * Return a formatted string displaying when a profile was last updated.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool|string
+	 */
+	function bp_get_profile_last_updated() {
+
+		$last_updated = bp_get_user_meta( bp_displayed_user_id(), 'profile_last_updated', true );
+
+		if ( ! empty( $last_updated ) ) {
+
+			/**
+			 * Filters the formatted string used to display when a profile was last updated.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param string $value Formatted last updated indicator string.
+			 */
+			return apply_filters( 'bp_get_profile_last_updated', sprintf( __( 'Profile updated %s', 'buddypress' ), bp_core_time_since( strtotime( $last_updated ) ) ) );
+		}
+
+		return false;
+	}
+
+/**
+ * Display the current profile group ID.
+ *
+ * @since 1.1.0
+ */
+function bp_current_profile_group_id() {
+	echo bp_get_current_profile_group_id();
+}
+
+	/**
+	 * Return the current profile group ID.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return int
+	 */
+	function bp_get_current_profile_group_id() {
+		$profile_group_id = bp_action_variable( 1 );
+		if ( empty( $profile_group_id ) ) {
+			$profile_group_id = 1;
+		}
+
+		/**
+		 * Filters the current profile group ID.
+		 *
+		 * Possible values are admin/profile/edit/[group-id].
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $profile_group_id Current profile group ID.
+		 */
+		return (int) apply_filters( 'bp_get_current_profile_group_id', $profile_group_id );
+	}
+
+/**
+ * Render an avatar delete link.
+ *
+ * @since 1.1.0
+ */
+function bp_avatar_delete_link() {
+	echo bp_get_avatar_delete_link();
+}
+
+	/**
+	 * Return an avatar delete link.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return string
+	 */
+	function bp_get_avatar_delete_link() {
+
+		/**
+		 * Filters the link used for deleting an avatar.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $value Nonced URL used for deleting an avatar.
+		 */
+		return apply_filters( 'bp_get_avatar_delete_link', wp_nonce_url( bp_displayed_user_domain() . bp_get_profile_slug() . '/change-avatar/delete-avatar/', 'bp_delete_avatar_link' ) );
+	}
+
+/**
+ * Render an edit profile button.
+ *
+ * @since 1.0.0
+ */
+function bp_edit_profile_button() {
+	bp_button( array(
+		'id'                => 'edit_profile',
+		'component'         => 'xprofile',
+		'must_be_logged_in' => true,
+		'block_self'        => true,
+		'link_href'         => trailingslashit( bp_displayed_user_domain() . bp_get_profile_slug() . '/edit' ),
+		'link_class'        => 'edit',
+		'link_text'         => __( 'Edit Profile', 'buddypress' ),
+	) );
+}
+
+/** Visibility ****************************************************************/
+
+/**
+ * Echo the field visibility radio buttons.
+ *
+ * @since 1.6.0
+ *
+ * @param array|string $args Args for the radio buttons. See {@link bp_profile_get_visibility_radio_buttons}
+ */
+function bp_profile_visibility_radio_buttons( $args = '' ) {
+	echo bp_profile_get_visibility_radio_buttons( $args );
+}
+	/**
+	 * Return the field visibility radio buttons.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param array|string $args {
+	 *    Args for the radio buttons.
+	 *
+	 *    @type int    $field_id     ID of the field to render.
+	 *    @type string $before       Markup to render before the field.
+	 *    @type string $after        Markup to render after the field.
+	 *    @type string $before_radio Markup to render before the radio button.
+	 *    @type string $after_radio  Markup to render after the radio button.
+	 *    @type string $class        Class to apply to the field markup.
+	 * }
+	 * @return string $retval
+	 */
+	function bp_profile_get_visibility_radio_buttons( $args = '' ) {
+
+		// Parse optional arguments.
+		$r = bp_parse_args( $args, array(
+			'field_id'     => bp_get_the_profile_field_id(),
+			'before'       => '<div class="radio">',
+			'after'        => '</div>',
+			'before_radio' => '',
+			'after_radio'  => '',
+			'class'        => 'bp-xprofile-visibility'
+		), 'xprofile_visibility_radio_buttons' );
+
+		// Empty return value, filled in below if a valid field ID is found.
+		$retval = '';
+
+		// Only do-the-do if there's a valid field ID.
+		if ( ! empty( $r['field_id'] ) ) :
+
+			// Start the output buffer.
+			ob_start();
+
+			// Output anything before.
+			echo $r['before']; ?>
+
+			<?php if ( bp_current_user_can( 'bp_xprofile_change_field_visibility' ) ) : ?>
+
+				<?php foreach( bp_xprofile_get_visibility_levels() as $level ) : ?>
+
+					<?php printf( $r['before_radio'], esc_attr( $level['id'] ) ); ?>
+
+					<label for="<?php echo esc_attr( 'see-field_' . $r['field_id'] . '_' . $level['id'] ); ?>">
+						<input type="radio" id="<?php echo esc_attr( 'see-field_' . $r['field_id'] . '_' . $level['id'] ); ?>" name="<?php echo esc_attr( 'field_' . $r['field_id'] . '_visibility' ); ?>" value="<?php echo esc_attr( $level['id'] ); ?>" <?php checked( $level['id'], bp_get_the_profile_field_visibility_level() ); ?> />
+						<span class="field-visibility-text"><?php echo esc_html( $level['label'] ); ?></span>
+					</label>
+
+					<?php echo $r['after_radio']; ?>
+
+				<?php endforeach; ?>
+
+			<?php endif;
+
+			// Output anything after.
+			echo $r['after'];
+
+			// Get the output buffer and empty it.
+			$retval = ob_get_clean();
+		endif;
+
+		/**
+		 * Filters the radio buttons for setting visibility.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param string $retval HTML output for the visibility radio buttons.
+		 * @param array  $r      Parsed arguments to be used with display.
+		 * @param array  $args   Original passed in arguments to be used with display.
+		 */
+		return apply_filters( 'bp_profile_get_visibility_radio_buttons', $retval, $r, $args );
+	}
+
+/**
+ * Output the XProfile field visibility select list for settings.
+ *
+ * @since 2.0.0
+ *
+ * @param array|string $args Args for the select list. See {@link bp_profile_get_settings_visibility_select}
+ */
+function bp_profile_settings_visibility_select( $args = '' ) {
+	echo bp_profile_get_settings_visibility_select( $args );
+}
+	/**
+	 * Return the XProfile field visibility select list for settings.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array|string $args {
+	 *    Args for the select list.
+	 *
+	 *    @type int    $field_id ID of the field to render.
+	 *    @type string $before   Markup to render before the field.
+	 *    @type string $before_controls  markup before form controls.
+	 *    @type string $after    Markup to render after the field.
+	 *    @type string $after_controls Markup after the form controls.
+	 *    @type string $class    Class to apply to the field markup.
+	 *    @type string $label_class Class to apply for the label element.
+	 *    @type string $notoggle_tag Markup element to use for notoggle tag.
+	 *    @type string $notoggle_class Class to apply to the notoggle element.
+	 * }
+	 * @return string $retval
+	 */
+	function bp_profile_get_settings_visibility_select( $args = '' ) {
+
+		// Parse optional arguments.
+		$r = bp_parse_args( $args, array(
+			'field_id'         => bp_get_the_profile_field_id(),
+			'before'           => '',
+			'before_controls'  => '',
+			'after'            => '',
+			'after_controls'   => '',
+			'class'            => 'bp-xprofile-visibility',
+			'label_class'      => 'bp-screen-reader-text',
+			'notoggle_tag'     => 'span',
+			'notoggle_class'   => 'field-visibility-settings-notoggle',
+		), 'xprofile_settings_visibility_select' );
+
+		// Empty return value, filled in below if a valid field ID is found.
+		$retval = '';
+
+		// Only do-the-do if there's a valid field ID.
+		if ( ! empty( $r['field_id'] ) ) :
+
+			// Start the output buffer.
+			ob_start();
+
+			// Output anything before.
+			echo $r['before']; ?>
+
+			<?php if ( bp_current_user_can( 'bp_xprofile_change_field_visibility' ) ) : ?>
+
+			<?php echo $r['before_controls']; ?>
+
+				<label for="<?php echo esc_attr( 'field_' . $r['field_id'] ) ; ?>_visibility" class="<?php echo esc_attr( $r['label_class'] ); ?>"><?php
+					/* translators: accessibility text */
+					_e( 'Select visibility', 'buddypress' );
+				?></label>
+				<select class="<?php echo esc_attr( $r['class'] ); ?>" name="<?php echo esc_attr( 'field_' . $r['field_id'] ) ; ?>_visibility" id="<?php echo esc_attr( 'field_' . $r['field_id'] ) ; ?>_visibility">
+
+					<?php foreach ( bp_xprofile_get_visibility_levels() as $level ) : ?>
+
+						<option value="<?php echo esc_attr( $level['id'] ); ?>" <?php selected( $level['id'], bp_get_the_profile_field_visibility_level() ); ?>><?php echo esc_html( $level['label'] ); ?></option>
+
+					<?php endforeach; ?>
+
+				</select>
+
+			<?php echo $r['after_controls']; ?>
+
+			<?php else : ?>
+
+				<<?php echo esc_html( $r['notoggle_tag'] ); ?> class="<?php echo esc_attr( $r['notoggle_class'] ); ?>"><?php bp_the_profile_field_visibility_level_label(); ?></<?php echo esc_html( $r['notoggle_tag'] ); ?>>
+
+			<?php endif;
+
+			// Output anything after.
+			echo $r['after'];
+
+			// Get the output buffer and empty it.
+			$retval = ob_get_clean();
+		endif;
+
+		/**
+		 * Filters the dropdown list for setting visibility.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string $retval HTML output for the visibility dropdown list.
+		 * @param array  $r      Parsed arguments to be used with display.
+		 * @param array  $args   Original passed in arguments to be used with display.
+		 */
+		return apply_filters( 'bp_profile_settings_visibility_select', $retval, $r, $args );
+	}
+
+/**
+ * Output the 'required' markup in extended profile field labels.
+ *
+ * @since 2.4.0
+ */
+function bp_the_profile_field_required_label() {
+	echo bp_get_the_profile_field_required_label();
+}
+
+	/**
+	 * Return the 'required' markup in extended profile field labels.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @return string HTML for the required label.
+	 */
+	function bp_get_the_profile_field_required_label() {
+		$retval = '';
+
+		if ( bp_get_the_profile_field_is_required() ) {
+			$translated_string = __( '(required)', 'buddypress' );
+
+			$retval = ' <span class="bp-required-field-label">';
+			$retval .= apply_filters( 'bp_get_the_profile_field_required_label', $translated_string, bp_get_the_profile_field_id() );
+			$retval .= '</span>';
+
+		}
+
+		return $retval;
+	}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-component.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-component.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-component.php	(revision 12265)
@@ -0,0 +1,492 @@
+<?php
+/**
+ * BuddyPress XProfile Loader.
+ *
+ * An extended profile component for users. This allows site admins to create
+ * groups of fields for users to enter information about themselves.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileLoader
+ * @since 1.5.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Creates our XProfile component.
+ *
+ * @since 1.5.0
+ */
+class BP_XProfile_Component extends BP_Component {
+
+	/**
+	 * Profile field types.
+	 *
+	 * @since 1.5.0
+	 * @var array
+	 */
+	public $field_types;
+
+	/**
+	 * The acceptable visibility levels for xprofile fields.
+	 *
+	 * @see bp_xprofile_get_visibility_levels()
+	 *
+	 * @since 1.6.0
+	 * @var array
+	 */
+	public $visibility_levels = array();
+
+	/**
+	 * Start the xprofile component creation process.
+	 *
+	 * @since 1.5.0
+	 */
+	public function __construct() {
+		parent::start(
+			'xprofile',
+			_x( 'Extended Profiles', 'Component page <title>', 'buddypress' ),
+			buddypress()->plugin_dir,
+			array(
+				'adminbar_myaccount_order' => 20
+			)
+		);
+
+		$this->setup_hooks();
+	}
+
+	/**
+	 * Include files.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $includes Array of files to include.
+	 */
+	public function includes( $includes = array() ) {
+		$includes = array(
+			'cssjs',
+			'cache',
+			'caps',
+			'filters',
+			'template',
+			'functions',
+		);
+
+		// Conditional includes.
+		if ( bp_is_active( 'activity' ) ) {
+			$includes[] = 'activity';
+		}
+		if ( bp_is_active( 'notifications' ) ) {
+			$includes[] = 'notifications';
+		}
+		if ( bp_is_active( 'settings' ) ) {
+			$includes[] = 'settings';
+		}
+		if ( is_admin() ) {
+			$includes[] = 'admin';
+		}
+
+		parent::includes( $includes );
+	}
+
+	/**
+	 * Late includes method.
+	 *
+	 * Only load up certain code when on specific pages.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// Bail if PHPUnit is running.
+		if ( defined( 'BP_TESTS_DIR' ) ) {
+			return;
+		}
+
+		// Bail if not on a user page.
+		if ( ! bp_is_user() ) {
+			return;
+		}
+
+		// User nav.
+		if ( bp_is_profile_component() ) {
+			require $this->path . 'bp-xprofile/screens/public.php';
+
+			// Action - Delete avatar.
+			if ( is_user_logged_in()&& bp_is_user_change_avatar() && bp_is_action_variable( 'delete-avatar', 0 ) ) {
+				require $this->path . 'bp-xprofile/actions/delete-avatar.php';
+			}
+
+			// Sub-nav items.
+			if ( is_user_logged_in() &&
+				in_array( bp_current_action(), array( 'edit', 'change-avatar', 'change-cover-image' ), true )
+			) {
+				require $this->path . 'bp-xprofile/screens/' . bp_current_action() . '.php';
+			}
+		}
+
+		// Settings.
+		if ( is_user_logged_in() && bp_is_user_settings_profile() ) {
+			require $this->path . 'bp-xprofile/screens/settings-profile.php';
+		}
+	}
+
+	/**
+	 * Setup globals.
+	 *
+	 * The BP_XPROFILE_SLUG constant is deprecated, and only used here for
+	 * backwards compatibility.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $args Array of globals to set up.
+	 */
+	public function setup_globals( $args = array() ) {
+		$bp = buddypress();
+
+		// Define a slug, if necessary.
+		if ( !defined( 'BP_XPROFILE_SLUG' ) ) {
+			define( 'BP_XPROFILE_SLUG', 'profile' );
+		}
+
+		// Assign the base group and fullname field names to constants
+		// to use in SQL statements.
+		// Defined conditionally to accommodate unit tests.
+		if ( ! defined( 'BP_XPROFILE_BASE_GROUP_NAME' ) ) {
+			define( 'BP_XPROFILE_BASE_GROUP_NAME', stripslashes( bp_core_get_root_option( 'avatar_default' ) ) );
+		}
+
+		if ( ! defined( 'BP_XPROFILE_FULLNAME_FIELD_NAME' ) ) {
+			define( 'BP_XPROFILE_FULLNAME_FIELD_NAME', stripslashes( bp_core_get_root_option( 'bp-xprofile-fullname-field-name' ) ) );
+		}
+
+		/**
+		 * Filters the supported field type IDs.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param array $value Array of IDs for the supported field types.
+		 */
+		$this->field_types = apply_filters( 'xprofile_field_types', array_keys( bp_xprofile_get_field_types() ) );
+
+		// 'option' is a special case. It is not a top-level field, so
+		// does not have an associated BP_XProfile_Field_Type class,
+		// but it must be whitelisted.
+		$this->field_types[] = 'option';
+
+		// Register the visibility levels. See bp_xprofile_get_visibility_levels() to filter.
+		$this->visibility_levels = array(
+			'public' => array(
+				'id'	  => 'public',
+				'label' => _x( 'Everyone', 'Visibility level setting', 'buddypress' )
+			),
+			'adminsonly' => array(
+				'id'	  => 'adminsonly',
+				'label' => _x( 'Only Me', 'Visibility level setting', 'buddypress' )
+			),
+			'loggedin' => array(
+				'id'	  => 'loggedin',
+				'label' => _x( 'All Members', 'Visibility level setting', 'buddypress' )
+			)
+		);
+
+		if ( bp_is_active( 'friends' ) ) {
+			$this->visibility_levels['friends'] = array(
+				'id'	=> 'friends',
+				'label'	=> _x( 'My Friends', 'Visibility level setting', 'buddypress' )
+			);
+		}
+
+		// Tables.
+		$global_tables = array(
+			'table_name_data'   => $bp->table_prefix . 'bp_xprofile_data',
+			'table_name_groups' => $bp->table_prefix . 'bp_xprofile_groups',
+			'table_name_fields' => $bp->table_prefix . 'bp_xprofile_fields',
+			'table_name_meta'   => $bp->table_prefix . 'bp_xprofile_meta',
+		);
+
+		$meta_tables = array(
+			'xprofile_group' => $bp->table_prefix . 'bp_xprofile_meta',
+			'xprofile_field' => $bp->table_prefix . 'bp_xprofile_meta',
+			'xprofile_data'  => $bp->table_prefix . 'bp_xprofile_meta',
+		);
+
+		$globals = array(
+			'slug'                  => BP_XPROFILE_SLUG,
+			'has_directory'         => false,
+			'notification_callback' => 'xprofile_format_notifications',
+			'global_tables'         => $global_tables,
+			'meta_tables'           => $meta_tables,
+		);
+
+		parent::setup_globals( $globals );
+	}
+
+	/**
+	 * Set up navigation.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global BuddyPress $bp The one true BuddyPress instance
+	 *
+	 * @param array $main_nav Array of main nav items to set up.
+	 * @param array $sub_nav  Array of sub nav items to set up.
+	 */
+	public function setup_nav( $main_nav = array(), $sub_nav = array() ) {
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		$access       = bp_core_can_edit_settings();
+		$slug         = bp_get_profile_slug();
+		$profile_link = trailingslashit( $user_domain . $slug );
+
+		// Add 'Profile' to the main navigation.
+		$main_nav = array(
+			'name'                => _x( 'Profile', 'Profile header menu', 'buddypress' ),
+			'slug'                => $slug,
+			'position'            => 20,
+			'screen_function'     => 'xprofile_screen_display_profile',
+			'default_subnav_slug' => 'public',
+			'item_css_id'         => $this->id
+		);
+
+		// Add the subnav items to the profile.
+		$sub_nav[] = array(
+			'name'            => _x( 'View', 'Profile header sub menu', 'buddypress' ),
+			'slug'            => 'public',
+			'parent_url'      => $profile_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'xprofile_screen_display_profile',
+			'position'        => 10
+		);
+
+		// Edit Profile.
+		$sub_nav[] = array(
+			'name'            => _x( 'Edit','Profile header sub menu', 'buddypress' ),
+			'slug'            => 'edit',
+			'parent_url'      => $profile_link,
+			'parent_slug'     => $slug,
+			'screen_function' => 'xprofile_screen_edit_profile',
+			'position'        => 20,
+			'user_has_access' => $access
+		);
+
+		// Change Avatar.
+		if ( buddypress()->avatar->show_avatars ) {
+			$sub_nav[] = array(
+				'name'            => _x( 'Change Profile Photo', 'Profile header sub menu', 'buddypress' ),
+				'slug'            => 'change-avatar',
+				'parent_url'      => $profile_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'xprofile_screen_change_avatar',
+				'position'        => 30,
+				'user_has_access' => $access
+			);
+		}
+
+		// Change Cover image.
+		if ( bp_displayed_user_use_cover_image_header() ) {
+			$sub_nav[] = array(
+				'name'            => _x( 'Change Cover Image', 'Profile header sub menu', 'buddypress' ),
+				'slug'            => 'change-cover-image',
+				'parent_url'      => $profile_link,
+				'parent_slug'     => $slug,
+				'screen_function' => 'xprofile_screen_change_cover_image',
+				'position'        => 40,
+				'user_has_access' => $access
+			);
+		}
+
+		// The Settings > Profile nav item can only be set up after
+		// the Settings component has run its own nav routine.
+		add_action( 'bp_settings_setup_nav', array( $this, 'setup_settings_nav' ) );
+
+		parent::setup_nav( $main_nav, $sub_nav );
+	}
+
+	/**
+	 * Set up the Settings > Profile nav item.
+	 *
+	 * Loaded in a separate method because the Settings component may not
+	 * be loaded in time for BP_XProfile_Component::setup_nav().
+	 *
+	 * @since 2.1.0
+	 */
+	public function setup_settings_nav() {
+		if ( ! bp_is_active( 'settings' ) ) {
+			return;
+		}
+
+		// Determine user to use.
+		if ( bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} elseif ( bp_loggedin_user_domain() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} else {
+			return;
+		}
+
+		// Get the settings slug.
+		$settings_slug = bp_get_settings_slug();
+
+		bp_core_new_subnav_item( array(
+			'name'            => _x( 'Profile Visibility', 'Profile settings sub nav', 'buddypress' ),
+			'slug'            => 'profile',
+			'parent_url'      => trailingslashit( $user_domain . $settings_slug ),
+			'parent_slug'     => $settings_slug,
+			'screen_function' => 'bp_xprofile_screen_settings',
+			'position'        => 30,
+			'user_has_access' => bp_core_can_edit_settings()
+		), 'members' );
+	}
+
+	/**
+	 * Set up the Admin Bar.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param array $wp_admin_nav Admin Bar items.
+	 */
+	public function setup_admin_bar( $wp_admin_nav = array() ) {
+
+		// Menus for logged in user.
+		if ( is_user_logged_in() ) {
+
+			// Profile link.
+			$profile_link = trailingslashit( bp_loggedin_user_domain() . bp_get_profile_slug() );
+
+			// Add the "Profile" sub menu.
+			$wp_admin_nav[] = array(
+				'parent' => buddypress()->my_account_menu_id,
+				'id'     => 'my-account-' . $this->id,
+				'title'  => _x( 'Profile', 'My Account Profile', 'buddypress' ),
+				'href'   => $profile_link
+			);
+
+			// View Profile.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-public',
+				'title'    => _x( 'View', 'My Account Profile sub nav', 'buddypress' ),
+				'href'     => $profile_link,
+				'position' => 10
+			);
+
+			// Edit Profile.
+			$wp_admin_nav[] = array(
+				'parent'   => 'my-account-' . $this->id,
+				'id'       => 'my-account-' . $this->id . '-edit',
+				'title'    => _x( 'Edit', 'My Account Profile sub nav', 'buddypress' ),
+				'href'     => trailingslashit( $profile_link . 'edit' ),
+				'position' => 20
+			);
+
+			// Edit Avatar.
+			if ( buddypress()->avatar->show_avatars ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-change-avatar',
+					'title'    => _x( 'Change Profile Photo', 'My Account Profile sub nav', 'buddypress' ),
+					'href'     => trailingslashit( $profile_link . 'change-avatar' ),
+					'position' => 30
+				);
+			}
+
+			if ( bp_displayed_user_use_cover_image_header() ) {
+				$wp_admin_nav[] = array(
+					'parent'   => 'my-account-' . $this->id,
+					'id'       => 'my-account-' . $this->id . '-change-cover-image',
+					'title'    => _x( 'Change Cover Image', 'My Account Profile sub nav', 'buddypress' ),
+					'href'     => trailingslashit( $profile_link . 'change-cover-image' ),
+					'position' => 40
+				);
+			}
+		}
+
+		parent::setup_admin_bar( $wp_admin_nav );
+	}
+
+	/**
+	 * Add custom hooks.
+	 *
+	 * @since 2.0.0
+	 */
+	public function setup_hooks() {
+		add_filter( 'bp_settings_admin_nav', array( $this, 'setup_settings_admin_nav' ), 2 );
+	}
+
+	/**
+	 * Sets up the title for pages and <title>.
+	 *
+	 * @since 1.5.0
+	 */
+	public function setup_title() {
+
+		if ( bp_is_profile_component() ) {
+			$bp = buddypress();
+
+			if ( bp_is_my_profile() ) {
+				$bp->bp_options_title = _x( 'My Profile', 'Page title', 'buddypress' );
+			} else {
+				$bp->bp_options_avatar = bp_core_fetch_avatar( array(
+					'item_id' => bp_displayed_user_id(),
+					'type'    => 'thumb',
+					'alt'	  => sprintf( _x( 'Profile picture of %s', 'Avatar alt', 'buddypress' ), bp_get_displayed_user_fullname() )
+				) );
+				$bp->bp_options_title = bp_get_displayed_user_fullname();
+			}
+		}
+
+		parent::setup_title();
+	}
+
+	/**
+	 * Setup cache groups.
+	 *
+	 * @since 2.2.0
+	 */
+	public function setup_cache_groups() {
+
+		// Global groups.
+		wp_cache_add_global_groups( array(
+			'bp_xprofile',
+			'bp_xprofile_data',
+			'bp_xprofile_fields',
+			'bp_xprofile_groups',
+			'xprofile_meta'
+		) );
+
+		parent::setup_cache_groups();
+	}
+
+	/**
+	 * Adds "Settings > Profile" subnav item under the "Settings" adminbar menu.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $wp_admin_nav The settings adminbar nav array.
+	 * @return array
+	 */
+	public function setup_settings_admin_nav( $wp_admin_nav ) {
+
+		// Setup the logged in user variables.
+		$settings_link = trailingslashit( bp_loggedin_user_domain() . bp_get_settings_slug() );
+
+		// Add the "Profile" subnav item.
+		$wp_admin_nav[] = array(
+			'parent' => 'my-account-' . buddypress()->settings->id,
+			'id'     => 'my-account-' . buddypress()->settings->id . '-profile',
+			'title'  => _x( 'Profile', 'My Account Settings sub nav', 'buddypress' ),
+			'href'   => trailingslashit( $settings_link . 'profile' )
+		);
+
+		return $wp_admin_nav;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-data-template.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-data-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-data-template.php	(revision 12265)
@@ -0,0 +1,364 @@
+<?php
+/**
+ * BuddyPress XProfile Template Loop Class.
+ *
+ * @package BuddyPress
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The main profile template loop class.
+ *
+ * This is responsible for loading profile field, group, and data and displaying it.
+ *
+ * @since 1.0.0
+ */
+class BP_XProfile_Data_Template {
+
+	/**
+	 * The loop iterator.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	public $current_group = -1;
+
+	/**
+	 * The number of groups returned by the paged query.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	public $group_count;
+
+	/**
+	 * Array of groups located by the query.
+	 *
+	 * @since 1.5.0
+	 * @var array
+	 */
+	public $groups;
+
+	/**
+	 * The group object currently being iterated on.
+	 *
+	 * @since 1.5.0
+	 * @var object
+	 */
+	public $group;
+
+	/**
+	 * The current field.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	public $current_field = -1;
+
+	/**
+	 * The field count.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	public $field_count;
+
+	/**
+	 * Field has data.
+	 *
+	 * @since 1.5.0
+	 * @var bool
+	 */
+	public $field_has_data;
+
+	/**
+	 * The field.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	public $field;
+
+	/**
+	 * A flag for whether the loop is currently being iterated.
+	 *
+	 * @since 1.5.0
+	 * @var bool
+	 */
+	public $in_the_loop;
+
+	/**
+	 * The user ID.
+	 *
+	 * @since 1.5.0
+	 * @var int
+	 */
+	public $user_id;
+
+	/**
+	 * Get activity items, as specified by parameters.
+	 *
+	 * @see BP_XProfile_Group::get() for more details about parameters.
+	 *
+	 * @since 1.5.0
+	 * @since 2.4.0 Introduced `$member_type` argument.
+	 *
+	 * @param array|string $args {
+	 *     An array of arguments. All items are optional.
+	 *
+	 *     @type int          $user_id                 Fetch field data for this user ID.
+	 *     @type string|array $member_type             Limit results to those matching member type(s).
+	 *     @type int          $profile_group_id        Field group to fetch fields & data for.
+	 *     @type int|bool     $hide_empty_groups       Should empty field groups be skipped.
+	 *     @type int|bool     $fetch_fields            Fetch fields for field group.
+	 *     @type int|bool     $fetch_field_data        Fetch field data for fields in group.
+	 *     @type array        $exclude_groups          Exclude these field groups.
+	 *     @type array        $exclude_fields          Exclude these fields.
+	 *     @type int|bool     $hide_empty_fields       Should empty fields be skipped.
+	 *     @type int|bool     $fetch_visibility_level  Fetch visibility levels.
+	 *     @type int|bool     $update_meta_cache       Should metadata cache be updated.
+	 * }
+	 */
+	public function __construct( $args = '' ) {
+
+		// Backward compatibility with old method of passing arguments.
+		if ( ! is_array( $args ) || func_num_args() > 1 ) {
+			_deprecated_argument( __METHOD__, '2.3.0', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );
+
+			$old_args_keys = array(
+				0 => 'user_id',
+				1 => 'profile_group_id',
+				2 => 'hide_empty_groups',
+				3 => 'fetch_fields',
+				4 => 'fetch_field_data',
+				5 => 'exclude_groups',
+				6 => 'exclude_fields',
+				7 => 'hide_empty_fields',
+				8 => 'fetch_visibility_level',
+				9 => 'update_meta_cache'
+			);
+
+			$args = bp_core_parse_args_array( $old_args_keys, func_get_args() );
+		}
+
+		$r = wp_parse_args( $args, array(
+			'profile_group_id'       => false,
+			'user_id'                => false,
+			'member_type'            => 'any',
+			'hide_empty_groups'      => false,
+			'hide_empty_fields'      => false,
+			'fetch_fields'           => false,
+			'fetch_field_data'       => false,
+			'fetch_visibility_level' => false,
+			'exclude_groups'         => false,
+			'exclude_fields'         => false,
+			'update_meta_cache'      => true
+		) );
+
+		$this->groups      = bp_xprofile_get_groups( $r );
+		$this->group_count = count( $this->groups );
+		$this->user_id     = $r['user_id'];
+	}
+
+	/**
+	 * Whether or not the loop has field groups.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function has_groups() {
+		if ( ! empty( $this->group_count ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Increments to the next group of fields.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return object
+	 */
+	public function next_group() {
+		$this->current_group++;
+
+		$this->group       = $this->groups[ $this->current_group ];
+		$this->field_count = 0;
+
+		if ( ! empty( $this->group->fields ) ) {
+
+			/**
+			 * Filters the group fields for the next_group method.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param array $fields Array of fields for the group.
+			 * @param int   $id     ID of the field group.
+			 */
+			$this->group->fields = apply_filters( 'xprofile_group_fields', $this->group->fields, $this->group->id );
+			$this->field_count   = count( $this->group->fields );
+		}
+
+		return $this->group;
+	}
+
+	/**
+	 * Rewinds to the start of the groups list.
+	 *
+	 * @since 1.0.0
+	 */
+	public function rewind_groups() {
+		$this->current_group = -1;
+		if ( $this->group_count > 0 ) {
+			$this->group = $this->groups[0];
+		}
+	}
+
+	/**
+	 * Kicks off the profile groups.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function profile_groups() {
+		if ( $this->current_group + 1 < $this->group_count ) {
+			return true;
+		} elseif ( $this->current_group + 1 == $this->group_count ) {
+
+			/**
+			 * Fires right before the rewinding of profile groups.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'xprofile_template_loop_end' );
+
+			// Do some cleaning up after the loop.
+			$this->rewind_groups();
+		}
+
+		$this->in_the_loop = false;
+		return false;
+	}
+
+	/**
+	 * Sets up the profile group.
+	 *
+	 * @since 1.0.0
+	 */
+	public function the_profile_group() {
+		global $group;
+
+		$this->in_the_loop = true;
+		$group = $this->next_group();
+
+		// Loop has just started.
+		if ( 0 === $this->current_group ) {
+
+			/**
+			 * Fires if the current group is the first in the loop.
+			 *
+			 * @since 1.1.0
+			 */
+			do_action( 'xprofile_template_loop_start' );
+		}
+	}
+
+	/** Fields ****************************************************************/
+
+	/**
+	 * Increments to the next field.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return int
+	 */
+	public function next_field() {
+		$this->current_field++;
+
+		$this->field = $this->group->fields[ $this->current_field ];
+
+		return $this->field;
+	}
+
+	/**
+	 * Rewinds to the start of the fields.
+	 *
+	 * @since 1.0.0
+	 */
+	public function rewind_fields() {
+		$this->current_field = -1;
+		if ( $this->field_count > 0 ) {
+			$this->field = $this->group->fields[0];
+		}
+	}
+
+	/**
+	 * Whether or not the loop has fields.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function has_fields() {
+		$has_data = false;
+
+		for ( $i = 0, $count = count( $this->group->fields ); $i < $count; ++$i ) {
+			$field = &$this->group->fields[ $i ];
+
+			if ( ! empty( $field->data ) && ( $field->data->value != null ) ) {
+				$has_data = true;
+			}
+		}
+
+		return $has_data;
+	}
+
+	/**
+	 * Kick off the profile fields.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function profile_fields() {
+		if ( $this->current_field + 1 < $this->field_count ) {
+			return true;
+		} elseif ( $this->current_field + 1 == $this->field_count ) {
+			// Do some cleaning up after the loop.
+			$this->rewind_fields();
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set up the profile fields.
+	 *
+	 * @since 1.0.0
+	 */
+	public function the_profile_field() {
+		global $field;
+
+		$field = $this->next_field();
+
+		// Valid field values of 0 or '0' get caught by empty(), so we have an extra check for these. See #BP5731.
+		if ( ! empty( $field->data ) && ( ! empty( $field->data->value ) || ( '0' === $field->data->value ) ) ) {
+			$value = maybe_unserialize( $field->data->value );
+		} else {
+			$value = false;
+		}
+
+		if ( ! empty( $value ) || ( '0' === $value ) ) {
+			$this->field_has_data = true;
+		} else {
+			$this->field_has_data = false;
+		}
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-checkbox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-checkbox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-checkbox.php	(revision 12265)
@@ -0,0 +1,213 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Checkbox xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Checkbox extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the checkbox field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Checkboxes', 'xprofile field type', 'buddypress' );
+
+		$this->supports_multiple_defaults = true;
+		$this->accepts_null_value         = true;
+		$this->supports_options           = true;
+
+		$this->set_format( '/^.+$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Checkbox class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Checkbox $this Current instance of
+		 *                                              the field type checkbox.
+		 */
+		do_action( 'bp_xprofile_field_type_checkbox', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.checkbox.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that we pass to
+		// {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			$user_id = (int) $raw_properties['user_id'];
+			unset( $raw_properties['user_id'] );
+		} else {
+			$user_id = bp_displayed_user_id();
+		} ?>
+
+			<legend>
+				<?php bp_the_profile_field_name(); ?>
+				<?php bp_the_profile_field_required_label(); ?>
+			</legend>
+
+			<?php if ( bp_get_the_profile_field_description() ) : ?>
+				<p class="description" tabindex="0"><?php bp_the_profile_field_description(); ?></p>
+			<?php endif; ?>
+
+			<?php
+
+			/** This action is documented in bp-xprofile/bp-xprofile-classes */
+			do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+			<?php bp_the_profile_field_options( array(
+				'user_id' => $user_id
+			) ); ?>
+
+		<?php
+	}
+
+	/**
+	 * Output the edit field options HTML for this field type.
+	 *
+	 * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
+	 * These are stored separately in the database, and their templating is handled separately.
+	 *
+	 * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
+	 * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
+	 */
+	public function edit_field_options_html( array $args = array() ) {
+		$options       = $this->field_obj->get_children();
+		$option_values = maybe_unserialize( BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] ) );
+
+		/*
+		 * Determine whether to pre-select the default option.
+		 *
+		 * If there's no saved value, take the following into account:
+		 * If the user has never saved a value for this field,
+		 * $option_values will be an empty string, and we should pre-select the default option.
+		 * If the user has specifically chosen none of the options,
+		 * $option_values will be an empty array, and we should respect that value.
+		 */
+		$select_default_option = false;
+		if ( empty( $option_values ) && ! is_array( $option_values ) ) {
+			$select_default_option = true;
+		}
+
+		$option_values = ( $option_values ) ? (array) $option_values : array();
+
+		$html = '';
+
+		// Check for updated posted values, but errors preventing them from
+		// being saved first time.
+		if ( isset( $_POST['field_' . $this->field_obj->id] ) && $option_values != maybe_serialize( $_POST['field_' . $this->field_obj->id] ) ) {
+			if ( ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
+				$option_values = array_map( 'sanitize_text_field', $_POST['field_' . $this->field_obj->id] );
+			}
+		}
+
+		for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
+			$selected = '';
+
+			// First, check to see whether the user's saved values match the option.
+			for ( $j = 0, $count_values = count( $option_values ); $j < $count_values; ++$j ) {
+
+				// Run the allowed option name through the before_save filter,
+				// so we'll be sure to get a match.
+				$allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
+
+				if ( $option_values[$j] === $allowed_options || in_array( $allowed_options, $option_values ) ) {
+					$selected = ' checked="checked"';
+					break;
+				}
+			}
+
+			// If the user has not yet supplied a value for this field, check to
+			// see whether there is a default value available.
+			if ( empty( $selected ) && $select_default_option && ! empty( $options[$k]->is_default_option ) ) {
+				$selected = ' checked="checked"';
+			}
+
+			$new_html = sprintf( '<label for="%3$s" class="option-label"><input %1$s type="checkbox" name="%2$s" id="%3$s" value="%4$s">%5$s</label>',
+				$selected,
+				esc_attr( bp_get_the_profile_field_input_name() . '[]' ),
+				esc_attr( "field_{$options[$k]->id}_{$k}" ),
+				esc_attr( stripslashes( $options[$k]->name ) ),
+				esc_html( stripslashes( $options[$k]->name ) )
+			);
+
+			/**
+			 * Filters the HTML output for an individual field options checkbox.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $new_html Label and checkbox input field.
+			 * @param object $value    Current option being rendered for.
+			 * @param int    $id       ID of the field object being rendered.
+			 * @param string $selected Current selected value.
+			 * @param string $k        Current index in the foreach loop.
+			 */
+			$html .= apply_filters( 'bp_get_the_profile_field_options_checkbox', $new_html, $options[$k], $this->field_obj->id, $selected, $k );
+		}
+
+		printf( '<div id="%1$s" class="input-options checkbox-options">%2$s</div>',
+			esc_attr( 'field_' . $this->field_obj->id ),
+			$html
+		);
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		bp_the_profile_field_options();
+	}
+
+	/**
+	 * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
+		parent::admin_new_field_html( $current_field, 'checkbox' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-datebox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-datebox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-datebox.php	(revision 12265)
@@ -0,0 +1,636 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Datebox xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Datebox extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the datebox field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Date Selector', 'xprofile field type', 'buddypress' );
+
+		$this->set_format( '/^\d{4}-\d{1,2}-\d{1,2} 00:00:00$/', 'replace' ); // "Y-m-d 00:00:00"
+
+		$this->do_settings_section = true;
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Datebox class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Datebox $this Current instance of
+		 *                                             the field type datebox.
+		 */
+		do_action( 'bp_xprofile_field_type_datebox', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that we pass to.
+		// {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			$user_id = (int) $raw_properties['user_id'];
+			unset( $raw_properties['user_id'] );
+		} else {
+			$user_id = bp_displayed_user_id();
+		}
+
+		$day_r = bp_parse_args( $raw_properties, array(
+			'id'   => bp_get_the_profile_field_input_name() . '_day',
+			'name' => bp_get_the_profile_field_input_name() . '_day'
+		) );
+
+		$month_r = bp_parse_args( $raw_properties, array(
+			'id'   => bp_get_the_profile_field_input_name() . '_month',
+			'name' => bp_get_the_profile_field_input_name() . '_month'
+		) );
+
+		$year_r = bp_parse_args( $raw_properties, array(
+			'id'   => bp_get_the_profile_field_input_name() . '_year',
+			'name' => bp_get_the_profile_field_input_name() . '_year'
+		) ); ?>
+
+			<legend>
+				<?php bp_the_profile_field_name(); ?>
+				<?php bp_the_profile_field_required_label(); ?>
+			</legend>
+
+			<?php if ( bp_get_the_profile_field_description() ) : ?>
+				<p class="description" tabindex="0"><?php bp_the_profile_field_description(); ?></p>
+			<?php endif; ?>
+
+			<div class="input-options datebox-selects">
+
+				<?php
+
+				/**
+				 * Fires after field label and displays associated errors for the field.
+				 *
+				 * This is a dynamic hook that is dependent on the associated
+				 * field ID. The hooks will be similar to `bp_field_12_errors`
+				 * where the 12 is the field ID. Simply replace the 12 with
+				 * your needed target ID.
+				 *
+				 * @since 1.8.0
+				 */
+				do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+				<label for="<?php bp_the_profile_field_input_name(); ?>_day" class="xprofile-field-label"><?php
+					esc_html_e( 'Day', 'buddypress' );
+				?></label>
+				<select <?php echo $this->get_edit_field_html_elements( $day_r ); ?>>
+					<?php bp_the_profile_field_options( array(
+						'type'    => 'day',
+						'user_id' => $user_id
+					) ); ?>
+				</select>
+
+				<label for="<?php bp_the_profile_field_input_name(); ?>_month" class="xprofile-field-label"><?php
+					esc_html_e( 'Month', 'buddypress' );
+				?></label>
+				<select <?php echo $this->get_edit_field_html_elements( $month_r ); ?>>
+					<?php bp_the_profile_field_options( array(
+						'type'    => 'month',
+						'user_id' => $user_id
+					) ); ?>
+				</select>
+
+				<label for="<?php bp_the_profile_field_input_name(); ?>_year" class="xprofile-field-label"><?php
+					esc_html_e( 'Year', 'buddypress' );
+				?></label>
+				<select <?php echo $this->get_edit_field_html_elements( $year_r ); ?>>
+					<?php bp_the_profile_field_options( array(
+						'type'    => 'year',
+						'user_id' => $user_id
+					) ); ?>
+				</select>
+
+			</div>
+
+	<?php
+	}
+
+	/**
+	 * Output the edit field options HTML for this field type.
+	 *
+	 * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
+	 * These are stored separately in the database, and their templating is handled separately.
+	 *
+	 * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
+	 * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
+	 */
+	public function edit_field_options_html( array $args = array() ) {
+
+		$date       = BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] );
+		$day        = 0;
+		$month      = 0;
+		$year       = 0;
+		$html       = '';
+		$eng_months = array( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' );
+
+		// Set day, month, year defaults.
+		if ( ! empty( $date ) ) {
+
+			// If Unix timestamp.
+			if ( is_numeric( $date ) ) {
+				$day   = date( 'j', $date );
+				$month = date( 'F', $date );
+				$year  = date( 'Y', $date );
+
+			// If MySQL timestamp.
+			} else {
+				$day   = mysql2date( 'j', $date );
+				$month = mysql2date( 'F', $date, false ); // Not localized, so that selected() works below.
+				$year  = mysql2date( 'Y', $date );
+			}
+		}
+
+		// Check for updated posted values, and errors preventing them from
+		// being saved first time.
+		if ( ! empty( $_POST['field_' . $this->field_obj->id . '_day'] ) ) {
+			$new_day = (int) $_POST['field_' . $this->field_obj->id . '_day'];
+			$day     = ( $day != $new_day ) ? $new_day : $day;
+		}
+
+		if ( ! empty( $_POST['field_' . $this->field_obj->id . '_month'] ) ) {
+			if ( in_array( $_POST['field_' . $this->field_obj->id . '_month'], $eng_months ) ) {
+				$new_month = $_POST['field_' . $this->field_obj->id . '_month'];
+			} else {
+				$new_month = $month;
+			}
+
+			$month = ( $month !== $new_month ) ? $new_month : $month;
+		}
+
+		if ( ! empty( $_POST['field_' . $this->field_obj->id . '_year'] ) ) {
+			$new_year = (int) $_POST['field_' . $this->field_obj->id . '_year'];
+			$year     = ( $year != $new_year ) ? $new_year : $year;
+		}
+
+		// $type will be passed by calling function when needed.
+		switch ( $args['type'] ) {
+			case 'day':
+				$html = sprintf( '<option value="" %1$s>%2$s</option>', selected( $day, 0, false ), /* translators: no option picked in select box */ __( '----', 'buddypress' ) );
+
+				for ( $i = 1; $i < 32; ++$i ) {
+					$html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', (int) $i, selected( $day, $i, false ), (int) $i );
+				}
+			break;
+
+			case 'month':
+				$months = array(
+					__( 'January',   'buddypress' ),
+					__( 'February',  'buddypress' ),
+					__( 'March',     'buddypress' ),
+					__( 'April',     'buddypress' ),
+					__( 'May',       'buddypress' ),
+					__( 'June',      'buddypress' ),
+					__( 'July',      'buddypress' ),
+					__( 'August',    'buddypress' ),
+					__( 'September', 'buddypress' ),
+					__( 'October',   'buddypress' ),
+					__( 'November',  'buddypress' ),
+					__( 'December',  'buddypress' )
+				);
+
+				$html = sprintf( '<option value="" %1$s>%2$s</option>', selected( $month, 0, false ), /* translators: no option picked in select box */ __( '----', 'buddypress' ) );
+
+				for ( $i = 0; $i < 12; ++$i ) {
+					$html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $eng_months[$i] ), selected( $month, $eng_months[$i], false ), $months[$i] );
+				}
+			break;
+
+			case 'year':
+				$html = sprintf( '<option value="" %1$s>%2$s</option>', selected( $year, 0, false ), /* translators: no option picked in select box */ __( '----', 'buddypress' ) );
+
+				$settings = $this->get_field_settings( $this->field_obj->id );
+
+				if ( 'relative' === $settings['range_type'] ) {
+					$start = date( 'Y' ) + $settings['range_relative_start'];
+					$end   = date( 'Y' ) + $settings['range_relative_end'];
+				} else {
+					$start = $settings['range_absolute_start'];
+					$end   = $settings['range_absolute_end'];
+				}
+
+				for ( $i = $end; $i >= $start; $i-- ) {
+					$html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', (int) $i, selected( $year, $i, false ), (int) $i );
+				}
+			break;
+		}
+
+		/**
+		 * Filters the output for the profile field datebox.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param string $html  HTML output for the field.
+		 * @param string $value Which date type is being rendered for.
+		 * @param string $day   Date formatted for the current day.
+		 * @param string $month Date formatted for the current month.
+		 * @param string $year  Date formatted for the current year.
+		 * @param int    $id    ID of the field object being rendered.
+		 * @param string $date  Current date.
+		 */
+		echo apply_filters( 'bp_get_the_profile_field_datebox', $html, $args['type'], $day, $month, $year, $this->field_obj->id, $date );
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+
+		$day_r = bp_parse_args( $raw_properties, array(
+			'id'   => bp_get_the_profile_field_input_name() . '_day',
+			'name' => bp_get_the_profile_field_input_name() . '_day'
+		) );
+
+		$month_r = bp_parse_args( $raw_properties, array(
+			'id'   => bp_get_the_profile_field_input_name() . '_month',
+			'name' => bp_get_the_profile_field_input_name() . '_month'
+		) );
+
+		$year_r = bp_parse_args( $raw_properties, array(
+			'id'   => bp_get_the_profile_field_input_name() . '_year',
+			'name' => bp_get_the_profile_field_input_name() . '_year'
+		) ); ?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>_day" class="xprofile-field-label"><?php
+			esc_html_e( 'Day', 'buddypress' );
+		?></label>
+		<select <?php echo $this->get_edit_field_html_elements( $day_r ); ?>>
+			<?php bp_the_profile_field_options( array( 'type' => 'day' ) ); ?>
+		</select>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>_month" class="xprofile-field-label"><?php
+			esc_html_e( 'Month', 'buddypress' );
+		?></label>
+		<select <?php echo $this->get_edit_field_html_elements( $month_r ); ?>>
+			<?php bp_the_profile_field_options( array( 'type' => 'month' ) ); ?>
+		</select>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>_year" class="xprofile-field-label"><?php
+			esc_html_e( 'Year', 'buddypress' );
+		?></label>
+		<select <?php echo $this->get_edit_field_html_elements( $year_r ); ?>>
+			<?php bp_the_profile_field_options( array( 'type' => 'year' ) ); ?>
+		</select>
+
+	<?php
+	}
+
+	/**
+	 * Get settings for a given date field.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int $field_id ID of the field.
+	 * @return array
+	 */
+	public static function get_field_settings( $field_id ) {
+		$defaults = array(
+			'date_format'          => 'Y-m-d',
+			'date_format_custom'   => '',
+			'range_type'           => 'absolute',
+			'range_absolute_start' => date( 'Y' ) - 60,
+			'range_absolute_end'   => date( 'Y' ) + 10,
+			'range_relative_start' => '-10',
+			'range_relative_end'   => '20',
+		);
+
+		$settings = array();
+		foreach ( $defaults as $key => $value ) {
+			$saved = bp_xprofile_get_meta( $field_id, 'field', $key, true );
+
+			if ( $saved ) {
+				$settings[ $key ] = $saved;
+			} else {
+				$settings[ $key ] = $value;
+			}
+		}
+
+		$settings = self::validate_settings( $settings );
+
+		return $settings;
+	}
+
+	/**
+	 * Validate date field settings.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $settings Raw settings.
+	 * @return array Validated settings.
+	 */
+	public static function validate_settings( $settings ) {
+		foreach ( $settings as $key => &$value ) {
+			switch ( $key ) {
+				case 'range_type' :
+					if ( $value !== 'absolute' ) {
+						$value = 'relative';
+					}
+				break;
+
+				// @todo More date restrictions?
+				case 'range_absolute_start' :
+				case 'range_absolute_end' :
+					$value = absint( $value );
+				break;
+
+				case 'range_relative_start' :
+				case 'range_relative_end' :
+					$value = intval( $value );
+				break;
+			}
+		}
+
+		return $settings;
+	}
+
+	/**
+	 * Save settings from the field edit screen in the Dashboard.
+	 *
+	 * @param int   $field_id ID of the field.
+	 * @param array $settings Array of settings.
+	 * @return bool True on success.
+	 */
+	public function admin_save_settings( $field_id, $settings ) {
+		$existing_settings = self::get_field_settings( $field_id );
+
+		$saved_settings = array();
+		foreach ( array_keys( $existing_settings ) as $setting ) {
+			switch ( $setting ) {
+				case 'range_relative_start' :
+				case 'range_relative_end' :
+					$op_key = $setting . '_type';
+					if ( isset( $settings[ $op_key ] ) && 'past' === $settings[ $op_key ] ) {
+						$value = 0 - intval( $settings[ $setting ] );
+					} else {
+						$value = intval( $settings[ $setting ] );
+					}
+
+					$saved_settings[ $setting ] = $value;
+				break;
+
+				default :
+					if ( isset( $settings[ $setting ] ) ) {
+						$saved_settings[ $setting ] = $settings[ $setting ];
+					}
+				break;
+			}
+		}
+
+		// Sanitize and validate saved settings.
+		$saved_settings = self::validate_settings( $saved_settings );
+
+		foreach ( $saved_settings as $setting_key => $setting_value ) {
+			bp_xprofile_update_meta( $field_id, 'field', $setting_key, $setting_value );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Generate the settings markup for Date fields.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
+		$type = array_search( get_class( $this ), bp_xprofile_get_field_types() );
+
+		if ( false === $type ) {
+			return;
+		}
+
+		$class = $current_field->type != $type ? 'display: none;' : '';
+
+		$settings = self::get_field_settings( $current_field->id );
+		?>
+
+<div id="<?php echo esc_attr( $type ); ?>" class="postbox bp-options-box" style="<?php echo esc_attr( $class ); ?> margin-top: 15px;">
+	<table class="form-table bp-date-options">
+		<tr>
+			<th scope="row">
+				<?php esc_html_e( 'Date format', 'buddypress' ); ?>
+			</th>
+
+			<td>
+				<fieldset>
+					<legend class="screen-reader-text">
+						<?php esc_html_e( 'Date format', 'buddypress' ); ?>
+					</legend>
+
+					<?php foreach ( $this->get_date_formats() as $format ): ?>
+						<div class="bp-date-format-option">
+							<label for="date-format-<?php echo esc_attr( $format ); ?>">
+								<input type="radio" name="field-settings[date_format]" id="date-format-<?php echo esc_attr( $format ); ?>" value="<?php echo esc_attr( $format ); ?>" <?php checked( $format, $settings['date_format'] ); ?> />
+								<span class="date-format-label"><?php echo date_i18n( $format ); ?></span>
+								<code><?php echo esc_html( $format ); ?></code>
+							</label>
+						</div>
+					<?php endforeach;?>
+
+					<div class="bp-date-format-option">
+						<label for="date-format-elapsed">
+							<input type="radio" name="field-settings[date_format]" id="date-format-elapsed" <?php checked( 'elapsed', $settings['date_format'] ); ?> value="elapsed" aria-describedby="date-format-elapsed-setting" />
+							<span class="date-format-label" id="date-format-elapsed-setting"><?php esc_html_e( 'Time elapsed', 'buddypress' ); ?></span> <?php _e( '<code>4 years ago</code>, <code>4 years from now</code>', 'buddypress' ); ?>
+						</label>
+					</div>
+
+					<div class="bp-date-format-option">
+						<label for="date-format-custom">
+							<input type="radio" name="field-settings[date_format]" id="date-format-custom" <?php checked( 'custom', $settings['date_format'] ); ?> value="custom" />
+							<span class="date-format-label"><?php esc_html_e( 'Custom:', 'buddypress' ); ?></span>
+						</label>
+						<label for="date-format-custom-value" class="screen-reader-text"><?php esc_html_e( 'Enter custom time format', 'buddypress' ); ?></label>
+						<input type="text" name="field-settings[date_format_custom]" id="date-format-custom-value" class="date-format-custom-value" value="<?php echo esc_attr( $settings['date_format_custom'] ); ?>" aria-describedby="date-format-custom-example" /> <span class="screen-reader-text"><?php esc_html_e( 'Example:', 'buddypress' ); ?></span><span class="date-format-custom-example" id="date-format-custom-sample"><?php if ( $settings['date_format_custom'] ) : ?><?php echo esc_html( date_i18n( $settings['date_format_custom'] ) ); endif; ?></span><span class="spinner" id="date-format-custom-spinner" aria-hidden="true"></span>
+
+						<p><a href="https://codex.wordpress.org/Formatting_Date_and_Time"><?php esc_html_e( 'Documentation on date and time formatting', 'buddypress' ); ?></a></p>
+					</div>
+
+				</fieldset>
+			</td>
+		</tr>
+
+		<tr>
+			<th scope="row">
+				<?php esc_html_e( 'Range', 'buddypress' ); ?>
+			</th>
+
+			<td>
+				<fieldset class="bp-range-types">
+					<legend class="screen-reader-text">
+						<?php esc_html_e( 'Range', 'buddypress' ); ?>
+					</legend>
+
+					<div class="bp-date-format-option">
+						<div class="bp-date-range-type-label">
+							<label for="range_type_absolute">
+								<input type="radio" name="field-settings[range_type]" id="range_type_absolute" value="absolute" <?php checked( 'absolute', $settings['range_type'] ); ?> />
+								<?php esc_html_e( 'Absolute', 'buddypress' ); ?>
+							</label>
+						</div>
+
+						<div class="bp-date-range-type-values">
+							<label for="field-settings[range_absolute_start]" aria-label="Year"><?php esc_html_e( 'Start:', 'buddypress' ); ?></label>
+							<?php printf( '<input class="date-range-numeric" type="text" name="field-settings[range_absolute_start]" id="field-settings[range_absolute_start]" value="%s" />', esc_attr( $settings['range_absolute_start'] ) ); ?>
+							<label for="field-settings[range_absolute_end]" aria-label="Year"><?php esc_html_e( 'End:', 'buddypress' ); ?></label>
+							<?php printf( '<input class="date-range-numeric" type="text" name="field-settings[range_absolute_end]" id="field-settings[range_absolute_end]" value="%s" />', esc_attr( $settings['range_absolute_end'] ) ); ?>
+						</div>
+					</div>
+
+					<div class="bp-date-format-option">
+						<div class="bp-date-range-type-label">
+							<label for="range_type_relative">
+								<input type="radio" name="field-settings[range_type]" id="range_type_relative" value="relative" <?php checked( 'relative', $settings['range_type'] ); ?> />
+								<?php esc_html_e( 'Relative', 'buddypress' ); ?>
+							</label>
+						</div>
+
+						<div class="bp-date-range-type-values">
+							<label for="field-settings[range_relative_start]"><?php esc_html_e( 'Start:', 'buddypress' ); ?></label>
+							<?php printf( '<input type="text" class="date-range-numeric" name="field-settings[range_relative_start]" id="field-settings[range_relative_start]" value="%s" />',
+								esc_attr( abs( $settings['range_relative_start'] ) )
+								);
+							?>
+
+							<label class="screen-reader-text" for="field-settings[range_relative_start_type]"><?php esc_html_e( 'Select range', 'buddypress' ); ?></label>
+							<?php printf( '<select name="field-settings[range_relative_start_type]" id="field-settings[range_relative_start_type]"><option value="past" %s>%s</option><option value="future" %s>%s</option></select>',
+								selected( true, $settings['range_relative_start'] <= 0, false ),
+								esc_attr__( 'years ago', 'buddypress' ),
+								selected( true, $settings['range_relative_start'] > 0, false ),
+								esc_attr__( 'years from now', 'buddypress' )
+								);
+							?>
+
+							<label for="field-settings[range_relative_end]"><?php esc_html_e( 'End:', 'buddypress' ); ?></label>
+							<?php printf( '<input type="text" class="date-range-numeric" name="field-settings[range_relative_end]" id="field-settings[range_relative_end]" value="%s" />',
+								esc_attr( abs( $settings['range_relative_end'] ) )
+								);
+							?>
+							<label class="screen-reader-text" for="field-settings[range_relative_end_type]"><?php esc_html_e( 'Select range', 'buddypress' ); ?></label>
+							<?php printf( '<select name="field-settings[range_relative_end_type]" id="field-settings[range_relative_end_type]"><option value="past" %s>%s</option><option value="future" %s>%s</option></select>',
+									selected( true, $settings['range_relative_end'] <= 0, false ),
+									esc_attr__( 'years ago', 'buddypress' ),
+									selected( true, $settings['range_relative_end'] > 0, false ),
+									esc_attr__( 'years from now', 'buddypress' )
+								);
+							?>
+						</div>
+					</div>
+
+				</fieldset>
+			</td>
+		</tr>
+	</table>
+</div>
+		<?php
+	}
+
+	/**
+	 * Format Date values for display.
+	 *
+	 * @since 2.1.0
+	 * @since 2.4.0 Added the `$field_id` parameter.
+	 *
+	 * @param string     $field_value The date value, as saved in the database. Typically, this is a MySQL-formatted
+	 *                                date string (Y-m-d H:i:s).
+	 * @param string|int $field_id    Optional. ID of the field.
+	 * @return string Date formatted by bp_format_time().
+	 */
+	public static function display_filter( $field_value, $field_id = '' ) {
+		if ( ! $field_value ) {
+			return $field_value;
+		}
+
+		// If Unix timestamp.
+		if ( ! is_numeric( $field_value ) ) {
+			$field_value = strtotime( $field_value );
+		}
+
+		$settings = self::get_field_settings( $field_id );
+
+		switch ( $settings['date_format'] ) {
+			case 'elapsed' :
+				$formatted = bp_core_time_since( $field_value );
+			break;
+
+			case 'custom' :
+				$formatted = date_i18n( $settings['date_format_custom'], $field_value );
+			break;
+
+			default :
+				$formatted = date_i18n( $settings['date_format'], $field_value );
+			break;
+		}
+
+		return $formatted;
+	}
+
+	/**
+	 * Gets the default date formats available when configuring a Date field.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return array
+	 */
+	public function get_date_formats() {
+		$date_formats = array_unique( apply_filters( 'date_formats', array( __( 'F j, Y', 'buddypress' ), 'Y-m-d', 'm/d/Y', 'd/m/Y' ) ) );
+
+
+		/**
+		 * Filters the available date formats for XProfile date fields.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param array $date_formats
+		 */
+		return apply_filters( 'bp_xprofile_date_field_date_formats', $date_formats );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-multiselectbox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-multiselectbox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-multiselectbox.php	(revision 12265)
@@ -0,0 +1,216 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Multi-selectbox xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Multiselectbox extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the multi-selectbox field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Multi Select Box', 'xprofile field type', 'buddypress' );
+
+		$this->supports_multiple_defaults = true;
+		$this->accepts_null_value         = true;
+		$this->supports_options           = true;
+
+		$this->set_format( '/^.+$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Multiselectbox class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Multiselectbox $this Current instance of
+		 *                                                    the field type multiple select box.
+		 */
+		do_action( 'bp_xprofile_field_type_multiselectbox', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/select.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that we pass to
+		// {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			$user_id = (int) $raw_properties['user_id'];
+			unset( $raw_properties['user_id'] );
+		} else {
+			$user_id = bp_displayed_user_id();
+		}
+
+		$r = bp_parse_args( $raw_properties, array(
+			'multiple' => 'multiple',
+			'id'       => bp_get_the_profile_field_input_name() . '[]',
+			'name'     => bp_get_the_profile_field_input_name() . '[]',
+		) ); ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+		<select <?php echo $this->get_edit_field_html_elements( $r ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3">
+			<?php bp_the_profile_field_options( array(
+				'user_id' => $user_id
+			) ); ?>
+		</select>
+
+		<?php if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif; ?>
+
+		<?php if ( ! bp_get_the_profile_field_is_required() ) : ?>
+
+			<a class="clear-value" href="javascript:clear( '<?php echo esc_js( bp_get_the_profile_field_input_name() ); ?>[]' );">
+				<?php esc_html_e( 'Clear', 'buddypress' ); ?>
+			</a>
+
+		<?php endif; ?>
+	<?php
+	}
+
+	/**
+	 * Output the edit field options HTML for this field type.
+	 *
+	 * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
+	 * These are stored separately in the database, and their templating is handled separately.
+	 *
+	 * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
+	 * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
+	 */
+	public function edit_field_options_html( array $args = array() ) {
+		$original_option_values = maybe_unserialize( BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] ) );
+
+		$options = $this->field_obj->get_children();
+		$html    = '';
+
+		if ( empty( $original_option_values ) && ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
+			$original_option_values = sanitize_text_field( $_POST['field_' . $this->field_obj->id] );
+		}
+
+		$option_values = ( $original_option_values ) ? (array) $original_option_values : array();
+		for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
+			$selected = '';
+
+			// Check for updated posted values, but errors preventing them from
+			// being saved first time.
+			foreach( $option_values as $i => $option_value ) {
+				if ( isset( $_POST['field_' . $this->field_obj->id] ) && $_POST['field_' . $this->field_obj->id][$i] != $option_value ) {
+					if ( ! empty( $_POST['field_' . $this->field_obj->id][$i] ) ) {
+						$option_values[] = sanitize_text_field( $_POST['field_' . $this->field_obj->id][$i] );
+					}
+				}
+			}
+
+			// Run the allowed option name through the before_save filter, so
+			// we'll be sure to get a match.
+			$allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
+
+			// First, check to see whether the user-entered value matches.
+			if ( in_array( $allowed_options, $option_values ) ) {
+				$selected = ' selected="selected"';
+			}
+
+			// Then, if the user has not provided a value, check for defaults.
+			if ( ! is_array( $original_option_values ) && empty( $option_values ) && ! empty( $options[$k]->is_default_option ) ) {
+				$selected = ' selected="selected"';
+			}
+
+			/**
+			 * Filters the HTML output for options in a multiselect input.
+			 *
+			 * @since 1.5.0
+			 *
+			 * @param string $value    Option tag for current value being rendered.
+			 * @param object $value    Current option being rendered for.
+			 * @param int    $id       ID of the field object being rendered.
+			 * @param string $selected Current selected value.
+			 * @param string $k        Current index in the foreach loop.
+			 */
+			$html .= apply_filters( 'bp_get_the_profile_field_options_multiselect', '<option' . $selected . ' value="' . esc_attr( stripslashes( $options[$k]->name ) ) . '">' . esc_html( stripslashes( $options[$k]->name ) ) . '</option>', $options[$k], $this->field_obj->id, $selected, $k );
+		}
+
+		echo $html;
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		$r = bp_parse_args( $raw_properties, array(
+			'multiple' => 'multiple'
+		) ); ?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Select', 'buddypress' );
+		?></label>
+		<select <?php echo $this->get_edit_field_html_elements( $r ); ?>>
+			<?php bp_the_profile_field_options(); ?>
+		</select>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type's children options on the wp-admin Profile Fields,
+	 * "Add Field" and "Edit Field" screens.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
+		parent::admin_new_field_html( $current_field, 'checkbox' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-number.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-number.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-number.php	(revision 12265)
@@ -0,0 +1,120 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Number xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Number extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the number field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Number', 'xprofile field type', 'buddypress' );
+
+		$this->set_format( '/^\d+|-\d+$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Number class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Number $this Current instance of
+		 *                                            the field type number.
+		 */
+		do_action( 'bp_xprofile_field_type_number', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.number.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that certain other fields
+		// types pass to {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			unset( $raw_properties['user_id'] );
+		}
+
+		$r = bp_parse_args( $raw_properties, array(
+			'type'  => 'number',
+			'value' =>  bp_get_the_profile_field_edit_value()
+		) ); ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3">
+
+		<?php if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif; ?>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		$r = bp_parse_args( $raw_properties, array(
+			'type' => 'number'
+		) ); ?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Number field', 'buddypress' );
+		?></label>
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
+	<?php
+	}
+
+	/**
+	 * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
+	 * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-placeholder.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-placeholder.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-placeholder.php	(revision 12265)
@@ -0,0 +1,65 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.1
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * A placeholder xprofile field type. Doesn't do anything.
+ *
+ * Used if an existing field has an unknown type (e.g. one provided by a missing third-party plugin).
+ *
+ * @since 2.0.1
+ */
+class BP_XProfile_Field_Type_Placeholder extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the placeholder field type.
+	 *
+	 * @since 2.0.1
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->set_format( '/.*/', 'replace' );
+	}
+
+	/**
+	 * Prevent any HTML being output for this field type.
+	 *
+	 * @since 2.0.1
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.text.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+	}
+
+	/**
+	 * Prevent any HTML being output for this field type.
+	 *
+	 * @since 2.0.1
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+	}
+
+	/**
+	 * Prevent any HTML being output for this field type.
+	 *
+	 * @since 2.0.1
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-radiobutton.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-radiobutton.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-radiobutton.php	(revision 12265)
@@ -0,0 +1,200 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Radio button xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Radiobutton extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the radio button field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Radio Buttons', 'xprofile field type', 'buddypress' );
+
+		$this->supports_options = true;
+
+		$this->set_format( '/^.+$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Radiobutton class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Radiobutton $this Current instance of
+		 *                                                 the field type radio button.
+		 */
+		do_action( 'bp_xprofile_field_type_radiobutton', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.radio.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that we pass to
+		// {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			$user_id = (int) $raw_properties['user_id'];
+			unset( $raw_properties['user_id'] );
+		} else {
+			$user_id = bp_displayed_user_id();
+		} ?>
+
+			<legend>
+				<?php bp_the_profile_field_name(); ?>
+				<?php bp_the_profile_field_required_label(); ?>
+			</legend>
+
+			<?php if ( bp_get_the_profile_field_description() ) : ?>
+				<p class="description" tabindex="0"><?php bp_the_profile_field_description(); ?></p>
+			<?php endif; ?>
+
+			<?php
+
+			/** This action is documented in bp-xprofile/bp-xprofile-classes */
+			do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+			<?php bp_the_profile_field_options( array( 'user_id' => $user_id ) );
+
+			if ( ! bp_get_the_profile_field_is_required() ) : ?>
+
+				<a class="clear-value" href="javascript:clear( '<?php echo esc_js( bp_get_the_profile_field_input_name() ); ?>' );">
+					<?php esc_html_e( 'Clear', 'buddypress' ); ?>
+				</a>
+
+			<?php endif; ?>
+
+		<?php
+	}
+
+	/**
+	 * Output the edit field options HTML for this field type.
+	 *
+	 * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
+	 * These are stored separately in the database, and their templating is handled separately.
+	 *
+	 * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
+	 * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
+	 */
+	public function edit_field_options_html( array $args = array() ) {
+		$option_value = BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] );
+		$options      = $this->field_obj->get_children();
+
+		$html = '';
+
+		for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
+
+			// Check for updated posted values, but errors preventing them from
+			// being saved first time.
+			if ( isset( $_POST['field_' . $this->field_obj->id] ) && $option_value != $_POST['field_' . $this->field_obj->id] ) {
+				if ( ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
+					$option_value = sanitize_text_field( $_POST['field_' . $this->field_obj->id] );
+				}
+			}
+
+			// Run the allowed option name through the before_save filter, so
+			// we'll be sure to get a match.
+			$allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
+			$selected        = '';
+
+			if ( $option_value === $allowed_options || ( empty( $option_value ) && ! empty( $options[$k]->is_default_option ) ) ) {
+				$selected = ' checked="checked"';
+			}
+
+			$new_html = sprintf( '<label for="%3$s" class="option-label"><input %1$s type="radio" name="%2$s" id="%3$s" value="%4$s">%5$s</label>',
+				$selected,
+				esc_attr( bp_get_the_profile_field_input_name() ),
+				esc_attr( "option_{$options[$k]->id}" ),
+				esc_attr( stripslashes( $options[$k]->name ) ),
+				esc_html( stripslashes( $options[$k]->name ) )
+			);
+
+			/**
+			 * Filters the HTML output for an individual field options radio button.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $new_html Label and radio input field.
+			 * @param object $value    Current option being rendered for.
+			 * @param int    $id       ID of the field object being rendered.
+			 * @param string $selected Current selected value.
+			 * @param string $k        Current index in the foreach loop.
+			 */
+			$html .= apply_filters( 'bp_get_the_profile_field_options_radio', $new_html, $options[$k], $this->field_obj->id, $selected, $k );
+		}
+
+		printf( '<div id="%1$s" class="input-options radio-button-options">%2$s</div>',
+			esc_attr( 'field_' . $this->field_obj->id ),
+			$html
+		);
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		bp_the_profile_field_options();
+
+		if ( bp_get_the_profile_field_is_required() ) {
+			return;
+		} ?>
+
+		<a class="clear-value" href="javascript:clear( '<?php echo esc_js( bp_get_the_profile_field_input_name() ); ?>' );">
+			<?php esc_html_e( 'Clear', 'buddypress' ); ?>
+		</a>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
+		parent::admin_new_field_html( $current_field, 'radio' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-selectbox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-selectbox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-selectbox.php	(revision 12265)
@@ -0,0 +1,196 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Selectbox xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Selectbox extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the selectbox field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Drop Down Select Box', 'xprofile field type', 'buddypress' );
+
+		$this->supports_options = true;
+
+		$this->set_format( '/^.+$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Selectbox class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Selectbox $this Current instance of
+		 *                                               the field type select box.
+		 */
+		do_action( 'bp_xprofile_field_type_selectbox', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/select.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that we pass to
+		// {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			$user_id = (int) $raw_properties['user_id'];
+			unset( $raw_properties['user_id'] );
+		} else {
+			$user_id = bp_displayed_user_id();
+		} ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+		<select <?php echo $this->get_edit_field_html_elements( $raw_properties ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3">
+			<?php bp_the_profile_field_options( array( 'user_id' => $user_id ) ); ?>
+		</select>
+
+		<?php if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif; ?>
+
+		<?php
+	}
+
+	/**
+	 * Output the edit field options HTML for this field type.
+	 *
+	 * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
+	 * These are stored separately in the database, and their templating is handled separately.
+	 *
+	 * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
+	 * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
+	 */
+	public function edit_field_options_html( array $args = array() ) {
+		$original_option_values = maybe_unserialize( BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] ) );
+
+		$options = $this->field_obj->get_children();
+		$html    = '<option value="">' . /* translators: no option picked in select box */ esc_html__( '----', 'buddypress' ) . '</option>';
+
+		if ( empty( $original_option_values ) && !empty( $_POST['field_' . $this->field_obj->id] ) ) {
+			$original_option_values = sanitize_text_field(  $_POST['field_' . $this->field_obj->id] );
+		}
+
+		$option_values = ( $original_option_values ) ? (array) $original_option_values : array();
+		for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
+			$selected = '';
+
+			// Check for updated posted values, but errors preventing them from
+			// being saved first time.
+			foreach( $option_values as $i => $option_value ) {
+				if ( isset( $_POST['field_' . $this->field_obj->id] ) && $_POST['field_' . $this->field_obj->id] != $option_value ) {
+					if ( ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
+						$option_values[$i] = sanitize_text_field( $_POST['field_' . $this->field_obj->id] );
+					}
+				}
+			}
+
+			// Run the allowed option name through the before_save filter, so
+			// we'll be sure to get a match.
+			$allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
+
+			// First, check to see whether the user-entered value matches.
+			if ( in_array( $allowed_options, $option_values ) ) {
+				$selected = ' selected="selected"';
+			}
+
+			// Then, if the user has not provided a value, check for defaults.
+			if ( ! is_array( $original_option_values ) && empty( $option_values ) && $options[$k]->is_default_option ) {
+				$selected = ' selected="selected"';
+			}
+
+			/**
+			 * Filters the HTML output for options in a select input.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $value    Option tag for current value being rendered.
+			 * @param object $value    Current option being rendered for.
+			 * @param int    $id       ID of the field object being rendered.
+			 * @param string $selected Current selected value.
+			 * @param string $k        Current index in the foreach loop.
+			 */
+			$html .= apply_filters( 'bp_get_the_profile_field_options_select', '<option' . $selected . ' value="' . esc_attr( stripslashes( $options[$k]->name ) ) . '">' . esc_html( stripslashes( $options[$k]->name ) ) . '</option>', $options[$k], $this->field_obj->id, $selected, $k );
+		}
+
+		echo $html;
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Select', 'buddypress' );
+		?></label>
+		<select <?php echo $this->get_edit_field_html_elements( $raw_properties ); ?>>
+			<?php bp_the_profile_field_options(); ?>
+		</select>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
+		parent::admin_new_field_html( $current_field, 'radio' );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-telephone.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-telephone.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-telephone.php	(revision 12265)
@@ -0,0 +1,156 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Telephone number xprofile field type.
+ *
+ * @since 3.0.0
+ */
+class BP_XProfile_Field_Type_Telephone extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the telephone number field type.
+	 *
+	 * @since 3.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Phone Number', 'xprofile field type', 'buddypress' );
+
+		$this->set_format( '/^.*$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Telephone class.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Telephone $this Current instance of the field type.
+		 */
+		do_action( 'bp_xprofile_field_type_telephone', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.text.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+		/*
+		 * User_id is a special optional parameter that certain other fields
+		 * types pass to {@link bp_the_profile_field_options()}.
+		 */
+		if ( isset( $raw_properties['user_id'] ) ) {
+			unset( $raw_properties['user_id'] );
+		}
+
+		$r = bp_parse_args( $raw_properties, array(
+			'type'  => 'tel',
+			'value' => bp_get_the_profile_field_edit_value(),
+		) ); ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3">
+
+		<?php if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif; ?>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		$r = bp_parse_args( $raw_properties, array(
+			'type' => 'tel',
+		) ); ?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Phone Number', 'buddypress' );
+		?></label>
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
+
+		<?php
+	}
+
+	/**
+	 * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
+	 * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the
+	 *                                         current field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
+
+	/**
+	 * Format URL values for display.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string     $field_value The URL value, as saved in the database.
+	 * @param string|int $field_id    Optional. ID of the field.
+	 *
+	 * @return string URL converted to a link.
+	 */
+	public static function display_filter( $field_value, $field_id = '' ) {
+		$url   = wp_strip_all_tags( $field_value );
+		$parts = parse_url( $url );
+
+		// Add the tel:// protocol to the field value.
+		if ( isset( $parts['scheme'] ) ) {
+			if ( strtolower( $parts['scheme'] ) !== 'tel' ) {
+				$scheme = preg_quote( $parts['scheme'], '#' );
+				$url    = preg_replace( '#^' . $scheme . '#i', 'tel', $url );
+			}
+
+			$url_text = preg_replace( '#^tel://#i', '', $url );
+
+		} else {
+			$url_text = $url;
+			$url      = 'tel://' . $url;
+		}
+
+		return sprintf(
+			'<a href="%1$s" rel="nofollow">%2$s</a>',
+			esc_url( $url, array( 'tel' ) ),
+			esc_html( $url_text )
+		);
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-textarea.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-textarea.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-textarea.php	(revision 12265)
@@ -0,0 +1,177 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Textarea xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Textarea extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the textarea field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Multi-line Text Area', 'xprofile field type', 'buddypress' );
+		$this->supports_richtext = true;
+
+		$this->set_format( '/^.*$/m', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Textarea class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Textarea $this Current instance of
+		 *                                              the field type textarea.
+		 */
+		do_action( 'bp_xprofile_field_type_textarea', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/textarea.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that certain other fields
+		// types pass to {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			unset( $raw_properties['user_id'] );
+		}
+
+		$richtext_enabled = bp_xprofile_is_richtext_enabled_for_field(); ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() );
+
+		if ( ! $richtext_enabled ) {
+			$r = wp_parse_args( $raw_properties, array(
+				'cols' => 40,
+				'rows' => 5,
+			) );
+
+			?>
+
+			<textarea <?php echo $this->get_edit_field_html_elements( $r ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_edit_value(); ?></textarea>
+
+			<?php
+
+		} else {
+
+			/**
+			 * Filters the arguments passed to `wp_editor()` in richtext xprofile fields.
+			 *
+			 * @since 2.4.0
+			 *
+			 * @param array $args {
+			 *     Array of optional arguments. See `wp_editor()`.
+			 *     @type bool $teeny         Whether to use the teeny version of TinyMCE. Default true.
+			 *     @type bool $media_buttons Whether to show media buttons. Default false.
+			 *     @type bool $quicktags     Whether to show the quicktags buttons. Default true.
+			 *     @type int  $textarea_rows Number of rows to display in the editor. Defaults to 1 in the
+			 *                               'admin' context, and 10 in the 'edit' context.
+			 * }
+			 * @param string $context The display context. 'edit' when the markup is intended for the
+			 *                        profile edit screen, 'admin' when intended for the Profile Fields
+			 *                        Dashboard panel.
+			 */
+			$editor_args = apply_filters( 'bp_xprofile_field_type_textarea_editor_args', array(
+				'teeny'         => true,
+				'media_buttons' => false,
+				'quicktags'     => true,
+				'textarea_rows' => 10,
+			), 'edit' );
+
+			wp_editor(
+				bp_get_the_profile_field_edit_value(),
+				bp_get_the_profile_field_input_name(),
+				$editor_args
+			);
+		}
+
+		if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif;
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+		$richtext_enabled = bp_xprofile_is_richtext_enabled_for_field();
+
+		if ( ! $richtext_enabled ) {
+
+			$r = bp_parse_args( $raw_properties, array(
+				'cols' => 40,
+				'rows' => 5,
+			) ); ?>
+
+			<textarea <?php echo $this->get_edit_field_html_elements( $r ); ?>></textarea>
+
+			<?php
+		} else {
+
+			/** This filter is documented in bp-xprofile/classes/class-bp-xprofile-field-type-textarea.php */
+			$editor_args = apply_filters( 'bp_xprofile_field_type_textarea_editor_args', array(
+				'teeny'         => true,
+				'media_buttons' => false,
+				'quicktags'     => true,
+				'textarea_rows' => 1,
+			), 'admin' );
+
+			wp_editor(
+				'',
+				'xprofile_textarea_' . bp_get_the_profile_field_id(),
+				$editor_args
+			);
+		}
+	}
+
+	/**
+	 * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
+	 * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-textbox.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-textbox.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-textbox.php	(revision 12265)
@@ -0,0 +1,121 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Textbox xprofile field type.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_Field_Type_Textbox extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the textbox field type.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'Text Box', 'xprofile field type', 'buddypress' );
+
+		$this->set_format( '/^.*$/', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_Textbox class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_Textbox $this Current instance of
+		 *                                             the field type text box.
+		 */
+		do_action( 'bp_xprofile_field_type_textbox', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.text.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// User_id is a special optional parameter that certain other fields
+		// types pass to {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			unset( $raw_properties['user_id'] );
+		}
+
+		$r = bp_parse_args( $raw_properties, array(
+			'type'  => 'text',
+			'value' => bp_get_the_profile_field_edit_value(),
+		) ); ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3">
+
+		<?php if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif; ?>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+
+		$r = bp_parse_args( $raw_properties, array(
+			'type' => 'text'
+		) ); ?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'Textbox', 'buddypress' );
+		?></label>
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
+
+		<?php
+	}
+
+	/**
+	 * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
+	 * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the
+	 *                                         current field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-url.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-url.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type-url.php	(revision 12265)
@@ -0,0 +1,178 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.1.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * URL xprofile field type.
+ *
+ * @since 2.1.0
+ */
+class BP_XProfile_Field_Type_URL extends BP_XProfile_Field_Type {
+
+	/**
+	 * Constructor for the URL field type
+	 *
+	 * @since 2.1.0
+	 */
+	public function __construct() {
+		parent::__construct();
+
+		$this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
+		$this->name     = _x( 'URL', 'xprofile field type', 'buddypress' );
+
+		$this->set_format( '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS', 'replace' );
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type_URL class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type_URL $this Current instance of
+		 *                                         the field type URL.
+		 */
+		do_action( 'bp_xprofile_field_type_url', $this );
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $raw_properties Optional key/value array of
+	 *                              {@link http://dev.w3.org/html5/markup/input.number.html permitted attributes}
+	 *                              that you want to add.
+	 */
+	public function edit_field_html( array $raw_properties = array() ) {
+
+		// `user_id` is a special optional parameter that certain other
+		// fields types pass to {@link bp_the_profile_field_options()}.
+		if ( isset( $raw_properties['user_id'] ) ) {
+			unset( $raw_properties['user_id'] );
+		}
+
+		$r = bp_parse_args( $raw_properties, array(
+			'type'      => 'text',
+			'inputmode' => 'url',
+			'value'     => esc_url( bp_get_the_profile_field_edit_value() ),
+		) ); ?>
+
+		<legend id="<?php bp_the_profile_field_input_name(); ?>-1">
+			<?php bp_the_profile_field_name(); ?>
+			<?php bp_the_profile_field_required_label(); ?>
+		</legend>
+
+		<?php
+
+		/** This action is documented in bp-xprofile/bp-xprofile-classes */
+		do_action( bp_get_the_profile_field_errors_action() ); ?>
+
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?> aria-labelledby="<?php bp_the_profile_field_input_name(); ?>-1" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-3">
+
+		<?php if ( bp_get_the_profile_field_description() ) : ?>
+			<p class="description" id="<?php bp_the_profile_field_input_name(); ?>-3"><?php bp_the_profile_field_description(); ?></p>
+		<?php endif; ?>
+
+		<?php
+	}
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted
+	 *                              attributes that you want to add.
+	 */
+	public function admin_field_html( array $raw_properties = array() ) {
+
+		$r = bp_parse_args( $raw_properties, array(
+			'type' => 'url'
+		) ); ?>
+
+		<label for="<?php bp_the_profile_field_input_name(); ?>" class="screen-reader-text"><?php
+			/* translators: accessibility text */
+			esc_html_e( 'URL', 'buddypress' );
+		?></label>
+		<input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
+
+		<?php
+	}
+
+	/**
+	 * This method usually outputs HTML for this field type's children options
+	 * on the wp-admin Profile Fields "Add Field" and "Edit Field" screens, but
+	 * for this field type, we don't want it, so it's stubbed out.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                                         field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
+
+	/**
+	 * Modify submitted URL values before validation.
+	 *
+	 * The URL validation regex requires a http(s) protocol, so that all
+	 * values saved in the database are fully-formed URLs. However, we
+	 * still want to allow users to enter URLs without a protocol, for a
+	 * better user experience. So we catch submitted URL values, and if
+	 * the protocol is missing, we prepend 'http://' before passing to
+	 * is_valid().
+	 *
+	 * @since 2.1.0
+	 * @since 2.4.0 Added the `$field_id` parameter.
+	 *
+	 * @param string     $submitted_value Raw value submitted by the user.
+	 * @param string|int $field_id        Optional. ID of the field.
+	 * @return string
+	 */
+	public static function pre_validate_filter( $submitted_value = '', $field_id = '' ) {
+
+		// Allow empty URL values.
+		if ( empty( $submitted_value ) ) {
+			return '';
+		}
+
+		// Run some checks on the submitted value.
+		if ( false === strpos( $submitted_value, ':'  ) &&
+			substr( $submitted_value, 0, 1 ) !== '/' &&
+			substr( $submitted_value, 0, 1 ) !== '#' &&
+			! preg_match( '/^[a-z0-9-]+?\.php/i', $submitted_value )
+		) {
+			$submitted_value = 'http://' . $submitted_value;
+		}
+
+		return $submitted_value;
+	}
+
+	/**
+	 * Format URL values for display.
+	 *
+	 * @since 2.1.0
+	 * @since 2.4.0 Added the `$field_id` parameter.
+	 *
+	 * @param string     $field_value The URL value, as saved in the database.
+	 * @param string|int $field_id    Optional. ID of the field.
+	 * @return string URL converted to a link.
+	 */
+	public static function display_filter( $field_value, $field_id = '' ) {
+		$link      = strip_tags( $field_value );
+		$no_scheme = preg_replace( '#^https?://#', '', rtrim( $link, '/' ) );
+		$url_text  = str_replace( $link, $no_scheme, $field_value );
+		return '<a href="' . esc_url( $field_value ) . '" rel="nofollow">' . esc_html( $url_text ) . '</a>';
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field-type.php	(revision 12265)
@@ -0,0 +1,532 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Represents a type of XProfile field and holds meta information about the type of value that it accepts.
+ *
+ * @since 2.0.0
+ */
+abstract class BP_XProfile_Field_Type {
+
+	/**
+	 * Validation regex rules for field type.
+	 *
+	 * @since 2.0.0
+	 * @var array Field type validation regexes.
+	 */
+	protected $validation_regex = array();
+
+	/**
+	 * Whitelisted values for field type.
+	 *
+	 * @since 2.0.0
+	 * @var array Field type whitelisted values.
+	 */
+	protected $validation_whitelist = array();
+
+	/**
+	 * Name for field type.
+	 *
+	 * @since 2.0.0
+	 * @var string The name of this field type.
+	 */
+	public $name = '';
+
+	/**
+	 * The name of the category that this field type should be grouped with. Used on the [Users > Profile Fields] screen in wp-admin.
+	 *
+	 * @since 2.0.0
+	 * @var string
+	 */
+	public $category = '';
+
+	/**
+	 * If allowed to store null/empty values.
+	 *
+	 * @since 2.0.0
+	 * @var bool If this is set, allow BP to store null/empty values for this field type.
+	 */
+	public $accepts_null_value = false;
+
+	/**
+	 * If this is set, BP will set this field type's validation whitelist from the field's options (e.g checkbox, selectbox).
+	 *
+	 * @since 2.0.0
+	 * @var bool Does this field support options? e.g. selectbox, radio buttons, etc.
+	 */
+	public $supports_options = false;
+
+	/**
+	 * If allowed to support multiple options as default.
+	 *
+	 * @since 2.0.0
+	 * @var bool Does this field type support multiple options being set as default values? e.g. multiselectbox, checkbox.
+	 */
+	public $supports_multiple_defaults = false;
+
+	/**
+	 * If the field type supports rich text by default.
+	 *
+	 * @since 2.4.0
+	 * @var bool
+	 */
+	public $supports_richtext = false;
+
+	/**
+	 * If the field type has a type-specific settings section on the Edit Field panel.
+	 *
+	 * @since 2.7.0
+	 * @var bool|null Boolean if set explicitly by the type object, otherwise null.
+	 */
+	protected $do_settings_section = null;
+
+	/**
+	 * If object is created by an BP_XProfile_Field object.
+	 *
+	 * @since 2.0.0
+	 * @var BP_XProfile_Field If this object is created by instantiating a {@link BP_XProfile_Field},
+	 *                        this is a reference back to that object.
+	 */
+	public $field_obj = null;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+
+		/**
+		 * Fires inside __construct() method for BP_XProfile_Field_Type class.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Field_Type $this Current instance of
+		 *                                     the field type class.
+		 */
+		do_action( 'bp_xprofile_field_type', $this );
+	}
+
+	/**
+	 * Set a regex that profile data will be asserted against.
+	 *
+	 * You can call this method multiple times to set multiple formats. When validation is performed,
+	 * it's successful as long as the new value matches any one of the registered formats.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $format         Regex string.
+	 * @param string $replace_format Optional; if 'replace', replaces the format instead of adding to it.
+	 *                               Defaults to 'add'.
+	 * @return BP_XProfile_Field_Type
+	 */
+	public function set_format( $format, $replace_format = 'add' ) {
+
+		/**
+		 * Filters the regex format for the field type.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param string                 $format         Regex string.
+		 * @param string                 $replace_format Optional replace format If "replace", replaces the
+		 *                                               format instead of adding to it. Defaults to "add".
+		 * @param BP_XProfile_Field_Type $this           Current instance of the BP_XProfile_Field_Type class.
+		 */
+		$format = apply_filters( 'bp_xprofile_field_type_set_format', $format, $replace_format, $this );
+
+		if ( 'add' === $replace_format ) {
+			$this->validation_regex[] = $format;
+		} elseif ( 'replace' === $replace_format ) {
+			$this->validation_regex = array( $format );
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Add a value to this type's whitelist that profile data will be asserted against.
+	 *
+	 * You can call this method multiple times to set multiple formats. When validation is performed,
+	 * it's successful as long as the new value matches any one of the registered formats.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string|array $values Whitelisted values.
+	 * @return BP_XProfile_Field_Type
+	 */
+	public function set_whitelist_values( $values ) {
+		foreach ( (array) $values as $value ) {
+
+			/**
+			 * Filters values for field type's whitelist that profile data will be asserted against.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param string                 $value  Field value.
+			 * @param array                  $values Original array of values.
+			 * @param BP_XProfile_Field_Type $this   Current instance of the BP_XProfile_Field_Type class.
+			 */
+			$this->validation_whitelist[] = apply_filters( 'bp_xprofile_field_type_set_whitelist_values', $value, $values, $this );
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Check the given string against the registered formats for this field type.
+	 *
+	 * This method doesn't support chaining.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string|array $values Value to check against the registered formats.
+	 * @return bool True if the value validates
+	 */
+	public function is_valid( $values ) {
+		$validated = false;
+
+		// Some types of field (e.g. multi-selectbox) may have multiple values to check.
+		foreach ( (array) $values as $value ) {
+
+			// Validate the $value against the type's accepted format(s).
+			foreach ( $this->validation_regex as $format ) {
+				if ( 1 === preg_match( $format, $value ) ) {
+					$validated = true;
+					continue;
+
+				} else {
+					$validated = false;
+				}
+			}
+		}
+
+		// Handle field types with accepts_null_value set if $values is an empty array.
+		if ( ( false === $validated ) && is_array( $values ) && empty( $values ) && $this->accepts_null_value ) {
+			$validated = true;
+		}
+
+		// If there's a whitelist set, make sure that each value is a whitelisted value.
+		if ( ( true === $validated ) && ! empty( $values ) && ! empty( $this->validation_whitelist ) ) {
+			foreach ( (array) $values as $value ) {
+				if ( ! in_array( $value, $this->validation_whitelist, true ) ) {
+					$validated = false;
+					break;
+				}
+			}
+		}
+
+		/**
+		 * Filters whether or not field type is a valid format.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param bool                   $validated Whether or not the field type is valid.
+		 * @param string|array           $values    Value to check against the registered formats.
+		 * @param BP_XProfile_Field_Type $this      Current instance of the BP_XProfile_Field_Type class.
+		 */
+		return (bool) apply_filters( 'bp_xprofile_field_type_is_valid', $validated, $values, $this );
+	}
+
+	/**
+	 * Check whether the current field type should have a settings ("options") section on the Edit Field panel.
+	 *
+	 * Falls back on `supports_options` if no value is set by the field type.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @return bool
+	 */
+	public function do_settings_section() {
+		if ( null === $this->do_settings_section ) {
+			$this->do_settings_section = $this->supports_options;
+		}
+
+		return (bool) $this->do_settings_section;
+	}
+
+	/**
+	 * Output the edit field HTML for this field type.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 * @return void
+	 */
+	abstract public function edit_field_html( array $raw_properties = array() );
+
+	/**
+	 * Output HTML for this field type on the wp-admin Profile Fields screen.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
+	 * @return void
+	 */
+	abstract public function admin_field_html( array $raw_properties = array() );
+
+	/**
+	 * Output the edit field options HTML for this field type.
+	 *
+	 * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
+	 * These are stored separately in the database, and their templating is handled separately.
+	 * Populate this method in a child class if it's required. Otherwise, you can leave it out.
+	 *
+	 * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
+	 * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
+	 */
+	public function edit_field_options_html( array $args = array() ) {}
+
+	/**
+	 * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
+	 *
+	 * You don't need to implement this method for all field types. It's used in core by the
+	 * selectbox, multi selectbox, checkbox, and radio button fields, to allow the admin to
+	 * enter the child option values (e.g. the choices in a select box).
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
+	 * @param string            $control_type  Optional. HTML input type used to render the current
+	 *                          field's child options.
+	 */
+	public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
+		$type = array_search( get_class( $this ), bp_xprofile_get_field_types() );
+		if ( false === $type ) {
+			return;
+		}
+
+		$class            = $current_field->type != $type ? 'display: none;' : '';
+		$current_type_obj = bp_xprofile_create_field_type( $type );
+		?>
+
+		<div id="<?php echo esc_attr( $type ); ?>" class="postbox bp-options-box" style="<?php echo esc_attr( $class ); ?> margin-top: 15px;">
+			<h3><?php esc_html_e( 'Please enter options for this Field:', 'buddypress' ); ?></h3>
+			<div class="inside" aria-live="polite" aria-atomic="true" aria-relevant="all">
+				<p>
+					<label for="sort_order_<?php echo esc_attr( $type ); ?>"><?php esc_html_e( 'Sort Order:', 'buddypress' ); ?></label>
+					<select name="sort_order_<?php echo esc_attr( $type ); ?>" id="sort_order_<?php echo esc_attr( $type ); ?>" >
+						<option value="custom" <?php selected( 'custom', $current_field->order_by ); ?>><?php esc_html_e( 'Custom',     'buddypress' ); ?></option>
+						<option value="asc"    <?php selected( 'asc',    $current_field->order_by ); ?>><?php esc_html_e( 'Ascending',  'buddypress' ); ?></option>
+						<option value="desc"   <?php selected( 'desc',   $current_field->order_by ); ?>><?php esc_html_e( 'Descending', 'buddypress' ); ?></option>
+					</select>
+				</p>
+
+				<?php
+
+				// Does option have children?
+				$options = $current_field->get_children( true );
+
+				// If no children options exists for this field, check in $_POST
+				// for a submitted form (e.g. on the "new field" screen).
+				if ( empty( $options ) ) {
+
+					$options = array();
+					$i       = 1;
+
+					while ( isset( $_POST[$type . '_option'][$i] ) ) {
+
+						// Multiselectbox and checkboxes support MULTIPLE default options; all other core types support only ONE.
+						if ( $current_type_obj->supports_options && ! $current_type_obj->supports_multiple_defaults && isset( $_POST["isDefault_{$type}_option"][$i] ) && (int) $_POST["isDefault_{$type}_option"] === $i ) {
+							$is_default_option = true;
+						} elseif ( isset( $_POST["isDefault_{$type}_option"][$i] ) ) {
+							$is_default_option = (bool) $_POST["isDefault_{$type}_option"][$i];
+						} else {
+							$is_default_option = false;
+						}
+
+						// Grab the values from $_POST to use as the form's options.
+						$options[] = (object) array(
+							'id'                => -1,
+							'is_default_option' => $is_default_option,
+							'name'              => sanitize_text_field( stripslashes( $_POST[$type . '_option'][$i] ) ),
+						);
+
+						++$i;
+					}
+
+					// If there are still no children options set, this must be the "new field" screen, so add one new/empty option.
+					if ( empty( $options ) ) {
+						$options[] = (object) array(
+							'id'                => -1,
+							'is_default_option' => false,
+							'name'              => '',
+						);
+					}
+				}
+
+				// Render the markup for the children options.
+				if ( ! empty( $options ) ) {
+					$default_name = '';
+
+					for ( $i = 0, $count = count( $options ); $i < $count; ++$i ) :
+						$j = $i + 1;
+
+						// Multiselectbox and checkboxes support MULTIPLE default options; all other core types support only ONE.
+						if ( $current_type_obj->supports_options && $current_type_obj->supports_multiple_defaults ) {
+							$default_name = '[' . $j . ']';
+						}
+						?>
+
+						<div id="<?php echo esc_attr( "{$type}_div{$j}" ); ?>" class="bp-option sortable">
+							<span class="bp-option-icon grabber"></span>
+							<label for="<?php echo esc_attr( "{$type}_option{$j}" ); ?>" class="screen-reader-text"><?php
+								/* translators: accessibility text */
+								esc_html_e( 'Add an option', 'buddypress' );
+							?></label>
+							<input type="text" name="<?php echo esc_attr( "{$type}_option[{$j}]" ); ?>" id="<?php echo esc_attr( "{$type}_option{$j}" ); ?>" value="<?php echo esc_attr( stripslashes( $options[$i]->name ) ); ?>" />
+							<label for="<?php echo esc_attr( "{$type}_option{$default_name}" ); ?>">
+								<input type="<?php echo esc_attr( $control_type ); ?>" id="<?php echo esc_attr( "{$type}_option{$default_name}" ); ?>" name="<?php echo esc_attr( "isDefault_{$type}_option{$default_name}" ); ?>" <?php checked( $options[$i]->is_default_option, true ); ?> value="<?php echo esc_attr( $j ); ?>" />
+								<?php _e( 'Default Value', 'buddypress' ); ?>
+							</label>
+
+							<?php if ( 1 !== $j ) : ?>
+								<div class ="delete-button">
+									<a href='javascript:hide("<?php echo esc_attr( "{$type}_div{$j}" ); ?>")' class="delete"><?php esc_html_e( 'Delete', 'buddypress' ); ?></a>
+								</div>
+							<?php endif; ?>
+
+						</div>
+
+					<?php endfor; ?>
+
+					<input type="hidden" name="<?php echo esc_attr( "{$type}_option_number" ); ?>" id="<?php echo esc_attr( "{$type}_option_number" ); ?>" value="<?php echo esc_attr( $j + 1 ); ?>" />
+				<?php } ?>
+
+				<div id="<?php echo esc_attr( "{$type}_more" ); ?>"></div>
+				<p><a href="javascript:add_option('<?php echo esc_js( $type ); ?>')"><?php esc_html_e( 'Add Another Option', 'buddypress' ); ?></a></p>
+
+				<?php
+
+				/**
+				 * Fires at the end of the new field additional settings area.
+				 *
+				 * @since 2.3.0
+				 *
+				 * @param BP_XProfile_Field $current_field Current field being rendered.
+				 */
+				do_action( 'bp_xprofile_admin_new_field_additional_settings', $current_field ) ?>
+			</div>
+		</div>
+
+		<?php
+	}
+
+	/**
+	 * Allow field types to modify submitted values before they are validated.
+	 *
+	 * In some cases, it may be appropriate for a field type to catch
+	 * submitted values and modify them before they are passed to the
+	 * is_valid() method. For example, URL validation requires the
+	 * 'http://' scheme (so that the value saved in the database is always
+	 * a fully-formed URL), but in order to allow users to enter a URL
+	 * without this scheme, BP_XProfile_Field_Type_URL prepends 'http://'
+	 * when it's not present.
+	 *
+	 * By default, this is a pass-through method that does nothing. Only
+	 * override in your own field type if you need this kind of pre-
+	 * validation filtering.
+	 *
+	 * @since 2.1.0
+	 * @since 2.4.0 Added the `$field_id` parameter.
+	 *
+	 * @param mixed      $field_value Submitted field value.
+	 * @param string|int $field_id    Optional. ID of the field.
+	 * @return mixed
+	 */
+	public static function pre_validate_filter( $field_value, $field_id = '' ) {
+		return $field_value;
+	}
+
+	/**
+	 * Allow field types to modify the appearance of their values.
+	 *
+	 * By default, this is a pass-through method that does nothing. Only
+	 * override in your own field type if you need to provide custom
+	 * filtering for output values.
+	 *
+	 * @since 2.1.0
+	 * @since 2.4.0 Added `$field_id` parameter.
+	 *
+	 * @param mixed      $field_value Field value.
+	 * @param string|int $field_id    ID of the field.
+	 * @return mixed
+	 */
+	public static function display_filter( $field_value, $field_id = '' ) {
+		return $field_value;
+	}
+
+	/**
+	 * Save miscellaneous settings related to this field type.
+	 *
+	 * Override in a specific field type if it requires an admin save routine.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int   $field_id Field ID.
+	 * @param array $settings Array of settings.
+	 */
+	public function admin_save_settings( $field_id, $settings ) {}
+
+	/** Protected *************************************************************/
+
+	/**
+	 * Get a sanitised and escaped string of the edit field's HTML elements and attributes.
+	 *
+	 * Must be used inside the {@link bp_profile_fields()} template loop.
+	 * This method was intended to be static but couldn't be because php.net/lsb/ requires PHP >= 5.3.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $properties Optional key/value array of attributes for this edit field.
+	 * @return string
+	 */
+	protected function get_edit_field_html_elements( array $properties = array() ) {
+
+		$r = bp_parse_args( $properties, array(
+			'id'   => bp_get_the_profile_field_input_name(),
+			'name' => bp_get_the_profile_field_input_name(),
+		) );
+
+		if ( bp_get_the_profile_field_is_required() ) {
+			$r['aria-required'] = 'true';
+
+			// Moderators can bypass field requirements.
+			if ( ! bp_current_user_can( 'bp_moderate' ) ) {
+				$r[] = 'required';
+			}
+		}
+
+		/**
+		 * Filters the edit html elements and attributes.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param array  $r     Array of parsed arguments.
+		 * @param string $value Class name for the current class instance.
+		 */
+		$r = (array) apply_filters( 'bp_xprofile_field_edit_html_elements', $r, get_class( $this ) );
+
+		return bp_get_form_field_attributes( sanitize_key( bp_get_the_profile_field_name() ), $r );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-field.php	(revision 12265)
@@ -0,0 +1,1644 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class to help set up XProfile fields.
+ *
+ * @since 1.0.0
+ */
+class BP_XProfile_Field {
+
+	/**
+	 * Field ID.
+	 *
+	 * @since 1.0.0
+	 * @var int ID of field.
+	 */
+	public $id;
+
+	/**
+	 * Field group ID.
+	 *
+	 * @since 1.0.0
+	 * @var int Field group ID for field.
+	 */
+	public $group_id;
+
+	/**
+	 * Field parent ID.
+	 *
+	 * @since 1.0.0
+	 * @var int Parent ID of field.
+	 */
+	public $parent_id;
+
+	/**
+	 * Field type.
+	 *
+	 * @since 1.0.0
+	 * @var string Field type.
+	 */
+	public $type;
+
+	/**
+	 * Field name.
+	 *
+	 * @since 1.0.0
+	 * @var string Field name.
+	 */
+	public $name;
+
+	/**
+	 * Field description.
+	 *
+	 * @since 1.0.0
+	 * @var string Field description.
+	 */
+	public $description;
+
+	/**
+	 * Required field?
+	 *
+	 * @since 1.0.0
+	 * @var bool Is field required to be filled out?
+	 */
+	public $is_required;
+
+	/**
+	 * Deletable field?
+	 *
+	 * @since 1.0.0
+	 * @var int Can field be deleted?
+	 */
+	public $can_delete = '1';
+
+	/**
+	 * Field position.
+	 *
+	 * @since 1.0.0
+	 * @var int Field position.
+	 */
+	public $field_order;
+
+	/**
+	 * Option order.
+	 *
+	 * @since 1.0.0
+	 * @var int Option order.
+	 */
+	public $option_order;
+
+	/**
+	 * Order child fields.
+	 *
+	 * @since 1.0.0
+	 * @var string Order child fields by.
+	 */
+	public $order_by;
+
+	/**
+	 * Is this the default option?
+	 *
+	 * @since 1.0.0
+	 * @var bool Is this the default option for this field?
+	 */
+	public $is_default_option;
+
+	/**
+	 * Field data visibility.
+	 *
+	 * @since 1.9.0
+	 * @since 2.4.0 Property marked protected. Now accessible by magic method or by `get_default_visibility()`.
+	 * @var string Default field data visibility.
+	 */
+	protected $default_visibility;
+
+	/**
+	 * Is the visibility able to be modified?
+	 *
+	 * @since 2.3.0
+	 * @since 2.4.0 Property marked protected. Now accessible by magic method or by `get_allow_custom_visibility()`.
+	 * @var string Members are allowed/disallowed to modify data visibility.
+	 */
+	protected $allow_custom_visibility;
+
+	/**
+	 * Whether values from this field are autolinked to directory searches.
+	 *
+	 * @since 2.5.0
+	 * @var bool
+	 */
+	public $do_autolink;
+
+	/**
+	 * Field type option.
+	 *
+	 * @since 2.0.0
+	 * @var BP_XProfile_Field_Type Field type object used for validation.
+	 */
+	public $type_obj = null;
+
+	/**
+	 * Field data for user ID.
+	 *
+	 * @since 2.0.0
+	 * @var BP_XProfile_ProfileData Field data for user ID.
+	 */
+	public $data;
+
+	/**
+	 * Member types to which the profile field should be applied.
+	 *
+	 * @since 2.4.0
+	 * @var array Array of member types.
+	 */
+	protected $member_types;
+
+	/**
+	 * Initialize and/or populate profile field.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param int|null $id Field ID.
+	 * @param int|null $user_id User ID.
+	 * @param bool     $get_data Get data.
+	 */
+	public function __construct( $id = null, $user_id = null, $get_data = true ) {
+
+		if ( ! empty( $id ) ) {
+			$this->populate( $id, $user_id, $get_data );
+
+		// Initialise the type obj to prevent fatals when creating new profile fields.
+		} else {
+			$this->type_obj            = bp_xprofile_create_field_type( 'textbox' );
+			$this->type_obj->field_obj = $this;
+		}
+	}
+
+	/**
+	 * Populate a profile field object.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $wpdb
+	 * @global object $userdata
+	 *
+	 * @param int      $id Field ID.
+	 * @param int|null $user_id User ID.
+	 * @param bool     $get_data Get data.
+	 */
+	public function populate( $id, $user_id = null, $get_data = true ) {
+		global $wpdb, $userdata;
+
+		if ( empty( $user_id ) ) {
+			$user_id = isset( $userdata->ID ) ? $userdata->ID : 0;
+		}
+
+		$field = wp_cache_get( $id, 'bp_xprofile_fields' );
+		if ( false === $field ) {
+			$bp = buddypress();
+
+			$field = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_fields} WHERE id = %d", $id ) );
+
+			if ( ! $field ) {
+				return false;
+			}
+
+			wp_cache_add( $id, $field, 'bp_xprofile_fields' );
+		}
+
+		$this->fill_data( $field );
+
+		if ( ! empty( $get_data ) && ! empty( $user_id ) ) {
+			$this->data = $this->get_field_data( $user_id );
+		}
+	}
+
+	/**
+	 * Retrieve a `BP_XProfile_Field` instance.
+	 *
+	 * @since 2.4.0
+	 * @since 2.8.0 Added `$user_id` and `$get_data` parameters.
+	 *
+	 * @static
+	 *
+	 * @param int      $field_id ID of the field.
+	 * @param int|null $user_id  Optional. ID of the user associated with the field.
+	 *                           Ignored if `$get_data` is false. If `$get_data` is
+	 *                           true, but no `$user_id` is provided, defaults to
+	 *                           logged-in user ID.
+	 * @param bool     $get_data Whether to fetch data for the specified `$user_id`.
+	 * @return BP_XProfile_Field|false Field object if found, otherwise false.
+	 */
+	public static function get_instance( $field_id, $user_id = null, $get_data = true ) {
+		global $wpdb;
+
+		$field_id = (int) $field_id;
+		if ( ! $field_id ) {
+			return false;
+		}
+
+		return new self( $field_id, $user_id, $get_data );
+	}
+
+	/**
+	 * Fill object vars based on data passed to the method.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array|object $args Array or object representing the `BP_XProfile_Field` properties.
+	 *                           Generally, this is a row from the fields database table.
+	 */
+	public function fill_data( $args ) {
+		if ( is_object( $args ) ) {
+			$args = (array) $args;
+		}
+
+		$int_fields = array(
+			'id', 'is_required', 'group_id', 'parent_id', 'is_default_option',
+			'field_order', 'option_order', 'can_delete'
+		);
+
+		foreach ( $args as $k => $v ) {
+			if ( 'name' === $k || 'description' === $k ) {
+				$v = stripslashes( $v );
+			}
+
+			// Cast numeric strings as integers.
+			if ( true === in_array( $k, $int_fields ) ) {
+				$v = (int) $v;
+			}
+
+			$this->{$k} = $v;
+		}
+
+		// Create the field type and store a reference back to this object.
+		$this->type_obj            = bp_xprofile_create_field_type( $this->type );
+		$this->type_obj->field_obj = $this;
+	}
+
+	/**
+	 * Magic getter.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $key Property name.
+	 * @return string|null
+	 */
+	public function __get( $key ) {
+		switch ( $key ) {
+			case 'default_visibility' :
+				return $this->get_default_visibility();
+				break;
+
+			case 'allow_custom_visibility' :
+				return $this->get_allow_custom_visibility();
+				break;
+		}
+	}
+
+	/**
+	 * Magic issetter.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $key Property name.
+	 * @return bool
+	 */
+	public function __isset( $key ) {
+		switch ( $key ) {
+			// Backward compatibility for when these were public methods.
+			case 'allow_custom_visibility' :
+			case 'default_visibility' :
+				return true;
+				break;
+		}
+	}
+
+	/**
+	 * Delete a profile field.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @param boolean $delete_data Whether or not to delete data.
+	 * @return boolean
+	 */
+	public function delete( $delete_data = false ) {
+		global $wpdb;
+
+		// Prevent deletion if no ID is present.
+		// Prevent deletion by url when can_delete is false.
+		// Prevent deletion of option 1 since this invalidates fields with options.
+		if ( empty( $this->id ) || empty( $this->can_delete ) || ( $this->parent_id && $this->option_order == 1 ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires before the current field instance gets deleted.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param BP_XProfile_Field $this        Current instance of the field being deleted. Passed by reference.
+		 * @param bool              $delete_data Whether or not to delete data.
+		 */
+		do_action_ref_array( 'xprofile_field_before_delete', array( &$this, $delete_data ) );
+
+		$bp  = buddypress();
+		$sql = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE id = %d OR parent_id = %d", $this->id, $this->id );
+
+		if ( ! $wpdb->query( $sql ) ) {
+			return false;
+		}
+
+		// Delete all metadata for this field.
+		bp_xprofile_delete_meta( $this->id, 'field' );
+
+		// Delete the data in the DB for this field.
+		if ( true === $delete_data ) {
+			BP_XProfile_ProfileData::delete_for_field( $this->id );
+		}
+
+		/**
+		 * Fires after the current field instance gets deleted.
+		 *
+		 * @since 3.0.0
+		 *
+		 * @param BP_XProfile_Field $this        Current instance of the field being deleted. Passed by reference.
+		 * @param bool              $delete_data Whether or not to delete data.
+		 */
+		do_action_ref_array( 'xprofile_field_after_delete', array( &$this, $delete_data ) );
+
+		return true;
+	}
+
+	/**
+	 * Save a profile field.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @return boolean
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$this->group_id     = apply_filters( 'xprofile_field_group_id_before_save',     $this->group_id,     $this->id );
+		$this->parent_id    = apply_filters( 'xprofile_field_parent_id_before_save',    $this->parent_id,    $this->id );
+		$this->type         = apply_filters( 'xprofile_field_type_before_save',         $this->type,         $this->id );
+		$this->name         = apply_filters( 'xprofile_field_name_before_save',         $this->name,         $this->id );
+		$this->description  = apply_filters( 'xprofile_field_description_before_save',  $this->description,  $this->id );
+		$this->is_required  = apply_filters( 'xprofile_field_is_required_before_save',  $this->is_required,  $this->id );
+		$this->order_by	    = apply_filters( 'xprofile_field_order_by_before_save',     $this->order_by,     $this->id );
+		$this->field_order  = apply_filters( 'xprofile_field_field_order_before_save',  $this->field_order,  $this->id );
+		$this->option_order = apply_filters( 'xprofile_field_option_order_before_save', $this->option_order, $this->id );
+		$this->can_delete   = apply_filters( 'xprofile_field_can_delete_before_save',   $this->can_delete,   $this->id );
+		$this->type_obj     = bp_xprofile_create_field_type( $this->type );
+
+		/**
+		 * Fires before the current field instance gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_XProfile_Field $this Current instance of the field being saved.
+		 */
+		do_action_ref_array( 'xprofile_field_before_save', array( $this ) );
+
+		$is_new_field = is_null( $this->id );
+
+		if ( ! $is_new_field ) {
+			$sql = $wpdb->prepare( "UPDATE {$bp->profile->table_name_fields} SET group_id = %d, parent_id = 0, type = %s, name = %s, description = %s, is_required = %d, order_by = %s, field_order = %d, option_order = %d, can_delete = %d, is_default_option = %d WHERE id = %d", $this->group_id, $this->type, $this->name, $this->description, $this->is_required, $this->order_by, $this->field_order, $this->option_order, $this->can_delete, $this->is_default_option, $this->id );
+		} else {
+			$sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_fields} (group_id, parent_id, type, name, description, is_required, order_by, field_order, option_order, can_delete, is_default_option ) VALUES ( %d, %d, %s, %s, %s, %d, %s, %d, %d, %d, %d )", $this->group_id, $this->parent_id, $this->type, $this->name, $this->description, $this->is_required, $this->order_by, $this->field_order, $this->option_order, $this->can_delete, $this->is_default_option );
+		}
+
+		/**
+		 * Check for null so field options can be changed without changing any
+		 * other part of the field. The described situation will return 0 here.
+		 */
+		if ( $wpdb->query( $sql ) !== null ) {
+
+			if ( $is_new_field ) {
+				$this->id = $wpdb->insert_id;
+			}
+
+			// Only do this if we are editing an existing field.
+			if ( ! $is_new_field ) {
+
+				/**
+				 * Remove any radio or dropdown options for this
+				 * field. They will be re-added if needed.
+				 * This stops orphan options if the user changes a
+				 * field from a radio button field to a text box.
+				 */
+				$this->delete_children();
+			}
+
+			/**
+			 * Check to see if this is a field with child options.
+			 * We need to add the options to the db, if it is.
+			 */
+			if ( $this->type_obj->supports_options ) {
+
+				$parent_id = $this->id;
+
+				// Allow plugins to filter the field's child options (i.e. the items in a selectbox).
+				$post_option  = ! empty( $_POST["{$this->type}_option"]           ) ? $_POST["{$this->type}_option"]           : '';
+				$post_default = ! empty( $_POST["isDefault_{$this->type}_option"] ) ? $_POST["isDefault_{$this->type}_option"] : '';
+
+				/**
+				 * Filters the submitted field option value before saved.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string            $post_option Submitted option value.
+				 * @param BP_XProfile_Field $type        Current field type being saved for.
+				 */
+				$options      = apply_filters( 'xprofile_field_options_before_save', $post_option,  $this->type );
+
+				/**
+				 * Filters the default field option value before saved.
+				 *
+				 * @since 1.5.0
+				 *
+				 * @param string            $post_default Default option value.
+				 * @param BP_XProfile_Field $type         Current field type being saved for.
+				 */
+				$defaults     = apply_filters( 'xprofile_field_default_before_save', $post_default, $this->type );
+
+				$counter = 1;
+				if ( !empty( $options ) ) {
+					foreach ( (array) $options as $option_key => $option_value ) {
+						$is_default = 0;
+
+						if ( is_array( $defaults ) ) {
+							if ( isset( $defaults[ $option_key ] ) ) {
+								$is_default = 1;
+							}
+						} else {
+							if ( (int) $defaults == $option_key ) {
+								$is_default = 1;
+							}
+						}
+
+						if ( '' != $option_value ) {
+							$sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_fields} (group_id, parent_id, type, name, description, is_required, option_order, is_default_option) VALUES (%d, %d, 'option', %s, '', 0, %d, %d)", $this->group_id, $parent_id, $option_value, $counter, $is_default );
+							if ( ! $wpdb->query( $sql ) ) {
+								return false;
+							}
+						}
+
+						$counter++;
+					}
+				}
+			}
+
+			/**
+			 * Fires after the current field instance gets saved.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param BP_XProfile_Field $this Current instance of the field being saved.
+			 */
+			do_action_ref_array( 'xprofile_field_after_save', array( $this ) );
+
+			// Recreate type_obj in case someone changed $this->type via a filter.
+			$this->type_obj            = bp_xprofile_create_field_type( $this->type );
+			$this->type_obj->field_obj = $this;
+
+			return $this->id;
+		} else {
+			return false;
+		}
+	}
+
+	/**
+	 * Get field data for a user ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id ID of the user to get field data for.
+	 * @return BP_XProfile_ProfileData
+	 */
+	public function get_field_data( $user_id = 0 ) {
+		return new BP_XProfile_ProfileData( $this->id, $user_id );
+	}
+
+	/**
+	 * Get all child fields for this field ID.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @param bool $for_editing Whether or not the field is for editing.
+	 * @return array
+	 */
+	public function get_children( $for_editing = false ) {
+		global $wpdb;
+
+		// This is done here so we don't have problems with sql injection.
+		if ( empty( $for_editing ) && ( 'asc' === $this->order_by ) ) {
+			$sort_sql = 'ORDER BY name ASC';
+		} elseif ( empty( $for_editing ) && ( 'desc' === $this->order_by ) ) {
+			$sort_sql = 'ORDER BY name DESC';
+		} else {
+			$sort_sql = 'ORDER BY option_order ASC';
+		}
+
+		// This eliminates a problem with getting all fields when there is no
+		// id for the object.
+		if ( empty( $this->id ) ) {
+			$parent_id = -1;
+		} else {
+			$parent_id = $this->id;
+		}
+
+		$bp  = buddypress();
+		$sql = $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_fields} WHERE parent_id = %d AND group_id = %d {$sort_sql}", $parent_id, $this->group_id );
+
+		$children = $wpdb->get_results( $sql );
+
+		/**
+		 * Filters the found children for a field.
+		 *
+		 * @since 1.2.5
+		 * @since 3.0.0 Added the `$this` parameter.
+		 *
+		 * @param object            $children    Found children for a field.
+		 * @param bool              $for_editing Whether or not the field is for editing.
+		 * @param BP_XProfile_Field $this        Field object
+		 */
+		return apply_filters( 'bp_xprofile_field_get_children', $children, $for_editing, $this );
+	}
+
+	/**
+	 * Delete all field children for this field.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $wpdb
+	 */
+	public function delete_children() {
+		global $wpdb;
+
+		$bp  = buddypress();
+		$sql = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE parent_id = %d", $this->id );
+
+		$wpdb->query( $sql );
+	}
+
+	/**
+	 * Gets the member types to which this field should be available.
+	 *
+	 * Will not return inactive member types, even if associated metadata is found.
+	 *
+	 * 'null' is a special pseudo-type, which represents users that do not have a member type.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @return array Array of member type names.
+	 */
+	public function get_member_types() {
+		if ( ! is_null( $this->member_types ) ) {
+			return $this->member_types;
+		}
+
+		$raw_types = bp_xprofile_get_meta( $this->id, 'field', 'member_type', false );
+
+		// If `$raw_types` is not an array, it probably means this is a new field (id=0).
+		if ( ! is_array( $raw_types ) ) {
+			$raw_types = array();
+		}
+
+		// If '_none' is found in the array, it overrides all types.
+		$types = array();
+		if ( ! in_array( '_none', $raw_types ) ) {
+			$registered_types = bp_get_member_types();
+
+			// Eliminate invalid member types saved in the database.
+			foreach ( $raw_types as $raw_type ) {
+				// 'null' is a special case - it represents users without a type.
+				if ( 'null' === $raw_type || isset( $registered_types[ $raw_type ] ) ) {
+					$types[] = $raw_type;
+				}
+			}
+
+			// If no member types have been saved, intepret as *all* member types.
+			if ( empty( $types ) ) {
+				$types = array_values( $registered_types );
+
+				// + the "null" type, ie users without a type.
+				$types[] = 'null';
+			}
+		}
+
+		/**
+		 * Filters the member types to which an XProfile object should be applied.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param array             $types Member types.
+		 * @param BP_XProfile_Field $field Field object.
+		 */
+		$this->member_types = apply_filters( 'bp_xprofile_field_member_types', $types, $this );
+
+		return $this->member_types;
+	}
+
+	/**
+	 * Sets the member types for this field.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param array $member_types Array of member types. Can include 'null' (users with no type) in addition to any
+	 *                            registered types.
+	 * @param bool  $append       Whether to append to existing member types. If false, all existing member type
+	 *                            associations will be deleted before adding your `$member_types`. Default false.
+	 * @return array Member types for the current field, after being saved.
+	 */
+	public function set_member_types( $member_types, $append = false ) {
+		// Unset invalid member types.
+		$types = array();
+		foreach ( $member_types as $member_type ) {
+			// 'null' is a special case - it represents users without a type.
+			if ( 'null' === $member_type || bp_get_member_type_object( $member_type ) ) {
+				$types[] = $member_type;
+			}
+		}
+
+		// When `$append` is false, delete all existing types before adding new ones.
+		if ( ! $append ) {
+			bp_xprofile_delete_meta( $this->id, 'field', 'member_type' );
+
+			/*
+			 * We interpret an empty array as disassociating the field from all types. This is
+			 * represented internally with the '_none' flag.
+			 */
+			if ( empty( $types ) ) {
+				bp_xprofile_add_meta( $this->id, 'field', 'member_type', '_none' );
+			}
+		}
+
+		/*
+		 * Unrestricted fields are represented in the database as having no 'member_type'.
+		 * We detect whether a field is being set to unrestricted by checking whether the
+		 * list of types passed to the method is the same as the list of registered types,
+		 * plus the 'null' pseudo-type.
+		 */
+		$_rtypes  = bp_get_member_types();
+		$rtypes   = array_values( $_rtypes );
+		$rtypes[] = 'null';
+
+		sort( $types );
+		sort( $rtypes );
+
+		// Only save if this is a restricted field.
+		if ( $types !== $rtypes ) {
+			// Save new types.
+			foreach ( $types as $type ) {
+				bp_xprofile_add_meta( $this->id, 'field', 'member_type', $type );
+			}
+		}
+
+		// Reset internal cache of member types.
+		$this->member_types = null;
+
+		/**
+		 * Fires after a field's member types have been updated.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param BP_XProfile_Field $this Field object.
+		 */
+		do_action( 'bp_xprofile_field_set_member_type', $this );
+
+		// Refetch fresh items from the database.
+		return $this->get_member_types();
+	}
+
+	/**
+	 * Gets a label representing the field's member types.
+	 *
+	 * This label is displayed alongside the field's name on the Profile Fields Dashboard panel.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @return string
+	 */
+	public function get_member_type_label() {
+		// Field 1 is always displayed to everyone, so never gets a label.
+		if ( 1 == $this->id ) {
+			return '';
+		}
+
+		// Return an empty string if no member types are registered.
+		$all_types = bp_get_member_types();
+		if ( empty( $all_types ) ) {
+			return '';
+		}
+
+		$member_types = $this->get_member_types();
+
+		// If the field applies to all member types, show no message.
+		$all_types[] = 'null';
+		if ( array_values( $all_types ) == $member_types ) {
+			return '';
+		}
+
+		$label = '';
+		if ( ! empty( $member_types ) ) {
+			$has_null = false;
+			$member_type_labels = array();
+			foreach ( $member_types as $member_type ) {
+				if ( 'null' === $member_type ) {
+					$has_null = true;
+					continue;
+				} else {
+					$mt_obj = bp_get_member_type_object( $member_type );
+					$member_type_labels[] = $mt_obj->labels['name'];
+				}
+			}
+
+			// Alphabetical sort.
+			natcasesort( $member_type_labels );
+			$member_type_labels = array_values( $member_type_labels );
+
+			// Add the 'null' option to the end of the list.
+			if ( $has_null ) {
+				$member_type_labels[] = __( 'Users with no member type', 'buddypress' );
+			}
+
+			$label = sprintf( __( '(Member types: %s)', 'buddypress' ), implode( ', ', array_map( 'esc_html', $member_type_labels ) ) );
+		} else {
+			$label = '<span class="member-type-none-notice">' . __( '(Unavailable to all members)', 'buddypress' ) . '</span>';
+		}
+
+		return $label;
+	}
+
+	/**
+	 * Get the field's default visibility setting.
+	 *
+	 * Lazy-loaded to reduce overhead.
+	 *
+	 * Defaults to 'public' if no visibility setting is found in the database.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @return string
+	 */
+	public function get_default_visibility() {
+		if ( ! isset( $this->default_visibility ) ) {
+			$this->default_visibility = bp_xprofile_get_meta( $this->id, 'field', 'default_visibility' );
+
+			if ( ! $this->default_visibility ) {
+				$this->default_visibility = 'public';
+			}
+		}
+
+		return $this->default_visibility;
+	}
+
+	/**
+	 * Get whether the field's default visibility can be overridden by users.
+	 *
+	 * Lazy-loaded to reduce overhead.
+	 *
+	 * Defaults to 'allowed'.
+	 *
+	 * @since 4.4.0
+	 *
+	 * @return string 'disabled' or 'allowed'.
+	 */
+	public function get_allow_custom_visibility() {
+		if ( ! isset( $this->allow_custom_visibility ) ) {
+			$allow_custom_visibility = bp_xprofile_get_meta( $this->id, 'field', 'allow_custom_visibility' );
+
+			if ( 'disabled' === $allow_custom_visibility ) {
+				$this->allow_custom_visibility = 'disabled';
+			} else {
+				$this->allow_custom_visibility = 'allowed';
+			}
+		}
+
+		return $this->allow_custom_visibility;
+	}
+
+	/**
+	 * Get whether the field values should be auto-linked to a directory search.
+	 *
+	 * Lazy-loaded to reduce overhead.
+	 *
+	 * Defaults to true for multi and default fields, false for single fields.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return bool
+	 */
+	public function get_do_autolink() {
+		if ( ! isset( $this->do_autolink ) ) {
+			$do_autolink = bp_xprofile_get_meta( $this->id, 'field', 'do_autolink' );
+
+			if ( '' === $do_autolink ) {
+				$this->do_autolink = $this->type_obj->supports_options;
+			} else {
+				$this->do_autolink = 'on' === $do_autolink;
+			}
+		}
+
+		return $this->do_autolink;
+	}
+
+	/* Static Methods ********************************************************/
+
+	/**
+	 * Get the type for provided field ID.
+	 *
+	 * @param int $field_id Field ID to get type of.
+	 * @return bool|null|string
+	 */
+	public static function get_type( $field_id = 0 ) {
+		global $wpdb;
+
+		// Bail if no field ID.
+		if ( empty( $field_id ) ) {
+			return false;
+		}
+
+		$bp   = buddypress();
+		$sql  = $wpdb->prepare( "SELECT type FROM {$bp->profile->table_name_fields} WHERE id = %d", $field_id );
+		$type = $wpdb->get_var( $sql );
+
+		// Return field type.
+		if ( ! empty( $type ) ) {
+			return $type;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Delete all fields in a field group.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @param int $group_id ID of the field group to delete fields from.
+	 * @return boolean
+	 */
+	public static function delete_for_group( $group_id = 0 ) {
+		global $wpdb;
+
+		// Bail if no group ID.
+		if ( empty( $group_id ) ) {
+			return false;
+		}
+
+		$bp      = buddypress();
+		$sql     = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE group_id = %d", $group_id );
+		$deleted = $wpdb->get_var( $sql );
+
+		// Return true if fields were deleted.
+		if ( false !== $deleted ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get field ID from field name.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @param string $field_name Name of the field to query the ID for.
+	 * @return int|null Field ID on success; null on failure.
+	 */
+	public static function get_id_from_name( $field_name = '' ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		if ( empty( $bp->profile->table_name_fields ) || empty( $field_name ) ) {
+			return false;
+		}
+
+		$id = bp_core_get_incremented_cache( $field_name, 'bp_xprofile_fields_by_name' );
+		if ( false === $id ) {
+			$sql = $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE name = %s AND parent_id = 0", $field_name );
+			$id = $wpdb->get_var( $sql );
+			bp_core_set_incremented_cache( $field_name, 'bp_xprofile_fields_by_name', $id );
+		}
+
+		return is_numeric( $id ) ? (int) $id : $id;
+	}
+
+	/**
+	 * Update field position and/or field group when relocating.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @param int      $field_id       ID of the field to update.
+	 * @param int|null $position       Field position to update.
+	 * @param int|null $field_group_id ID of the field group.
+	 * @return boolean
+	 */
+	public static function update_position( $field_id, $position = null, $field_group_id = null ) {
+		global $wpdb;
+
+		// Bail if invalid position or field group.
+		if ( ! is_numeric( $position ) || ! is_numeric( $field_group_id ) ) {
+			return false;
+		}
+
+		// Get table name and field parent.
+		$table_name = buddypress()->profile->table_name_fields;
+		$sql        = $wpdb->prepare( "UPDATE {$table_name} SET field_order = %d, group_id = %d WHERE id = %d", $position, $field_group_id, $field_id );
+		$parent     = $wpdb->query( $sql );
+
+		// Update $field_id with new $position and $field_group_id.
+		if ( ! empty( $parent ) && ! is_wp_error( $parent ) ) {
+
+			// Update any children of this $field_id.
+			$sql = $wpdb->prepare( "UPDATE {$table_name} SET group_id = %d WHERE parent_id = %d", $field_group_id, $field_id );
+			$wpdb->query( $sql );
+
+			// Invalidate profile field cache.
+			wp_cache_delete( $field_id, 'bp_xprofile_fields' );
+
+			return $parent;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Gets the IDs of fields applicable for a given member type or array of member types.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string|array $member_types Member type or array of member types. Use 'any' to return unrestricted
+	 *                                   fields (those available for anyone, regardless of member type).
+	 * @return array Multi-dimensional array, with field IDs as top-level keys, and arrays of member types
+	 *               associated with each field as values.
+	 */
+	public static function get_fields_for_member_type( $member_types ) {
+		global $wpdb;
+
+		$fields = array();
+
+		if ( empty( $member_types ) ) {
+			$member_types = array( 'any' );
+		} elseif ( ! is_array( $member_types ) ) {
+			$member_types = array( $member_types );
+		}
+
+		$bp = buddypress();
+
+		// Pull up all recorded field member type data.
+		$mt_meta = wp_cache_get( 'field_member_types', 'bp_xprofile' );
+		if ( false === $mt_meta ) {
+			$mt_meta = $wpdb->get_results( "SELECT object_id, meta_value FROM {$bp->profile->table_name_meta} WHERE meta_key = 'member_type' AND object_type = 'field'" );
+			wp_cache_set( 'field_member_types', $mt_meta, 'bp_xprofile' );
+		}
+
+		// Keep track of all fields with recorded member_type metadata.
+		$all_recorded_field_ids = wp_list_pluck( $mt_meta, 'object_id' );
+
+		// Sort member_type matches in arrays, keyed by field_id.
+		foreach ( $mt_meta as $_mt_meta ) {
+			if ( ! isset( $fields[ $_mt_meta->object_id ] ) ) {
+				$fields[ $_mt_meta->object_id ] = array();
+			}
+
+			$fields[ $_mt_meta->object_id ][] = $_mt_meta->meta_value;
+		}
+
+		/*
+		 * Filter out fields that don't match any passed types, or those marked '_none'.
+		 * The 'any' type is implicitly handled here: it will match no types.
+		 */
+		foreach ( $fields as $field_id => $field_types ) {
+			if ( ! array_intersect( $field_types, $member_types ) ) {
+				unset( $fields[ $field_id ] );
+			}
+		}
+
+		// Any fields with no member_type metadata are available to all member types.
+		if ( ! in_array( '_none', $member_types ) ) {
+			if ( ! empty( $all_recorded_field_ids ) ) {
+				$all_recorded_field_ids_sql = implode( ',', array_map( 'absint', $all_recorded_field_ids ) );
+				$unrestricted_field_ids = $wpdb->get_col( "SELECT id FROM {$bp->profile->table_name_fields} WHERE id NOT IN ({$all_recorded_field_ids_sql})" );
+			} else {
+				$unrestricted_field_ids = $wpdb->get_col( "SELECT id FROM {$bp->profile->table_name_fields}" );
+			}
+
+			// Append the 'null' pseudo-type.
+			$all_member_types   = bp_get_member_types();
+			$all_member_types   = array_values( $all_member_types );
+			$all_member_types[] = 'null';
+
+			foreach ( $unrestricted_field_ids as $unrestricted_field_id ) {
+				$fields[ $unrestricted_field_id ] = $all_member_types;
+			}
+		}
+
+		return $fields;
+	}
+
+	/**
+	 * Validate form field data on sumbission.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @global $message
+	 *
+	 * @return boolean
+	 */
+	public static function admin_validate() {
+		global $message;
+
+		// Check field name.
+		if ( ! isset( $_POST['title'] ) || ( '' === $_POST['title'] ) ) {
+			$message = esc_html__( 'Profile fields must have a name.', 'buddypress' );
+			return false;
+		}
+
+		// Check field requirement.
+		if ( ! isset( $_POST['required'] ) ) {
+			$message = esc_html__( 'Profile field requirement is missing.', 'buddypress' );
+			return false;
+		}
+
+		// Check field type.
+		if ( empty( $_POST['fieldtype'] ) ) {
+			$message = esc_html__( 'Profile field type is missing.', 'buddypress' );
+			return false;
+		}
+
+		// Check that field is of valid type.
+		if ( ! in_array( $_POST['fieldtype'], array_keys( bp_xprofile_get_field_types() ), true ) ) {
+			$message = sprintf( esc_html__( 'The profile field type %s is not registered.', 'buddypress' ), '<code>' . esc_attr( $_POST['fieldtype'] ) . '</code>' );
+			return false;
+		}
+
+		// Get field type so we can check for and lavidate any field options.
+		$field_type = bp_xprofile_create_field_type( $_POST['fieldtype'] );
+
+		// Field type requires options.
+		if ( true === $field_type->supports_options ) {
+
+			// Build the field option key.
+			$option_name = sanitize_key( $_POST['fieldtype'] ) . '_option';
+
+			// Check for missing or malformed options.
+			if ( empty( $_POST[ $option_name ] ) || ! is_array( $_POST[ $option_name ] ) ) {
+				$message = esc_html__( 'These field options are invalid.', 'buddypress' );
+				return false;
+			}
+
+			// Trim out empty field options.
+			$field_values  = array_values( $_POST[ $option_name ] );
+			$field_options = array_map( 'sanitize_text_field', $field_values );
+			$field_count   = count( $field_options );
+
+			// Check for missing or malformed options.
+			if ( 0 === $field_count ) {
+				$message = sprintf( esc_html__( '%s require at least one option.', 'buddypress' ), $field_type->name );
+				return false;
+			}
+
+			// If only one option exists, it cannot be an empty string.
+			if ( ( 1 === $field_count ) && ( '' === $field_options[0] ) ) {
+				$message = sprintf( esc_html__( '%s require at least one option.', 'buddypress' ), $field_type->name );
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Save miscellaneous settings for this field.
+	 *
+	 * Some field types have type-specific settings, which are saved here.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $settings Array of settings.
+	 */
+	public function admin_save_settings( $settings ) {
+		return $this->type_obj->admin_save_settings( $this->id, $settings );
+	}
+
+	/**
+	 * Populates the items for radio buttons, checkboxes, and dropdown boxes.
+	 */
+	public function render_admin_form_children() {
+		foreach ( array_keys( bp_xprofile_get_field_types() ) as $field_type ) {
+			$type_obj = bp_xprofile_create_field_type( $field_type );
+			$type_obj->admin_new_field_html( $this );
+		}
+	}
+
+	/**
+	 * Oupput the admin form for this field.
+	 *
+	 * @since 1.9.0
+	 *
+	 * @param string $message Message to display.
+	 */
+	public function render_admin_form( $message = '' ) {
+
+		// Users Admin URL
+		$users_url = bp_get_admin_url( 'users.php' );
+
+		// Add New
+		if ( empty( $this->id ) ) {
+			$title  = __( 'Add New Field', 'buddypress' );
+			$button	= __( 'Save',          'buddypress' );
+			$action = add_query_arg( array(
+				'page'     => 'bp-profile-setup',
+				'mode'     => 'add_field',
+				'group_id' => (int) $this->group_id
+			), $users_url . '#tabs-' . (int) $this->group_id );
+
+			if ( !empty( $_POST['saveField'] ) ) {
+				$this->name        = $_POST['title'];
+				$this->description = $_POST['description'];
+				$this->is_required = $_POST['required'];
+				$this->type        = $_POST['fieldtype'];
+				$this->field_order = $_POST['field_order'];
+
+				if ( ! empty( $_POST["sort_order_{$this->type}"] ) ) {
+					$this->order_by = $_POST["sort_order_{$this->type}"];
+				}
+			}
+
+		// Edit
+		} else {
+			$title  = __( 'Edit Field', 'buddypress' );
+			$button	= __( 'Update',     'buddypress' );
+			$action = add_query_arg( array(
+				'page'     => 'bp-profile-setup',
+				'mode'     => 'edit_field',
+				'group_id' => (int) $this->group_id,
+				'field_id' => (int) $this->id
+			), $users_url . '#tabs-' . (int) $this->group_id );
+		} ?>
+
+		<div class="wrap">
+
+			<h1><?php echo esc_html( $title ); ?></h1>
+
+			<?php if ( !empty( $message ) ) : ?>
+
+				<div id="message" class="error fade">
+					<p><?php echo esc_html( $message ); ?></p>
+				</div>
+
+			<?php endif; ?>
+
+			<form id="bp-xprofile-add-field" action="<?php echo esc_url( $action ); ?>" method="post">
+				<div id="poststuff">
+					<div id="post-body" class="metabox-holder columns-<?php echo ( 1 == get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
+						<div id="post-body-content">
+
+							<?php
+
+							// Output the name & description fields.
+							$this->name_and_description(); ?>
+
+						</div><!-- #post-body-content -->
+
+						<div id="postbox-container-1" class="postbox-container">
+
+							<?php
+
+							// Output the sumbit metabox.
+							$this->submit_metabox( $button );
+
+							// Output the required metabox.
+							$this->required_metabox();
+
+							// Output the Member Types metabox.
+							$this->member_type_metabox();
+
+							// Output the field visibility metaboxes.
+							$this->visibility_metabox();
+
+							// Output the autolink metabox.
+							$this->autolink_metabox();
+
+
+							/**
+							 * Fires after XProfile Field sidebar metabox.
+							 *
+							 * @since 2.2.0
+							 *
+							 * @param BP_XProfile_Field $this Current XProfile field.
+							 */
+							do_action( 'xprofile_field_after_sidebarbox', $this ); ?>
+
+						</div>
+
+						<div id="postbox-container-2" class="postbox-container">
+
+							<?php
+
+							/**
+							 * Fires before XProfile Field content metabox.
+							 *
+							 * @since 2.3.0
+							 *
+							 * @param BP_XProfile_Field $this Current XProfile field.
+							 */
+							do_action( 'xprofile_field_before_contentbox', $this );
+
+							// Output the field attributes metabox.
+							$this->type_metabox();
+
+							// Output hidden inputs for default field.
+							$this->default_field_hidden_inputs();
+
+							/**
+							 * Fires after XProfile Field content metabox.
+							 *
+							 * @since 2.2.0
+							 *
+							 * @param BP_XProfile_Field $this Current XProfile field.
+							 */
+							do_action( 'xprofile_field_after_contentbox', $this ); ?>
+
+						</div>
+					</div><!-- #post-body -->
+				</div><!-- #poststuff -->
+			</form>
+		</div>
+
+	<?php
+	}
+
+	/**
+	 * Private method used to display the submit metabox.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string $button_text Text to put on button.
+	 */
+	private function submit_metabox( $button_text = '' ) {
+
+		// Setup the URL for deleting
+		$users_url  = bp_get_admin_url( 'users.php' );
+		$cancel_url = add_query_arg( array(
+			'page' => 'bp-profile-setup'
+		), $users_url );
+
+		/**
+		 * Fires before XProfile Field submit metabox.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param BP_XProfile_Field $this Current XProfile field.
+		 */
+		do_action( 'xprofile_field_before_submitbox', $this ); ?>
+
+		<div id="submitdiv" class="postbox">
+			<h2><?php esc_html_e( 'Submit', 'buddypress' ); ?></h2>
+			<div class="inside">
+				<div id="submitcomment" class="submitbox">
+					<div id="major-publishing-actions">
+
+						<?php
+
+						/**
+						 * Fires at the beginning of the XProfile Field publishing actions section.
+						 *
+						 * @since 2.1.0
+						 *
+						 * @param BP_XProfile_Field $this Current XProfile field.
+						 */
+						do_action( 'xprofile_field_submitbox_start', $this ); ?>
+
+						<input type="hidden" name="field_order" id="field_order" value="<?php echo esc_attr( $this->field_order ); ?>" />
+
+						<?php if ( ! empty( $button_text ) ) : ?>
+
+							<div id="publishing-action">
+								<input type="submit" name="saveField" value="<?php echo esc_attr( $button_text ); ?>" class="button-primary" />
+							</div>
+
+						<?php endif; ?>
+
+						<div id="delete-action">
+							<a href="<?php echo esc_url( $cancel_url ); ?>" class="deletion"><?php esc_html_e( 'Cancel', 'buddypress' ); ?></a>
+						</div>
+
+						<?php wp_nonce_field( 'xprofile_delete_option' ); ?>
+
+						<div class="clear"></div>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<?php
+
+		/**
+		 * Fires after XProfile Field submit metabox.
+		 *
+		 * @since 2.1.0
+		 *
+		 * @param BP_XProfile_Field $this Current XProfile field.
+		 */
+		do_action( 'xprofile_field_after_submitbox', $this );
+	}
+
+	/**
+	 * Private method used to output field name and description fields.
+	 *
+	 * @since 2.3.0
+	 */
+	private function name_and_description() {
+	?>
+
+		<div id="titlediv">
+			<div class="titlewrap">
+				<label id="title-prompt-text" for="title"><?php echo esc_html_x( 'Name (required)', 'XProfile admin edit field', 'buddypress' ); ?></label>
+				<input type="text" name="title" id="title" value="<?php echo esc_attr( $this->name ); ?>" autocomplete="off" />
+			</div>
+		</div>
+
+		<div class="postbox">
+			<h2><?php echo esc_html_x( 'Description', 'XProfile admin edit field', 'buddypress' ); ?></h2>
+			<div class="inside">
+				<label for="description" class="screen-reader-text"><?php
+					/* translators: accessibility text */
+					esc_html_e( 'Add description', 'buddypress' );
+				?></label>
+				<textarea name="description" id="description" rows="8" cols="60"><?php echo esc_textarea( $this->description ); ?></textarea>
+			</div>
+		</div>
+
+	<?php
+	}
+
+	/**
+	 * Private method used to output field Member Type metabox.
+	 *
+	 * @since 2.4.0
+	 */
+	private function member_type_metabox() {
+
+		// The primary field is for all, so bail.
+		if ( 1 === (int) $this->id ) {
+			return;
+		}
+
+		// Bail when no member types are registered.
+		if ( ! $member_types = bp_get_member_types( array(), 'objects' ) ) {
+			return;
+		}
+
+		$field_member_types = $this->get_member_types();
+
+		?>
+
+		<div id="member-types-div" class="postbox">
+			<h2><?php _e( 'Member Types', 'buddypress' ); ?></h2>
+			<div class="inside">
+				<p class="description"><?php _e( 'This field should be available to:', 'buddypress' ); ?></p>
+
+				<ul>
+					<?php foreach ( $member_types as $member_type ) : ?>
+					<li>
+						<label for="member-type-<?php echo $member_type->labels['name']; ?>">
+							<input name="member-types[]" id="member-type-<?php echo $member_type->labels['name']; ?>" class="member-type-selector" type="checkbox" value="<?php echo $member_type->name; ?>" <?php checked( in_array( $member_type->name, $field_member_types ) ); ?>/>
+							<?php echo $member_type->labels['name']; ?>
+						</label>
+					</li>
+					<?php endforeach; ?>
+
+					<li>
+						<label for="member-type-none">
+							<input name="member-types[]" id="member-type-none" class="member-type-selector" type="checkbox" value="null" <?php checked( in_array( 'null', $field_member_types ) ); ?>/>
+							<?php _e( 'Users with no member type', 'buddypress' ); ?>
+						</label>
+					</li>
+
+				</ul>
+				<p class="description member-type-none-notice<?php if ( ! empty( $field_member_types ) ) : ?> hide<?php endif; ?>"><?php _e( 'Unavailable to all members.', 'buddypress' ) ?></p>
+			</div>
+
+			<input type="hidden" name="has-member-types" value="1" />
+		</div>
+
+		<?php
+	}
+
+	/**
+	 * Private method used to output field visibility metaboxes.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return void If default field id 1.
+	 */
+	private function visibility_metabox() {
+
+		// Default field cannot have custom visibility.
+		if ( true === $this->is_default_field() ) {
+			return;
+		} ?>
+
+		<div class="postbox">
+			<h2><label for="default-visibility"><?php esc_html_e( 'Visibility', 'buddypress' ); ?></label></h2>
+			<div class="inside">
+				<div>
+					<select name="default-visibility" id="default-visibility">
+
+						<?php foreach( bp_xprofile_get_visibility_levels() as $level ) : ?>
+
+							<option value="<?php echo esc_attr( $level['id'] ); ?>" <?php selected( $this->get_default_visibility(), $level['id'] ); ?>>
+								<?php echo esc_html( $level['label'] ); ?>
+							</option>
+
+						<?php endforeach ?>
+
+					</select>
+				</div>
+
+				<div>
+					<ul>
+						<li>
+							<input type="radio" id="allow-custom-visibility-allowed" name="allow-custom-visibility" value="allowed" <?php checked( $this->get_allow_custom_visibility(), 'allowed' ); ?> />
+							<label for="allow-custom-visibility-allowed"><?php esc_html_e( 'Allow members to override', 'buddypress' ); ?></label>
+						</li>
+						<li>
+							<input type="radio" id="allow-custom-visibility-disabled" name="allow-custom-visibility" value="disabled" <?php checked( $this->get_allow_custom_visibility(), 'disabled' ); ?> />
+							<label for="allow-custom-visibility-disabled"><?php esc_html_e( 'Enforce field visibility', 'buddypress' ); ?></label>
+						</li>
+					</ul>
+				</div>
+			</div>
+		</div>
+
+		<?php
+	}
+
+	/**
+	 * Output the metabox for setting if field is required or not.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return void If default field.
+	 */
+	private function required_metabox() {
+
+		// Default field is always required.
+		if ( true === $this->is_default_field() ) {
+			return;
+		} ?>
+
+		<div class="postbox">
+			<h2><label for="required"><?php esc_html_e( 'Requirement', 'buddypress' ); ?></label></h2>
+			<div class="inside">
+				<select name="required" id="required">
+					<option value="0"<?php selected( $this->is_required, '0' ); ?>><?php esc_html_e( 'Not Required', 'buddypress' ); ?></option>
+					<option value="1"<?php selected( $this->is_required, '1' ); ?>><?php esc_html_e( 'Required',     'buddypress' ); ?></option>
+				</select>
+			</div>
+		</div>
+
+	<?php
+	}
+
+	/**
+	 * Private method used to output autolink metabox.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return void If default field id 1.
+	 */
+	private function autolink_metabox() {
+		?>
+
+		<div class="postbox">
+			<h2><?php esc_html_e( 'Autolink', 'buddypress' ); ?></h2>
+			<div class="inside">
+				<p class="description"><?php esc_html_e( 'On user profiles, link this field to a search of the Members directory, using the field value as a search term.', 'buddypress' ); ?></p>
+
+				<p>
+					<label for="do-autolink" class="screen-reader-text"><?php
+						/* translators: accessibility text */
+						esc_html_e( 'Autolink status for this field', 'buddypress' );
+					?></label>
+					<select name="do_autolink" id="do-autolink">
+						<option value="on" <?php selected( $this->get_do_autolink() ); ?>><?php esc_html_e( 'Enabled', 'buddypress' ); ?></option>
+						<option value="" <?php selected( $this->get_do_autolink(), false ); ?>><?php esc_html_e( 'Disabled', 'buddypress' ); ?></option>
+					</select>
+				</p>
+			</div>
+		</div>
+
+		<?php
+	}
+
+	/**
+	 * Output the metabox for setting what type of field this is.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return void If default field.
+	 */
+	private function type_metabox() {
+
+		// Default field cannot change type.
+		if ( true === $this->is_default_field() ) {
+			return;
+		} ?>
+
+		<div class="postbox">
+			<h2><label for="fieldtype"><?php esc_html_e( 'Type', 'buddypress'); ?></label></h2>
+			<div class="inside" aria-live="polite" aria-atomic="true" aria-relevant="all">
+				<select name="fieldtype" id="fieldtype" onchange="show_options(this.value)">
+
+					<?php bp_xprofile_admin_form_field_types( $this->type ); ?>
+
+				</select>
+
+				<?php
+
+				// Deprecated filter, don't use. Go look at {@link BP_XProfile_Field_Type::admin_new_field_html()}.
+				do_action( 'xprofile_field_additional_options', $this );
+
+				$this->render_admin_form_children(); ?>
+
+			</div>
+		</div>
+
+	<?php
+	}
+
+	/**
+	 * Output hidden fields used by default field.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @return void If not default field.
+	 */
+	private function default_field_hidden_inputs() {
+
+		// Nonce
+		wp_nonce_field( 'bp_xprofile_admin_field', 'bp_xprofile_admin_field' );
+
+		// Field 1 is the fullname field, which cannot have custom visibility.
+		if ( false === $this->is_default_field() ) {
+			return;
+		} ?>
+
+		<input type="hidden" name="required"  id="required"  value="1"       />
+		<input type="hidden" name="fieldtype" id="fieldtype" value="textbox" />
+
+		<?php
+	}
+
+	/**
+	 * Return if a field ID is the default field.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param int $field_id ID of field to check.
+	 * @return bool
+	 */
+	private function is_default_field( $field_id = 0 ) {
+
+		// Fallback to current field ID if none passed.
+		if ( empty( $field_id ) ) {
+			$field_id = $this->id;
+		}
+
+		// Compare & return.
+		return (bool) ( 1 === (int) $field_id );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-group.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-group.php	(revision 12265)
@@ -0,0 +1,860 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class to help set up XProfile Groups.
+ *
+ * @since 1.0.0
+ */
+class BP_XProfile_Group {
+
+	/**
+	 * Field group ID.
+	 *
+	 * @since 1.1.0
+	 * @var int ID of field group.
+	 */
+	public $id = null;
+
+	/**
+	 * Field group name.
+	 *
+	 * @since 1.1.0
+	 * @var string Name of field group.
+	 */
+	public $name;
+
+	/**
+	 * Field group Description.
+	 *
+	 * @since 1.1.0
+	 * @var string Description of field group.
+	 */
+	public $description;
+
+	/**
+	 * Group deletion boolean.
+	 *
+	 * @since 1.1.0
+	 * @var bool Can this group be deleted?
+	 */
+	public $can_delete;
+
+	/**
+	 * Group order.
+	 *
+	 * @since 1.1.0
+	 * @var int Group order relative to other groups.
+	 */
+	public $group_order;
+
+	/**
+	 * Group fields.
+	 *
+	 * @since 1.1.0
+	 * @var array Fields of group.
+	 */
+	public $fields;
+
+	/**
+	 * Initialize and/or populate profile field group.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @param int|null $id Field group ID.
+	 */
+	public function __construct( $id = null ) {
+		if ( ! empty( $id ) ) {
+			$this->populate( $id );
+		}
+	}
+
+	/**
+	 * Populate a profile field group.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global $wpdb $wpdb
+	 *
+	 * @param int $id Field group ID.
+	 * @return boolean
+	 */
+	public function populate( $id ) {
+
+		// Get this group.
+		$group = self::get( array(
+			'profile_group_id' => $id
+		) );
+
+		// Bail if group not found.
+		if ( empty( $group ) ) {
+			return false;
+		}
+
+		// Get the first array element.
+		$group = reset( $group );
+
+		// Set object properties.
+		$this->id          = $group->id;
+		$this->name        = $group->name;
+		$this->description = $group->description;
+		$this->can_delete  = $group->can_delete;
+		$this->group_order = $group->group_order;
+	}
+
+	/**
+	 * Save a profile field group.
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @return boolean
+	 */
+	public function save() {
+		global $wpdb;
+
+		// Filter the field group attributes.
+		$this->name        = apply_filters( 'xprofile_group_name_before_save',        $this->name,        $this->id );
+		$this->description = apply_filters( 'xprofile_group_description_before_save', $this->description, $this->id );
+
+		/**
+		 * Fires before the current group instance gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_XProfile_Group $this Current instance of the group being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'xprofile_group_before_save', array( &$this ) );
+
+		$bp = buddypress();
+
+		// Update or insert.
+		if ( ! empty( $this->id ) ) {
+			$sql = $wpdb->prepare( "UPDATE {$bp->profile->table_name_groups} SET name = %s, description = %s WHERE id = %d", $this->name, $this->description, $this->id );
+		} else {
+			$sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_groups} (name, description, can_delete) VALUES (%s, %s, 1)", $this->name, $this->description );
+		}
+
+		// Attempt to insert or update.
+		$query = $wpdb->query( $sql );
+
+		// Bail if query fails. If `$query` is 0, it means the save was successful, but no fields were updated.
+		if ( false === $query || is_wp_error( $query ) ) {
+			return false;
+		}
+
+		// If not set, update the ID in the group object.
+		if ( empty( $this->id ) ) {
+			$this->id = $wpdb->insert_id;
+		}
+
+		/**
+		 * Fires after the current group instance gets saved.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_XProfile_Group $this Current instance of the group being saved. Passed by reference.
+		 */
+		do_action_ref_array( 'xprofile_group_after_save', array( &$this ) );
+
+		return $this->id;
+	}
+
+	/**
+	 * Delete a profile field group
+	 *
+	 * @since 1.1.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @return boolean
+	 */
+	public function delete() {
+		global $wpdb;
+
+		// Bail if field group cannot be deleted.
+		if ( empty( $this->can_delete ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires before the current group instance gets deleted.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Group $this Current instance of the group being deleted. Passed by reference.
+		 */
+		do_action_ref_array( 'xprofile_group_before_delete', array( &$this ) );
+
+		$bp      = buddypress();
+		$sql     = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_groups} WHERE id = %d", $this->id );
+		$deleted = $wpdb->query( $sql );
+
+		// Delete field group.
+		if ( empty( $deleted ) || is_wp_error( $deleted ) ) {
+			return false;
+		}
+
+		// Remove the group's fields.
+		if ( BP_XProfile_Field::delete_for_group( $this->id ) ) {
+
+			// Remove profile data for the groups fields.
+			if ( ! empty( $this->fields ) ) {
+				for ( $i = 0, $count = count( $this->fields ); $i < $count; ++$i ) {
+					BP_XProfile_ProfileData::delete_for_field( $this->fields[$i]->id );
+				}
+			}
+		}
+
+		/**
+		 * Fires after the current group instance gets deleted.
+		 *
+		 * @since 2.0.0
+		 *
+		 * @param BP_XProfile_Group $this Current instance of the group being deleted. Passed by reference.
+		 */
+		do_action_ref_array( 'xprofile_group_after_delete', array( &$this ) );
+
+		return true;
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Populates the BP_XProfile_Group object with profile field groups, fields,
+	 * and field data.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @global object $wpdb WordPress DB access object.
+	 *
+	 * @param array $args {
+	 *  Array of optional arguments:
+	 *      @type int          $profile_group_id  Limit results to a single profile group.
+	 *      @type int          $user_id           Required if you want to load a specific user's data.
+	 *                                            Default: displayed user's ID.
+	 *      @type array|string $member_type       Limit fields by those restricted to a given member type, or array of
+	 *                                            member types. If `$user_id` is provided, the value of `$member_type`
+	 *                                            will be overridden by the member types of the provided user. The
+	 *                                            special value of 'any' will return only those fields that are
+	 *                                            unrestricted by member type - i.e., those applicable to any type.
+	 *      @type bool         $hide_empty_groups True to hide groups that don't have any fields. Default: false.
+	 *      @type bool         $hide_empty_fields True to hide fields where the user has not provided data.
+	 *                                            Default: false.
+	 *      @type bool         $fetch_fields      Whether to fetch each group's fields. Default: false.
+	 *      @type bool         $fetch_field_data  Whether to fetch data for each field. Requires a $user_id.
+	 *                                            Default: false.
+	 *      @type array        $exclude_groups    Comma-separated list or array of group IDs to exclude.
+	 *      @type array        $exclude_fields    Comma-separated list or array of field IDs to exclude.
+	 *      @type bool         $update_meta_cache Whether to pre-fetch xprofilemeta for all retrieved groups, fields,
+	 *                                            and data. Default: true.
+	 * }
+	 * @return array $groups
+	 */
+	public static function get( $args = array() ) {
+		global $wpdb;
+
+		// Parse arguments.
+		$r = wp_parse_args( $args, array(
+			'profile_group_id'       => false,
+			'user_id'                => bp_displayed_user_id(),
+			'member_type'            => false,
+			'hide_empty_groups'      => false,
+			'hide_empty_fields'      => false,
+			'fetch_fields'           => false,
+			'fetch_field_data'       => false,
+			'fetch_visibility_level' => false,
+			'exclude_groups'         => false,
+			'exclude_fields'         => false,
+			'update_meta_cache'      => true,
+		) );
+
+		// Keep track of object IDs for cache-priming.
+		$object_ids = array(
+			'group' => array(),
+			'field' => array(),
+			'data'  => array(),
+		);
+
+		// WHERE.
+		if ( ! empty( $r['profile_group_id'] ) ) {
+			$where_sql = $wpdb->prepare( 'WHERE g.id = %d', $r['profile_group_id'] );
+		} elseif ( $r['exclude_groups'] ) {
+			$exclude   = join( ',', wp_parse_id_list( $r['exclude_groups'] ) );
+			$where_sql = "WHERE g.id NOT IN ({$exclude})";
+		} else {
+			$where_sql = '';
+		}
+
+		$bp = buddypress();
+
+		// Include or exclude empty groups.
+		if ( ! empty( $r['hide_empty_groups'] ) ) {
+			$group_ids = $wpdb->get_col( "SELECT DISTINCT g.id FROM {$bp->profile->table_name_groups} g INNER JOIN {$bp->profile->table_name_fields} f ON g.id = f.group_id {$where_sql} ORDER BY g.group_order ASC" );
+		} else {
+			$group_ids = $wpdb->get_col( "SELECT DISTINCT g.id FROM {$bp->profile->table_name_groups} g {$where_sql} ORDER BY g.group_order ASC" );
+		}
+
+		// Get all group data.
+		$groups = self::get_group_data( $group_ids );
+
+		// Bail if not also getting fields.
+		if ( empty( $r['fetch_fields'] ) ) {
+			return $groups;
+		}
+
+		// Get the group ids from the groups we found.
+		$group_ids = wp_list_pluck( $groups, 'id' );
+
+		// Store for meta cache priming.
+		$object_ids['group'] = $group_ids;
+
+		// Bail if no groups found.
+		if ( empty( $group_ids ) ) {
+			return $groups;
+		}
+
+		// Setup IN query from group IDs.
+		$group_ids_in = implode( ',', (array) $group_ids );
+
+		// Support arrays and comma-separated strings.
+		$exclude_fields_cs = wp_parse_id_list( $r['exclude_fields'] );
+
+		// Visibility - Handled here so as not to be overridden by sloppy use of the
+		// exclude_fields parameter. See bp_xprofile_get_hidden_fields_for_user().
+		$hidden_user_fields = bp_xprofile_get_hidden_fields_for_user( $r['user_id'] );
+		$exclude_fields_cs  = array_merge( $exclude_fields_cs, $hidden_user_fields );
+		$exclude_fields_cs  = implode( ',', $exclude_fields_cs );
+
+		// Set up NOT IN query for excluded field IDs.
+		if ( ! empty( $exclude_fields_cs ) ) {
+			$exclude_fields_sql = "AND id NOT IN ({$exclude_fields_cs})";
+		} else {
+			$exclude_fields_sql = '';
+		}
+
+		// Set up IN query for included field IDs.
+		$include_field_ids = array();
+
+		// Member-type restrictions.
+		if ( bp_get_member_types() ) {
+			if ( $r['user_id'] || false !== $r['member_type'] ) {
+				$member_types = $r['member_type'];
+				if ( $r['user_id'] ) {
+					$member_types = bp_get_member_type( $r['user_id'], false );
+					if ( empty( $member_types ) ) {
+						$member_types = array( 'null' );
+					}
+				}
+
+				$member_types_fields = BP_XProfile_Field::get_fields_for_member_type( $member_types );
+				$include_field_ids += array_keys( $member_types_fields );
+			}
+		}
+
+		$in_sql = '';
+		if ( ! empty( $include_field_ids ) ) {
+			$include_field_ids_cs = implode( ',', array_map( 'intval', $include_field_ids ) );
+			$in_sql = " AND id IN ({$include_field_ids_cs}) ";
+		}
+
+		// Fetch the fields.
+		$field_ids = $wpdb->get_col( "SELECT id FROM {$bp->profile->table_name_fields} WHERE group_id IN ( {$group_ids_in} ) AND parent_id = 0 {$exclude_fields_sql} {$in_sql} ORDER BY field_order" );
+
+		foreach( $groups as $group ) {
+			$group->fields = array();
+		}
+
+		// Bail if no fields.
+		if ( empty( $field_ids ) ) {
+			return $groups;
+		}
+
+		$field_ids = array_map( 'intval', $field_ids );
+
+		// Prime the field cache.
+		$uncached_field_ids = bp_get_non_cached_ids( $field_ids, 'bp_xprofile_fields' );
+		if ( ! empty( $uncached_field_ids ) ) {
+			$_uncached_field_ids = implode( ',', array_map( 'intval', $uncached_field_ids ) );
+			$uncached_fields = $wpdb->get_results( "SELECT * FROM {$bp->profile->table_name_fields} WHERE id IN ({$_uncached_field_ids})" );
+			foreach ( $uncached_fields as $uncached_field ) {
+				$fid = intval( $uncached_field->id );
+				wp_cache_set( $fid, $uncached_field, 'bp_xprofile_fields' );
+			}
+		}
+
+		// Pull field objects from the cache.
+		$fields = array();
+		foreach ( $field_ids as $field_id ) {
+			$fields[] = xprofile_get_field( $field_id, null, false );
+		}
+
+		// Store field IDs for meta cache priming.
+		$object_ids['field'] = $field_ids;
+
+		// Maybe fetch field data.
+		if ( ! empty( $r['fetch_field_data'] ) ) {
+
+			// Get field data for user ID.
+			if ( ! empty( $field_ids ) && ! empty( $r['user_id'] ) ) {
+				$field_data = BP_XProfile_ProfileData::get_data_for_user( $r['user_id'], $field_ids );
+			}
+
+			// Remove data-less fields, if necessary.
+			if ( ! empty( $r['hide_empty_fields'] ) && ! empty( $field_ids ) && ! empty( $field_data ) ) {
+
+				// Loop through the results and find the fields that have data.
+				foreach( (array) $field_data as $data ) {
+
+					// Empty fields may contain a serialized empty array.
+					$maybe_value = maybe_unserialize( $data->value );
+
+					// Valid field values of 0 or '0' get caught by empty(), so we have an extra check for these. See #BP5731.
+					if ( ( ! empty( $maybe_value ) || '0' == $maybe_value ) && false !== $key = array_search( $data->field_id, $field_ids ) ) {
+
+						// Fields that have data get removed from the list.
+						unset( $field_ids[ $key ] );
+					}
+				}
+
+				// The remaining members of $field_ids are empty. Remove them.
+				foreach( $fields as $field_key => $field ) {
+					if ( in_array( $field->id, $field_ids ) ) {
+						unset( $fields[ $field_key ] );
+					}
+				}
+
+				// Reset indexes.
+				$fields = array_values( $fields );
+			}
+
+			// Field data was found.
+			if ( ! empty( $fields ) && ! empty( $field_data ) && ! is_wp_error( $field_data ) ) {
+
+				// Loop through fields.
+				foreach( (array) $fields as $field_key => $field ) {
+
+					// Loop through the data in each field.
+					foreach( (array) $field_data as $data ) {
+
+						// Assign correct data value to the field.
+						if ( $field->id == $data->field_id ) {
+							$fields[ $field_key ]->data        = new stdClass;
+							$fields[ $field_key ]->data->value = $data->value;
+							$fields[ $field_key ]->data->id    = $data->id;
+						}
+
+						// Store for meta cache priming.
+						$object_ids['data'][] = $data->id;
+					}
+				}
+			}
+		}
+
+		// Prime the meta cache, if necessary.
+		if ( ! empty( $r['update_meta_cache'] ) ) {
+			bp_xprofile_update_meta_cache( $object_ids );
+		}
+
+		// Maybe fetch visibility levels.
+		if ( ! empty( $r['fetch_visibility_level'] ) ) {
+			$fields = self::fetch_visibility_level( $r['user_id'], $fields );
+		}
+
+		// Merge the field array back in with the group array.
+		foreach( (array) $groups as $group ) {
+			// Indexes may have been shifted after previous deletions, so we get a
+			// fresh one each time through the loop.
+			$index = array_search( $group, $groups );
+
+			foreach( (array) $fields as $field ) {
+				if ( $group->id === $field->group_id ) {
+					$groups[ $index ]->fields[] = $field;
+				}
+			}
+
+			// When we unset fields above, we may have created empty groups.
+			// Remove them, if necessary.
+			if ( empty( $group->fields ) && ! empty( $r['hide_empty_groups'] ) ) {
+				unset( $groups[ $index ] );
+			}
+
+			// Reset indexes.
+			$groups = array_values( $groups );
+		}
+
+		return $groups;
+	}
+
+	/**
+	 * Get data about a set of groups, based on IDs.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param array $group_ids Array of IDs.
+	 * @return array
+	 */
+	protected static function get_group_data( $group_ids ) {
+		global $wpdb;
+
+		// Bail if no group IDs are passed.
+		if ( empty( $group_ids ) ) {
+			return array();
+		}
+
+		// Setup empty arrays.
+		$groups        = array();
+		$uncached_gids = array();
+
+		// Loop through groups and look for cached & uncached data.
+		foreach ( $group_ids as $group_id ) {
+
+			// If cached data is found, use it.
+			$group_data = wp_cache_get( $group_id, 'bp_xprofile_groups' );
+			if ( false !== $group_data ) {
+				$groups[ $group_id ] = $group_data;
+
+			// Otherwise leave a placeholder so we don't lose the order.
+			} else {
+				$groups[ $group_id ] = '';
+
+				// Add to the list of items to be queried.
+				$uncached_gids[] = $group_id;
+			}
+		}
+
+		// Fetch uncached data from the DB if necessary.
+		if ( ! empty( $uncached_gids ) ) {
+
+			// Setup IN query for uncached group data.
+			$uncached_gids_sql = implode( ',', wp_parse_id_list( $uncached_gids ) );
+
+			// Get table name to query.
+			$table_name = buddypress()->profile->table_name_groups;
+
+			// Fetch data, preserving order.
+			$queried_gdata = $wpdb->get_results( "SELECT * FROM {$table_name} WHERE id IN ({$uncached_gids_sql}) ORDER BY FIELD( id, {$uncached_gids_sql} )");
+
+			// Make sure query returned valid data.
+			if ( ! empty( $queried_gdata ) && ! is_wp_error( $queried_gdata ) ) {
+
+				// Put queried data into the placeholders created earlier,
+				// and add it to the cache.
+				foreach ( (array) $queried_gdata as $gdata ) {
+
+					// Add group to groups array.
+					$groups[ $gdata->id ] = $gdata;
+
+					// Cache previously uncached group data.
+					wp_cache_set( $gdata->id, $gdata, 'bp_xprofile_groups' );
+				}
+			}
+		}
+
+		// Integer casting.
+		foreach ( (array) $groups as $key => $data ) {
+			$groups[ $key ]->id          = (int) $groups[ $key ]->id;
+			$groups[ $key ]->group_order = (int) $groups[ $key ]->group_order;
+			$groups[ $key ]->can_delete  = (int) $groups[ $key ]->can_delete;
+		}
+
+		// Reset indexes & return.
+		return array_values( $groups );
+	}
+
+	/**
+	 * Validate field group when form submitted.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global string $message
+	 *
+	 * @return boolean
+	 */
+	public static function admin_validate() {
+		global $message;
+
+		// Validate Form.
+		if ( empty( $_POST['group_name'] ) ) {
+			$message = __( 'Please make sure you give the group a name.', 'buddypress' );
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	/**
+	 * Update field group position.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @global $wpdb $wpdb
+	 *
+	 * @param  int $field_group_id ID of the group the field belongs to.
+	 * @param  int $position       Field group position.
+	 * @return boolean
+	 */
+	public static function update_position( $field_group_id, $position ) {
+		global $wpdb;
+
+		if ( ! is_numeric( $position ) ) {
+			return false;
+		}
+
+		// Purge profile field group cache.
+		wp_cache_delete( 'all', 'bp_xprofile_groups' );
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "UPDATE {$bp->profile->table_name_groups} SET group_order = %d WHERE id = %d", $position, $field_group_id ) );
+	}
+
+	/**
+	 * Fetch the field visibility level for the fields returned by the query.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int   $user_id The profile owner's user_id.
+	 * @param array $fields  The database results returned by the get() query.
+	 * @return array $fields The database results, with field_visibility added
+	 */
+	public static function fetch_visibility_level( $user_id = 0, $fields = array() ) {
+
+		// Get the user's visibility level preferences.
+		$visibility_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
+
+		foreach( (array) $fields as $key => $field ) {
+
+			// Does the admin allow this field to be customized?
+			$visibility   = bp_xprofile_get_meta( $field->id, 'field', 'allow_custom_visibility' );
+			$allow_custom = (bool) ( 'disabled' !== $visibility );
+
+			// Look to see if the user has set the visibility for this field.
+			if ( ( true === $allow_custom ) && isset( $visibility_levels[ $field->id ] ) ) {
+				$field_visibility = $visibility_levels[ $field->id ];
+
+			// If no admin-set default is saved, fall back on a global default.
+			} else {
+				$fallback_visibility = bp_xprofile_get_meta( $field->id, 'field', 'default_visibility' );
+
+				/**
+				 * Filters the XProfile default visibility level for a field.
+				 *
+				 * @since 1.6.0
+				 *
+				 * @param string $value Default visibility value.
+				 */
+				$field_visibility = ! empty( $fallback_visibility )
+					? $fallback_visibility
+					: apply_filters( 'bp_xprofile_default_visibility_level', 'public' );
+			}
+
+			$fields[ $key ]->visibility_level = $field_visibility;
+		}
+
+		return $fields;
+	}
+
+	/**
+	 * Fetch the admin-set preferences for all fields.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @return array $default_visibility_levels An array, keyed by field_id, of default
+	 *                                          visibility level + allow_custom
+	 *                                          (whether the admin allows this
+	 *                                          field to be set by user)
+	 */
+	public static function fetch_default_visibility_levels() {
+		global $wpdb;
+
+		$default_visibility_levels = wp_cache_get( 'default_visibility_levels', 'bp_xprofile' );
+
+		if ( false === $default_visibility_levels ) {
+			$bp = buddypress();
+
+			$levels = $wpdb->get_results( "SELECT object_id, meta_key, meta_value FROM {$bp->profile->table_name_meta} WHERE object_type = 'field' AND ( meta_key = 'default_visibility' OR meta_key = 'allow_custom_visibility' )" );
+
+			// Arrange so that the field id is the key and the visibility level the value.
+			$default_visibility_levels = array();
+			foreach ( $levels as $level ) {
+				switch ( $level->meta_key ) {
+					case 'default_visibility' :
+						$default_visibility_levels[ $level->object_id ]['default']      = $level->meta_value;
+						break;
+					case 'allow_custom_visibility' :
+						$default_visibility_levels[ $level->object_id ]['allow_custom'] = $level->meta_value;
+						break;
+				}
+			}
+
+			wp_cache_set( 'default_visibility_levels', $default_visibility_levels, 'bp_xprofile' );
+		}
+
+		return $default_visibility_levels;
+	}
+
+	/** Admin Output **********************************************************/
+
+	/**
+	 * Output the admin area field group form.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global string $message
+	 */
+	public function render_admin_form() {
+		global $message;
+
+		// Users Admin URL
+		$users_url = bp_get_admin_url( 'users.php' );
+
+		// URL to cancel to
+		$cancel_url = add_query_arg( array(
+			'page' => 'bp-profile-setup'
+		), $users_url );
+
+		// New field group.
+		if ( empty( $this->id ) ) {
+			$title	= __( 'Add New Field Group', 'buddypress' );
+			$button	= __( 'Save',                'buddypress' );
+			$action	= add_query_arg( array(
+				'page' => 'bp-profile-setup',
+				'mode' => 'add_group'
+			), $users_url );
+
+		// Existing field group.
+		} else {
+			$title  = __( 'Edit Field Group', 'buddypress' );
+			$button	= __( 'Update',           'buddypress' );
+			$action	= add_query_arg( array(
+				'page'     => 'bp-profile-setup',
+				'mode'     => 'edit_group',
+				'group_id' => (int) $this->id
+			), $users_url );
+		} ?>
+
+		<div class="wrap">
+
+			<h1><?php echo esc_html( $title ); ?></h1>
+
+			<?php if ( ! empty( $message ) ) : ?>
+
+				<div id="message" class="error fade">
+					<p><?php echo esc_html( $message ); ?></p>
+				</div>
+
+			<?php endif; ?>
+
+			<form id="bp-xprofile-add-field-group" action="<?php echo esc_url( $action ); ?>" method="post">
+				<div id="poststuff">
+					<div id="post-body" class="metabox-holder columns-<?php echo ( 1 == get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
+						<div id="post-body-content">
+							<div id="titlediv">
+								<div class="titlewrap">
+									<label id="title-prompt-text" for="title"><?php esc_html_e( 'Field Group Name (required)', 'buddypress') ?></label>
+									<input type="text" name="group_name" id="title" value="<?php echo esc_attr( $this->name ); ?>" autocomplete="off" />
+								</div>
+							</div>
+							<div class="postbox">
+								<h2><?php esc_html_e( 'Field Group Description', 'buddypress' ); ?></h2>
+								<div class="inside">
+									<label for="group_description" class="screen-reader-text"><?php
+										/* translators: accessibility text */
+										esc_html_e( 'Add description', 'buddypress' );
+									?></label>
+									<textarea name="group_description" id="group_description" rows="8" cols="60"><?php echo esc_textarea( $this->description ); ?></textarea>
+								</div>
+							</div>
+
+							<?php
+
+							/**
+							 * Fires after the XProfile group description field is rendered in wp-admin.
+							 *
+							 * @since 2.6.0
+							 *
+							 * @param BP_XProfile_Group $this Current XProfile group.
+							 */
+							do_action( 'xprofile_group_admin_after_description', $this ); ?>
+
+						</div><!-- #post-body-content -->
+
+						<div id="postbox-container-1" class="postbox-container">
+
+							<?php
+
+							/**
+							 * Fires before XProfile Group submit metabox.
+							 *
+							 * @since 2.1.0
+							 *
+							 * @param BP_XProfile_Group $this Current XProfile group.
+							 */
+							do_action( 'xprofile_group_before_submitbox', $this ); ?>
+
+							<div id="submitdiv" class="postbox">
+								<h2><?php _e( 'Submit', 'buddypress' ); ?></h2>
+								<div class="inside">
+									<div id="submitcomment" class="submitbox">
+										<div id="major-publishing-actions">
+
+											<?php
+
+											// Nonce fields
+											wp_nonce_field( 'bp_xprofile_admin_group', 'bp_xprofile_admin_group' );
+
+											/**
+											 * Fires at the beginning of the XProfile Group publishing actions section.
+											 *
+											 * @since 2.1.0
+											 *
+											 * @param BP_XProfile_Group $this Current XProfile group.
+											 */
+											do_action( 'xprofile_group_submitbox_start', $this ); ?>
+
+											<input type="hidden" name="group_order" id="group_order" value="<?php echo esc_attr( $this->group_order ); ?>" />
+											<div id="publishing-action">
+												<input type="submit" name="save_group" value="<?php echo esc_attr( $button ); ?>" class="button-primary"/>
+											</div>
+											<div id="delete-action">
+												<a href="<?php echo esc_url( $cancel_url ); ?>" class="deletion"><?php _e( 'Cancel', 'buddypress' ); ?></a>
+											</div>
+											<div class="clear"></div>
+										</div>
+									</div>
+								</div>
+							</div>
+
+							<?php
+
+							/**
+							 * Fires after XProfile Group submit metabox.
+							 *
+							 * @since 2.1.0
+							 *
+							 * @param BP_XProfile_Group $this Current XProfile group.
+							 */
+							do_action( 'xprofile_group_after_submitbox', $this ); ?>
+
+						</div>
+					</div>
+				</div>
+			</form>
+		</div>
+
+	<?php
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-meta-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-meta-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-meta-query.php	(revision 12265)
@@ -0,0 +1,363 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.3.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class for generating SQL clauses that filter a primary query according to
+ * XProfile metadata keys and values.
+ *
+ * `BP_XProfile_Meta_Query` is a helper that allows primary query classes, such
+ * as {@see WP_Query} and {@see WP_User_Query}, to filter their results by object
+ * metadata, by generating `JOIN` and `WHERE` subclauses to be attached
+ * to the primary SQL query string.
+ *
+ * @since 2.3.0
+ */
+class BP_XProfile_Meta_Query extends WP_Meta_Query {
+
+	/**
+	 * Determine whether a query clause is first-order.
+	 *
+	 * A first-order meta query clause is one that has either a 'key', 'value',
+	 * or 'object' array key.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $query Meta query arguments.
+	 * @return bool Whether the query clause is a first-order clause.
+	 */
+	protected function is_first_order_clause( $query ) {
+		return isset( $query['key'] ) || isset( $query['value'] ) || isset( $query['object'] );
+	}
+
+	/**
+	 * Constructs a meta query based on 'meta_*' query vars.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array $qv The query variables.
+	 */
+	public function parse_query_vars( $qv ) {
+		$meta_query = array();
+
+		/*
+		 * For orderby=meta_value to work correctly, simple query needs to be
+		 * first (so that its table join is against an unaliased meta table) and
+		 * needs to be its own clause (so it doesn't interfere with the logic of
+		 * the rest of the meta_query).
+		 */
+		$primary_meta_query = array();
+		foreach ( array( 'key', 'compare', 'type' ) as $key ) {
+			if ( ! empty( $qv[ "meta_$key" ] ) ) {
+				$primary_meta_query[ $key ] = $qv[ "meta_$key" ];
+			}
+		}
+
+		// WP_Query sets 'meta_value' = '' by default.
+		if ( isset( $qv['meta_value'] ) && ( '' !== $qv['meta_value'] ) && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
+			$primary_meta_query['value'] = $qv['meta_value'];
+		}
+
+		// BP_XProfile_Query sets 'object_type' = '' by default.
+		if ( isset( $qv[ 'object_type' ] ) && ( '' !== $qv[ 'object_type' ] ) && ( ! is_array( $qv[ 'object_type' ] ) || $qv[ 'object_type' ] ) ) {
+			$meta_query[0]['object'] = $qv[ 'object_type' ];
+		}
+
+		$existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
+
+		if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
+			$meta_query = array(
+				'relation' => 'AND',
+				$primary_meta_query,
+				$existing_meta_query,
+			);
+		} elseif ( ! empty( $primary_meta_query ) ) {
+			$meta_query = array(
+				$primary_meta_query,
+			);
+		} elseif ( ! empty( $existing_meta_query ) ) {
+			$meta_query = $existing_meta_query;
+		}
+
+		$this->__construct( $meta_query );
+	}
+
+	/**
+	 * Generates SQL clauses to be appended to a main query.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param string      $type              Type of meta, eg 'user', 'post'.
+	 * @param string      $primary_table     Database table where the object being filtered is stored (eg wp_users).
+	 * @param string      $primary_id_column ID column for the filtered object in $primary_table.
+	 * @param object|null $context           Optional. The main query object.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
+		if ( ! $meta_table = _get_meta_table( $type ) ) {
+			return false;
+		}
+
+		$this->meta_table     = $meta_table;
+		$this->meta_id_column = 'object_id';
+
+		$this->primary_table     = $primary_table;
+		$this->primary_id_column = $primary_id_column;
+
+		$sql = $this->get_sql_clauses();
+
+		/*
+		 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
+		 * be LEFT. Otherwise posts with no metadata will be excluded from results.
+		 */
+		if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
+			$sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
+		}
+
+		/**
+		 * Filter the meta query's generated SQL.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array $args {
+		 *     An array of meta query SQL arguments.
+		 *
+		 *     @type array  $clauses           Array containing the query's JOIN and WHERE clauses.
+		 *     @type array  $queries           Array of meta queries.
+		 *     @type string $type              Type of meta.
+		 *     @type string $primary_table     Primary table.
+		 *     @type string $primary_id_column Primary column ID.
+		 *     @type object $context           The main query object.
+		 * }
+		 */
+		return apply_filters_ref_array( 'bp_xprofile_get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
+	}
+
+	/**
+	 * Generate SQL JOIN and WHERE clauses for a first-order query clause.
+	 *
+	 * "First-order" means that it's an array with a 'key' or 'value'.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param array  $clause       Query clause, passed by reference.
+	 * @param array  $parent_query Parent query array.
+	 * @param string $clause_key   Optional. The array key used to name the clause in the original `$meta_query`
+	 *                             parameters. If not provided, a key will be generated automatically.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
+		global $wpdb;
+
+		$sql_chunks = array(
+			'where' => array(),
+			'join'  => array(),
+		);
+
+		if ( isset( $clause['compare'] ) ) {
+			$clause['compare'] = strtoupper( $clause['compare'] );
+		} else {
+			$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
+		}
+
+		if ( ! in_array( $clause['compare'], array(
+			'=', '!=', '>', '>=', '<', '<=',
+			'LIKE', 'NOT LIKE',
+			'IN', 'NOT IN',
+			'BETWEEN', 'NOT BETWEEN',
+			'EXISTS', 'NOT EXISTS',
+			'REGEXP', 'NOT REGEXP', 'RLIKE'
+		) ) ) {
+			$clause['compare'] = '=';
+		}
+
+		$meta_compare = $clause['compare'];
+
+		// First build the JOIN clause, if one is required.
+		$join = '';
+
+		// We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
+		$alias = $this->find_compatible_table_alias( $clause, $parent_query );
+		if ( false === $alias ) {
+			$i = count( $this->table_aliases );
+			$alias = $i ? 'mt' . $i : $this->meta_table;
+
+			// JOIN clauses for NOT EXISTS have their own syntax.
+			if ( 'NOT EXISTS' === $meta_compare ) {
+				$join .= " LEFT JOIN $this->meta_table";
+				$join .= $i ? " AS $alias" : '';
+				$join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
+
+			// All other JOIN clauses.
+			} else {
+				$join .= " INNER JOIN $this->meta_table";
+				$join .= $i ? " AS $alias" : '';
+				$join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
+			}
+
+			$this->table_aliases[] = $alias;
+			$sql_chunks['join'][]  = $join;
+		}
+
+		// Save the alias to this clause, for future siblings to find.
+		$clause['alias'] = $alias;
+
+		// Determine the data type.
+		$_meta_type     = isset( $clause['type'] ) ? $clause['type'] : '';
+		$meta_type      = $this->get_cast_for_type( $_meta_type );
+		$clause['cast'] = $meta_type;
+
+		// Fallback for clause keys is the table alias.
+		if ( ! $clause_key ) {
+			$clause_key = $clause['alias'];
+		}
+
+		// Ensure unique clause keys, so none are overwritten.
+		$iterator = 1;
+		$clause_key_base = $clause_key;
+		while ( isset( $this->clauses[ $clause_key ] ) ) {
+			$clause_key = $clause_key_base . '-' . $iterator;
+			$iterator++;
+		}
+
+		// Store the clause in our flat array.
+		$this->clauses[ $clause_key ] =& $clause;
+
+		// Next, build the WHERE clause.
+		// Meta_key.
+		if ( array_key_exists( 'key', $clause ) ) {
+			if ( 'NOT EXISTS' === $meta_compare ) {
+				$sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
+			} else {
+				$sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
+			}
+		}
+
+		// Meta_value.
+		if ( array_key_exists( 'value', $clause ) ) {
+			$meta_value = $clause['value'];
+
+			if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
+				if ( ! is_array( $meta_value ) ) {
+					$meta_value = preg_split( '/[,\s]+/', $meta_value );
+				}
+			} else {
+				$meta_value = trim( $meta_value );
+			}
+
+			switch ( $meta_compare ) {
+				case 'IN' :
+				case 'NOT IN' :
+					$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
+					$where = $wpdb->prepare( $meta_compare_string, $meta_value );
+					break;
+
+				case 'BETWEEN' :
+				case 'NOT BETWEEN' :
+					$meta_value = array_slice( $meta_value, 0, 2 );
+					$where = $wpdb->prepare( '%s AND %s', $meta_value );
+					break;
+
+				case 'LIKE' :
+				case 'NOT LIKE' :
+					$meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
+					$where = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+				// EXISTS with a value is interpreted as '='.
+				case 'EXISTS' :
+					$meta_compare = '=';
+					$where = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+				// 'value' is ignored for NOT EXISTS.
+				case 'NOT EXISTS' :
+					$where = '';
+					break;
+
+				default :
+					$where = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+			}
+
+			if ( $where ) {
+				$sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
+			}
+		}
+
+		// Object_type.
+		if ( array_key_exists( 'object', $clause ) ) {
+			$object_type = $clause['object'];
+
+			if ( in_array( $meta_compare, array( 'IN', 'NOT IN' ) ) ) {
+				if ( ! is_array( $object_type ) ) {
+					$object_type = preg_split( '/[,\s]+/', $object_type );
+				}
+			} else {
+				$object_type = trim( $object_type );
+			}
+
+			switch ( $meta_compare ) {
+				case 'IN' :
+				case 'NOT IN' :
+					$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $object_type ) ), 1 ) . ')';
+					$object_where        = $wpdb->prepare( $meta_compare_string, $object_type );
+					break;
+
+				case 'LIKE' :
+				case 'NOT LIKE' :
+					$object_type  = '%' . $wpdb->esc_like( $object_type ) . '%';
+					$object_where = $wpdb->prepare( '%s', $object_type );
+					break;
+
+				// EXISTS with a value is interpreted as '='.
+				case 'EXISTS' :
+					$meta_compare = '=';
+					$object_where = $wpdb->prepare( '%s', $object_type );
+					break;
+
+				// 'value' is ignored for NOT EXISTS.
+				case 'NOT EXISTS' :
+					$object_where = '';
+					break;
+
+				default :
+					$object_where = $wpdb->prepare( '%s', $object_type );
+					break;
+			}
+
+			if ( ! empty( $object_where ) ) {
+				$sql_chunks['where'][] = "{$alias}.object_type {$meta_compare} {$object_where}";
+			}
+		}
+
+		/*
+		 * Multiple WHERE clauses (for meta_key and meta_value) should
+		 * be joined in parentheses.
+		 */
+		if ( 1 < count( $sql_chunks['where'] ) ) {
+			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
+		}
+
+		return $sql_chunks;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-profiledata.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-profiledata.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-profiledata.php	(revision 12265)
@@ -0,0 +1,722 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 1.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class for XProfile Profile Data setup.
+ *
+ * @since 1.6.0
+ */
+class BP_XProfile_ProfileData {
+
+	/**
+	 * XProfile ID.
+	 *
+	 * @since 1.6.0
+	 * @var int $id
+	 */
+	public $id;
+
+	/**
+	 * User ID.
+	 *
+	 * @since 1.6.0
+	 * @var int $user_id
+	 */
+	public $user_id;
+
+	/**
+	 * XProfile field ID.
+	 *
+	 * @since 1.6.0
+	 * @var int $field_id
+	 */
+	public $field_id;
+
+	/**
+	 * XProfile field value.
+	 *
+	 * @since 1.6.0
+	 * @var string $value
+	 */
+	public $value;
+
+	/**
+	 * XProfile field last updated time.
+	 *
+	 * @since 1.6.0
+	 * @var string $last_updated
+	 */
+	public $last_updated;
+
+	/**
+	 * BP_XProfile_ProfileData constructor.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|null $field_id Field ID to instantiate.
+	 * @param int|null $user_id  User ID to instantiate for.
+	 */
+	public function __construct( $field_id = null, $user_id = null ) {
+		if ( !empty( $field_id ) ) {
+			$this->populate( $field_id, $user_id );
+		}
+	}
+
+	/**
+	 * Populates the XProfile profile data.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $field_id Field ID to populate.
+	 * @param int $user_id  User ID to populate for.
+	 */
+	public function populate( $field_id, $user_id ) {
+		global $wpdb;
+
+		$cache_key   = "{$user_id}:{$field_id}";
+		$profiledata = wp_cache_get( $cache_key, 'bp_xprofile_data' );
+
+		if ( false === $profiledata ) {
+			$bp = buddypress();
+
+			$sql         = $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id = %d", $field_id, $user_id );
+			$profiledata = $wpdb->get_row( $sql );
+
+			if ( $profiledata ) {
+				wp_cache_set( $cache_key, $profiledata, 'bp_xprofile_data' );
+			}
+		}
+
+		if ( $profiledata ) {
+			$this->id           = (int) $profiledata->id;
+			$this->user_id      = (int) $profiledata->user_id;
+			$this->field_id     = (int) $profiledata->field_id;
+			$this->value        = stripslashes( $profiledata->value );
+			$this->last_updated = $profiledata->last_updated;
+
+		} else {
+			// When no row is found, we'll need to set these properties manually.
+			$this->field_id	    = (int) $field_id;
+			$this->user_id	    = (int) $user_id;
+		}
+	}
+
+	/**
+	 * Check if there is data already for the user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $wpdb
+	 * @global array $bp
+	 *
+	 * @return bool
+	 */
+	public function exists() {
+		global $wpdb;
+
+		// Check cache first.
+		$cache_key = "{$this->user_id}:{$this->field_id}";
+		$cached    = wp_cache_get( $cache_key, 'bp_xprofile_data' );
+
+		if ( $cached && ! empty( $cached->id ) ) {
+			$retval = true;
+		} else {
+			$bp = buddypress();
+			$retval = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_data} WHERE user_id = %d AND field_id = %d", $this->user_id, $this->field_id ) );
+		}
+
+		/**
+		 * Filters whether or not data already exists for the user.
+		 *
+		 * @since 1.2.7
+		 *
+		 * @param bool                    $retval Whether or not data already exists.
+		 * @param BP_XProfile_ProfileData $this   Instance of the current BP_XProfile_ProfileData class.
+		 */
+		return apply_filters_ref_array( 'xprofile_data_exists', array( (bool)$retval, $this ) );
+	}
+
+	/**
+	 * Check if this data is for a valid field.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @return bool
+	 */
+	public function is_valid_field() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$retval = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE id = %d", $this->field_id ) );
+
+		/**
+		 * Filters whether or not data is for a valid field.
+		 *
+		 * @since 1.2.7
+		 *
+		 * @param bool                    $retval Whether or not data is valid.
+		 * @param BP_XProfile_ProfileData $this   Instance of the current BP_XProfile_ProfileData class.
+		 */
+		return apply_filters_ref_array( 'xprofile_data_is_valid_field', array( (bool)$retval, $this ) );
+	}
+
+	/**
+	 * Save the data for the XProfile field.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return bool
+	 */
+	public function save() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		/**
+		 * Filters the data's user ID before saving to the database.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $user_id The user ID.
+		 * @param int $data_id The field data ID.
+		 */
+		$this->user_id = apply_filters( 'xprofile_data_user_id_before_save', $this->user_id, $this->id );
+
+		/**
+		 * Filters the data's field ID before saving to the database.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $field_id The field ID.
+		 * @param int $data_id  The field data ID.
+		 */
+		$this->field_id = apply_filters( 'xprofile_data_field_id_before_save', $this->field_id, $this->id );
+
+		/**
+		 * Filters the data's value before saving to the database.
+		 *
+		 * @since 1.0.0
+		 * @since 2.1.0 Added `$reserialize` and `$this` parameters.
+		 *
+		 * @param string                  $field_value The field value.
+		 * @param int                     $data_id     The field data ID.
+		 * @param bool                    $reserialize Whether to reserialize arrays before returning. Defaults to true.
+		 * @param BP_XProfile_ProfileData $this        Current instance of the profile data being saved.
+		 */
+		$this->value = apply_filters( 'xprofile_data_value_before_save', $this->value, $this->id, true, $this );
+
+		/**
+		 * Filters the data's last updated timestamp before saving to the database.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $last_updated The last updated timestamp.
+		 * @param int $data_id      The field data ID.
+		 */
+		$this->last_updated = apply_filters( 'xprofile_data_last_updated_before_save', bp_core_current_time(), $this->id );
+
+		/**
+		 * Fires before the current profile data instance gets saved.
+		 *
+		 * Please use this hook to filter the properties above. Each part will be passed in.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param BP_XProfile_ProfileData $this Current instance of the profile data being saved.
+		 */
+		do_action_ref_array( 'xprofile_data_before_save', array( $this ) );
+
+		if ( $this->is_valid_field() ) {
+			if ( $this->exists() && strlen( trim( $this->value ) ) ) {
+				$result   = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->profile->table_name_data} SET value = %s, last_updated = %s WHERE user_id = %d AND field_id = %d", $this->value, $this->last_updated, $this->user_id, $this->field_id ) );
+
+			} elseif ( $this->exists() && empty( $this->value ) ) {
+				// Data removed, delete the entry.
+				$result   = $this->delete();
+
+			} else {
+				$result   = $wpdb->query( $wpdb->prepare("INSERT INTO {$bp->profile->table_name_data} (user_id, field_id, value, last_updated) VALUES (%d, %d, %s, %s)", $this->user_id, $this->field_id, $this->value, $this->last_updated ) );
+				$this->id = $wpdb->insert_id;
+			}
+
+			if ( false === $result ) {
+				return false;
+			}
+
+			/**
+			 * Fires after the current profile data instance gets saved.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param BP_XProfile_ProfileData $this Current instance of the profile data being saved.
+			 */
+			do_action_ref_array( 'xprofile_data_after_save', array( $this ) );
+
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Delete specific XProfile field data.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @global object $wpdb
+	 *
+	 * @return boolean
+	 */
+	public function delete() {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		/**
+		 * Fires before the current profile data instance gets deleted.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param BP_XProfile_ProfileData $this Current instance of the profile data being deleted.
+		 */
+		do_action_ref_array( 'xprofile_data_before_delete', array( $this ) );
+
+		$deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id = %d", $this->field_id, $this->user_id ) );
+		if ( empty( $deleted ) ) {
+			return false;
+		}
+
+		/**
+		 * Fires after the current profile data instance gets deleted.
+		 *
+		 * @since 1.9.0
+		 *
+		 * @param BP_XProfile_ProfileData $this Current instance of the profile data being deleted.
+		 */
+		do_action_ref_array( 'xprofile_data_after_delete', array( $this ) );
+
+		return true;
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Get a user's profile data for a set of fields.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int   $user_id   ID of user whose data is being queried.
+	 * @param array $field_ids Array of field IDs to query for.
+	 * @return array
+	 */
+	public static function get_data_for_user( $user_id, $field_ids ) {
+		global $wpdb;
+
+		$data = array();
+
+		$uncached_field_ids = bp_xprofile_get_non_cached_field_ids( $user_id, $field_ids );
+
+		// Prime the cache.
+		if ( ! empty( $uncached_field_ids ) ) {
+			$bp = buddypress();
+			$uncached_field_ids_sql = implode( ',', wp_parse_id_list( $uncached_field_ids ) );
+			$uncached_data = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, field_id, value, last_updated FROM {$bp->profile->table_name_data} WHERE field_id IN ({$uncached_field_ids_sql}) AND user_id = %d", $user_id ) );
+
+			// Rekey.
+			$queried_data = array();
+			foreach ( $uncached_data as $ud ) {
+				$d               = new stdClass;
+				$d->id           = $ud->id;
+				$d->user_id      = $ud->user_id;
+				$d->field_id     = $ud->field_id;
+				$d->value        = $ud->value;
+				$d->last_updated = $ud->last_updated;
+
+				$queried_data[ $ud->field_id ] = $d;
+			}
+
+			// Set caches.
+			foreach ( $uncached_field_ids as $field_id ) {
+
+				$cache_key = "{$user_id}:{$field_id}";
+
+				// If a value was found, cache it.
+				if ( isset( $queried_data[ $field_id ] ) ) {
+					wp_cache_set( $cache_key, $queried_data[ $field_id ], 'bp_xprofile_data' );
+
+				// If no value was found, cache an empty item
+				// to avoid future cache misses.
+				} else {
+					$d               = new stdClass;
+					$d->id           = '';
+					$d->user_id      = $user_id;
+					$d->field_id     = $field_id;
+					$d->value        = '';
+					$d->last_updated = '';
+
+					wp_cache_set( $cache_key, $d, 'bp_xprofile_data' );
+				}
+			}
+		}
+
+		// Now that all items are cached, fetch them.
+		foreach ( $field_ids as $field_id ) {
+			$cache_key = "{$user_id}:{$field_id}";
+			$data[]    = wp_cache_get( $cache_key, 'bp_xprofile_data' );
+		}
+
+		// Integer casting.
+		foreach ( (array) $data as $key => $d ) {
+			if ( isset( $data[ $key ]->id ) ) {
+				$data[ $key ]->id = (int) $data[ $key ]->id;
+			}
+			if ( isset( $data[ $key ]->user_id ) ) {
+				$data[ $key ]->user_id  = (int) $data[ $key ]->user_id;
+			}
+
+			$data[ $key ]->field_id = (int) $data[ $key ]->field_id;
+		}
+
+		return $data;
+	}
+
+	/**
+	 * Get all of the profile information for a specific user.
+	 *
+	 * @since 1.2.0
+	 *
+	 * @param int $user_id ID of the user.
+	 * @return array
+	 */
+	public static function get_all_for_user( $user_id ) {
+
+		$groups = bp_xprofile_get_groups( array(
+			'user_id'                => $user_id,
+			'hide_empty_groups'      => true,
+			'hide_empty_fields'      => true,
+			'fetch_fields'           => true,
+			'fetch_field_data'       => true,
+		) );
+
+		$profile_data = array();
+
+		if ( ! empty( $groups ) ) {
+			$user = new WP_User( $user_id );
+
+			$profile_data['user_login']    = $user->user_login;
+			$profile_data['user_nicename'] = $user->user_nicename;
+			$profile_data['user_email']    = $user->user_email;
+
+			foreach ( (array) $groups as $group ) {
+				if ( empty( $group->fields ) ) {
+					continue;
+				}
+
+				foreach ( (array) $group->fields as $field ) {
+					$profile_data[ $field->name ] = array(
+						'field_group_id'   => $group->id,
+						'field_group_name' => $group->name,
+						'field_id'         => $field->id,
+						'field_type'       => $field->type,
+						'field_data'       => $field->data->value,
+					);
+				}
+			}
+		}
+
+		return $profile_data;
+	}
+
+	/**
+	 * Get the user's field data id by the id of the xprofile field.
+	 *
+	 * @since 1.6.0
+	 *
+	 * @param int $field_id Field ID being queried for.
+	 * @param int $user_id  User ID associated with field.
+	 * @return int $fielddata_id
+	 */
+	public static function get_fielddataid_byid( $field_id, $user_id ) {
+		global $wpdb;
+
+		if ( empty( $field_id ) || empty( $user_id ) ) {
+			$fielddata_id = 0;
+		} else {
+			$bp = buddypress();
+
+			// Check cache first.
+			$cache_key = "{$user_id}:{$field_id}";
+			$fielddata = wp_cache_get( $cache_key, 'bp_xprofile_data' );
+			if ( false === $fielddata || empty( $fielddata->id ) ) {
+				$fielddata_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id = %d", $field_id, $user_id ) );
+			} else {
+				$fielddata_id = $fielddata->id;
+			}
+		}
+
+		return (int) $fielddata_id;
+	}
+
+	/**
+	 * Get profile field values by field ID and user IDs.
+	 *
+	 * Supports multiple user IDs.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int            $field_id ID of the field.
+	 * @param int|array|null $user_ids ID or IDs of user(s).
+	 * @return string|array Single value if a single user is queried,
+	 *                      otherwise an array of results.
+	 */
+	public static function get_value_byid( $field_id, $user_ids = null ) {
+		global $wpdb;
+
+		if ( empty( $user_ids ) ) {
+			$user_ids = bp_displayed_user_id();
+		}
+
+		$return_single_result = false;
+		if ( ! is_array( $user_ids ) ) {
+			$return_single_result = true;
+		}
+
+		$user_ids = wp_parse_id_list( $user_ids );
+
+		// Assemble uncached IDs.
+		$uncached_ids = array();
+		foreach ( $user_ids as $user_id ) {
+			$cache_key = "{$user_id}:{$field_id}";
+			if ( false === wp_cache_get( $cache_key, 'bp_xprofile_data' ) ) {
+				$uncached_ids[] = $user_id;
+			}
+		}
+
+		// Prime caches.
+		if ( ! empty( $uncached_ids ) ) {
+			$bp = buddypress();
+			$uncached_ids_sql = implode( ',', $uncached_ids );
+			$queried_data = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, field_id, value, last_updated FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id IN ({$uncached_ids_sql})", $field_id ) );
+
+			// Rekey.
+			$qd = array();
+			foreach ( $queried_data as $data ) {
+				$qd[ $data->user_id ] = $data;
+			}
+
+			foreach ( $uncached_ids as $id ) {
+				// The value was successfully fetched.
+				if ( isset( $qd[ $id ] ) ) {
+					$d = $qd[ $id ];
+
+				// No data found for the user, so we fake it to
+				// avoid cache misses and PHP notices.
+				} else {
+					$d = new stdClass;
+					$d->id           = '';
+					$d->user_id      = $id;
+					$d->field_id     = $field_id;
+					$d->value        = '';
+					$d->last_updated = '';
+				}
+
+				$cache_key = "{$d->user_id}:{$field_id}";
+				wp_cache_set( $cache_key, $d, 'bp_xprofile_data' );
+			}
+		}
+
+		// Now that the cache is primed with all data, fetch it.
+		$data = array();
+		foreach ( $user_ids as $user_id ) {
+			$cache_key = "{$user_id}:{$field_id}";
+			$data[]    = wp_cache_get( $cache_key, 'bp_xprofile_data' );
+		}
+
+		// Integer casting.
+		foreach ( (array) $data as $key => $d ) {
+			if ( isset( $data[ $key ]->id ) ) {
+				$data[ $key ]->id = (int) $data[ $key ]->id;
+			}
+			if ( isset( $data[ $key ]->user_id ) ) {
+				$data[ $key ]->user_id  = (int) $data[ $key ]->user_id;
+			}
+
+			$data[ $key ]->field_id = (int) $data[ $key ]->field_id;
+		}
+
+		// If a single ID was passed, just return the value.
+		if ( $return_single_result ) {
+			return $data[0]->value;
+
+		// Otherwise return the whole array.
+		} else {
+			return $data;
+		}
+	}
+
+	/**
+	 * Get profile field values by field name and user ID.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param array|string $fields  Field(s) to get.
+	 * @param int|null     $user_id User ID to get field data for.
+	 * @return array|bool
+	 */
+	public static function get_value_byfieldname( $fields, $user_id = null ) {
+		global $wpdb;
+
+		if ( empty( $fields ) ) {
+			return false;
+		}
+
+		$bp = buddypress();
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_displayed_user_id();
+		}
+
+		$field_sql = '';
+
+		if ( is_array( $fields ) ) {
+			for ( $i = 0, $count = count( $fields ); $i < $count; ++$i ) {
+				if ( $i == 0 ) {
+					$field_sql .= $wpdb->prepare( "AND ( f.name = %s ", $fields[$i] );
+				} else {
+					$field_sql .= $wpdb->prepare( "OR f.name = %s ", $fields[$i] );
+				}
+			}
+
+			$field_sql .= ')';
+		} else {
+			$field_sql .= $wpdb->prepare( "AND f.name = %s", $fields );
+		}
+
+		$sql    = $wpdb->prepare( "SELECT d.value, f.name FROM {$bp->profile->table_name_data} d, {$bp->profile->table_name_fields} f WHERE d.field_id = f.id AND d.user_id = %d AND f.parent_id = 0 $field_sql", $user_id );
+		$values = $wpdb->get_results( $sql );
+
+		if ( empty( $values ) || is_wp_error( $values ) ) {
+			return false;
+		}
+
+		$new_values = array();
+
+		if ( is_array( $fields ) ) {
+			for ( $i = 0, $count = count( $values ); $i < $count; ++$i ) {
+				for ( $j = 0; $j < count( $fields ); $j++ ) {
+					if ( $values[$i]->name == $fields[$j] ) {
+						$new_values[$fields[$j]] = $values[$i]->value;
+					} elseif ( !array_key_exists( $fields[$j], $new_values ) ) {
+						$new_values[$fields[$j]] = NULL;
+					}
+				}
+			}
+		} else {
+			$new_values = $values[0]->value;
+		}
+
+		return $new_values;
+	}
+
+	/**
+	 * Delete field.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $field_id ID of the field to delete.
+	 * @return bool
+	 */
+	public static function delete_for_field( $field_id ) {
+		global $wpdb;
+
+		$bp      = buddypress();
+		$deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_data} WHERE field_id = %d", $field_id ) );
+		if ( empty( $deleted ) || is_wp_error( $deleted ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Get time for last XProfile field data update by user.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id User ID to get time for.
+	 * @return null|string
+	 */
+	public static function get_last_updated( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$last_updated = $wpdb->get_var( $wpdb->prepare( "SELECT last_updated FROM {$bp->profile->table_name_data} WHERE user_id = %d ORDER BY last_updated LIMIT 1", $user_id ) );
+
+		return $last_updated;
+	}
+
+	/**
+	 * Delete all data for provided user ID.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id User ID to remove data for.
+	 * @return false|int
+	 */
+	public static function delete_data_for_user( $user_id ) {
+		global $wpdb;
+
+		$bp = buddypress();
+
+		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_data} WHERE user_id = %d", $user_id ) );
+	}
+
+	/**
+	 * Get random field type by user ID.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int    $user_id          User ID to query for.
+	 * @param string $exclude_fullname SQL portion used to exclude by field ID.
+	 * @return array|null|object
+	 */
+	public static function get_random( $user_id, $exclude_fullname ) {
+		global $wpdb;
+
+		$exclude_sql = ! empty( $exclude_fullname ) ? ' AND pf.id != 1' : '';
+
+		$bp = buddypress();
+
+		return $wpdb->get_results( $wpdb->prepare( "SELECT pf.type, pf.name, pd.value FROM {$bp->profile->table_name_data} pd INNER JOIN {$bp->profile->table_name_fields} pf ON pd.field_id = pf.id AND pd.user_id = %d {$exclude_sql} ORDER BY RAND() LIMIT 1", $user_id ) );
+	}
+
+	/**
+	 * Get fullname for provided user ID.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param int $user_id ID of the user to query.
+	 * @return mixed
+	 */
+	public static function get_fullname( $user_id = 0 ) {
+
+		if ( empty( $user_id ) ) {
+			$user_id = bp_displayed_user_id();
+		}
+
+		return xprofile_get_field_data( bp_xprofile_fullname_field_id(), $user_id );
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-query.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-query.php	(revision 12265)
@@ -0,0 +1,532 @@
+<?php
+/**
+ * BuddyPress XProfile Classes.
+ *
+ * @package BuddyPress
+ * @subpackage XProfileClasses
+ * @since 2.2.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class for generating SQL clauses to filter a user query by xprofile data.
+ *
+ * @since 2.2.0
+ */
+class BP_XProfile_Query {
+
+	/**
+	 * Array of xprofile queries.
+	 *
+	 * See {@see WP_XProfile_Query::__construct()} for information on parameters.
+	 *
+	 * @since 2.2.0
+	 * @var array
+	 */
+	public $queries = array();
+
+	/**
+	 * Database table that where the metadata's objects are stored (eg $wpdb->users).
+	 *
+	 * @since 2.2.0
+	 * @var string
+	 */
+	public $primary_table;
+
+	/**
+	 * Column in primary_table that represents the ID of the object.
+	 *
+	 * @since 2.2.0
+	 * @var string
+	 */
+	public $primary_id_column;
+
+	/**
+	 * A flat list of table aliases used in JOIN clauses.
+	 *
+	 * @since 2.2.0
+	 * @var array
+	 */
+	protected $table_aliases = array();
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $xprofile_query {
+	 *     Array of xprofile query clauses.
+	 *
+	 *     @type string $relation Optional. The MySQL keyword used to join the clauses of the query.
+	 *                            Accepts 'AND', or 'OR'. Default 'AND'.
+	 *     @type array {
+	 *         Optional. An array of first-order clause parameters, or another fully-formed xprofile query.
+	 *
+	 *         @type string|int $field   XProfile field to filter by. Accepts a field name or ID.
+	 *         @type string     $value   XProfile value to filter by.
+	 *         @type string     $compare MySQL operator used for comparing the $value. Accepts '=', '!=', '>',
+	 *                                   '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN',
+	 *                                   'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', or 'RLIKE'. Default is 'IN'
+	 *                                   when `$value` is an array, '=' otherwise.
+	 *         @type string     $type    MySQL data type that the `value` column will be CAST to for comparisons.
+	 *                                   Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL',
+	 *                                   'SIGNED', 'TIME', or 'UNSIGNED'. Default is 'CHAR'.
+	 *     }
+	 * }
+	 */
+	public function __construct( $xprofile_query ) {
+		if ( empty( $xprofile_query ) ) {
+			return;
+		}
+
+		$this->queries = $this->sanitize_query( $xprofile_query );
+	}
+
+	/**
+	 * Ensure the `xprofile_query` argument passed to the class constructor is well-formed.
+	 *
+	 * Eliminates empty items and ensures that a 'relation' is set.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $queries Array of query clauses.
+	 * @return array Sanitized array of query clauses.
+	 */
+	public function sanitize_query( $queries ) {
+		$clean_queries = array();
+
+		if ( ! is_array( $queries ) ) {
+			return $clean_queries;
+		}
+
+		foreach ( $queries as $key => $query ) {
+			if ( 'relation' === $key ) {
+				$relation = $query;
+
+			} elseif ( ! is_array( $query ) ) {
+				continue;
+
+			// First-order clause.
+			} elseif ( $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.
+		 */
+		} elseif ( 1 === count( $clean_queries ) ) {
+			$clean_queries['relation'] = 'OR';
+
+		// Default to AND.
+		} else {
+			$clean_queries['relation'] = 'AND';
+		}
+
+		return $clean_queries;
+	}
+
+	/**
+	 * Determine whether a query clause is first-order.
+	 *
+	 * A first-order query clause is one that has either a 'key' or a 'value' array key.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param  array $query XProfile query arguments.
+	 * @return bool  Whether the query clause is a first-order clause.
+	 */
+	protected function is_first_order_clause( $query ) {
+		return isset( $query['field'] ) || isset( $query['value'] );
+	}
+
+	/**
+	 * Return the appropriate alias for the given field type if applicable.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $type MySQL type to cast `value`.
+	 * @return string MySQL type.
+	 */
+	public function get_cast_for_type( $type = '' ) {
+		if ( empty( $type ) ) {
+			return 'CHAR';
+		}
+
+		$meta_type = strtoupper( $type );
+
+		if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) {
+			return 'CHAR';
+		}
+
+		if ( 'NUMERIC' === $meta_type ) {
+			$meta_type = 'SIGNED';
+		}
+
+		return $meta_type;
+	}
+
+	/**
+	 * Generate SQL clauses to be appended to a main query.
+	 *
+	 * Called by the public {@see BP_XProfile_Query::get_sql()}, this method is abstracted out to maintain parity
+	 * with WP's Query classes.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	protected function get_sql_clauses() {
+		/*
+		 * $queries are passed by reference to get_sql_for_query() for recursion.
+		 * To keep $this->queries unaltered, pass a copy.
+		 */
+		$queries = $this->queries;
+		$sql = $this->get_sql_for_query( $queries );
+
+		if ( ! empty( $sql['where'] ) ) {
+			$sql['where'] = ' AND ' . $sql['where'];
+		}
+
+		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.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param  array $query Query to parse. Passed by reference.
+	 * @param  int   $depth Optional. Number of tree levels deep we currently are. Used to calculate indentation.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	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 .= "  ";
+		}
+
+		foreach ( $query as $key => &$clause ) {
+			if ( 'relation' === $key ) {
+				$relation = $query['relation'];
+			} elseif ( 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'][] = '';
+					} elseif ( 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, so we recurse.
+				} else {
+					$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
+
+					$sql_chunks['where'][] = $clause_sql['where'];
+					$sql_chunks['join'][]  = $clause_sql['join'];
+				}
+			}
+		}
+
+		// Filter to remove empties.
+		$sql_chunks['join']  = array_filter( $sql_chunks['join'] );
+		$sql_chunks['where'] = array_filter( $sql_chunks['where'] );
+
+		if ( empty( $relation ) ) {
+			$relation = 'AND';
+		}
+
+		// Filter duplicate JOIN clauses and combine into a single string.
+		if ( ! empty( $sql_chunks['join'] ) ) {
+			$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
+		}
+
+		// Generate a single WHERE clause with proper brackets and indentation.
+		if ( ! empty( $sql_chunks['where'] ) ) {
+			$sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
+		}
+
+		return $sql;
+	}
+
+	/**
+	 * Generates SQL clauses to be appended to a main query.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
+	 * @param string $primary_id_column ID column for the filtered object in $primary_table.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql( $primary_table, $primary_id_column ) {
+
+		$this->primary_table     = $primary_table;
+		$this->primary_id_column = $primary_id_column;
+
+		$sql = $this->get_sql_clauses();
+
+		/*
+		 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
+		 * be LEFT. Otherwise posts with no metadata will be excluded from results.
+		 */
+		if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
+			$sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
+		}
+
+		return $sql;
+	}
+
+	/**
+	 * Generate SQL JOIN and WHERE clauses for a first-order query clause.
+	 *
+	 * "First-order" means that it's an array with a 'field' or 'value'.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $clause       Query clause.
+	 * @param array $parent_query Parent query array.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql_for_clause( &$clause, $parent_query ) {
+		global $wpdb;
+
+		$sql_chunks = array(
+			'where' => array(),
+			'join' => array(),
+		);
+
+		if ( isset( $clause['compare'] ) ) {
+			$clause['compare'] = strtoupper( $clause['compare'] );
+		} else {
+			$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
+		}
+
+		if ( ! in_array( $clause['compare'], array(
+			'=', '!=', '>', '>=', '<', '<=',
+			'LIKE', 'NOT LIKE',
+			'IN', 'NOT IN',
+			'BETWEEN', 'NOT BETWEEN',
+			'EXISTS', 'NOT EXISTS',
+			'REGEXP', 'NOT REGEXP', 'RLIKE'
+		) ) ) {
+			$clause['compare'] = '=';
+		}
+
+		$field_compare = $clause['compare'];
+
+		// First build the JOIN clause, if one is required.
+		$join = '';
+
+		$data_table = buddypress()->profile->table_name_data;
+
+		// We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
+		$alias = $this->find_compatible_table_alias( $clause, $parent_query );
+		if ( false === $alias ) {
+			$i = count( $this->table_aliases );
+			$alias = $i ? 'xpq' . $i : $data_table;
+
+			// JOIN clauses for NOT EXISTS have their own syntax.
+			if ( 'NOT EXISTS' === $field_compare ) {
+				$join .= " LEFT JOIN $data_table";
+				$join .= $i ? " AS $alias" : '';
+				$join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.user_id AND $alias.field_id = %d )", $clause['field'] );
+
+			// All other JOIN clauses.
+			} else {
+				$join .= " INNER JOIN $data_table";
+				$join .= $i ? " AS $alias" : '';
+				$join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.user_id )";
+			}
+
+			$this->table_aliases[] = $alias;
+			$sql_chunks['join'][] = $join;
+		}
+
+		// Save the alias to this clause, for future siblings to find.
+		$clause['alias'] = $alias;
+
+		// Next, build the WHERE clause.
+		$where = '';
+
+		// Field_id.
+		if ( array_key_exists( 'field', $clause ) ) {
+			// Convert field name to ID if necessary.
+			if ( ! is_numeric( $clause['field'] ) ) {
+				$clause['field'] = xprofile_get_field_id_from_name( $clause['field'] );
+			}
+
+			// NOT EXISTS has its own syntax.
+			if ( 'NOT EXISTS' === $field_compare ) {
+				$sql_chunks['where'][] = $alias . '.user_id IS NULL';
+			} else {
+				$sql_chunks['where'][] = $wpdb->prepare( "$alias.field_id = %d", $clause['field'] );
+			}
+		}
+
+		// Value.
+		if ( array_key_exists( 'value', $clause ) ) {
+			$field_value = $clause['value'];
+			$field_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
+
+			if ( in_array( $field_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
+				if ( ! is_array( $field_value ) ) {
+					$field_value = preg_split( '/[,\s]+/', $field_value );
+				}
+			} else {
+				$field_value = trim( $field_value );
+			}
+
+			switch ( $field_compare ) {
+				case 'IN' :
+				case 'NOT IN' :
+					$field_compare_string = '(' . substr( str_repeat( ',%s', count( $field_value ) ), 1 ) . ')';
+					$where = $wpdb->prepare( $field_compare_string, $field_value );
+					break;
+
+				case 'BETWEEN' :
+				case 'NOT BETWEEN' :
+					$field_value = array_slice( $field_value, 0, 2 );
+					$where = $wpdb->prepare( '%s AND %s', $field_value );
+					break;
+
+				case 'LIKE' :
+				case 'NOT LIKE' :
+					$field_value = '%' . bp_esc_like( $field_value ) . '%';
+					$where = $wpdb->prepare( '%s', $field_value );
+					break;
+
+				default :
+					$where = $wpdb->prepare( '%s', $field_value );
+					break;
+
+			}
+
+			if ( $where ) {
+				$sql_chunks['where'][] = "CAST($alias.value AS {$field_type}) {$field_compare} {$where}";
+			}
+		}
+
+		/*
+		 * Multiple WHERE clauses (`field` and `value` pairs) should be joined in parentheses.
+		 */
+		if ( 1 < count( $sql_chunks['where'] ) ) {
+			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
+		}
+
+		return $sql_chunks;
+	}
+
+	/**
+	 * Identify an existing table alias that is compatible with the current query clause.
+	 *
+	 * We avoid unnecessary table joins by allowing each clause to look for an existing table alias that is
+	 * compatible with the query that it needs to perform. An existing alias is compatible if (a) it is a
+	 * sibling of $clause (ie, it's under the scope of the same relation), and (b) the combination of
+	 * operator and relation between the clauses allows for a shared table join. In the case of BP_XProfile_Query,
+	 * this * only applies to IN clauses that are connected by the relation OR.
+	 *
+	 * @since 2.2.0
+	 *
+	 * @param array $clause       Query clause.
+	 * @param array $parent_query Parent query of $clause.
+	 * @return string|bool Table alias if found, otherwise false.
+	 */
+	protected function find_compatible_table_alias( $clause, $parent_query ) {
+		$alias = false;
+
+		foreach ( $parent_query as $sibling ) {
+			// If the sibling has no alias yet, there's nothing to check.
+			if ( empty( $sibling['alias'] ) ) {
+				continue;
+			}
+
+			// We're only interested in siblings that are first-order clauses.
+			if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
+				continue;
+			}
+
+			$compatible_compares = array();
+
+			// Clauses connected by OR can share joins as long as they have "positive" operators.
+			if ( 'OR' === $parent_query['relation'] ) {
+				$compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
+
+			// Clauses joined by AND with "negative" operators share a join only if they also share a key.
+			} elseif ( isset( $sibling['field'] ) && isset( $clause['field'] ) && $sibling['field'] === $clause['field'] ) {
+				$compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
+			}
+
+			$clause_compare  = strtoupper( $clause['compare'] );
+			$sibling_compare = strtoupper( $sibling['compare'] );
+			if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) {
+				$alias = $sibling['alias'];
+				break;
+			}
+		}
+
+		return $alias;
+	}
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-user-admin.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-user-admin.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/classes/class-bp-xprofile-user-admin.php	(revision 12265)
@@ -0,0 +1,493 @@
+<?php
+/**
+ * BuddyPress XProfile Admin Class.
+ *
+ * @package BuddyPress
+ * @since 2.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+if ( ! class_exists( 'BP_XProfile_User_Admin' ) ) :
+
+/**
+ * Load xProfile Profile admin area.
+ *
+ * @since 2.0.0
+ */
+class BP_XProfile_User_Admin {
+
+	/**
+	 * Setup xProfile User Admin.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @return BP_XProfile_User_Admin
+	 */
+	public static function register_xprofile_user_admin() {
+
+		// Bail if not in admin.
+		if ( ! is_admin() ) {
+			return;
+		}
+
+		$bp = buddypress();
+
+		if ( empty( $bp->profile->admin ) ) {
+			$bp->profile->admin = new self;
+		}
+
+		return $bp->profile->admin;
+	}
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 2.0.0
+	 */
+	public function __construct() {
+		$this->setup_actions();
+	}
+
+	/**
+	 * Set admin-related actions and filters.
+	 *
+	 * @since 2.0.0
+	 */
+	private function setup_actions() {
+		// Enqueue scripts.
+		add_action( 'bp_members_admin_enqueue_scripts',  array( $this, 'enqueue_scripts'    ), 10, 1 );
+
+		// Register the metabox in Member's community admin profile.
+		add_action( 'bp_members_admin_xprofile_metabox', array( $this, 'register_metaboxes' ), 10, 3 );
+
+		// Saves the profile actions for user ( avatar, profile fields ).
+		add_action( 'bp_members_admin_update_user',      array( $this, 'user_admin_load'    ), 10, 4 );
+	}
+
+	/**
+	 * Enqueue needed scripts.
+	 *
+	 * @since 2.3.0
+	 *
+	 * @param int $screen_id Screen ID being displayed.
+	 */
+	public function enqueue_scripts( $screen_id ) {
+		if ( ( false === strpos( $screen_id, 'users_page_bp-profile-edit' )
+			&& false === strpos( $screen_id, 'profile_page_bp-profile-edit' ) )
+			|| bp_core_get_root_option( 'bp-disable-avatar-uploads' )
+			|| ! buddypress()->avatar->show_avatars
+			|| ! bp_attachments_is_wp_version_supported() ) {
+			return;
+		}
+
+		/**
+		 * Get Thickbox.
+		 *
+		 * We cannot simply use add_thickbox() here as WordPress is not playing
+		 * nice with Thickbox width/height see https://core.trac.wordpress.org/ticket/17249
+		 * Using media-upload might be interesting in the future for the send to editor stuff
+		 * and we make sure the tb_window is wide enough
+		 */
+		wp_enqueue_style ( 'thickbox' );
+		wp_enqueue_script( 'media-upload' );
+
+		// Get Avatar Uploader.
+		bp_attachments_enqueue_scripts( 'BP_Attachment_Avatar' );
+	}
+
+	/**
+	 * Register the xProfile metabox on Community Profile admin page.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param int         $user_id       ID of the user being edited.
+	 * @param string      $screen_id     Screen ID to load the metabox in.
+	 * @param object|null $stats_metabox Context and priority for the stats metabox.
+	 */
+	public function register_metaboxes( $user_id = 0, $screen_id = '', $stats_metabox = null ) {
+
+		// Set the screen ID if none was passed.
+		if ( empty( $screen_id ) ) {
+			$screen_id = buddypress()->members->admin->user_page;
+		}
+
+		// Setup a new metabox class if none was passed.
+		if ( empty( $stats_metabox ) ) {
+			$stats_metabox = new StdClass();
+		}
+
+		// Moving the Stats Metabox.
+		$stats_metabox->context  = 'side';
+		$stats_metabox->priority = 'low';
+
+		// Each Group of fields will have his own metabox.
+		$profile_args = array(
+			'fetch_fields' => false,
+			'user_id'      => $user_id,
+		);
+
+		if ( ! bp_is_user_spammer( $user_id ) && bp_has_profile( $profile_args ) ) {
+
+			// Loop through field groups and add a metabox for each one.
+			while ( bp_profile_groups() ) : bp_the_profile_group();
+				add_meta_box(
+					'bp_xprofile_user_admin_fields_' . sanitize_key( bp_get_the_profile_group_slug() ),
+					esc_html( bp_get_the_profile_group_name() ),
+					array( $this, 'user_admin_profile_metaboxes' ),
+					$screen_id,
+					'normal',
+					'core',
+					array( 'profile_group_id' => bp_get_the_profile_group_id() )
+				);
+			endwhile;
+
+
+		} else {
+			// If member is already a spammer, show a generic metabox.
+			add_meta_box(
+				'bp_xprofile_user_admin_empty_profile',
+				_x( 'User marked as a spammer', 'xprofile user-admin edit screen', 'buddypress' ),
+				array( $this, 'user_admin_spammer_metabox' ),
+				$screen_id,
+				'normal',
+				'core'
+			);
+		}
+
+		if ( buddypress()->avatar->show_avatars ) {
+			// Avatar Metabox.
+			add_meta_box(
+				'bp_xprofile_user_admin_avatar',
+				_x( 'Profile Photo', 'xprofile user-admin edit screen', 'buddypress' ),
+				array( $this, 'user_admin_avatar_metabox' ),
+				$screen_id,
+				'side',
+				'low'
+			);
+		}
+	}
+
+	/**
+	 * Save the profile fields in Members community profile page.
+	 *
+	 * Loaded before the page is rendered, this function is processing form
+	 * requests.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $doaction    Action being run.
+	 * @param int    $user_id     ID for the user whose profile is being saved.
+	 * @param array  $request     Request being made.
+	 * @param string $redirect_to Where to redirect user to.
+	 */
+	public function user_admin_load( $doaction = '', $user_id = 0, $request = array(), $redirect_to = '' ) {
+
+		// Eventually delete avatar.
+		if ( 'delete_avatar' === $doaction ) {
+
+			check_admin_referer( 'delete_avatar' );
+
+			$redirect_to = remove_query_arg( '_wpnonce', $redirect_to );
+
+			if ( bp_core_delete_existing_avatar( array( 'item_id' => $user_id ) ) ) {
+				$redirect_to = add_query_arg( 'updated', 'avatar', $redirect_to );
+			} else {
+				$redirect_to = add_query_arg( 'error', 'avatar', $redirect_to );
+			}
+
+			bp_core_redirect( $redirect_to );
+
+		} elseif ( isset( $_POST['field_ids'] ) ) {
+			// Update profile fields.
+			// Check the nonce.
+			check_admin_referer( 'edit-bp-profile_' . $user_id );
+
+			// Check we have field ID's.
+			if ( empty( $_POST['field_ids'] ) ) {
+				$redirect_to = add_query_arg( 'error', '1', $redirect_to );
+				bp_core_redirect( $redirect_to );
+			}
+
+			/**
+			 * Unlike front-end edit-fields screens, the wp-admin/profile
+			 * displays all groups of fields on a single page, so the list of
+			 * field ids is an array gathering for each group of fields a
+			 * distinct comma separated list of ids.
+			 *
+			 * As a result, before using the wp_parse_id_list() function, we
+			 * must ensure that these ids are "merged" into a single comma
+			 * separated list.
+			 */
+			$merge_ids = join( ',', $_POST['field_ids'] );
+
+			// Explode the posted field IDs into an array so we know which fields have been submitted.
+			$posted_field_ids = wp_parse_id_list( $merge_ids );
+			$is_required      = array();
+
+			// Loop through the posted fields formatting any datebox values then validate the field.
+			foreach ( (array) $posted_field_ids as $field_id ) {
+				bp_xprofile_maybe_format_datebox_post_data( $field_id );
+
+				$is_required[ $field_id ] = xprofile_check_is_required_field( $field_id ) && ! bp_current_user_can( 'bp_moderate' );
+				if ( $is_required[ $field_id ] && empty( $_POST['field_' . $field_id ] ) ) {
+					$redirect_to = add_query_arg( 'error', '2', $redirect_to );
+					bp_core_redirect( $redirect_to );
+				}
+			}
+
+			// Set the errors var.
+			$errors = false;
+
+			// Now we've checked for required fields, let's save the values.
+			$old_values = $new_values = array();
+			foreach ( (array) $posted_field_ids as $field_id ) {
+
+				/*
+				 * Certain types of fields (checkboxes, multiselects) may come
+				 * through empty. Save them as an empty array so that they don't
+				 * get overwritten by the default on the next edit.
+				 */
+				$value = isset( $_POST['field_' . $field_id] ) ? $_POST['field_' . $field_id] : '';
+
+				$visibility_level = ! empty( $_POST['field_' . $field_id . '_visibility'] ) ? $_POST['field_' . $field_id . '_visibility'] : 'public';
+				/*
+				 * Save the old and new values. They will be
+				 * passed to the filter and used to determine
+				 * whether an activity item should be posted.
+				 */
+				$old_values[ $field_id ] = array(
+					'value'      => xprofile_get_field_data( $field_id, $user_id ),
+					'visibility' => xprofile_get_field_visibility_level( $field_id, $user_id ),
+				);
+
+				// Update the field data and visibility level.
+				xprofile_set_field_visibility_level( $field_id, $user_id, $visibility_level );
+				$field_updated = xprofile_set_field_data( $field_id, $user_id, $value, $is_required[ $field_id ] );
+				$value         = xprofile_get_field_data( $field_id, $user_id );
+
+				$new_values[ $field_id ] = array(
+					'value'      => $value,
+					'visibility' => xprofile_get_field_visibility_level( $field_id, $user_id ),
+				);
+
+				if ( ! $field_updated ) {
+					$errors = true;
+				} else {
+
+					/**
+					 * Fires after the saving of each profile field, if successful.
+					 *
+					 * @since 1.1.0
+					 *
+					 * @param int    $field_id ID of the field being updated.
+					 * @param string $value    Value that was saved to the field.
+					 */
+					do_action( 'xprofile_profile_field_data_updated', $field_id, $value );
+				}
+			}
+
+			/**
+			 * Fires after all XProfile fields have been saved for the current profile.
+			 *
+			 * @since 1.0.0
+			 * @since 2.6.0 Added $old_values and $new_values parameters.
+			 *
+			 * @param int   $user_id          ID for the user whose profile is being saved.
+			 * @param array $posted_field_ids Array of field IDs that were edited.
+			 * @param bool  $errors           Whether or not any errors occurred.
+			 * @param array $old_values       Array of original values before update.
+			 * @param array $new_values       Array of newly saved values after update.
+			 */
+			do_action( 'xprofile_updated_profile', $user_id, $posted_field_ids, $errors, $old_values, $new_values );
+
+			// Set the feedback messages.
+			if ( ! empty( $errors ) ) {
+				$redirect_to = add_query_arg( 'error',   '3', $redirect_to );
+			} else {
+				$redirect_to = add_query_arg( 'updated', '1', $redirect_to );
+			}
+
+			bp_core_redirect( $redirect_to );
+		}
+	}
+
+	/**
+	 * Render the xprofile metabox for Community Profile screen.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User|null $user The WP_User object for the user being edited.
+	 * @param array        $args Aray of arguments for metaboxes.
+	 */
+	public function user_admin_profile_metaboxes( $user = null, $args = array() ) {
+
+		// Bail if no user ID.
+		if ( empty( $user->ID ) ) {
+			return;
+		}
+
+		$r = bp_parse_args( $args['args'], array(
+			'profile_group_id' => 0,
+			'user_id'          => $user->ID
+		), 'bp_xprofile_user_admin_profile_loop_args' );
+
+		// We really need these args.
+		if ( empty( $r['profile_group_id'] ) || empty( $r['user_id'] ) ) {
+			return;
+		}
+
+		// Bail if no profile fields are available.
+		if ( ! bp_has_profile( $r ) ) {
+			return;
+		}
+
+		// Loop through profile groups & fields.
+		while ( bp_profile_groups() ) : bp_the_profile_group(); ?>
+
+			<input type="hidden" name="field_ids[]" id="<?php echo esc_attr( 'field_ids_' . bp_get_the_profile_group_slug() ); ?>" value="<?php echo esc_attr( bp_get_the_profile_group_field_ids() ); ?>" />
+
+			<?php if ( bp_get_the_profile_group_description() ) : ?>
+
+				<p class="description"><?php bp_the_profile_group_description(); ?></p>
+
+			<?php endif; ?>
+
+			<?php while ( bp_profile_fields() ) : bp_the_profile_field(); ?>
+
+				<div<?php bp_field_css_class( 'bp-profile-field' ); ?>>
+					<fieldset>
+
+					<?php
+
+					$field_type = bp_xprofile_create_field_type( bp_get_the_profile_field_type() );
+					$field_type->edit_field_html( array( 'user_id' => $r['user_id'] ) );
+
+					/**
+					 * Fires before display of visibility form elements for profile metaboxes.
+					 *
+					 * @since 1.7.0
+					 */
+					do_action( 'bp_custom_profile_edit_fields_pre_visibility' );
+
+					$can_change_visibility = bp_current_user_can( 'bp_xprofile_change_field_visibility' ); ?>
+
+					<p class="field-visibility-settings-<?php echo $can_change_visibility ? 'toggle' : 'notoggle'; ?>" id="field-visibility-settings-toggle-<?php bp_the_profile_field_id(); ?>"><span id="<?php bp_the_profile_field_input_name(); ?>-2">
+
+						<?php
+						printf(
+							__( 'This field can be seen by: %s', 'buddypress' ),
+							'<span class="current-visibility-level">' . bp_get_the_profile_field_visibility_level_label() . '</span>'
+						);
+						?>
+						</span>
+
+						<?php if ( $can_change_visibility ) : ?>
+
+							<button type="button" class="button visibility-toggle-link" aria-describedby="<?php bp_the_profile_field_input_name(); ?>-2" aria-expanded="false"><?php esc_html_e( 'Change', 'buddypress' ); ?></button>
+
+						<?php endif; ?>
+					</p>
+
+					<?php if ( $can_change_visibility ) : ?>
+
+						<div class="field-visibility-settings" id="field-visibility-settings-<?php bp_the_profile_field_id() ?>">
+							<fieldset>
+								<legend><?php _e( 'Who can see this field?', 'buddypress' ); ?></legend>
+
+								<?php bp_profile_visibility_radio_buttons(); ?>
+
+							</fieldset>
+							<button type="button" class="button field-visibility-settings-close"><?php esc_html_e( 'Close', 'buddypress' ); ?></button>
+						</div>
+
+					<?php endif; ?>
+
+					<?php
+
+					/**
+					 * Fires at end of custom profile field items on your xprofile screen tab.
+					 *
+					 * @since 1.1.0
+					 */
+					do_action( 'bp_custom_profile_edit_fields' ); ?>
+
+					</fieldset>
+				</div>
+
+			<?php endwhile; // End bp_profile_fields(). ?>
+
+		<?php endwhile; // End bp_profile_groups.
+	}
+
+	/**
+	 * Render the fallback metabox in case a user has been marked as a spammer.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User|null $user The WP_User object for the user being edited.
+	 */
+	public function user_admin_spammer_metabox( $user = null ) {
+	?>
+		<p><?php printf( __( '%s has been marked as a spammer. All BuddyPress data associated with the user has been removed', 'buddypress' ), esc_html( bp_core_get_user_displayname( $user->ID ) ) ) ;?></p>
+	<?php
+	}
+
+	/**
+	 * Render the Avatar metabox to moderate inappropriate images.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param WP_User|null $user The WP_User object for the user being edited.
+	 */
+	public function user_admin_avatar_metabox( $user = null ) {
+
+		if ( empty( $user->ID ) ) {
+			return;
+		} ?>
+
+		<div class="avatar">
+
+			<?php echo bp_core_fetch_avatar( array(
+				'item_id' => $user->ID,
+				'object'  => 'user',
+				'type'    => 'full',
+				'title'   => $user->display_name
+			) ); ?>
+
+			<?php if ( bp_get_user_has_avatar( $user->ID ) ) :
+
+				$query_args = array(
+					'user_id' => $user->ID,
+					'action'  => 'delete_avatar'
+				);
+
+				if ( ! empty( $_REQUEST['wp_http_referer'] ) ) {
+					$wp_http_referer = wp_unslash( $_REQUEST['wp_http_referer'] );
+					$wp_http_referer = remove_query_arg( array( 'action', 'updated' ), $wp_http_referer );
+					$wp_http_referer = wp_validate_redirect( esc_url_raw( $wp_http_referer ) );
+					$query_args['wp_http_referer'] = urlencode( $wp_http_referer );
+				}
+
+				$community_url = add_query_arg( $query_args, buddypress()->members->admin->edit_profile_url );
+				$delete_link   = wp_nonce_url( $community_url, 'delete_avatar' ); ?>
+
+				<a href="<?php echo esc_url( $delete_link ); ?>" class="bp-xprofile-avatar-user-admin"><?php esc_html_e( 'Delete Profile Photo', 'buddypress' ); ?></a>
+
+			<?php endif;
+
+			// Load the Avatar UI templates if user avatar uploads are enabled and current WordPress version is supported.
+			if ( ! bp_core_get_root_option( 'bp-disable-avatar-uploads' ) && bp_attachments_is_wp_version_supported() ) : ?>
+				<a href="#TB_inline?width=800px&height=400px&inlineId=bp-xprofile-avatar-editor" class="thickbox bp-xprofile-avatar-user-edit"><?php esc_html_e( 'Edit Profile Photo', 'buddypress' ); ?></a>
+				<div id="bp-xprofile-avatar-editor" style="display:none;">
+					<?php bp_attachments_get_template_part( 'avatars/index' ); ?>
+				</div>
+			<?php endif; ?>
+
+		</div>
+		<?php
+	}
+
+}
+endif; // End class_exists check.
Index: /tags/4.0.0-beta1/src/bp-xprofile/screens/change-avatar.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/screens/change-avatar.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/screens/change-avatar.php	(revision 12265)
@@ -0,0 +1,101 @@
+<?php
+/**
+ * XProfile: User's "Profile > Change Avatar" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage XProfileScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the uploading and cropping of a user avatar. Displays the change avatar page.
+ *
+ * @since 1.0.0
+ *
+ */
+function xprofile_screen_change_avatar() {
+
+	// Bail if not the correct screen.
+	if ( ! bp_is_my_profile() && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	// Bail if there are action variables.
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	$bp = buddypress();
+
+	if ( ! isset( $bp->avatar_admin ) ) {
+		$bp->avatar_admin = new stdClass();
+	}
+
+	$bp->avatar_admin->step = 'upload-image';
+
+	if ( !empty( $_FILES ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'bp_avatar_upload' );
+
+		// Pass the file to the avatar upload handler.
+		if ( bp_core_avatar_handle_upload( $_FILES, 'xprofile_avatar_upload_dir' ) ) {
+			$bp->avatar_admin->step = 'crop-image';
+
+			// Make sure we include the jQuery jCrop file for image cropping.
+			add_action( 'wp_print_scripts', 'bp_core_add_jquery_cropper' );
+		}
+	}
+
+	// If the image cropping is done, crop the image and save a full/thumb version.
+	if ( isset( $_POST['avatar-crop-submit'] ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'bp_avatar_cropstore' );
+
+		$args = array(
+			'item_id'       => bp_displayed_user_id(),
+			'original_file' => $_POST['image_src'],
+			'crop_x'        => $_POST['x'],
+			'crop_y'        => $_POST['y'],
+			'crop_w'        => $_POST['w'],
+			'crop_h'        => $_POST['h']
+		);
+
+		if ( ! bp_core_avatar_handle_crop( $args ) ) {
+			bp_core_add_message( __( 'There was a problem cropping your profile photo.', 'buddypress' ), 'error' );
+		} else {
+
+			/**
+			 * Fires right before the redirect, after processing a new avatar.
+			 *
+			 * @since 1.1.0
+			 * @since 2.3.4 Add two new parameters to inform about the user id and
+			 *              about the way the avatar was set (eg: 'crop' or 'camera').
+			 *
+			 * @param string $item_id Inform about the user id the avatar was set for.
+			 * @param string $value   Inform about the way the avatar was set ('crop').
+			 */
+			do_action( 'xprofile_avatar_uploaded', (int) $args['item_id'], 'crop' );
+			bp_core_add_message( __( 'Your new profile photo was uploaded successfully.', 'buddypress' ) );
+			bp_core_redirect( bp_displayed_user_domain() );
+		}
+	}
+
+	/**
+	 * Fires right before the loading of the XProfile change avatar screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'xprofile_screen_change_avatar' );
+
+	/**
+	 * Filters the template to load for the XProfile change avatar screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the XProfile change avatar template to load.
+	 */
+	bp_core_load_template( apply_filters( 'xprofile_template_change_avatar', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/screens/change-cover-image.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/screens/change-cover-image.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/screens/change-cover-image.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * XProfile: User's "Profile > Change Cover Image" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage XProfileScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Displays the change cover image page.
+ *
+ * @since 2.4.0
+ */
+function xprofile_screen_change_cover_image() {
+
+	// Bail if not the correct screen.
+	if ( ! bp_is_my_profile() && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	/**
+	 * Fires right before the loading of the XProfile change cover image screen template file.
+	 *
+	 * @since 2.4.0
+	 */
+	do_action( 'xprofile_screen_change_cover_image' );
+
+	/**
+	 * Filters the template to load for the XProfile cover image screen.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $template Path to the XProfile cover image template to load.
+	 */
+	bp_core_load_template( apply_filters( 'xprofile_template_cover_image', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/screens/edit.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/screens/edit.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/screens/edit.php	(revision 12265)
@@ -0,0 +1,156 @@
+<?php
+/**
+ * XProfile: User's "Profile > Edit" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage XProfileScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the display of the profile edit page by loading the correct template file.
+ * Also checks to make sure this can only be accessed for the logged in users profile.
+ *
+ * @since 1.0.0
+ *
+ */
+function xprofile_screen_edit_profile() {
+
+	if ( ! bp_is_my_profile() && ! bp_current_user_can( 'bp_moderate' ) ) {
+		return false;
+	}
+
+	// Make sure a group is set.
+	if ( ! bp_action_variable( 1 ) ) {
+		bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_profile_slug() . '/edit/group/1' ) );
+	}
+
+	// Check the field group exists.
+	if ( ! bp_is_action_variable( 'group' ) || ! xprofile_get_field_group( bp_action_variable( 1 ) ) ) {
+		bp_do_404();
+		return;
+	}
+
+	// No errors.
+	$errors = false;
+
+	// Check to see if any new information has been submitted.
+	if ( isset( $_POST['field_ids'] ) ) {
+
+		// Check the nonce.
+		check_admin_referer( 'bp_xprofile_edit' );
+
+		// Check we have field ID's.
+		if ( empty( $_POST['field_ids'] ) ) {
+			bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_profile_slug() . '/edit/group/' . bp_action_variable( 1 ) ) );
+		}
+
+		// Explode the posted field IDs into an array so we know which
+		// fields have been submitted.
+		$posted_field_ids = wp_parse_id_list( $_POST['field_ids'] );
+		$is_required      = array();
+
+		// Loop through the posted fields formatting any datebox values then validate the field.
+		foreach ( (array) $posted_field_ids as $field_id ) {
+			bp_xprofile_maybe_format_datebox_post_data( $field_id );
+
+			$is_required[ $field_id ] = xprofile_check_is_required_field( $field_id ) && ! bp_current_user_can( 'bp_moderate' );
+			if ( $is_required[$field_id] && empty( $_POST['field_' . $field_id] ) ) {
+				$errors = true;
+			}
+		}
+
+		// There are errors.
+		if ( !empty( $errors ) ) {
+			bp_core_add_message( __( 'Your changes have not been saved. Please fill in all required fields, and save your changes again.', 'buddypress' ), 'error' );
+
+		// No errors.
+		} else {
+
+			// Reset the errors var.
+			$errors = false;
+
+			// Now we've checked for required fields, lets save the values.
+			$old_values = $new_values = array();
+			foreach ( (array) $posted_field_ids as $field_id ) {
+
+				// Certain types of fields (checkboxes, multiselects) may come through empty. Save them as an empty array so that they don't get overwritten by the default on the next edit.
+				$value = isset( $_POST['field_' . $field_id] ) ? $_POST['field_' . $field_id] : '';
+
+				$visibility_level = !empty( $_POST['field_' . $field_id . '_visibility'] ) ? $_POST['field_' . $field_id . '_visibility'] : 'public';
+
+				// Save the old and new values. They will be
+				// passed to the filter and used to determine
+				// whether an activity item should be posted.
+				$old_values[ $field_id ] = array(
+					'value'      => xprofile_get_field_data( $field_id, bp_displayed_user_id() ),
+					'visibility' => xprofile_get_field_visibility_level( $field_id, bp_displayed_user_id() ),
+				);
+
+				// Update the field data and visibility level.
+				xprofile_set_field_visibility_level( $field_id, bp_displayed_user_id(), $visibility_level );
+				$field_updated = xprofile_set_field_data( $field_id, bp_displayed_user_id(), $value, $is_required[ $field_id ] );
+				$value         = xprofile_get_field_data( $field_id, bp_displayed_user_id() );
+
+				$new_values[ $field_id ] = array(
+					'value'      => $value,
+					'visibility' => xprofile_get_field_visibility_level( $field_id, bp_displayed_user_id() ),
+				);
+
+				if ( ! $field_updated ) {
+					$errors = true;
+				} else {
+
+					/**
+					 * Fires on each iteration of an XProfile field being saved with no error.
+					 *
+					 * @since 1.1.0
+					 *
+					 * @param int    $field_id ID of the field that was saved.
+					 * @param string $value    Value that was saved to the field.
+					 */
+					do_action( 'xprofile_profile_field_data_updated', $field_id, $value );
+				}
+			}
+
+			/**
+			 * Fires after all XProfile fields have been saved for the current profile.
+			 *
+			 * @since 1.0.0
+			 *
+			 * @param int   $value            Displayed user ID.
+			 * @param array $posted_field_ids Array of field IDs that were edited.
+			 * @param bool  $errors           Whether or not any errors occurred.
+			 * @param array $old_values       Array of original values before updated.
+			 * @param array $new_values       Array of newly saved values after update.
+			 */
+			do_action( 'xprofile_updated_profile', bp_displayed_user_id(), $posted_field_ids, $errors, $old_values, $new_values );
+
+			// Set the feedback messages.
+			if ( !empty( $errors ) ) {
+				bp_core_add_message( __( 'There was a problem updating some of your profile information. Please try again.', 'buddypress' ), 'error' );
+			} else {
+				bp_core_add_message( __( 'Changes saved.', 'buddypress' ) );
+			}
+
+			// Redirect back to the edit screen to display the updates and message.
+			bp_core_redirect( trailingslashit( bp_displayed_user_domain() . bp_get_profile_slug() . '/edit/group/' . bp_action_variable( 1 ) ) );
+		}
+	}
+
+	/**
+	 * Fires right before the loading of the XProfile edit screen template file.
+	 *
+	 * @since 1.0.0
+	 */
+	do_action( 'xprofile_screen_edit_profile' );
+
+	/**
+	 * Filters the template to load for the XProfile edit screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the XProfile edit template to load.
+	 */
+	bp_core_load_template( apply_filters( 'xprofile_template_edit_profile', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/screens/public.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/screens/public.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/screens/public.php	(revision 12265)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * XProfile: User's "Profile" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage XProfileScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Handles the display of the profile page by loading the correct template file.
+ *
+ * @since 1.0.0
+ *
+ */
+function xprofile_screen_display_profile() {
+	$new = isset( $_GET['new'] ) ? $_GET['new'] : '';
+
+	/**
+	 * Fires right before the loading of the XProfile screen template file.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $new $_GET parameter holding the "new" parameter.
+	 */
+	do_action( 'xprofile_screen_display_profile', $new );
+
+	/**
+	 * Filters the template to load for the XProfile screen.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param string $template Path to the XProfile template to load.
+	 */
+	bp_core_load_template( apply_filters( 'xprofile_template_display_profile', 'members/single/home' ) );
+}
Index: /tags/4.0.0-beta1/src/bp-xprofile/screens/settings-profile.php
===================================================================
--- /tags/4.0.0-beta1/src/bp-xprofile/screens/settings-profile.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/bp-xprofile/screens/settings-profile.php	(revision 12265)
@@ -0,0 +1,115 @@
+<?php
+/**
+ * XProfile: User's "Settings > Profile Visibility" screen handler
+ *
+ * @package BuddyPress
+ * @subpackage XProfileScreens
+ * @since 3.0.0
+ */
+
+/**
+ * Show the xprofile settings template.
+ *
+ * @since 2.0.0
+ */
+function bp_xprofile_screen_settings() {
+
+	// Redirect if no privacy settings page is accessible.
+	if ( bp_action_variables() || ! bp_is_active( 'xprofile' ) ) {
+		bp_do_404();
+		return;
+	}
+
+	/**
+	 * Filters the template to load for the XProfile settings screen.
+	 *
+	 * @since 2.0.0
+	 *
+	 * @param string $template Path to the XProfile change avatar template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_settings_screen_xprofile', '/members/single/settings/profile' ) );
+}
+
+/**
+ * Handles the saving of xprofile field visibilities.
+ *
+ * @since 1.9.0
+ */
+function bp_xprofile_action_settings() {
+
+	// Bail if not a POST action.
+	if ( ! bp_is_post_request() ) {
+		return;
+	}
+
+	// Bail if no submit action.
+	if ( ! isset( $_POST['xprofile-settings-submit'] ) ) {
+		return;
+	}
+
+	// Bail if not in settings.
+	if ( ! bp_is_user_settings_profile() ) {
+		return;
+	}
+
+	// 404 if there are any additional action variables attached
+	if ( bp_action_variables() ) {
+		bp_do_404();
+		return;
+	}
+
+	// Nonce check.
+	check_admin_referer( 'bp_xprofile_settings' );
+
+	/**
+	 * Fires before saving xprofile field visibilities.
+	 *
+	 * @since 2.0.0
+	 */
+	do_action( 'bp_xprofile_settings_before_save' );
+
+	/* Save ******************************************************************/
+
+	// Only save if there are field ID's being posted.
+	if ( ! empty( $_POST['field_ids'] ) ) {
+
+		// Get the POST'ed field ID's.
+		$posted_field_ids = explode( ',', $_POST['field_ids'] );
+
+		// Backward compatibility: a bug in BP 2.0 caused only a single
+		// group's field IDs to be submitted. Look for values submitted
+		// in the POST request that may not appear in 'field_ids', and
+		// add them to the list of IDs to save.
+		foreach ( $_POST as $posted_key => $posted_value ) {
+			preg_match( '/^field_([0-9]+)_visibility$/', $posted_key, $matches );
+			if ( ! empty( $matches[1] ) && ! in_array( $matches[1], $posted_field_ids ) ) {
+				$posted_field_ids[] = $matches[1];
+			}
+		}
+
+		// Save the visibility settings.
+		foreach ( $posted_field_ids as $field_id ) {
+
+			$visibility_level = 'public';
+
+			if ( !empty( $_POST['field_' . $field_id . '_visibility'] ) ) {
+				$visibility_level = $_POST['field_' . $field_id . '_visibility'];
+			}
+
+			xprofile_set_field_visibility_level( $field_id, bp_displayed_user_id(), $visibility_level );
+		}
+	}
+
+	/* Other *****************************************************************/
+
+	/**
+	 * Fires after saving xprofile field visibilities.
+	 *
+	 * @since 2.0.0
+	 */
+	do_action( 'bp_xprofile_settings_after_save' );
+
+	// Redirect to the root domain.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_settings_slug() . '/profile' );
+}
+add_action( 'bp_actions', 'bp_xprofile_action_settings' );
Index: /tags/4.0.0-beta1/src/class-buddypress.php
===================================================================
--- /tags/4.0.0-beta1/src/class-buddypress.php	(revision 12265)
+++ /tags/4.0.0-beta1/src/class-buddypress.php	(revision 12265)
@@ -0,0 +1,776 @@
+<?php
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Main BuddyPress Class.
+ *
+ * Tap tap tap... Is this thing on?
+ *
+ * @since 1.6.0
+ */
+class BuddyPress {
+
+	/** Magic *****************************************************************/
+
+	/**
+	 * BuddyPress uses many variables, most of which can be filtered to
+	 * customize the way that it works. To prevent unauthorized access,
+	 * these variables are stored in a private array that is magically
+	 * updated using PHP 5.2+ methods. This is to prevent third party
+	 * plugins from tampering with essential information indirectly, which
+	 * would cause issues later.
+	 *
+	 * @see BuddyPress::setup_globals()
+	 * @var array
+	 */
+	private $data;
+
+	/** Not Magic *************************************************************/
+
+	/**
+	 * @var array Primary BuddyPress navigation.
+	 */
+	public $bp_nav = array();
+
+	/**
+	 * @var array Secondary BuddyPress navigation to $bp_nav.
+	 */
+	public $bp_options_nav = array();
+
+	/**
+	 * @var array The unfiltered URI broken down into chunks.
+	 * @see bp_core_set_uri_globals()
+	 */
+	public $unfiltered_uri = array();
+
+	/**
+	 * @var array The canonical URI stack.
+	 * @see bp_redirect_canonical()
+	 * @see bp_core_new_nav_item()
+	 */
+	public $canonical_stack = array();
+
+	/**
+	 * @var array Additional navigation elements (supplemental).
+	 */
+	public $action_variables = array();
+
+	/**
+	 * @var string Current member directory type.
+	 */
+	public $current_member_type = '';
+
+	/**
+	 * @var array Required components (core, members).
+	 */
+	public $required_components = array();
+
+	/**
+	 * @var array Additional active components.
+	 */
+	public $loaded_components = array();
+
+	/**
+	 * @var array Active components.
+	 */
+	public $active_components = array();
+
+	/**
+	 * Whether autoload is in use.
+	 *
+	 * @since 2.5.0
+	 * @var bool
+	 */
+	public $do_autoload = true;
+
+	/** Option Overload *******************************************************/
+
+	/**
+	 * @var array Optional Overloads default options retrieved from get_option().
+	 */
+	public $options = array();
+
+	/** Singleton *************************************************************/
+
+	/**
+	 * Main BuddyPress Instance.
+	 *
+	 * BuddyPress is great.
+	 * Please load it only one time.
+	 * For this, we thank you.
+	 *
+	 * Insures that only one instance of BuddyPress exists in memory at any
+	 * one time. Also prevents needing to define globals all over the place.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @static object $instance
+	 * @see buddypress()
+	 *
+	 * @return BuddyPress|null The one true BuddyPress.
+	 */
+	public static function instance() {
+
+		// Store the instance locally to avoid private static replication
+		static $instance = null;
+
+		// Only run these methods if they haven't been run previously
+		if ( null === $instance ) {
+			$instance = new BuddyPress;
+			$instance->constants();
+			$instance->setup_globals();
+			$instance->legacy_constants();
+			$instance->includes();
+			$instance->setup_actions();
+		}
+
+		// Always return the instance
+		return $instance;
+
+		// The last metroid is in captivity. The galaxy is at peace.
+	}
+
+	/** Magic Methods *********************************************************/
+
+	/**
+	 * A dummy constructor to prevent BuddyPress from being loaded more than once.
+	 *
+	 * @since 1.7.0
+	 * @see BuddyPress::instance()
+	 * @see buddypress()
+	 */
+	private function __construct() { /* Do nothing here */ }
+
+	/**
+	 * A dummy magic method to prevent BuddyPress from being cloned.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __clone() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'buddypress' ), '1.7' ); }
+
+	/**
+	 * A dummy magic method to prevent BuddyPress from being unserialized.
+	 *
+	 * @since 1.7.0
+	 */
+	public function __wakeup() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'buddypress' ), '1.7' ); }
+
+	/**
+	 * Magic method for checking the existence of a certain custom field.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $key Key to check the set status for.
+	 *
+	 * @return bool
+	 */
+	public function __isset( $key ) { return isset( $this->data[$key] ); }
+
+	/**
+	 * Magic method for getting BuddyPress variables.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $key Key to return the value for.
+	 *
+	 * @return mixed
+	 */
+	public function __get( $key ) { return isset( $this->data[$key] ) ? $this->data[$key] : null; }
+
+	/**
+	 * Magic method for setting BuddyPress variables.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $key   Key to set a value for.
+	 * @param mixed  $value Value to set.
+	 */
+	public function __set( $key, $value ) { $this->data[$key] = $value; }
+
+	/**
+	 * Magic method for unsetting BuddyPress variables.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $key Key to unset a value for.
+	 */
+	public function __unset( $key ) { if ( isset( $this->data[$key] ) ) unset( $this->data[$key] ); }
+
+	/**
+	 * Magic method to prevent notices and errors from invalid method calls.
+	 *
+	 * @since 1.7.0
+	 *
+	 * @param string $name
+	 * @param array  $args
+	 *
+	 * @return null
+	 */
+	public function __call( $name = '', $args = array() ) { unset( $name, $args ); return null; }
+
+	/** Private Methods *******************************************************/
+
+	/**
+	 * Bootstrap constants.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	private function constants() {
+
+		// Place your custom code (actions/filters) in a file called
+		// '/plugins/bp-custom.php' and it will be loaded before anything else.
+		if ( file_exists( WP_PLUGIN_DIR . '/bp-custom.php' ) ) {
+			require( WP_PLUGIN_DIR . '/bp-custom.php' );
+		}
+
+		// Path and URL
+		if ( ! defined( 'BP_PLUGIN_DIR' ) ) {
+			define( 'BP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
+		}
+
+		if ( ! defined( 'BP_PLUGIN_URL' ) ) {
+			define( 'BP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
+		}
+
+		// Legacy forum constant - supported for compatibility with bbPress 2.
+		if ( ! defined( 'BP_FORUMS_PARENT_FORUM_ID' ) ) {
+			define( 'BP_FORUMS_PARENT_FORUM_ID', 1 );
+		}
+
+		// Legacy forum constant - supported for compatibility with bbPress 2.
+		if ( ! defined( 'BP_FORUMS_SLUG' ) ) {
+			define( 'BP_FORUMS_SLUG', 'forums' );
+		}
+
+		// Only applicable to those running trunk
+		if ( ! defined( 'BP_SOURCE_SUBDIRECTORY' ) ) {
+			define( 'BP_SOURCE_SUBDIRECTORY', '' );
+		}
+
+		// Define on which blog ID BuddyPress should run
+		if ( ! defined( 'BP_ROOT_BLOG' ) ) {
+
+			// Default to use current blog ID
+			// Fulfills non-network installs and BP_ENABLE_MULTIBLOG installs
+			$root_blog_id = get_current_blog_id();
+
+			// Multisite check
+			if ( is_multisite() ) {
+
+				// Multiblog isn't enabled
+				if ( ! defined( 'BP_ENABLE_MULTIBLOG' ) || ( defined( 'BP_ENABLE_MULTIBLOG' ) && (int) constant( 'BP_ENABLE_MULTIBLOG' ) === 0 ) ) {
+					// Check to see if BP is network-activated
+					// We're not using is_plugin_active_for_network() b/c you need to include the
+					// /wp-admin/includes/plugin.php file in order to use that function.
+
+					// get network-activated plugins
+					$plugins = get_site_option( 'active_sitewide_plugins');
+
+					// basename
+					$basename = basename( constant( 'BP_PLUGIN_DIR' ) ) . '/bp-loader.php';
+
+					// plugin is network-activated; use main site ID instead
+					if ( isset( $plugins[ $basename ] ) ) {
+						$current_site = get_current_site();
+						$root_blog_id = $current_site->blog_id;
+					}
+				}
+
+			}
+
+			define( 'BP_ROOT_BLOG', $root_blog_id );
+		}
+
+		// The search slug has to be defined nice and early because of the way
+		// search requests are loaded
+		//
+		// @todo Make this better
+		if ( ! defined( 'BP_SEARCH_SLUG' ) ) {
+			define( 'BP_SEARCH_SLUG', 'search' );
+		}
+	}
+
+	/**
+	 * Component global variables.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	private function setup_globals() {
+
+		/** Versions **********************************************************/
+
+		$this->version    = '4.0.0-beta1';
+		$this->db_version = 11105;
+
+		/** Loading ***********************************************************/
+
+		/**
+		 * Should deprecated code be loaded?
+		 *
+		 * @since 2.0.0 Defaults to false always
+		 * @since 2.8.0 Defaults to true on upgrades, false for new installs.
+		 */
+		$this->load_deprecated = false;
+
+		/** Toolbar ***********************************************************/
+
+		/**
+		 * @var string The primary toolbar ID.
+		 */
+		$this->my_account_menu_id = '';
+
+		/** URIs **************************************************************/
+
+		/**
+		 * @var int The current offset of the URI.
+		 * @see bp_core_set_uri_globals()
+		 */
+		$this->unfiltered_uri_offset = 0;
+
+		/**
+		 * @var bool Are status headers already sent?
+		 */
+		$this->no_status_set = false;
+
+		/** Components ********************************************************/
+
+		/**
+		 * @var string Name of the current BuddyPress component (primary).
+		 */
+		$this->current_component = '';
+
+		/**
+		 * @var string Name of the current BuddyPress item (secondary).
+		 */
+		$this->current_item = '';
+
+		/**
+		 * @var string Name of the current BuddyPress action (tertiary).
+		 */
+		$this->current_action = '';
+
+		/**
+		 * @var bool Displaying custom 2nd level navigation menu (I.E a group).
+		 */
+		$this->is_single_item = false;
+
+		/** Root **************************************************************/
+
+		/**
+		 * Filters the BuddyPress Root blog ID.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @const constant BP_ROOT_BLOG BuddyPress Root blog ID.
+		 */
+		$this->root_blog_id = (int) apply_filters( 'bp_get_root_blog_id', BP_ROOT_BLOG );
+
+		/** Paths**************************************************************/
+
+		// BuddyPress root directory
+		$this->file           = constant( 'BP_PLUGIN_DIR' ) . 'bp-loader.php';
+		$this->basename       = basename( constant( 'BP_PLUGIN_DIR' ) ) . '/bp-loader.php';
+		$this->plugin_dir     = trailingslashit( constant( 'BP_PLUGIN_DIR' ) . constant( 'BP_SOURCE_SUBDIRECTORY' ) );
+		$this->plugin_url     = trailingslashit( constant( 'BP_PLUGIN_URL' ) . constant( 'BP_SOURCE_SUBDIRECTORY' ) );
+
+		// Languages
+		$this->lang_dir       = $this->plugin_dir . 'bp-languages';
+
+		// Templates (theme compatibility)
+		$this->themes_dir     = $this->plugin_dir . 'bp-templates';
+		$this->themes_url     = $this->plugin_url . 'bp-templates';
+
+		// Themes (for bp-default)
+		$this->old_themes_dir = $this->plugin_dir . 'bp-themes';
+		$this->old_themes_url = $this->plugin_url . 'bp-themes';
+
+		/** Theme Compat ******************************************************/
+
+		$this->theme_compat   = new stdClass(); // Base theme compatibility class
+		$this->filters        = new stdClass(); // Used when adding/removing filters
+
+		/** Users *************************************************************/
+
+		$this->current_user   = new stdClass();
+		$this->displayed_user = new stdClass();
+
+		/** Post types and taxonomies *****************************************/
+
+		/**
+		 * Filters the post type slug for the email component.
+		 *
+		 * since 2.5.0
+		 *
+		 * @param string $value Email post type slug.
+		 */
+		$this->email_post_type     = apply_filters( 'bp_email_post_type', 'bp-email' );
+
+		/**
+		 * Filters the taxonomy slug for the email type component.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $value Email type taxonomy slug.
+		 */
+		$this->email_taxonomy_type = apply_filters( 'bp_email_tax_type', 'bp-email-type' );
+	}
+
+	/**
+	 * Legacy BuddyPress constants.
+	 *
+	 * Try to avoid using these. Their values have been moved into variables
+	 * in the instance, and have matching functions to get/set their values.
+	 *
+	 * @since 1.7.0
+	 */
+	private function legacy_constants() {
+
+		// Define the BuddyPress version
+		if ( ! defined( 'BP_VERSION' ) ) {
+			define( 'BP_VERSION', $this->version );
+		}
+
+		// Define the database version
+		if ( ! defined( 'BP_DB_VERSION' ) ) {
+			define( 'BP_DB_VERSION', $this->db_version );
+		}
+
+		// Define if deprecated functions should be ignored
+		if ( ! defined( 'BP_IGNORE_DEPRECATED' ) ) {
+			define( 'BP_IGNORE_DEPRECATED', true );
+		}
+	}
+
+	/**
+	 * Include required files.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	private function includes() {
+		spl_autoload_register( array( $this, 'autoload' ) );
+
+		// Load the WP abstraction file so BuddyPress can run on all WordPress setups.
+		require( $this->plugin_dir . 'bp-core/bp-core-wpabstraction.php' );
+
+		// Setup the versions (after we include multisite abstraction above)
+		$this->versions();
+
+		/** Update/Install ****************************************************/
+
+		// Theme compatibility
+		require( $this->plugin_dir . 'bp-core/bp-core-template-loader.php'     );
+		require( $this->plugin_dir . 'bp-core/bp-core-theme-compatibility.php' );
+
+		// Require all of the BuddyPress core libraries
+		require( $this->plugin_dir . 'bp-core/bp-core-dependency.php'       );
+		require( $this->plugin_dir . 'bp-core/bp-core-actions.php'          );
+		require( $this->plugin_dir . 'bp-core/bp-core-caps.php'             );
+		require( $this->plugin_dir . 'bp-core/bp-core-cache.php'            );
+		require( $this->plugin_dir . 'bp-core/bp-core-cssjs.php'            );
+		require( $this->plugin_dir . 'bp-core/bp-core-update.php'           );
+		require( $this->plugin_dir . 'bp-core/bp-core-options.php'          );
+		require( $this->plugin_dir . 'bp-core/bp-core-taxonomy.php'         );
+		require( $this->plugin_dir . 'bp-core/bp-core-filters.php'          );
+		require( $this->plugin_dir . 'bp-core/bp-core-attachments.php'      );
+		require( $this->plugin_dir . 'bp-core/bp-core-avatars.php'          );
+		require( $this->plugin_dir . 'bp-core/bp-core-widgets.php'          );
+		require( $this->plugin_dir . 'bp-core/bp-core-template.php'         );
+		require( $this->plugin_dir . 'bp-core/bp-core-adminbar.php'         );
+		require( $this->plugin_dir . 'bp-core/bp-core-buddybar.php'         );
+		require( $this->plugin_dir . 'bp-core/bp-core-catchuri.php'         );
+		require( $this->plugin_dir . 'bp-core/bp-core-functions.php'        );
+		require( $this->plugin_dir . 'bp-core/bp-core-moderation.php'       );
+		require( $this->plugin_dir . 'bp-core/bp-core-loader.php'           );
+		require( $this->plugin_dir . 'bp-core/bp-core-customizer-email.php' );
+
+		// Maybe load deprecated functionality (this double negative is proof positive!)
+		if ( ! bp_get_option( '_bp_ignore_deprecated_code', ! $this->load_deprecated ) ) {
+			require( $this->plugin_dir . 'bp-core/deprecated/1.2.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/1.5.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/1.6.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/1.7.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/1.9.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.0.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.1.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.2.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.3.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.4.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.5.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.6.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.7.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.8.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/2.9.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/3.0.php' );
+			require( $this->plugin_dir . 'bp-core/deprecated/4.0.php' );
+		}
+
+		if ( defined( 'WP_CLI' ) && file_exists( $this->plugin_dir . 'cli/wp-cli-bp.php' ) ) {
+			require( $this->plugin_dir . 'cli/wp-cli-bp.php' );
+		}
+	}
+
+	/**
+	 * Autoload classes.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $class
+	 */
+	public function autoload( $class ) {
+		$class_parts = explode( '_', strtolower( $class ) );
+
+		if ( 'bp' !== $class_parts[0] ) {
+			return;
+		}
+
+		$components = array(
+			'activity',
+			'blogs',
+			'core',
+			'friends',
+			'groups',
+			'members',
+			'messages',
+			'notifications',
+			'settings',
+			'xprofile',
+		);
+
+		// These classes don't have a name that matches their component.
+		$irregular_map = array(
+			'BP_Akismet' => 'activity',
+
+			'BP_Admin'                     => 'core',
+			'BP_Attachment_Avatar'         => 'core',
+			'BP_Attachment_Cover_Image'    => 'core',
+			'BP_Attachment'                => 'core',
+			'BP_Button'                    => 'core',
+			'BP_Component'                 => 'core',
+			'BP_Customizer_Control_Range'  => 'core',
+			'BP_Date_Query'                => 'core',
+			'BP_Email_Delivery'            => 'core',
+			'BP_Email_Recipient'           => 'core',
+			'BP_Email'                     => 'core',
+			'BP_Embed'                     => 'core',
+			'BP_Media_Extractor'           => 'core',
+			'BP_Members_Suggestions'       => 'core',
+			'BP_PHPMailer'                 => 'core',
+			'BP_Recursive_Query'           => 'core',
+			'BP_Suggestions'               => 'core',
+			'BP_Theme_Compat'              => 'core',
+			'BP_User_Query'                => 'core',
+			'BP_Walker_Category_Checklist' => 'core',
+			'BP_Walker_Nav_Menu_Checklist' => 'core',
+			'BP_Walker_Nav_Menu'           => 'core',
+
+			'BP_Core_Friends_Widget' => 'friends',
+
+			'BP_Group_Extension'    => 'groups',
+			'BP_Group_Member_Query' => 'groups',
+
+			'BP_Core_Members_Template'       => 'members',
+			'BP_Core_Members_Widget'         => 'members',
+			'BP_Core_Recently_Active_Widget' => 'members',
+			'BP_Core_Whos_Online_Widget'     => 'members',
+			'BP_Registration_Theme_Compat'   => 'members',
+			'BP_Signup'                      => 'members',
+		);
+
+		$component = null;
+
+		// First check to see if the class is one without a properly namespaced name.
+		if ( isset( $irregular_map[ $class ] ) ) {
+			$component = $irregular_map[ $class ];
+
+		// Next chunk is usually the component name.
+		} elseif ( in_array( $class_parts[1], $components, true ) ) {
+			$component = $class_parts[1];
+		}
+
+		if ( ! $component ) {
+			return;
+		}
+
+		// Sanitize class name.
+		$class = strtolower( str_replace( '_', '-', $class ) );
+
+		$path = dirname( __FILE__ ) . "/bp-{$component}/classes/class-{$class}.php";
+
+		// Sanity check.
+		if ( ! file_exists( $path ) ) {
+			return;
+		}
+
+		/*
+		 * Sanity check 2 - Check if component is active before loading class.
+		 * Skip if PHPUnit is running, or BuddyPress is installing for the first time.
+		 */
+		if (
+			! in_array( $component, array( 'core', 'members' ), true ) &&
+			! bp_is_active( $component ) &&
+			! function_exists( 'tests_add_filter' )
+		) {
+			return;
+		}
+
+		require $path;
+	}
+
+	/**
+	 * Set up the default hooks and actions.
+	 *
+	 * @since 1.6.0
+	 *
+	 */
+	private function setup_actions() {
+
+		// Add actions to plugin activation and deactivation hooks
+		add_action( 'activate_'   . $this->basename, 'bp_activation'   );
+		add_action( 'deactivate_' . $this->basename, 'bp_deactivation' );
+
+		// If BuddyPress is being deactivated, do not add any actions
+		if ( bp_is_deactivation( $this->basename ) ) {
+			return;
+		}
+
+		// Array of BuddyPress core actions
+		$actions = array(
+			'setup_theme',              // Setup the default theme compat
+			'setup_current_user',       // Setup currently logged in user
+			'register_post_types',      // Register post types
+			'register_post_statuses',   // Register post statuses
+			'register_taxonomies',      // Register taxonomies
+			'register_views',           // Register the views
+			'register_theme_directory', // Register the theme directory
+			'register_theme_packages',  // Register bundled theme packages (bp-themes)
+			'load_textdomain',          // Load textdomain
+			'add_rewrite_tags',         // Add rewrite tags
+			'generate_rewrite_rules'    // Generate rewrite rules
+		);
+
+		// Add the actions
+		foreach( $actions as $class_action ) {
+			if ( method_exists( $this, $class_action ) ) {
+				add_action( 'bp_' . $class_action, array( $this, $class_action ), 5 );
+			}
+		}
+
+		/**
+		 * Fires after the setup of all BuddyPress actions.
+		 *
+		 * Includes bbp-core-hooks.php.
+		 *
+		 * @since 1.7.0
+		 *
+		 * @param BuddyPress $this. Current BuddyPress instance. Passed by reference.
+		 */
+		do_action_ref_array( 'bp_after_setup_actions', array( &$this ) );
+	}
+
+	/**
+	 * Private method to align the active and database versions.
+	 *
+	 * @since 1.7.0
+	 */
+	private function versions() {
+
+		// Get the possible DB versions (boy is this gross)
+		$versions               = array();
+		$versions['1.6-single'] = get_blog_option( $this->root_blog_id, '_bp_db_version' );
+
+		// 1.6-single exists, so trust it
+		if ( !empty( $versions['1.6-single'] ) ) {
+			$this->db_version_raw = (int) $versions['1.6-single'];
+
+		// If no 1.6-single exists, use the max of the others
+		} else {
+			$versions['1.2']        = get_site_option(                      'bp-core-db-version' );
+			$versions['1.5-multi']  = get_site_option(                           'bp-db-version' );
+			$versions['1.6-multi']  = get_site_option(                          '_bp_db_version' );
+			$versions['1.5-single'] = get_blog_option( $this->root_blog_id,      'bp-db-version' );
+
+			// Remove empty array items
+			$versions             = array_filter( $versions );
+			$this->db_version_raw = (int) ( !empty( $versions ) ) ? (int) max( $versions ) : 0;
+		}
+	}
+
+	/** Public Methods ********************************************************/
+
+	/**
+	 * Set up BuddyPress's legacy theme directory.
+	 *
+	 * Starting with version 1.2, and ending with version 1.8, BuddyPress
+	 * registered a custom theme directory - bp-themes - which contained
+	 * the bp-default theme. Since BuddyPress 1.9, bp-themes is no longer
+	 * registered (and bp-default no longer offered) on new installations.
+	 * Sites using bp-default (or a child theme of bp-default) will
+	 * continue to have bp-themes registered as before.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @todo Move bp-default to wordpress.org/extend/themes and remove this.
+	 */
+	public function register_theme_directory() {
+		if ( ! bp_do_register_theme_directory() ) {
+			return;
+		}
+
+		register_theme_directory( $this->old_themes_dir );
+	}
+
+	/**
+	 * Register bundled theme packages.
+	 *
+	 * Note that since we currently have complete control over bp-themes and
+	 * the bp-legacy folders, it's fine to hardcode these here. If at a
+	 * later date we need to automate this, an API will need to be built.
+	 *
+	 * @since 1.7.0
+	 */
+	public function register_theme_packages() {
+
+		// Register the default theme compatibility package
+		bp_register_theme_package( array(
+			'id'      => 'legacy',
+			'name'    => __( 'BuddyPress Legacy', 'buddypress' ),
+			'version' => bp_get_version(),
+			'dir'     => trailingslashit( $this->themes_dir . '/bp-legacy' ),
+			'url'     => trailingslashit( $this->themes_url . '/bp-legacy' )
+		) );
+
+		bp_register_theme_package( array(
+			'id'      => 'nouveau',
+			'name'    => __( 'BuddyPress Nouveau', 'buddypress' ),
+			'version' => bp_get_version(),
+			'dir'     => trailingslashit( $this->themes_dir . '/bp-nouveau' ),
+			'url'     => trailingslashit( $this->themes_url . '/bp-nouveau' )
+		) );
+
+		// Register the basic theme stack. This is really dope.
+		bp_register_template_stack( 'get_stylesheet_directory', 10 );
+		bp_register_template_stack( 'get_template_directory',   12 );
+		bp_register_template_stack( 'bp_get_theme_compat_dir',  14 );
+	}
+
+	/**
+	 * Set up the default BuddyPress theme compatibility location.
+	 *
+	 * @since 1.7.0
+	 */
+	public function setup_theme() {
+
+		// Bail if something already has this under control
+		if ( ! empty( $this->theme_compat->theme ) ) {
+			return;
+		}
+
+		// Setup the theme package to use for compatibility
+		bp_setup_theme_compat( bp_get_theme_package_id() );
+	}
+}
Index: /tags/4.0.0-beta1/src/humans.txt
===================================================================
--- /tags/4.0.0-beta1/src/humans.txt	(revision 12265)
+++ /tags/4.0.0-beta1/src/humans.txt	(revision 12265)
@@ -0,0 +1,116 @@
+               .$$$$$$$Z$.
+          .~$$7...........$$$.
+        .$$...?$$$$$$$$$$Z....$Z.
+      .Z$..Z$$$$$$$$$$$$$$$$Z:..$$.
+     ,$..$$$$$$$$$$$$$7...$$$$$?..$.
+   .$Z .$$$$$$$$$$..?$$.....$$$$$..Z:
+  .$$.$$$$$$$$$........$$....Z$$$$..Z,
+  :$.7$$$$$$$$..........$$....$$$$$..$.
+ .$. $$$$$$$$$...........$....$$$$$$..$.
+.$$.$$$$$$$$$$...........$...$$$$$$$$.$.
+.$. $$$$$$$$$$ ..........Z..$$$$$$$$$.$$.
+.$.,$$$$$$$$$$$.........$$$$Z$$$$$$$$..$.
+.Z $$$$$$$$$$$$$$....+$$$$$,,,,~$$$$$..$.
+.$.=$$$$$$Z,,,,,?$$$$$,,,,,Z$,,,,$$$$..$.
+.$..$$$$$$,,,,,,,,Z$$,,,,,,,,$:,,~$$$.:$.
+.$$.$$$$$,,,,,,,,,,,,,,,,,,,,:$,,:$$$.Z.
+ .$..$$$$,,,,,,,,,,,,,,,,,,,,:$,,,$$..$.
+ .$$.$$$:,,,,,,,,,,,,,,,,,,,,,Z$$$$+.$.
+  .$= $$~,,,,,,:::,:,,:,,::,:,$$$$Z.$Z.
+   .$+.$$$$$$$$$$$$$$$$$$$$$$$$$$:.$$.
+    .$$..Z$$$$$$$$$$$$$$$$$$$$$Z..$7.
+      .$...$$$$$$$$$$$$$$$$$$$..Z$.
+        .Z$=..?Z$$$$$$$$$$$Z...$$.
+          .$$7.............Z$$.
+             .7Z$$$$$$$$$$~
+
+   We are the humans behind BuddyPress
+
+/* TEAM */
+Name: Andy Peatling
+Title: Founding Developer
+Twitter: apeatling
+
+Name: John James Jacoby
+Title: Project Lead
+Twitter: jjj
+Favorite Food: Truffle Lasagna
+
+Name: Boone B. Gorges
+Title: Lead Developer
+Twitter: boone
+Favorite Pudgy Pie Filling: Cherry
+
+Name: Paul Gibbs
+Title: Lead Developer
+Twitter: pgibbs
+Favourite Food: Pizza
+
+Name: Ray
+Title: Core Developer
+Twitter: r-a-y
+Favourite Food: tbd
+
+Name: Mathieu Viet
+Title: Core Developer
+Twitter: imath
+Favorite Food: Spaghetti Bolognese
+
+Name: Mercime
+Title: Navigator
+Twitter: mercime_one
+Favorite Food: Paella Valenciana
+
+Name: David Cavins
+Title: Core Developer
+Twitter: daveycavey
+Favorite Food: Quad-Cities pizza
+
+Name: Michael Beckwith
+Title: Core Developer
+Twitter: tw2113
+Favorite Food: Jelly Beans
+
+Name: Hugo Ashmore (hnla)
+Title: Core Developer
+Twitter: hnla
+Favourite Food: Sunday Roast or Curries
+
+Name: Henry Wright
+Title: Community Support
+Twitter: henrywright
+Favourite Food: Broccoli
+
+Name: danbp
+Title: Community Support
+Twitter: danbpfr
+Favorite Food: Saltimbocca alla Romana
+
+Name: shanebp
+Title: Community Support
+
+Name: Stephen Edgar
+Title: Core Developer
+Twitter: netweb
+Favorite Food: Tacos
+
+Name: Slava Abakumov
+Title: Core Developer
+Twitter: slaFFik
+Favorite Food: Cakes
+
+Name: Venutius
+Title: Community Support
+Favourite Food: Apple Crumble
+
+Name: Laurens Offereins
+Title: Core Developer
+Twitter: lmoffereins
+Favorite Food: Lasagna
+
+/* THANKS */
+modemlooper, cnorris23, karmatosed, photomatt
+
+/* META */
+Updated: 2018/04/05
+See: http://humanstxt.org/
Index: /tags/4.0.0-beta1/src/license.txt
===================================================================
--- /tags/4.0.0-beta1/src/license.txt	(revision 12265)
+++ /tags/4.0.0-beta1/src/license.txt	(revision 12265)
@@ -0,0 +1,280 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
Index: /tags/4.0.0-beta1/src/readme.txt
===================================================================
--- /tags/4.0.0-beta1/src/readme.txt	(revision 12265)
+++ /tags/4.0.0-beta1/src/readme.txt	(revision 12265)
@@ -0,0 +1,136 @@
+=== BuddyPress ===
+Contributors: johnjamesjacoby, DJPaul, boonebgorges, r-a-y, imath, mercime, tw2113, dcavins, hnla, karmatosed, slaFFik, dimensionmedia, henrywright, netweb, offereins, espellcaste, modemlooper, danbp, Venutius, apeatling, shanebp
+Tags: user profiles, activity streams, messaging, friends, user groups, notifications, community, social networking
+Requires at least: 4.6
+Tested up to: 4.9.8
+Requires PHP: 5.3
+Stable tag: 3.0.0
+License: GPLv2 or later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+BuddyPress helps site builders & developers add community features to their websites, with user profiles, activity streams, and more!
+
+== Description ==
+
+Are you looking for modern, robust, and sophisticated social network software? BuddyPress is a suite of components that are common to a typical social network, and allows for great add-on features through WordPress's extensive plugin system.
+
+Aimed at site builders & developers, BuddyPress is focused on ease of integration, ease of use, and extensibility. It is deliberately powerful yet unbelievably simple social network software, built by contributors to WordPress.
+
+https://wordpress.tv/2015/08/23/rocio-valdivia-buddypress-much-more-than-a-plugin/
+
+Members can register on your site to create user profiles, have private conversations, make social connections, create and interact in groups, and much more. Truly a social network in a box, BuddyPress helps you build a home for your company, school, sports team, or other niche community.
+
+= Built with developers in mind =
+
+BuddyPress helps site builders & developers add community features to their websites. It comes with a robust theme compatibility API that does its best to make every BuddyPress content page look and feel right with just about any WordPress theme. You will likely need to adjust some styling on your own to make everything look pristine.
+
+BuddyPress themes are just WordPress themes with additional templates, and with a little work, you could easily create your own, too! A handful of BuddyPress-specific themes are readily available for download from WordPress.org, and lots more are available from third-party theme authors.
+
+BuddyPress also comes with built-in support for Akismet and [bbPress](https://wordpress.org/plugins/bbpress/), two very popular and very powerful WordPress plugins. If you're using either, visit their settings pages and ensure everything is configured to your liking.
+
+= The BuddyPress ecosystem =
+
+WordPress.org is home to some amazing extensions for BuddyPress, including:
+
+- [rtMedia for WordPress, BuddyPress and bbPress](https://wordpress.org/plugins/buddypress-media/)
+- [BuddyPress Docs](https://wordpress.org/plugins/buddypress-docs/)
+- [BuddyPress Activity Plus](https://wordpress.org/plugins/buddypress-activity-plus/)
+
+Search WordPress.org for "BuddyPress" to find them all!
+
+= Join our community =
+
+If you're interested in contributing to BuddyPress, we'd love to have you. Head over to the [BuddyPress Documentation](https://codex.buddypress.org/participate-and-contribute/) site to find out how you can pitch in.
+
+BuddyPress is available in many languages thanks to the volunteer efforts of individuals all around the world. Check out our <a href="https://codex.buddypress.org/translations/">translations page</a> on the BuddyPress Documentation site for more details. If you are a polygot, please <a href="https://translate.wordpress.org/projects/wp-plugins/buddypress">consider helping translate BuddyPress</a> into your language.
+
+Growing the BuddyPress community means better software for everyone!
+
+== Installation ==
+
+= Requirements =
+
+To run BuddyPress, we recommend your host supports:
+
+* PHP version 7.2 or greater.
+* MySQL version 5.6 or greater, or, MariaDB version 10.0 or greater.
+* HTTPS support.
+
+= Automatic installation =
+
+Automatic installation is the easiest option as WordPress handles everything itself. To do an automatic install of BuddyPress, log in to your WordPress dashboard, navigate to the Plugins menu and click Add New.
+
+In the search field type "BuddyPress" and click Search Plugins. Once you've found it, you can view details about the latest release, such as community reviews, ratings, and description. Install BuddyPress by simply pressing "Install Now".
+
+Once activated:
+
+1. Visit 'Settings > BuddyPress > Components' and adjust the active components to match your community. (You can always toggle these later.)
+2. Visit 'Settings > BuddyPress > Pages' and setup your directories and special pages. We create a few automatically, but suggest you customize these to fit the flow and verbiage of your site.
+3. Visit 'Settings > BuddyPress > Settings' and take a moment to match BuddyPress's settings to your expectations. We pick the most common configuration by default, but every community is different.
+
+== Frequently Asked Questions ==
+
+= Can I use my existing WordPress theme? =
+
+Yes! BuddyPress works out-of-the-box with nearly every WordPress theme.
+
+= Will this work on WordPress multisite? =
+
+Yes! If your WordPress installation has multisite enabled, BuddyPress will support the global tracking of blogs, posts, comments, and even custom post types with a little bit of custom code.
+
+Furthermore, BuddyPress can be activated and operate in just about any scope you need for it to:
+
+* Activate at the site level to only load BuddyPress on that site.
+* Activate at the network level for full integration with all sites in your network. (This is the most common multisite installation type.)
+* Enable <a href="https://codex.buddypress.org/getting-started/customizing/bp_enable_multiblog/">multiblog</a> mode to allow your BuddyPress content to be displayed on any site in your WordPress Multisite network, using the same central data.
+* Extend BuddyPress with a third-party multi-network plugin to allow each site or network to have an isolated and dedicated community, all from the same WordPress installation.
+
+Read <a href="https://codex.buddypress.org/getting-started/installation-in-wordpress-multisite/">custom BuddyPress activations </a> for more information.
+
+= Where can I get support? =
+
+Our community provides free support at <a href="https://buddypress.org/support/">https://buddypress.org/support/</a>.
+
+= Where can I find documentation? =
+
+Our documentation site can be found at <a href="https://codex.buddypress.org/">https://codex.buddypress.org/</a>.
+
+= Where can I report a bug? =
+
+Report bugs, suggest ideas, and participate in development at <a href="https://buddypress.trac.wordpress.org/">https://buddypress.trac.wordpress.org</a>.
+
+= Where can I get the bleeding edge version of BuddyPress? =
+
+Check out the development trunk of BuddyPress from Subversion at <a href="https://buddypress.svn.wordpress.org/trunk/">https://buddypress.svn.wordpress.org/trunk/</a>, or clone from Git at git://buddypress.git.wordpress.org/.
+
+= Who builds BuddyPress? =
+
+BuddyPress is free software, built by an international community of volunteers. Some contributors to BuddyPress are employed by companies that use BuddyPress, while others are consultants who offer BuddyPress-related services for hire. No one is paid by the BuddyPress project for his or her contributions.
+
+If you would like to provide monetary support to BuddyPress, please consider a donation to the <a href="https://wordpressfoundation.org">WordPress Foundation</a>, or ask your favorite contributor how they prefer to have their efforts rewarded.
+
+= Discussion Forums =
+
+Try <a href="https://wordpress.org/plugins/bbpress/">bbPress</a>. It integrates with BuddyPress Groups, Profiles, and Notifications. Each group on your site can choose to have its own forum, and each user's topics, replies, favorites, and subscriptions appear in their profiles.
+
+== Screenshots ==
+
+1. **Activity Streams** - Global, personal, and group activity streams with threaded commenting, direct posting, favoriting and @mentions. All with full RSS feeds and email notification support.
+2. **Extended Profiles** - Fully editable profile fields allow you to define the fields users can fill in to describe themselves. Tailor profile fields to suit your audience.
+3. **User Settings** - Give your users complete control over profile and notification settings. Settings are fully integrated into your theme, and can be disabled by the administrator.
+4. **Extensible Groups** - Powerful public, private or hidden groups allow your users to break the discussion down into specific topics. Extend groups with your own custom features using the group extension API.
+5. **Friend Connections** - Let your users make connections so they can track the activity of others, or filter to show only those users they care about the most.
+6. **Private Messaging** - Private messaging will allow your users to talk to each other directly and in private. Not just limited to one-on-one discussions, your users can send messages to multiple recipients.
+7. **Site Tracking** - Track posts and comments in the activity stream, and allow your users to add their own blogs using WordPress' Multisite feature.
+8. **Notifications** - Keep your members up-to-date with relevant activity via toolbar and email notifications.
+
+== Upgrade Notice ==
+
+= 4.0.0 =
+See: https://codex.buddypress.org/releases/version-4-0-0/
+
+== Changelog ==
+
+= 4.0.0 =
+
+See: https://codex.buddypress.org/releases/version-4-0-0/
Index: /tags/4.0.0-beta1/tests/phpunit/assets/attachment-extensions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/assets/attachment-extensions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/assets/attachment-extensions.php	(revision 12265)
@@ -0,0 +1,16 @@
+<?php
+/**
+ * The following implementations of BP_Attachment act as dummy plugins
+ * for our unit tests
+ */
+class BPTest_Attachment_Extension extends BP_Attachment {
+	public function __construct( $args = array() ) {
+		return parent::__construct( $args );
+	}
+
+	public function upload_dir_filter( $upload_dir = array() ) {
+		$this->original_upload_dir = $upload_dir;
+
+		return parent::upload_dir_filter( $upload_dir );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/assets/group-extensions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/assets/group-extensions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/assets/group-extensions.php	(revision 12265)
@@ -0,0 +1,428 @@
+<?php
+
+/**
+ * The following implementations of BP_Group_Extension act as dummy plugins
+ * for our unit tests
+ */
+
+class BPTest_Group_Extension_Parse_Legacy_Properties extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+		$this->name = $class_name;
+		$this->slug = sanitize_title( $class_name );
+		$this->admin_name = $this->name . ' Edit';
+		$this->admin_slug = $this->slug . '-edit';
+		$this->create_name = $this->name . ' Create';
+		$this->create_slug = $this->slug . '-create';
+		$this->visibility = 'private';
+		$this->create_step_position = 58;
+		$this->nav_item_position = 63;
+		$this->admin_metabox_context = 'high';
+		$this->admin_metabox_priority = 'side';
+		$this->enable_create_step = false;
+		$this->enable_nav_item = true;
+		$this->enable_edit_item = false;
+		$this->enable_admin_item = true;
+		$this->nav_item_name = $this->name . ' Nav';
+		$this->display_hook = 'foo_hook';
+		$this->template_file = 'foo_template';
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _parse_legacy_properties() {
+		return $this->parse_legacy_properties();
+	}
+
+	/**
+	 * Provides access to protected property unneeded in BP
+	 */
+	function _get_legacy_properties_converted() {
+		return $this->legacy_properties_converted;
+	}
+}
+
+class BPTest_Group_Extension_Setup_Screens_Use_Global_Fallbacks extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+		$this->slug = sanitize_title( $class_name );
+		$this->name = $class_name;
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _get_default_screens() {
+		return $this->get_default_screens();
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _setup_class_info() {
+		return $this->setup_class_info();
+	}
+
+	function settings_screen( $group_id = null ) {}
+	function settings_screen_save( $group_id = null ) {}
+}
+
+class BPTest_Group_Extension_Setup_Screens_Define_Edit_Screens_Locally extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+		$this->slug = sanitize_title( $class_name );
+		$this->name = $class_name;
+	}
+
+	function edit_screen( $group_id = null ) {}
+	function edit_screen_save( $group_id = null ) {}
+	function settings_screen( $group_id = null ) {}
+	function settings_screen_save( $group_id = null ) {}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _get_default_screens() {
+		return $this->get_default_screens();
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _setup_class_info() {
+		return $this->setup_class_info();
+	}
+
+}
+
+class BPTest_Group_Extension_Access_Root_Property extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'slug' => sanitize_title( $class_name ),
+			'name' => $class_name,
+			'nav_item_position' => 39,
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_Init_Property_Using_Legacy_Location extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'slug' => sanitize_title( $class_name ),
+			'name' => $class_name,
+			'screens' => array(
+				'create' => array(
+					'position' => 18,
+				),
+			),
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Get_Screen_Callback_Fallbacks extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'slug' => sanitize_title( $class_name ),
+			'name' => $class_name,
+		);
+
+		parent::init( $args );
+	}
+
+	function settings_screen( $group_id = null ) {}
+	function settings_screen_save( $group_id = null ) {}
+
+	function edit_screen( $group_id = null ) {}
+	function edit_screen_save( $group_id = null ) {}
+}
+
+class BPTest_Group_Extension_Enable_Nav_Item_True extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'enable_nav_item' => true,
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Enable_Nav_Item_False extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'enable_nav_item' => false,
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Visibility_Private extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'visibility' => 'private',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Visibility_Public extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'visibility' => 'public',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Inferred_Access_Settings_EnableNavItem_True extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'enable_nav_item' => true,
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Inferred_Access_Settings_EnableNavItem_False extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'enable_nav_item' => false,
+		);
+
+		parent::init( $args );
+	}
+}
+class BPTest_Group_Extension_Access_Anyone extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'anyone',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_Loggedin extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'loggedin',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_Member extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'member',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_AdminMod extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => array(
+				'mod',
+				'admin',
+			),
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_Admin extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'admin',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_Noone extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'noone',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_ShowTab_Anyone extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'admin',
+			'show_tab' => 'anyone',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_ShowTab_Loggedin extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'admin',
+			'show_tab' => 'loggedin',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_ShowTab_Member extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'admin',
+			'show_tab' => 'member',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_ShowTab_AdminMod extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'admin',
+			'show_tab' => array(
+				'mod',
+				'admin',
+			),
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_ShowTab_Admin extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'admin',
+			'show_tab' => 'admin',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_ShowTab_Noone extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+			'access' => 'noone',
+			'show_tab' => 'noone',
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Widget_Method extends BP_Group_Extension {
+	public function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'name' => $class_name,
+			'slug' => sanitize_title( $class_name ),
+		);
+
+		parent::init( $args );
+	}
+
+	public function widget_display() {
+		echo 'Widget Displayed';
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/bootstrap.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/bootstrap.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/bootstrap.php	(revision 12265)
@@ -0,0 +1,20 @@
+<?php
+
+require( dirname( __FILE__ ) . '/includes/define-constants.php' );
+
+if ( ! file_exists( WP_TESTS_DIR . '/includes/functions.php' ) ) {
+	die( "The WordPress PHPUnit test suite could not be found.\n" );
+}
+
+require_once WP_TESTS_DIR . '/includes/functions.php';
+
+function _install_and_load_buddypress() {
+	require BP_TESTS_DIR . '/includes/loader.php';
+}
+tests_add_filter( 'muplugins_loaded', '_install_and_load_buddypress' );
+
+require WP_TESTS_DIR . '/includes/bootstrap.php';
+
+// Load the BP-specific testing tools
+require BP_TESTS_DIR . '/includes/testcase.php';
+require BP_TESTS_DIR . '/includes/testcase-emails.php';
Index: /tags/4.0.0-beta1/tests/phpunit/codecoverage.xml
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/codecoverage.xml	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/codecoverage.xml	(revision 12265)
@@ -0,0 +1,23 @@
+<phpunit
+	bootstrap="bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./testcases/</directory>
+		</testsuite>
+	</testsuites>
+	<filter>
+		<whitelist>
+			<directory suffix=".php">../../src/</directory>
+			<file>../../bp-loader.php</file>
+		</whitelist>
+	</filter>
+	<logging>
+		<log type="coverage-clover" target="../../clover.xml"/>
+	</logging>
+</phpunit>
Index: /tags/4.0.0-beta1/tests/phpunit/includes/define-constants.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/define-constants.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/define-constants.php	(revision 12265)
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * Define constants needed by test suite.
+ */
+
+define( 'BP_PLUGIN_DIR', dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/src/' );
+
+if ( ! defined( 'BP_TESTS_DIR' ) ) {
+	define( 'BP_TESTS_DIR', dirname( dirname( __FILE__ ) ) . '/' );
+}
+
+/**
+ * Determine where the WP test suite lives. Three options are supported:
+ *
+ * - Define a WP_DEVELOP_DIR environment variable, which points to a checkout
+ *   of the develop.svn.wordpress.org repository (this is recommended)
+ * - Define a WP_TESTS_DIR environment variable, which points to a checkout of
+ *   WordPress test suite
+ * - Assume that we are inside of a develop.svn.wordpress.org setup, and walk
+ *   up the directory tree
+ */
+if ( false !== getenv( 'WP_TESTS_DIR' ) ) {
+	define( 'WP_TESTS_DIR', getenv( 'WP_TESTS_DIR' ) );
+	define( 'WP_ROOT_DIR', WP_TESTS_DIR );
+} else {
+	// Support WP_DEVELOP_DIR, as used by some plugins
+	if ( false !== getenv( 'WP_DEVELOP_DIR' ) ) {
+		define( 'WP_ROOT_DIR', getenv( 'WP_DEVELOP_DIR' ) );
+	} else {
+		define( 'WP_ROOT_DIR', dirname( dirname( dirname( dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) ) ) ) ) );
+	}
+
+	define( 'WP_TESTS_DIR', WP_ROOT_DIR . '/tests/phpunit' );
+}
+
+// Based on the tests directory, look for a config file
+if ( file_exists( WP_ROOT_DIR . '/wp-tests-config.php' ) ) {
+	// Standard develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_ROOT_DIR . '/wp-tests-config.php' );
+
+} elseif ( file_exists( WP_TESTS_DIR . '/wp-tests-config.php' ) ) {
+	// Legacy unit-test.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_TESTS_DIR . '/wp-tests-config.php' );
+
+} elseif ( file_exists( dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' ) ) {
+	// Environment variable exists and points to tests/phpunit of
+	// develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' );
+
+} else {
+	die( "wp-tests-config.php could not be found.\n" );
+}
Index: /tags/4.0.0-beta1/tests/phpunit/includes/factory.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/factory.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/factory.php	(revision 12265)
@@ -0,0 +1,343 @@
+<?php
+class BP_UnitTest_Factory extends WP_UnitTest_Factory {
+	public $activity = null;
+
+	function __construct() {
+		parent::__construct();
+
+		$this->user = new BP_UnitTest_Factory_For_User( $this );
+		$this->activity = new BP_UnitTest_Factory_For_Activity( $this );
+		$this->group = new BP_UnitTest_Factory_For_Group( $this );
+		$this->message = new BP_UnitTest_Factory_For_Message( $this );
+		$this->xprofile_group = new BP_UnitTest_Factory_For_XProfileGroup( $this );
+		$this->xprofile_field = new BP_UnitTest_Factory_For_XProfileField( $this );
+		$this->notification = new BP_UnitTest_Factory_For_Notification( $this );
+		$this->signup = new BP_UnitTest_Factory_For_Signup( $this );
+		$this->friendship = new BP_UnitTest_Factory_For_Friendship( $this );
+	}
+}
+
+class BP_UnitTest_Factory_For_User extends WP_UnitTest_Factory_For_User {
+	/**
+	 * When creating a new user, it's almost always necessary to have the
+	 * last_activity usermeta set right away, so that the user shows up in
+	 * directory queries. This is a shorthand wrapper for the user factory
+	 * create() method.
+	 *
+	 * Also set a display name
+	 */
+	public function create_object( $args ) {
+		$r = wp_parse_args( $args, array(
+			'role' => 'subscriber',
+			'last_activity' => date( 'Y-m-d H:i:s', strtotime( bp_core_current_time() ) - 60*60*24*365 ),
+		) );
+
+		$last_activity = $r['last_activity'];
+		unset( $r['last_activity'] );
+
+		$user_id = wp_insert_user( $r );
+
+		bp_update_user_last_activity( $user_id, $last_activity );
+
+		if ( bp_is_active( 'xprofile' ) ) {
+			$user = new WP_User( $user_id );
+			xprofile_set_field_data( 1, $user_id, $user->display_name );
+		}
+
+		return $user_id;
+	}
+}
+
+class BP_UnitTest_Factory_For_Activity extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'component'    => buddypress()->activity->id,
+			'content'      => new WP_UnitTest_Generator_Sequence( 'Activity content %s' ),
+			'primary_link' => 'http://example.com',
+			'type'         => 'activity_update',
+			'recorded_time' => bp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['user_id'] ) )
+			$args['user_id'] = get_current_user_id();
+
+		return bp_activity_add( $args );
+	}
+
+	function update_object( $activity_id, $fields ) {
+		$activity = new BP_Activity_Activity( $activity_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $activity->$field_name ) )
+				$activity->$field_name = $value;
+		}
+
+		$activity->save();
+		return $activity;
+	}
+
+	function get_object_by_id( $user_id ) {
+		return new BP_Activity_Activity( $user_id );
+	}
+}
+
+class BP_UnitTest_Factory_For_Group extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'Group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'Group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'group-slug-%s' ),
+			'status'       => 'public',
+			'enable_forum' => true,
+			'date_created' => bp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['creator_id'] ) ) {
+			if ( is_user_logged_in() ) {
+				$args['creator_id'] = get_current_user_id();
+
+			// Create a user. This is based on from BP_UnitTestCase->create_user().
+			} else {
+				$last_activity      = date( 'Y-m-d H:i:s', strtotime( bp_core_current_time() ) - 60 * 60 * 24 * 365 );
+				$user_factory       = new WP_UnitTest_Factory_For_User();
+				$args['creator_id'] = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+
+				bp_update_user_last_activity( $args['creator_id'] , $last_activity );
+
+				if ( bp_is_active( 'xprofile' ) ) {
+					$user = new WP_User( $args['creator_id']  );
+					xprofile_set_field_data( 1, $args['creator_id'] , $user->display_name );
+				}
+			}
+		}
+
+		$group_id = groups_create_group( $args );
+		if ( ! $group_id ) {
+			return false;
+		}
+
+		groups_update_groupmeta( $group_id, 'total_member_count', 1 );
+		$last_activity = isset( $args['last_activity'] ) ? $args['last_activity'] : bp_core_current_time();
+		groups_update_groupmeta( $group_id, 'last_activity', $last_activity );
+
+		return $group_id;
+	}
+
+	function update_object( $group_id, $fields ) {
+		$group = new BP_Groups_Group( $group_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $group->field_name ) )
+				$group->field_name = $value;
+		}
+
+		$group->save();
+		return $group;
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BP_Groups_Group( $group_id );
+	}
+}
+
+class BP_UnitTest_Factory_For_Message extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'sender_id'  => get_current_user_id(),
+			'thread_id'  => 0,
+			'subject'    => new WP_UnitTest_Generator_Sequence( 'Message subject %s' ),
+			'content'    => new WP_UnitTest_Generator_Sequence( 'Message content %s' ),
+			'date_sent'  => bp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( empty( $args['sender_id'] ) ) {
+			$args['sender_id'] = $this->factory->user->create();
+		}
+
+		if ( empty( $args['recipients'] ) ) {
+			$recipient = $this->factory->user->create_and_get();
+			$args['recipients'] = array( $recipient->user_nicename );
+		}
+
+		$thread_id = messages_new_message( $args );
+		$thread = new BP_Messages_Thread( $thread_id );
+		return end( $thread->messages )->id;
+	}
+
+	function update_object( $message_id, $fields ) {
+		// todo
+	}
+
+	function get_object_by_id( $message_id ) {
+		return new BP_Messages_Message( $message_id );
+	}
+}
+
+class BP_UnitTest_Factory_For_XProfileGroup extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'xprofile-group-slug-%s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$group_id = xprofile_insert_field_group( $args );
+		return $group_id;
+	}
+
+	function update_object( $group_id, $fields ) {
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BP_XProfile_Group( $group_id );
+	}
+}
+
+class BP_UnitTest_Factory_For_XProfileField extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'        => new WP_UnitTest_Generator_Sequence( 'XProfile field %s' ),
+			'description' => new WP_UnitTest_Generator_Sequence( 'XProfile field description %s' ),
+			'type'        => 'textbox',
+		);
+	}
+
+	function create_object( $args ) {
+		$field_id = xprofile_insert_field( $args );
+		return $field_id;
+	}
+
+	function update_object( $field_id, $fields ) {
+	}
+
+	function get_object_by_id( $field_id ) {
+		return new BP_XProfile_Field( $field_id );
+	}
+}
+
+class BP_UnitTest_Factory_For_Notification extends WP_UnitTest_Factory_For_Thing {
+	public function __construct( $factory = null ) {
+		parent::__construct( $factory );
+	}
+
+	public function create_object( $args ) {
+		return bp_notifications_add_notification( $args );
+	}
+
+	public function update_object( $id, $fields ) {}
+
+	public function get_object_by_id( $id ) {
+		return new BP_Notifications_Notification( $id );
+	}
+}
+
+class BP_UnitTest_Factory_For_Signup extends WP_UnitTest_Factory_For_Thing {
+	public function __construct( $factory = null ) {
+		parent::__construct( $factory );
+	}
+
+	public function create_object( $args ) {
+		return BP_Signup::add( $args );
+	}
+
+	public function update_object( $id, $fields ) {}
+
+	public function get_object_by_id( $id ) {
+		return new BP_Signup( $id );
+	}
+}
+
+/**
+ * Factory for friendships.
+ *
+ * @since 2.7.0
+ */
+class BP_UnitTest_Factory_For_Friendship extends WP_UnitTest_Factory_For_Thing {
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param $factory WP_UnitTest_Factory
+	 */
+	public function __construct( $factory = null ) {
+		parent::__construct( $factory );
+	}
+
+	/**
+	 * Create friendship object.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param array $args Array of arguments.
+	 * @return int Friendship ID.
+	 */
+	public function create_object( $args ) {
+		$friendship = new BP_Friends_Friendship();
+
+		foreach ( array( 'initiator_user_id', 'friend_user_id' ) as $arg ) {
+			if ( isset( $args[ $arg ] ) ) {
+				$friendship->$arg = $args[ $arg ];
+			} else {
+				$friendship->$arg = $this->factory->user->create();
+			}
+		}
+
+		foreach ( array( 'is_confirmed', 'is_limited', 'date_created' ) as $arg ) {
+			if ( isset( $args[ $arg ] ) ) {
+				$friendship->$arg = $args[ $arg ];
+			}
+		}
+
+		$friendship->save();
+
+		return $friendship->id;
+	}
+
+	/**
+	 * Update a friendship object.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @todo Implement.
+	 *
+	 * @param int   $id     ID of the friendship.
+	 * @param array $fields Fields to update.
+	 */
+	public function update_object( $id, $fields ) {}
+
+	/**
+	 * Get a friendship object by its ID.
+	 *
+	 * @since 2.7.0
+	 *
+	 * @param int $id
+	 * @return BP_Friends_Friendship
+	 */
+	public function get_object_by_id( $id ) {
+		return new BP_Friends_Friendship( $id );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/includes/install.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/install.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/install.php	(revision 12265)
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Installs BuddyPress for the purpose of the unit-tests
+ *
+ * @todo Reuse the init/load code in init.php
+ * @todo Support MULTIBLOG
+ */
+error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT );
+
+$config_file_path = $argv[1];
+$tests_dir_path = $argv[2];
+$multisite = ! empty( $argv[3] );
+
+require_once $config_file_path;
+require_once $tests_dir_path . '/includes/functions.php';
+require_once $tests_dir_path . '/includes/mock-mailer.php';
+
+function _load_buddypress() {
+	require dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/src/bp-loader.php';
+	do_action( 'activate_src/bp-loader.php' );
+}
+tests_add_filter( 'muplugins_loaded', '_load_buddypress' );
+
+define( 'BP_PLUGIN_DIR', dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/src/' );
+define( 'BP_ROOT_BLOG', 1 );
+
+// Always load admin bar
+tests_add_filter( 'show_admin_bar', '__return_true' );
+
+function wp_test_bp_install( $value ) {
+	return array( 'activity' => 1, 'blogs' => 1, 'friends' => 1, 'groups' => 1, 'members' => 1, 'messages' => 1, 'notifications' => 1, 'settings' => 1, 'xprofile' => 1, );
+}
+tests_add_filter( 'bp_new_install_default_components', 'wp_test_bp_install' );
+
+tests_add_filter( 'bp_get_signup_allowed', '__return_true' );
+
+$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
+$_SERVER['HTTP_HOST'] = WP_TESTS_DOMAIN;
+$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php';
+
+require_once ABSPATH . '/wp-settings.php';
+
+echo "Installing BuddyPress...\n";
+
+$wpdb->query( 'SET default_storage_engine = INNODB' );
+$wpdb->select( DB_NAME, $wpdb->dbh );
+
+// Drop BuddyPress tables.
+foreach ( $wpdb->get_col( "SHOW TABLES LIKE '" . $wpdb->prefix . "bp%'" ) as $bp_table ) {
+	$wpdb->query( "DROP TABLE {$bp_table}" );
+}
+
+function _bp_mock_mailer( $class ) {
+	return 'BP_UnitTest_Mailer';
+}
+tests_add_filter( 'bp_send_email_delivery_class', '_bp_mock_mailer' );
+
+// Install BuddyPress
+bp_version_updater();
Index: /tags/4.0.0-beta1/tests/phpunit/includes/loader.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/loader.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/loader.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+
+require_once( dirname( __FILE__ ) . '/define-constants.php' );
+
+$multisite = (int) ( defined( 'WP_TESTS_MULTISITE') && WP_TESTS_MULTISITE );
+system( WP_PHP_BINARY . ' ' . escapeshellarg( dirname( __FILE__ ) . '/install.php' ) . ' ' . escapeshellarg( WP_TESTS_CONFIG_PATH ) . ' ' . escapeshellarg( WP_TESTS_DIR ) . ' ' . $multisite );
+
+// Bootstrap BP
+require dirname( __FILE__ ) . '/../../../src/bp-loader.php';
+
+// Bail from redirects as they throw 'headers already sent' warnings.
+tests_add_filter( 'wp_redirect', '__return_false' );
+
+require_once( dirname( __FILE__ ) . '/mock-mailer.php' );
+function _bp_mock_mailer( $class ) {
+	return 'BP_UnitTest_Mailer';
+}
+tests_add_filter( 'bp_send_email_delivery_class', '_bp_mock_mailer' );
+
+/**
+ * Load up component action and screen code.
+ *
+ * In BuddyPress, this is loaded conditionally, but PHPUnit needs all files
+ * loaded at the same time to prevent weird load order issues.
+ */
+$components = array( 'activity', 'blogs', 'friends', 'groups', 'members', 'messages', 'notifications', 'settings', 'xprofile' );
+foreach ( $components as $component ) {
+	add_action( "bp_{$component}_includes", function() use ( $component ) {
+		$dirs = array(
+			buddypress()->plugin_dir . 'bp-' . $component . '/actions/',
+			buddypress()->plugin_dir . 'bp-' . $component . '/screens/',
+		);
+
+		foreach ( $dirs as $dir ) {
+			foreach ( glob( $dir . "*.php" ) as $file ) {
+				require $file;
+			}
+		}
+	} );
+}
Index: /tags/4.0.0-beta1/tests/phpunit/includes/mock-mailer.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/mock-mailer.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/mock-mailer.php	(revision 12265)
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * Mock email delivery implementation.
+ *
+ * @since 2.5.0
+ */
+class BP_UnitTest_Mailer implements BP_Email_Delivery {
+
+	/**
+	 * Send email(s).
+	 *
+	 * @param BP_Email $email Email to send.
+	 * @return bool False if some error occurred.
+	 * @since 2.5.0
+	 */
+	public function bp_email( BP_Email $email ) {
+		return true;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/includes/testcase-emails.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/testcase-emails.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/testcase-emails.php	(revision 12265)
@@ -0,0 +1,35 @@
+<?php
+require_once dirname( __FILE__ ) . '/testcase.php';
+
+class BP_UnitTestCase_Emails extends BP_UnitTestCase {
+
+	public static function setUpBeforeClass() {
+		parent::setUpBeforeClass();
+
+		require_once( buddypress()->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
+
+		/*
+		 * WP's test suite wipes out BP's email posts.
+		 * We must reestablish them before our tests can be successfully run.
+		 */
+		bp_core_install_emails();
+	}
+
+	public static function tearDownAfterClass() {
+		$emails = get_posts( array(
+			'fields'           => 'ids',
+			'post_status'      => 'any',
+			'post_type'        => bp_get_email_post_type(),
+			'posts_per_page'   => 200,
+			'suppress_filters' => false,
+		) );
+
+		if ( $emails ) {
+			foreach ( $emails as $email_id ) {
+				wp_delete_post( $email_id, true );
+			}
+		}
+
+		parent::tearDownAfterClass();
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/includes/testcase.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/includes/testcase.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/includes/testcase.php	(revision 12265)
@@ -0,0 +1,423 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/factory.php';
+
+class BP_UnitTestCase extends WP_UnitTestCase {
+
+	protected $temp_has_bp_moderate = array();
+	protected static $cached_SERVER_NAME = null;
+
+	/**
+	 * A flag indicating whether an autocommit has been detected inside of a test.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @var bool
+	 */
+	protected $autocommitted = false;
+
+	/**
+	 * A list of components that have been deactivated during a test.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @var array
+	 */
+	protected $deactivated_components = array();
+
+	/**
+	 * Cribbed from WP so that the self::factory() call comes from this class.
+	 *
+	 * @since 3.0.0
+	 */
+	public static function setUpBeforeClass() {
+		global $wpdb;
+
+		// Fake WP mail globals, to avoid errors
+		add_filter( 'wp_mail', array( 'BP_UnitTestCase', 'setUp_wp_mail' ) );
+		add_filter( 'wp_mail_from', array( 'BP_UnitTestCase', 'tearDown_wp_mail' ) );
+
+		$c = self::get_called_class();
+		if ( ! method_exists( $c, 'wpSetUpBeforeClass' ) ) {
+			self::commit_transaction();
+			return;
+		}
+
+		call_user_func( array( $c, 'wpSetUpBeforeClass' ), self::factory() );
+
+		self::commit_transaction();
+	}
+
+	public function setUp() {
+		parent::setUp();
+
+		/*
+		 * WP's test suite wipes out BP's directory page mappings with `_delete_all_posts()`.
+		 * We must reestablish them before our tests can be successfully run.
+		 */
+		bp_core_add_page_mappings( bp_get_option( 'bp-active-components' ), 'delete' );
+
+
+		$this->factory = new BP_UnitTest_Factory;
+
+		// Fixes warnings in multisite functions
+		$_SERVER['REMOTE_ADDR'] = '';
+		global $wpdb;
+
+		// Clean up after autocommits.
+		add_action( 'bp_blogs_recorded_existing_blogs', array( $this, 'set_autocommit_flag' ) );
+
+		// Make sure Activity actions are reset before each test
+		$this->reset_bp_activity_actions();
+
+		// Make sure all Post types activities globals are reset before each test
+		$this->reset_bp_activity_post_types_globals();
+	}
+
+	public function tearDown() {
+		global $wpdb;
+
+		remove_action( 'bp_blogs_recorded_existing_blogs', array( $this, 'set_autocommit_flag' ) );
+
+		parent::tearDown();
+
+		// If we detect that a COMMIT has been triggered during the test, clean up blog and user fixtures.
+		if ( $this->autocommitted ) {
+			if ( is_multisite() ) {
+				foreach ( $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs WHERE blog_id != 1" ) as $blog_id ) {
+					if ( function_exists( 'wp_uninitialize_site' ) ) {
+						wp_uninitialize_site( $blog_id );
+					} else {
+						wpmu_delete_blog( $blog_id, true );
+					}
+				}
+			}
+
+			foreach ( $wpdb->get_col( "SELECT ID FROM $wpdb->users WHERE ID != 1" ) as $user_id ) {
+				if ( is_multisite() ) {
+					wpmu_delete_user( $user_id );
+				} else {
+					wp_delete_user( $user_id );
+				}
+			}
+		}
+
+		$this->commit_transaction();
+
+		// Reactivate any components that have been deactivated.
+		foreach ( $this->deactivated_components as $component ) {
+			buddypress()->active_components[ $component ] = 1;
+		}
+		$this->deactivated_components = array();
+	}
+
+	/**
+	 * Multisite-agnostic way to delete a user from the database.
+	 *
+	 * @since 3.0.0
+	 */
+	public static function delete_user( $user_id ) {
+		$deleted = parent::delete_user( $user_id );
+
+		// When called in tearDownAfterClass(), BP's cleanup functions may no longer be hooked.
+		if ( bp_is_active( 'activity' ) ) {
+			bp_activity_remove_all_user_data( $user_id );
+		}
+
+		bp_core_remove_data( $user_id );
+
+		return $deleted;
+	}
+
+	function clean_up_global_scope() {
+		buddypress()->bp_nav                = buddypress()->bp_options_nav = buddypress()->action_variables = buddypress()->canonical_stack = buddypress()->unfiltered_uri = $GLOBALS['bp_unfiltered_uri'] = array();
+		buddypress()->current_component     = buddypress()->current_item = buddypress()->current_action = buddypress()->current_member_type = '';
+		buddypress()->unfiltered_uri_offset = 0;
+		buddypress()->is_single_item        = false;
+		buddypress()->current_user          = new stdClass();
+		buddypress()->displayed_user        = new stdClass();
+		buddypress()->loggedin_user         = new stdClass();
+		buddypress()->pages                 = array();
+
+		parent::clean_up_global_scope();
+	}
+
+	/**
+	 * Returns a factory that can be used across tests, even in static methods.
+	 *
+	 * @since 3.0
+	 *
+	 * @return BP_UnitTest_Factory
+	 */
+	protected static function factory() {
+		static $factory = null;
+		if ( ! $factory ) {
+			$factory = new BP_UnitTest_Factory();
+		}
+		return $factory;
+	}
+
+	protected function reset_bp_activity_actions() {
+		buddypress()->activity->actions = new stdClass();
+
+		/**
+		 * Populate the global with default activity actions only
+		 * before each test.
+		 */
+		do_action( 'bp_register_activity_actions' );
+	}
+
+	protected function reset_bp_activity_post_types_globals() {
+		global $wp_post_types;
+
+		// Remove all remaining tracking arguments to each post type
+		foreach ( $wp_post_types as $post_type => $post_type_arg ) {
+			if ( post_type_supports( $post_type, 'buddypress-activity' ) ) {
+				remove_post_type_support( $post_type, 'buddypress-activity' );
+			}
+
+			if ( isset( $post_type_arg->bp_activity ) ) {
+				unset( $post_type_arg->bp_activity );
+			}
+		}
+
+		buddypress()->activity->track = array();
+	}
+
+	function assertPreConditions() {
+		parent::assertPreConditions();
+
+		// Reinit some of the globals that might have been cleared by BP_UnitTestCase::clean_up_global_scope().
+		// This is here because it didn't work in clean_up_global_scope(); I don't know why.
+		do_action( 'bp_setup_globals' );
+	}
+
+	function go_to( $url ) {
+		$GLOBALS['bp']->loggedin_user = NULL;
+		$GLOBALS['bp']->pages = bp_core_get_directory_pages();
+
+		parent::go_to( $url );
+
+		do_action( 'bp_init' );
+		do_action( 'bp_template_redirect' );
+	}
+
+	/**
+	 * WP's core tests use wp_set_current_user() to change the current
+	 * user during tests. BP caches the current user differently, so we
+	 * have to do a bit more work to change it
+	 */
+	public static function set_current_user( $user_id ) {
+		$bp = buddypress();
+
+		$bp->loggedin_user->id = $user_id;
+		$bp->loggedin_user->fullname       = bp_core_get_user_displayname( $user_id );
+		$bp->loggedin_user->is_super_admin = $bp->loggedin_user->is_site_admin = is_super_admin( $user_id );
+		$bp->loggedin_user->domain         = bp_core_get_user_domain( $user_id );
+		$bp->loggedin_user->userdata       = bp_core_get_core_userdata( $user_id );
+
+		wp_set_current_user( $user_id );
+	}
+
+	public static function add_user_to_group( $user_id, $group_id, $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 1,
+			'is_admin'      => 0,
+			'is_mod'        => 0,
+			'invite_sent'   => 0,
+			'inviter_id'    => 0,
+		) );
+
+		$new_member                = new BP_Groups_Member;
+		$new_member->group_id      = $group_id;
+		$new_member->user_id       = $user_id;
+		$new_member->inviter_id    = 0;
+		$new_member->is_admin      = $r['is_admin'];
+		$new_member->is_mod        = $r['is_mod'];
+		$new_member->user_title    = '';
+		$new_member->date_modified = $r['date_modified'];
+		$new_member->is_confirmed  = $r['is_confirmed'];
+		$new_member->invite_sent   = $r['invite_sent'];
+		$new_member->inviter_id    = $r['inviter_id'];
+
+		$new_member->save();
+		return $new_member->id;
+	}
+
+	/**
+	 * We can't use grant_super_admin() because we will need to modify
+	 * the list more than once, and grant_super_admin() can only be run
+	 * once because of its global check
+	 */
+	public function grant_super_admin( $user_id ) {
+		global $super_admins;
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$user = get_userdata( $user_id );
+		$super_admins[] = $user->user_login;
+	}
+
+	public function restore_admins() {
+		// We assume that the global can be wiped out
+		// @see grant_super_admin()
+		unset( $GLOBALS['super_admins'] );
+	}
+
+	public function grant_bp_moderate( $user_id ) {
+		if ( ! isset( $this->temp_has_bp_moderate[ $user_id ] ) ) {
+			$this->temp_has_bp_moderate[ $user_id ] = 1;
+		}
+		add_filter( 'bp_current_user_can', array( $this, 'grant_bp_moderate_cb' ), 10, 2 );
+	}
+
+	public function revoke_bp_moderate( $user_id ) {
+		if ( isset( $this->temp_has_bp_moderate[ $user_id ] ) ) {
+			unset( $this->temp_has_bp_moderate[ $user_id ] );
+		}
+		remove_filter( 'bp_current_user_can', array( $this, 'grant_bp_moderate_cb' ), 10 );
+	}
+
+	public function grant_bp_moderate_cb( $retval, $capability ) {
+		$current_user = bp_loggedin_user_id();
+		if ( ! isset( $this->temp_has_bp_moderate[ $current_user ] ) ) {
+			return $retval;
+		}
+
+		if ( 'bp_moderate' == $capability ) {
+			$retval = true;
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Go to the root blog. This helps reset globals after moving between
+	 * blogs.
+	 */
+	public function go_to_root() {
+		$blog_1_url = get_blog_option( 1, 'home' );
+		$this->go_to( str_replace( $blog_1_url, '', trailingslashit( bp_get_root_domain() ) ) );
+	}
+
+	/**
+	 * Set up globals necessary to avoid errors when using wp_mail()
+	 */
+	public static function setUp_wp_mail( $args ) {
+		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
+			self::$cached_SERVER_NAME = $_SERVER['SERVER_NAME'];
+		}
+
+		$_SERVER['SERVER_NAME'] = 'example.com';
+
+		// passthrough
+		return $args;
+	}
+
+	/**
+	 * Tear down globals set up in setUp_wp_mail()
+	 */
+	public static function tearDown_wp_mail( $args ) {
+		if ( ! empty( self::$cached_SERVER_NAME ) ) {
+			$_SERVER['SERVER_NAME'] = self::$cached_SERVER_NAME;
+			self::$cached_SERVER_NAME = '';
+		} else {
+			unset( $_SERVER['SERVER_NAME'] );
+		}
+
+		// passthrough
+		return $args;
+	}
+
+	/**
+	 * Commit a MySQL transaction.
+	 */
+	public static function commit_transaction() {
+		global $wpdb;
+		$wpdb->query( 'COMMIT;' );
+	}
+
+	/**
+	 * Clean up created directories/files
+	 */
+	public function rrmdir( $dir ) {
+		// Make sure we are only removing files/dir from uploads
+		if ( 0 !== strpos( $dir, bp_core_avatar_upload_path() ) ) {
+			return;
+		}
+
+		$d = glob( $dir . '/*' );
+
+		if ( ! empty( $d ) ) {
+			foreach ( $d as $file ) {
+				if ( is_dir( $file ) ) {
+					$this->rrmdir( $file );
+				} else {
+					@unlink( $file );
+				}
+			}
+		}
+
+		@rmdir( $dir );
+	}
+
+	/**
+	 * Set a flag that an autocommit has taken place inside of a test method.
+	 *
+	 * @since 2.4.0
+	 */
+	public function set_autocommit_flag() {
+		$this->autocommitted = true;
+	}
+
+	/**
+	 * Deactivate a component for the duration of a test.
+	 *
+	 * @since 2.4.0
+	 *
+	 * @param string $component Component name.
+	 */
+	public function deactivate_component( $component ) {
+		$is_active = isset( buddypress()->active_components[ $component ] );
+
+		if ( ! isset( $component ) ) {
+			return false;
+		}
+
+		unset( buddypress()->active_components[ $component ] );
+		$this->deactivated_components[] = $component;
+	}
+
+	/**
+	 * Fake an attachment upload (doesn't actually upload a file).
+	 *
+	 * @param string $file Absolute path to valid file.
+	 * @param int $parent Optional. Post ID to attach the new post to.
+	 * @return int Attachment post ID.
+	 */
+	public function fake_attachment_upload( $file, $parent = 0 ) {
+		$mime = wp_check_filetype( $file );
+		if ( $mime ) {
+			$type = $mime['type'];
+		} else {
+			$type = '';
+		}
+
+		$url = 'http://' . WP_TESTS_DOMAIN . '/wp-content/uploads/' . basename( $file );
+		$attachment = array(
+			'guid'           => 'http://' . WP_TESTS_DOMAIN . '/wp-content/uploads/' . $url,
+			'post_content'   => '',
+			'post_mime_type' => $type,
+			'post_parent'    => $parent,
+			'post_title'     => basename( $file ),
+			'post_type'      => 'attachment',
+		);
+
+		$id = wp_insert_attachment( $attachment, $url, $parent );
+		wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $url ) );
+
+		return $id;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/multisite.xml
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/multisite.xml	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/multisite.xml	(revision 12265)
@@ -0,0 +1,17 @@
+<phpunit
+	bootstrap="bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<php>
+		<const name="WP_TESTS_MULTISITE" value="1" />
+	</php>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./testcases/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/actions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/actions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/actions.php	(revision 12265)
@@ -0,0 +1,180 @@
+<?php
+/**
+ * @group activity
+ */
+class BP_Tests_Activity_Actions extends BP_UnitTestCase {
+
+	/**
+	 * @group bp_activity_catch_transition_post_type_status
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_catch_transition_post_type_status_publish() {
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'foo',
+		) );
+
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id, 'new_foo' ), 'Published post type should have activity' );
+
+		_unregister_post_type( 'foo' );
+	}
+
+	/**
+	 * @group bp_activity_catch_transition_post_type_status
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_catch_transition_post_type_status_publish_to_publish() {
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'foo',
+		) );
+
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id, 'new_foo' ), 'Published post type should have activity' );
+
+		_unregister_post_type( 'foo' );
+	}
+
+	/**
+	 * @group bp_activity_catch_transition_post_type_status
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_catch_transition_post_type_status_publish_existing_post() {
+		$u = self::factory()->user->create();
+
+		$labels = array(
+			'bp_activity_admin_filter' => 'New Foo',
+			'bp_activity_front_filter' => 'Foos',
+		        'bp_activity_new_post'    => '%1$s posted a new <a href="%2$s">foo</a>',
+		        'bp_activity_new_post_ms' => '%1$s posted a new <a href="%2$s">foo</a>, on the site %3$s',
+		);
+
+		/**
+		 * 'public' must be set to true, otherwise bp_activity_get_post_types_tracking_args() fails.
+		 */
+		register_post_type( 'foo', array(
+			'labels'      => $labels,
+			'public'      => true,
+			'supports'    => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'action_id'    => 'new_foo',
+				'contexts'     => array( 'activity' ),
+				'position'     => 40,
+			)
+		) );
+
+		// Temporarily remove post type activity hook so activity item isn't created.
+		remove_action( 'transition_post_status', 'bp_activity_catch_transition_post_type_status', 10 );
+
+		// Create the initial post.
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$this->assertEmpty( bp_activity_get_activity_id( array( 'type' => 'new_foo' ) ) );
+
+		// Add the post type activity hook back.
+		add_action( 'transition_post_status', 'bp_activity_catch_transition_post_type_status', 10, 3 );
+
+		// Emulate updating a post; this should create an activity item.
+		wp_update_post( array(
+			'ID'     => $p,
+			'post_title' => 'This is an edit',
+		) );
+
+		// Assert!
+		$this->assertNotEmpty( bp_activity_get_activity_id( array( 'type' => 'new_foo' ) ), 'Activity item was not created during an edit of an existing WordPress post.' );
+
+		// Clean up.
+		_unregister_post_type( 'foo' );
+	}
+
+	/**
+	 * @group bp_activity_catch_transition_post_type_status
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_catch_transition_post_type_status_publish_password() {
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'foo',
+		) );
+
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id, 'new_foo' ), 'Published post type should have activity' );
+
+		$post->post_status   = 'publish';
+		$post->post_password = 'foo';
+
+		wp_update_post( $post );
+
+		// 'publish' => 'publish' (password protected)
+		$this->assertFalse( $this->activity_exists_for_post( $post_id, 'new_foo' ), 'Password protected post type should not have activity' );
+
+		_unregister_post_type( 'foo' );
+	}
+
+	/**
+	 * @group bp_activity_catch_transition_post_type_status
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_catch_transition_post_type_status_publish_trash() {
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'foo',
+		) );
+
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id, 'new_foo' ), 'Published post type should have activity' );
+
+		wp_trash_post( $post->ID );
+
+		// 'publish' => 'publish' (password protected)
+		$this->assertFalse( $this->activity_exists_for_post( $post_id, 'new_foo' ), 'Unpublished post type should not have activity' );
+
+		_unregister_post_type( 'foo' );
+	}
+
+	protected function activity_exists_for_post( $post_id, $action ) {
+		$a = bp_activity_get( array(
+			'action'            => $action,
+			'item_id'           => get_current_blog_id(),
+			'secondary_item_id' => $post_id,
+		) );
+
+		return ! empty( $a['activities'] );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/cache.php	(revision 12265)
@@ -0,0 +1,361 @@
+<?php
+
+/**
+ * @group activity
+ * @group cache
+ */
+class BP_Tests_Activity_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_activity_update_meta_cache
+	 */
+	public function test_bp_activity_update_meta_cache() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+
+		// Set up some data
+		bp_activity_update_meta( $a1, 'foo', 'bar' );
+		bp_activity_update_meta( $a1, 'Boone', 'Rules' );
+
+		bp_activity_update_meta( $a2, 'foo', 'baz' );
+		bp_activity_update_meta( $a2, 'BuddyPress', 'Is Cool' );
+
+		// Prime the cache for $a1
+		bp_activity_get_meta( $a1, 'foo' );
+
+		// Ensure an empty cache for $a2
+		wp_cache_delete( $a2, 'activity_meta' );
+
+		bp_activity_update_meta_cache( array( $a1, $a2 ) );
+
+		$expected = array(
+			$a1 => array(
+				'foo' => array(
+					'bar',
+				),
+				'Boone' => array(
+					'Rules',
+				),
+			),
+			$a2 => array(
+				'foo' => array(
+					'baz',
+				),
+				'BuddyPress' => array(
+					'Is Cool',
+				),
+			),
+		);
+
+		$found = array(
+			$a1 => wp_cache_get( $a1, 'activity_meta' ),
+			$a2 => wp_cache_get( $a2, 'activity_meta' ),
+		);
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	/**
+	 * @group bp_activity_clear_cache_for_activity
+	 */
+	public function test_bp_activity_clear_cache_for_activity() {
+		$u = self::factory()->user->create();
+
+		$a = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$a_fp = bp_activity_get( array(
+			'type' => 'activity_update',
+			'user' => array( 'filter' => array( 'user_id' => $u ) ),
+		) );
+
+		$activity_updated = new BP_Activity_Activity( $a );
+		$activity_updated->content = 'bar foo';
+		$activity_updated->save();
+
+		$a_fp = bp_activity_get( array(
+			'type' => 'activity_update',
+			'user' => array( 'filter' => array( 'user_id' => $u ) ),
+		) );
+
+		$this->assertSame( 'bar foo', $a_fp['activities'][0]->content );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_should_be_cached() {
+		global $wpdb;
+
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$activity_args = array(
+			'per_page' => 10,
+			'fields' => 'ids',
+			'count_total' => true,
+		);
+
+		$a1 = bp_activity_get( $activity_args );
+
+		$num_queries = $wpdb->num_queries;
+
+		$a2 = bp_activity_get( $activity_args );
+
+		$this->assertEqualSets( $a1, $a2 );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_skipped_for_different_query_params() {
+		global $wpdb;
+
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$activity_args = array(
+			'per_page' => 10,
+			'fields' => 'ids',
+			'count_total' => true,
+			'filter' => array(
+				'component' => buddypress()->activity->id,
+			),
+		);
+
+		$a1 = bp_activity_get( $activity_args );
+
+		$num_queries = $wpdb->num_queries;
+
+		// This is enough to make the ID and COUNT clause miss the cache.
+		$activity_args['filter']['action'] = 'activity_update';
+		$a2 = bp_activity_get( $activity_args );
+
+		$this->assertEqualSets( $a1, $a2 );
+
+		// Two extra queries: one for the IDs, one for the count.
+		$n = $num_queries + 2;
+		$this->assertSame( $num_queries + 2, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_invalidated_by_activity_add() {
+		global $wpdb;
+
+		$u = self::factory()->user->create();
+		$a1 = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$activity_args = array(
+			'per_page' => 10,
+			'fields' => 'ids',
+			'count_total' => true,
+		);
+
+		$q1 = bp_activity_get( $activity_args );
+
+		// Bust the cache.
+		$a2 = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$num_queries = $wpdb->num_queries;
+
+		$q2 = bp_activity_get( $activity_args );
+
+		$expected = array( $a1, $a2 );
+
+		$this->assertEqualSets( $expected, $q2['activities'] );
+		$this->assertEquals( 2, $q2['total'] );
+		$this->assertSame( $num_queries + 2, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_invalidated_by_activity_edit() {
+		global $wpdb;
+
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$activity_args = array(
+			'per_page' => 10,
+			'fields' => 'ids',
+			'count_total' => true,
+		);
+
+		$q1 = bp_activity_get( $activity_args );
+
+		// Bust the cache.
+		self::factory()->activity->create( array(
+			'id'            => $a,
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar baz',
+		) );
+
+		$num_queries = $wpdb->num_queries;
+
+		$q2 = bp_activity_get( $activity_args );
+
+		$this->assertEqualSets( $q1, $q2 );
+		$this->assertSame( $num_queries + 2, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_invalidated_by_activity_delete() {
+		global $wpdb;
+
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component'     => buddypress()->activity->id,
+			'type'          => 'activity_update',
+			'user_id'       => $u,
+			'content'       => 'foo bar',
+		) );
+
+		$activity_args = array(
+			'per_page' => 10,
+			'fields' => 'ids',
+			'count_total' => true,
+		);
+
+		$q1 = bp_activity_get( $activity_args );
+
+		// Bust the cache.
+		bp_activity_delete( array(
+			'id' => $a,
+		) );
+
+		$num_queries = $wpdb->num_queries;
+
+		$q2 = bp_activity_get( $activity_args );
+
+		$this->assertEqualSets( array(), $q2['activities'] );
+		$this->assertEquals( 0, $q2['total'] );
+		$this->assertSame( $num_queries + 2, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_invalidated_by_activitymeta_add() {
+		global $wpdb;
+
+		$activities = self::factory()->activity->create_many( 2 );
+		bp_activity_add_meta( $activities[0], 'foo', 'bar' );
+
+		$activity_args = array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		);
+
+		$q1 = bp_activity_get( $activity_args );
+		$this->assertEqualSets( array( $activities[0] ), wp_list_pluck( $q1['activities'], 'id' ) );
+
+		bp_activity_add_meta( $activities[1], 'foo', 'bar' );
+
+		$q2 = bp_activity_get( $activity_args );
+		$this->assertEqualSets( array( $activities[0], $activities[1] ), wp_list_pluck( $q2['activities'], 'id' ) );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_invalidated_by_activitymeta_update() {
+		global $wpdb;
+
+		$activities = self::factory()->activity->create_many( 2 );
+		bp_activity_add_meta( $activities[0], 'foo', 'bar' );
+		bp_activity_add_meta( $activities[1], 'foo', 'baz' );
+
+		$activity_args = array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		);
+
+		$q1 = bp_activity_get( $activity_args );
+		$this->assertEqualSets( array( $activities[0] ), wp_list_pluck( $q1['activities'], 'id' ) );
+
+		bp_activity_update_meta( $activities[1], 'foo', 'bar' );
+
+		$q2 = bp_activity_get( $activity_args );
+		$this->assertEqualSets( array( $activities[0], $activities[1] ), wp_list_pluck( $q2['activities'], 'id' ) );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 */
+	public function test_query_cache_should_be_invalidated_by_activitymeta_delete() {
+		global $wpdb;
+
+		$activities = self::factory()->activity->create_many( 2 );
+		bp_activity_add_meta( $activities[0], 'foo', 'bar' );
+		bp_activity_add_meta( $activities[1], 'foo', 'bar' );
+
+		$activity_args = array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		);
+
+		$q1 = bp_activity_get( $activity_args );
+		$this->assertEqualSets( array( $activities[0], $activities[1] ), wp_list_pluck( $q1['activities'], 'id' ) );
+
+		bp_activity_delete_meta( $activities[1], 'foo', 'bar' );
+
+		$q2 = bp_activity_get( $activity_args );
+		$this->assertEqualSets( array( $activities[0] ), wp_list_pluck( $q2['activities'], 'id' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/class.BP_Activity_Activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/class.BP_Activity_Activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/class.BP_Activity_Activity.php	(revision 12265)
@@ -0,0 +1,788 @@
+<?php
+/**
+ * @group activity
+ */
+class BP_Tests_Activity_Class extends BP_UnitTestCase {
+
+	/**
+	 * @group check_exists_by_content
+	 */
+	public function test_check_exists_by_content() {
+		$content  = 'A classy girl who know how to enjoy the freedom of a cup of coffee';
+		$activity = self::factory()->activity->create( array(
+			'content' => $content,
+			'type'    => 'activity_update',
+		) );
+
+		$result = BP_Activity_Activity::check_exists_by_content( $content );
+		$this->assertEquals( $activity, $result );
+	}
+
+	/**
+	 * @group delete_activity_item_comments
+	 */
+	public function test_delete_activity_item_comments() {
+		$parent_activity = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+		) );
+
+		$comments = self::factory()->activity->create_many( 3, array(
+			'item_id' => $parent_activity,
+			'type'    => 'activity_comment',
+		) );
+
+		BP_Activity_Activity::delete_activity_item_comments( $parent_activity );
+
+		$result = BP_Activity_Activity::get( array( 'in' => wp_list_pluck( $comments, 'id' ), ) );
+		$this->assertEmpty( $result['activities'] );
+	}
+
+	/**
+	 * @ticket BP4804
+	 * @group delete_activity_meta_entries
+	 * @group activitymeta
+	 */
+	public function test_delete_activity_meta_entries() {
+		$activity = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+		) );
+
+		bp_activity_update_meta( $activity, 'Paul', 'is cool' );
+		BP_Activity_Activity::delete_activity_meta_entries( $activity );
+
+		$meta = bp_activity_get_meta( $activity, 'Paul' );
+		$this->assertSame( '', $meta );
+	}
+
+	/**
+	 * @group get
+	 * @group fields
+	 * @ticket BP6426
+	 */
+	public function test_get_with_fields_parameter_by_id() {
+		$a = self::factory()->activity->create_many( 3, array(
+			'type' => 'activity_update',
+		) );
+
+		$result = BP_Activity_Activity::get( array(
+			'fields' => 'ids',
+		) );
+		$this->assertEqualSets( $a, $result['activities'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_hide_all_for_user() {
+		$activity = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+		) );
+
+		BP_Activity_Activity::hide_all_for_user( get_current_user_id() );
+
+		$activity = BP_Activity_Activity::get( array(
+			'in'          => $activity,
+			'show_hidden' => true,
+		) );
+		$this->assertEquals( $activity['activities'][0]->hide_sitewide, 1 );
+	}
+
+	/**
+	 * @group get
+	 * @group meta_query
+	 */
+	public function test_get_with_meta_query() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		bp_activity_update_meta( $a1, 'foo', 'bar' );
+
+		$activity = BP_Activity_Activity::get( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( $ids, array( $a1 ) );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_meta_query_two_clauses_with_or_relation() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 60 ),
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 120 ),
+		) );
+		bp_activity_update_meta( $a1, 'foo', 'bar' );
+		bp_activity_update_meta( $a2, 'foo', 'bar' );
+		bp_activity_update_meta( $a1, 'baz', 'barry' );
+
+		$activity = BP_Activity_Activity::get( array(
+			'meta_query' => array(
+				'relation' => 'OR',
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+				array(
+					'key' => 'baz',
+					'value' => 'barry',
+				),
+			),
+			'count_total' => 'count_query',
+		) );
+
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( array( $a1, $a2 ), $ids );
+		$this->assertEquals( 2, $activity['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @group date_query
+	 * @requires PHP 5.3
+	 */
+	public function test_get_with_date_query_before() {
+		if ( ! class_exists( 'WP_Date_Query' ) ) {
+			return;
+		}
+
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create( array(
+			'recorded_time' => '2001-01-01 12:00'
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'recorded_time' => '2005-01-01 12:00'
+		) );
+
+		// 'date_query' before test
+		$query = BP_Activity_Activity::get( array(
+			'date_query' => array( array(
+				'before' => array(
+					'year'  => 2004,
+					'month' => 1,
+					'day'   => 1,
+				),
+			) )
+		) );
+		$this->assertEquals( array( $a2 ), wp_list_pluck( $query['activities'], 'id' ) );
+	}
+
+	/**
+	 * @group get
+	 * @group date_query
+	 * @requires PHP 5.3
+	 */
+	public function test_get_with_date_query_range() {
+		if ( ! class_exists( 'WP_Date_Query' ) ) {
+			return;
+		}
+
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create( array(
+			'recorded_time' => '2001-01-01 12:00'
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'recorded_time' => '2005-01-01 12:00'
+		) );
+
+		// 'date_query' range test
+		$query = BP_Activity_Activity::get( array(
+			'date_query' => array( array(
+				'after'  => 'January 2nd, 2001',
+				'before' => array(
+					'year'  => 2013,
+					'month' => 1,
+					'day'   => 1,
+				),
+				'inclusive' => true,
+			) )
+		) );
+		$this->assertEquals( array( $a3 ), wp_list_pluck( $query['activities'], 'id' ) );
+	}
+
+	/**
+	 * @group get
+	 * @group date_query
+	 * @requires PHP 5.3
+	 */
+	public function test_get_with_date_query_after() {
+		if ( ! class_exists( 'WP_Date_Query' ) ) {
+			return;
+		}
+
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create( array(
+			'recorded_time' => '2001-01-01 12:00'
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'recorded_time' => '2005-01-01 12:00'
+		) );
+
+		// 'date_query' after and relative test
+		$query = BP_Activity_Activity::get( array(
+			'date_query' => array( array(
+				'after' => '1 day ago'
+			) )
+		) );
+		$this->assertEquals( array( $a1 ), wp_list_pluck( $query['activities'], 'id' ) );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_search_terms() {
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Boone is a cool guy',
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'No he isn\'t',
+		) );
+
+		$activity = BP_Activity_Activity::get( array(
+			'search_terms' => 'cool',
+		) );
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( $ids, array( $a1 ) );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_display_comments_threaded() {
+		$u = self::factory()->user->create();
+
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'user_id' => $u,
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'user_id' => $u,
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'user_id' => $u,
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		$activity = BP_Activity_Activity::get( array(
+			'display_comments' => 'threaded',
+		) );
+
+		// Kinda crummy, but let's construct a skeleton
+		$expected = array(
+			$a1 => array( $a3 ),
+			$a2 => array(),
+		);
+
+		$found = array();
+		foreach ( $activity['activities'] as $a ) {
+			$found[ $a->id ] = ! empty( $a->children ) ? array_keys( $a->children ) : array();
+		}
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_display_comments_stream() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// bp_activity_new_comment() doesn't allow date_recorded
+		$a3 = bp_activity_add( array(
+			'action'            => sprintf( __( '%s posted a new activity comment', 'buddypress' ), bp_get_loggedin_user_link() ) ,
+			'content'           => 'Candy is good',
+			'component'         => buddypress()->activity->id,
+			'type'              => 'activity_comment',
+			'user_id'           => bp_loggedin_user_id(),
+			'item_id'           => $a1,
+			'secondary_item_id' => $a1,
+			'recorded_time'     => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		$activity = BP_Activity_Activity::get( array(
+			'display_comments' => 'stream',
+		) );
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( array( $a1, $a3, $a2 ), $ids );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_display_comments_false() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		$activity = BP_Activity_Activity::get( array(
+			'display_comments' => false,
+		) );
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( array( $a1, $a2 ), $ids );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_offset() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 10 ),
+		) );
+
+		$activity = BP_Activity_Activity::get( array(
+			'filter' => array(
+				'offset' => $a2,
+			),
+		) );
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( array( $a3, $a2 ), $ids );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_since() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 10 ),
+		) );
+
+		$activity = BP_Activity_Activity::get( array(
+			'filter' => array(
+				'since' => date( 'Y-m-d H:i:s', $now - 70 ),
+			),
+		) );
+		$ids = wp_list_pluck( $activity['activities'], 'id' );
+		$this->assertEquals( array( $a3, $a2 ), $ids );
+	}
+
+	/**
+	 * @group get
+	 * @group count_total
+	 */
+	public function test_get_with_count_total() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+
+		$activity = BP_Activity_Activity::get( array(
+			'count_total' => 'count_query',
+		) );
+
+		$this->assertEquals( 2, $activity['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @group count_total
+	 */
+	public function test_get_with_count_total_false() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+
+		$activity = BP_Activity_Activity::get( array(
+			'count_total' => false,
+		) );
+
+		$this->assertSame( null, $activity['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @group count_total
+	 */
+	public function test_get_with_count_total_default_to_false() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+
+		$activity = BP_Activity_Activity::get();
+
+		$this->assertSame( null, $activity['total'] );
+	}
+
+	/**
+	 * @group get_id
+	 */
+	public function test_get_id_with_item_id() {
+		$a1 = self::factory()->activity->create( array(
+			'item_id' => 523,
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'item_id' => 1888,
+		) );
+
+		$activity = BP_Activity_Activity::get_id( false, false, false, 523, false, false, false, false );
+		$this->assertEquals( $a1, $activity );
+	}
+
+	/**
+	 * @group get_id
+	 */
+	public function test_get_id_with_secondary_item_id() {
+		$a1 = self::factory()->activity->create( array(
+			'secondary_item_id' => 523,
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'secondary_content' => 1888,
+		) );
+
+		$activity = BP_Activity_Activity::get_id( false, false, false, false, 523, false, false, false );
+		$this->assertEquals( $a1, $activity );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_with_item_id() {
+		$a1 = self::factory()->activity->create( array(
+			'item_id' => 523,
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'item_id' => 1888,
+		) );
+
+		$activity = BP_Activity_Activity::delete( array(
+			'item_id' => 523,
+		) );
+		$this->assertEquals( array( $a1 ), $activity );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_with_secondary_item_id() {
+		$a1 = self::factory()->activity->create( array(
+			'secondary_item_id' => 523,
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'secondary_item_id' => 1888,
+		) );
+
+		$activity = BP_Activity_Activity::delete( array(
+			'secondary_item_id' => 523,
+		) );
+		$this->assertEquals( array( $a1 ), $activity );
+	}
+
+	/**
+	 * @group get_activity_comments
+	 *
+	 * Verify the format of the activity comments array, for internal
+	 * refactoring
+	 */
+	public function test_get_activity_comments_format() {
+		$now = time();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+			'user_id' => $u1,
+		) );
+		$a2 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+			'user_id' => $u1,
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Bread is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 25 ),
+			'user_id' => $u2,
+		) );
+
+		$keys = array( 'id', 'item_id', 'secondary_item_id', 'user_id', 'primary_link', 'component', 'type', 'action', 'content', 'date_recorded', 'hide_sitewide', 'mptt_left', 'mptt_right', 'is_spam' );
+
+		$a2_obj = new BP_Activity_Activity( $a2 );
+
+		$e2 = new stdClass;
+
+		foreach ( $keys as $key ) {
+			$e2->{$key} = $a2_obj->{$key};
+		}
+
+		$e2_user = new WP_User( $a2_obj->user_id );
+
+		$e2->user_email = $e2_user->user_email;
+		$e2->user_nicename = $e2_user->user_nicename;
+		$e2->user_login = $e2_user->user_login;
+		$e2->display_name = $e2_user->display_name;
+		$e2->user_fullname = bp_core_get_user_displayname( $e2->user_id );
+		$e2->children = array();
+		$e2->depth = 1;
+
+		$a3_obj = new BP_Activity_Activity( $a3 );
+
+		$e3 = new stdClass;
+
+		foreach ( $keys as $key ) {
+			$e3->{$key} = $a3_obj->{$key};
+		}
+
+		$e3_user = new WP_User( $e3->user_id );
+
+		$e3->user_email = $e3_user->user_email;
+		$e3->user_nicename = $e3_user->user_nicename;
+		$e3->user_login = $e3_user->user_login;
+		$e3->display_name = $e3_user->display_name;
+		$e3->user_fullname = bp_core_get_user_displayname( $e3->user_id );
+		$e3->children = array();
+		$e3->depth = 1;
+
+		$expected = array(
+			$a2 => $e2,
+			$a3 => $e3,
+		);
+
+		$a1_obj = new BP_Activity_Activity( $a1 );
+		$comments = BP_Activity_Activity::get_activity_comments( $a1, $a1_obj->mptt_left, $a1_obj->mptt_right, 'ham_only', $a1 );
+
+		$this->assertEquals( $expected, $comments );
+	}
+
+	/**
+	 * @group get_last_updated
+	 */
+	public function test_get_last_updated() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 500 ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 300 ),
+		) );
+
+		$this->assertSame( date( 'Y-m-d H:i:s', $now - 100 ), BP_Activity_Activity::get_last_updated() );
+	}
+
+	/**
+	 * @group get_recorded_components
+	 */
+	public function test_get_recorded_components_skip_last_activity_false() {
+		$a1 = self::factory()->activity->create( array(
+			'component' => 'members',
+			'action' => 'last_activity',
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'component' => 'groups',
+			'action' => 'created_group',
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'component' => 'friends',
+			'action' => 'friendship_accepted',
+		) );
+
+		$found = BP_Activity_Activity::get_recorded_components( false );
+		sort( $found );
+
+		$this->assertSame( array( 'friends', 'groups', 'members' ), BP_Activity_Activity::get_recorded_components( false ) );
+	}
+
+	/**
+	 * @group get_recorded_components
+	 */
+	public function test_get_recorded_components_skip_last_activity_true_filter_empty_component() {
+		$a1 = self::factory()->activity->create( array(
+			'component' => 'members',
+			'action' => 'last_activity',
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'component' => 'groups',
+			'action' => 'created_group',
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'component' => 'friends',
+			'action' => 'friendship_accepted',
+		) );
+
+		$found = BP_Activity_Activity::get_recorded_components( true );
+		sort( $found );
+
+		$this->assertSame( array( 'friends', 'groups' ), BP_Activity_Activity::get_recorded_components() );
+	}
+
+	/**
+	 * @group get_recorded_components
+	 */
+	public function test_get_recorded_components_skip_last_activity_true_members_component_not_empty() {
+		$a1 = self::factory()->activity->create( array(
+			'component' => 'members',
+			'action' => 'last_activity',
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'component' => 'groups',
+			'action' => 'created_group',
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'component' => 'friends',
+			'action' => 'friendship_accepted',
+		) );
+		$a4 = self::factory()->activity->create( array(
+			'component' => 'members',
+			'action' => 'foo',
+		) );
+
+		$found = BP_Activity_Activity::get_recorded_components( true );
+		sort( $found );
+
+		$this->assertSame( array( 'friends', 'groups', 'members' ), BP_Activity_Activity::get_recorded_components() );
+	}
+
+	/**
+	 * @group get_recorded_components
+	 */
+	public function test_get_recorded_components_skip_last_activity_true_la_in_multiple_components() {
+		$a1 = self::factory()->activity->create( array(
+			'component' => 'members',
+			'action' => 'last_activity',
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'component' => 'groups',
+			'action' => 'created_group',
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'component' => 'friends',
+			'action' => 'friendship_accepted',
+		) );
+		$a4 = self::factory()->activity->create( array(
+			'component' => 'groups',
+			'action' => 'last_activity',
+		) );
+
+		$found = BP_Activity_Activity::get_recorded_components( true );
+		sort( $found );
+
+		$this->assertSame( array( 'friends', 'groups', ), BP_Activity_Activity::get_recorded_components() );
+	}
+	/**
+	 * @group activity_action
+	 */
+	public function test_instantiated_action_with_dynamic_callback() {
+		bp_activity_set_action(
+			'foo',
+			'bar',
+			'Foo Bar',
+			array( $this, 'action_cb' )
+		);
+
+		// Create the activity item with a manual activity string
+		$a = self::factory()->activity->create( array(
+			'component' => 'foo',
+			'type' => 'bar',
+			'action' => 'baz',
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( 'Woo Hoo!', $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 */
+	public function test_instantiated_action_without_dynamic_callback_but_with_stored_action() {
+		// No callback is registered - this mimics a legacy plugin
+
+		// Create the activity item with a manual activity string
+		$a = self::factory()->activity->create( array(
+			'component' => 'foo',
+			'type' => 'bar1',
+			'action' => 'baz',
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( 'baz', $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 */
+	public function test_instantiated_action_without_dynamic_callback_but_with_no_stored_action() {
+		// No callback is registered - this mimics a legacy plugin
+
+		// Create the activity item with a manual activity string
+		$a = self::factory()->activity->create( array(
+			'component' => 'foo',
+			'type' => 'bar2',
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( '', $a_obj->action );
+	}
+
+	/**
+	 * @ticket BP7394
+	 */
+	public function test_nonexistent_activity_should_have_id_0_after_population() {
+		$a = self::factory()->activity->create();
+		bp_activity_delete_by_activity_id( $a );
+
+		$a_obj = new BP_Activity_Activity( $a );
+		$this->assertSame( 0, $a_obj->id );
+	}
+
+	public function action_cb( $activity ) {
+		return 'Woo Hoo!';
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/filters.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/filters.php	(revision 12265)
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @group activity
+ */
+class BP_Tests_Activity_Filters extends BP_UnitTestCase {
+	/**
+	 * @group mentions
+	 * @group bp_activity_at_name_filter
+	 */
+	public function test_bp_activity_at_name_filter() {
+		$u1 = self::factory()->user->create( array(
+			'user_login' => 'foobarbaz',
+			'user_nicename' => 'foobarbaz',
+		) );
+
+		$u2 = self::factory()->user->create( array(
+			'user_login' => 'foo2',
+			'user_nicename' => 'foo2',
+		) );
+
+		$u1_mention_name = bp_activity_get_user_mentionname( $u1 );
+		$u1_domain = bp_core_get_user_domain( $u1 );
+		$u2_mention_name = bp_activity_get_user_mentionname( $u2 );
+		$u2_domain = bp_core_get_user_domain( $u2 );
+
+		// mentions normal text should be replaced
+		$at_name_in_text = sprintf( 'Hello @%s', $u1_mention_name );
+		$at_name_in_text_final = "Hello <a class='bp-suggestions-mention' href='" . $u1_domain . "' rel='nofollow'>@$u1_mention_name</a>";
+		$this->assertEquals( $at_name_in_text_final, bp_activity_at_name_filter( $at_name_in_text ) );
+
+		// mentions inside links sholudn't be replaced
+		// inside href
+		$at_name_in_mailto = sprintf( "Send messages to <a href='mail@%s.com'>Foo Bar Baz</a>", $u1_mention_name );
+		$at_name_in_mailto_final = sprintf( "Send messages to <a href='mail@%s.com'>Foo Bar Baz</a>", $u1_mention_name );
+		$this->assertEquals( $at_name_in_mailto_final, bp_activity_at_name_filter( $at_name_in_mailto ) );
+
+		// inside linked text
+		$at_name_in_link = sprintf( '<a href="https://twitter.com/%1$s">@%1$s</a>', $u1_mention_name );
+		$at_name_in_link_final = sprintf( '<a href="https://twitter.com/%1$s">@%1$s</a>', $u1_mention_name );
+		$this->assertEquals( $at_name_in_link_final, bp_activity_at_name_filter( $at_name_in_link ) );
+
+		// Don't link non-existent users
+		$text = "Don't link @non @existent @users";
+		$this->assertSame( $text, bp_activity_at_name_filter( $text ) );
+
+		// Don't link the domain name of the site
+		preg_match( '|https?://([^/]+)|', home_url(), $matches );
+		if ( ! empty( $matches[1] ) ) {
+			$text = $matches[1] . " Don't link the domain name " . $matches[1];
+		}
+		$this->assertSame( $text, bp_activity_at_name_filter( $text ) );
+
+		// Multiples
+		$at_name_in_mailto = sprintf( "Send messages to @%s <a href='mail@%s.com'>Foo Bar Baz</a>. Please CC <a href='http://twitter.com/foo2'>@foo2</a>.", $u1_mention_name, $u1_mention_name, $u2_mention_name, $u2_mention_name );
+		$at_name_in_mailto_final = sprintf( 'Send messages to <a class=\'bp-suggestions-mention\' href=\'%s\' rel=\'nofollow\'>@%s</a> <a href=\'mail@%s.com\'>Foo Bar Baz</a>. Please CC <a href=\'http://twitter.com/%s\'>@%s</a>.', $u1_domain, $u1_mention_name, $u1_mention_name, $u2_mention_name, $u2_mention_name );
+		$this->assertEquals( $at_name_in_mailto_final, bp_activity_at_name_filter( $at_name_in_mailto ) );
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions.php	(revision 12265)
@@ -0,0 +1,1761 @@
+<?php
+/**
+ * @group activity
+ */
+class BP_Tests_Activity_Functions extends BP_UnitTestCase {
+
+	/**
+	 * @ticket BP4488
+	 */
+	public function test_thumbnail_content_images() {
+		// No images
+		$post_content = 'foo bar';
+		$this->assertEquals( bp_activity_thumbnail_content_images( $post_content ), 'foo bar' );
+
+		// Image first, no caption. See #BP4488
+		$post_content = '<img src="http://example.com/foo.jpg" alt="foo" width="40" height="40" class="alignnone size-full wp-image-236" /> foo bar';
+		$this->assertEquals( bp_activity_thumbnail_content_images( $post_content ), '<img src="http://example.com/foo.jpg" width="40" height="40" alt="Thumbnail" class="align-left thumbnail" /> foo bar' );
+
+		// Image first, caption. See #BP4488
+		$post_content = '[caption id="attachment_236" align="alignnone" width="40"]<img src="http://example.com/foo.jpg" alt="FOO!" width="40" height="40" class="size-full wp-image-236" /> FOO![/caption] Awesome.';
+		$this->assertEquals( bp_activity_thumbnail_content_images( $post_content ), '<img src="http://example.com/foo.jpg" width="40" height="40" alt="Thumbnail" class="align-left thumbnail" /> Awesome.' );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_by_id() {
+
+		// create an activity update
+		$activity = self::factory()->activity->create( array(
+			'type' => 'activity_update'
+		) );
+
+		// now delete the activity item
+		bp_activity_delete( array(
+			'id' => $activity
+		) );
+
+		// now fetch the deleted activity entries
+		$get = bp_activity_get( array(
+			'id' => $activity
+		) );
+
+		// activities should equal zero
+		$this->assertEquals( 0, $get['total'] );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_by_type() {
+
+		// Setup criteria
+		$criteria = array(
+			'type' => 'activity_update'
+		);
+
+		// create an activity update
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+
+		// now delete the activity items
+		bp_activity_delete( $criteria );
+
+		// now fetch the deleted activity entries
+		$get = bp_activity_get( $criteria );
+
+		// activities should equal zero
+		$this->assertEquals( 0, $get['total'] );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_by_component() {
+
+		// Setup criteria
+		$criteria = array(
+			'component' => 'xprofile'
+		);
+
+		// create an activity update
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+
+		// now delete the activity items
+		bp_activity_delete( $criteria );
+
+		// now fetch the deleted activity entries
+		$get = bp_activity_get( $criteria );
+
+		// activities should equal zero
+		$this->assertEquals( 0, $get['total'] );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_by_user_id() {
+
+		// Setup criteria
+		$criteria = array(
+			'user_id' => '1'
+		);
+
+		// create an activity update
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+		self::factory()->activity->create( $criteria );
+
+		// now delete the activity items
+		bp_activity_delete( $criteria );
+
+		// now fetch the deleted activity entries
+		$get = bp_activity_get( $criteria );
+
+		// activities should equal zero
+		$this->assertEquals( 0, $get['total'] );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_meta() {
+
+		// create an activity update
+		$activity = self::factory()->activity->create( array(
+			'type' => 'activity_update'
+		) );
+
+		// add some meta to the activity items
+		bp_activity_update_meta( $activity, 'foo', 'bar' );
+
+		// now delete the parent activity item meta entry
+		bp_activity_delete_meta(  $activity, 'foo', 'bar' );
+
+		// now fetch activity meta for the deleted activity entries
+		$m1 = bp_activity_get_meta( $activity );
+
+		// test if activity meta entries still exist
+		$this->assertEmpty( $m1 );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_all_meta() {
+
+		// create an activity update
+		$activity = self::factory()->activity->create( array(
+			'type' => 'activity_update'
+		) );
+
+		// add some meta to the activity items
+		bp_activity_update_meta( $activity, 'foo1', 'bar' );
+		bp_activity_update_meta( $activity, 'foo2', 'bar' );
+		bp_activity_update_meta( $activity, 'foo3', 'bar' );
+		bp_activity_update_meta( $activity, 'foo4', 'bar' );
+		bp_activity_update_meta( $activity, 'foo5', 'bar' );
+
+		// now delete the parent activity item meta entry
+		bp_activity_delete_meta( $activity );
+
+		// now fetch activity meta for the deleted activity entries
+		$m1 = bp_activity_get_meta( $activity );
+		$m2 = bp_activity_get_meta( $activity );
+		$m3 = bp_activity_get_meta( $activity );
+		$m4 = bp_activity_get_meta( $activity );
+		$m5 = bp_activity_get_meta( $activity );
+
+		// test if activity meta entries still exist
+		$this->assertEmpty( $m1 );
+		$this->assertEmpty( $m2 );
+		$this->assertEmpty( $m3 );
+		$this->assertEmpty( $m4 );
+		$this->assertEmpty( $m5 );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_and_comments() {
+
+		// create an activity update
+		$parent_activity = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+		) );
+
+		// create some activity comments
+		$comment_one = self::factory()->activity->create( array(
+			'type'              => 'activity_comment',
+			'item_id'           => $parent_activity,
+			'secondary_item_id' => $parent_activity,
+		) );
+
+		$comment_two = self::factory()->activity->create( array(
+			'type'              => 'activity_comment',
+			'item_id'           => $parent_activity,
+			'secondary_item_id' => $parent_activity,
+		) );
+
+		// now delete the parent activity item
+		// this should hopefully delete the associated comments and meta entries
+		bp_activity_delete( array(
+			'id' => $parent_activity
+		) );
+
+		// now fetch the deleted activity entries
+		$get = bp_activity_get( array(
+			'in'               => array( $parent_activity, $comment_one, $comment_two ),
+			'display_comments' => 'stream'
+		) );
+
+		// activities should equal zero
+		$this->assertEquals( 0, $get['total'] );
+	}
+
+	/**
+	 * @group delete
+	 */
+	public function test_delete_activity_meta_for_comments() {
+
+		// create an activity update
+		$parent_activity = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+		) );
+
+		// create some activity comments
+		$comment_one = self::factory()->activity->create( array(
+			'type'              => 'activity_comment',
+			'item_id'           => $parent_activity,
+			'secondary_item_id' => $parent_activity,
+		) );
+
+		$comment_two = self::factory()->activity->create( array(
+			'type'              => 'activity_comment',
+			'item_id'           => $parent_activity,
+			'secondary_item_id' => $parent_activity,
+		) );
+
+		// add some meta to the activity items
+		bp_activity_update_meta( $parent_activity, 'foo', 'bar' );
+		bp_activity_update_meta( $comment_one,     'foo', 'bar' );
+		bp_activity_update_meta( $comment_two,     'foo', 'bar' );
+
+		// now delete the parent activity item
+		// this should hopefully delete the associated comments and meta entries
+		bp_activity_delete( array(
+			'id' => $parent_activity
+		) );
+
+		// now fetch activity meta for the deleted activity entries
+		$m1 = bp_activity_get_meta( $parent_activity );
+		$m2 = bp_activity_get_meta( $comment_one );
+		$m3 = bp_activity_get_meta( $comment_two );
+
+		// test if activity meta entries still exist
+		$this->assertEmpty( $m1 );
+		$this->assertEmpty( $m2 );
+		$this->assertEmpty( $m3 );
+	}
+
+	/**
+	 * @group bp_activity_update_meta
+	 * @ticket BP5180
+	 */
+	public function test_bp_activity_update_meta_with_line_breaks() {
+		$a = self::factory()->activity->create();
+		$meta_value = 'Foo!
+
+
+Bar!';
+		bp_activity_update_meta( $a, 'linebreak_test', $meta_value );
+		$this->assertEquals( $meta_value, bp_activity_get_meta( $a, 'linebreak_test' ) );
+	}
+
+	/**
+	 * @group bp_activity_update_meta
+	 * @ticket BP5083
+	 */
+	public function test_bp_activity_update_meta_with_0() {
+		$a = self::factory()->activity->create();
+		$meta_value = 0;
+
+		bp_activity_update_meta( $a, '0_test', $meta_value );
+
+		$this->assertNotSame( false, bp_activity_get_meta( $a, '0_test' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_non_numeric_id() {
+		$this->assertFalse( bp_activity_update_meta( 'foo', 'bar', 'baz' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 * @ticket BP5399
+	 */
+	public function test_bp_activity_update_meta_with_illegal_key_characters() {
+		$a = self::factory()->activity->create();
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		bp_activity_update_meta( $a, $krazy_key, 'bar' );
+
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_stripslashes() {
+		$a = self::factory()->activity->create();
+		$value = "This string is totally slashin\'!";
+		bp_activity_update_meta( $a, 'foo', $value );
+
+		$this->assertSame( stripslashes( $value ), bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_false_value_deletes() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', false );
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_new() {
+		$a = self::factory()->activity->create();
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ), '"foo" meta should be empty for this activity item.' );
+		$this->assertNotEmpty( bp_activity_update_meta( $a, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_existing() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo' ) );
+		$this->assertTrue( bp_activity_update_meta( $a, 'foo', 'baz' ) );
+		$this->assertSame( 'baz', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_same_value() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo' ) );
+		$this->assertFalse( bp_activity_update_meta( $a, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_update_meta
+	 */
+	public function test_bp_activity_update_meta_prev_value() {
+		$a = self::factory()->activity->create();
+		bp_activity_add_meta( $a, 'foo', 'bar' );
+
+		// In earlier versions of WordPress, bp_activity_update_meta()
+		// returns true even on failure. However, we know that in these
+		// cases the update is failing as expected, so we skip this
+		// assertion just to keep our tests passing
+		// See https://core.trac.wordpress.org/ticket/24933
+		if ( version_compare( $GLOBALS['wp_version'], '3.7', '>=' ) ) {
+			$this->assertFalse( bp_activity_update_meta( $a, 'foo', 'bar2', 'baz' ) );
+		}
+
+		$this->assertTrue( bp_activity_update_meta( $a, 'foo', 'bar2', 'bar' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 */
+	public function test_bp_activity_get_meta_empty_activity_id() {
+		$this->assertFalse( bp_activity_get_meta( 0 ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 */
+	public function test_bp_activity_get_meta_non_numeric_activity_id() {
+		$this->assertFalse( bp_activity_get_meta( 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 * @ticket BP5399
+	 */
+	public function test_bp_activity_get_meta_with_illegal_characters() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		$this->assertSame( '', bp_activity_get_meta( $a, $krazy_key ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 */
+	public function test_bp_activity_get_meta_multiple() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		bp_activity_update_meta( $a, 'foo1', 'bar1' );
+
+		$expected = array(
+			'foo' => array(
+				'bar',
+			),
+			'foo1' => array(
+				'bar1',
+			),
+		);
+
+		$this->assertEquals( $expected, bp_activity_get_meta( $a ) );
+	}
+
+	/**
+	 * @group bp_activity_get_meta
+	 * @group activitymeta
+	 * @ticket BP5399
+	 */
+	public function test_bp_activity_get_meta_no_results_returns_false() {
+		$a = self::factory()->activity->create();
+
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 */
+	public function test_bp_activity_get_meta_single_true() {
+		$a = self::factory()->activity->create();
+		bp_activity_add_meta( $a, 'foo', 'bar' );
+		bp_activity_add_meta( $a, 'foo', 'baz' );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo' ) ); // default is true
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo', true ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 */
+	public function test_bp_activity_get_meta_single_false() {
+		$a = self::factory()->activity->create();
+		bp_activity_add_meta( $a, 'foo', 'bar' );
+		bp_activity_add_meta( $a, 'foo', 'baz' );
+		$this->assertSame( array( 'bar', 'baz' ), bp_activity_get_meta( $a, 'foo', false ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_get_meta
+	 * @group cache
+	 */
+	public function test_bp_activity_get_meta_cache_all_on_get() {
+		$a = self::factory()->activity->create();
+		bp_activity_add_meta( $a, 'foo', 'bar' );
+		bp_activity_add_meta( $a, 'foo1', 'baz' );
+		$this->assertFalse( wp_cache_get( $a, 'activity_meta' ) );
+
+		// A single query should prime the whole meta cache
+		bp_activity_get_meta( $a, 'foo' );
+
+		$c = wp_cache_get( $a, 'activity_meta' );
+		$this->assertNotEmpty( $c['foo1'] );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 */
+	public function test_bp_activity_delete_meta_non_numeric_activity_id() {
+		$this->assertFalse( bp_activity_delete_meta( 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 * @ticket BP5399
+	 */
+	public function test_bp_activity_delete_meta_trim_meta_value() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		bp_activity_delete_meta( $a, 'foo', ' bar ' );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 */
+	public function test_bp_activity_delete_meta_single() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		$this->assertTrue( bp_activity_delete_meta( $a, 'foo', 'bar' ) );
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 */
+	public function test_bp_activity_delete_meta_all_for_activity() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		bp_activity_update_meta( $a, 'foo1', 'bar1' );
+		$this->assertTrue( bp_activity_delete_meta( $a ) );
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ) );
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo1' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 */
+	public function test_bp_activity_delete_meta_with_meta_value() {
+		$a = self::factory()->activity->create();
+		bp_activity_update_meta( $a, 'foo', 'bar' );
+		$this->assertTrue( bp_activity_delete_meta( $a, 'foo', 'bar' ) );
+		$this->assertSame( '', bp_activity_get_meta( $a, 'foo' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 */
+	public function test_bp_activity_delete_meta_with_delete_all_but_no_meta_key() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		bp_activity_update_meta( $a1, 'foo', 'bar' );
+		bp_activity_update_meta( $a1, 'foo1', 'bar1' );
+		bp_activity_update_meta( $a2, 'foo', 'bar' );
+		bp_activity_update_meta( $a2, 'foo1', 'bar1' );
+
+		$this->assertTrue( bp_activity_delete_meta( $a1, '', '', true ) );
+		$this->assertEmpty( bp_activity_get_meta( $a1 ) );
+		$this->assertSame( 'bar', bp_activity_get_meta( $a2, 'foo' ) );
+		$this->assertSame( 'bar1', bp_activity_get_meta( $a2, 'foo1' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_delete_meta
+	 */
+	public function test_bp_activity_delete_meta_with_delete_all() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		bp_activity_update_meta( $a1, 'foo', 'bar' );
+		bp_activity_update_meta( $a1, 'foo1', 'bar1' );
+		bp_activity_update_meta( $a2, 'foo', 'bar' );
+		bp_activity_update_meta( $a2, 'foo1', 'bar1' );
+
+		$this->assertTrue( bp_activity_delete_meta( $a1, 'foo', '', true ) );
+		$this->assertSame( '', bp_activity_get_meta( $a1, 'foo' ) );
+		$this->assertSame( '', bp_activity_get_meta( $a2, 'foo' ) );
+		$this->assertSame( 'bar1', bp_activity_get_meta( $a1, 'foo1' ) );
+		$this->assertSame( 'bar1', bp_activity_get_meta( $a2, 'foo1' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_add_meta
+	 */
+	public function test_bp_activity_add_meta_no_meta_key() {
+		$this->assertFalse( bp_activity_add_meta( 1, '', 'bar' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_add_meta
+	 */
+	public function test_bp_activity_add_meta_empty_object_id() {
+		$this->assertFalse( bp_activity_add_meta( 0, 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_add_meta
+	 */
+	public function test_bp_activity_add_meta_existing_unique() {
+		$a = self::factory()->activity->create();
+		bp_activity_add_meta( $a, 'foo', 'bar' );
+		$this->assertFalse( bp_activity_add_meta( $a, 'foo', 'baz', true ) );
+	}
+
+	/**
+	 * @group activitymeta
+	 * @group bp_activity_add_meta
+	 */
+	public function test_bp_activity_add_meta_existing_not_unique() {
+		$a = self::factory()->activity->create();
+		bp_activity_add_meta( $a, 'foo', 'bar' );
+		$this->assertNotEmpty( bp_activity_add_meta( $a, 'foo', 'baz' ) );
+	}
+
+	/**
+	 * @group bp_activity_get_user_mentionname
+	 */
+	public function test_bp_activity_get_user_mentionname_compatibilitymode_off() {
+		add_filter( 'bp_is_username_compatibility_mode', '__return_false' );
+
+		$u = self::factory()->user->create( array(
+			'user_login' => 'foo bar baz',
+			'user_nicename' => 'foo-bar-baz',
+		) );
+
+		$this->assertEquals( 'foo-bar-baz', bp_activity_get_user_mentionname( $u ) );
+
+		remove_filter( 'bp_is_username_compatibility_mode', '__return_false' );
+	}
+
+	/**
+	 * @group bp_activity_get_user_mentionname
+	 */
+	public function test_bp_activity_get_user_mentionname_compatibilitymode_on() {
+		add_filter( 'bp_is_username_compatibility_mode', '__return_true' );
+
+		$u1 = self::factory()->user->create( array(
+			'user_login' => 'foo bar baz',
+			'user_nicename' => 'foo-bar-baz',
+		) );
+
+		$u2 = self::factory()->user->create( array(
+			'user_login' => 'foo.bar.baz',
+			'user_nicename' => 'foo-bar-baz',
+		) );
+
+		$this->assertEquals( 'foo-bar-baz', bp_activity_get_user_mentionname( $u1 ) );
+		$this->assertEquals( 'foo.bar.baz', bp_activity_get_user_mentionname( $u2 ) );
+
+		remove_filter( 'bp_is_username_compatibility_mode', '__return_true' );
+	}
+
+	/**
+	 * @group bp_activity_get_userid_from_mentionname
+	 */
+	public function test_bp_activity_get_userid_from_mentionname_compatibilitymode_off() {
+		add_filter( 'bp_is_username_compatibility_mode', '__return_false' );
+
+		$u = self::factory()->user->create( array(
+			'user_login' => 'foo bar baz',
+			'user_nicename' => 'foo-bar-baz',
+		) );
+
+		$this->assertEquals( $u, bp_activity_get_userid_from_mentionname( 'foo-bar-baz' ) );
+
+		remove_filter( 'bp_is_username_compatibility_mode', '__return_false' );
+	}
+
+	/**
+	 * @group bp_activity_get_userid_from_mentionname
+	 */
+	public function test_bp_activity_get_userid_from_mentionname_compatibilitymode_on() {
+		add_filter( 'bp_is_username_compatibility_mode', '__return_true' );
+
+		// all spaces are hyphens
+		$u1 = self::factory()->user->create( array(
+			'user_login' => 'foo bar baz',
+			'user_nicename' => 'foobarbaz',
+		) );
+
+		// no spaces are hyphens
+		$u2 = self::factory()->user->create( array(
+			'user_login' => 'foo-bar-baz-1',
+			'user_nicename' => 'foobarbaz-1',
+		) );
+
+		// some spaces are hyphens
+		$u3 = self::factory()->user->create( array(
+			'user_login' => 'foo bar-baz 2',
+			'user_nicename' => 'foobarbaz-2',
+		) );
+
+		$u4 = self::factory()->user->create( array(
+			'user_login' => 'foo.bar.baz',
+			'user_nicename' => 'foo-bar-baz',
+		) );
+
+		$this->assertEquals( $u1, bp_activity_get_userid_from_mentionname( 'foo-bar-baz' ) );
+		$this->assertEquals( $u2, bp_activity_get_userid_from_mentionname( 'foo-bar-baz-1' ) );
+		$this->assertEquals( $u3, bp_activity_get_userid_from_mentionname( 'foo-bar-baz-2' ) );
+		$this->assertEquals( $u4, bp_activity_get_userid_from_mentionname( 'foo.bar.baz' ) );
+
+		remove_filter( 'bp_is_username_compatibility_mode', '__return_true' );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_activity_update
+	 */
+	public function test_bp_activity_format_activity_action_activity_update() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_update',
+			'user_id' => $u,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$expected = sprintf( '%s posted an update', bp_core_get_userlink( $u ) );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_activity_comment
+	 */
+	public function test_bp_activity_format_activity_action_activity_comment() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_comment',
+			'user_id' => $u,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$expected = sprintf( '%s posted a new activity comment', bp_core_get_userlink( $u ) );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_custom_post_type_post
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_format_activity_action_custom_post_type_post_nonms() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		// Build the actions to fetch the tracking args
+		bp_activity_get_actions();
+
+		$u = self::factory()->user->create();
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$a = self::factory()->activity->create( array(
+			'component'         => 'activity',
+			'type'              => 'new_foo',
+			'user_id'           => $u,
+			'item_id'           => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_home_url();
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_link = '<a href="' . $post_url . '">item</a>';
+
+		$expected = sprintf( '%s wrote a new %s', $user_link, $post_link );
+
+		$this->assertSame( $expected, $a_obj->action );
+
+		_unregister_post_type( 'foo' );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_custom_post_type_post_ms
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_format_activity_action_custom_post_type_post_ms() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		switch_to_blog( $b );
+
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		// Build the actions to fetch the tracking args
+		bp_activity_get_actions();
+
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$activity_args = array(
+			'component'         => 'activity',
+			'type'              => 'new_foo',
+			'user_id'           => $u,
+			'item_id'           => $b,
+			'secondary_item_id' => $p,
+		);
+
+		_unregister_post_type( 'foo' );
+		bp_activity_get_actions();
+
+		restore_current_blog();
+
+		$a = self::factory()->activity->create( $activity_args );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_blog_option( $a_obj->item_id, 'home' );
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+
+		$post_link = '<a href="' . $post_url . '">item</a>';
+
+		$expected = sprintf( '%s wrote a new %s, on the site %s', $user_link, $post_link, '<a href="' . $blog_url . '">' . get_blog_option( $a_obj->item_id, 'blogname' ) . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_custom_post_type_post
+	 */
+	public function test_bp_activity_format_activity_action_custom_string_post_type_post_nonms() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$labels = array(
+			'name'                 => 'bars',
+			'singular_name'        => 'bar',
+			'bp_activity_new_post' => '%1$s shared a new <a href="%2$s">bar</a>',
+		);
+
+		register_post_type( 'foo', array(
+			'labels'      => $labels,
+			'public'      => true,
+			'supports'    => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'action_id' => 'foo_bar',
+			),
+		) );
+
+		// Build the actions to fetch the tracking args
+		bp_activity_get_actions();
+
+		$u = self::factory()->user->create();
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$a = self::factory()->activity->create( array(
+			'component'         => 'activity',
+			'type'              => 'foo_bar',
+			'user_id'           => $u,
+			'item_id'           => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_home_url();
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+
+		$expected = sprintf( '%1$s shared a new <a href="%2$s">bar</a>', $user_link, $post_url );
+
+		$this->assertSame( $expected, $a_obj->action );
+
+		_unregister_post_type( 'foo' );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_custom_post_type_post_ms
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_format_activity_action_custom_string_post_type_post_ms() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		switch_to_blog( $b );
+
+		$labels = array(
+			'name'                    => 'bars',
+			'singular_name'           => 'bar',
+			'bp_activity_new_post_ms' => '%1$s shared a new <a href="%2$s">bar</a>, on the site %3$s',
+		);
+
+		register_post_type( 'foo', array(
+			'labels'   => $labels,
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		// Build the actions to fetch the tracking args
+		bp_activity_get_actions();
+
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$activity_args = array(
+			'component'         => 'activity',
+			'type'              => 'new_foo',
+			'user_id'           => $u,
+			'item_id'           => $b,
+			'secondary_item_id' => $p,
+		);
+
+		_unregister_post_type( 'foo' );
+
+		restore_current_blog();
+
+		$a = self::factory()->activity->create( $activity_args );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_blog_option( $a_obj->item_id, 'home' );
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+
+		$expected = sprintf( '%1$s shared a new <a href="%2$s">bar</a>, on the site %3$s', $user_link, $post_url, '<a href="' . $blog_url . '">' . get_blog_option( $a_obj->item_id, 'blogname' ) . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group bp_activity_set_post_type_tracking_args
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_set_post_type_tracking_args() {
+		$bp = buddypress();
+
+		add_post_type_support( 'page', 'buddypress-activity' );
+
+		bp_activity_set_post_type_tracking_args( 'page', array(
+			'component_id' => $bp->blogs->id,
+			'dummy'        => 'dummy value',
+		) );
+
+		// Build the actions to fetch the tracking args
+		bp_activity_get_actions();
+
+		$u = self::factory()->user->create();
+
+		$post_id = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_status' => 'publish',
+			'post_type'   => 'page',
+		) );
+
+		$a = bp_activity_get( array(
+			'action'            => 'new_page',
+			'item_id'           => get_current_blog_id(),
+			'secondary_item_id' => $post_id,
+		) );
+
+		$this->assertSame( $bp->blogs->id, $a['activities'][0]->component );
+
+		remove_post_type_support( 'page', 'buddypress-activity' );
+	}
+
+	/**
+	 * @group bp_activity_set_post_type_tracking_args
+	 * @group activity_tracking
+	 */
+	public function test_bp_activity_set_post_type_tracking_args_check_post_type_global() {
+		$labels = array(
+			'bp_activity_admin_filter' => 'New Foo',
+			'bp_activity_front_filter' => 'Foos',
+		);
+
+		$bp_activity_args = array(
+			'action_id'    => 'new_foo',
+			'contexts'     => array( 'activity' ),
+			'position'     => 40,
+		);
+
+		register_post_type( 'foo', array(
+			'labels'      => $labels,
+			'supports'    => array( 'buddypress-activity' ),
+			'bp_activity' => $bp_activity_args
+		) );
+
+		$register_bp_activity = get_post_type_object( 'foo' )->bp_activity;
+		_unregister_post_type( 'foo' );
+
+		register_post_type( 'foo', array(
+			'label'       => 'foo',
+			'supports'    => array( 'buddypress-activity' ),
+		) );
+
+		bp_activity_set_post_type_tracking_args( 'foo', $labels + $bp_activity_args );
+
+		$set_bp_activity = get_post_type_object( 'foo' )->bp_activity;
+		_unregister_post_type( 'foo' );
+
+		$this->assertSame( $set_bp_activity, $register_bp_activity );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_activity_format_activity_action_custom_post_type_post_ms
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_activity_format_activity_action_custom_post_type_comment() {
+		if ( is_multisite() ) {
+			if ( function_exists( 'wp_initialize_site' ) ) {
+				$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			}
+
+			$b = self::factory()->blog->create();
+			switch_to_blog( $b );
+			add_filter( 'comment_flood_filter', '__return_false' );
+		} else {
+			$b = get_current_blog_id();
+		}
+
+		$u = self::factory()->user->create();
+		$userdata = get_userdata( $u );
+
+		$labels = array(
+			'name'                       => 'bars',
+			'singular_name'              => 'bar',
+			'bp_activity_new_comment'    => __( '%1$s commented on the <a href="%2$s">bar</a>', 'buddypress' ),
+			'bp_activity_new_comment_ms' => __( '%1$s commented on the <a href="%2$s">bar</a>, on the site %3$s', 'buddypress' ),
+		);
+
+		register_post_type( 'foo', array(
+			'labels'   => $labels,
+			'public'   => true,
+			'supports' => array( 'buddypress-activity', 'comments' ),
+			'bp_activity' => array(
+				'action_id'         => 'new_bar',
+				'comment_action_id' => 'new_bar_comment',
+			),
+		) );
+
+		// Build the actions to fetch the tracking args
+		bp_activity_get_actions();
+
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$c = wp_new_comment( array(
+			'comment_post_ID'      => $p,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is a blog comment',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u,
+		) );
+
+		$a = bp_activity_get_activity_id( array( 'type' => 'new_bar_comment' ) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link    = bp_core_get_userlink( $u );
+		$comment_url  = get_comment_link( $c );
+
+		_unregister_post_type( 'foo' );
+
+		if ( is_multisite() ) {
+			$blog_url  = get_blog_option( $a_obj->item_id, 'home' );
+			restore_current_blog();
+			remove_filter( 'comment_flood_filter', '__return_false' );
+
+			$expected = sprintf( $labels['bp_activity_new_comment_ms'], $user_link, $comment_url, '<a href="' . $blog_url . '">' . get_blog_option( $a_obj->item_id, 'blogname' ) . '</a>' );
+		} else {
+			$expected = sprintf( $labels['bp_activity_new_comment'], $user_link, $comment_url );
+		}
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group bp_activity_new_comment
+	 * @group cache
+	 */
+	public function test_bp_activity_new_comment_clear_comment_caches() {
+		$u = self::factory()->user->create();
+		$a1 = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+		$a2 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a1,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a2,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+		$a4 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a3,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+		$a5 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a3,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+
+		// prime caches
+		bp_activity_get( array(
+			'in' => array( $a1 ),
+			'display_comments' => 'threaded',
+		) );
+
+		// should be populated
+		$this->assertNotEmpty( wp_cache_get( $a1, 'bp_activity_comments' ) );
+
+		bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a4,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+
+		// should be empty
+		$this->assertFalse( wp_cache_get( $a1, 'bp_activity_comments' ) );
+	}
+
+	/**
+	 * @group bp_activity_new_comment
+	 * @group cache
+	 */
+	public function test_bp_activity_new_comment_clear_activity_caches() {
+		$u = self::factory()->user->create();
+		$a1 = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+		$a2 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a1,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a2,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+		$a4 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a3,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+		$a5 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a3,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+
+		// prime caches
+		bp_activity_get( array(
+			'in' => array( $a1 ),
+			'display_comments' => 'threaded',
+		) );
+
+		// should be populated
+		$this->assertNotEmpty( wp_cache_get( $a1, 'bp_activity' ) );
+		$this->assertNotEmpty( wp_cache_get( $a2, 'bp_activity' ) );
+		$this->assertNotEmpty( wp_cache_get( $a3, 'bp_activity' ) );
+		$this->assertNotEmpty( wp_cache_get( $a4, 'bp_activity' ) );
+		$this->assertNotEmpty( wp_cache_get( $a5, 'bp_activity' ) );
+
+		// Stuff may run on bp_activity_comment_posted that loads the
+		// cache, so we use this dumb technique to check cache values
+		// before any of that stuff gets a chance to run. WordPress
+		// sure is neat sometimes
+		$this->acaches = array(
+			$a1 => '',
+			$a2 => '',
+			$a3 => '',
+			$a4 => '',
+		);
+		add_action( 'bp_activity_comment_posted', array( $this, 'check_activity_caches' ), 0 );
+
+		bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a4,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+
+		// should be empty
+		foreach ( $this->acaches as $k => $v ) {
+			$this->assertFalse( $v, "Cache should be false for $k" );
+		}
+	}
+
+	/**
+	 * @group bp_activity_delete_comment
+	 * @group cache
+	 */
+	public function test_bp_activity_delete_comment_clear_cache() {
+		$u = self::factory()->user->create();
+		// add new activity update and comment to this update
+		$a1 = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+		$a2 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'parent_id' => $a1,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+
+		// prime cache
+		bp_activity_get( array(
+			'in' => array( $a1 ),
+			'display_comments' => 'threaded',
+		) );
+
+		// delete activity comment
+		bp_activity_delete_comment( $a1, $a2 );
+
+		// assert comment cache as empty for $a1
+		$this->assertEmpty( wp_cache_get( $a1, 'bp_activity_comments' ) );
+	}
+
+	/**
+	 * @group  bp_activity_delete_comment
+	 * @ticket BP7450
+	 */
+	public function test_bp_activity_delete_comment_shouldnt_delete_all_comments_when_parameters_are_empty() {
+		$u = self::factory()->user->create();
+
+		// create an activity update
+		$parent_activity = self::factory()->activity->create( array(
+			'type'    => 'activity_update',
+			'user_id' => $u
+		) );
+
+		// create some activity comments
+		$comment_one = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'content'     => 'depth 1'
+		) );
+
+		$comment_one_one = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'parent_id'   => $comment_one,
+			'content'     => 'depth 2'
+		) );
+
+		$comment_two = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'content'     => 'depth 1'
+		) );
+
+		// Pass empty values to bp_activity_delete_comment()
+		$retval = bp_activity_delete_comment( 0, 0 );
+		$this->assertFalse( $retval );
+
+		// Instantiate activity loop, which also includes activity comments.
+		bp_has_activities( 'display_comments=stream' );
+
+		// Activity comments should not be deleted.
+		$this->assertSame( 4, $GLOBALS['activities_template']->activity_count );
+
+		// Clean up after ourselves!
+		$GLOBALS['activities_template'] = null;
+	}
+
+	/**
+	 * @group bp_activity_new_comment
+	 * @group BP5907
+	 */
+	public function test_bp_activity_comment_on_deleted_activity() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create();
+
+		bp_activity_delete_by_activity_id( $a );
+
+		$c = bp_activity_new_comment( array(
+			'activity_id' => $a,
+			'parent_id' => $a,
+			'content' => 'foo',
+			'user_id' => $u,
+		) );
+
+		$this->assertEmpty( $c );
+	}
+
+	/**
+	 * @group favorites
+	 * @group bp_activity_add_user_favorite
+	 */
+	public function test_add_user_favorite_already_favorited() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create();
+
+		// bp_activity_add_user_favorite() requires a logged-in user.
+		$current_user = bp_loggedin_user_id();
+		$this->set_current_user( $u );
+
+		$this->assertTrue( bp_activity_add_user_favorite( $a, $u ) );
+
+		$this->assertFalse( bp_activity_add_user_favorite( $a, $u ) );
+		$this->assertSame( array( $a ), bp_activity_get_user_favorites( $u ) );
+		$this->assertEquals( 1, bp_activity_get_meta( $a, 'favorite_count' ) );
+
+		$this->set_current_user( $current_user );
+	}
+
+	/**
+	 * @group favorites
+	 * @group bp_activity_add_user_favorite
+	 */
+	public function test_add_user_favorite_not_yet_favorited() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create();
+
+		// bp_activity_add_user_favorite() requires a logged-in user.
+		$current_user = bp_loggedin_user_id();
+		$this->set_current_user( $u );
+		$this->assertTrue( bp_activity_add_user_favorite( $a, $u ) );
+
+		$this->set_current_user( $current_user );
+	}
+
+	/**
+	 * @group favorites
+	 * @group bp_activity_remove_user_favorite
+	 */
+	public function test_remove_user_favorite_bad_activity_id() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$a = self::factory()->activity->create();
+
+		// bp_activity_add_user_favorite() requires a logged-in user.
+		$current_user = bp_loggedin_user_id();
+		$this->set_current_user( $u1 );
+
+		// Only favorite for user 1
+		bp_activity_add_user_favorite( $a, $u1 );
+
+		// Removing for user 2 should fail
+		$this->assertFalse( bp_activity_remove_user_favorite( $a, $u2 ) );
+		$this->assertEquals( 1, bp_activity_get_meta( $a, 'favorite_count' ) );
+
+		$this->set_current_user( $current_user );
+	}
+
+	/**
+	 * @group bp_activity_post_update
+	 */
+	public function test_bp_activity_post_update_empty_content() {
+		$this->assertFalse( bp_activity_post_update( array( 'user_id' => 3, ) ) );
+	}
+
+	/**
+	 * @group bp_activity_post_update
+	 */
+	public function test_bp_activity_post_update_inactive_user() {
+		$this->assertFalse( bp_activity_post_update( array(
+			'user_id' => 3456,
+			'content' => 'foo',
+		) ) );
+	}
+
+	/**
+	 * @group bp_activity_post_update
+	 */
+	public function test_bp_activity_post_update_success() {
+		$u = self::factory()->user->create();
+
+		$a = bp_activity_post_update( array(
+			'user_id' => $u,
+			'content' => 'foo',
+		) );
+
+		$this->assertNotEmpty( $a );
+	}
+
+	/**
+	 * @group bp_activity_get_activity_id
+	 */
+	public function test_bp_activity_get_activity_id() {
+		$args = array(
+			'user_id' => 5,
+			'component' => 'foo',
+			'type' => 'bar',
+			'item_id' => 12,
+			'secondary_item_id' => 44,
+		);
+
+		$a = self::factory()->activity->create( $args );
+
+		$this->assertEquals( $a, bp_activity_get_activity_id( $args ) );
+	}
+
+	/**
+	 * @group bp_activity_delete_by_item_id
+	 */
+	public function test_bp_activity_delete_by_item_id() {
+		$args = array(
+			'user_id' => 5,
+			'component' => 'foo',
+			'type' => 'bar',
+			'item_id' => 12,
+			'secondary_item_id' => 44,
+		);
+
+		$a = self::factory()->activity->create( $args );
+
+		$this->assertTrue( bp_activity_delete_by_item_id( $args ) );
+
+		$found = bp_activity_get_specific( array(
+			'activity_ids' => array( $a ),
+		) );
+
+		$this->assertSame( array(), $found['activities'] );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_user_can_access_their_own_activity() {
+		$u = self::factory()->user->create();
+
+		$a = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_admin_can_access_someone_elses_activity() {
+		$u = self::factory()->user->create();
+		$u2 = self::factory()->user->create( array( 'role' => 'administrator' ) );
+
+		$a = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u ) );
+
+		$this->set_current_user( $u2 );
+		$this->assertTrue( bp_activity_user_can_read( $o, $u2 ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_user_cannot_access_spam_activity() {
+		$u = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$a = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		bp_activity_mark_as_spam( $o );
+
+		$this->assertFalse( bp_activity_user_can_read( $o, $u ) );
+		$this->assertFalse( bp_activity_user_can_read( $o, $u2 ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_admin_can_access_spam_activity() {
+		$u = self::factory()->user->create();
+		$u2 = self::factory()->user->create( array( 'role' => 'administrator' ) );
+
+		$a = self::factory()->activity->create( array(
+			'user_id' => $u,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		bp_activity_mark_as_spam( $o );
+
+		$this->set_current_user( $u2 );
+		$this->assertTrue( bp_activity_user_can_read( $o, $u2 ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_group_admin_access_someone_elses_activity_in_a_group() {
+		$u  = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$g  = self::factory()->group->create();
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'user_id'   => $u,
+			'item_id'   => $g,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u ) );
+
+		self::add_user_to_group( $u2, $g );
+
+		$m1 = new BP_Groups_Member( $u2, $g );
+		$m1->promote( 'admin' );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u2 ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_non_member_can_access_to_someone_elses_activity_in_a_group() {
+		$u  = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$g  = self::factory()->group->create();
+
+		self::add_user_to_group( $u, $g );
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'user_id'   => $u,
+			'item_id'   => $g,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u ) );
+		$this->assertTrue( bp_activity_user_can_read( $o, $u2 ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_user_access_to_his_activity_in_hidden_group() {
+		$u  = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'status' => 'hidden',
+		) );
+
+		self::add_user_to_group( $u, $g );
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'user_id'   => $u,
+			'item_id'   => $g,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_user_access_to_his_activity_in_private_group() {
+		$u  = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		self::add_user_to_group( $u, $g );
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'user_id'   => $u,
+			'item_id'   => $g,
+		) );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertTrue( bp_activity_user_can_read( $o, $u ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_banned_user_cannot_access_to_his_activity_in_a_private_group() {
+		$u  = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		self::add_user_to_group( $u, $g );
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'user_id'   => $u,
+			'item_id'   => $g,
+		) );
+
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u, $g );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertFalse( bp_activity_user_can_read( $o, $u ) );
+	}
+
+	/**
+	 * @group bp_activity_user_can_read
+	 */
+	public function test_removed_member_cannot_access_to_his_activity_in_a_private_group() {
+		$u  = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		self::add_user_to_group( $u, $g );
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'user_id'   => $u,
+			'item_id'   => $g,
+		) );
+
+		buddypress()->is_item_admin = true;
+		groups_remove_member( $u, $g );
+
+		$o = self::factory()->activity->get_object_by_id( $a );
+
+		$this->assertFalse( bp_activity_user_can_read( $o, $u ) );
+	}
+
+	public function check_activity_caches() {
+		foreach ( $this->acaches as $k => $v ) {
+			$this->acaches[ $k ] = wp_cache_get( $k, 'bp_activity' );
+		}
+	}
+
+	/**
+	 * @ticket BP7818
+	 * @ticket BP7698
+	 */
+	public function test_bp_activity_personal_data_exporter() {
+		$u = self::factory()->user->create();
+
+		$a1 = self::factory()->activity->create(
+			array(
+				'user_id'   => $u,
+				'component' => 'activity',
+				'type'      => 'activity_update',
+			)
+		);
+
+		$a2 = self::factory()->activity->create(
+			array(
+				'user_id'           => $u,
+				'component'         => 'activity',
+				'type'              => 'activity_comment',
+				'item_id'           => $a1,
+				'secondary_item_id' => $a1,
+			)
+		);
+
+		$g = self::factory()->group->create(
+			array(
+				'name' => 'My Cool Group',
+			)
+		);
+
+		$a3 = self::factory()->activity->create(
+			array(
+				'user_id'   => $u,
+				'component' => 'groups',
+				'type'      => 'activity_update',
+				'item_id'   => $g,
+			)
+		);
+
+		$test_user = new WP_User( $u );
+
+		$actual = bp_activity_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+
+		// Number of exported activity items.
+		$this->assertSame( 3, count( $actual['data'] ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetActions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetActions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetActions.php	(revision 12265)
@@ -0,0 +1,229 @@
+<?php
+
+/**
+ * @group activity
+ * @covers ::bp_activity_get_actions
+ */
+class BP_Tests_Activity_Functions_BpActivityGetActions extends BP_UnitTestCase {
+	protected $reset_actions;
+	protected $reset_actions_sorted;
+
+	public function setUp() {
+		parent::setUp();
+		$bp = buddypress();
+
+		$this->reset_actions = clone $bp->activity->actions;
+		$bp->activity->actions = new stdClass();
+
+		$this->reset_actions_sorted = ! empty( $bp->activity->actions_sorted );
+		unset( $bp->activity->actions_sorted );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$bp = buddypress();
+
+		$bp->activity->actions = $this->reset_actions;
+
+		if ( $this->reset_actions_sorted ) {
+			$bp->activity->actions_sorted = true;
+		} else {
+			unset( $bp->activity->actions_sorted );
+		}
+	}
+
+	/**
+	 * @group activity_action
+	 */
+	public function test_bp_activity_get_actions_should_sort_by_position() {
+		register_post_type( 'foo5', array(
+			'public'      => true,
+			'supports'    => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'component_id' => 'foo',
+				'action_id' => 'foo_bar_5',
+				'position' => 5,
+			),
+		) );
+
+		register_post_type( 'foo50', array(
+			'public'      => true,
+			'supports'    => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'component_id' => 'foo',
+				'action_id' => 'foo_bar_50',
+				'position' => 50,
+			),
+		) );
+
+		register_post_type( 'foo25', array(
+			'public'      => true,
+			'supports'    => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'component_id' => 'foo',
+				'action_id' => 'foo_bar_25',
+				'position' => 25,
+			),
+		) );
+
+		$actions = bp_activity_get_actions();
+
+		_unregister_post_type( 'foo5' );
+		_unregister_post_type( 'foo25' );
+		_unregister_post_type( 'foo50' );
+
+		$expected = array(
+			'foo_bar_5',
+			'foo_bar_25',
+			'foo_bar_50',
+		);
+		$foo_actions = (array) $actions->foo;
+		$this->assertEquals( $expected, array_values( wp_list_pluck( $foo_actions, 'key' ) ) );
+	}
+
+	/**
+	 * @ticket BP6865
+	 */
+	public function test_bp_activity_get_actions_sort() {
+		bp_activity_set_action(
+			'foo',
+			'new_foo',
+			'Did a foo',
+			'',
+			'',
+			array(),
+			10
+		);
+
+		bp_activity_set_action(
+			'foo',
+			'new_bar',
+			'Did a bar',
+			'',
+			'',
+			array(),
+			5
+		);
+
+		$actions = bp_activity_get_actions();
+
+		$expected = array(
+			'new_bar' => 'new_bar',
+			'new_foo' => 'new_foo',
+		);
+
+		$this->assertSame( $expected, wp_list_pluck( (array) $actions->foo, 'key' ) );
+	}
+
+	/**
+	 * @ticket BP6865
+	 */
+	public function test_sort_new_post_type_once() {
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'component_id' => 'blogs',
+				'action_id'    => 'new_foo',
+				'position'     => 1,
+			),
+		) );
+
+		$actions = bp_activity_get_actions();
+
+		_unregister_post_type( 'foo' );
+
+		$expected = array(
+			'new_foo'          => 'new_foo',
+			'new_blog_post'    => 'new_blog_post',
+			'new_blog_comment' => 'new_blog_comment',
+		);
+
+		$this->assertSame( $expected, wp_list_pluck( (array) $actions->blogs, 'key' ) );
+	}
+
+	/**
+	 * @ticket BP6865
+	 */
+	public function test_sort_new_post_type_twice() {
+		$reset_activity_track = buddypress()->activity->track;
+
+		$actions = bp_activity_get_actions();
+		$expected = array(
+			'new_blog_post'    => 'new_blog_post',
+			'new_blog_comment' => 'new_blog_comment',
+		);
+		$this->assertSame( $expected, wp_list_pluck( (array) $actions->blogs, 'key' ) );
+
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+			'bp_activity' => array(
+				'component_id' => 'blogs',
+				'action_id'    => 'new_foo',
+				'position'     => 1,
+			),
+		) );
+
+		/*
+		 * Reset manually as bp_activity_get_actions() -- at the top of this test --
+		 * sets ->track[] for the previously-registered post types.
+		 */
+		buddypress()->activity->track = bp_activity_get_post_types_tracking_args();
+
+		$actions = bp_activity_get_actions();
+
+		_unregister_post_type( 'foo' );
+
+		$expected = array(
+			'new_foo'          => 'new_foo',
+			'new_blog_post'    => 'new_blog_post',
+			'new_blog_comment' => 'new_blog_comment',
+		);
+
+		$this->assertSame( $expected, wp_list_pluck( (array) $actions->blogs, 'key' ) );
+
+		buddypress()->activity->track = $reset_activity_track;
+	}
+
+	/**
+	 * @ticket BP6865
+	 */
+	public function test_sort_no_post_type_registered() {
+		bp_activity_set_action(
+			'foo',
+			'new_foo',
+			'Did a foo',
+			'',
+			'',
+			array(),
+			10
+		);
+
+		bp_activity_set_action(
+			'foo',
+			'new_bar',
+			'Did a bar',
+			'',
+			'',
+			array(),
+			5
+		);
+
+		remove_post_type_support( 'post', 'buddypress-activity' );
+
+		$actions = bp_activity_get_actions();
+
+		$expected = array(
+			'new_bar' => 'new_bar',
+			'new_foo' => 'new_foo',
+		);
+
+		$this->assertSame( $expected, wp_list_pluck( (array) $actions->foo, 'key' ) );
+
+		// Clean up.
+		add_post_type_support( 'post', 'buddypress-activity' );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetActionsForContext.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetActionsForContext.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetActionsForContext.php	(revision 12265)
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @group activity
+ */
+class BP_Tests_Activity_Functions_BpActivityGetActionsForContext extends BP_UnitTestCase {
+	public function setUp() {
+		parent::setUp();
+
+		$bp = buddypress();
+
+		$this->reset_actions = clone $bp->activity->actions;
+		$bp->activity->actions = new stdClass();
+
+		$this->reset_actions_sorted = ! empty( $bp->activity->actions_sorted );
+		unset( $bp->activity->actions_sorted );
+	}
+
+	public function tearDown() {
+		$bp = buddypress();
+
+		$bp->activity->actions = $this->reset_actions;
+
+		if ( $this->reset_actions_sorted ) {
+			$bp->activity->actions_sorted = true;
+		} else {
+			unset( $bp->activity->actions_sorted );
+		}
+
+		parent::tearDown();
+	}
+
+	public function test_should_include_actions_registered_for_context() {
+		bp_activity_set_action(
+			'foo',
+			'new_foo',
+			'Did a foo',
+			'',
+			'',
+			array( 'member' ),
+			10
+		);
+
+		$actions = bp_activity_get_actions_for_context( 'member' );
+		$keys = wp_list_pluck( $actions, 'key' );
+		$this->assertContains( 'new_foo', $keys );
+	}
+
+	public function test_should_ignore_actions_not_registered_for_context() {
+		bp_activity_set_action(
+			'foo',
+			'new_foo',
+			'Did a foo',
+			'',
+			'',
+			array( 'member' ),
+			10
+		);
+
+		$actions = bp_activity_get_actions_for_context( 'group' );
+		$keys = wp_list_pluck( $actions, 'key' );
+		$this->assertNotContains( 'new_foo', $keys );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetCommentDepth.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetCommentDepth.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/functions/bpActivityGetCommentDepth.php	(revision 12265)
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @group activity
+ * @group bp_activity_get_comment_depth
+ */
+class BP_Tests_Activity_Functions_BpActivityGetCommentDepth extends BP_UnitTestCase {
+	/**
+	 * @ticket BP7329
+	 */
+	public function test_depth_inside_activity_comment_loop() {
+		$u = self::factory()->user->create();
+
+		// create an activity update
+		$parent_activity = self::factory()->activity->create( array(
+			'type'    => 'activity_update',
+			'user_id' => $u
+		) );
+
+		// create some activity comments
+		$comment_one = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'content'     => 'depth 1'
+		) );
+
+		$comment_one_one = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'parent_id'   => $comment_one,
+			'content'     => 'depth 2'
+		) );
+
+		$comment_two = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'content'     => 'depth 1'
+		) );
+
+		// Instantiate activity loop, which also includes activity comments.
+		bp_has_activities( 'display_comments=threaded' );
+
+		// Loop through activity comments generated in activity loop.
+		$recursive = new RecursiveIteratorIterator( new RecursiveArrayIterator( $GLOBALS['activities_template']->activities[0]->children ), RecursiveIteratorIterator::SELF_FIRST );
+		foreach ( $recursive as $aid => $a ) {
+			if ( ! is_numeric( $aid ) || ! is_object( $a ) ) {
+				continue;
+			}
+
+			/*
+			 * Emulate activity comment loop global, which bp_activity_get_comment_depth()
+			 * relies on by default.
+			 */
+			$GLOBALS['activities_template']->activity = new stdClass;
+			$GLOBALS['activities_template']->activity->current_comment = $a;
+
+			// $aid is the activity ID for the current activity comment.
+			switch ( $aid ) {
+				case $comment_one :
+				case $comment_two :
+					$this->assertSame( bp_activity_get_comment_depth(), 1 );
+					break;
+
+				case $comment_one_one :
+					$this->assertSame( bp_activity_get_comment_depth(), 2 );
+					break;
+			}
+
+		}
+
+		// Clean up after ourselves!
+		$GLOBALS['activities_template'] = null;
+	}
+
+	/**
+	 * @ticket BP7329
+	 */
+	public function test_depth_outside_of_activity_comment_loop() {
+		$u = self::factory()->user->create();
+
+		// create an activity update
+		$parent_activity = self::factory()->activity->create( array(
+			'type'    => 'activity_update',
+			'user_id' => $u
+		) );
+
+		// create some activity comments
+		$comment_one = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'content'     => 'depth 1'
+		) );
+
+		$comment_one_one = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'parent_id'   => $comment_one,
+			'content'     => 'depth 2'
+		) );
+
+		$comment_two = bp_activity_new_comment( array(
+			'user_id'     => $u,
+			'activity_id' => $parent_activity,
+			'content'     => 'depth 1'
+		) );
+
+		$this->assertSame( bp_activity_get_comment_depth( $comment_one ), 1 );
+		$this->assertSame( bp_activity_get_comment_depth( $comment_one_one ), 2 );
+		$this->assertSame( bp_activity_get_comment_depth( $comment_two ), 1 );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/notifications.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/notifications.php	(revision 12265)
@@ -0,0 +1,490 @@
+<?php
+
+/**
+ * @group activity
+ * @group notifications
+ */
+class BP_Tests_Activity_Notifications extends BP_UnitTestCase {
+	protected $current_user;
+	protected $u1;
+	protected $u2;
+	protected $a1;
+	protected $a2;
+
+	public function setUp() {
+		parent::setUp();
+		$this->current_user = get_current_user_id();
+		$this->u1 = self::factory()->user->create();
+		$this->u2 = self::factory()->user->create();
+		$this->set_current_user( $this->u1 );
+
+		/**
+		 * Tests suite in WP < 4.0 does not include the WP_UnitTestCase->_restore_hooks() function
+		 * When updating an activity, the following filter is fired to prevent sending more than one
+		 * notification. Once we've reached this filter all at_mentions tests fails so we need to
+		 * temporarly remove it and restore it in $this->tearDown()
+		 */
+		remove_filter( 'bp_activity_at_name_do_notifications', '__return_false' );
+	}
+
+	public function tearDown() {
+		$this->set_current_user( $this->current_user );
+		parent::tearDown();
+
+		// Restore the filter
+		add_filter( 'bp_activity_at_name_do_notifications', '__return_false' );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_single_activity_permalink() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// Go to the activity permalink page
+		$this->go_to( bp_core_get_user_domain( $this->u1 ) . 'activity/' . $this->a1 );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Should be empty
+		$this->assertEquals( array(), $notifications );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_single_activity_permalink_logged_out() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// Log out
+		$this->set_current_user( 0 );
+
+		// Go to the activity permalink page
+		$this->go_to( bp_core_get_user_domain( $this->u1 ) . 'activity/' . $this->a1 );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Should be untouched
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		$this->set_current_user( $this->u1 );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_single_activity_permalink_wrong_user() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// Switch user
+		$this->set_current_user( $this->u2 );
+
+		// Go to the activity permalink page
+		$this->go_to( bp_core_get_user_domain( $this->u1 ) . 'activity/' . $this->a1 );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Should be untouched
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		$this->set_current_user( $this->u1 );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_mentions() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// Go to the My Activity page
+		$this->go_to( bp_core_get_user_domain( $this->u1 ) . bp_get_activity_slug() . '/mentions/' );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Should be empty
+		$this->assertEquals( array(), $notifications );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_mentions_logged_out() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// Log out
+		$this->set_current_user( 0 );
+
+		// Go to the My Activity page
+		$this->go_to( bp_core_get_user_domain( $this->u1 ) . bp_get_activity_slug() . '/mentions/' );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Should be untouched
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// clean up
+		$this->set_current_user( $this->u1 );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_mentions_wrong_user() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// Log out
+		$this->set_current_user( $this->u2 );
+
+		// Go to the My Activity page
+		$this->go_to( bp_core_get_user_domain( $this->u1 ) . bp_get_activity_slug() . '/mentions/' );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		// Should be untouched
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		// clean up
+		$this->set_current_user( $this->u1 );
+	}
+
+	/**
+	 * @group bp_notifications_delete_all_notifications_by_type
+	 * @group bp_activity_at_mention_delete_notification
+	 */
+	public function test_bp_activity_at_mention_delete_notification() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'item_id' => $this->a1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+
+		bp_activity_delete( array(
+			'id' => $this->a1,
+		) );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'item_id' => $this->a1,
+		) );
+
+		$this->assertEmpty( $notifications );
+	}
+
+	/**
+	 * @group bp_activity_remove_screen_notifications
+	 * @group mentions
+	 * @ticket BP6687
+	 */
+	public function test_bp_activity_remove_screen_notifications_on_new_mentions_cleared() {
+		$this->create_notifications();
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'item_id' => $this->a1,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $this->a1 ), wp_list_pluck( $notifications, 'item_id' ) );
+		$this->assertEquals( 1, bp_get_total_mention_count_for_user( $this->u1 ) );
+
+		// Clear notifications for $this->u1
+		bp_activity_clear_new_mentions( $this->u1 );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'item_id' => $this->a1,
+		) );
+
+		$this->assertEmpty( $notifications, 'Notifications should be cleared when new mention metas are removed' );
+		$this->assertEmpty( bp_get_total_mention_count_for_user( $this->u1 ) );
+	}
+
+	/**
+	 * Creates two notifications for $u1, one of which is for mentions
+	 */
+	protected function create_notifications() {
+		$u1_mentionname = bp_activity_get_user_mentionname( $this->u1 );
+		$this->a1 = self::factory()->activity->create( array(
+			'user_id' => $this->u2,
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_update',
+			'content' => sprintf( 'Hello! @%s', $u1_mentionname ),
+		) );
+		$u2_mentionname = bp_activity_get_user_mentionname( $this->u2 );
+		$this->a2 = self::factory()->activity->create( array(
+			'user_id' => $this->u1,
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_update',
+			'content' => sprintf( 'Hello! @%s', $u2_mentionname ),
+		) );
+	}
+
+	/**
+	 * @group bp_activity_format_notifications
+	 */
+	public function test_bp_activity_format_notifications_new_at_mention() {
+		$this->test_format_filter = array();
+
+		// Current user is $this->u1, so $this->u2 posted the mention
+		$a = self::factory()->activity->create( array(
+			'user_id' => $this->u2,
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_update',
+			'content' => 'fake new_at_mention activity',
+		) );
+
+		add_filter( 'bp_activity_single_at_mentions_notification', array( $this, 'format_notification_filter' ), 10, 1 );
+		add_filter( 'bp_activity_multiple_at_mentions_notification', array( $this, 'format_notification_filter' ), 10, 1 );
+
+		$format_tests = array(
+			'array_single'    => bp_activity_format_notifications( 'new_at_mention', $a, $this->u2, 1, 'array' ),
+			'string_single'   => bp_activity_format_notifications( 'new_at_mention', $a, $this->u2, 1 ),
+			'array_multiple'  => bp_activity_format_notifications( 'new_at_mention', $a, $this->u2, 2, 'array' ),
+			'string_multiple' => bp_activity_format_notifications( 'new_at_mention', $a, $this->u2, 2 ),
+		);
+
+		remove_filter( 'bp_activity_single_at_mentions_notification', array( $this, 'format_notification_filter' ), 10 );
+		remove_filter( 'bp_activity_multiple_at_mentions_notification', array( $this, 'format_notification_filter' ), 10 );
+
+		$single = sprintf( __( '%1$s mentioned you', 'buddypress' ), bp_core_get_user_displayname( $this->u2 ) );
+		$multiple = 'You have 2 new mentions';
+
+		$this->assertContains( $single, $format_tests['string_single'] );
+		$this->assertContains( $single, $format_tests['array_single']['text'] );
+		$this->assertContains( $multiple, $format_tests['string_multiple'] );
+		$this->assertContains( $multiple, $format_tests['array_multiple']['text'] );
+
+		// Check filters
+		$this->assertTrue( 4 === count( $this->test_format_filter ) );
+	}
+
+	public function format_notification_filter( $return ) {
+		$this->test_format_filter[] = current_filter();
+		return $return;
+	}
+
+	/**
+	 * @group bp_activity_update_reply_add_notification
+	 * @group bp_activity_comment_reply_add_notification
+	 */
+	public function test_bp_activity_comment_add_notification() {
+		$a = self::factory()->activity->create( array(
+			'user_id' => $this->u1,
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_update',
+			'content' => 'Please comment this activity.',
+		) );
+
+		$c = bp_activity_new_comment( array(
+			'content'     => 'this is the comment',
+			'user_id'     => $this->u2,
+			'activity_id' => $a, // ID of the root activity item.
+			'parent_id'   => false  // ID of a parent comment (optional).
+		) );
+
+		$u3 = self::factory()->user->create();
+
+		$r3 = bp_activity_new_comment( array(
+			'content'     => 'this is a reply to a comment',
+			'user_id'     => $u3,
+			'activity_id' => $a, // ID of the root activity item.
+			'parent_id'   => $c  // ID of a parent comment (optional).
+		) );
+
+		$u1_notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) );
+
+		$expected_commenters = array( $this->u2, $u3 );
+		$this->assertEquals( $expected_commenters, wp_list_pluck( $u1_notifications, 'secondary_item_id' ) );
+
+		$u2_notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $this->u2,
+		) );
+
+		$expected_commenter = array( $u3 );
+		$this->assertEquals( $expected_commenter, wp_list_pluck( $u2_notifications, 'secondary_item_id' ) );
+
+		// Attempt to mark 'update_reply' notifications as read for user 1.
+		foreach ( $u1_notifications as $i => $n ) {
+			$n = bp_activity_format_notifications( $n->component_action, $n->item_id, $n->secondary_item_id, 1, 'array', $n->id );
+			if ( ! empty( $n['link'] ) ) {
+				// Remove redirecter for unit tests.
+				$n['link'] = str_replace( '/p/', '/', $n['link'] );
+
+				// Attempt to clear the notification by going to the activity permalink.
+				$this->go_to( $n['link'] );
+			}
+		}
+
+		// Assert that notifications for user 1 are cleared and empty.
+		$this->assertEmpty( BP_Notifications_Notification::get( array(
+			'user_id' => $this->u1,
+		) ) );
+
+		// Attempt to mark 'comment_reply' notifications as read for user 2.
+		$this->set_current_user( $this->u2 );
+		foreach ( $u2_notifications as $i => $n ) {
+			$n = bp_activity_format_notifications( $n->component_action, $n->item_id, $n->secondary_item_id, 1, 'array', $n->id );
+			if ( ! empty( $n['link'] ) ) {
+				// Remove redirecter for unit tests.
+				$n['link'] = str_replace( '/p/', '/', $n['link'] );
+
+				// Attempt to clear the notification by going to the activity permalink.
+				$this->go_to( $n['link'] );
+			}
+		}
+
+		// Assert that notifications for user 2 are cleared and empty.
+		$this->assertEmpty( BP_Notifications_Notification::get( array(
+			'user_id' => $this->u2,
+		) ) );
+	}
+
+	/**
+	 * @ticket BP7135
+	 */
+	public function test_activity_reply_notifications_for_blog_comment_to_activity_comment_sync() {
+		$old_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$this->set_current_user( $u1 );
+		$userdata = get_userdata( $u1 );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// Silence comment flood errors.
+		add_filter( 'comment_flood_filter', '__return_false' );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test post',
+		) );
+
+		$this->set_current_user( $u2 );
+		$userdata = get_userdata( $u2 );
+
+		$c1 = wp_new_comment( array(
+			'comment_post_ID'      => $post_id,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is a blog comment',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u2,
+		) );
+		// Approve the comment
+		self::factory()->comment->update_object( $c1, array( 'comment_approved' => 1 ) );
+
+		$this->set_current_user( $u3 );
+		$userdata = get_userdata( $u3 );
+
+		$c2 = wp_new_comment( array(
+			'comment_post_ID'      => $post_id,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is a blog comment',
+			'comment_type'         => '',
+			'comment_parent'       => $c1,
+			'user_id'              => $u3,
+		) );
+		// Approve the comment
+		self::factory()->comment->update_object( $c2, array( 'comment_approved' => 1 ) );
+
+		// Get activity IDs.
+		$ac1 = get_comment_meta( $c1, 'bp_activity_comment_id', true );
+		$ac2 = get_comment_meta( $c2, 'bp_activity_comment_id', true );
+
+		// Check if notifications exists for user 1.
+		$n1 = BP_Notifications_Notification::get( array(
+			'component_name' => 'activity',
+			'user_id'        => $u1
+		) );
+		$this->assertEquals( 2, count( $n1 ) );
+		$this->assertEquals(
+			array( $ac1, $ac2 ),
+			wp_list_pluck( $n1, 'item_id' )
+		);
+
+		// Check if notification exists for user 2.
+		$n2 = BP_Notifications_Notification::get( array(
+			'component_action' => 'comment_reply',
+			'item_id'          => $ac2,
+			'user_id'          => $u2
+		) );
+		$this->assertNotEmpty( $n2 );
+
+		// Reset.
+		$this->set_current_user( $old_user );
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+		remove_filter( 'comment_flood_filter', '__return_false' );
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/activity/template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/activity/template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/activity/template.php	(revision 12265)
@@ -0,0 +1,1688 @@
+<?php
+/**
+ * @group activity
+ */
+class BP_Tests_Activity_Template extends BP_UnitTestCase {
+
+	/**
+	 * @ticket BP4735
+	 */
+	public function test_user_can_delete() {
+		$bp = buddypress();
+		$u = self::factory()->user->create();
+		$original_user = bp_loggedin_user_id();
+		$this->set_current_user( $u );
+
+		$a = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+			'user_id' => $u,
+		) );
+
+		// User can delete his own items
+		$activity = self::factory()->activity->get_object_by_id( $a );
+		$this->assertTrue( bp_activity_user_can_delete( $activity ) );
+
+		// Logged-out user can't delete
+		$this->set_current_user( 0 );
+		$this->assertFalse( bp_activity_user_can_delete( $activity ) );
+
+		// Miscellaneous user can't delete
+		$misc_user = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+		$this->set_current_user( $misc_user );
+		$this->assertFalse( bp_activity_user_can_delete( $activity ) );
+
+		// Item admin can delete
+		$is_single_item = $bp->is_single_item;
+		$bp->is_single_item = true;
+
+		$is_item_admin = $bp->is_item_admin;
+		$bp->is_item_admin = true;
+
+		$this->assertTrue( bp_activity_user_can_delete( $activity ) );
+
+		$bp->is_single_item = $is_single_item;
+		$bp->is_item_admin = $is_item_admin;
+		$this->set_current_user( $original_user );
+	}
+
+	/**
+	 * Test if a non-admin can delete their own activity.
+	 */
+	public function test_user_can_delete_for_nonadmin() {
+		// save the current user and override logged-in user
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// create an activity update for the user
+		self::factory()->activity->create( array(
+			'component' => buddypress()->activity->id,
+			'type' => 'activity_update',
+			'user_id' => $u,
+		) );
+
+		// start the activity loop
+		bp_has_activities( array( 'user_id' => $u ) );
+		while ( bp_activities() ) : bp_the_activity();
+			// assert!
+			$this->assertTrue( bp_activity_user_can_delete() );
+		endwhile;
+
+		// reset
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * Make sure that action filters ('activity_update', etc) work when
+	 * limiting query to user favorites
+	 *
+	 * @ticket BP4872
+	 * @group scope
+	 */
+	public function test_bp_has_activities_favorites_action_filter() {
+		$user_id = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+
+		$now = time();
+
+		$a1 = self::factory()->activity->create( array(
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		$a2 = self::factory()->activity->create( array(
+			'type' => 'joined_group',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		$current_user = bp_loggedin_user_id();
+		$this->set_current_user( $user_id );
+
+		bp_activity_add_user_favorite( $a1, $user_id );
+		bp_activity_add_user_favorite( $a2, $user_id );
+
+		$this->set_current_user( $current_user );
+
+		// groan. It sucks that you have to invoke the global
+		global $activities_template;
+
+		// Case 1: no action filter
+		bp_has_activities( array(
+			'user_id' => $user_id,
+			'scope' => 'favorites',
+		) );
+
+		// The formatting of $activities_template->activities is messed
+		// up, so we're just going to look at the IDs. This should be
+		// fixed in BP at some point
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+
+		$this->assertEquals( array( $a1, $a2 ), $ids );
+
+		$activities_template = null;
+
+		// Case 2: action filter
+		bp_has_activities( array(
+			'user_id' => $user_id,
+			'scope' => 'favorites',
+			'action' => 'activity_update',
+		) );
+
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+
+		$this->assertEquals( array( $a1 ), $ids );
+
+		$activities_template = null;
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_just_me_scope_with_no_user_id() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// save the current user and override logged-in user
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+
+		$now = time();
+
+		// activity item
+		$a1 = self::factory()->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// misc activity items
+
+		self::factory()->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		self::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 just-me scope with no user ID
+		// user ID should fallback to logged-in user ID
+		bp_has_activities( array(
+			'user_id' => false,
+			'scope' => 'just-me',
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_mentions_scope() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$now = time();
+
+		// mentioned activity item
+		$mention_username = '@' . bp_activity_get_user_mentionname( $u1 );
+		$a1 = self::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
+		self::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 ),
+		) );
+		self::factory()->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		self::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 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		// user 1 becomes friends with user 2
+		friends_add_friend( $u1, $u2, true );
+
+		$now = time();
+
+		// friend status update
+		$a1 = self::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 = self::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
+		self::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 ),
+		) );
+		self::factory()->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		self::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 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		// user 1 becomes friends with user 2
+		friends_add_friend( $u1, $u2, true );
+
+		// user 1 joins a group
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		$now = time();
+
+		// friend status update
+		$a1 = self::factory()->activity->create( array(
+			'user_id' => $u2,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		// group activity
+		$a2 = self::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
+		self::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 ),
+		) );
+		self::factory()->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// Make sure we get a fake group ID.
+		global $wpdb, $bp;
+		$max_group_id = $wpdb->get_var( "SELECT id FROM {$bp->groups->table_name} ORDER BY id DESC LIMIT 1" );
+		self::factory()->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'groups',
+			'item_id'   => $max_group_id + 1,
+			'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 scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_scope_friends_no_items() {
+		$u1 = self::factory()->user->create();
+
+		$now = time();
+
+		// Create a random activity
+		self::factory()->activity->create( array(
+			'user_id' => $u1,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		// grab activities from friends scope
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'friends',
+		) );
+
+		// assert!
+		$this->assertEmpty( $activities_template->activities, 'When a user does not have any friendship, no activities should be fetched when on friends scope' );
+
+		// clean up!
+		$activities_template = $reset_activities_template;
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_scope_favorites_no_items() {
+		$u1 = self::factory()->user->create();
+
+		$now = time();
+
+		// Create a random activity
+		self::factory()->activity->create( array(
+			'user_id' => $u1,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		// grab activities from favorites scope
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'favorites',
+		) );
+
+		// assert!
+		$this->assertEmpty( $activities_template->activities, 'When a user has not favorited any activity, no activities should be fetched when on favorites scope' );
+
+		// clean up!
+		$activities_template = $reset_activities_template;
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_scope_groups_no_items() {
+		$u1 = self::factory()->user->create();
+
+		$now = time();
+
+		// Create a random activity
+		self::factory()->activity->create( array(
+			'user_id' => $u1,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		// grab activities from groups scope
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'groups',
+		) );
+
+		// assert!
+		$this->assertEmpty( $activities_template->activities, 'When a user is not a member of any group, no activities should be fetched when on groups scope' );
+
+		// clean up!
+		$activities_template = $reset_activities_template;
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_scope_mentions_no_items() {
+		$u1 = self::factory()->user->create();
+
+		$now = time();
+
+		// Create a random activity
+		self::factory()->activity->create( array(
+			'user_id' => $u1,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		// grab activities from mentions scope
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'mentions',
+		) );
+
+		// assert!
+		$this->assertEmpty( $activities_template->activities, 'When a user has no mention, no activities should be fetched when on the mentions scope' );
+
+		// clean up!
+		$activities_template = $reset_activities_template;
+	}
+
+	/**
+	 * @group scope
+	 * @ticket BP6720
+	 */
+	public function test_bp_has_activities_scope_friends_should_respect_id_order_when_record_dates_are_same() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		friends_add_friend( $u1, $u2, true );
+
+		// Friend's very fast status updates.
+		$a1 = self::factory()->activity->create( array(
+			'user_id' => $u2,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', 1451944920 ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'user_id' => $u2,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', 1451944920 ),
+		) );
+
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		// Get activities in 'friends' scope
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'friends',
+		) );
+
+		$found = $activities_template->activities;
+
+		// Clean up!
+		$activities_template = $reset_activities_template;
+
+		$this->assertEquals( array( $a2, $a1 ), wp_list_pluck( $found, 'id' ) );
+	}
+
+	/**
+	 * @group scope
+	 * @ticket BP6720
+	 */
+	public function test_bp_has_activities_scope_groups_should_respect_id_order_when_record_dates_are_same() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Two user join first user's group same time
+		$a1 = self::factory()->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'groups',
+			'item_id'   => $g1,
+			'type'      => 'joined_group',
+			'recorded_time' => date( 'Y-m-d H:i:s', 1451944920 ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'groups',
+			'item_id'   => $g1,
+			'type'      => 'joined_group',
+			'recorded_time' => date( 'Y-m-d H:i:s', 1451944920 ),
+		) );
+
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		// Get activities in 'groups' scope
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'groups',
+		) );
+
+		$found = $activities_template->activities;
+
+		// Clean up!
+		$activities_template = $reset_activities_template;
+
+		$this->assertEquals( array( $a2, $a1 ), wp_list_pluck( $found, 'id' ) );
+	}
+
+	/**
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_with_filter_query_nested_conditions() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$now = time();
+
+		$a1 = self::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 = self::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
+		self::factory()->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		self::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 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = self::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 = self::factory()->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = self::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 = self::factory()->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = self::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 = self::factory()->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = self::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 = self::factory()->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = self::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 = self::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 = self::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 = self::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
+	 * @group post_type_comment_activities
+	 */
+	function test_bp_has_activities_with_filter_query_compare_regex() {
+		$u1 = self::factory()->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = self::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 = self::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 = self::factory()->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = self::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;
+	}
+
+	/**
+	 * @ticket BP6169
+	 * @group bp_has_activities
+	 */
+	public function test_bp_has_activities_private_group_home_scope() {
+		global $activities_template;
+		$bp = buddypress();
+		$reset_current_group = $bp->groups->current_group;
+		$reset_current_action = $bp->current_action;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$this->set_current_user( $u1 );
+
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		groups_join_group( $g, $u2 );
+		groups_join_group( $g, $u3 );
+
+		$a1 = self::factory()->activity->create( array(
+			'component' => $bp->groups->id,
+			'item_id'   => $g,
+			'type'      => 'activity_update',
+			'user_id'   => $u2,
+			'content'   => 'foo bar',
+		) );
+
+		$a2 = self::factory()->activity->create( array(
+			'component' => $bp->groups->id,
+			'item_id'   => $g,
+			'type'      => 'activity_update',
+			'user_id'   => $u3,
+			'content'   => 'bar foo',
+		) );
+
+		$bp->groups->current_group = groups_get_group( $g );
+
+		// On group's home the scope is set to 'home'
+		$bp->current_action = 'home';
+
+		bp_has_activities( array( 'action' => 'activity_update' ) );
+
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+		$bp->groups->current_group = $reset_current_group;
+		$bp->current_action = $reset_current_action;
+	}
+
+	/**
+	 * @ticket BP6169
+	 * @group bp_has_activities
+	 */
+	public function test_bp_has_activities_hidden_group_home_scope() {
+		global $activities_template;
+		$bp = buddypress();
+		$reset_current_group = $bp->groups->current_group;
+		$reset_current_action = $bp->current_action;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$this->set_current_user( $u1 );
+
+		$g = self::factory()->group->create( array(
+			'status' => 'hidden',
+		) );
+
+		groups_join_group( $g, $u2 );
+		groups_join_group( $g, $u3 );
+
+		$a1 = self::factory()->activity->create( array(
+			'component' => $bp->groups->id,
+			'item_id'   => $g,
+			'type'      => 'activity_update',
+			'user_id'   => $u2,
+			'content'   => 'foo bar',
+		) );
+
+		$a2 = self::factory()->activity->create( array(
+			'component' => $bp->groups->id,
+			'item_id'   => $g,
+			'type'      => 'activity_update',
+			'user_id'   => $u3,
+			'content'   => 'bar foo',
+		) );
+
+		$bp->groups->current_group = groups_get_group( $g );
+
+		// On group's home the scope is set to 'home'
+		$bp->current_action = 'home';
+
+		bp_has_activities( array( 'action' => 'activity_update' ) );
+
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+		$bp->groups->current_group = $reset_current_group;
+		$bp->current_action = $reset_current_action;
+	}
+
+	/**
+	 * Integration test for 'meta_query' param
+	 */
+	function test_bp_has_activities_with_meta_query() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		bp_activity_update_meta( $a1, 'foo', 'bar' );
+
+		global $activities_template;
+		bp_has_activities( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+		$this->assertEquals( $ids, array( $a1 ) );
+	}
+
+	/**
+	 * @ticket BP5029
+	 * @group bp_has_activities
+	 */
+	public function test_bp_has_activities_with_display_comments_false() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		global $activities_template;
+		bp_has_activities( array(
+			'display_comments' => false,
+		) );
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+
+		$this->assertEquals( array( $a1, $a2 ), wp_parse_id_list( $ids ) );
+
+	}
+
+	/**
+	 * @ticket BP5029
+	 * @group bp_has_activities
+	 */
+	public function test_bp_has_activities_with_display_comments_0() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		global $activities_template;
+		bp_has_activities( array(
+			'display_comments' => 0,
+		) );
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+
+		$this->assertEquals( array( $a1, $a2 ), wp_parse_id_list( $ids ) );
+
+	}
+
+	/**
+	 * @ticket BP5029
+	 * @group bp_has_activities
+	 */
+	public function test_bp_has_activities_with_display_comments_0_querystring() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		global $activities_template;
+		bp_has_activities( 'display_comments=0' );
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+
+		$this->assertEquals( array( $a1, $a2 ), $ids );
+
+	}
+
+	/**
+	 * @ticket BP5029
+	 * @group bp_has_activities
+	 */
+	public function test_bp_has_activities_with_display_comments_none_querystring() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		global $activities_template;
+		bp_has_activities( 'display_comments=none' );
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+
+		$this->assertEquals( array( $a1, $a2 ), $ids );
+
+	}
+
+	/**
+	 * @group bp_has_activities
+	 * @group cache
+	 */
+	public function test_bp_has_activities_with_update_meta_cache_false() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		bp_activity_add_meta( $a1, 'foo', 'bar' );
+		bp_activity_add_meta( $a2, 'foo1', 'bar2' );
+
+		// prime
+		bp_has_activities( array(
+			'update_meta_cache' => false,
+		) );
+
+		$this->assertFalse( wp_cache_get( $a1, 'activity_meta' ) );
+		$this->assertFalse( wp_cache_get( $a2, 'activity_meta' ) );
+	}
+
+	/**
+	 * @group bp_has_activities
+	 * @group cache
+	 */
+	public function test_bp_has_activities_with_update_meta_cache_true() {
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		bp_activity_add_meta( $a1, 'foo', 'bar' );
+		bp_activity_add_meta( $a2, 'foo1', 'bar2' );
+
+		// prime
+		bp_has_activities( array(
+			'update_meta_cache' => true,
+		) );
+
+		$this->assertNotEmpty( wp_cache_get( $a1, 'activity_meta' ) );
+		$this->assertNotEmpty( wp_cache_get( $a2, 'activity_meta' ) );
+	}
+
+	/**
+	 * @group bp_has_activities
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_has_activities_with_type_new_blog_comments() {
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$u = self::factory()->user->create();
+
+		$now = time();
+		$a1 = self::factory()->activity->create( array(
+			'content' => 'Life Rules',
+			'component' => 'blogs',
+			'type' => 'new_blog_post',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+			'user_id' => $u,
+		) );
+		$a2 = self::factory()->activity->create( array(
+			'content' => 'Life Drools',
+			'component' => 'blogs',
+			'type' => 'new_blog_comment',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+			'user_id' => $u,
+		) );
+
+		// This one will show up in the stream because it's a comment
+		// on a blog post
+		$a3 = bp_activity_new_comment( array(
+			'activity_id' => $a1,
+			'content' => 'Candy is good',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 200 ),
+			'user_id' => $u,
+		) );
+
+		$a4 = self::factory()->activity->create( array(
+			'content' => 'Life Rulez',
+			'component' => 'activity',
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 300 ),
+			'user_id' => $u,
+		) );
+
+		// This one should not show up in the stream because it's a
+		// comment on an activity item
+		$a5 = bp_activity_new_comment( array(
+			'activity_id' => $a4,
+			'content' => 'Candy is great',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 400 ),
+			'user_id' => $u,
+		) );
+		global $activities_template;
+
+		// prime
+		bp_has_activities( array(
+			'component' => 'blogs',
+			'action' => 'new_blog_comment',
+		) );
+
+		$this->assertEquals( array( $a3, $a2 ), wp_parse_id_list( wp_list_pluck( $activities_template->activities, 'id' ) ) );
+
+		// Clean up
+		$activities_template = null;
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+	}
+
+	/**
+	 * @group bp_activity_can_comment_reply
+	 */
+	public function test_bp_activity_can_comment_reply_thread_comments_on() {
+		$tc = get_option( 'thread_comments' );
+		update_option( 'thread_comments', '1' );
+
+		$tcd = get_option( 'thread_comments_depth' );
+		update_option( 'thread_comments_depth', '4' );
+
+		// Fake the global
+		global $activities_template;
+		$activities_template = new stdClass;
+		$activities_template->activity = new stdClass;
+		$activities_template->activity->current_comment = new stdClass;
+
+		$comment = new stdClass;
+		$comment->item_id = 4;
+
+		$activities_template->activity->current_comment->depth = 1;
+		$this->assertTrue( bp_activity_can_comment_reply( $comment ) );
+
+		$activities_template->activity->current_comment->depth = 3;
+		$this->assertTrue( bp_activity_can_comment_reply( $comment ) );
+
+		$activities_template->activity->current_comment->depth = 4;
+		$this->assertFalse( bp_activity_can_comment_reply( $comment ) );
+
+		$activities_template->activity->current_comment->depth = 5;
+		$this->assertFalse( bp_activity_can_comment_reply( $comment ) );
+
+		// Set right what once went wrong
+		update_option( 'thread_comments', $tc );
+		update_option( 'thread_comments_depth', $tcd );
+		$activities_template = null;
+	}
+
+	/**
+	 * @group bp_activity_can_comment_reply
+	 */
+	public function test_bp_activity_can_comment_reply_thread_comments_off() {
+		$tc = get_option( 'thread_comments' );
+		update_option( 'thread_comments', '0' );
+
+		$tcd = get_option( 'thread_comments_depth' );
+		update_option( 'thread_comments_depth', '4' );
+
+		// Fake the global
+		global $activities_template;
+		$activities_template = new stdClass;
+		$activities_template->activity = new stdClass;
+		$activities_template->activity->current_comment = new stdClass;
+
+		$comment = new stdClass;
+		$comment->item_id = 4;
+
+		$activities_template->activity->current_comment->depth = 1;
+		$this->assertFalse( bp_activity_can_comment_reply( $comment ) );
+
+		$activities_template->activity->current_comment->depth = 2;
+		$this->assertFalse( bp_activity_can_comment_reply( $comment ) );
+
+		// Set right what once went wrong
+		update_option( 'thread_comments', $tc );
+		update_option( 'thread_comments_depth', $tcd );
+		$activities_template = null;
+	}
+
+	/**
+	 * @group bp_activity_can_comment
+	 */
+	public function test_bp_activity_can_comment() {
+		global $activities_template;
+		$reset_activities_template = $activities_template;
+
+		$activities_template = new stdClass;
+		$activities_template->disable_blogforum_replies = true;
+		$activities_template->activity = (object) array( 'type' => 'activity_comment' );
+
+		$this->assertFalse( bp_activity_can_comment(), 'bp_activity_can_comment() should return false if the activity type is activity_comment' );
+
+		$types = array(
+			'new_blog_post',
+			'new_blog_comment',
+			'new_forum_topic',
+			'new_forum_post'
+		);
+
+		foreach ( $types as $type_false ) {
+			$activities_template->activity->type = $type_false;
+			$this->assertFalse( bp_activity_can_comment(), 'Comments about blog or forum posts/replies are disabled' );
+		}
+
+		$activities_template->disable_blogforum_replies = false;
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		foreach ( $types as $type_true ) {
+			$activities_template->activity->type = $type_true;
+			$this->assertTrue( bp_activity_can_comment(), 'Comments about blog or forum posts/replies are enabled' );
+		}
+
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// clean up!
+		$activities_template = $reset_activities_template;
+	}
+
+	/**
+	 * @group bp_activity_can_comment
+	 */
+	public function test_bp_activity_can_comment_post_type_activity() {
+		global $activities_template;
+		$bp = buddypress();
+
+		$reset_activities_template = $activities_template;
+		$reset_activity_track = $bp->activity->track;
+
+		$activities_template = new stdClass;
+		$activities_template->disable_blogforum_replies = true;
+
+		register_post_type( 'foo', array(
+			'label'   => 'foo',
+			'public'   => true,
+			'supports' => array( 'buddypress-activity' ),
+		) );
+
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+
+		$activities_template->activity = (object) array( 'type' => 'new_foo' );
+
+		$this->assertTrue( bp_activity_can_comment(), 'If post type does not support comments, a post type activity can be commented' );
+
+		add_post_type_support( 'foo', 'comments' );
+
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+
+		$this->assertFalse( bp_activity_can_comment(), 'If post type support comments, a post type activity cannot be commented' );
+
+		$bp_activity_support = (array) $bp->activity->track['new_foo'];
+		$bp_activity_support['activity_comment'] = true;
+
+		bp_activity_set_post_type_tracking_args( 'foo', $bp_activity_support );
+		$bp->activity->track = bp_activity_get_post_types_tracking_args();
+
+		$this->assertTrue( bp_activity_can_comment(), 'If post type supports activity comments, a post type activity can be commented' );
+
+		// clean up!
+		$activities_template = $reset_activities_template;
+		$bp->activity->track = $reset_activity_track;
+	}
+
+	/**
+	 * @group bp_activity_has_more_items
+	 */
+	public function test_bp_activity_has_more_items_no_count_total_false() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+
+		$args = array(
+			'count_total' => false,
+		);
+
+		if ( bp_has_activities( $args ) ) {
+			global $activities_template;
+			$this->assertFalse( bp_activity_has_more_items() );
+			$activities_template = null;
+		}
+	}
+
+	/**
+	 * @group bp_activity_has_more_items
+	 */
+	public function test_bp_activity_has_more_items_no_count_total_true() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		$a3 = self::factory()->activity->create();
+		$a4 = self::factory()->activity->create();
+
+		$args = array(
+			'count_total' => false,
+			'per_page' => 2,
+		);
+
+		if ( bp_has_activities( $args ) ) {
+			global $activities_template;
+			$this->assertTrue( bp_activity_has_more_items() );
+			$activities_template = null;
+		}
+	}
+
+	/**
+	 * @group bp_activity_has_more_items
+	 */
+	public function test_bp_activity_has_more_items_count_total_false() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		$a3 = self::factory()->activity->create();
+		$a4 = self::factory()->activity->create();
+
+		$args = array(
+			'count_total' => 'count_query',
+		);
+
+		if ( bp_has_activities( $args ) ) {
+			global $activities_template;
+			$this->assertFalse( bp_activity_has_more_items() );
+			$activities_template = null;
+		}
+	}
+
+	/**
+	 * @group bp_activity_has_more_items
+	 */
+	public function test_bp_activity_has_more_items_count_total_true() {
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create();
+		$a3 = self::factory()->activity->create();
+		$a4 = self::factory()->activity->create();
+
+		$args = array(
+			'count_total' => 'count_query',
+			'per_page' => 2,
+		);
+
+		if ( bp_has_activities( $args ) ) {
+			global $activities_template;
+			$this->assertTrue( bp_activity_has_more_items() );
+			$activities_template = null;
+		}
+	}
+
+	/**
+	 * Integration test for 'date_query' param
+	 *
+	 * @group date_query
+	 * @requires PHP 5.3
+	 */
+	function test_bp_has_activities_with_date_query() {
+		if ( ! class_exists( 'WP_Date_Query' ) ) {
+			return;
+		}
+
+		$a1 = self::factory()->activity->create();
+		$a2 = self::factory()->activity->create( array(
+			'recorded_time' => '2001-01-01 12:00'
+		) );
+		$a3 = self::factory()->activity->create( array(
+			'recorded_time' => '2005-01-01 12:00'
+		) );
+
+		global $activities_template;
+		bp_has_activities( array(
+			'date_query' => array( array(
+				'after' => '1 day ago'
+			) )
+		) );
+
+		$ids = wp_list_pluck( $activities_template->activities, 'id' );
+		$this->assertEquals( $ids, array( $a1 ) );
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Activity_Template
+	 */
+	public function test_bp_activity_template_should_give_precedence_to_acpage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['acpage'] = '5';
+
+		$at = new BP_Activity_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Activity_Template
+	 */
+	public function test_bp_activity_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['acpage'] = '0';
+
+		$at = new BP_Activity_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Activity_Template
+	 */
+	public function test_bp_activity_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Activity_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Activity_Template
+	 */
+	public function test_bp_activity_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Activity_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/admin/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/admin/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/admin/functions.php	(revision 12265)
@@ -0,0 +1,247 @@
+<?php
+/**
+ * @group admin
+ */
+class BP_Tests_Admin_Functions extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
+
+		if ( ! function_exists( 'bp_admin' ) ) {
+			require_once( BP_PLUGIN_DIR . 'bp-core/bp-core-admin.php' );
+		}
+
+		if ( ! function_exists( 'bp_new_site' ) ) {
+			bp_admin();
+		}
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	public function test_bp_admin_list_table_current_bulk_action() {
+		$_REQUEST['action'] = 'foo';
+		$_REQUEST['action2'] = '-1';
+		$this->assertEquals( bp_admin_list_table_current_bulk_action(), 'foo' );
+
+		$_REQUEST['action'] = '-1';
+		$_REQUEST['action2'] = 'foo';
+		$this->assertEquals( bp_admin_list_table_current_bulk_action(), 'foo' );
+
+		$_REQUEST['action'] = 'bar';
+		$_REQUEST['action2'] = 'foo';
+		$this->assertEquals( bp_admin_list_table_current_bulk_action(), 'foo' );
+	}
+
+	/**
+	 * @group bp_core_admin_get_active_components_from_submitted_settings
+	 */
+	public function test_bp_core_admin_get_active_components_from_submitted_settings() {
+		$get_action = isset( $_GET['action'] ) ? $_GET['action'] : null;
+		$ac = buddypress()->active_components;
+
+		// Standard deactivation from All screen
+		unset( $_GET['action'] );
+		buddypress()->active_components = array(
+			'activity' => 1,
+			'friends' => 1,
+			'groups' => 1,
+			'members' => 1,
+			'messages' => 1,
+			'settings' => 1,
+			'xprofile' => 1,
+		);
+
+		$submitted = array(
+			'groups' => 1,
+			'members' => 1,
+			'messages' => 1,
+			'settings' => 1,
+			'xprofile' => 1,
+		);
+
+		$this->assertEquals( bp_core_admin_get_active_components_from_submitted_settings( $submitted ), array( 'groups' => 1, 'members' => 1, 'messages' => 1, 'settings' => 1, 'xprofile' => 1 ) );
+
+		// Activating deactivated components from the Inactive screen
+		$_GET['action'] = 'inactive';
+		buddypress()->active_components = array(
+			'activity' => 1,
+			'members' => 1,
+			'messages' => 1,
+			'settings' => 1,
+			'xprofile' => 1,
+		);
+
+		$submitted2 = array(
+			'groups' => 1,
+		);
+
+		$this->assertEquals( bp_core_admin_get_active_components_from_submitted_settings( $submitted2 ), array( 'activity' => 1, 'groups' => 1, 'members' => 1, 'messages' => 1, 'settings' => 1, 'xprofile' => 1 ) );
+
+		// Deactivating from the Retired screen
+		$_GET['action'] = 'retired';
+		buddypress()->active_components = array(
+			'activity' => 1,
+			'members' => 1,
+			'messages' => 1,
+			'settings' => 1,
+			'xprofile' => 1,
+		);
+
+		$submitted4 = array();
+
+		$this->assertEquals( bp_core_admin_get_active_components_from_submitted_settings( $submitted4 ), array( 'activity' => 1, 'members' => 1, 'messages' => 1, 'settings' => 1, 'xprofile' => 1 ) );
+
+		// reset
+		if ( $get_action ) {
+			$_GET['action'] = $get_action;
+		} else {
+			unset( $_GET['action'] );
+		}
+
+		buddypress()->active_components = $ac;
+	}
+
+	/**
+	 * @group BP6244
+	 * @group bp_core_admin_get_active_components_from_submitted_settings
+	 */
+	public function test_bp_core_admin_get_active_components_from_submitted_settings_should_keep_custom_component_directory_page() {
+		$bp = buddypress();
+		$reset_active_components = $bp->active_components;
+
+		// Create and activate the foo component
+		$bp->foo = new BP_Component;
+		$bp->foo->id   = 'foo';
+		$bp->foo->slug = 'foo';
+		$bp->foo->name = 'Foo';
+		$bp->active_components[ $bp->foo->id ] = 1;
+		$new_page_ids = array( $bp->foo->id => self::factory()->post->create( array(
+			'post_type'  => 'page',
+			'post_title' => $bp->foo->name,
+			'post_name'  => $bp->foo->slug,
+		) ) );
+
+		$page_ids = array_merge( $new_page_ids, bp_core_get_directory_page_ids( 'all' ) );
+		bp_core_update_directory_page_ids( $page_ids );
+
+		$bp->active_components = bp_core_admin_get_active_components_from_submitted_settings( $reset_active_components );
+		bp_core_add_page_mappings( $bp->active_components );
+
+		$this->assertContains( $bp->foo->id, array_keys( bp_core_get_directory_page_ids( 'all' ) ) );
+
+		// Reset buddypress() vars
+		$bp->active_components = $reset_active_components;
+	}
+
+	/**
+	 * @group bp_core_activation_notice
+	 */
+	public function test_bp_core_activation_notice_register_activate_pages_notcreated_signup_allowed() {
+		$bp = buddypress();
+		$reset_bp_pages = $bp->pages;
+		$reset_admin_notices = $bp->admin->notices;
+
+		// Reset pages
+		$bp->pages = bp_core_get_directory_pages();
+
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		bp_core_activation_notice();
+
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$missing_pages = array();
+		foreach( buddypress()->admin->notices as $notice ) {
+			preg_match_all( '/<strong>(.+?)<\/strong>/', $notice['message'], $missing_pages );
+		}
+
+		$this->assertContains( 'Register', $missing_pages[1] );
+		$this->assertContains( 'Activate', $missing_pages[1] );
+
+		// Reset buddypress() vars
+		$bp->pages = $reset_bp_pages;
+		$bp->admin->notices = $reset_admin_notices;
+	}
+
+	/**
+	 * @group bp_core_activation_notice
+	 */
+	public function test_bp_core_activation_notice_register_activate_pages_created_signup_allowed() {
+		$bp = buddypress();
+		$reset_bp_pages = $bp->pages;
+		$reset_admin_notices = $bp->admin->notices;
+
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+
+		// Reset pages
+		$bp->pages = bp_core_get_directory_pages();
+
+		bp_core_activation_notice();
+
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$missing_pages = array();
+		foreach( buddypress()->admin->notices as $notice ) {
+			if ( false !== strpos( $notice['message'], 'BuddyPress is almost ready' ) ) {
+				continue;
+			}
+
+			preg_match_all( '/<strong>(.+?)<\/strong>/', $notice['message'], $missing_pages );
+		}
+
+		$this->assertEmpty( $missing_pages );
+
+		// Reset buddypress() vars
+		$bp->pages = $reset_bp_pages;
+		$bp->admin->notices = $reset_admin_notices;
+	}
+
+	/**
+	 * @ticket BP6936
+	 */
+	public function test_email_type_descriptions_should_match_when_split_terms_exist() {
+		global $wpdb;
+
+		// Delete all existing email types and descriptions.
+		$emails = get_posts( array(
+			'fields' => 'ids',
+			'post_type' => bp_get_email_post_type(),
+		) );
+		foreach ( $emails as $email ) {
+			wp_delete_post( $email, true );
+		}
+
+		$descriptions = get_terms( bp_get_email_tax_type(), array(
+			'fields' => 'ids',
+			'hide_empty' => false,
+		) );
+		foreach ( $descriptions as $description ) {
+			wp_delete_term( (int) $description, bp_get_email_tax_type() );
+		}
+
+		// Fake the existence of split terms by offsetting the term_taxonomy table.
+		$wpdb->insert( $wpdb->term_taxonomy, array( 'term_id' => 9999, 'taxonomy' => 'post_tag', 'description' => 'foo description', 'parent' => 0, 'count' => 0 ) );
+
+		require_once( BP_PLUGIN_DIR . '/bp-core/admin/bp-core-admin-schema.php' );
+		bp_core_install_emails();
+
+		$d_terms = get_terms( bp_get_email_tax_type(), array(
+			'hide_empty' => false,
+		) );
+
+		$correct_descriptions = bp_email_get_type_schema( 'description' );
+		foreach ( $d_terms as $d_term ) {
+			$correct_description = $correct_descriptions[ $d_term->slug ];
+			$this->assertSame( $correct_description, $d_term->description );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/activity.php	(revision 12265)
@@ -0,0 +1,787 @@
+<?php
+
+class BP_Tests_Blogs_Activity extends BP_UnitTestCase {
+	/**
+	 * @group bp_blogs_register_activity_actions
+	 * @group activity_tracking
+	 */
+	public function test_bp_blogs_loader_post_tracking_args_filter() {
+		$bp = buddypress();
+
+		$expected = array( 'new_blog_post', 'new_blog_comment' );
+
+		if ( is_multisite() ) {
+			$expected = array_merge( array( 'new_blog' ), $expected );
+		}
+
+		$actions = bp_activity_get_actions();
+		$actions = array_keys( (array) $actions->blogs );
+
+		$this->assertEquals( $expected, $actions );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog',
+			'user_id' => $u,
+			'item_id' => $b,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$expected = sprintf( '%s created the site %s', bp_core_get_userlink( $u ), '<a href="' . get_blog_option( $b, 'home' ) . '">' . get_blog_option( $b, 'blogname' ) . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog_post
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog_post_nonms() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$u = self::factory()->user->create();
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+		) );
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog_post',
+			'user_id' => $u,
+			'item_id' => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_home_url();
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_title = bp_activity_get_meta( $a, 'post_title' );
+		$post_link = '<a href="' . $post_url . '">' . $post_title . '</a>';
+
+		$expected = sprintf( '%s wrote a new post, %s', $user_link, $post_link );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog_post
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog_post_ms_rootblog() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$u = self::factory()->user->create();
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+		) );
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog_post',
+			'user_id' => $u,
+			'item_id' => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_home_url();
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_title = bp_activity_get_meta( $a, 'post_title' );
+		$post_link = '<a href="' . $post_url . '">' . $post_title . '</a>';
+
+		$expected = sprintf( '%s wrote a new post, %s, on the site %s', $user_link, $post_link, '<a href="' . $blog_url . '">' . bp_blogs_get_blogmeta( $a_obj->item_id, 'name' ) . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog_post
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog_post_ms_nonrootblog() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		switch_to_blog( $b );
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+		) );
+		$p_obj = get_post( $p );
+		restore_current_blog();
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog_post',
+			'user_id' => $u,
+			'item_id' => $b,
+			'secondary_item_id' => $p,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_blog_option( $a_obj->item_id, 'home' );
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_title = $p_obj->post_title;
+		$post_link = '<a href="' . $post_url . '">' . $post_title . '</a>';
+
+		$expected = sprintf( '%s wrote a new post, %s, on the site %s', $user_link, $post_link, '<a href="' . $blog_url . '">' . get_blog_option( $a_obj->item_id, 'blogname' ) . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog_comment_ms_nonrootblog() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		switch_to_blog( $b );
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+		) );
+		$p_obj = get_post( $p );
+		$c = self::factory()->comment->create( array(
+			'comment_post_ID' => $p,
+		) );
+		$c_obj = get_comment( $c );
+		restore_current_blog();
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog_comment',
+			'user_id' => $u,
+			'item_id' => $b,
+			'secondary_item_id' => $c,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_blog_option( $a_obj->item_id, 'home' );
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_title = $p_obj->post_title;
+		$post_link = '<a href="' . $post_url . '">' . $post_title . '</a>';
+
+		$expected = sprintf( '%s commented on the post, %s, on the site %s', $user_link, $post_link, '<a href="' . $blog_url . '">' . get_blog_option( $a_obj->item_id, 'blogname' ) . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group bp_blogs_format_activity_action_new_blog
+	 */
+	public function test_bp_activity_format_activity_action_new_blog_backpat() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		add_filter( 'bp_blogs_activity_created_blog_action', array( $this, 'created_blog_passthrough' ), 10, 2 );
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		$recorded_blog          = new BP_Blogs_Blog;
+		$recorded_blog->user_id = $u;
+		$recorded_blog->blog_id = $b;
+		$recorded_blog_id       = $recorded_blog->save();
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog',
+			'user_id' => $u,
+			'item_id' => $b,
+		) );
+
+		$this->assertEquals( $this->userblog_id, $recorded_blog_id );
+	}
+
+	/**
+	 * @group bp_blogs_format_activity_action_new_blog_post
+	 */
+	public function test_bp_activity_format_activity_action_new_blog_post_backpat() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		add_filter( 'bp_blogs_activity_new_post_action', array( $this, 'new_post_passthrough' ), 10, 2 );
+
+		$b = self::factory()->blog->create();
+
+		switch_to_blog( $b );
+		$p = self::factory()->post->create();
+		restore_current_blog();
+
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog_post',
+			'user_id' => $u,
+			'item_id' => $b,
+			'secondary_item_id' => $p,
+		) );
+
+		$this->assertEquals( $this->post_id, $p );
+	}
+
+	/**
+	 * @group bp_blogs_format_activity_action_new_blog_comment
+	 */
+	public function test_bp_activity_format_activity_action_new_blog_comment_backpat() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		add_filter( 'bp_blogs_activity_new_comment_action', array( $this, 'new_comment_passthrough' ), 10, 2 );
+
+		$b = self::factory()->blog->create();
+
+		switch_to_blog( $b );
+		$p = self::factory()->post->create();
+		$c = self::factory()->comment->create( array(
+			'comment_post_ID' => $p,
+		) );
+		restore_current_blog();
+
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->blogs->id,
+			'type' => 'new_blog_comment',
+			'user_id' => $u,
+			'item_id' => $b,
+			'secondary_item_id' => $c,
+		) );
+
+		$this->assertEquals( $this->comment_post_id, $p );
+	}
+
+	/**
+	 * @ticket BP6126
+	 */
+	public function test_check_activity_actions_are_set_when_creating_activity_object() {
+		$bp = buddypress();
+
+		if ( isset( $bp->activity->actions ) ) {
+			unset( $bp->activity->actions );
+		}
+
+		$u = self::factory()->user->create();
+		$p = self::factory()->post->create( array( 'post_author' => $u, ) );
+		$a = self::factory()->activity->create( array(
+			'component'         => buddypress()->blogs->id,
+			'item_id'           => 1,
+			'secondary_item_id' => $p,
+			'type'              => 'new_blog_post',
+			'user_id'           => $u,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+		$this->assertTrue( ! empty( $a_obj->action ) );
+
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog_post
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog_post_no_title() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		buddypress()->activity->actions = new stdClass();
+
+		$u = self::factory()->user->create();
+		$p = wp_insert_post( array(
+			'post_author' => $u,
+			'post_title'  => '', // no title: the object of the test
+			'post_status' => 'publish',
+			'post_content' => 'foo bar',
+		) );
+
+		$user_link = bp_core_get_userlink( $u );
+		$blog_url = get_home_url();
+		$post_url = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_link = '<a href="' . $post_url . '">(no title)</a>';
+
+		// Set activity actions
+		bp_activity_get_actions();
+
+		$a_obj = bp_activity_get( array(
+			'item_id'           => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$expected = sprintf( '%s wrote a new post, %s', $user_link, $post_link );
+
+		$this->assertSame( $expected, $a_obj['activities'][0]->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_blogs_format_activity_action_new_blog_post
+	 */
+	public function test_bp_blogs_format_activity_action_new_blog_post_updated_without_title() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		buddypress()->activity->actions = new stdClass();
+
+		$u = self::factory()->user->create();
+		$p = wp_insert_post( array(
+			'post_author' => $u,
+			'post_title'  => 'foo',
+			'post_status' => 'publish',
+			'post_content' => 'foo bar',
+		) );
+
+		$user_link  = bp_core_get_userlink( $u );
+		$blog_url   = get_home_url();
+		$post_url   = add_query_arg( 'p', $p, trailingslashit( $blog_url ) );
+		$post_title = get_the_title( $p );
+		$post_link  = '<a href="' . $post_url . '">' . $post_title . '</a>';
+
+		// Set actions
+		bp_activity_get_actions();
+
+		$a_obj = bp_activity_get( array(
+			'item_id'           => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$expected = sprintf( '%s wrote a new post, %s', $user_link, $post_link );
+
+		$this->assertSame( $expected, $a_obj['activities'][0]->action );
+
+		// Update the post by removing its title
+		wp_update_post( array(
+			'ID'         => $p,
+			'post_title' => '',
+		) );
+
+		// we now expect the (no title) post link
+		$post_link = '<a href="' . $post_url . '">(no title)</a>';
+		$expected = sprintf( '%s wrote a new post, %s', $user_link, $post_link );
+
+		$a_obj = bp_activity_get( array(
+			'item_id'           => 1,
+			'secondary_item_id' => $p,
+		) );
+
+		$this->assertSame( $expected, $a_obj['activities'][0]->action );
+	}
+
+	/**
+	 * @group bp_blogs_sync_add_from_activity_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_sync_add_from_activity_comment() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test activity comment to post comment',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'this content shoud be in a new post comment',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		$approved_comments = get_approved_comments( $post_id );
+		$comment = reset( $approved_comments );
+
+		$this->assertTrue( (int) $comment->comment_ID === (int) bp_activity_get_meta( $a2, 'bp_blogs_post_comment_id' ), 'The comment ID should be in the activity meta' );
+		$this->assertTrue( (int) $a2 === (int) get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true ), 'The activity ID should be in the comment meta' );
+
+		// reset
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_sync_delete_from_activity_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_sync_delete_from_activity_comment() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test activity comment to post comment',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'the generated comment should be deleted once the activity comment is removed',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		bp_activity_delete_comment( $a1, $a2 );
+
+		$post_comments = get_comments( array( 'post_id' => $post_id ) );
+
+		$this->assertEmpty( $post_comments, 'A post comment should be deleted when the corresponding activity is' );
+
+		// reset
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_sync_activity_edit_to_post_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_sync_activity_edit_to_post_comment_spam_unspam_activity_comment() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test activity comment to post comment',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'the generated comment should be spamed/unspamed once the activity comment is spamed/unspamed',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		$activity = new BP_Activity_Activity( $a2 );
+
+		bp_activity_mark_as_spam( $activity );
+		$activity->save();
+
+		$post_comments = get_comments( array( 'post_id' => $post_id, 'status' => 'approve' ) );
+
+		$this->assertEmpty( $post_comments, 'A post comment should be spammed when the corresponding activity is spammed' );
+
+		bp_activity_mark_as_ham( $activity );
+		$activity->save();
+
+		$post_comments = get_comments( array( 'post_id' => $post_id, 'status' => 'approve' ) );
+		$comment = reset( $post_comments );
+
+		$this->assertTrue( (int) $comment->comment_ID === (int) bp_activity_get_meta( $a2, 'bp_blogs_post_comment_id' ), 'The comment ID should be in the activity meta' );
+		$this->assertTrue( (int) $a2 === (int) get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true ), 'The activity ID should be in the comment meta' );
+
+		// reset
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_sync_activity_edit_to_post_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_sync_activity_edit_to_post_comment_spam_activity_comment_unspam_post_comment() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test activity comment to post comment',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'the generated comment should be spamed/unspamed once the activity comment is spamed/unspamed',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		$c = bp_activity_get_meta( $a2, 'bp_blogs_post_comment_id' );
+
+		$activity = new BP_Activity_Activity( $a2 );
+
+		bp_activity_mark_as_spam( $activity );
+		$activity->save();
+
+		wp_unspam_comment( $c );
+
+		$post_comments = get_comments( array( 'post_id' => $post_id, 'status' => 'approve' ) );
+		$comment = reset( $post_comments );
+
+		$this->assertTrue( (int) $comment->comment_ID === (int) bp_activity_get_meta( $a2, 'bp_blogs_post_comment_id' ), 'The comment ID should be in the activity meta' );
+		$this->assertTrue( (int) $a2 === (int) get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true ), 'The activity ID should be in the comment meta' );
+
+		// reset
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_sync_activity_edit_to_post_comment
+	 * @group post_type_comment_activities
+	 * @group imath
+	 */
+	public function test_bp_blogs_sync_activity_edit_to_post_comment_trash_comment_ham_activity() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test activity comment to post comment',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'the generated comment should be spamed/unspamed once the activity comment is spamed/unspamed',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		$c = bp_activity_get_meta( $a2, 'bp_blogs_post_comment_id' );
+
+		wp_trash_comment( $c );
+
+		$activity = new BP_Activity_Activity( $a2 );
+
+		bp_activity_mark_as_ham( $activity );
+		$activity->save();
+
+		$post_comments = get_comments( array( 'post_id' => $post_id, 'status' => 'approve' ) );
+		$comment = reset( $post_comments );
+
+		$this->assertTrue( (int) $comment->comment_ID === (int) bp_activity_get_meta( $a2, 'bp_blogs_post_comment_id' ), 'The comment ID should be in the activity meta' );
+		$this->assertTrue( (int) $a2 === (int) get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true ), 'The activity ID should be in the comment meta' );
+
+		// reset
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_sync_activity_edit_to_post_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_spammed_activity_comment_should_not_create_post_comment() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items.
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// create the blog post.
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test activity comment to post comment',
+		) );
+
+		// Grab the activity ID for the activity comment.
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		// Set activity item to spam.
+		add_action( 'bp_activity_before_save', array( $this, 'set_activity_to_spam' ) );
+
+		// Create spammed activity comment.
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'this activity comment shoud not be created as a new post comment. yolo.',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		// Grab post comments.
+		$approved_comments = get_approved_comments( $post_id );
+		$comment = reset( $approved_comments );
+
+		// Assert that post comment wasn't created.
+		$this->assertEmpty( $comment );
+
+		// Reset.
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+		remove_action( 'bp_activity_before_save', array( $this, 'set_activity_to_spam' ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * Dopey passthrough method so we can check that the correct values
+	 * are being passed to the filter
+	 */
+	public function created_blog_passthrough( $a, $b ) {
+		$this->userblog_id = isset( $b->id ) ? $b->id : '';
+		return $a;
+	}
+
+	/**
+	 * Dopey passthrough method so we can check that the correct values
+	 * are being passed to the filter
+	 */
+	public function new_post_passthrough( $a, $b ) {
+		$this->post_id = isset( $b->ID ) ? $b->ID : '';
+		return $a;
+	}
+
+	/**
+	 * Dopey passthrough method so we can check that the correct values
+	 * are being passed to the filter
+	 */
+	public function new_comment_passthrough( $a, $b ) {
+		$this->comment_post_id = isset( $b->comment_post_ID ) ? $b->comment_post_ID : '';
+		return $a;
+	}
+
+	/**
+	 * Explicitly set activity to spam.
+	 */
+	public function set_activity_to_spam( $activity ) {
+		$activity->is_spam = 1;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/cache.php	(revision 12265)
@@ -0,0 +1,338 @@
+<?php
+
+/**
+ * @group blogs
+ * @group cache
+ */
+class BP_Tests_Blogs_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_blogs_update_meta_cache
+	 */
+	public function test_bp_blogs_update_meta_cache() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b1 = self::factory()->blog->create();
+		$b2 = self::factory()->blog->create();
+
+		bp_blogs_add_blogmeta( $b1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( $b1, 'foo2', 'bar2' );
+		bp_blogs_add_blogmeta( $b2, 'foo', 'bar' );
+
+		// Need this info
+		$b1_name = bp_blogs_get_blogmeta( $b1, 'name' );
+		$b1_description = bp_blogs_get_blogmeta( $b1, 'description' );
+		$b1_last_activity = bp_blogs_get_blogmeta( $b1, 'last_activity' );
+
+		$b2_name = bp_blogs_get_blogmeta( $b2, 'name' );
+		$b2_description = bp_blogs_get_blogmeta( $b2, 'description' );
+		$b2_last_activity = bp_blogs_get_blogmeta( $b2, 'last_activity' );
+
+		// Clear caches (due to _get_)
+		wp_cache_delete( $b1, 'bp_blog_meta' );
+		wp_cache_delete( $b2, 'bp_blog_meta' );
+
+		// Caches should be empty
+		$this->assertFalse( wp_cache_get( $b1, 'bp_blog_meta' ) );
+		$this->assertFalse( wp_cache_get( $b2, 'bp_blog_meta' ) );
+
+		bp_blogs_update_meta_cache( array( $b1, $b2 ) );
+
+		$b1_expected = array(
+			'name' => array(
+				$b1_name,
+			),
+			'description' => array(
+				$b1_description,
+			),
+			'last_activity' => array(
+				$b1_last_activity,
+			),
+			'foo' => array(
+				'bar',
+			),
+			'foo2' => array(
+				'bar2',
+			),
+		);
+
+		$b2_expected = array(
+			'name' => array(
+				$b2_name,
+			),
+			'description' => array(
+				$b2_description,
+			),
+			'last_activity' => array(
+				$b2_last_activity,
+			),
+			'foo' => array(
+				'bar',
+			),
+		);
+
+		// The cache may contain more than just this, so loop through
+		// and check only relevant keys
+		$b1_found = wp_cache_get( $b1, 'bp_blog_meta' );
+		foreach ( $b1_expected as $k => $v ) {
+			$this->assertSame( $v, $b1_found[ $k ] );
+		}
+
+		$b2_found = wp_cache_get( $b2, 'bp_blog_meta' );
+		foreach ( $b2_expected as $k => $v ) {
+			$this->assertSame( $v, $b2_found[ $k ] );
+		}
+	}
+
+	/**
+	 * @group bp_blogs_update_meta_cache
+	 * @group bp_has_blogs
+	 */
+	public function test_bp_blogs_update_meta_cache_bp_has_blogs() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$u = self::factory()->user->create();
+
+		// Switch user so we have access to non-public blogs
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$b1 = self::factory()->blog->create();
+		$b2 = self::factory()->blog->create();
+
+		bp_blogs_record_blog( $b1, $u, true );
+		bp_blogs_record_blog( $b2, $u, true );
+
+		bp_blogs_add_blogmeta( $b1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( $b1, 'foo2', 'bar2' );
+		bp_blogs_add_blogmeta( $b2, 'foo', 'bar' );
+
+		// Need this info
+		$b1_name = bp_blogs_get_blogmeta( $b1, 'name' );
+		$b1_description = bp_blogs_get_blogmeta( $b1, 'description' );
+		$b1_last_activity = bp_blogs_get_blogmeta( $b1, 'last_activity' );
+
+		$b2_name = bp_blogs_get_blogmeta( $b2, 'name' );
+		$b2_description = bp_blogs_get_blogmeta( $b2, 'description' );
+		$b2_last_activity = bp_blogs_get_blogmeta( $b2, 'last_activity' );
+
+		// Clear caches (due to _get_)
+		wp_cache_delete( $b1, 'bp_blog_meta' );
+		wp_cache_delete( $b2, 'bp_blog_meta' );
+
+		// Caches should be empty
+		$this->assertFalse( wp_cache_get( $b1, 'bp_blog_meta' ) );
+		$this->assertFalse( wp_cache_get( $b2, 'bp_blog_meta' ) );
+
+		bp_has_blogs( array(
+			'user_id' => $u,
+		) );
+
+		$b1_expected = array(
+			'name' => array(
+				$b1_name,
+			),
+			'description' => array(
+				$b1_description,
+			),
+			'last_activity' => array(
+				$b1_last_activity,
+			),
+			'foo' => array(
+				'bar',
+			),
+			'foo2' => array(
+				'bar2',
+			),
+		);
+
+		$b2_expected = array(
+			'name' => array(
+				$b2_name,
+			),
+			'description' => array(
+				$b2_description,
+			),
+			'last_activity' => array(
+				$b2_last_activity,
+			),
+			'foo' => array(
+				'bar',
+			),
+		);
+
+		// The cache may contain more than just this, so loop through
+		// and check only relevant keys
+		$b1_found = wp_cache_get( $b1, 'bp_blog_meta' );
+		foreach ( $b1_expected as $k => $v ) {
+			$this->assertSame( $v, $b1_found[ $k ] );
+		}
+
+		$b2_found = wp_cache_get( $b2, 'bp_blog_meta' );
+		foreach ( $b2_expected as $k => $v ) {
+			$this->assertSame( $v, $b2_found[ $k ] );
+		}
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_update_meta_cache
+	 * @group bp_has_blogs
+	 */
+	public function test_bp_blogs_update_meta_cache_bp_has_blogs_false() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$u = self::factory()->user->create();
+
+		// Switch user so we have access to non-public blogs
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$b1 = self::factory()->blog->create();
+		$b2 = self::factory()->blog->create();
+
+		bp_blogs_record_blog( $b1, $u, true );
+		bp_blogs_record_blog( $b2, $u, true );
+
+		bp_blogs_add_blogmeta( $b1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( $b1, 'foo2', 'bar2' );
+		bp_blogs_add_blogmeta( $b2, 'foo', 'bar' );
+
+		// Need this info
+		$b1_name = bp_blogs_get_blogmeta( $b1, 'name' );
+		$b1_description = bp_blogs_get_blogmeta( $b1, 'description' );
+		$b1_last_activity = bp_blogs_get_blogmeta( $b1, 'last_activity' );
+
+		$b2_name = bp_blogs_get_blogmeta( $b2, 'name' );
+		$b2_description = bp_blogs_get_blogmeta( $b2, 'description' );
+		$b2_last_activity = bp_blogs_get_blogmeta( $b2, 'last_activity' );
+
+		// Clear caches (due to _get_)
+		wp_cache_delete( $b1, 'bp_blog_meta' );
+		wp_cache_delete( $b2, 'bp_blog_meta' );
+
+		// Caches should be empty
+		$this->assertFalse( wp_cache_get( $b1, 'bp_blog_meta' ) );
+		$this->assertFalse( wp_cache_get( $b2, 'bp_blog_meta' ) );
+
+		bp_has_blogs( array(
+			'update_meta_cache' => false,
+		) );
+
+		// Caches should be empty
+		$this->assertFalse( wp_cache_get( $b1, 'bp_blog_meta' ) );
+		$this->assertFalse( wp_cache_get( $b2, 'bp_blog_meta' ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_total_blogs
+	 * @group counts
+	 */
+	public function test_bp_blogs_total_count_should_respect_cached_value_of_0() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		global $wpdb;
+
+		// prime cache
+		// no blogs are created by default, so count is zero
+		bp_blogs_total_blogs();
+		$first_query_count = $wpdb->num_queries;
+
+		// run function again
+		bp_blogs_total_blogs();
+
+		// check if function references cache or hits the DB by comparing query count
+		$this->assertEquals( $first_query_count, $wpdb->num_queries );
+	}
+
+	/**
+	 * @group bp_blogs_total_blogs
+	 */
+	public function test_bp_blogs_total_blogs_count_after_delete_blog() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			$this->setExpectedDeprecated( 'delete_blog' );
+		}
+
+		$u = self::factory()->user->create();
+
+		// need to make sure we set the 'public' flag due to how BP_Blogs_Blogs:get_all() works
+		$b1 = self::factory()->blog->create( array(
+			'meta' => array(
+				'public' => 1
+			)
+		) );
+		$b2 = self::factory()->blog->create( array(
+			'meta' => array(
+				'public' => 1
+			)
+		) );
+
+		bp_blogs_record_blog( $b1, $u );
+		bp_blogs_record_blog( $b2, $u );
+
+		// prime total blog count
+		bp_blogs_total_blogs();
+
+		// delete a blog
+		wpmu_delete_blog( $b2 );
+
+		$this->assertEquals( 1, bp_blogs_total_blogs() );
+	}
+
+	/**
+	 * @group update_blog_details
+	 */
+	public function test_update_blog_details_should_purge_blogmeta_cache() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$u = self::factory()->user->create();
+
+		$b1 = self::factory()->blog->create();
+		bp_blogs_record_blog( $b1, $u, true );
+
+		// prime cache
+		bp_blogs_get_blogmeta( $b1, 'url' );
+		$this->assertNotEmpty( wp_cache_get( $b1, 'bp_blog_meta' ) );
+
+		// updating blog details should purge cache
+		update_blog_details( $b1, array(
+			'domain' => 'awesome.com'
+		) );
+
+		// assert cache is purged
+		$this->assertEmpty( wp_cache_get( $b1, 'bp_blog_meta' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/class-bp-blogs-blog.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/class-bp-blogs-blog.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/class-bp-blogs-blog.php	(revision 12265)
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * @group blogs
+ * @group BP_Blogs_Blog
+ */
+class BP_Tests_BP_Blogs_Blog_TestCases extends BP_UnitTestCase {
+	public function test_get_with_search_terms() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$old_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$b = self::factory()->blog->create( array(
+			'title' => 'The Foo Bar Blog',
+			'user_id' => $u,
+		) );
+		bp_blogs_record_existing_blogs();
+
+		// make the blog public or it won't turn up in generic results
+		update_blog_option( $b, 'blog_public', '1' );
+
+		$blogs = BP_Blogs_Blog::get( 'active', false, false, 0, 'Foo' );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+
+		$this->assertEquals( array( $b ), $blog_ids );
+	}
+
+	/**
+	 * @ticket BP5858
+	 */
+	public function test_get_with_search_terms_should_match_description() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$old_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$b = self::factory()->blog->create( array(
+			'title' => 'The Foo Bar Blog',
+			'domain' => __METHOD__,
+			'user_id' => $u,
+		) );
+		update_blog_option( $b, 'blogdescription', 'Full of foorificness' );
+		bp_blogs_record_existing_blogs();
+
+		// make the blog public or it won't turn up in generic results
+		update_blog_option( $b, 'blog_public', '1' );
+
+		$blogs = BP_Blogs_Blog::get( 'active', false, false, 0, 'Full' );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+
+		$this->assertEquals( array( $b ), $blog_ids );
+		$this->assertEquals( 1, $blogs['total'] );
+	}
+
+	public function test_search_blogs() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$old_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$b = self::factory()->blog->create( array(
+			'title' => 'The Foo Bar Blog',
+			'user_id' => $u,
+			'path' => '/path' . rand() . time() . '/',
+		) );
+		bp_blogs_record_existing_blogs();
+
+		// make the blog public or it won't turn up in generic results
+		update_blog_option( $b, 'blog_public', '1' );
+
+		$blogs = BP_Blogs_Blog::search_blogs( 'Foo' );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+
+		$this->assertEquals( array( $b ), $blog_ids );
+	}
+
+	/**
+	 * @group get_by_letter
+	 */
+	public function test_get_by_letter() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+			return;
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$old_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$b = self::factory()->blog->create( array(
+			'title' => 'Foo Bar Blog',
+			'user_id' => $u,
+			'path' => '/path' . rand() . time() . '/',
+		) );
+		bp_blogs_record_existing_blogs();
+
+		// make the blog public or it won't turn up in generic results
+		update_blog_option( $b, 'blog_public', '1' );
+
+		$blogs = BP_Blogs_Blog::get_by_letter( 'F' );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+
+		$this->assertEquals( array( $b ), $blog_ids );
+	}
+
+	/**
+	 * @group get_order_by
+	 */
+	public function test_get_order_by() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$old_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$bs = array(
+			'foobar' => self::factory()->blog->create( array(
+				'title' => 'Foo Bar Blog',
+				'user_id' => $u,
+				'path' => '/path' . rand() . time() . '/',
+			) ),
+			'barfoo' => self::factory()->blog->create( array(
+				'title' => 'Bar foo Blog',
+				'user_id' => $u,
+				'path' => '/path' . rand() . time() . '/',
+			) ),
+		);
+
+		bp_blogs_record_existing_blogs();
+
+		// make the blog public or it won't turn up in generic results
+		foreach ( $bs as $b ) {
+			update_blog_option( $b, 'blog_public', '1' );
+		}
+
+		// Used to make sure barfoo is older than foobar
+		$b_time = date_i18n( 'Y-m-d H:i:s', strtotime( '-5 minutes' ) );
+
+		/* Alphabetical */
+		$blogs = BP_Blogs_Blog::get( 'alphabetical', false, false, $u );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+		$this->assertEquals( array( $bs['barfoo'], $bs['foobar'] ), $blog_ids );
+
+		/* Newest */
+		update_blog_details( $bs['barfoo'], array( 'registered' => $b_time ) );
+		$blogs = BP_Blogs_Blog::get( 'newest', false, false, $u );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+		$this->assertEquals( array( $bs['foobar'], $bs['barfoo'] ), $blog_ids );
+
+		/* Active */
+		bp_blogs_update_blogmeta( $bs['barfoo'], 'last_activity', $b_time );
+		$blogs = BP_Blogs_Blog::get( 'active', false, false, $u );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+		$this->assertEquals( array( $bs['foobar'],$bs['barfoo'] ), $blog_ids );
+
+		/* Random */
+		$blogs = BP_Blogs_Blog::get( 'random', false, false, $u );
+		$this->assertTrue( 2 == count( $blogs['blogs'] ) );
+
+		$this->set_current_user( $old_user );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/filters.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/filters.php	(revision 12265)
@@ -0,0 +1,135 @@
+<?php
+/**
+ * @group blogs
+ * @ticket BP6306
+ */
+class BP_Tests_Blogs_Filters extends BP_UnitTestCase {
+	protected $activity_actions;
+	protected $custom_post_types;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->custom_post_types = array( 'using_old_filter' );
+
+		register_post_type( 'using_old_filter', array(
+			'label'   => 'using_old_filter',
+			'public'   => true,
+			'supports' => array( 'comments' ),
+		) );
+
+		add_filter( 'bp_blogs_record_post_post_types',    array( $this, 'filter_post_types'), 10, 1 );
+		add_filter( 'bp_blogs_record_comment_post_types', array( $this, 'filter_post_types'), 10, 1 );
+	}
+
+	function tearDown() {
+		parent::tearDown();
+
+		$bp = buddypress();
+
+		_unregister_post_type( 'using_old_filter' );
+		remove_filter( 'bp_blogs_record_post_post_types',    array( $this, 'filter_post_types' ), 10 );
+		remove_filter( 'bp_blogs_record_comment_post_types', array( $this, 'filter_post_types' ), 10 );
+	}
+
+	/**
+	 * @goup bp_activity_get_actions
+	 */
+	public function test_bp_activity_get_actions() {
+		$activity_actions = bp_activity_get_actions();
+
+		$this->assertTrue( ! isset( $activity_actions->activity->new_using_old_filter ), 'Post types registering using the bp_blogs_record_post_post_types filter should not have a specific action' );
+	}
+
+	/**
+	 * @goup bp_activity_catch_transition_post_type_status
+	 */
+	public function test_bp_activity_catch_transition_post_type_status() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'using_old_filter',
+		) );
+
+		$this->assertTrue( $this->activity_exists_for_post_type( get_current_blog_id(), $post_id, 'new_blog_post' ), 'Generated activity for a post type registering using the bp_blogs_record_post_post_types filter should have a new_blog_post action' );
+	}
+
+	/**
+	 * @goup bp_blogs_record_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_record_comment() {
+		$u = self::factory()->user->create();
+		$user = self::factory()->user->get_object_by_id( $u );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'using_old_filter',
+			'post_author' => $u,
+		) );
+
+		$comment_id = self::factory()->comment->create( array(
+			'user_id'              => $u,
+			'comment_author_email' => $user->user_email,
+			'comment_post_ID'      => $post_id,
+		) );
+
+		// Approve the comment
+		self::factory()->comment->update_object( $comment_id, array( 'comment_approved' => 1 ) );
+
+		$this->assertTrue( $this->activity_exists_for_post_type( get_current_blog_id(), $comment_id, 'new_blog_comment' ), 'Generated activity for comments about a post type registering using the bp_blogs_record_post_post_types filter should have a new_blog_comment action' );
+	}
+
+	/**
+	 * @goup bp_blogs_record_comment_sync_activity_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_record_comment_sync_activity_comment() {
+		$u = self::factory()->user->create();
+		$user = self::factory()->user->get_object_by_id( $u );
+
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'using_old_filter',
+			'post_author' => $u,
+		) );
+
+		$parent_activity_id = bp_activity_get_activity_id( array(
+			'component'         => 'blogs',
+			'type'              => 'new_blog_post',
+			'item_id'           => get_current_blog_id(),
+			'secondary_item_id' => $post_id
+		) );
+
+		$comment_id = self::factory()->comment->create( array(
+			'user_id'              => $u,
+			'comment_author_email' => $user->user_email,
+			'comment_post_ID'      => $post_id,
+		) );
+
+		// Approve the comment
+		self::factory()->comment->update_object( $comment_id, array( 'comment_approved' => 1 ) );
+
+		$this->assertTrue( $this->activity_exists_for_post_type( $parent_activity_id, '', 'activity_comment', 'stream' ), 'Generated activity for comments about a post type registering using the bp_blogs_record_post_post_types filter having sync on should have a activity_comment action' );
+
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+	}
+
+	public function filter_post_types( $post_types ) {
+		$post_types = array_merge( $post_types, $this->custom_post_types );
+		return $post_types;
+	}
+
+	protected function activity_exists_for_post_type( $item_id, $secondary_item_id, $action, $display_comments = false ) {
+		$a = bp_activity_get( array(
+			'display_comments'  => $display_comments,
+			'filter'            => array(
+				'action'        => $action,
+				'primary_id'    => $item_id,
+				'secondary_id'  => $secondary_item_id,
+		) ) );
+
+		return ! empty( $a['activities'] );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/functions.php	(revision 12265)
@@ -0,0 +1,1144 @@
+<?php
+
+/**
+ * @group blogs
+ */
+class BP_Tests_Blogs_Functions extends BP_UnitTestCase {
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 */
+	public function test_bp_blogs_delete_blogmeta_non_numeric_blog_id() {
+		$this->assertFalse( bp_blogs_delete_blogmeta( 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 * @ticket BP5399
+	 */
+	public function test_bp_blogs_delete_blogmeta_illegal_characters() {
+		$this->assertNotEmpty( bp_blogs_update_blogmeta( 1, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		$this->assertFalse( bp_blogs_delete_blogmeta( 1, $krazy_key ) );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 * @ticket BP5399
+	 */
+	public function test_bp_blogs_delete_blogmeta_trim_meta_value() {
+		$this->assertNotEmpty( bp_blogs_update_blogmeta( 1, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		bp_blogs_delete_blogmeta( 1, 'foo', '   bar  ' );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 */
+	public function test_bp_blogs_delete_blogmeta_no_meta_key() {
+		bp_blogs_update_blogmeta( 1, 'foo', 'bar' );
+		bp_blogs_update_blogmeta( 1, 'foo2', 'bar2' );
+		$this->assertNotEmpty( bp_blogs_get_blogmeta( 1 ) );
+		$this->assertTrue( bp_blogs_delete_blogmeta( 1 ) );
+		$this->assertSame( array(), bp_blogs_get_blogmeta( 1 ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 */
+	public function test_bp_blogs_delete_blogmeta_with_meta_value() {
+		bp_blogs_update_blogmeta( 1, 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		bp_blogs_delete_blogmeta( 1, 'foo', 'baz' );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		$this->assertTrue( bp_blogs_delete_blogmeta( 1, 'foo', 'bar' ) );
+		$this->assertSame( '', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 */
+	public function test_bp_blogs_delete_blogmeta_with_delete_all_but_no_meta_key() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( 1, 'foo1', 'bar1' );
+		bp_blogs_add_blogmeta( 2, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( 2, 'foo1', 'bar1' );
+
+		$this->assertTrue( bp_blogs_delete_blogmeta( 1, '', '', true ) );
+		$this->assertEmpty( bp_blogs_get_blogmeta( 1 ) );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 2, 'foo' ) );
+		$this->assertSame( 'bar1', bp_blogs_get_blogmeta( 2, 'foo1' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_delete_blogmeta
+	 */
+	public function test_bp_blogs_delete_blogmeta_with_delete_all() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( 1, 'foo1', 'bar1' );
+		bp_blogs_add_blogmeta( 2, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( 2, 'foo1', 'bar1' );
+
+		$this->assertTrue( bp_blogs_delete_blogmeta( 1, 'foo', '', true ) );
+		$this->assertSame( '', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		$this->assertSame( '', bp_blogs_get_blogmeta( 2, 'foo' ) );
+		$this->assertSame( 'bar1', bp_blogs_get_blogmeta( 1, 'foo1' ) );
+		$this->assertSame( 'bar1', bp_blogs_get_blogmeta( 2, 'foo1' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_get_blogmeta
+	 */
+	public function test_bp_blogs_get_blogmeta_empty_blog_id() {
+		$this->assertFalse( bp_blogs_get_blogmeta( 0 ) );
+		$this->assertFalse( bp_blogs_get_blogmeta( '' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_get_blogmeta
+	 * @ticket BP5399
+	 */
+	public function test_bp_blogs_get_blogmeta_illegal_characters() {
+		bp_blogs_update_blogmeta( 1, 'foo', 'bar' );
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		$this->assertSame( '', bp_blogs_get_blogmeta( 1, $krazy_key ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_get_blogmeta
+	 */
+	public function test_bp_blogs_get_blogmeta_no_meta_key() {
+		bp_blogs_update_blogmeta( 1, 'foo', 'bar' );
+		bp_blogs_update_blogmeta( 1, 'foo2', 'bar2' );
+
+		$expected = array(
+			'foo' => array(
+				'bar',
+			),
+			'foo2' => array(
+				'bar2',
+			),
+		);
+
+		$this->assertSame( $expected, bp_blogs_get_blogmeta( 1 ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_get_blogmeta
+	 */
+	public function test_bp_blogs_get_blogmeta_no_meta_key_empty() {
+		$this->assertSame( array(), bp_blogs_get_blogmeta( 1 ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_get_blogmeta
+	 */
+	public function test_bp_blogs_get_blogmeta_single_true() {
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( 1, 'foo', 'baz' );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) ); // default is true
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo', true ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_get_blogmeta
+	 */
+	public function test_bp_blogs_get_blogmeta_single_false() {
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+		bp_blogs_add_blogmeta( 1, 'foo', 'baz' );
+		$this->assertSame( array( 'bar', 'baz' ), bp_blogs_get_blogmeta( 1, 'foo', false ) );
+	}
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 */
+	public function test_bp_blogs_update_blogmeta_non_numeric_blog_id() {
+		$this->assertFalse( bp_blogs_update_blogmeta( 'foo', 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 * @ticket BP5399
+	 */
+	public function test_bp_blogs_update_blogmeta_illegal_characters() {
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		bp_blogs_update_blogmeta( 1, $krazy_key, 'bar' );
+		$this->assertSame( '', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 */
+	public function test_bp_blogs_update_blogmeta_stripslashes() {
+		$slashed = 'This \"string\" is cool';
+		bp_blogs_update_blogmeta( 1, 'foo', $slashed );
+		$this->assertSame( 'This "string" is cool', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 */
+	public function test_bp_blogs_update_blogmeta_new() {
+		$this->assertNotEmpty( bp_blogs_update_blogmeta( 1, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 */
+	public function test_bp_blogs_update_blogmeta_existing() {
+		bp_blogs_update_blogmeta( 1, 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		$this->assertTrue( bp_blogs_update_blogmeta( 1, 'foo', 'baz' ) );
+		$this->assertSame( 'baz', bp_blogs_get_blogmeta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 */
+	public function test_bp_blogs_update_blogmeta_existing_no_change() {
+		bp_blogs_update_blogmeta( 1, 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_blogs_get_blogmeta( 1, 'foo' ) );
+		$this->assertFalse( bp_blogs_update_blogmeta( 1, 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_update_blogmeta
+	 */
+	public function test_bp_blogs_update_meta_prev_value() {
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+
+		// In earlier versions of WordPress, bp_activity_update_meta()
+		// returns true even on failure. However, we know that in these
+		// cases the update is failing as expected, so we skip this
+		// assertion just to keep our tests passing
+		// See https://core.trac.wordpress.org/ticket/24933
+		if ( version_compare( $GLOBALS['wp_version'], '3.7', '>=' ) ) {
+			$this->assertFalse( bp_blogs_update_blogmeta( 1, 'foo', 'bar2', 'baz' ) );
+		}
+
+		$this->assertTrue( bp_blogs_update_blogmeta( 1, 'foo', 'bar2', 'bar' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_add_blogmeta
+	 */
+	public function test_bp_blogs_add_blogmeta_no_meta_key() {
+		$this->assertFalse( bp_blogs_add_blogmeta( 1, '', 'bar' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_add_blogmeta
+	 */
+	public function test_bp_blogs_add_blogmeta_empty_object_id() {
+		$this->assertFalse( bp_blogs_add_blogmeta( 0, 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_add_blogmeta
+	 */
+	public function test_bp_blogs_add_blogmeta_existing_unique() {
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+		$this->assertFalse( bp_blogs_add_blogmeta( 1, 'foo', 'baz', true ) );
+	}
+
+	/**
+	 * @group blogmeta
+	 * @group bp_blogs_add_blogmeta
+	 */
+	public function test_bp_blogs_add_blogmeta_existing_not_unique() {
+		bp_blogs_add_blogmeta( 1, 'foo', 'bar' );
+		$this->assertNotEmpty( bp_blogs_add_blogmeta( 1, 'foo', 'baz' ) );
+	}
+
+	/**
+	 * @group bp_blogs_restore_data
+	 */
+	public function test_bp_blogs_restore_data() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		// Create a regular member
+		$u = self::factory()->user->create();
+
+		// Create blogs
+		$b1 = self::factory()->blog->create( array( 'user_id' => $u ) );
+		$b2 = self::factory()->blog->create( array( 'user_id' => $u ) );
+
+		$expected = array(
+			$b1 => $b1,
+			$b2 => $b2
+		);
+
+		// Mark the user as spam
+		bp_core_process_spammer_status( $u, 'spam' );
+
+		// get all blogs for user
+		$blogs = bp_blogs_get_blogs_for_user( $u, true );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+
+		$this->assertNotEquals( $expected, array_map( 'intval', $blog_ids ), 'User marked as spam should not have any blog registered' );
+
+		// Ham the user
+		bp_core_process_spammer_status( $u, 'ham' );
+
+		// get all blogs for user
+		$blogs = bp_blogs_get_blogs_for_user( $u, true );
+		$blog_ids = wp_list_pluck( $blogs['blogs'], 'blog_id' );
+
+		$this->assertEquals( $expected, array_map( 'intval', $blog_ids ) );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_publish_to_publish() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		// 'publish' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+
+		$post->post_status = 'publish';
+		$post->post_content .= ' foo';
+
+		wp_update_post( $post );
+
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity (no change)' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_password_publish() {
+		$post_id = self::factory()->post->create( array(
+			'post_status'   => 'publish',
+			'post_type'     => 'post',
+			'post_password' => 'pass',
+		) );
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish with password'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Published with password post should not have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_publish_update_password() {
+		$post_id = self::factory()->post->create( array(
+			'post_status'   => 'publish',
+			'post_type'     => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		// 'publish' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+
+		$post->post_content .= ' foo';
+		$post->post_password = 'pass';
+
+		wp_update_post( $post );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Updated with password post should not have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_private_publish() {
+		$post_id = self::factory()->post->create( array(
+			'post_status'   => 'private',
+			'post_type'     => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		// 'new' => 'private'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Private post should not have activity' );
+
+		$post->post_status = 'publish';
+
+		wp_update_post( $post );
+
+		// 'private' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_publish_private() {
+		$post_id = self::factory()->post->create( array(
+			'post_status'   => 'publish',
+			'post_type'     => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+
+		$post->post_status = 'private';
+
+		wp_update_post( $post );
+
+		// 'publish' => 'private'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Private post should not have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_draft_to_draft() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'draft',
+			'post_type' => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Unpublished post should not have activity' );
+
+		$post->post_status = 'draft';
+		$post->post_content .= ' foo';
+
+		wp_update_post( $post );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Unpublished post should not have activity (no change)' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_draft_to_publish() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'draft',
+			'post_type' => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Unpublished post should not have activity' );
+
+		$post->post_status = 'publish';
+		$post->post_content .= ' foo';
+
+		wp_update_post( $post );
+
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_publish_to_draft() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+
+		$post->post_status = 'draft';
+		$post->post_content .= ' foo';
+
+		wp_update_post( $post );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Unpublished post should not have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_wp_delete_post() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+
+		wp_delete_post( $post->ID );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Unpublished post should not have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_transition_post_status_wp_trash_post() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		$this->assertTrue( $this->activity_exists_for_post( $post_id ), 'Published post should have activity' );
+
+		wp_trash_post( $post->ID );
+
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Unpublished post should not have activity' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 * @group post_type_comment_activities
+	 */
+	public function test_update_blog_post_and_new_blog_comment_and_activity_comment_meta() {
+		// save the current user and override logged-in user
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+			'post_title' => 'First title',
+		) );
+
+		// remove comment flood protection temporarily
+		add_filter( 'comment_flood_filter', '__return_false' );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+		$c1 = wp_new_comment( array(
+			'comment_post_ID'      => $post_id,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is a blog comment',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u,
+		) );
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// let's also add a "new_blog_comment" activity entry
+		$c2 = wp_new_comment( array(
+			'comment_post_ID'      => $post_id,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is another blog comment',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u,
+		) );
+
+		// bring back flood protection
+		remove_filter( 'comment_flood_filter', '__return_false' );
+
+		// update the initial blog post
+		wp_update_post( array(
+			'ID'        => $post_id,
+			'post_title' => 'Second title',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'              => 'activity_comment',
+			'display_comments'  => 'stream',
+			'meta_query'        => array( array(
+				'key'     => 'bp_blogs_post_comment_id',
+				'value'   => $c1,
+			) )
+		) );
+
+		// grab the activity ID for the blog comment
+		$a2 = bp_activity_get_activity_id( array(
+			'component'         => buddypress()->blogs->id,
+			'type'              => 'new_blog_comment',
+			'secondary_item_id' => $c2,
+		) );
+
+		// see if blog comment activity meta matches the post items
+		$this->assertEquals( 'Second title', bp_activity_get_meta( $a1, 'post_title' ) );
+		$this->assertEquals( add_query_arg( 'p', $post_id, home_url( '/' ) ), bp_activity_get_meta( $a1, 'post_url' ) );
+
+		$this->assertEquals( 'Second title', bp_activity_get_meta( $a2, 'post_title' ) );
+		$this->assertEquals( add_query_arg( 'p', $post_id, home_url( '/' ) ), bp_activity_get_meta( $a2, 'post_url' ) );
+
+		// reset
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_transition_activity_status
+	 * @group bp_blogs_post_type_remove_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_remove_comment_should_remove_spammed_activity_comment() {
+		// save the current user and override logged-in user
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+			'post_title' => 'First title',
+		) );
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+		$c1 = wp_new_comment( array(
+			'comment_post_ID'      => $post_id,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is a blog comment',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u,
+		) );
+
+		// save the corresponding activity comment ID
+		$a1 = bp_activity_get_activity_id( array(
+			'type'              => 'activity_comment',
+			'display_comments'  => 'stream',
+			'meta_query'        => array( array(
+				'key'     => 'bp_blogs_post_comment_id',
+				'value'   => $c1,
+			) )
+		) );
+
+		// trash the parent comment.
+		// corresponding activity comment should now be marked as spam
+		// @see bp_blogs_transition_activity_status()
+		wp_trash_comment( $c1 );
+
+		// now permanently delete the comment
+		wp_delete_comment( $c1, true );
+
+		// activity comment should no longer exist
+		$a = bp_activity_get( array(
+			'in'               => $a1,
+			'display_comments' => 'stream',
+			'spam'             => 'all'
+		) );
+		// this is a convoluted way of testing if the activity comment still exists
+		$this->assertTrue( empty( $a['activities'][0] ) );
+
+		// reset
+		$this->set_current_user( $old_user );
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+	}
+
+	/**
+	 * @group bp_blogs_post_type_remove_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_post_type_remove_comment() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+
+		// create the blog post
+		$p = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'post',
+			'post_title' => 'First title',
+		) );
+
+		$c = wp_new_comment( array(
+			'comment_post_ID'      => $p,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this comment will be removed',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u,
+		) );
+
+		// An activity should exist
+		$a = bp_activity_get_activity_id( array(
+			'user_id' => $u,
+			'type'    => 'new_blog_comment'
+		) );
+
+		// now permanently delete the comment
+		wp_delete_comment( $c, true );
+
+		// The activity comment should no longer exist
+		$ac = bp_activity_get( array( 'in' => $a ) );
+		$this->assertTrue( empty( $ac['activities'] ) );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_bp_blogs_is_blog_trackable_false_publish_post() {
+		add_filter( 'bp_blogs_is_blog_trackable', '__return_false' );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status'   => 'publish',
+			'post_type'     => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Not trackable blog post should not have activity' );
+
+		$post->post_content .= ' foo';
+
+		wp_update_post( $post );
+
+		// 'publish' => 'publish'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Not trackable blog post should not have activity' );
+
+		remove_filter( 'bp_blogs_is_blog_trackable', '__return_false' );
+	}
+
+	/**
+	 * @group bp_blogs_catch_transition_post_status
+	 */
+	public function test_bp_is_blog_public_zero_publish_post() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		add_filter( 'bp_is_blog_public', '__return_zero' );
+
+		$post_id = self::factory()->post->create( array(
+			'post_status'   => 'publish',
+			'post_type'     => 'post',
+		) );
+		$post = get_post( $post_id );
+
+		// 'new' => 'publish'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Not public blog post should not have activity' );
+
+		$post->post_content .= ' foo';
+
+		wp_update_post( $post );
+
+		// 'publish' => 'publish'
+		$this->assertFalse( $this->activity_exists_for_post( $post_id ), 'Not public blog post should not have activity' );
+
+		remove_filter( 'bp_is_blog_public', '__return_zero' );
+	}
+
+	/**
+	 * @group bp_blogs_record_comment
+	 * @group unique
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_record_comment_no_duplicate_activity_comments() {
+		// save the current user and override logged-in user
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$userdata = get_userdata( $u );
+		$this->activity_saved_comment_count = 0;
+		$this->comment_saved_count = 0;
+
+		// let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+		add_action( 'bp_activity_add', array( $this, 'count_activity_comment_saved' ) );
+		add_action( 'wp_insert_comment', array( $this, 'count_post_comment_saved' ) );
+		add_action( 'edit_comment', array( $this, 'count_post_comment_saved' ) );
+
+		// create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+			'post_title'  => 'Test Duplicate activity comments',
+		) );
+
+		// grab the activity ID for the activity comment
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'activity comment should be unique',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		$activities = bp_activity_get( array(
+			'type'             => 'activity_comment',
+			'display_comments' => 'stream',
+			'search_terms'     => 'activity comment should be unique',
+		) );
+
+		$this->assertTrue( count( $activities['activities'] ) === 1, 'An activity comment should be unique' );
+
+		$this->assertTrue( 2 === $this->activity_saved_comment_count, 'An activity comment should be saved only twice' );
+		$this->assertTrue( 1 === $this->comment_saved_count, 'A comment should be saved only once' );
+
+		// reset
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+		remove_action( 'bp_activity_add', array( $this, 'count_activity_comment_saved' ) );
+		remove_action( 'wp_insert_comment', array( $this, 'count_post_comment_saved' ) );
+		remove_action( 'edit_comment', array( $this, 'count_post_comment_saved' ) );
+
+		$this->activity_saved_comment_count = 0;
+		$this->comment_saved_count = 0;
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_record_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_record_comment_should_record_parent_blog_post_activity_if_not_found() {
+		// Save the current user and override logged-in user
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Get user details
+		$user = get_userdata( $u );
+
+		// Let's use activity comments instead of single "new_blog_comment" activity items
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// Create the blog post
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type'   => 'post',
+		) );
+
+		// Now, delete the activity item for the blog post
+		bp_activity_delete( array(
+			'component'         => buddypress()->blogs->id,
+			'type'              => 'new_blog_post',
+			'item_id'           => get_current_blog_id(),
+			'secondary_item_id' => $post_id,
+		) );
+
+		// Add a comment to blog post
+		wp_new_comment( array(
+			'comment_post_ID' => $post_id,
+			'user_id' => $u,
+			'comment_content' => 'Dummy comment',
+			'comment_author' => 'Dumbo',
+			'comment_author_url' => 'http://buddypress.org',
+
+			// Important to pass check in bp_blogs_record_comment()
+			'comment_author_email' => $user->user_email
+		) );
+
+		// Fetch the activity ID for the blog post to see if it exists
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_blog_post',
+			'component' => buddypress()->blogs->id,
+			'filter'    => array(
+				'item_id' => get_current_blog_id(),
+				'secondary_item_id' => $post_id
+			),
+		) );
+
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		// Assert that activity item for blog post was created after adding a comment
+		$this->assertNotNull( $a1, 'Activity item was not created for existing blog post when recording post comment.' );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_comment_sync_activity_comment
+	 * @group post_type_comment_activities
+	 */
+	public function test_bp_blogs_comment_sync_activity_comment_for_custom_post_type() {
+		if ( is_multisite() ) {
+			if ( function_exists( 'wp_initialize_site' ) ) {
+				$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			}
+
+			$b = self::factory()->blog->create();
+			switch_to_blog( $b );
+			add_filter( 'comment_flood_filter', '__return_false' );
+		} else {
+			$b = get_current_blog_id();
+		}
+
+		$u = self::factory()->user->create();
+		$userdata = get_userdata( $u );
+
+		$labels = array(
+			'name'                       => 'bars',
+			'singular_name'              => 'bar',
+		);
+
+		register_post_type( 'foo', array(
+			'labels'   => $labels,
+			'public'   => true,
+			'supports' => array( 'comments' ),
+		) );
+
+		add_post_type_support( 'foo', 'buddypress-activity' );
+
+		bp_activity_set_post_type_tracking_args( 'foo', array(
+			'comment_action_id' => 'new_foo_comment',
+		) );
+
+		add_filter( 'bp_disable_blogforum_comments', '__return_false' );
+
+		$p = self::factory()->post->create( array(
+			'post_author' => $u,
+			'post_type'   => 'foo',
+		) );
+
+		$a1 = bp_activity_get_activity_id( array(
+			'type'      => 'new_foo',
+			'filter'    => array(
+				'item_id' => $b,
+				'secondary_item_id' => $p
+			),
+		) );
+
+		$c = wp_new_comment( array(
+			'comment_post_ID'      => $p,
+			'comment_author'       => $userdata->user_nicename,
+			'comment_author_url'   => 'http://buddypress.org',
+			'comment_author_email' => $userdata->user_email,
+			'comment_content'      => 'this is a foo comment',
+			'comment_type'         => '',
+			'comment_parent'       => 0,
+			'user_id'              => $u,
+		) );
+
+		$a2 = bp_activity_new_comment( array(
+			'content'     => 'this should generate a new foo comment',
+			'user_id'     => $u,
+			'activity_id' => $a1,
+		) );
+
+		$activity_args = array(
+			'type'              => 'activity_comment',
+			'display_comments'  => 'stream',
+			'meta_query'        => array( array(
+				'key'       => 'bp_blogs_foo_comment_id',
+				'compare'   => 'exists',
+			) )
+		);
+
+		$a = bp_activity_get( $activity_args );
+		$aids = wp_list_pluck( $a['activities'], 'id' );
+		$cids = wp_list_pluck( get_approved_comments( $p ), 'comment_ID' );
+
+		foreach ( $aids as $aid ) {
+			$this->assertTrue( in_array( bp_activity_get_meta( $aid, 'bp_blogs_foo_comment_id' ), $cids ), 'The comment ID should be in the activity meta' );
+		}
+
+		foreach ( $cids as $cid ) {
+			$this->assertTrue( in_array( get_comment_meta( $cid, 'bp_activity_comment_id', true ), $aids ), 'The activity ID should be in the comment meta' );
+		}
+
+		_unregister_post_type( 'foo' );
+
+		if ( is_multisite() ) {
+			restore_current_blog();
+			remove_filter( 'comment_flood_filter', '__return_false' );
+		}
+
+		remove_filter( 'bp_disable_blogforum_comments', '__return_false' );
+	}
+
+	public function count_activity_comment_saved() {
+		$this->activity_saved_comment_count += 1;
+	}
+
+	public function count_post_comment_saved() {
+		$this->comment_saved_count += 1;
+	}
+
+	/**
+	 * @group bp_blogs_record_existing_blogs
+	 */
+	public function test_bp_blogs_record_existing_blogs_limit() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$old_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Create three sites.
+		self::factory()->blog->create_many( 3, array(
+			'user_id' => $u
+		) );
+
+		// Record each site one at a time
+		bp_blogs_record_existing_blogs( array(
+			'limit' => 1
+		) );
+
+		// Assert!
+		$blogs = bp_blogs_get_blogs( array(
+			'user_id' => $u
+		) );
+		$this->assertSame( 3, (int) $blogs['total'] );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_remove_blog
+	 */
+	public function test_bp_blogs_remove_blog() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			$this->setExpectedDeprecated( 'delete_blog' );
+		}
+
+		$reset_post = $_POST;
+		$old_user = get_current_user_id();
+
+		// Simulate a new "BuddyPress generated" blog
+		$_POST['blog_public'] = 1;
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Create three sites.
+		$b = self::factory()->blog->create( array(
+			'user_id' => $u
+		) );
+
+		$activity = bp_activity_get( array(
+			'filter' => array(
+				'object'     => 'blogs',
+				'action'     => 'new_blog',
+				'primary_id' => $b,
+			),
+		) );
+
+		$new_blog = array_map( 'intval', wp_list_pluck( $activity['activities'], 'item_id', 'id' ) );
+		$this->assertSame( $b, reset( $new_blog ) );
+
+		// Removing the blog should delete the activity and the blog association.
+		wpmu_delete_blog( $b );
+
+		$deleted = bp_activity_get( array(
+			'in' => array_keys( $new_blog ),
+		) );
+
+		$this->assertEmpty( $deleted['activities'] );
+		$this->assertEmpty( BP_Blogs_Blog::is_recorded( $b ) );
+
+		$_POST = $reset_post;
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_blogs_remove_blog_for_user
+	 */
+	public function test_bp_blogs_remove_blog_for_user_is_contributor() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$reset_post = $_POST;
+		$old_user = get_current_user_id();
+
+		// Simulate a new "BuddyPress generated" blog
+		$_POST['blog_public'] = 1;
+
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Create three sites.
+		$b = self::factory()->blog->create( array(
+			'user_id' => $u
+		) );
+
+		$u2 = self::factory()->user->create();
+		add_user_to_blog( $b, $u2, 'contributor' );
+
+		$u2_blogs = BP_Blogs_Blog::get_blog_ids_for_user( $u2 );
+		$this->assertContains( $b, $u2_blogs, 'The user should be associated to the blog as he is a contributor' );
+
+		remove_user_from_blog( $u2, $b );
+		$u2_blogs = BP_Blogs_Blog::get_blog_ids_for_user( $u2 );
+		$this->assertNotContains( $b, $u2_blogs, 'The user should not be associated anymore to the blog' );
+
+		$activity = bp_activity_get( array(
+			'filter' => array(
+				'object'     => 'blogs',
+				'action'     => 'new_blog',
+				'primary_id' => $b,
+			),
+		) );
+
+		$new_blog = array_map( 'intval', wp_list_pluck( $activity['activities'], 'item_id', 'id' ) );
+		$this->assertSame( $b, reset( $new_blog ), 'The new_blog activity should not be deleted when a contributor is removed from the blog.' );
+
+		$_POST = $reset_post;
+		$this->set_current_user( $old_user );
+	}
+
+	protected function activity_exists_for_post( $post_id ) {
+		$a = bp_activity_get( array(
+			'component' => buddypress()->blogs->id,
+			'action' => 'new_blog_post',
+			'item_id' => get_current_blog_id(),
+			'secondary_item_id' => $post_id,
+		) );
+
+		return ! empty( $a['activities'] );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/blogs/template.php	(revision 12265)
@@ -0,0 +1,370 @@
+<?php
+
+/**
+ * @group blogs
+ */
+class BP_Tests_Blogs_Template extends BP_UnitTestCase {
+	/**
+	 * @group bp_get_blog_last_active
+	 */
+	public function test_bp_get_blog_last_active_default_params() {
+		// Fake the global
+		global $blogs_template;
+
+		$time = date( 'Y-m-d h:i:s', time() - 24 * 60 * 60 );
+		$blogs_template = new stdClass;
+		$blogs_template->blog = new stdClass;
+		$blogs_template->blog->last_activity = $time;
+
+		$this->assertEquals( bp_core_get_last_activity( $time, __( 'active %s', 'buddypress' ) ), bp_get_blog_last_active() );
+
+		$blogs_template->blog = null;
+	}
+
+	/**
+	 * @group bp_get_blog_last_active
+	 */
+	public function test_bp_get_blog_last_active_active_format_true() {
+		// Fake the global
+		global $blogs_template;
+
+		$time = date( 'Y-m-d h:i:s', time() - 24 * 60 * 60 );
+		$blogs_template = new stdClass;
+		$blogs_template->blog = new stdClass;
+		$blogs_template->blog->last_activity = $time;
+
+		$this->assertEquals( bp_core_get_last_activity( $time, __( 'active %s', 'buddypress' ) ), bp_get_blog_last_active( array( 'active_format' => true, ) ) );
+
+		$blogs_template->blog = null;
+	}
+
+	/**
+	 * @group bp_get_blog_last_active
+	 */
+	public function test_bp_get_blog_last_active_active_format_false() {
+		// Fake the global
+		global $blogs_template;
+
+		$time = date( 'Y-m-d h:i:s', time() - 24 * 60 * 60 );
+		$blogs_template = new stdClass;
+		$blogs_template->blog = new stdClass;
+		$blogs_template->blog->last_activity = $time;
+
+		$this->assertEquals( bp_core_time_since( $time ), bp_get_blog_last_active( array( 'active_format' => false, ) ) );
+
+		$blogs_template->blog = null;
+	}
+
+	/**
+	 * @group bp_get_blog_last_active
+	 */
+	public function test_bp_get_blog_last_active_active_no_last_activity() {
+		$this->assertEquals( __( 'Never active', 'buddypress' ), bp_get_blog_last_active() );
+	}
+
+	/**
+	 * @group bp_get_blog_latest_post
+	 */
+	public function test_bp_get_blog_latest_post_default_params() {
+		// Fake the global
+		global $blogs_template;
+
+		$blogs_template = new stdClass;
+		$blogs_template->blog = new stdClass;
+		$blogs_template->blog->latest_post = new stdClass;
+		$blogs_template->blog->latest_post->guid = 'foo';
+		$blogs_template->blog->latest_post->post_title = 'bar';
+
+		$this->assertSame( sprintf( __( 'Latest Post: %s', 'buddypress' ), '<a href="foo">bar</a>' ), bp_get_blog_latest_post() );
+
+		$blogs_template->blog = null;
+	}
+
+	/**
+	 * @group bp_get_blog_latest_post
+	 */
+	public function test_bp_get_blog_latest_post_latest_format_true() {
+		// Fake the global
+		global $blogs_template;
+
+		$blogs_template = new stdClass;
+		$blogs_template->blog = new stdClass;
+		$blogs_template->blog->latest_post = new stdClass;
+		$blogs_template->blog->latest_post->guid = 'foo';
+		$blogs_template->blog->latest_post->post_title = 'bar';
+
+		$this->assertSame( sprintf( __( 'Latest Post: %s', 'buddypress' ), '<a href="foo">bar</a>' ), bp_get_blog_latest_post( array( 'latest_format' => true, ) ) );
+
+		$blogs_template->blog = null;
+	}
+
+	/**
+	 * @group bp_get_blog_latest_post
+	 */
+	public function test_bp_get_blog_latest_post_latest_format_false() {
+		// Fake the global
+		global $blogs_template;
+
+		$blogs_template = new stdClass;
+		$blogs_template->blog = new stdClass;
+		$blogs_template->blog->latest_post = new stdClass;
+		$blogs_template->blog->latest_post->guid = 'foo';
+		$blogs_template->blog->latest_post->post_title = 'bar';
+
+		$this->assertSame( '<a href="foo">bar</a>', bp_get_blog_latest_post( array( 'latest_format' => false, ) ) );
+
+		$blogs_template->blog = null;
+	}
+
+	/**
+	 * @group bp_blog_signup_enabled
+	 */
+	public function test_bp_signup_enabled_when_registration_setting_does_not_exist_should_default_to_true() {
+		$old_settings = $settings = buddypress()->site_options;
+		if ( is_array( $settings ) && isset( $settings['registration'] ) ) {
+			unset( $settings['registration'] );
+		}
+		buddypress()->site_options = $settings;
+
+		$this->assertTrue( bp_blog_signup_enabled() );
+
+		buddypress()->site_options = $old_settings;
+	}
+
+	/**
+	 * @group bp_blog_signup_enabled
+	 */
+	public function test_bp_signup_enabled_when_registration_setting_is_all_should_return_true() {
+		$old_settings = $settings = buddypress()->site_options;
+
+		if ( ! is_array( $settings ) ) {
+			$settings = array();
+		}
+
+		$settings['registration'] = 'all';
+		buddypress()->site_options = $settings;
+
+		$this->assertTrue( bp_blog_signup_enabled() );
+
+		buddypress()->site_options = $old_settings;
+	}
+
+	/**
+	 * @group bp_blog_signup_enabled
+	 */
+	public function test_bp_signup_enabled_when_registration_setting_is_blog_should_return_true() {
+		$old_settings = $settings = buddypress()->site_options;
+
+		if ( ! is_array( $settings ) ) {
+			$settings = array();
+		}
+
+		$settings['registration'] = 'blog';
+		buddypress()->site_options = $settings;
+
+		$this->assertTrue( bp_blog_signup_enabled() );
+
+		buddypress()->site_options = $old_settings;
+	}
+
+	/**
+	 * @group bp_blog_signup_enabled
+	 */
+	public function test_bp_signup_enabled_when_registration_setting_is_user_should_return_false() {
+		$old_settings = $settings = buddypress()->site_options;
+
+		if ( ! is_array( $settings ) ) {
+			$settings = array();
+		}
+
+		$settings['registration'] = 'user';
+		buddypress()->site_options = $settings;
+
+		$this->assertFalse( bp_blog_signup_enabled() );
+
+		buddypress()->site_options = $old_settings;
+	}
+
+	/**
+	 * @group bp_blog_signup_enabled
+	 */
+	public function test_bp_signup_enabled_when_registration_setting_is_none_should_return_false() {
+		$old_settings = $settings = buddypress()->site_options;
+
+		if ( ! is_array( $settings ) ) {
+			$settings = array();
+		}
+
+		$settings['registration'] = 'none';
+		buddypress()->site_options = $settings;
+
+		$this->assertFalse( bp_blog_signup_enabled() );
+
+		buddypress()->site_options = $old_settings;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Blogs_Template
+	 */
+	public function test_bp_blogs_template_should_give_precedence_to_bpage_URL_param() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$request = $_REQUEST;
+		$_REQUEST['bpage'] = '5';
+
+		$r = array(
+			'type'              => 'active',
+			'page_arg'          => 'bpage',
+			'page'              => 8,
+			'per_page'          => 20,
+			'max'               => false,
+			'user_id'           => 0,
+			'include_blog_ids'  => false,
+			'search_terms'      => '',
+			'update_meta_cache' => true
+		);
+
+		$at = new BP_Blogs_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['page_arg'],
+			$r['update_meta_cache'],
+			$r['include_blog_ids']
+		);
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Blogs_Template
+	 */
+	public function test_bp_blogs_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$request = $_REQUEST;
+		$_REQUEST['bpage'] = '0';
+
+		$r = array(
+			'type'              => 'active',
+			'page_arg'          => 'bpage',
+			'page'              => 8,
+			'per_page'          => 20,
+			'max'               => false,
+			'user_id'           => 0,
+			'include_blog_ids'  => false,
+			'search_terms'      => '',
+			'update_meta_cache' => true
+		);
+
+		$at = new BP_Blogs_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['page_arg'],
+			$r['update_meta_cache'],
+			$r['include_blog_ids']
+		);
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Blogs_Template
+	 */
+	public function test_bp_blogs_template_should_give_precedence_to_num_URL_param() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$r = array(
+			'type'              => 'active',
+			'page_arg'          => 'bpage',
+			'page'              => 1,
+			'per_page'          => 13,
+			'max'               => false,
+			'user_id'           => 0,
+			'include_blog_ids'  => false,
+			'search_terms'      => '',
+			'update_meta_cache' => true
+		);
+
+		$at = new BP_Blogs_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['page_arg'],
+			$r['update_meta_cache'],
+			$r['include_blog_ids']
+		);
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Blogs_Template
+	 */
+	public function test_bp_blogs_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$r = array(
+			'type'              => 'active',
+			'page_arg'          => 'bpage',
+			'page'              => 1,
+			'per_page'          => 13,
+			'max'               => false,
+			'user_id'           => 0,
+			'include_blog_ids'  => false,
+			'search_terms'      => '',
+			'update_meta_cache' => true
+		);
+
+		$at = new BP_Blogs_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['page_arg'],
+			$r['update_meta_cache'],
+			$r['include_blog_ids']
+		);
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/avatars.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/avatars.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/avatars.php	(revision 12265)
@@ -0,0 +1,373 @@
+<?php
+/**
+ * @group core
+ * @group avatars
+ */
+class BP_Tests_Avatars extends BP_UnitTestCase {
+	private $params = array();
+
+	private function clean_existing_avatars( $type = 'user' ) {
+		if ( 'user' === $type ) {
+			$avatar_dir = 'avatars';
+		} elseif ( 'group' === $object ) {
+			$avatar_dir = 'group-avatars';
+		}
+
+		$this->rrmdir( bp_core_avatar_upload_path() . '/' . $avatar_dir );
+	}
+
+	/**
+	 * @ticket BP4948
+	 */
+	function test_avatars_on_non_root_blog() {
+		// Do not pass 'Go', do not collect $200
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$u = self::factory()->user->create();
+
+		// get BP root blog's upload directory data
+		$upload_dir = wp_upload_dir();
+
+		// create new subsite
+		$blog_id = self::factory()->blog->create( array(
+			'user_id' => $u,
+			'title'   => 'Test Title',
+			'path'    => '/path' . rand() . time() . '/',
+		) );
+
+		// emulate a page load on the new sub-site
+		$this->go_to( get_blog_option( $blog_id, 'siteurl' ) );
+
+		// test to see if the upload dir is correct
+		$this->assertEquals( $upload_dir['baseurl'], bp_core_avatar_url() );
+
+		// reset globals
+		$this->go_to( '/' );
+	}
+
+	/**
+	 * @group bp_get_user_has_avatar
+	 */
+	public function test_bp_get_user_has_avatar_no_avatar_uploaded() {
+		$this->clean_existing_avatars();
+
+		$u = self::factory()->user->create();
+		$this->assertFalse( bp_get_user_has_avatar( $u ) );
+	}
+
+	/**
+	 * @group bp_get_user_has_avatar
+	 */
+	public function test_bp_get_user_has_avatar_has_avatar_uploaded() {
+		$u = self::factory()->user->create();
+
+		// Fake it
+		add_filter( 'bp_core_fetch_avatar_url', array( $this, 'avatar_cb' ) );
+
+		$this->assertTrue( bp_get_user_has_avatar( $u ) );
+
+		remove_filter( 'bp_core_fetch_avatar_url', array( $this, 'avatar_cb' ) );
+	}
+
+	public function avatar_cb() {
+		return 'foo';
+	}
+
+	/**
+	 * @group bp_core_fetch_avatar
+	 */
+	public function test_bp_core_fetch_avatar_parameter_conservation() {
+		// First, run the check with custom parameters, specifying no gravatar.
+		$this->params = array(
+			'item_id'       => 1406,
+			'object'        => 'custom_object',
+			'type'          => 'full',
+			'avatar_dir'    => 'custom-dir',
+			'width'         => 48,
+			'height'        => 54,
+			'class'         => 'custom-class',
+			'css_id'        => 'custom-css-id',
+			'alt'           => 'custom alt',
+			'email'         => 'avatar@avatar.org',
+			'no_grav'       => true,
+			'html'          => true,
+			'title'         => 'custom-title',
+			'extra_attr'    => 'data-testing="buddypress"',
+			'scheme'        => 'http',
+			'rating'        => get_option( 'avatar_rating' ),
+			'force_default' => false,
+		);
+
+		// Check to make sure the custom parameters survived the function all the way up to output
+		add_filter( 'bp_core_fetch_avatar', array( $this, 'bp_core_fetch_avatar_filter_check' ), 12, 2 );
+		$avatar = bp_core_fetch_avatar( $this->params );
+
+		// Re-run check, allowing gravatars.
+		$this->params['no_grav'] = false;
+		$avatar = bp_core_fetch_avatar( $this->params );
+
+		remove_filter( 'bp_core_fetch_avatar', array( $this, 'bp_core_fetch_avatar_filter_check' ), 12 );
+
+		unset( $this->params );
+	}
+
+	public function bp_core_fetch_avatar_filter_check( $html, $params ) {
+		// Check that the passed parameters match the original custom parameters.
+		$this->assertEmpty( array_merge( array_diff( $params, $this->params ), array_diff( $this->params, $params ) ) );
+
+		// Check the returned html to see that it matches an expected value.
+		// Get the correct default avatar, based on whether gravatars are allowed.
+		if ( $params['no_grav'] ) {
+			$avatar_url = bp_core_avatar_default( 'local', $params );
+		} else {
+			// This test has the slight odor of hokum since it recreates so much code that could be changed at any time.
+			$bp = buddypress();
+			$host = '//www.gravatar.com/avatar/';
+
+			// Set expected gravatar type
+			if ( empty( $bp->grav_default->{$this->params['object']} ) ) {
+				$default_grav = 'wavatar';
+			} elseif ( 'mystery' == $bp->grav_default->{$this->params['object']} ) {
+				$default_grav = apply_filters( 'bp_core_mysteryman_src', 'mm', $this->params['width'] );
+			} else {
+				$default_grav = $bp->grav_default->{$this->params['object']};
+			}
+
+			$avatar_url = $host . md5( strtolower( $this->params['email'] ) );
+
+			// Main Gravatar URL args.
+			$url_args = array(
+				's' => $this->params['width']
+			);
+
+			// Force default.
+			if ( ! empty( $this->params['force_default'] ) ) {
+				$url_args['f'] = 'y';
+			}
+
+			// Gravatar rating; http://bit.ly/89QxZA
+			$rating = strtolower( get_option( 'avatar_rating' ) );
+			if ( ! empty( $rating ) ) {
+				$url_args['r'] = $rating;
+			}
+
+			// Default avatar.
+			if ( 'gravatar_default' !== $default_grav ) {
+				$url_args['d'] = $default_grav;
+			}
+
+			// Set up the Gravatar URL.
+			$avatar_url = esc_url( add_query_arg(
+				rawurlencode_deep( array_filter( $url_args ) ),
+				$avatar_url
+			) );
+
+		}
+
+		$expected_html = '<img src="' . $avatar_url . '" id="' . $this->params['css_id'] . '" class="' . $this->params['class'] . ' ' . $this->params['object'] . '-' . $this->params['item_id'] . '-avatar avatar-' . $this->params['width'] . ' photo" width="' . $this->params['width'] . '" height="' . $this->params['height'] . '" alt="' . $this->params['alt'] . '" title="' . $this->params['title'] . '" ' . $this->params['extra_attr'] . ' />';
+
+		$this->assertEquals( $html, $expected_html );
+	}
+
+	/**
+	 * @group bp_core_fetch_avatar
+	 */
+	public function test_bp_core_fetch_avatar_class_attribute() {
+		$u = self::factory()->user->create();
+
+		$hw = 100;
+		$args = array(
+			'item_id'    => $u,
+			'object'     => 'user',
+			'type'       => 'full',
+			'width'      => $hw,
+			'height'     => $hw,
+			'class'      => '',
+			'no_grav'    => true,
+			'html'       => true,
+		);
+
+		// Class attribute is empty
+		$avatar = bp_core_fetch_avatar( $args );
+		$expected = array( 'avatar', 'user-' . $u . '-avatar', 'avatar-' . $hw );
+		preg_match( '/class=["\']?([^"\']*)["\' ]/is', $avatar, $matches );
+		$classes = explode( ' ', $matches[1] );
+		$this->assertSame( $expected, array_intersect_key( $expected, $classes ) );
+
+		// Class attribute is a String
+		$args['class'] = 'custom-class class-custom';
+		$avatar = bp_core_fetch_avatar( $args );
+		$expected = array_merge( explode( ' ', $args['class'] ), array( 'user-' . $u . '-avatar', 'avatar-' . $hw ) );
+		preg_match( '/class=["\']?([^"\']*)["\' ]/is', $avatar, $matches );
+		$classes = explode( ' ', $matches[1] );
+		$this->assertSame( $expected, array_intersect_key( $expected, $classes ) );
+
+		// Class attribute is an Array
+		$args['class'] = array( 'custom-class', 'class-custom' );
+		$avatar = bp_core_fetch_avatar( $args );
+		$expected = array_merge( $args['class'], array( 'user-' . $u . '-avatar', 'avatar-' . $hw ) );
+		preg_match( '/class=["\']?([^"\']*)["\' ]/is', $avatar, $matches );
+		$classes = explode( ' ', $matches[1] );
+		$this->assertSame( $expected, array_intersect_key( $expected, $classes ) );
+	}
+
+	/**
+	 * @group bp_core_check_avatar_type
+	 */
+	public function test_bp_core_check_avatar_type() {
+		$plugin_dir = trailingslashit( buddypress()->plugin_dir );
+
+		$file = array(
+			'file' => array(
+				'name' => 'humans.txt',
+				'type' => 'text/plain',
+				'tmp_name' => $plugin_dir . 'humans.txt',
+			)
+		);
+
+		$this->assertFalse( bp_core_check_avatar_type( $file ) );
+
+		$file = array(
+			'file' => array(
+				'name' => 'mystery-man.jpg',
+				'type' => 'image/jpeg',
+				'tmp_name' => $plugin_dir . 'bp-core/images/mystery-man.jpg',
+			)
+		);
+
+		$this->assertTrue( bp_core_check_avatar_type( $file ) );
+
+		$file = array(
+			'file' => array(
+				'name' => 'mystery-man.jpg',
+				'type' => 'application/octet-stream',
+				'tmp_name' => $plugin_dir . 'bp-core/images/mystery-man.jpg',
+			)
+		);
+
+		$this->assertTrue( bp_core_check_avatar_type( $file ), 'flash is using application/octet-stream for image uploads' );
+	}
+
+	/**
+	 * @group bp_core_check_avatar_type
+	 * @group bp_core_get_allowed_avatar_types
+	 */
+	public function test_bp_core_get_allowed_avatar_types_filter() {
+		add_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_add_type' ) );
+
+		$this->assertEquals( array( 'jpeg', 'gif', 'png' ), bp_core_get_allowed_avatar_types() );
+
+		remove_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_add_type' ) );
+
+		add_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_remove_type' ) );
+
+		$this->assertEquals( array( 'gif', 'png' ), bp_core_get_allowed_avatar_types() );
+
+		remove_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_remove_type' ) );
+
+		add_filter( 'bp_core_get_allowed_avatar_types', '__return_empty_array' );
+
+		$this->assertEquals( array( 'jpeg', 'gif', 'png' ), bp_core_get_allowed_avatar_types() );
+
+		remove_filter( 'bp_core_get_allowed_avatar_types', '__return_empty_array' );
+	}
+
+	/**
+	 * @group bp_core_check_avatar_type
+	 * @group bp_core_get_allowed_avatar_mimes
+	 */
+	public function test_bp_core_get_allowed_avatar_mimes() {
+		$mimes = bp_core_get_allowed_avatar_mimes();
+
+		$this->assertEqualSets( array( 'jpeg', 'gif', 'png', 'jpg' ), array_keys( $mimes ) );
+		$this->assertEqualSets( array( 'image/jpeg', 'image/gif', 'image/png', 'image/jpeg' ), array_values( $mimes ) );
+
+		add_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_add_type' ) );
+
+		$this->assertEqualSets( array( 'image/jpeg', 'image/gif', 'image/png', 'image/jpeg' ), array_values( bp_core_get_allowed_avatar_mimes() ) );
+
+		remove_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_add_type' ) );
+
+		add_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_remove_type' ) );
+
+		$this->assertEqualSets( array( 'image/gif', 'image/png' ), array_values( bp_core_get_allowed_avatar_mimes() ) );
+
+		remove_filter( 'bp_core_get_allowed_avatar_types', array( $this, 'avatar_types_filter_remove_type' ) );
+
+		add_filter( 'bp_core_get_allowed_avatar_types', '__return_empty_array' );
+
+		$this->assertEqualSets( array( 'image/jpeg', 'image/gif', 'image/png', 'image/jpeg' ), array_values( bp_core_get_allowed_avatar_mimes() ) );
+
+		remove_filter( 'bp_core_get_allowed_avatar_types', '__return_empty_array' );
+	}
+
+	public function avatar_types_filter_add_type( $types ) {
+		$types[] = 'bmp';
+
+		return $types;
+	}
+
+	public function avatar_types_filter_remove_type( $types ) {
+		$jpeg = array_shift( $types );
+
+		return $types;
+	}
+
+	/**
+	 * @group BP7056
+	 */
+	public function test_no_grav_default_should_respect_thumb_type() {
+		$found = bp_core_fetch_avatar( array(
+			'item_id' => 12345,
+			'object' => 'user',
+			'type' => 'thumb',
+			'no_grav' => true,
+			'html' => false,
+		) );
+
+		$this->assertContains( 'mystery-man-50.jpg', $found );
+	}
+
+	/**
+	 * @group BP7056
+	 */
+	public function test_no_grav_default_should_return_thumb_avatar_for_small_enough_width() {
+		$found = bp_core_fetch_avatar( array(
+			'item_id' => 12345,
+			'object' => 'user',
+			'type' => 'full',
+			'width' => '50',
+			'no_grav' => true,
+			'html' => false,
+		) );
+
+		$this->assertContains( 'mystery-man-50.jpg', $found );
+	}
+
+	/**
+	 * @group BP7056
+	 */
+	public function test_no_grav_default_should_return_full_avatar_for_thumb_when_thumb_width_is_too_wide() {
+		add_filter( 'bp_core_avatar_thumb_width', array( $this, 'filter_thumb_width' ) );
+		$found = bp_core_fetch_avatar( array(
+			'item_id' => 12345,
+			'object' => 'user',
+			'type' => 'thumb',
+			'no_grav' => true,
+			'html' => false,
+		) );
+		remove_filter( 'bp_core_avatar_thumb_width', array( $this, 'filter_thumb_width' ) );
+
+		$this->assertContains( 'mystery-man.jpg', $found );
+	}
+
+	public function filter_thumb_width() {
+		return 51;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/cache.php	(revision 12265)
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @group cache
+ * @group core
+ */
+class BP_Tests_Core_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_update_meta_cache
+	 */
+	public function test_bp_update_meta_cache_with_cache_misses() {
+		// Use activity just because
+		$a1 = self::factory()->activity->create();
+
+		// Confirm that all activitymeta is deleted
+		global $wpdb;
+
+		$bp = buddypress();
+
+		$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->activity->table_name_meta} WHERE activity_id = %d", $a1 ) );
+
+		bp_update_meta_cache( array(
+			'object_ids' => array( $a1 ),
+			'object_type' => 'activity',
+			'cache_group' => 'activity_meta',
+			'meta_table' => buddypress()->activity->table_name_meta,
+			'object_column' => 'activity_id',
+		) );
+
+		$this->assertSame( array(), wp_cache_get( $a1, 'activity_meta' ) );
+	}
+}
+
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/caps.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/caps.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/caps.php	(revision 12265)
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @group core
+ * @group caps
+ */
+class BP_Tests_Core_Caps extends BP_UnitTestCase {
+	public function test_bp_current_user_can_should_interpret_integer_second_param_as_a_blog_id() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped( __METHOD__ . ' requires multisite.' );
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		$this->set_current_user( $u );
+
+		add_filter( 'user_has_cap', array( $this, 'grant_cap_foo' ), 10, 2 );
+		$can  = bp_current_user_can( 'foo', bp_get_root_blog_id() );
+		$cant = bp_current_user_can( 'foo', $b );
+		remove_filter( 'user_has_cap', array( $this, 'grant_cap_foo' ), 10 );
+
+		$this->assertTrue( $can );
+		$this->assertFalse( $cant );
+	}
+
+	/**
+	 * @ticket BP6501
+	 */
+	public function test_bp_current_user_can_should_respect_blog_id_passed_in_args_array() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped( __METHOD__ . ' requires multisite.' );
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$b = self::factory()->blog->create();
+		$u = self::factory()->user->create();
+
+		$this->set_current_user( $u );
+
+		add_filter( 'user_has_cap', array( $this, 'grant_cap_foo' ), 10, 2 );
+		$can  = bp_current_user_can( 'foo', array( 'blog_id' => bp_get_root_blog_id() ) );
+		$cant = bp_current_user_can( 'foo', array( 'blog_id' => $b ) );
+		remove_filter( 'user_has_cap', array( $this, 'grant_cap_foo' ), 10 );
+
+		$this->assertTrue( $can );
+		$this->assertFalse( $cant );
+	}
+
+	public function grant_cap_foo( $allcaps, $caps ) {
+		if ( bp_is_root_blog() ) {
+			$allcaps['foo'] = 1;
+		}
+
+		return $allcaps;
+	}
+
+	public function check_cap_args( $caps, $cap, $user_id, $args ) {
+		$this->test_args = $args;
+		return $caps;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-attachment.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-attachment.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-attachment.php	(revision 12265)
@@ -0,0 +1,526 @@
+<?php
+
+include_once BP_TESTS_DIR . 'assets/attachment-extensions.php';
+
+/**
+ * @group bp_attachments
+ * @group BP_Attachment
+ */
+class BP_Tests_BP_Attachment_TestCases extends BP_UnitTestCase {
+	private $upload_results;
+	private $image_file;
+
+	public function setUp() {
+		parent::setUp();
+		add_filter( 'bp_attachment_upload_overrides',        array( $this, 'filter_overrides' ),       10, 1 );
+		add_filter( 'upload_dir',                            array( $this, 'filter_upload_dir' ),      20, 1 );
+		add_filter( 'bp_attachments_cover_image_upload_dir', array( $this, 'filter_cover_image_dir' ), 10, 2 );
+		$this->upload_results = array();
+		$this->image_file = trailingslashit( buddypress()->plugin_dir ) . 'bp-core/images/mystery-man.jpg';
+		$this->original_upload_dir = array();
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		remove_filter( 'bp_attachment_upload_overrides',     array( $this, 'filter_overrides' ),       10 );
+		remove_filter( 'upload_dir',                         array( $this, 'filter_upload_dir' ),      20 );
+		add_filter( 'bp_attachments_cover_image_upload_dir', array( $this, 'filter_cover_image_dir' ), 10, 2 );
+		$this->upload_results = array();
+		$this->image_file = '';
+		$this->original_upload_dir = array();
+	}
+
+	public function filter_overrides( $overrides ) {
+		$overrides['upload_error_handler'] = array( $this, 'upload_error_handler' );
+
+		// Don't test upload for WordPress < 4.0
+		$overrides['test_upload'] = false;
+		return $overrides;
+	}
+
+	public function filter_upload_dir( $upload_dir ) {
+		$upload_dir['error'] = 'fake_upload_success';
+
+		$this->upload_results = array(
+			'new_file' => $upload_dir['path'] . '/mystery-man.jpg',
+			'url'      => $upload_dir['url'] . '/mystery-man.jpg',
+		);
+
+		return $upload_dir;
+	}
+
+	public function filter_cover_image_dir( $cover_dir, $upload_dir ) {
+		$this->original_upload_dir = $upload_dir;
+
+		return $cover_dir;
+	}
+
+	/**
+	 * To avoid copying files in tests, we're faking a succesfull uploads
+	 * as soon as all the test_form have been executed in _wp_handle_upload
+	 */
+	public function upload_error_handler( $file, $message ) {
+		if ( 'fake_upload_success' !== $message ) {
+			return array( 'error' => $message );
+		} else {
+			return array(
+				'file' => $this->upload_results['new_file'],
+				'url'  => $this->upload_results['url'],
+				'type' => 'image/jpeg',
+			);
+		}
+	}
+
+	private function clean_files( $basedir = 'attachment_base_dir' ) {
+		$upload_dir = bp_upload_dir();
+
+		$this->rrmdir( $upload_dir['basedir'] . '/' . $basedir );
+	}
+
+	private function clean_avatars( $type = 'user' ) {
+		if ( 'user' === $type ) {
+			$avatar_dir = 'avatars';
+		} elseif ( 'group' === $type ) {
+			$avatar_dir = 'group-avatars';
+		}
+
+		$this->rrmdir( bp_core_avatar_upload_path() . '/' . $avatar_dir );
+	}
+
+	public function max_filesize() {
+		return 1000;
+	}
+
+	public function test_bp_attachment_construct_missing_required_parameter() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$_FILES['file'] = array(
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => 1000
+		);
+
+		$attachment_class = new BPTest_Attachment_Extension();
+		$upload = $attachment_class->upload( $_FILES );
+
+		$this->assertTrue( empty( $upload ) );
+
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	public function test_bp_attachment_set_upload_dir() {
+		$upload_dir = bp_upload_dir();
+
+		$attachment_class = new BPTest_Attachment_Extension( array(
+			'action'     => 'attachment_action',
+			'file_input' => 'attachment_file_input'
+		) );
+
+		$this->assertSame( $attachment_class->upload_dir, bp_upload_dir() );
+
+		$attachment_class = new BPTest_Attachment_Extension( array(
+			'action'     => 'attachment_action',
+			'file_input' => 'attachment_file_input',
+			'base_dir'   => 'attachment_base_dir',
+		) );
+
+		$this->assertTrue( file_exists( $upload_dir['basedir'] . '/attachment_base_dir'  ) );
+
+		// clean up
+		$this->clean_files();
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_bp_attachment_upload() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$attachment_class = new BPTest_Attachment_Extension( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+			'base_dir'   		    => 'attachment_base_dir',
+			'original_max_filesize' => 1000,
+		) );
+
+		$_POST['action'] = $attachment_class->action;
+		$_FILES[ $attachment_class->file_input ] = array(
+			'tmp_name' => $this->image_file,
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => filesize( $this->image_file ),
+		);
+
+		// Error: file size
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertFalse( empty( $upload['error'] ) );
+
+		$attachment_class->allowed_mime_types    = array( 'pdf' );
+		$attachment_class->original_max_filesize = false;
+
+		// Error: file type
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertFalse( empty( $upload['error'] ) );
+
+		$attachment_class->allowed_mime_types = array();
+
+		// Success
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertEquals( $upload['file'], $attachment_class->upload_path . '/mystery-man.jpg' );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+		$this->clean_files();
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_bp_attachment_upload_no_base_dir_specific_time() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$attachment_class = new BPTest_Attachment_Extension( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+		) );
+
+		$_POST['action'] = $attachment_class->action;
+		$_FILES[ $attachment_class->file_input ] = array(
+			'tmp_name' => $this->image_file,
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => filesize( $this->image_file ),
+		);
+
+		$time = '2015/01';
+
+		$upload = $attachment_class->upload( $_FILES, '', $time );
+
+		// If no base_dir was provided, default WordPress uploads dir should be used.
+		$this->assertEquals( $upload['file'], $attachment_class->upload_path . '/' . $time . '/mystery-man.jpg' );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 * @group avatar
+	 */
+	public function test_bp_attachment_avatar_user_upload() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+		$bp = buddypress();
+		$displayed_user = $bp->displayed_user;
+		$bp->displayed_user = new stdClass;
+
+		$u1 = self::factory()->user->create();
+		$bp->displayed_user->id = $u1;
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $this->image_file,
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => filesize( $this->image_file )
+		);
+
+		/* No error */
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+		$this->assertEquals( $user_avatar['file'], $bp->avatar->upload_path . '/avatars/' . $u1 .'/mystery-man.jpg' );
+
+		/* File size error */
+		add_filter( 'bp_core_avatar_original_max_filesize', array( $this, 'max_filesize' ) );
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		remove_filter( 'bp_core_avatar_original_max_filesize', array( $this, 'max_filesize' ) );
+		$this->assertFalse( empty( $user_avatar['error'] ) );
+
+		/* File type error */
+		$_FILES[ $avatar_attachment->file_input ]['name'] = 'buddypress_logo.pdf';
+		$_FILES[ $avatar_attachment->file_input ]['type'] = 'application/pdf';
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+		$this->assertFalse( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$bp->displayed_user = $displayed_user;
+		$this->clean_avatars();
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 * @group avatar
+	 */
+	public function test_bp_attachment_avatar_group_upload() {
+		$bp = buddypress();
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+		$reset_current_group = $bp->groups->current_group;
+
+		$g = self::factory()->group->create();
+
+		$bp->groups->current_group = groups_get_group( $g );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $this->image_file,
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => filesize( $this->image_file )
+		);
+
+		$group_avatar = $avatar_attachment->upload( $_FILES, 'groups_avatar_upload_dir' );
+		$this->assertEquals( $group_avatar['file'], $bp->avatar->upload_path . '/group-avatars/' . $g .'/mystery-man.jpg' );
+
+		// clean up!
+		$this->clean_avatars( 'group' );
+		$bp->groups->current_group = $reset_current_group;
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group crop
+	 */
+	public function test_bp_attachment_crop() {
+		$crop_args = array(
+			'original_file' => $this->image_file,
+			'crop_x'        => 0,
+			'crop_y'        => 0,
+			'crop_w'        => 150,
+			'crop_h'        => 150,
+			'dst_w'         => 150,
+			'dst_h'         => 150,
+		);
+
+		$attachment_class = new BPTest_Attachment_Extension( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+			'base_dir'   		    => 'attachment_base_dir',
+		) );
+
+		$cropped = $attachment_class->crop( $crop_args );
+
+		// Image must come from the upload basedir
+		$this->assertTrue( is_wp_error( $cropped ) );
+
+		$crop_args['original_file'] = $attachment_class->upload_path . '/mystery-man.jpg';
+
+		// Image must stay in the upload basedir
+		$crop_args['dst_file'] = BP_TESTS_DIR . 'assets/error.jpg';
+		$cropped = $attachment_class->crop( $crop_args );
+
+		// Image must stay in the upload basedir
+		$this->assertTrue( is_wp_error( $cropped ) );
+
+		// clean up!
+		$this->clean_files();
+	}
+
+	/**
+	 * @group upload
+	 * @group cover_image
+	 */
+	public function test_bp_attachment_cover_image_user_upload() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+		$bp = buddypress();
+		$displayed_user = $bp->displayed_user;
+		$bp->displayed_user = new stdClass;
+
+		$u1 = self::factory()->user->create();
+		$bp->displayed_user->id = $u1;
+
+		// Upload the file
+		$cover_image_attachment = new BP_Attachment_Cover_Image();
+		$_POST['action'] = $cover_image_attachment->action;
+		$_FILES[ $cover_image_attachment->file_input ] = array(
+			'tmp_name' => $this->image_file,
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => filesize( $this->image_file )
+		);
+
+		/* No error */
+		$cover_image = $cover_image_attachment->upload( $_FILES );
+		$this->assertEquals( $cover_image['file'], $bp->avatar->upload_path . '/buddypress/members/' . $u1 .'/cover-image/mystery-man.jpg' );
+
+		// clean up!
+		$bp->displayed_user = $displayed_user;
+		$this->clean_files( 'buddypress' );
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group shrink
+	 * @group avatars
+	 */
+	public function test_bp_attachment_avatar_shrink() {
+		if ( false === _wp_image_editor_choose() ) {
+			$this->markTestSkipped( 'This test requires PHP to have a valid image editor that is compatible with WordPress.' );
+		}
+
+		$image = BP_TESTS_DIR . 'assets/upside-down.jpg';
+
+		$dir_copy = bp_upload_dir();
+
+		// in case cleaning files fails
+		if ( ! is_dir( $dir_copy['basedir'] . '/shrink' ) ) {
+			mkdir( $dir_copy['basedir'] . '/shrink' );
+		}
+
+		$abs_path_copy = $dir_copy['basedir'] . '/shrink/upside-down.jpg';
+
+		copy( $image, $abs_path_copy );
+
+		add_filter( 'bp_core_avatar_original_max_width', array( $this, 'limit_to_50px' ) );
+
+		$shrink = BP_Attachment_Avatar::shrink( $abs_path_copy );
+
+		remove_filter( 'bp_core_avatar_original_max_width', array( $this, 'limit_to_50px' ) );
+
+		$this->assertTrue( 50 === $shrink['width'] && 50 === $shrink['height'] );
+
+		// Cleanup
+		$this->clean_files( 'shrink' );
+	}
+
+	public function limit_to_50px( $max_width ) {
+		return 50;
+	}
+
+	/**
+	 * @group shrink
+	 * @group avatars
+	 */
+	public function test_bp_attachment_avatar_shrink_not_needed() {
+		$shrink = BP_Attachment_Avatar::shrink( $this->image_file );
+
+		$this->assertTrue( empty( $shrink ) );
+	}
+
+	/**
+	 * @group shrink
+	 * @group cover_images
+	 */
+	public function test_bp_attachment_cover_image_fit() {
+		if ( false === _wp_image_editor_choose() ) {
+			$this->markTestSkipped( 'This test requires PHP to have a valid image editor that is compatible with WordPress.' );
+		}
+
+		$image = BP_TESTS_DIR . 'assets/upside-down.jpg';
+
+		$cover_image_class = new BP_Attachment_Cover_Image();
+
+		$abs_path_copy = $cover_image_class->upload_path . '/upside-down.jpg';
+
+		copy( $image, $abs_path_copy );
+
+		$fit = $cover_image_class->fit( $abs_path_copy, array( 'width' => 50, 'height' => 50 ) );
+
+		$this->assertTrue( 50 === $fit['width'] && 50 === $fit['height'] );
+
+		// Cleanup
+		$this->clean_files( 'buddypress' );
+	}
+
+	/**
+	 * @group shrink
+	 * @group cover_images
+	 */
+	public function test_bp_attachment_cover_image_fit_not_needed() {
+		$cover_image_class = new BP_Attachment_Cover_Image();
+		$fit = $cover_image_class->fit( $this->image_file, array( 'width' => 1300, 'height' => 225 ) );
+
+		$this->assertTrue( empty( $fit ) );
+
+		// Cleanup
+		$this->clean_files( 'buddypress' );
+	}
+
+	/**
+	 * @group avatars
+	 * @group cover_images
+	 */
+	public function test_bp_attachment_get_image_data() {
+		if ( ! is_callable( 'exif_read_data' ) ) {
+			$this->markTestSkipped( 'This test requires PHP to be compiled with EXIF support.' );
+		}
+
+		$image_data = BP_Attachment::get_image_data( BP_TESTS_DIR . 'assets/upside-down.jpg' );
+
+		$this->assertTrue( 3 == $image_data['meta']['orientation'] );
+	}
+
+	/**
+	 * @group upload
+	 * @group cover_images
+	 */
+	public function test_bp_attachment_upload_dir_filter_arg() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$attachment_class = new BPTest_Attachment_Extension( array(
+			'action'                 => 'attachment_action',
+			'file_input'             => 'attachment_file_input',
+			'base_dir'               => 'attachment_base_dir',
+			'upload_dir_filter_args' => 1,
+		) );
+
+		$_POST['action'] = $attachment_class->action;
+		$_FILES[ $attachment_class->file_input ] = array(
+			'tmp_name' => $this->image_file,
+			'name'     => 'mystery-man.jpg',
+			'type'     => 'image/jpeg',
+			'error'    => 0,
+			'size'     => filesize( $this->image_file ),
+		);
+
+		// Simulate an upload
+		$attachment_class->upload( $_FILES );
+
+		// Remove the filter used to fake uploads
+		remove_filter( 'upload_dir', array( $this, 'filter_upload_dir' ), 20 );
+
+		$this->assertSame( $attachment_class->original_upload_dir, wp_upload_dir() );
+
+		// Restore the filter used to fake uploads
+		add_filter( 'upload_dir', array( $this, 'filter_upload_dir' ), 20, 1 );
+
+		$this->assertTrue( 1 === $attachment_class->upload_dir_filter_args );
+
+		$cover_image_class = new BP_Attachment_Cover_Image();
+
+		// Simulate an upload
+		$cover_image_class->upload( $_FILES );
+
+		// Should be empty
+		$this->assertEmpty( $this->original_upload_dir );
+
+		$this->assertTrue( 0 === $cover_image_class->upload_dir_filter_args );
+
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-button.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-button.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-button.php	(revision 12265)
@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * @group core
+ * @group BP_Button
+ */
+class BP_Tests_BP_Button extends BP_UnitTestCase {
+	/**
+	 * @group block_self
+	 */
+	public function test_block_self_own_profile() {
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		$this->go_to( bp_core_get_user_domain( $u ) );
+
+		$b = new BP_Button( array(
+			'id' => 'foo',
+			'component' => 'members',
+			'block_self' => true,
+		) );
+
+		$this->assertEquals( '', $b->contents );
+	}
+
+	/**
+	 * @group block_self
+	 */
+	public function test_block_self_others_profile() {
+		$u1 = self::factory()->user->create();
+		$this->set_current_user( $u1 );
+
+		$u2 = self::factory()->user->create();
+		$this->go_to( bp_core_get_user_domain( $u2 ) );
+
+		$b = new BP_Button( array(
+			'id' => 'foo',
+			'component' => 'members',
+			'block_self' => true,
+		) );
+
+		$this->assertNotEmpty( $b->contents );
+	}
+
+	/**
+	 * @group block_self
+	 */
+	public function test_block_self_inside_members_loop() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		$this->set_current_user( $u1 );
+
+		$found = array();
+		if ( bp_has_members() ) {
+			while ( bp_members() ) {
+				bp_the_member();
+
+				$b = new BP_Button( array(
+					'id' => 'foo',
+					'component' => 'members',
+					'block_self' => true,
+				) );
+
+				$found[ bp_get_member_user_id() ] = empty( $b->contents );
+			}
+		}
+
+		$expected = array(
+			$u1 => true,
+			$u2 => false,
+		);
+
+		$this->assertSame( $expected, $found );
+
+		// clean up
+		$GLOBALS['members_template'] = null;
+	}
+
+	/**
+	 * @group block_self
+	 */
+	public function test_block_self_false_inside_members_loop() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		$this->set_current_user( $u1 );
+
+		$found = array();
+		if ( bp_has_members() ) {
+			while ( bp_members() ) {
+				bp_the_member();
+
+				$b = new BP_Button( array(
+					'id' => 'foo',
+					'component' => 'members',
+					'block_self' => false,
+				) );
+
+				$found[ bp_get_member_user_id() ] = empty( $b->contents );
+			}
+		}
+
+		$expected = array(
+			$u1 => false,
+			$u2 => false,
+		);
+
+		$this->assertSame( $expected, $found );
+
+		// clean up
+		$GLOBALS['members_template'] = null;
+	}
+
+	/**
+	 * @group block_self
+	 */
+	public function test_block_self_inside_members_loop_on_my_profile_page() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		$this->set_current_user( $u1 );
+		$this->go_to( bp_core_get_user_domain( $u1 ) );
+
+		$found = array();
+		if ( bp_has_members() ) {
+			while ( bp_members() ) {
+				bp_the_member();
+
+				$b = new BP_Button( array(
+					'id' => 'foo',
+					'component' => 'members',
+					'block_self' => true,
+				) );
+
+				$found[ bp_get_member_user_id() ] = empty( $b->contents );
+			}
+		}
+
+		$expected = array(
+			$u1 => true,
+			$u2 => false,
+		);
+
+		$this->assertSame( $expected, $found );
+
+		// clean up
+		$GLOBALS['members_template'] = null;
+	}
+
+	/**
+	 * @ticket BP7226
+	 */
+	public function test_bp_button_new_args() {
+		$b = new BP_Button( array(
+			'id' => 'foo',
+			'component' => 'members',
+			'block_self' => false,
+			'must_be_logged_in' => false,
+			'parent_element' => 'section',
+			'parent_attr' => array(
+				'class' => 'section-class',
+				'id' => 'section-id',
+				'data-parent' => 'foo',
+			),
+			'button_element' => 'button',
+			'button_attr' => array(
+				'autofocus' => 'autofocus',
+				'type' => 'submit',
+				'name' => 'my-button'
+			)
+		) );
+
+		$this->assertInternalType( 'int', strpos( $b->contents, '<section ' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'class="section-class ' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'id="section-id"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'data-parent="foo"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, '<button ' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'autofocus="autofocus"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'type="submit"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'name="my-button"' ) );
+	}
+
+	/**
+	 * @ticket BP7226
+	 */
+	public function test_bp_button_deprecated_args_should_still_render() {
+		$b = new BP_Button( array(
+			'id' => 'foo',
+			'component' => 'members',
+			'block_self' => false,
+			'must_be_logged_in' => false,
+			'wrapper' => 'section',
+			'wrapper_class' => 'section-class',
+			'wrapper_id' => 'section-id',
+			'link_href' => 'http://example.com',
+			'link_class' => 'link-class',
+			'link_id' => 'link-id',
+			'link_rel' => 'nofollow',
+			'link_title' => 'link-title'
+		) );
+
+		$this->assertInternalType( 'int', strpos( $b->contents, '<section ' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'class="section-class ' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'id="section-id"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'href="http://example.com"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'class="link-class"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'id="link-id"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'rel="nofollow"' ) );
+		$this->assertInternalType( 'int', strpos( $b->contents, 'title="link-title"' ) );
+	}
+
+	/**
+	 * @ticket BP7226
+	 */
+	public function test_bp_button_new_element_attrs_have_precedence_over_deprecated_element_attrs() {
+		$b = new BP_Button( array(
+			'id' => 'foo',
+			'component' => 'members',
+			'block_self' => false,
+			'must_be_logged_in' => false,
+			'button_element' => 'button',
+			'button_attr' => array(
+				'class' => 'new-class',
+			),
+			'link_class' => 'old-class'
+		) );
+
+		$this->assertInternalType( 'int', strpos( $b->contents, '<button class="new-class"' ) );
+	}
+
+	/**
+	 * @ticket BP7226
+	 */
+	public function test_bp_button_new_element_attrs_should_not_render_for_empty_attrs() {
+		$b = new BP_Button( array(
+			'id' => 'foo',
+			'component' => 'members',
+			'block_self' => false,
+			'must_be_logged_in' => false,
+			'button_element' => 'button',
+			'button_attr' => array(
+				'class' => '',
+			),
+		) );
+
+		$this->assertFalse( strpos( $b->contents, '<button class=""' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-core-user.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-core-user.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-core-user.php	(revision 12265)
@@ -0,0 +1,307 @@
+<?php
+
+/**
+ * @group core
+ * @group BP_Core_User
+ */
+class BP_Tests_BP_Core_User_TestCases extends BP_UnitTestCase {
+	/**
+	 * @expectedDeprecated BP_Core_User::get_users
+	 */
+	public function test_get_users_with_exclude_querystring() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$exclude_qs = $u1 . ',junkstring,' . $u3;
+
+		$users = BP_Core_User::get_users( 'active', 0, 1, 0, false, false, true, $exclude_qs );
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users['users'], 'id' ) );
+
+		$this->assertEquals( array( $u2 ), $user_ids );
+	}
+
+	/**
+	 * @expectedDeprecated BP_Core_User::get_users
+	 */
+	public function test_get_users_with_exclude_array() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$exclude_array = array(
+			$u1,
+			'junkstring',
+			$u3,
+		);
+
+		$users = BP_Core_User::get_users( 'active', 0, 1, 0, false, false, true, $exclude_array );
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users['users'], 'id' ) );
+
+		$this->assertEquals( array( $u2 ), $user_ids );
+	}
+
+	/**
+	 * @expectedDeprecated BP_Core_User::get_users
+	 */
+	public function test_get_users_with_include_querystring() {
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s' ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 1000 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 50 ),
+		) );
+
+		$include_qs = $u1 . ',junkstring,' . $u3;
+
+		$users = BP_Core_User::get_users( 'active', 0, 1, 0, $include_qs );
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users['users'], 'id' ) );
+
+		$this->assertEquals( array( $u1, $u3 ), $user_ids );
+	}
+
+	/**
+	 * @expectedDeprecated BP_Core_User::get_users
+	 */
+	public function test_get_users_with_include_array() {
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s' ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 1000 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 50 ),
+		) );
+
+
+		$include_array = array(
+			$u1,
+			'junkstring',
+			$u3,
+		);
+
+		$users = BP_Core_User::get_users( 'active', 0, 1, 0, $include_array );
+		$user_ids = wp_list_pluck( $users['users'], 'id' );
+
+		// typecast...ugh
+		$user_ids = array_map( 'intval', $user_ids );
+
+		$this->assertEquals( array( $u1, $u3 ), $user_ids );
+	}
+
+	/**
+	 * @expectedDeprecated BP_Core_User::get_users
+	 * @group get_users
+	 * @group type
+	 */
+	public function test_type_alphabetical() {
+		$u1 = self::factory()->user->create( array(
+			'display_name' => 'foo',
+		) );
+		$u2 = self::factory()->user->create( array(
+			'display_name' => 'bar',
+		) );
+
+		global $wpdb;
+
+		$q = BP_Core_User::get_users( 'alphabetical' );
+		$found = array_map( 'intval', wp_list_pluck( $q['users'], 'id' ) );
+
+		$this->assertEquals( array( $u2, $u1 ), $found );
+	}
+
+	/**
+	 * @group get_users_by_letter
+	 */
+	public function test_get_users_by_letter() {
+		$u1 = self::factory()->user->create( array(
+			'display_name' => 'foo',
+		) );
+		$u2 = self::factory()->user->create( array(
+			'display_name' => 'bar',
+		) );
+
+		$q = BP_Core_User::get_users_by_letter( 'b' );
+		$found = array_map( 'intval', wp_list_pluck( $q['users'], 'id' ) );
+
+		$this->assertEquals( array( $u2 ), $found );
+	}
+
+	/**
+	 * @group search_users
+	 */
+	public function test_search_users() {
+		$u1 = self::factory()->user->create( array(
+			'display_name' => 'foo',
+		) );
+		$u2 = self::factory()->user->create( array(
+			'display_name' => 'bar',
+		) );
+
+		$q = BP_Core_User::search_users( 'ar' );
+		$found = array_map( 'intval', wp_list_pluck( $q['users'], 'id' ) );
+
+		$this->assertEquals( array( $u2 ), $found );
+	}
+
+	public function test_get_specific_users() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$include_array = array(
+			$u1,
+			'junkstring',
+			$u3,
+		);
+
+		$users = BP_Core_User::get_specific_users( $include_array );
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users['users'], 'id' ) );
+
+		$this->assertEquals( array( $u1, $u3 ), $user_ids );
+	}
+
+	/**
+	 * @group last_activity
+	 */
+	public function test_get_last_activity() {
+		$u = self::factory()->user->create();
+		$time = bp_core_current_time();
+
+		BP_Core_User::update_last_activity( $u, $time );
+
+		$a = BP_Core_User::get_last_activity( $u );
+		$found = isset( $a[ $u ]['date_recorded'] ) ? $a[ $u ]['date_recorded'] : '';
+
+		$this->assertEquals( $time, $found );
+	}
+
+	/**
+	 * @group last_activity
+	 * @group cache
+	 */
+	public function test_get_last_activity_store_in_cache() {
+		$u = self::factory()->user->create();
+		$time = bp_core_current_time();
+
+		// Cache is set during user creation. Clear to reflect actual
+		// pageload
+		wp_cache_delete( $u, 'bp_last_activity' );
+
+		// prime cache
+		$a = BP_Core_User::get_last_activity( $u );
+
+		$this->assertSame( $a[ $u ], wp_cache_get( $u, 'bp_last_activity' ) );
+	}
+
+	/**
+	 * @group last_activity
+	 * @group cache
+	 */
+	public function test_get_last_activity_store_in_cache_multiple_users() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = bp_core_current_time();
+
+		// Cache is set during user creation. Clear to reflect actual
+		// pageload
+		wp_cache_delete( $u1, 'bp_last_activity' );
+		wp_cache_delete( $u2, 'bp_last_activity' );
+
+		// prime cache
+		$a = BP_Core_User::get_last_activity( array( $u1, $u2 ) );
+
+		$this->assertSame( $a[ $u1 ], wp_cache_get( $u1, 'bp_last_activity' ) );
+		$this->assertSame( $a[ $u2 ], wp_cache_get( $u2, 'bp_last_activity' ) );
+	}
+
+	/**
+	 * @group last_activity
+	 * @group cache
+	 */
+	public function test_get_last_activity_from_cache_single_user() {
+		$u    = self::factory()->user->create();
+		$time = bp_core_current_time();
+
+		BP_Core_User::update_last_activity( $u, $time );
+
+		// Cache is set during user creation. Clear to reflect actual
+		// pageload
+		wp_cache_delete( $u, 'bp_last_activity' );
+
+		// Prime cache
+		$uncached = BP_Core_User::get_last_activity( $u );
+
+		// Fetch again to get from the cache
+		$cached = BP_Core_User::get_last_activity( $u );
+
+		$this->assertSame( $uncached, $cached );
+	}
+
+	/**
+	 * @group last_activity
+	 * @group cache
+	 */
+	public function test_get_last_activity_in_cache_multiple_users() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = bp_core_current_time();
+
+		BP_Core_User::update_last_activity( $u1, $time );
+		BP_Core_User::update_last_activity( $u2, $time );
+
+		// Cache is set during user creation. Clear to reflect actual pageload
+		wp_cache_delete( $u1, 'bp_last_activity' );
+		wp_cache_delete( $u2, 'bp_last_activity' );
+
+		// Prime cache
+		$uncached = BP_Core_User::get_last_activity( array( $u1, $u2 ) );
+
+		// Second grab will be from the cache
+		$cached = BP_Core_User::get_last_activity( array( $u1, $u2 ) );
+		$cached_u1 = wp_cache_get( $u1, 'bp_last_activity' );
+
+		$this->assertSame( $cached, $uncached );
+	}
+
+	/**
+	 * @group last_activity
+	 */
+	public function test_update_last_activity() {
+		$u = self::factory()->user->create();
+		$time = bp_core_current_time();
+		$time2 = '1968-12-25 01:23:45';
+
+		BP_Core_User::update_last_activity( $u, $time );
+		$a = BP_Core_User::get_last_activity( $u );
+		$found = isset( $a[ $u ]['date_recorded'] ) ? $a[ $u ]['date_recorded'] : '';
+		$this->assertEquals( $time, $found );
+
+		BP_Core_User::update_last_activity( $u, $time2 );
+		$a = BP_Core_User::get_last_activity( $u );
+		$found = isset( $a[ $u ]['date_recorded'] ) ? $a[ $u ]['date_recorded'] : '';
+		$this->assertEquals( $time2, $found );
+	}
+
+	/**
+	 * @group last_activity
+	 */
+	public function test_delete_last_activity() {
+		$u = self::factory()->user->create();
+		$time = bp_core_current_time();
+
+		BP_Core_User::update_last_activity( $u, $time );
+		$a = BP_Core_User::get_last_activity( $u );
+		$found = isset( $a[ $u ]['date_recorded'] ) ? $a[ $u ]['date_recorded'] : '';
+		$this->assertEquals( $time, $found );
+
+		BP_Core_User::delete_last_activity( $u );
+		$a = BP_Core_User::get_last_activity( $u );
+		$found = isset( $a[ $u ]['date_recorded'] ) ? $a[ $u ]['date_recorded'] : '';
+		$this->assertEquals( '', $found );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-email-recipient.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-email-recipient.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-email-recipient.php	(revision 12265)
@@ -0,0 +1,136 @@
+<?php
+/**
+ * @group core
+ * @group BP_Email_Recipient
+ */
+class BP_Email_Recipient_Tests extends BP_UnitTestCase {
+	protected $u1;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->u1 = self::factory()->user->create( array(
+			'display_name' => 'Unit Test',
+			'user_email'   => 'test@example.com',
+		) );
+	}
+
+	public function test_return_with_address_and_name() {
+		$email     = 'test@example.com';
+		$name      = 'Unit Test';
+		$recipient = new BP_Email_Recipient( $email, $name );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_array() {
+		$email     = 'test@example.com';
+		$name      = 'Unit Test';
+		$recipient = new BP_Email_Recipient( array( $email => $name ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_user_id() {
+		$recipient = new BP_Email_Recipient( $this->u1 );
+
+		$this->assertSame( 'test@example.com', $recipient->get_address() );
+		$this->assertSame( 'Unit Test', $recipient->get_name() );
+	}
+
+	public function test_return_with_wp_user_object() {
+		$recipient = new BP_Email_Recipient( get_user_by( 'id', $this->u1 ) );
+
+		$this->assertSame( 'test@example.com', $recipient->get_address() );
+		$this->assertSame( 'Unit Test', $recipient->get_name() );
+	}
+
+	public function test_return_with_known_address_and_optional_name() {
+		$email     = 'test@example.com';
+		$name      = 'Custom';
+		$recipient = new BP_Email_Recipient( $email, $name );
+
+		$this->assertSame( 'test@example.com', $recipient->get_address() );
+		$this->assertSame( 'Custom', $recipient->get_name() );
+	}
+
+	public function test_return_with_known_address_and_empty_name() {
+		$email     = 'test@example.com';
+		$recipient = new BP_Email_Recipient( $email );
+
+		$this->assertSame( 'test@example.com', $recipient->get_address() );
+
+		// Should fallback to WP user name.
+		$this->assertSame( 'Unit Test', $recipient->get_name() );
+	}
+
+	public function test_return_with_unknown_address_and_optional_name() {
+		$email     = 'unknown@example.com';
+		$name      = 'Custom';
+		$recipient = new BP_Email_Recipient( $email, $name );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_unknown_address_and_empty_name() {
+		$email     = 'unknown@example.com';
+		$recipient = new BP_Email_Recipient( $email );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertEmpty( $recipient->get_name() );
+	}
+
+	public function test_return_with_unknown_array_and_optional_name() {
+		$email     = 'unknown@example.com';
+		$name      = 'Custom';
+		$recipient = new BP_Email_Recipient( array( $email => $name ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_unknown_array_and_empty_name() {
+		$email     = 'unknown@example.com';
+		$recipient = new BP_Email_Recipient( array( $email ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertEmpty( $recipient->get_name() );
+	}
+
+	public function test_return_with_known_array_and_optional_name() {
+		$email     = 'test@example.com';
+		$name      = 'Custom';
+		$recipient = new BP_Email_Recipient( array( $email => $name ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_known_array_and_empty_name() {
+		$email     = 'test@example.com';
+		$recipient = new BP_Email_Recipient( array( $email ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+
+		// Should fallback to WP user name.
+		$this->assertSame( 'Unit Test', $recipient->get_name() );
+	}
+
+	public function test_should_return_empty_string_if_user_id_id_invalid() {
+		$recipient = new BP_Email_Recipient( time() );
+
+		$this->assertEmpty( $recipient->get_address() );
+		$this->assertEmpty( $recipient->get_name() );
+	}
+
+	public function test_get_wp_user_object_from_email_address() {
+		$recipient = new BP_Email_Recipient( 'test@example.com' );
+		$recipient = $recipient->get_user( 'search-email' );
+
+		$this->assertSame( $this->u1, $recipient->ID );
+		$this->assertSame( 'test@example.com', $recipient->user_email );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-email.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-email.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-email.php	(revision 12265)
@@ -0,0 +1,299 @@
+<?php
+/**
+ * @group core
+ * @group BP_Email
+ */
+class BP_Tests_Email extends BP_UnitTestCase_Emails {
+	protected $u1;
+	protected $u2;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->u1 = self::factory()->user->create( array(
+			'display_name' => 'Unit Test',
+			'user_email'   => 'test1@example.com',
+		) );
+
+		$this->u2 = self::factory()->user->create( array(
+			'display_name' => 'Unit Test2',
+			'user_email'   => 'test2@example.com',
+		) );
+	}
+
+	public function test_valid_subject() {
+		$message = 'test';
+		$email   = new BP_Email( 'activity-at-message' );
+
+		$email->set_subject( $message )->set_tokens( array( 'poster.name' => 'example' ) );
+		$this->assertSame( $message, $email->get_subject() );
+	}
+
+	public function test_valid_html_content() {
+		$message = '<b>test</b>';
+		$email   = new BP_Email( 'activity-at-message' );
+
+		$email->set_content_html( $message );
+		$email->set_content_type( 'html' );
+
+		$this->assertSame( $message, $email->get_content() );
+	}
+
+	public function test_valid_plaintext_content() {
+		$message = 'test';
+		$email   = new BP_Email( 'activity-at-message' );
+
+		$email->set_content_plaintext( $message );
+		$email->set_content_type( 'plaintext' );
+
+		$this->assertSame( $message, $email->get_content() );
+	}
+
+	public function test_valid_template() {
+		$message = 'test';
+		$email   = new BP_Email( 'activity-at-message' );
+
+		$email->set_template( $message );
+		$this->assertSame( $message, $email->get_template() );
+	}
+
+	public function test_tokens() {
+		$email          = new BP_Email( 'activity-at-message' );
+		$default_tokens = $email->get_tokens();
+		$tokens         = array( 'test1' => 'hello', 'test2' => 'world' );
+
+		$email->set_tokens( $tokens );
+
+		$this->assertSame(
+			array_keys( $tokens + $default_tokens ),
+			array_keys( $email->get_tokens() )
+		);
+
+		$this->assertSame(
+			array_values( $tokens + $default_tokens ),
+			array_values( $email->get_tokens() )
+		);
+	}
+
+	public function test_headers() {
+		$email           = new BP_Email( 'activity-at-message' );
+		$default_headers = $email->get_headers();
+		$headers         = array( 'custom_header' => 'custom_value' );
+
+		$email->set_headers( $headers );
+
+		$this->assertSame( $headers + $default_headers, $email->get_headers() );
+	}
+
+	public function test_validation() {
+		$email = new BP_Email( 'activity-at-message' );
+		$email->set_from( 'test1@example.com' )->set_to( 'test2@example.com' )->set_subject( 'testing' );
+		$email->set_content_html( 'testing' )->set_tokens( array( 'poster.name' => 'example' ) );
+
+		$this->assertTrue( $email->validate() );
+	}
+
+	public function test_invalid_characters_are_stripped_from_tokens() {
+		$email          = new BP_Email( 'activity-at-message' );
+		$default_tokens = $email->get_tokens();
+
+		$email->set_tokens( array( 'te{st}1' => 'hello world' ) );
+
+		$this->assertSame(
+			array_keys( array( 'test1' => 'hello world' ) + $default_tokens ),
+			array_keys( $email->get_tokens() )
+		);
+	}
+
+	public function test_token_are_escaped() {
+		$token = '<blink>';
+		$email = new BP_Email( 'activity-at-message' );
+		$email->set_content_html( '{{test}}' )->set_tokens( array( 'test' => $token ) );
+
+		$this->assertSame(
+			esc_html( $token ),
+			$email->get_content( 'replace-tokens' )
+		);
+	}
+
+	public function test_token_are_not_escaped() {
+		$token = '<blink>';
+		$email = new BP_Email( 'activity-at-message' );
+		$email->set_content_html( '{{{test}}}' )->set_tokens( array( 'test' => $token ) );
+
+		$this->assertSame(
+			$token,
+			$email->get_content( 'replace-tokens' )
+		);
+	}
+
+	public function test_invalid_headers() {
+		$email           = new BP_Email( 'activity-at-message' );
+		$default_headers = $email->get_headers();
+		$headers         = array( 'custom:header' => 'custom:value' );
+
+		$email->set_headers( $headers );
+
+		$this->assertNotSame( $headers + $default_headers, $email->get( 'headers' ) );
+		$this->assertSame( array( 'customheader' => 'customvalue' ) + $default_headers, $email->get( 'headers' ) );
+	}
+
+	public function test_validation_with_missing_required_data() {
+		$email  = new BP_Email( 'activity-at-message' );
+		$email->set_from( 'test1@example.com' )->set_to( 'test2@example.com' )->set_subject( 'testing' );  // Content
+		$email->set_tokens( array( 'poster.name' => 'example' ) );
+		$result = $email->validate();
+
+		$this->assertTrue( is_wp_error( $result ) );
+		$this->assertSame( 'missing_parameter', $result->get_error_code() );
+	}
+
+	public function test_validation_with_missing_template() {
+		$email  = new BP_Email( 'activity-at-message' );
+		$email->set_from( 'test1@example.com' )->set_to( 'test2@example.com' )->set_subject( 'testing' );
+		$email->set_content_html( 'testing' )->set_tokens( array( 'poster.name' => 'example' ) )->set_template( '' );
+		$result = $email->validate();
+
+		// Template has a default value, but it can't be blank.
+		$this->assertTrue( is_wp_error( $result ) );
+		$this->assertSame( 'missing_parameter', $result->get_error_code() );
+	}
+
+	public function test_invalid_tags_should_be_removed_from_html_content() {
+		$message = '<b>hello world</b><iframe src="https://example.com"></iframe><b>hello world</b>';
+		$email   = new BP_Email( 'activity-at-message' );
+
+		$email->set_content_html( $message );
+		$email->set_content_type( 'html' );
+
+		$this->assertSame( '<b>hello world</b><b>hello world</b>', $email->get_content() );
+	}
+
+	public function test_multiple_recipients_are_supported_by_address() {
+		$email1 = 'test1@example.com';
+		$email2 = 'test2@example.com';
+		$email  = new BP_Email( 'activity-at-message' );
+
+		$email->set_to( array( $email1, $email2 ) );
+		$addresses = $email->get_to();
+
+		$this->assertCount( 2, $addresses );
+		$this->assertSame( $email1, $addresses[0]->get_address() );
+		$this->assertSame( $email2, $addresses[1]->get_address() );
+	}
+
+	public function test_multiple_recipients_are_supported_by_wp_user_object() {
+		$user1 = get_user_by( 'id', $this->u1 );
+		$user2 = get_user_by( 'id', $this->u2 );
+		$email = new BP_Email( 'activity-at-message' );
+
+		$email->set_to( array( $user1, $user2 ) );
+		$addresses = $email->get_to();
+
+		$this->assertCount( 2, $addresses );
+		$this->assertSame( $user1->user_email, $addresses[0]->get_address() );
+		$this->assertSame( $user2->user_email, $addresses[1]->get_address() );
+	}
+
+	public function test_multiple_recipients_are_supported_by_wp_user_id() {
+		$user1 = get_user_by( 'id', $this->u1 );
+		$user2 = get_user_by( 'id', $this->u2 );
+		$email = new BP_Email( 'activity-at-message' );
+		$email->set_to( array( $this->u1, $this->u2 ) );
+		$addresses = $email->get_to();
+
+		$this->assertCount( 2, $addresses );
+		$this->assertSame( $user1->user_email, $addresses[0]->get_address() );
+		$this->assertSame( $user2->user_email, $addresses[1]->get_address() );
+	}
+
+	public function test_multiple_recipients_are_supported() {
+		$user1 = get_user_by( 'id', $this->u1 );
+		$user2 = get_user_by( 'id', $this->u2 );
+		$user3 = 'test3@example.com';
+		$email = new BP_Email( 'activity-at-message' );
+
+		$email->set_to( array( $user1, $this->u2, $user3 ) );
+		$addresses = $email->get_to();
+
+		$this->assertCount( 3, $addresses );
+		$this->assertSame( $user1->user_email, $addresses[0]->get_address() );
+		$this->assertSame( $user2->user_email, $addresses[1]->get_address() );
+		$this->assertSame( $user3,             $addresses[2]->get_address() );
+	}
+
+	public function test_replacing_existing_recipients_with_new_recipients() {
+		$email              = new BP_Email( 'activity-at-message' );
+		$original_recipient = 'test1@example.com';
+		$new_recipient      = 'test2@example.com';
+
+		$email->set_to( $original_recipient );
+		$addresses = $email->get_to();
+		$this->assertSame( $original_recipient, $addresses[0]->get_address() );
+
+		$email->set_to( $new_recipient );
+		$addresses = $email->get_to();
+		$this->assertSame( $new_recipient, $addresses[0]->get_address() );
+	}
+
+	public function test_appending_new_recipients_to_existing_recipients() {
+		$email              = new BP_Email( 'activity-at-message' );
+		$original_recipient = 'test1@example.com';
+		$new_recipient      = 'test2@example.com';
+
+		$email->set_to( $original_recipient );
+		$addresses = $email->get_to();
+		$this->assertSame( $original_recipient, $addresses[0]->get_address() );
+
+		$email->set_to( $new_recipient, '', 'add' );
+		$addresses = $email->get_to();
+		$this->assertSame( $original_recipient, $addresses[0]->get_address() );
+		$this->assertSame( $new_recipient, $addresses[1]->get_address() );
+	}
+
+	public function test_sending_email() {
+		require_once( BP_PLUGIN_DIR . '/bp-core/admin/bp-core-admin-schema.php' );
+		bp_core_install_emails();
+
+		$user1  = get_user_by( 'id', $this->u1 );
+		$result = bp_send_email( 'activity-comment', $this->u1, array(
+			'tokens' => array(
+				'comment.id'                => 123,
+				'commenter.id'              => $this->u2,
+				'usermessage'               => 'hello world',
+				'original_activity.user_id' => $this->u1,
+				'poster.name'               => 'name',
+				'thread.url'                => 'http://example.com',
+				'unsubscribe'               => 'http://example.com',
+			),
+		) );
+
+		$this->assertTrue( $result );
+	}
+
+	public function test_html_entities_are_decoded_in_email_subject() {
+		// Emulate custom post title for an email post type.
+		$subject = "It's pretty <new & magical.";
+
+		$email = new BP_Email( 'activity-at-message' );
+		$email->set_subject( $subject )->set_tokens( array( 'poster.name' => 'blah' ) );
+
+		// Subject always has to have tokens replaced before sending.
+		$this->assertSame( $subject, $email->get_subject( 'replace-tokens' ) );
+	}
+
+	public function test_html_entities_are_decoded_in_email_recipient_names() {
+		// Raw display name.
+		$name = "Test o'Toole";
+
+		// Emulate rendered {poster.name} token.
+		$token = apply_filters( 'bp_core_get_user_displayname', $name );
+
+		$email = new BP_Email( 'activity-at-message' );
+		$email->set_subject( '{{poster.name}}' )->set_tokens( array( 'poster.name' => $token ) );
+
+		// Subject always has to have tokens replaced before sending.
+		$this->assertSame( $name, $email->get_subject( 'replace-tokens' ) );
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-media-extractor.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-media-extractor.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-media-extractor.php	(revision 12265)
@@ -0,0 +1,483 @@
+<?php
+/**
+ * @group core
+ * @group BP_Media_Extractor
+ */
+class BP_Tests_Media_Extractor extends BP_UnitTestCase {
+	public static $media_extractor = null;
+	public static $richtext        = '';
+
+	public static function wpSetUpBeforeClass( $f ) {
+		self::$media_extractor = new BP_Media_Extractor();
+		self::$richtext        = "Hello world.
+
+		This sample text is used to test the media extractor parsing class. @paulgibbs thinks it's pretty cool.
+		Another thing really cool is this @youtube:
+
+		https://www.youtube.com/watch?v=2mjvfnUAfyo
+
+		This video is literally out of the world, but uses a different protocol to the embed above:
+
+		http://www.youtube.com/watch?v=KaOC9danxNo
+
+		<a href='https://example.com'>Testing a regular link.</a>
+		<strong>But we should throw in some markup and maybe even an <img src='http://example.com/image.gif'>.
+		<a href='http://example.com'><img src='http://example.com/image-in-a-link.gif' /></a></strong>.
+		It definitely does not like <img src='data:1234567890A'>data URIs</img>. @
+
+		The parser only extracts wp_allowed_protocols() protocols, not something like <a href='phone:004400'>phone</a>.
+
+		[caption id='example']Here is a caption shortcode.[/caption]
+
+		There are two types of [gallery] shortcodes; one like that, and another with IDs specified.
+
+		Audio shortcodes:
+		[audio src='http://example.com/source.mp3']
+		[audio src='http://example.com/source.wav' loop='on' autoplay='off' preload='metadata'].
+
+		The following shortcode should be picked up by the shortcode extractor, but not the audio extractor, because
+		it has an unrecognised file extension (for an audio file). [audio src='http://example.com/not_audio.gif']
+		<a href='http://example.com/more_audio.mp3'>This should be picked up, too</a>.
+
+		Video shortcodes:
+		[video src='http://example.com/source.ogv']
+		[video src='http://example.com/source.webm' loop='on' autoplay='off' preload='metadata']
+
+		The following shortcode should be picked up by the shortcode extractor, but not the video extractor, because
+		it has an unrecognised file extension (for a video file). [video src='http://example.com/not_video.mp3']
+		";
+	}
+
+
+	/**
+	 * General.
+	 */
+
+	public function test_check_media_extraction_return_types() {
+		$media = self::$media_extractor->extract( self::$richtext );
+
+		foreach ( array( 'has', 'embeds', 'images', 'links', 'mentions', 'shortcodes', 'audio' ) as $key ) {
+			$this->assertArrayHasKey( $key, $media );
+			$this->assertInternalType( 'array', $media[ $key ] );
+		}
+
+		foreach ( $media['has'] as $item ) {
+			$this->assertInternalType( 'int', $item );
+		}
+
+		foreach ( $media['links'] as $item ) {
+			$this->assertArrayHasKey( 'url', $item );
+			$this->assertInternalType( 'string', $item['url'] );
+			$this->assertNotEmpty( $item['url'] );
+		}
+
+		foreach ( $media['mentions'] as $item ) {
+			$this->assertArrayHasKey( 'name', $item );
+			$this->assertInternalType( 'string', $item['name'] );
+			$this->assertNotEmpty( $item['name'] );
+		}
+
+		foreach ( $media['images'] as $item ) {
+			$this->assertArrayHasKey( 'height', $item );
+			$this->assertInternalType( 'int', $item['height'] );
+
+			$this->assertArrayHasKey( 'width', $item );
+			$this->assertInternalType( 'int', $item['width'] );
+
+			$this->assertArrayHasKey( 'source', $item );
+			$this->assertInternalType( 'string', $item['source'] );
+			$this->assertNotEmpty( $item['source'] );
+
+			$this->assertArrayHasKey( 'url', $item );
+			$this->assertInternalType( 'string', $item['url'] );
+			$this->assertNotEmpty( $item['url'] );
+		}
+
+		foreach ( $media['shortcodes'] as $shortcode_type => $item ) {
+			$this->assertArrayHasKey( 'attributes', $item );
+			$this->assertInternalType( 'array', $item['attributes'] );
+
+			$this->assertArrayHasKey( 'content', $item );
+			$this->assertInternalType( 'string', $item['content'] );
+
+			$this->assertArrayHasKey( 'type', $item );
+			$this->assertInternalType( 'string', $item['type'] );
+
+			$this->assertArrayHasKey( 'original', $item );
+			$this->assertInternalType( 'string', $item['original'] );
+		}
+
+		foreach ( $media['embeds'] as $item ) {
+			$this->assertArrayHasKey( 'url', $item );
+			$this->assertInternalType( 'string', $item['url'] );
+			$this->assertNotEmpty( $item['url'] );
+		}
+
+		foreach ( $media['audio'] as $item ) {
+			$this->assertArrayHasKey( 'url', $item );
+			$this->assertInternalType( 'string', $item['url'] );
+			$this->assertNotEmpty( $item['url'] );
+
+			$this->assertArrayHasKey( 'source', $item );
+			$this->assertInternalType( 'string', $item['source'] );
+			$this->assertNotEmpty( $item['source'] );
+		}
+	}
+
+	public function test_check_media_extraction_counts_are_correct() {
+		$media = self::$media_extractor->extract( self::$richtext );
+		$types = array_keys( $media );
+
+		foreach ( $types as $type ) {
+			if ( $type === 'has' ) {
+				continue;
+			}
+
+			$this->assertArrayHasKey( $type, $media['has'] );
+			$this->assertSame( count( $media[ $type ] ), $media['has'][ $type ], "Difference with the 'has' count for {$type}." );
+		}
+	}
+
+
+	public function test_extract_multiple_media_types_from_content() {
+		self::factory()->user->create( array( 'user_login' => 'paulgibbs' ) );
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::LINKS | BP_Media_Extractor::MENTIONS );
+
+		$this->assertNotEmpty( $media['links'] );
+		$this->assertNotEmpty( $media['mentions'] );
+		$this->assertArrayNotHasKey( 'shortcodes', $media );
+	}
+
+	public function test_extract_media_from_a_wp_post() {
+		$post_id = self::factory()->post->create( array( 'post_content' => self::$richtext ) );
+		$media   = self::$media_extractor->extract( get_post( $post_id ), BP_Media_Extractor::LINKS );
+
+		$this->assertArrayHasKey( 'links', $media );
+		$this->assertSame( 'https://example.com', $media['links'][0]['url'] );
+		$this->assertSame( 'http://example.com',  $media['links'][1]['url'] );
+	}
+
+
+	/**
+	 * Link extraction.
+	 */
+
+	public function test_extract_links_from_content() {
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::LINKS );
+
+		$this->assertArrayHasKey( 'links', $media );
+		$this->assertSame( 'https://example.com', $media['links'][0]['url'] );
+		$this->assertSame( 'http://example.com',  $media['links'][1]['url'] );
+	}
+
+	public function test_extract_no_links_from_content_with_invalid_links() {
+		$richtext = "This is some sample text, with links, but not the kinds we want.
+		<a href=''>Empty links should be ignore<a/> and
+		<a href='phone:004400'>weird protocols should be ignored, too</a>.
+		";
+
+		$media = self::$media_extractor->extract( $richtext, BP_Media_Extractor::LINKS );
+		$this->assertSame( 0, $media['has']['links'] );
+	}
+
+
+	/**
+	 * at-mentions extraction.
+	 */
+
+	public function test_extract_mentions_from_content_with_activity_enabled() {
+		self::factory()->user->create( array( 'user_login' => 'paulgibbs' ) );
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::MENTIONS );
+
+		$this->assertArrayHasKey( 'user_id', $media['mentions'][0] );
+		$this->assertSame( 'paulgibbs', $media['mentions'][0]['name'] );
+	}
+
+	public function test_extract_mentions_from_content_with_activity_disabled() {
+		self::factory()->user->create( array( 'user_login' => 'paulgibbs' ) );
+		$was_activity_enabled = false;
+
+		// Turn activity off.
+		if ( isset( buddypress()->active_components['activity'] ) ) {
+			unset( buddypress()->active_components['activity'] );
+			$was_activity_enabled = true;
+		}
+
+
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::MENTIONS );
+
+		$this->assertArrayNotHasKey( 'user_id', $media['mentions'][0] );
+		$this->assertSame( 'paulgibbs', $media['mentions'][0]['name'] );
+
+
+		// Turn activity on.
+		if ( $was_activity_enabled ) {
+			buddypress()->active_components['activity'] = 1;
+		}
+	}
+
+
+	/**
+	 * Shortcodes extraction.
+	 */
+
+	public function test_extract_shortcodes_from_content() {
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::SHORTCODES );
+
+		$this->assertArrayHasKey( 'shortcodes', $media );
+
+		$this->assertSame( 'caption', $media['shortcodes'][0]['type'] );
+		$this->assertSame( 'Here is a caption shortcode.', $media['shortcodes'][0]['content'] );
+		$this->assertSame( 'example', $media['shortcodes'][0]['attributes']['id'] );
+
+		$this->assertSame( 'gallery', $media['shortcodes'][1]['type'] );
+		$this->assertEmpty( $media['shortcodes'][1]['content'] );
+
+		$this->assertSame( 'audio', $media['shortcodes'][2]['type'] );
+		$this->assertEmpty( $media['shortcodes'][2]['content'] );
+		$this->assertSame( 'http://example.com/source.mp3', $media['shortcodes'][2]['attributes']['src'] );
+
+		$this->assertSame( 'audio', $media['shortcodes'][3]['type'] );
+		$this->assertEmpty( $media['shortcodes'][3]['content'] );
+		$this->assertSame( 'http://example.com/source.wav', $media['shortcodes'][3]['attributes']['src'] );
+		$this->assertSame( 'on', $media['shortcodes'][3]['attributes']['loop'] );
+		$this->assertSame( 'off', $media['shortcodes'][3]['attributes']['autoplay'] );
+		$this->assertSame( 'metadata', $media['shortcodes'][3]['attributes']['preload'] );
+	}
+
+	public function test_extract_no_shortcodes_from_content_with_unregistered_shortcodes() {
+		$richtext = 'This sample text has some made-up [fake]shortcodes[/fake].';
+
+		$media = self::$media_extractor->extract( $richtext, BP_Media_Extractor::SHORTCODES );
+		$this->assertSame( 0, $media['has']['shortcodes'] );
+	}
+
+
+	/**
+	 * oEmbeds extraction.
+	 */
+
+	public function test_extract_oembeds_from_content() {
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::EMBEDS );
+
+		$this->assertArrayHasKey( 'embeds', $media );
+		$this->assertSame( 'https://www.youtube.com/watch?v=2mjvfnUAfyo', $media['embeds'][0]['url'] );
+		$this->assertSame( 'http://www.youtube.com/watch?v=KaOC9danxNo',  $media['embeds'][1]['url'] );
+	}
+
+
+	/**
+	 * Images extraction (src tags).
+	 */
+
+	// both quote styles
+	public function test_extract_images_from_content_with_src_tags() {
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::IMAGES );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'html' ) ) );
+
+		$this->assertSame( 'http://example.com/image.gif',           $media[0]['url'] );
+		$this->assertSame( 'http://example.com/image-in-a-link.gif', $media[1]['url'] );
+	}
+
+	// empty src attributes, data: URIs
+	public function test_extract_no_images_from_content_with_invalid_src_tags() {
+		$richtext = 'This sample text will contain images with invalid src tags, like this:
+		<img src="data://abcd"> or <img src="phone://0123" />.
+		';
+
+		$media = self::$media_extractor->extract( $richtext, BP_Media_Extractor::IMAGES );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$this->assertSame( 0, $media['has']['images'] );
+	}
+
+
+	/**
+	 * Images extraction (galleries).
+	 */
+
+	public function test_extract_images_from_content_with_galleries_variant_no_ids() {
+		// To test the [gallery] shortcode, we need to create a post and an attachment.
+		$post_id       = self::factory()->post->create( array( 'post_content' => self::$richtext ) );
+		$attachment_id = self::factory()->attachment->create_object( 'image.jpg', $post_id, array(
+			'post_mime_type' => 'image/jpeg',
+			'post_type'      => 'attachment'
+		) );
+		wp_update_attachment_metadata( $attachment_id, array( 'width' => 100, 'height' => 100 ) );
+
+
+		// Extract the gallery images.
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::IMAGES, array(
+			'post' => get_post( $post_id ),
+		) );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'galleries' ) ) );
+		$this->assertCount( 1, $media );
+
+		$this->assertSame( WP_CONTENT_URL . '/uploads/image.jpg', $media[0]['url'] );
+	}
+
+	public function test_extract_images_from_content_with_galleries_variant_ids() {
+		$attachment_ids   = array();
+		$attachment_ids[] = $this->fake_attachment_upload( DIR_TESTDATA . '/images/test-image-large.png' );
+		$attachment_ids[] = $this->fake_attachment_upload( DIR_TESTDATA . '/images/canola.jpg' );
+		$attachment_ids   = join( ',', $attachment_ids );
+		$post_id          = self::factory()->post->create( array( 'post_content' => "[gallery ids='{$attachment_ids}']" ) );
+
+		// Extract the gallery images.
+		$media = self::$media_extractor->extract( '', BP_Media_Extractor::IMAGES, array(
+			'post' => get_post( $post_id ),
+		) );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'galleries' ) ) );
+		$this->assertCount( 2, $media );
+
+		$this->assertSame( WP_CONTENT_URL . '/uploads/test-image-large.png', $media[0]['url'] );
+		$this->assertSame( WP_CONTENT_URL . '/uploads/canola.jpg', $media[1]['url'] );
+	}
+
+	public function test_extract_no_images_from_content_with_invalid_galleries_variant_no_ids() {
+		$post_id = self::factory()->post->create( array( 'post_content' => self::$richtext ) );
+		$media   = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::IMAGES, array(
+			'post' => get_post( $post_id ),
+		) );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'galleries' ) ) );
+		$this->assertCount( 0, $media );
+	}
+
+	public function test_extract_no_images_from_content_with_invalid_galleries_variant_ids() {
+		$post_id = self::factory()->post->create( array( 'post_content' => '[gallery ids="117,4529"]' ) );
+		$media   = self::$media_extractor->extract( '', BP_Media_Extractor::IMAGES, array(
+			'post' => get_post( $post_id ),
+		) );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'galleries' ) ) );
+		$this->assertCount( 0, $media );
+	}
+
+
+	/**
+	 * Images extraction (thumbnail).
+	 */
+
+	public function test_extract_no_images_from_content_with_featured_image() {
+		$post_id      = self::factory()->post->create( array( 'post_content' => self::$richtext ) );
+		$thumbnail_id = self::factory()->attachment->create_object( 'image.jpg', $post_id, array(
+			'post_mime_type' => 'image/jpeg',
+			'post_type'      => 'attachment'
+		) );
+		set_post_thumbnail( $post_id, $thumbnail_id );
+
+
+		// Extract the gallery images.
+		$media = self::$media_extractor->extract( '', BP_Media_Extractor::IMAGES, array(
+			'post' => get_post( $post_id ),
+		) );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'featured_images' ) ) );
+		$this->assertCount( 1, $media );
+
+		$this->assertSame( WP_CONTENT_URL . '/uploads/image.jpg', $media[0]['url'] );
+	}
+
+	public function test_extract_images_from_content_without_featured_image() {
+		$post_id = self::factory()->post->create( array( 'post_content' => self::$richtext ) );
+		$media   = self::$media_extractor->extract( '', BP_Media_Extractor::IMAGES, array(
+			'post' => get_post( $post_id ),
+		) );
+
+		$this->assertArrayHasKey( 'images', $media );
+		$media = array_values( wp_list_filter( $media['images'], array( 'source' => 'featured_images' ) ) );
+		$this->assertCount( 0, $media );
+	}
+
+
+	/**
+	 * Audio extraction.
+	 */
+
+	public function test_extract_audio_from_content() {
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::AUDIO );
+
+		$this->assertArrayHasKey( 'audio', $media );
+		$this->assertCount( 3, $media['audio'] );
+
+		$this->assertSame( 'shortcodes', $media['audio'][0]['source'] );
+		$this->assertSame( 'shortcodes', $media['audio'][1]['source'] );
+		$this->assertSame( 'html',       $media['audio'][2]['source'] );
+
+		$this->assertSame( 'http://example.com/source.mp3',     $media['audio'][0]['url'] );
+		$this->assertSame( 'http://example.com/source.wav',     $media['audio'][1]['url'] );
+		$this->assertSame( 'http://example.com/more_audio.mp3', $media['audio'][2]['url'] );
+	}
+
+	public function test_extract_audio_shortcode_with_no_src_param() {
+		$richtext = '[audio http://example.com/a-song.mp3]';
+		$media = self::$media_extractor->extract( $richtext, BP_Media_Extractor::AUDIO );
+
+		$this->assertArrayHasKey( 'audio', $media );
+		$this->assertCount( 1, $media['audio'] );
+		$this->assertSame( 'http://example.com/a-song.mp3', $media['audio'][0]['url'] );
+	}
+
+	public function test_extract_no_audio_from_invalid_content() {
+		$richtext = '[audio src="http://example.com/not_audio.gif"]
+		<a href="http://example.com/more_not_audio.mp33">Hello</a>.';
+
+		$media = self::$media_extractor->extract( $richtext, BP_Media_Extractor::AUDIO );
+		$this->assertSame( 0, $media['has']['audio'] );
+	}
+
+	public function test_extract_no_audio_from_empty_audio_shortcode() {
+		$media = self::$media_extractor->extract( '[audio]', BP_Media_Extractor::AUDIO );
+		$this->assertSame( 0, $media['has']['audio'] );
+	}
+
+
+	/**
+	 * Video extraction.
+	 */
+
+	public function test_extract_video_from_content() {
+		$media = self::$media_extractor->extract( self::$richtext, BP_Media_Extractor::VIDEOS );
+
+		$this->assertArrayHasKey( 'videos', $media );
+		$this->assertCount( 2, $media['videos'] );
+
+		$this->assertSame( 'shortcodes', $media['videos'][0]['source'] );
+		$this->assertSame( 'shortcodes', $media['videos'][1]['source'] );
+
+		$this->assertSame( 'http://example.com/source.ogv',  $media['videos'][0]['url'] );
+		$this->assertSame( 'http://example.com/source.webm', $media['videos'][1]['url'] );
+	}
+
+
+	public function test_extract_video_shortcode_with_no_src_param() {
+		$richtext = '[video http://example.com/source.ogv]';
+		$media = self::$media_extractor->extract( $richtext, BP_Media_Extractor::VIDEOS );
+
+		$this->assertArrayHasKey( 'videos', $media );
+		$this->assertCount( 1, $media['videos'] );
+		$this->assertSame( 'http://example.com/source.ogv', $media['videos'][0]['url'] );
+	}
+
+	public function test_extract_no_video_from_invalid_content() {
+		$richtext = '[video src="http://example.com/not_video.mp3"]';
+		$media    = self::$media_extractor->extract( $richtext, BP_Media_Extractor::VIDEOS );
+
+		$this->assertSame( 0, $media['has']['videos'] );
+	}
+
+	public function test_extract_no_videos_from_empty_video_shortcodes() {
+		$media = self::$media_extractor->extract( '[video]', BP_Media_Extractor::VIDEOS );
+		$this->assertSame( 0, $media['has']['videos'] );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-user-query.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-user-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/class-bp-user-query.php	(revision 12265)
@@ -0,0 +1,764 @@
+<?php
+
+/**
+ * @group core
+ * @group BP_User_Query
+ */
+class BP_Tests_BP_User_Query_TestCases extends BP_UnitTestCase {
+	/**
+	 * Checks that user_id returns friends
+	 */
+	public function test_bp_user_query_friends() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		friends_add_friend( $u1, $u2, true );
+
+		$q = new BP_User_Query( array(
+			'user_id' => $u2,
+		) );
+
+		$friends = is_array( $q->results ) ? array_values( $q->results ) : array();
+		$friend_ids = wp_list_pluck( $friends, 'ID' );
+		$this->assertEquals( $friend_ids, array( $u1 ) );
+	}
+
+	/**
+	 * @ticket BP4938
+	 */
+	public function test_bp_user_query_friends_with_include() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$u4 = self::factory()->user->create();
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, true );
+
+		$q = new BP_User_Query( array(
+			'user_id' => $u1,
+
+			// Represents an independent filter passed by a plugin
+			// u4 is not a friend of u1 and should not be returned
+			'include' => array( $u2, $u4 ),
+		) );
+
+		$friends = is_array( $q->results ) ? array_values( $q->results ) : array();
+		$friend_ids = wp_list_pluck( $friends, 'ID' );
+		$this->assertEquals( $friend_ids, array( $u2 ) );
+	}
+
+	public function test_bp_user_query_friends_with_include_but_zero_friends() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$u4 = self::factory()->user->create();
+
+		$q = new BP_User_Query( array(
+			'user_id' => $u1,
+
+			// Represents an independent filter passed by a plugin
+			// u4 is not a friend of u1 and should not be returned
+			'include' => array( $u2, $u4 ),
+		) );
+
+		$friends = is_array( $q->results ) ? array_values( $q->results ) : array();
+		$friend_ids = wp_list_pluck( $friends, 'ID' );
+		$this->assertEquals( $friend_ids, array() );
+	}
+
+	/**
+	 * @ticket BP7248
+	 */
+	public function test_include_array_contaning_only_0_should_result_in_no_results_query() {
+		$q = new BP_User_Query( array(
+			'include' => array( 0 ),
+		) );
+
+		$this->assertContains( '0 = 1', $q->uid_clauses['where'] );
+	}
+
+	/**
+	 * @ticket BP7248
+	 */
+	public function test_include_array_contaning_0_but_also_real_IDs_should_not_result_in_no_results_query() {
+		$q = new BP_User_Query( array(
+			'include' => array( 0, 1 ),
+		) );
+
+		$this->assertNotContains( '0 = 1', $q->uid_clauses['where'] );
+	}
+
+	/**
+	 * @group user_ids
+	 */
+	public function test_bp_user_query_user_ids_with_invalid_user_id() {
+		$now = time();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// invalid user ID
+		$u3 = $u2 + 1;
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+
+		// pass 'user_ids' to user query to trigger this bug
+		$q = new BP_User_Query( array(
+			'user_ids' => array( $u2, $u3 )
+		) );
+
+		// $q->user_ids property should now not contain invalid user IDs
+		$this->assertNotContains( $u3, $q->user_ids );
+
+		// clean up
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_bp_user_query_sort_by_popular() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$u4 = self::factory()->user->create();
+
+		bp_update_user_meta( $u1, bp_get_user_meta_key( 'total_friend_count' ), '5' );
+		bp_update_user_meta( $u2, bp_get_user_meta_key( 'total_friend_count' ), '90' );
+		bp_update_user_meta( $u3, bp_get_user_meta_key( 'total_friend_count' ), '101' );
+		bp_update_user_meta( $u4, bp_get_user_meta_key( 'total_friend_count' ), '3002' );
+
+		$q = new BP_User_Query( array(
+			'type' => 'popular',
+		) );
+
+		$users = is_array( $q->results ) ? array_values( $q->results ) : array();
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users, 'ID' ) );
+
+		$expected = array( $u4, $u3, $u2, $u1 );
+		$this->assertEquals( $expected, $user_ids );
+	}
+
+	/**
+	 * @group online
+	 */
+	public function test_bp_user_query_type_online() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*13 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*16 ),
+		) );
+
+		$q = new BP_User_Query( array(
+			'type' => 'online',
+		) );
+
+		$users = is_array( $q->results ) ? array_values( $q->results ) : array();
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users, 'ID' ) );
+		$this->assertEquals( array( $u1, $u2 ), $user_ids );
+	}
+
+	/**
+	 * @group online
+	 */
+	public function test_bp_user_query_type_online_five_minute_interval() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*4 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*6 ),
+		) );
+
+		add_filter( 'bp_user_query_online_interval', function() { return 5; } );
+
+		$q = new BP_User_Query( array(
+			'type' => 'online',
+		) );
+
+		$users = is_array( $q->results ) ? array_values( $q->results ) : array();
+		$user_ids = wp_parse_id_list( wp_list_pluck( $users, 'ID' ) );
+		$this->assertEquals( array( $u1, $u2 ), $user_ids );
+	}
+
+
+	public function test_bp_user_query_search_with_apostrophe() {
+		// Apostrophe. Search_terms must escaped to mimic POST payload
+		$user_id = self::factory()->user->create();
+		xprofile_set_field_data( 1, $user_id, "Foo'Bar" );
+		$q = new BP_User_Query( array( 'search_terms' => "oo\'Ba", ) );
+
+		$found_user_id = null;
+		if ( ! empty( $q->results ) ) {
+			$found_user = array_pop( $q->results );
+			$found_user_id = $found_user->ID;
+		}
+
+		$this->assertEquals( $user_id, $found_user_id );
+	}
+
+	public function test_bp_user_query_search_with_percent_sign() {
+
+		// LIKE special character: %
+		$user_id = self::factory()->user->create();
+		xprofile_set_field_data( 1, $user_id, "Foo%Bar" );
+		$q = new BP_User_Query( array( 'search_terms' => "oo%Bar", ) );
+
+		$found_user_id = null;
+		if ( ! empty( $q->results ) ) {
+			$found_user = array_pop( $q->results );
+			$found_user_id = $found_user->ID;
+		}
+
+		$this->assertEquals( $user_id, $found_user_id );
+
+	}
+
+	public function test_bp_user_query_search_with_underscore() {
+
+		// LIKE special character: _
+		$user_id = self::factory()->user->create();
+		xprofile_set_field_data( 1, $user_id, "Foo_Bar" );
+		$q = new BP_User_Query( array( 'search_terms' => "oo_Bar", ) );
+
+		$found_user_id = null;
+		if ( ! empty( $q->results ) ) {
+			$found_user = array_pop( $q->results );
+			$found_user_id = $found_user->ID;
+		}
+
+		$this->assertEquals( $user_id, $found_user_id );
+	}
+
+	public function test_bp_user_query_search_with_ampersand_sign() {
+
+		// LIKE special character: &
+		$user_id = self::factory()->user->create();
+		xprofile_set_field_data( 1, $user_id, "a&mpersand" );
+		$q = new BP_User_Query( array( 'search_terms' => "a&m", ) );
+
+		$found_user_id = null;
+		if ( ! empty( $q->results ) ) {
+			$found_user = array_pop( $q->results );
+			$found_user_id = $found_user->ID;
+		}
+
+		$this->assertEquals( $user_id, $found_user_id );
+
+	}
+
+	/**
+	 * @group search_terms
+	 */
+	public function test_bp_user_query_search_core_fields() {
+		$user_id = self::factory()->user->create( array(
+			'user_login' => 'foo',
+		) );
+		xprofile_set_field_data( 1, $user_id, "Bar" );
+		$q = new BP_User_Query( array( 'search_terms' => 'foo', ) );
+
+		$found_user_id = null;
+		if ( ! empty( $q->results ) ) {
+			$found_user = array_pop( $q->results );
+			$found_user_id = $found_user->ID;
+		}
+
+		$this->assertEquals( $user_id, $found_user_id );
+	}
+
+	public function test_bp_user_query_search_wildcards() {
+		$u1 = self::factory()->user->create( array(
+			'user_login' => 'xfoo',
+		) );
+		xprofile_set_field_data( 1, $u1, "Bar" );
+		$q1 = new BP_User_Query( array( 'search_terms' => 'foo', 'search_wildcard' => 'left' ) );
+
+		$u2 = self::factory()->user->create( array(
+			'user_login' => 'foox',
+		) );
+		xprofile_set_field_data( 1, $u2, "Bar" );
+		$q2 = new BP_User_Query( array( 'search_terms' => 'foo', 'search_wildcard' => 'right' ) );
+
+		$u3 = self::factory()->user->create( array(
+			'user_login' => 'xfoox',
+		) );
+		xprofile_set_field_data( 1, $u3, "Bar" );
+		$q3 = new BP_User_Query( array( 'search_terms' => 'foo', 'search_wildcard' => 'both' ) );
+
+		$this->assertNotEmpty( $q1->results );
+		$q1 = array_pop( $q1->results );
+		$this->assertEquals( $u1, $q1->ID );
+
+		$this->assertNotEmpty( $q2->results );
+		$q2 = array_pop( $q2->results );
+		$this->assertEquals( $u2, $q2->ID );
+
+		$this->assertNotEmpty( $q3->results );
+		foreach ( $q3->results as $user ) {
+			$this->assertTrue( in_array( $user->ID, array( $u1, $u2, $u3 ) ) );
+		}
+	}
+
+	/**
+	 * @group exclude
+	 */
+	public function test_bp_user_query_with_exclude() {
+		// Grab list of existing users who should also be excluded
+		global $wpdb;
+		$existing_users = $wpdb->get_col( "SELECT ID FROM {$wpdb->users}" );
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$exclude = array_merge( array( $u1 ), $existing_users );
+		$q = new BP_User_Query( array( 'exclude' => $exclude, ) );
+
+		$found_user_ids = null;
+		if ( ! empty( $q->results ) ) {
+			$found_user_ids = array_values( wp_parse_id_list( wp_list_pluck( $q->results, 'ID' ) ) );
+		}
+
+		$this->assertEquals( array( $u2 ), $found_user_ids );
+	}
+
+	/**
+	 * @group type
+	 * @group spam
+	 */
+	public function test_bp_user_query_type_alphabetical_spam_xprofileon() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		// Make sure xprofile is on
+		$xprofile_toggle = isset( buddypress()->active_components['xprofile'] );
+		buddypress()->active_components['xprofile'] = 1;
+		add_filter( 'bp_disable_profile_sync', '__return_false' );
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		global $wpdb;
+		bp_core_process_spammer_status( $u1, 'spam' );
+
+		$q = new BP_User_Query( array( 'type' => 'alphabetical', ) );
+
+		// Restore xprofile setting
+		if ( $xprofile_toggle ) {
+			buddypress()->active_components['xprofile'] = 1;
+		} else {
+			unset( buddypress()->active_components['xprofile'] );
+		}
+		remove_filter( 'bp_disable_profile_sync', '__return_false' );
+
+		$found_user_ids = null;
+
+		if ( ! empty( $q->results ) ) {
+			$found_user_ids = array_values( wp_parse_id_list( wp_list_pluck( $q->results, 'ID' ) ) );
+		}
+
+		// Do a assertNotContains because there are weird issues with user #1 as created by WP
+		$this->assertNotContains( $u1, $found_user_ids );
+	}
+
+	/**
+	 * @group type
+	 * @group spam
+	 */
+	public function test_bp_user_query_type_alphabetical_spam_xprofileoff() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// Make sure xprofile and profile sync are off
+		$xprofile_toggle = isset( buddypress()->active_components['xprofile'] );
+		buddypress()->active_components['xprofile'] = 0;
+		add_filter( 'bp_disable_profile_sync', '__return_false' );
+
+		bp_core_process_spammer_status( $u1, 'spam' );
+
+		$q = new BP_User_Query( array( 'type' => 'alphabetical', ) );
+
+		// Restore xprofile setting
+		if ( $xprofile_toggle ) {
+			buddypress()->active_components['xprofile'] = 1;
+		} else {
+			unset( buddypress()->active_components['xprofile'] );
+		}
+		remove_filter( 'bp_disable_profile_sync', '__return_false' );
+
+		$found_user_ids = null;
+
+		if ( ! empty( $q->results ) ) {
+			$found_user_ids = array_values( wp_parse_id_list( wp_list_pluck( $q->results, 'ID' ) ) );
+		}
+
+		// Do a assertNotContains because there are weird issues with user #1 as created by WP
+		$this->assertNotContains( $u1, $found_user_ids );
+	}
+
+	/**
+	 * @group meta
+	 * @group BP5904
+	 */
+	public function test_bp_user_query_with_user_meta_argument() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		bp_update_user_meta( $u2, 'foo', 'bar' );
+
+		$q = new BP_User_Query( array(
+			'meta_key'        => 'foo',
+			'meta_value'      => 'bar',
+		) );
+
+		$found_user_ids = array_values( wp_parse_id_list( wp_list_pluck( $q->results, 'ID' ) ) );
+
+		// Do a assertNotContains because there are weird issues with user #1 as created by WP
+		$this->assertNotContains( $u1, $found_user_ids );
+		$this->assertEquals( array( $u2 ), $found_user_ids );
+	}
+
+	/**
+	 * @group meta
+	 * @group BP5904
+	 */
+	public function test_bp_user_query_with_user_meta_argument_no_user() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$q = new BP_User_Query( array(
+			'meta_key'        => 'foo',
+			'meta_value'      => 'bar',
+		) );
+
+		$found_user_ids = array_values( wp_parse_id_list( wp_list_pluck( $q->results, 'ID' ) ) );
+
+		$this->assertEmpty( $found_user_ids );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type_single_value() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type' => 'bar',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEquals( array( $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type_array_with_single_value() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type' => array( 'bar' ),
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEquals( array( $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type_comma_separated_values() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type' => 'foo, bar',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[0], $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type_array_with_multiple_values() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type' => array( 'foo', 'bar' ),
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[0], $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type_comma_separated_values_should_discard_non_existent_taxonomies() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type' => 'foo, baz',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[0] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP6334
+	 */
+	public function test_should_return_no_results_when_no_users_match_the_specified_member_type() {
+		bp_register_member_type( 'foo' );
+		$users = self::factory()->user->create_many( 3 );
+
+		$q = new BP_User_Query( array(
+			'member_type' => 'foo, baz',
+		) );
+
+		$this->assertEmpty( $q->results );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__in_single_value() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => 'bar',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEquals( array( $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__in_array_with_single_value() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => array( 'bar' ),
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEquals( array( $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__in_comma_separated_values() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => 'foo, bar',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[0], $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__in_array_with_multiple_values() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => array( 'foo', 'bar' ),
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[0], $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__in_comma_separated_values_should_discard_non_existent_taxonomies() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => 'foo, baz',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[0] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_should_return_no_results_when_no_users_match_the_specified_member_type__in() {
+		bp_register_member_type( 'foo' );
+		$users = self::factory()->user->create_many( 3 );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => 'foo, baz',
+		) );
+
+		$this->assertEmpty( $q->results );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type_should_take_precedence_over_member_type__in() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__in' => 'foo',
+			'member_type' => 'bar'
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[1] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__not_in_returns_members_from_other_types_and_members_with_no_types() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'member_type__not_in' => 'foo',
+		) );
+
+		$found = array_values( wp_list_pluck( $q->results, 'ID' ) );
+		$this->assertEqualSets( array( $users[1], $users[2] ), $found );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_should_return_no_results_when_all_users_match_the_specified_member_type__not_in() {
+		bp_register_member_type( 'foo' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'foo' );
+		bp_set_member_type( $users[2], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'member_type__not_in' => 'foo',
+		) );
+
+		$this->assertEmpty( $q->results );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__not_in_takes_precedence_over_member_type() {
+		bp_register_member_type( 'foo' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'foo' );
+		bp_set_member_type( $users[2], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'member_type__not_in' => 'foo',
+			'member_type' => 'foo'
+		) );
+
+		$this->assertEmpty( $q->results );
+	}
+
+	/**
+	 * @group member_types
+	 */
+	public function test_member_type__not_in_takes_precedence_over_member_type__in() {
+		bp_register_member_type( 'foo' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'foo' );
+		bp_set_member_type( $users[2], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'member_type__not_in' => 'foo',
+			'member_type__in' => 'foo'
+		) );
+
+		$this->assertEmpty( $q->results );
+	}
+
+	/**
+	 * @group cache
+	 * @group member_types
+	 */
+	public function test_member_type_should_be_prefetched_into_cache_during_user_query() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 4 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+		bp_set_member_type( $users[2], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'include' => $users,
+		) );
+
+		$this->assertSame( array( 'foo' ), wp_cache_get( $users[0], 'bp_member_member_type' ) );
+		$this->assertSame( array( 'bar' ), wp_cache_get( $users[1], 'bp_member_member_type' ) );
+		$this->assertSame( array( 'foo' ), wp_cache_get( $users[2], 'bp_member_member_type' ) );
+		$this->assertSame( '', wp_cache_get( $users[3], 'bp_member_member_type' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions.php	(revision 12265)
@@ -0,0 +1,842 @@
+<?php
+
+/**
+ * @group core
+ */
+
+class BP_Tests_Core_Functions extends BP_UnitTestCase {
+	/**
+	 * @group bp_esc_sql_order
+	 */
+	public function test_bp_esc_sql_order_ASC() {
+		$this->assertEquals( 'ASC', bp_esc_sql_order( 'ASC' ) );
+	}
+
+	/**
+	 * @group bp_esc_sql_order
+	 */
+	public function test_bp_esc_sql_order_DESC() {
+		$this->assertEquals( 'DESC', bp_esc_sql_order( 'DESC' ) );
+	}
+
+	/**
+	 * @group bp_esc_sql_order
+	 */
+	public function test_bp_esc_sql_order_desc_lowercase() {
+		$this->assertEquals( 'DESC', bp_esc_sql_order( 'desc' ) );
+	}
+
+	/**
+	 * @group bp_esc_sql_order
+	 */
+	public function test_bp_esc_sql_order_desc_whitespace() {
+		$this->assertEquals( 'DESC', bp_esc_sql_order( ' desc ' ) );
+	}
+
+	/**
+	 * @group bp_esc_sql_order
+	 */
+	public function test_bp_esc_sql_order_invalid() {
+		$this->assertEquals( 'ASC', bp_esc_sql_order( 'In ur base killin ur d00dz' ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_years_months() {
+		$now = time();
+		$then = $now - ( 3 * YEAR_IN_SECONDS ) - ( 3 * 30 * DAY_IN_SECONDS );
+		$this->assertEquals( '3 years, 3 months ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_years_nomonths() {
+		$now = time();
+		$then = $now - ( 3 * YEAR_IN_SECONDS );
+		$this->assertEquals( '3 years ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_months_weeks() {
+		$now = time();
+		$then = $now - ( 3 * 30 * DAY_IN_SECONDS ) - ( 3 * WEEK_IN_SECONDS );
+		$this->assertEquals( '3 months, 3 weeks ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_months_noweeks() {
+		$now = time();
+		$then = $now - ( 3 * 30 * DAY_IN_SECONDS );
+		$this->assertEquals( '3 months ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_weeks_days() {
+		$now = time();
+		$then = $now - ( 3 * WEEK_IN_SECONDS ) - ( 3 * DAY_IN_SECONDS );
+		$this->assertEquals( '3 weeks, 3 days ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_weeks_nodays() {
+		$now = time();
+		$then = $now - ( 3 * WEEK_IN_SECONDS );
+		$this->assertEquals( '3 weeks ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_days_hours() {
+		$now = time();
+		$then = $now - ( 3 * DAY_IN_SECONDS ) - ( 3 * HOUR_IN_SECONDS );
+		$this->assertEquals( '3 days, 3 hours ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_days_nohours() {
+		$now = time();
+		$then = $now - ( 3 * DAY_IN_SECONDS );
+		$this->assertEquals( '3 days ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_hours_minutes() {
+		$now = time();
+		$then = $now - ( 3 * HOUR_IN_SECONDS ) - ( 3 * MINUTE_IN_SECONDS );
+		$this->assertEquals( '3 hours, 3 minutes ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_hours_nominutes() {
+		$now = time();
+		$then = $now - ( 3 * HOUR_IN_SECONDS );
+		$this->assertEquals( '3 hours ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 * @ticket BP5017
+	 */
+	public function test_bp_core_time_since_minutes_seconds() {
+		$now = time();
+		$then = $now - ( 3 * MINUTE_IN_SECONDS ) - 3;
+		$this->assertEquals( '3 minutes ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_minutes_noseconds() {
+		$now = time();
+		$then = $now - ( 3 * MINUTE_IN_SECONDS );
+		$this->assertEquals( '3 minutes ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_seconds() {
+		$now = time();
+		$then = $now - 3;
+		$this->assertEquals( '3 seconds ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * Sanity check for the singular version of 'year'
+	 *
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_year() {
+		$now = time();
+		$then = $now - YEAR_IN_SECONDS;
+		$this->assertEquals( '1 year ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_rightnow() {
+		$now = time();
+		$then = $now;
+		$this->assertEquals( 'right now', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_future() {
+		$now = time();
+		$then = $now + 100;
+		$this->assertEquals( 'sometime ago', bp_core_time_since( $then, $now ) );
+	}
+
+	/**
+	 * @group bp_format_time
+	 */
+	public function test_bp_format_time_gmt_offset_no_timezone_string() {
+		$time          = 1395169200;
+		$gmt_offset    = '-6';
+		$just_date     = false;
+		$localize_time = true;
+
+		update_option( 'date_format', 'F j, Y' );
+		update_option( 'time_format', 'g:i a' );
+		update_option( 'gmt_offset', $gmt_offset );
+		update_option( 'timezone_string', '' );
+
+		$this->assertEquals( 'March 18, 2014 at 1:00 pm', bp_format_time( $time, $just_date, $localize_time ) );
+	}
+
+	/**
+	 * @group bp_format_time
+	 */
+	public function test_bp_format_time_timezone_string_no_gmt_offset() {
+		$time           = 1395169200;
+		$timzone_string = 'America/Chicago';
+		$just_date      = false;
+		$localize_time  = true;
+
+		update_option( 'date_format', 'F j, Y' );
+		update_option( 'time_format', 'g:i a' );
+		update_option( 'timezone_string', $timzone_string );
+		update_option( 'gmt_offset', '0' );
+
+		$this->assertEquals( 'March 18, 2014 at 2:00 pm', bp_format_time( $time, $just_date, $localize_time ) );
+	}
+
+	/**
+	 * @group bp_format_time
+	 */
+	public function test_bp_format_time_gmt_offset_no_localize() {
+		$time          = 1395169200;
+		$gmt_offset    = '-6';
+		$just_date     = false;
+		$localize_time = false;
+
+		update_option( 'date_format', 'F j, Y' );
+		update_option( 'time_format', 'g:i a' );
+		update_option( 'gmt_offset', $gmt_offset );
+		update_option( 'timezone_string', '' );
+
+		$this->assertEquals( 'March 18, 2014 at 7:00 pm', bp_format_time( $time, $just_date, $localize_time ) );
+	}
+
+	/**
+	 * @group bp_format_time
+	 */
+	public function test_bp_format_time_timezone_string_no_localize() {
+		$time           = 1395169200;
+		$timzone_string = 'America/Chicago';
+		$just_date      = false;
+		$localize_time  = false;
+
+		update_option( 'date_format', 'F j, Y' );
+		update_option( 'time_format', 'g:i a' );
+		update_option( 'timezone_string', $timzone_string );
+		update_option( 'gmt_offset', '0' );
+
+		$this->assertEquals( 'March 18, 2014 at 7:00 pm', bp_format_time( $time, $just_date, $localize_time ) );
+	}
+
+	/**
+	 * @group bp_core_get_iso8601_date
+	 */
+	public function test_bp_core_get_iso8601_date_invalid_date() {
+		$this->assertEquals( '', bp_core_get_iso8601_date( 'Not a date' ) );
+	}
+
+	/**
+	 * @group bp_sort_by_key
+	 */
+	public function test_bp_sort_by_key_arrays_num() {
+		$items = array(
+			array(
+				'foo' => 'bar',
+				'value' => 5,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 10,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 1,
+			),
+		);
+
+		$expected = array(
+			array(
+				'foo' => 'bar',
+				'value' => 1,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 5,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 10,
+			),
+		);
+
+		$this->assertEquals( $expected, bp_sort_by_key( $items, 'value', 'num' ) );
+	}
+
+	/**
+	 * @group bp_sort_by_key
+	 */
+	public function test_bp_sort_by_key_objects_num() {
+		$items = array(
+			new stdClass,
+			new stdClass,
+			new stdClass,
+		);
+		$items[0]->foo = 'bar';
+		$items[0]->value = 5;
+		$items[1]->foo = 'bar';
+		$items[1]->value = 10;
+		$items[2]->foo = 'bar';
+		$items[2]->value = 1;
+
+		$expected = array(
+			new stdClass,
+			new stdClass,
+			new stdClass,
+		);
+		$expected[0]->foo = 'bar';
+		$expected[0]->value = 1;
+		$expected[1]->foo = 'bar';
+		$expected[1]->value = 5;
+		$expected[2]->foo = 'bar';
+		$expected[2]->value = 10;
+
+		$this->assertEquals( $expected, bp_sort_by_key( $items, 'value', 'num' ) );
+	}
+
+	/**
+	 * @group bp_sort_by_key
+	 */
+	public function test_bp_sort_by_key_num_should_respect_0() {
+		$items = array(
+			array(
+				'foo' => 'bar',
+				'value' => 2,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 0,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 4,
+			),
+		);
+
+		$expected = array(
+			array(
+				'foo' => 'bar',
+				'value' => 0,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 2,
+			),
+			array(
+				'foo' => 'bar',
+				'value' => 4,
+			),
+		);
+
+		$this->assertEquals( $expected, bp_sort_by_key( $items, 'value', 'num' ) );
+	}
+
+	/**
+	 * @group bp_alpha_sort_by_key
+	 */
+	public function test_bp_alpha_sort_by_key_arrays() {
+		$items = array(
+			array(
+				'foo' => 'bar',
+				'name' => 'alpha',
+			),
+			array(
+				'foo' => 'bar',
+				'name' => 'charlie',
+			),
+			array(
+				'foo' => 'bar',
+				'name' => 'beta',
+			),
+		);
+
+		$expected = array(
+			array(
+				'foo' => 'bar',
+				'name' => 'alpha',
+			),
+			array(
+				'foo' => 'bar',
+				'name' => 'beta',
+			),
+			array(
+				'foo' => 'bar',
+				'name' => 'charlie',
+			),
+		);
+
+		$this->assertEquals( $expected, bp_alpha_sort_by_key( $items, 'name' ) );
+	}
+
+	/**
+	 * @group bp_alpha_sort_by_key
+	 */
+	public function test_bp_alpha_sort_by_key_objects() {
+		$items = array(
+			new stdClass,
+			new stdClass,
+			new stdClass,
+		);
+		$items[0]->foo = 'bar';
+		$items[0]->name = 'alpha';
+		$items[1]->foo = 'bar';
+		$items[1]->name = 'charlie';
+		$items[2]->foo = 'bar';
+		$items[2]->name = 'beta';
+
+		$expected = array(
+			new stdClass,
+			new stdClass,
+			new stdClass,
+		);
+		$expected[0]->foo = 'bar';
+		$expected[0]->name = 'alpha';
+		$expected[1]->foo = 'bar';
+		$expected[1]->name = 'beta';
+		$expected[2]->foo = 'bar';
+		$expected[2]->name = 'charlie';
+
+		$this->assertEquals( $expected, bp_alpha_sort_by_key( $items, 'name' ) );
+	}
+
+	/**
+	 * @group bp_sort_by_key
+	 */
+	public function test_bp_sort_by_key_arrays_num_preserve_keys() {
+		$items = array(
+			'p' => array(
+				'foo' => 'bar',
+				'value' => 5,
+			),
+			'q' => array(
+				'foo' => 'bar',
+				'value' => 10,
+			),
+			'r' => array(
+				'foo' => 'bar',
+				'value' => 1,
+			),
+		);
+
+		$expected = array(
+			'r' => array(
+				'foo' => 'bar',
+				'value' => 1,
+			),
+			'p' => array(
+				'foo' => 'bar',
+				'value' => 5,
+			),
+			'q' => array(
+				'foo' => 'bar',
+				'value' => 10,
+			),
+		);
+
+		$this->assertEquals( $expected, bp_sort_by_key( $items, 'value', 'num', true ) );
+	}
+
+	/**
+	 * @group bp_sort_by_key
+	 */
+	public function test_bp_sort_by_key_num_should_respect_0_preserve_keys() {
+		$items = array(
+			's' => array(
+				'foo' => 'bar',
+				'value' => 2,
+			),
+			't' => array(
+				'foo' => 'bar',
+				'value' => 0,
+			),
+			'u' => array(
+				'foo' => 'bar',
+				'value' => 4,
+			),
+		);
+
+		$expected = array(
+			't' => array(
+				'foo' => 'bar',
+				'value' => 0,
+			),
+			's' => array(
+				'foo' => 'bar',
+				'value' => 2,
+			),
+			'u' => array(
+				'foo' => 'bar',
+				'value' => 4,
+			),
+		);
+
+		$this->assertEquals( $expected, bp_sort_by_key( $items, 'value', 'num', true ) );
+	}
+
+	/**
+	 * @group pagination
+	 * @group bp_sanitize_pagination_arg
+	 */
+	public function test_bp_sanitize_pagination_arg_zero() {
+		$request          = $_REQUEST;
+		$arg              = 'bp_pagination_test';
+		$page             = 1;
+		$_REQUEST[ $arg ] = '0';
+		$value            = bp_sanitize_pagination_arg( $arg, $page );
+
+		$this->assertEquals( $value, $page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group bp_sanitize_pagination_arg
+	 */
+	public function test_bp_sanitize_pagination_arg_negative() {
+		$request          = $_REQUEST;
+		$arg              = 'bp_pagination_test';
+		$page             = 25;
+		$_REQUEST[ $arg ] = '-25';
+		$value            = bp_sanitize_pagination_arg( $arg, $page );
+
+		$this->assertEquals( $value, $page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group bp_core_get_directory_pages
+	 */
+	public function test_bp_core_get_directory_pages_after_page_edit() {
+		// Set the cache
+		$pages = bp_core_get_directory_pages();
+
+		// Update one of the posts
+		switch_to_blog( bp_get_root_blog_id() );
+
+		// grab the first one
+		foreach ( $pages as $page ) {
+			$p = $page;
+			break;
+		}
+
+		$post = get_post( $p->id );
+		$post->post_title .= ' Foo';
+		wp_update_post( $post );
+
+		restore_current_blog();
+
+		$this->assertFalse( wp_cache_get( 'directory_pages', 'bp' ) );
+	}
+
+	/**
+	 * @group bp_core_get_root_options
+	 */
+	public function test_bp_core_get_root_options_cache_invalidate() {
+		$keys = array_keys( bp_get_default_options() );
+		$keys[] = 'registration';
+		$keys[] = 'avatar_default';
+
+		foreach ( $keys as $key ) {
+			// prime cache
+			$root_options = bp_core_get_root_options();
+
+			bp_update_option( $key, 'foo' );
+
+			$this->assertFalse( wp_cache_get( 'root_blog_options', 'bp' ), 'Cache not invalidated after updating "' . $key . '"' );
+		}
+
+		if ( is_multisite() ) {
+			$ms_keys = array(
+				'tags_blog_id',
+				'sitewide_tags_blog',
+				'registration',
+				'fileupload_mask',
+			);
+
+			foreach ( $ms_keys as $ms_key ) {
+				$root_options = bp_core_get_root_options();
+
+				update_site_option( $ms_key, 'foooooooo' );
+
+				$this->assertFalse( wp_cache_get( 'root_blog_options', 'bp' ), 'Cache not invalidated after updating "' . $ms_key . '"' );
+			}
+		}
+	}
+
+	/**
+	 * @group bp_core_get_root_option
+	 */
+	public function test_bp_core_get_root_option_with_unpopulated_cache() {
+		// Back up and unset global cache.
+		$old_options = buddypress()->site_options;
+		unset( buddypress()->site_options );
+
+		$this->assertSame( $old_options['avatar_default'], bp_core_get_root_option( 'avatar_default' ) );
+
+		// Clean up.
+		buddypress()->site_options = $old_options;
+	}
+
+	/**
+	 * @group bp_core_get_root_option
+	 */
+	public function test_bp_core_get_root_option_with_populated_cache() {
+		// Back up and unset global cache.
+		$old_options = buddypress()->site_options;
+		buddypress()->site_options = bp_core_get_root_options();
+		$expected = buddypress()->site_options['avatar_default'];
+
+		$this->assertSame( $expected, bp_core_get_root_option( 'avatar_default' ) );
+	}
+
+	/**
+	 * @group bp_core_add_root_component
+	 */
+	public function test_add_root_component_not_in_bp_pages() {
+		buddypress()->foo = new stdClass;
+		buddypress()->foo->id = 'foo';
+		buddypress()->foo->slug = 'foo';
+
+		bp_core_add_root_component( 'foo' );
+
+		$this->assertTrue( in_array( 'foo', buddypress()->add_root ) );
+		$this->assertTrue( buddypress()->foo->has_directory );
+		$this->assertNotEmpty( buddypress()->loaded_components['foo'] );
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 * @group bp_core_current_time
+	 */
+	public function test_bp_core_time_since_timezone_minute_ago() {
+		// backup timezone
+		$tz_backup = date_default_timezone_get();
+
+		// set timezone to something other than UTC
+		date_default_timezone_set( 'Europe/Paris' );
+
+		$this->assertSame( '1 minute ago', bp_core_time_since( time() - 60 ) );
+
+		// revert timezone back to normal
+		if ( $tz_backup ) {
+			date_default_timezone_set( $tz_backup );
+		}
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 * @group bp_core_current_time
+	 */
+	public function test_bp_core_time_since_timezone() {
+		// backup timezone
+		$tz_backup = date_default_timezone_get();
+
+		// set timezone to something other than UTC
+		date_default_timezone_set( 'Europe/Paris' );
+
+		$this->assertSame( '1 hour ago', bp_core_time_since( time() - 60*60 ) );
+
+		// revert timezone back to normal
+		if ( $tz_backup ) {
+			date_default_timezone_set( $tz_backup );
+		}
+	}
+
+	/**
+	 * @group bp_core_time_since
+	 */
+	public function test_bp_core_time_since_mysql_and_unix_timestamp_return_same_value() {
+		$mysql_date   = '2008-03-25 17:13:55';
+
+		$ts_mysql     = bp_core_time_since( $mysql_date );
+		$ts_timestamp = bp_core_time_since( strtotime( $mysql_date ) );
+
+		$this->assertSame( $ts_mysql, $ts_timestamp );
+	}
+
+	/**
+	 * @group bp_attachments
+	 * @group bp_upload_dir
+	 */
+	public function test_bp_upload_dir() {
+		$expected_upload_dir = wp_upload_dir();
+
+		if ( is_multisite() ) {
+			if ( function_exists( 'wp_initialize_site' ) ) {
+				$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			}
+
+			$b = self::factory()->blog->create();
+			switch_to_blog( $b );
+		}
+
+		$tested_upload_dir = bp_upload_dir();
+
+		if ( is_multisite() ) {
+			restore_current_blog();
+		}
+
+		$this->assertSame( $expected_upload_dir, $tested_upload_dir );
+	}
+
+	/**
+	 * @group bp_is_active
+	 */
+	public function test_bp_is_active_component() {
+		$bp = buddypress();
+		$reset_active_components = $bp->active_components;
+
+		$this->assertTrue( bp_is_active( 'members' ) );
+
+		$this->assertFalse( bp_is_active( 'foo' ) );
+
+		// Create and activate the foo component
+		$bp->foo = new BP_Component;
+		$bp->foo->id   = 'foo';
+		$bp->foo->slug = 'foo';
+		$bp->foo->name = 'Foo';
+		$bp->active_components[ $bp->foo->id ] = 1;
+
+		$this->assertTrue( bp_is_active( 'foo' ) );
+
+		add_filter( 'bp_is_active', '__return_false' );
+
+		$this->assertFalse( bp_is_active( 'foo' ) );
+
+		remove_filter( 'bp_is_active', '__return_false' );
+
+		// Reset buddypress() vars
+		$bp->active_components = $reset_active_components;
+	}
+
+	/**
+	 * @group bp_is_active
+	 */
+	public function test_bp_is_active_feature() {
+		$bp = buddypress();
+		$reset_active_components = $bp->active_components;
+
+		// Create and activate the foo component
+		$bp->foo = new BP_Component;
+		$bp->foo->id   = 'foo';
+		$bp->foo->slug = 'foo';
+		$bp->foo->name = 'Foo';
+		$bp->active_components[ $bp->foo->id ] = 1;
+
+		// foo did not register 'bar' as a feature
+		$this->assertFalse( bp_is_active( 'foo', 'bar' ) );
+
+		// fake registering the 'bar' feature
+		$bp->foo->features = array( 'bar' );
+		$this->assertTrue( bp_is_active( 'foo', 'bar' ) );
+
+		// test the feature filter
+		add_filter( 'bp_is_foo_bar_active', '__return_false' );
+		$this->assertFalse( bp_is_active( 'foo', 'bar' ) );
+		remove_filter( 'bp_is_foo_bar_active', '__return_false' );
+
+		// test the main component filter
+		add_filter( 'bp_is_active', '__return_false' );
+		$this->assertFalse( bp_is_active( 'foo', 'bar' ) );
+		remove_filter( 'bp_is_active', '__return_false' );
+
+		// Reset buddypress() vars
+		$bp->active_components = $reset_active_components;
+	}
+
+	/**
+	 * @group bp_attachments
+	 */
+	public function test_bp_attachments_get_allowed_types() {
+		$supported = array( 'jpeg', 'gif', 'png' );
+
+		$avatar = bp_attachments_get_allowed_types( 'avatar' );
+		$this->assertSame( $supported, $avatar );
+
+		$cover_image = bp_attachments_get_allowed_types( 'cover_image' );
+		$this->assertSame( $supported, $cover_image );
+
+		$images = bp_attachments_get_allowed_types( 'image/' );
+
+		foreach ( $images as $image ) {
+			if ( 'image' !== wp_ext2type( $image ) ) {
+				$not_image = $image;
+			}
+		}
+
+		$this->assertTrue( empty( $not_image ) );
+	}
+
+	public function test_emails_should_have_correct_link_color() {
+		$appearance = bp_email_get_appearance_settings();
+
+		$content    = '<a href="http://example.com">example</a>';
+		$link_color = 'style="color: ' . esc_attr( $appearance['highlight_color'] ) . ';';
+		$result     = bp_email_add_link_color_to_template( $content, 'template', 'add-content' );
+		$this->assertContains( $link_color, $result );
+
+		$content     = '<a href="http://example.com" style="display: block">example</a>';
+		$link_color .= 'display: block';
+		$result      = bp_email_add_link_color_to_template( $content, 'template', 'add-content' );
+		$this->assertContains( $link_color, $result );
+	}
+
+	/**
+	 * @group bp_core_add_page_mappings
+	 */
+	public function test_bp_core_add_page_mappings() {
+		$bp = buddypress();
+		$reset_bp_pages = $bp->pages;
+
+		$expected = array( 'activity', 'groups', 'members' );
+		if ( is_multisite() ) {
+			$expected = array( 'activity', 'blogs', 'groups', 'members' );
+		}
+
+		bp_core_add_page_mappings( $bp->active_components );
+		$bp_pages = array_keys( bp_get_option( 'bp-pages' ) );
+		sort( $bp_pages );
+
+		$this->assertEquals( $expected, $bp_pages );
+
+		$bp->pages = $reset_bp_pages;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpCoreGetDirectoryPageId.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpCoreGetDirectoryPageId.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpCoreGetDirectoryPageId.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @group core
+ * @group BP7025
+ */
+class BP_Tests_Core_BpCoreGetDirectoryPageId extends BP_UnitTestCase {
+	public function test_should_fall_back_on_current_component() {
+		$this->go_to( bp_get_activity_directory_permalink() );
+
+		$found = bp_core_get_directory_page_id();
+
+		$pages = bp_core_get_directory_page_ids();
+		$this->assertSame( $pages['activity'], $found );
+	}
+
+	public function test_should_accept_component_override() {
+		$this->go_to( bp_get_activity_directory_permalink() );
+
+		$found = bp_core_get_directory_page_id( 'members' );
+
+		$pages = bp_core_get_directory_page_ids();
+		$this->assertSame( $pages['members'], $found );
+	}
+
+	public function test_should_return_false_for_invalid_component() {
+		$found = bp_core_get_directory_page_id( 'foo' );
+
+		$pages = bp_core_get_directory_page_ids();
+		$this->assertFalse( $found );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpCoreGetDirectoryPageIds.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpCoreGetDirectoryPageIds.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpCoreGetDirectoryPageIds.php	(revision 12265)
@@ -0,0 +1,344 @@
+<?php
+
+/**
+ * @group core
+ * @covers ::bp_core_get_directory_page_ids
+ */
+class BP_Tests_Core_Functions_BpCoreGetDirectoryPageIds extends BP_UnitTestCase {
+	public function test_bp_core_get_directory_page_ids_on_directory_page_to_trash() {
+		$old_page_ids = bp_core_get_directory_page_ids();
+
+		// Grab the and remove the first page.
+		foreach ( $old_page_ids as $component => $page_id ) {
+			$p = $page_id;
+			unset( $old_page_ids[ $component ] );
+			break;
+		}
+
+		// Move page to trash.
+		wp_delete_post( $p, false );
+
+		$new_page_ids = bp_core_get_directory_page_ids();
+
+		$this->assertEquals( $old_page_ids, $new_page_ids );
+	}
+
+	public function test_bp_core_get_directory_page_ids_on_directory_page_delete() {
+		$old_page_ids = bp_core_get_directory_page_ids();
+
+		// Grab the and remove the first page.
+		foreach ( $old_page_ids as $component => $page_id ) {
+			$p = $page_id;
+			unset( $old_page_ids[ $component ] );
+			break;
+		}
+
+		// Force delete page.
+		wp_delete_post( $p, true );
+
+		$new_page_ids = bp_core_get_directory_page_ids();
+
+		$this->assertEquals( $old_page_ids, $new_page_ids );
+	}
+
+	public function test_bp_core_get_directory_page_ids_on_non_directory_page_delete() {
+		$old_page_ids = bp_core_get_directory_page_ids();
+
+		$p = self::factory()->post->create( array(
+			'post_status' => 'publish',
+			'post_type' => 'page',
+		) );
+
+		// Force delete page.
+		wp_delete_post( $p, true );
+
+		$new_page_ids = bp_core_get_directory_page_ids();
+
+		$this->assertEquals( $old_page_ids, $new_page_ids );
+	}
+
+	public function test_bp_core_get_directory_page_ids_non_active_component() {
+		$old_page_ids = bp_core_get_directory_page_ids();
+		$bp = buddypress();
+
+		// Grab the and remove the first page.
+		foreach ( $old_page_ids as $component => $page_id ) {
+			$p = $page_id;
+			$c = $component;
+			unset( $old_page_ids[ $component ] );
+			break;
+		}
+
+		// Deactivate component.
+		unset( $bp->active_components[ $c ] );
+
+		$new_page_ids = bp_core_get_directory_page_ids();
+
+		// Restore components.
+		$bp->active_components[ $c ] = 1;
+
+		$this->assertEquals( $old_page_ids, $new_page_ids );
+	}
+
+	/**
+	 * @ticket BP6280
+	 */
+	public function test_inactive_components_should_not_be_removed_if_status_is_all() {
+		$old_page_ids = bp_core_get_directory_page_ids( 'all' );
+
+		$page_ids = $old_page_ids;
+		$page_ids['foo'] = 12345;
+
+		bp_core_update_directory_page_ids( $page_ids );
+		$found = bp_core_get_directory_page_ids( 'all' );
+
+		$this->assertEquals( 12345, $found['foo'] );
+	}
+
+	/**
+	 * @ticket BP6280
+	 */
+	public function test_inactive_components_should_be_removed_if_status_is_active() {
+		$old_page_ids = bp_core_get_directory_page_ids( 'all' );
+
+		$page_ids = $old_page_ids;
+		$page_ids['foo'] = 12345;
+
+		bp_core_update_directory_page_ids( $page_ids );
+		$found = bp_core_get_directory_page_ids( 'active' );
+
+		$this->assertFalse( isset( $found['foo'] ) );
+	}
+
+	/**
+	 * @ticket BP6280
+	 */
+	public function test_inactive_components_should_be_removed_if_status_is_unspecified() {
+		$old_page_ids = bp_core_get_directory_page_ids( 'all' );
+
+		$page_ids = $old_page_ids;
+		$page_ids['foo'] = 12345;
+
+		bp_core_update_directory_page_ids( $page_ids );
+		$found = bp_core_get_directory_page_ids( 'active' );
+
+		$this->assertFalse( isset( $found['foo'] ) );
+	}
+
+	public function test_bp_core_get_directory_page_ids_should_contain_register_and_activet_pages_when_registration_is_open() {
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+
+		$page_ids = bp_core_get_directory_page_ids();
+		$page_names = array_keys( $page_ids );
+
+		$this->assertContains( 'register', $page_names );
+		$this->assertContains( 'activate', $page_names );
+
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+	}
+
+	public function test_bp_core_get_directory_page_ids_should_not_contain_register_and_activet_pages_when_registration_is_closed() {
+
+		// Make sure the pages exist, to verify they're filtered out.
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		// Get page ids
+		$page_ids = bp_core_get_directory_page_ids();
+
+		// Need to delete these pages as previously created.
+		wp_delete_post( $page_ids['register'], true );
+		wp_delete_post( $page_ids['activate'], true );
+
+		add_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+		bp_core_add_page_mappings( array_keys( $ac ) );
+		$page_ids = bp_core_get_directory_page_ids();
+		remove_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		$page_names = array_keys( $page_ids );
+
+		$this->assertNotContains( 'register', $page_names );
+		$this->assertNotContains( 'activate', $page_names );
+	}
+
+	public function test_bp_core_get_directory_pages_register_activate_page_created_signups_allowed() {
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+		$directory_pages = bp_core_get_directory_pages();
+
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$this->assertTrue( isset( $directory_pages->register ) );
+		$this->assertTrue( isset( $directory_pages->activate ) );
+
+		$r = get_post( $directory_pages->register->id );
+		$this->assertTrue( 'publish' == $r->post_status );
+
+		$a = get_post( $directory_pages->activate->id );
+		$this->assertTrue( 'publish' == $a->post_status );
+	}
+
+	public function test_bp_core_get_directory_pages_register_activate_page_notcreated_signups_allowed() {
+		add_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+
+		remove_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$directory_pages = bp_core_get_directory_pages();
+
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$this->assertFalse( isset( $directory_pages->register ) );
+		$this->assertFalse( isset( $directory_pages->activate ) );
+	}
+
+	public function test_bp_core_get_directory_pages_register_activate_page_created_signups_notallowed() {
+		add_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+
+		remove_filter( 'bp_get_signup_allowed', '__return_true', 999 );
+
+		add_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		$directory_pages = bp_core_get_directory_pages();
+
+		remove_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		$this->assertTrue( isset( $directory_pages->register ) );
+		$this->assertTrue( isset( $directory_pages->activate ) );
+
+		$r = get_post( $directory_pages->register->id );
+		$this->assertTrue( 'publish' == $r->post_status );
+
+		$a = get_post( $directory_pages->activate->id );
+		$this->assertTrue( 'publish' == $a->post_status );
+	}
+
+	public function test_bp_core_get_directory_pages_register_activate_page_notcreated_signups_notallowed() {
+
+		add_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		$ac = buddypress()->active_components;
+		bp_core_add_page_mappings( array_keys( $ac ) );
+		$directory_pages = bp_core_get_directory_pages();
+
+		remove_filter( 'bp_get_signup_allowed', '__return_false', 999 );
+
+		$this->assertFalse( isset( $directory_pages->register ) );
+		$this->assertFalse( isset( $directory_pages->activate ) );
+	}
+
+	public function test_bp_core_get_directory_pages_pages_settings_update() {
+		// Set the cache
+		$pages = bp_core_get_directory_pages();
+
+		// Mess with it but put it back
+		$v = bp_get_option( 'bp-pages' );
+		bp_update_option( 'bp-pages', 'foo' );
+
+		$this->assertFalse( wp_cache_get( 'directory_pages', 'bp' ) );
+
+		bp_update_option( 'bp-pages', $v );
+	}
+
+	public function test_bp_core_get_directory_pages_multisite_delete_post_with_same_bp_page_id() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		if ( function_exists( 'wp_initialize_site' ) ) {
+			$this->setExpectedDeprecated( 'wpmu_new_blog' );
+		}
+
+		$dir_pages = bp_core_get_directory_pages();
+
+		// create a blog
+		$u = self::factory()->user->create();
+		$b1 = self::factory()->blog->create( array( 'user_id' => $u ) );
+
+		// Switch to blog and create a post with the same BP activity page ID.
+		switch_to_blog( $b1 );
+		$p = self::factory()->post->create( array(
+			'import_id' => $dir_pages->activity->id
+		) );
+
+		// delete the post that matches the BP activity page ID on this sub-site
+		wp_delete_post( $dir_pages->activity->id, true );
+
+		// restore blog
+		restore_current_blog();
+
+		// refetch BP directory pages
+		$dir_pages = bp_core_get_directory_pages();
+
+		// Now verify that our BP activity page was not wiped out
+		$this->assertNotEmpty( $dir_pages->activity );
+	}
+
+	/**
+	 * @ticket BP7193
+	 */
+	public function test_bp_core_get_directory_pages_autocreate_register_pages_single_site() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		// Emulate being in the admin area.
+		if ( ! class_exists( 'BP_Members_Admin', false ) ) {
+			require BP_PLUGIN_DIR . 'bp-members/classes/class-bp-members-admin.php';
+		}
+		$admin = new BP_Members_Admin;
+		add_action( 'update_option_users_can_register', array( $admin, 'single_site_registration_on' ), 10, 2 );
+
+		// Emulate turning registration on.
+		update_option( 'users_can_register', 1 );
+
+		// Now check directory pages.
+		$pages = bp_core_get_directory_pages();
+
+		$this->assertNotEmpty( $pages->register );
+		$this->assertNotEmpty( $pages->activate );
+
+		remove_action( 'update_option_users_can_register', array( $admin, 'single_site_registration_on' ), 10 );
+	}
+
+	/**
+	 * @ticket BP7193
+	 */
+	public function test_bp_core_get_directory_pages_autocreate_register_pages_multisite() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		// Emulate being in the network admin area.
+		if ( ! class_exists( 'BP_Members_Admin', false ) ) {
+			require BP_PLUGIN_DIR . 'bp-members/classes/class-bp-members-admin.php';
+		}
+		$admin = new BP_Members_Admin;
+		add_action( 'update_site_option_registration', array( $admin, 'multisite_registration_on' ), 10, 2 );
+
+		// Emulate turning registration on.
+		update_site_option( 'registration', 'user' );
+
+		// Now check directory pages.
+		$pages = bp_core_get_directory_pages();
+
+		$this->assertNotEmpty( $pages->register );
+		$this->assertNotEmpty( $pages->activate );
+
+		remove_action( 'update_site_option_registration', array( $admin, 'multisite_registration_on' ), 10 );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpGetRefererPath.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpGetRefererPath.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpGetRefererPath.php	(revision 12265)
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @group core
+ * @group functions
+ * @group bp_get_referer_path
+ */
+class BP_Tests_Core_Functions_BPGetRefererPath extends BP_UnitTestCase {
+	private $_wp_http_referer = '';
+	private $http_referer = '';
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->_wp_http_referer = '';
+		$this->http_referer = '';
+
+		if ( isset( $_REQUEST['_wp_http_referer'] ) ) {
+			$this->_wp_http_referer = $_REQUEST['_wp_http_referer'];
+		}
+
+		if ( isset( $_SERVER['HTTP_REFERER'] ) ) {
+			$this->http_referer = $_SERVER['HTTP_REFERER'];
+		}
+	}
+
+	public function tearDown() {
+		if ( isset( $_REQUEST['_wp_http_referer'] ) ) {
+			unset( $_REQUEST['_wp_http_referer'] );
+		}
+
+		if ( isset( $_SERVER['HTTP_REFERER'] ) ) {
+			unset( $_SERVER['HTTP_REFERER'] );
+		}
+
+		if ( $this->_wp_http_referer ) {
+			$_REQUEST['_wp_http_referer'] = $this->_wp_http_referer;
+		}
+
+		if ( $this->http_referer ) {
+			$_SERVER['HTTP_REFERER'] = $this->http_referer;
+		}
+
+		parent::tearDown();
+	}
+
+	public function test_from__wp_http_referer_fully_qualified_uri() {
+		$home = get_option( 'home' );
+		$_REQUEST['_wp_http_referer'] = trailingslashit( $home ) . 'foo/';
+		$found = bp_get_referer_path();
+		$this->assertSame( '/foo/', bp_get_referer_path() );
+	}
+
+	public function test_from__wp_http_referer_absolute_path() {
+		$_REQUEST['_wp_http_referer'] = '/foo/';
+		$found = bp_get_referer_path();
+		$this->assertSame( '/foo/', bp_get_referer_path() );
+	}
+
+	public function test_from_server_request_uri_fully_qualified_uri() {
+		$home = get_option( 'home' );
+		$_SERVER['HTTP_REFERER'] = trailingslashit( $home ) . 'foo/';
+		$found = bp_get_referer_path();
+		$this->assertSame( '/foo/', bp_get_referer_path() );
+	}
+
+	public function test_from_server_request_uri_absolute_path() {
+		$_SERVER['HTTP_REFERER'] = '/foo/';
+		$found = bp_get_referer_path();
+		$this->assertSame( '/foo/', bp_get_referer_path() );
+	}
+
+	public function test__wp_http_referer_should_take_precedence_over_server_superglobal() {
+		$_SERVER['HTTP_REFERER'] = '/foo/';
+		$_REQUEST['_wp_http_referer'] = '/bar/';
+		$found = bp_get_referer_path();
+		$this->assertSame( '/bar/', bp_get_referer_path() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpVerifyNonceRequest.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpVerifyNonceRequest.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/functions/bpVerifyNonceRequest.php	(revision 12265)
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @group core
+ * @group functions
+ * @group bp_verify_nonce_request
+ */
+class BP_Tests_Core_Functions_BPVerifyNonceRequest extends BP_UnitTestCase {
+	private $http_host = '';
+	private $server_port = '';
+	private $request_uri = '';
+
+	public function setUp() {
+		parent::setUp();
+
+		if ( isset( $_SERVER['HTTP_HOST'] ) ) {
+			$this->http_host = $_SERVER['HTTP_HOST'];
+		}
+
+		if ( isset( $_SERVER['SERVER_PORT'] ) ) {
+			$this->server_port = $_SERVER['SERVER_PORT'];
+		}
+
+		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
+			$this->request_uri = $_SERVER['REQUEST_URI'];
+		}
+	}
+
+	public function tearDown() {
+		if ( '' !== $this->http_host ) {
+			$_SERVER['HTTP_HOST'] = $this->http_host;
+		}
+
+		if ( '' !== $this->server_port ) {
+			$_SERVER['SERVER_PORT'] = $this->server_port;
+		}
+
+		if ( '' !== $this->request_uri ) {
+			$_SERVER['REQUEST_URI'] = $this->request_uri;
+		}
+
+		parent::tearDown();
+	}
+
+	public function test_bp_verify_nonce_request_with_port_in_home_url_and_wordpress_installed_in_subdirectory() {
+		// fake various $_SERVER parameters
+		$host = explode( ':', $_SERVER['HTTP_HOST'] );
+		$_SERVER['HTTP_HOST'] = $host[0] . ':80';
+		$_SERVER['SERVER_PORT'] = 80;
+		$_SERVER['REQUEST_URI'] = '/wordpress/';
+
+		// add port number and subdirectory to home URL for testing
+		add_filter( 'home_url', array( $this, 'add_port_and_subdirectory_to_home_url' ), 10, 3 );
+
+		// test bp_verify_nonce_request()
+		$action = 'verify-this';
+		$_REQUEST[$action] = wp_create_nonce( $action );
+		$test = bp_verify_nonce_request( $action, $action );
+
+		// clean up!
+		remove_filter( 'home_url', array( $this, 'add_port_and_subdirectory_to_home_url' ), 10 );
+		unset( $_REQUEST[$action] );
+
+		// assert!
+		$this->assertSame( 1, $test );
+	}
+
+	/**
+	 * Add port 80 and /wordpress/ subdirectory to home URL.
+	 *
+	 * @param string      $url         The complete home URL including scheme and path.
+	 * @param string      $path        Path relative to the home URL. Blank string if no path is specified.
+	 * @param string|null $orig_scheme Scheme to give the home URL context. Accepts 'http', 'https', 'relative' or null.
+	 * @return string
+	 */
+	public function add_port_and_subdirectory_to_home_url( $url, $path, $scheme ) {
+		$home = parse_url( get_option( 'home' ) );
+		$home_path = isset( $home['path'] ) ? $home['path'] : '';
+		return $scheme . '://' . $home['host'] . ':80' . $home_path . '/wordpress' . $path;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/backCompat.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/backCompat.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/backCompat.php	(revision 12265)
@@ -0,0 +1,321 @@
+<?php
+
+/**
+ * @group core
+ * @group nav
+ * @ticket BP6534
+ * @expectedIncorrectUsage bp_nav
+ */
+class BP_Core_Nav_BackCompat extends BP_UnitTestCase {
+	protected $bp_nav;
+	protected $bp_options_nav;
+
+	public function setUp() {
+		parent::setUp();
+		$this->bp_nav = buddypress()->bp_nav;
+		$this->bp_options_nav = buddypress()->bp_options_nav;
+	}
+
+	public function tearDown() {
+		buddypress()->bp_nav = $this->bp_nav;
+		buddypress()->bp_options_nav = $this->bp_options_nav;
+		parent::tearDown();
+	}
+
+	protected function create_nav_items() {
+		bp_core_new_nav_item( array(
+			'name'                => 'Foo',
+			'slug'                => 'foo',
+			'position'            => 25,
+			'screen_function'     => 'foo_screen_function',
+			'default_subnav_slug' => 'foo-subnav'
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name'            => 'Foo Subnav',
+			'slug'            => 'foo-subnav',
+			'parent_url'      => 'example.com/foo',
+			'parent_slug'     => 'foo',
+			'screen_function' => 'foo_screen_function',
+			'position'        => 10
+		) );
+	}
+
+	/**
+	 * Create a group, set up nav item, and go to the group.
+	 */
+	protected function set_up_group() {
+		$g = self::factory()->group->create( array(
+			'slug' => 'testgroup',
+		) );
+
+		$group = groups_get_group( $g );
+		$group_permalink = bp_get_group_permalink( $group );
+
+		$this->go_to( $group_permalink );
+
+		bp_core_new_subnav_item( array(
+			'name'            => 'Foo',
+			'slug'            => 'foo',
+			'parent_url'      => $group_permalink,
+			'parent_slug'     => 'testgroup',
+			'screen_function' => 'foo_screen_function',
+			'position'        => 10
+		), 'groups' );
+	}
+
+	public function test_bp_nav_isset() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		$this->assertTrue( isset( $bp->bp_nav ) );
+		$this->assertTrue( isset( $bp->bp_nav['foo'] ) );
+		$this->assertTrue( isset( $bp->bp_nav['foo']['name'] ) );
+	}
+
+	public function test_bp_nav_unset() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		// No support for this - it would create a malformed nav item.
+		/*
+		unset( $bp->bp_nav['foo']['css_id'] );
+		$this->assertFalse( isset( $bp->bp_nav['foo']['css_id'] ) );
+		*/
+
+		unset( $bp->bp_nav['foo'] );
+		$this->assertFalse( isset( $bp->bp_nav['foo'] ) );
+	}
+
+	public function test_bp_nav_get() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		$foo = $bp->bp_nav['foo'];
+		$this->assertSame( 'Foo', $foo['name'] );
+
+		$this->assertSame( 'Foo', $bp->bp_nav['foo']['name'] );
+	}
+
+	public function test_bp_nav_set() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		$bp->bp_nav['foo']['name'] = 'Bar';
+
+		$nav = bp_get_nav_menu_items();
+
+		foreach ( $nav as $_nav ) {
+			if ( 'foo' === $_nav->css_id ) {
+				$found = $_nav;
+				break;
+			}
+		}
+
+		$this->assertSame( 'Bar', $found->name );
+	}
+
+	public function test_bp_options_nav_isset() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		$this->assertTrue( isset( $bp->bp_options_nav ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['foo'] ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['foo']['foo-subnav'] ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['foo']['foo-subnav']['name'] ) );
+	}
+
+	public function test_bp_options_nav_unset() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		// No support for this - it would create a malformed nav item.
+		/*
+		unset( $bp->bp_options_nav['foo']['foo-subnav']['user_has_access'] );
+		$this->assertFalse( isset( $bp->bp_options_nav['foo']['foo-subnav']['user_has_access'] ) );
+		*/
+
+		unset( $bp->bp_options_nav['foo']['foo-subnav'] );
+		$this->assertFalse( isset( $bp->bp_options_nav['foo']['foo-subnav'] ) );
+
+		// Make sure the parent nav hasn't been wiped out.
+		$this->assertTrue( isset( $bp->bp_options_nav['foo'] ) );
+
+		unset( $bp->bp_options_nav['foo'] );
+		$this->assertFalse( isset( $bp->bp_options_nav['foo'] ) );
+	}
+
+	public function test_bp_options_nav_get() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		$foo_subnav = $bp->bp_options_nav['foo']['foo-subnav'];
+		$this->assertSame( 'Foo Subnav', $foo_subnav['name'] );
+
+		$this->assertSame( 'Foo Subnav', $bp->bp_options_nav['foo']['foo-subnav']['name'] );
+	}
+
+	public function test_bp_options_nav_set() {
+		$this->create_nav_items();
+
+		$bp = buddypress();
+
+		$bp->bp_options_nav['foo']['foo-subnav']['name'] = 'Bar';
+		$nav = bp_get_nav_menu_items();
+
+		foreach ( $nav as $_nav ) {
+			if ( 'foo-subnav' === $_nav->css_id ) {
+				$found = $_nav;
+				break;
+			}
+		}
+
+		$this->assertSame( 'Bar', $found->name );
+
+		$subnav = array(
+			'name' => 'Bar',
+			'css_id' => 'bar-id',
+			'link' => 'bar-link',
+			'slug' => 'bar-slug',
+			'user_has_access' => true,
+		);
+		$bp->bp_options_nav['foo']['foo-subnav'] = $subnav;
+		$nav = bp_get_nav_menu_items();
+
+		foreach ( $nav as $_nav ) {
+			if ( 'bar-id' === $_nav->css_id ) {
+				$found = $_nav;
+				break;
+			}
+		}
+
+		$this->assertSame( 'Bar', $found->name );
+	}
+
+	/**
+	 * @group groups
+	 */
+	public function test_bp_options_nav_isset_group_nav() {
+		$this->set_up_group();
+
+		$bp = buddypress();
+
+		$this->assertTrue( isset( $bp->bp_options_nav ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['testgroup'] ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['testgroup']['foo'] ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['testgroup']['foo']['name'] ) );
+	}
+
+	/**
+	 * @group groups
+	 */
+	public function test_bp_options_nav_unset_group_nav() {
+		$this->set_up_group();
+
+		$bp = buddypress();
+
+		// No support for this - it would create a malformed nav item.
+		/*
+		unset( $bp->bp_options_nav['testgroup']['foo']['user_has_access'] );
+		$this->assertFalse( isset( $bp->bp_options_nav['testgroup']['foo']['user_has_access'] ) );
+		*/
+
+		unset( $bp->bp_options_nav['testgroup']['foo'] );
+		$this->assertFalse( isset( $bp->bp_options_nav['testgroup']['foo'] ) );
+
+		unset( $bp->bp_options_nav['testgroup'] );
+		$this->assertFalse( isset( $bp->bp_options_nav['testgroup'] ) );
+	}
+
+	/**
+	 * @group groups
+	 */
+	public function test_bp_options_nav_get_group_nav() {
+		$this->set_up_group();
+
+		$bp = buddypress();
+
+		$foo = $bp->bp_options_nav['testgroup']['foo'];
+		$this->assertSame( 'Foo', $foo['name'] );
+
+		$this->assertSame( 'Foo', $bp->bp_options_nav['testgroup']['foo']['name'] );
+	}
+
+	/**
+	 * @group groups
+	 */
+	public function test_bp_options_nav_set_group_nav() {
+		$this->set_up_group();
+
+		$bp = buddypress();
+
+		$bp->bp_options_nav['testgroup']['foo']['name'] = 'Bar';
+		$nav = bp_get_nav_menu_items( 'groups' );
+
+		foreach ( $nav as $_nav ) {
+			if ( 'foo' === $_nav->css_id ) {
+				$found = $_nav;
+				break;
+			}
+		}
+
+		$this->assertSame( 'Bar', $found->name );
+
+		$subnav = array(
+			'name' => 'Bar',
+			'css_id' => 'bar-id',
+			'link' => 'bar-link',
+			'slug' => 'bar-slug',
+			'user_has_access' => true,
+		);
+		$bp->bp_options_nav['testgroup']['foo'] = $subnav;
+		$nav = bp_get_nav_menu_items( 'groups' );
+
+		foreach ( $nav as $_nav ) {
+			if ( 'bar-id' === $_nav->css_id ) {
+				$found = $_nav;
+				break;
+			}
+		}
+
+		$this->assertSame( 'Bar', $found->name );
+	}
+
+	/**
+	 * @group groups
+	 */
+	public function test_bp_core_new_subnav_item_should_work_in_group_context() {
+		$this->set_up_group();
+
+		bp_core_new_subnav_item( array(
+			'name' => 'Foo Subnav',
+			'slug' => 'foo-subnav',
+			'parent_slug' => bp_get_current_group_slug(),
+			'parent_url' => bp_get_group_permalink( groups_get_current_group() ),
+			'screen_function' => 'foo_subnav',
+		) );
+
+		$bp = buddypress();
+
+		// Touch bp_nav since we told PHPUnit it was expectedDeprecated.
+		$f = $bp->bp_options_nav[ bp_get_current_group_slug() ];
+
+		$nav = bp_get_nav_menu_items( 'groups' );
+
+		foreach ( $nav as $_nav ) {
+			if ( 'foo-subnav' === $_nav->css_id ) {
+				$found = $_nav;
+				break;
+			}
+		}
+
+		$this->assertSame( 'Foo Subnav', $found->name );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreMaybeHookNewSubnavScreenFunction.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreMaybeHookNewSubnavScreenFunction.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreMaybeHookNewSubnavScreenFunction.php	(revision 12265)
@@ -0,0 +1,191 @@
+<?php
+/**
+ * @group core
+ * @group nav
+ */
+class BP_Tests_Core_Nav_BpCoreMaybeHookNewSubnavScreenFunction extends BP_UnitTestCase {
+
+	public function test_user_has_access_true_no_callable_function() {
+		$subnav_item = array(
+			'user_has_access' => true,
+			'screen_function' => '123foo456',
+		);
+
+		$expected = array(
+			'status' => 'failure',
+		);
+
+		$this->assertSame( $expected, bp_core_maybe_hook_new_subnav_screen_function( $subnav_item ) );
+	}
+
+	public function test_user_has_access_true_callable_function() {
+		$subnav_item = array(
+			'user_has_access' => true,
+			'screen_function' => 'wptexturize', // any old callable function
+		);
+
+		$expected = array(
+			'status' => 'success',
+		);
+
+		$this->assertSame( $expected, bp_core_maybe_hook_new_subnav_screen_function( $subnav_item ) );
+
+		// clean up
+		remove_action( 'bp_screens', 'wptexturize', 3 );
+	}
+
+	public function test_user_has_access_false_user_logged_out() {
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$subnav_item = array(
+			'user_has_access' => false,
+		);
+
+		$expected = array(
+			'status' => 'failure',
+			'redirect_args' => array(),
+		);
+
+		$this->assertSame( $expected, bp_core_maybe_hook_new_subnav_screen_function( $subnav_item ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_user_has_access_false_user_logged_in_my_profile() {
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->go_to( bp_core_get_user_domain( $u ) );
+
+		$subnav_item = array(
+			'user_has_access' => false,
+		);
+
+		// Just test relevant info
+		$found = bp_core_maybe_hook_new_subnav_screen_function( $subnav_item );
+		$this->assertSame( 'failure', $found['status'] );
+		$this->assertSame( bp_core_get_user_domain( $u ), $found['redirect_args']['root'] );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_user_has_access_false_user_logged_in_others_profile_default_component_accessible() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+
+		$this->go_to( bp_core_get_user_domain( $u2 ) );
+
+		$old_bp_nav = buddypress()->bp_nav;
+		$old_default_component = buddypress()->default_component;
+		buddypress()->default_component = 'foo';
+
+		bp_core_new_nav_item( array(
+			'slug' => 'foo',
+			'name' => 'Foo',
+			'screen_function' => 'foo',
+			'default_subnav_item' => 'bar',
+		) );
+
+		$subnav_item = array(
+			'user_has_access' => false,
+		);
+
+		// Just test relevant info
+		$found = bp_core_maybe_hook_new_subnav_screen_function( $subnav_item );
+
+		// Clean up
+		$this->set_current_user( $old_current_user );
+		buddypress()->default_component = $old_default_component;
+		buddypress()->bp_nav = $old_bp_nav;
+
+		$this->assertSame( 'failure', $found['status'] );
+		$this->assertSame( bp_core_get_user_domain( $u2 ), $found['redirect_args']['root'] );
+	}
+
+	public function test_user_has_access_false_user_logged_in_others_profile_default_component_not_accessible() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+
+		$this->go_to( bp_core_get_user_domain( $u2 ) );
+
+		$old_bp_nav = buddypress()->bp_nav;
+		$old_default_component = buddypress()->default_component;
+		buddypress()->default_component = 'foo';
+
+		bp_core_new_nav_item( array(
+			'slug' => 'foo',
+			'name' => 'Foo',
+			'screen_function' => 'foo',
+			'default_subnav_item' => 'bar',
+			'show_for_displayed_user' => false,
+		) );
+
+		$subnav_item = array(
+			'user_has_access' => false,
+		);
+
+		// Just test relevant info
+		$found = bp_core_maybe_hook_new_subnav_screen_function( $subnav_item );
+
+		// Clean up
+		$this->set_current_user( $old_current_user );
+		buddypress()->default_component = $old_default_component;
+		buddypress()->bp_nav = $old_bp_nav;
+
+		$this->assertSame( 'failure', $found['status'] );
+		$this->assertSame( bp_core_get_user_domain( $u2 ) . bp_get_activity_slug() . '/', $found['redirect_args']['root'] );
+	}
+
+	public function test_user_has_access_false_user_logged_in_group() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$group = groups_get_group( $g );
+
+		$this->go_to( bp_get_group_permalink( $group ) );
+
+		$subnav_item = array(
+			'user_has_access' => false,
+			'no_access_url' => bp_get_group_permalink( $group ),
+		);
+
+		// Just test relevant info
+		$found = bp_core_maybe_hook_new_subnav_screen_function( $subnav_item );
+		$this->assertSame( 'failure', $found['status'] );
+		$this->assertSame( bp_get_group_permalink( $group ), $found['redirect_args']['root'] );
+
+		// Clean up
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_user_has_access_false_user_logged_in_group_no_redirect_url_provided() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$group = groups_get_group( $g );
+
+		$this->go_to( bp_get_group_permalink( $group ) );
+
+		$subnav_item = array(
+			'user_has_access' => false,
+		);
+
+		// Just test relevant info
+		$found = bp_core_maybe_hook_new_subnav_screen_function( $subnav_item );
+		$this->assertSame( 'failure', $found['status'] );
+		$this->assertSame( bp_get_root_domain(), $found['redirect_args']['root'] );
+
+		// Clean up
+		$this->set_current_user( $old_current_user );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreNewNavItem.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreNewNavItem.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreNewNavItem.php	(revision 12265)
@@ -0,0 +1,215 @@
+<?php
+/**
+ * @group core
+ * @group nav
+ */
+class BP_Tests_Core_Nav_BpCoreNewNavItem extends BP_UnitTestCase {
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_user_nav() {
+		$bp_nav = buddypress()->bp_nav;
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->go_to( bp_core_get_user_domain( $u ) );
+
+		bp_core_new_nav_item( array(
+			'name'                    => 'Foo',
+			'slug'                    => 'foo',
+			'position'                => 25,
+			'screen_function'         => 'foo_screen_function',
+			'default_subnav_slug'     => 'foo-sub'
+		) );
+
+		$expected = array(
+			'name'                    => 'Foo',
+			'slug'                    => 'foo',
+			'link'                    => trailingslashit( bp_core_get_user_domain( $u ) . 'foo' ),
+			'css_id'                  => 'foo',
+			'show_for_displayed_user' => true,
+			'position'                => 25,
+			'screen_function'         => 'foo_screen_function',
+			'default_subnav_slug'     => 'foo-sub'
+		);
+
+		foreach ( $expected as $k => $v ) {
+			$this->assertEquals( $v, buddypress()->bp_nav['foo'][ $k ] );
+		}
+
+		// Clean up
+		buddypress()->bp_nav = $bp_nav;
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_group_nav() {
+		$bp_nav = buddypress()->bp_nav;
+
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$group = groups_get_group( $g );
+
+		$this->go_to( bp_get_group_permalink( $group ) );
+
+		$this->assertTrue( buddypress()->bp_nav[ $group->slug ]['position'] === -1 );
+
+		// Clean up
+		buddypress()->bp_nav = $bp_nav;
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_should_return_false_if_name_is_not_provided() {
+		$args = array(
+			'slug' => 'foo',
+		);
+
+		$this->assertFalse( bp_core_new_nav_item( $args ) );
+	}
+
+	public function test_should_return_false_if_slug_is_not_provided() {
+		$args = array(
+			'name' => 'foo',
+		);
+
+		$this->assertFalse( bp_core_new_nav_item( $args ) );
+	}
+
+	public function test_should_return_false_if_site_admin_only_and_current_user_cannot_bp_moderate() {
+		// Should already be set to a 0 user.
+		$this->assertFalse( bp_current_user_can( 'bp_moderate' ) );
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'site_admin_only' => true,
+		);
+
+		$this->assertFalse( bp_core_new_nav_item( $args ) );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_css_id_should_fall_back_on_slug() {
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+		);
+		bp_core_new_nav_item( $args );
+
+		$this->assertSame( 'foo', buddypress()->bp_nav['foo']['css_id'] );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_css_id_should_be_respected() {
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'item_css_id' => 'bar',
+		);
+		bp_core_new_nav_item( $args );
+
+		$this->assertSame( 'bar', buddypress()->bp_nav['foo']['css_id'] );
+	}
+
+	public function test_show_for_displayed_user_false_should_force_function_to_return_false_when_bp_user_has_access_is_also_false() {
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'show_for_displayed_user' => false,
+		);
+
+		add_filter( 'bp_user_has_access', '__return_false' );
+		$retval = bp_core_new_nav_item( $args );
+		remove_filter( 'bp_user_has_access', '__return_false' );
+
+		$this->assertFalse( $retval );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_existence_of_access_protected_user_nav() {
+		$bp_nav = buddypress()->bp_nav;
+
+		$u = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+
+		$this->go_to( bp_core_get_user_domain( $u ) );
+
+		$expected = array(
+			'name'                    => 'Settings',
+			'slug'                    => 'settings',
+			'link'                    => trailingslashit( bp_loggedin_user_domain() . 'settings' ),
+			'css_id'                  => 'settings',
+			'show_for_displayed_user' => false,
+			'position'                => 100,
+			'screen_function'         => 'bp_settings_screen_general',
+			'default_subnav_slug'     => 'general'
+		);
+
+		foreach ( $expected as $k => $v ) {
+			$this->assertEquals( $v, buddypress()->bp_nav['settings'][ $k ] );
+		}
+
+		// Clean up
+		buddypress()->bp_nav = $bp_nav;
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_creation_of_access_protected_user_nav() {
+		// The nav item must be added to bp_nav, even if the current user
+		// can't visit that nav item.
+		$bp_nav = buddypress()->bp_nav;
+
+		$u = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+
+		$this->go_to( bp_core_get_user_domain( $u ) );
+
+		bp_core_new_nav_item( array(
+			'name'                    => 'Woof',
+			'slug'                    => 'woof',
+			'show_for_displayed_user' => false,
+			'position'                => 35,
+			'screen_function'         => 'woof_screen_function',
+			'default_subnav_slug'     => 'woof-one'
+		) );
+
+		$expected = array(
+			'name'                    => 'Woof',
+			'slug'                    => 'woof',
+			'link'                    => trailingslashit( bp_loggedin_user_domain() . 'woof' ),
+			'css_id'                  => 'woof',
+			'show_for_displayed_user' => false,
+			'position'                => 35,
+			'screen_function'         => 'woof_screen_function',
+			'default_subnav_slug'     => 'woof-one'
+		);
+
+		foreach ( $expected as $k => $v ) {
+			$this->assertEquals( $v, buddypress()->bp_nav['woof'][ $k ] );
+		}
+
+		// Clean up
+		buddypress()->bp_nav = $bp_nav;
+		$this->set_current_user( $old_current_user );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreNewSubnavItem.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreNewSubnavItem.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreNewSubnavItem.php	(revision 12265)
@@ -0,0 +1,379 @@
+<?php
+/**
+ * @group core
+ * @group nav
+ */
+class BP_Tests_Core_Nav_BpCoreNewSubnavItem extends BP_UnitTestCase {
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_user_subnav() {
+		$bp_options_nav = buddypress()->bp_options_nav;
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$user_domain = bp_core_get_user_domain( $u );
+
+		$this->go_to( $user_domain );
+
+		bp_core_new_nav_item( array(
+			'name'            => 'Foo Parent',
+			'slug'            => 'foo-parent',
+			'link'            => trailingslashit( $user_domain . 'foo-parent' ),
+			'screen_function' => 'foo_screen_function',
+			'position'        => 10,
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name'            => 'Foo',
+			'slug'            => 'foo',
+			'parent_url'      => trailingslashit( $user_domain . 'foo-parent' ),
+			'parent_slug'     => 'foo-parent',
+			'screen_function' => 'foo_screen_function',
+			'position'        => 10
+		) );
+
+		$expected = array(
+			'name'              => 'Foo',
+			'link'              => trailingslashit( $user_domain . 'foo-parent/foo' ),
+			'slug'              => 'foo',
+			'css_id'            => 'foo',
+			'position'          => 10,
+			'user_has_access'   => true,
+			'no_access_url'     => '',
+			'screen_function'   => 'foo_screen_function',
+			'show_in_admin_bar' => false,
+		);
+
+		foreach ( $expected as $k => $v ) {
+			$this->assertSame( $v, buddypress()->bp_options_nav['foo-parent']['foo'][ $k ] );
+		}
+
+		// Clean up
+		buddypress()->bp_options_nav = $bp_options_nav;
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_required_params() {
+		// 'name'
+		$this->assertFalse( bp_core_new_subnav_item( array(
+			'slug' => 'foo',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'foo',
+		) ) );
+
+		// 'slug'
+		$this->assertFalse( bp_core_new_subnav_item( array(
+			'name' => 'foo',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'foo',
+		) ) );
+
+		// 'parent_slug'
+		$this->assertFalse( bp_core_new_subnav_item( array(
+			'name' => 'foo',
+			'slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'foo',
+		) ) );
+
+		// 'parent_url'
+		$this->assertFalse( bp_core_new_subnav_item( array(
+			'name' => 'foo',
+			'slug' => 'foo',
+			'parent_slug' => 'foo',
+			'screen_function' => 'foo',
+		) ) );
+
+		// 'screen_function'
+		$this->assertFalse( bp_core_new_subnav_item( array(
+			'name' => 'foo',
+			'slug' => 'foo',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+		) ) );
+	}
+
+	public function test_site_admin_only() {
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertFalse( bp_core_new_subnav_item( array(
+			'name' => 'foo',
+			'slug' => 'foo',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'foo',
+			'site_admin_only' => true,
+		) ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_link_provided() {
+		$bp_options_nav = buddypress()->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+			'link' => 'https://buddypress.org/',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'foo',
+			'link' => 'https://buddypress.org/',
+		) );
+
+		$this->assertSame( 'https://buddypress.org/', buddypress()->bp_options_nav['foo']['bar']['link'] );
+
+		buddypress()->bp_options_nav = $bp_options_nav;
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_link_built_from_parent_url_and_slug() {
+		$bp_options_nav = buddypress()->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+			'link' => 'https://buddypress.org/',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'http://example.com/foo/',
+			'screen_function' => 'foo',
+		) );
+
+		$this->assertSame( 'http://example.com/foo/bar/', buddypress()->bp_options_nav['foo']['bar']['link'] );
+
+		buddypress()->bp_options_nav = $bp_options_nav;
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_link_built_from_parent_url_and_slug_where_slug_is_default() {
+		$bp_nav = buddypress()->bp_nav;
+		$bp_options_nav = buddypress()->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+			'default_subnav_slug' => 'bar',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'http://example.com/foo/',
+			'screen_function' => 'foo',
+		) );
+
+		$this->assertSame( 'http://example.com/foo/', buddypress()->bp_options_nav['foo']['bar']['link'] );
+
+		// clean up
+		buddypress()->bp_nav = $bp_nav;
+		buddypress()->bp_options_nav = $bp_options_nav;
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_should_trailingslash_link_when_link_is_autogenerated_using_slug() {
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+			'link' => 'https://buddypress.org/',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => bp_get_root_domain() . 'foo/',
+			'screen_function' => 'foo',
+		) );
+
+		$expected = bp_get_root_domain() . 'foo/bar/';
+		$this->assertSame( $expected, buddypress()->bp_options_nav['foo']['bar']['link'] );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_should_trailingslash_link_when_link_is_autogenerated_not_using_slug() {
+		bp_core_new_nav_item( array(
+			'name' => 'foo',
+			'slug' => 'foo-parent',
+			'link' => bp_get_root_domain() . 'foo-parent/',
+			'default_subnav_slug' => 'bar',
+			'screen_function' => 'foo',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo-parent',
+			'parent_url' => bp_get_root_domain() . '/foo-parent/',
+			'screen_function' => 'bar',
+		) );
+
+		$expected = bp_get_root_domain() . '/foo-parent/';
+		$this->assertSame( $expected, buddypress()->bp_options_nav['foo-parent']['bar']['link'] );
+	}
+
+	/**
+	 * @ticket BP6353
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_link_should_not_trailingslash_link_explicit_link() {
+		$link = 'http://example.com/foo/bar/blah/?action=edit&id=30';
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+			'link' => 'http://example.com/foo/',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'http://example.com/foo/',
+			'screen_function' => 'foo',
+			'link' => $link,
+		) );
+
+		$this->assertSame( $link, buddypress()->bp_options_nav['foo']['bar']['link'] );
+	}
+
+	public function test_should_return_false_if_site_admin_only_and_current_user_cannot_bp_moderate() {
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+		) );
+
+		// Should already be set to a 0 user.
+		$this->assertFalse( bp_current_user_can( 'bp_moderate' ) );
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'parent_slug' => 'parent',
+			'parent_url' => bp_get_root_domain() . '/parent/',
+			'screen_function' => 'foo',
+			'site_admin_only' => true,
+		);
+
+		$this->assertFalse( bp_core_new_subnav_item( $args ) );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_css_id_should_fall_back_on_slug() {
+		bp_core_new_nav_item( array(
+			'name' => 'Parent',
+			'slug' => 'parent',
+			'screen_function' => 'foo',
+		) );
+
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'parent_slug' => 'parent',
+			'parent_url' => bp_get_root_domain() . '/parent/',
+			'screen_function' => 'foo',
+		);
+		bp_core_new_subnav_item( $args );
+
+		$this->assertSame( 'foo', buddypress()->bp_options_nav['parent']['foo']['css_id'] );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_css_id_should_be_respected() {
+		bp_core_new_nav_item( array(
+			'name' => 'Parent',
+			'slug' => 'parent',
+			'screen_function' => 'foo',
+		) );
+
+		$args = array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'parent_slug' => 'parent',
+			'parent_url' => bp_get_root_domain() . '/parent/',
+			'screen_function' => 'foo',
+			'item_css_id' => 'bar',
+		);
+		bp_core_new_subnav_item( $args );
+
+		$this->assertSame( 'bar', buddypress()->bp_options_nav['parent']['foo']['css_id'] );
+	}
+
+	/**
+	 * @ticket BP7931
+	 */
+	public function test_subnav_should_not_404_on_early_bp_setup_nav_priority() {
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$user_domain = bp_core_get_user_domain( $u );
+
+		// Register a subnav on 'bp_setup_nav' hook early (at priority zero).
+		add_action( 'bp_setup_nav', function() use ( $user_domain ) {
+			/*
+			 * Emulate a subnav screen.
+			 *
+			 * The bp_core_load_template() call is imperative for our 404 check to work!
+			 */
+			$screen = function() {
+				bp_core_load_template ('members/single/plugins');
+			};
+
+			// Register the subnav.
+			bp_core_new_subnav_item( array (
+				'name'            => 'Testing',
+				'slug'            => 'testing',
+				'parent_url'      => $user_domain . bp_get_profile_slug (). '/',
+				'parent_slug'     => bp_get_profile_slug (),
+				'screen_function' => $screen,
+				'position'        => 20
+			) );
+		}, 0 );
+
+		// Emulate visit to our new subnav page.
+		$this->go_to( $user_domain . bp_get_profile_slug () . '/testing/' );
+
+		// Assert that subnav page does not 404.
+		$this->assertFalse( is_404() );
+
+		$this->set_current_user( $old_current_user );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreRemoveNavItem.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreRemoveNavItem.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreRemoveNavItem.php	(revision 12265)
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @group core
+ * @group nav
+ */
+class BP_Tests_Core_Nav_BpCoreRemoveNavItem extends BP_UnitTestCase {
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_should_remove_subnav_items() {
+		$bp = buddypress();
+
+		$_bp_nav = $bp->bp_nav;
+		$_bp_options_nav = $bp->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'Bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'bar',
+		) );
+
+		$this->assertTrue( isset( $bp->bp_nav['foo'] ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['foo'] ) );
+		$this->assertTrue( isset( $bp->bp_options_nav['foo']['bar'] ) );
+
+		bp_core_remove_nav_item( 'foo' );
+
+		$this->assertFalse( isset( $bp->bp_options_nav['foo']['bar'] ) );
+
+		$bp->bp_nav = $_bp_nav;
+		$bp->bp_options_nav = $_bp_options_nav;
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_should_remove_nav_item() {
+		$bp = buddypress();
+
+		$_bp_nav = $bp->bp_nav;
+		$_bp_options_nav = $bp->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+		) );
+
+		$this->assertTrue( isset( $bp->bp_nav['foo'] ) );
+
+		bp_core_remove_nav_item( 'foo' );
+
+		$this->assertFalse( isset( $bp->bp_nav['foo'] ) );
+
+		$bp->bp_nav = $_bp_nav;
+		$bp->bp_options_nav = $_bp_options_nav;
+	}
+
+	/**
+	 * @ticket BP7203
+	 */
+	public function test_remove_subnav_item_array_as_screen_function() {
+		$bp = buddypress();
+
+		add_filter( 'bp_is_active', array( $this, 'foo_is_active' ), 10, 2 );
+
+		$bp->foo = new stdClass;
+		$bp->foo->nav = new BP_Core_Nav( 0 );
+
+		$expected = array( 'foo', 'bar' );
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => $expected,
+		), 'foo' );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'Bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => $expected,
+		), 'foo' );
+
+		remove_filter( 'bp_is_active', array( $this, 'foo_is_active' ), 10 );
+
+		$this->assertNotEmpty( $bp->foo->nav->get_primary( array( 'slug' => 'foo' ), false ) );
+
+		$tested = $bp->foo->nav->delete_nav( 'foo' );
+		$this->assertSame( $expected, reset( $tested ) );
+
+		$this->assertEmpty( $bp->foo->nav->get_primary( array( 'slug' => 'foo' ), false ) );
+	}
+
+	/**
+	 * Helper method to filter 'bp_is_active' for unit tests.
+	 */
+	public function foo_is_active( $retval, $component ) {
+		if ( 'foo' === $component ) {
+			$retval = true;
+		}
+
+		return $retval;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreRemoveSubnavItem.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreRemoveSubnavItem.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpCoreRemoveSubnavItem.php	(revision 12265)
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @group core
+ * @group nav
+ */
+class BP_Tests_Core_Nav_BpCoreRemoveSubnavItem extends BP_UnitTestCase {
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_should_remove_subnav_item() {
+		$bp = buddypress();
+
+		$_bp_nav = $bp->bp_nav;
+		$_bp_options_nav = $bp->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'Bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'bar',
+		) );
+
+		$this->assertTrue( isset( $bp->bp_options_nav['foo']['bar'] ) );
+
+		bp_core_remove_subnav_item( 'foo', 'bar' );
+
+		$this->assertFalse( isset( $bp->bp_options_nav['foo']['bar'] ) );
+
+		$bp->bp_nav = $_bp_nav;
+		$bp->bp_options_nav = $_bp_options_nav;
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_should_fail_on_incorrect_parent() {
+		$bp = buddypress();
+
+		$_bp_nav = $bp->bp_nav;
+		$_bp_options_nav = $bp->bp_options_nav;
+
+		bp_core_new_nav_item( array(
+			'name' => 'Foo',
+			'slug' => 'foo',
+			'screen_function' => 'foo',
+		) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'Bar',
+			'slug' => 'bar',
+			'parent_slug' => 'foo',
+			'parent_url' => 'foo',
+			'screen_function' => 'bar',
+		) );
+
+		$this->assertTrue( isset( $bp->bp_options_nav['foo']['bar'] ) );
+
+		bp_core_remove_subnav_item( 'bad-parent', 'bar' );
+
+		$this->assertTrue( isset( $bp->bp_options_nav['foo']['bar'] ) );
+
+		$bp->bp_nav = $_bp_nav;
+		$bp->bp_options_nav = $_bp_options_nav;
+	}
+
+	public function test_backcompat_remove_group_nav_items() {
+		$g1 = self::factory()->group->create();
+
+		// In group context
+		$g_obj = groups_get_group( $g1 );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		bp_core_new_subnav_item( array(
+			'name' => 'Clam',
+			'slug' => 'clam',
+			'parent_slug' => bp_get_current_group_slug(),
+			'parent_url' => bp_get_group_permalink( $g_obj ),
+			'screen_function' => 'clam_subnav',
+		) );
+
+		bp_core_remove_subnav_item( $g_obj->slug, 'clam' );
+
+		$nav = bp_get_nav_menu_items( 'groups' );
+		$found = false;
+		foreach ( $nav as $_nav ) {
+			if ( 'clam' === $_nav->css_id ) {
+				$found = true;
+				break;
+			}
+		}
+
+		$this->assertFalse( $found );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpGetNavMenuItems.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpGetNavMenuItems.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/nav/bpGetNavMenuItems.php	(revision 12265)
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @group core
+ * @group nav
+ */
+class BP_Tests_Core_Nav_BpGetNavMenuItems extends BP_UnitTestCase {
+	/**
+	 * @ticket BP7110
+	 */
+	public function test_top_level_link_should_point_to_displayed_user_for_loggedin_user() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$this->set_current_user( $users[0] );
+		$user_1_domain = bp_core_get_user_domain( $users[1] );
+		$this->go_to( $user_1_domain );
+
+		$found = bp_get_nav_menu_items();
+
+		// Find the Activity top-level item.
+		$activity_item = null;
+		foreach ( $found as $f ) {
+			if ( 'activity' === $f->css_id ) {
+				$activity_item = $f;
+				break;
+			}
+		}
+
+		$this->assertSame( trailingslashit( $user_1_domain ) . 'activity/', $activity_item->link );
+	}
+
+	/**
+	 * @ticket BP7110
+	 */
+	public function test_top_level_link_should_point_to_displayed_user_for_loggedout_user() {
+		$user = self::factory()->user->create();
+
+		$this->set_current_user( 0 );
+		$user_domain = bp_core_get_user_domain( $user );
+		$this->go_to( $user_domain );
+
+		$found = bp_get_nav_menu_items();
+
+		// Find the Activity top-level item.
+		$activity_item = null;
+		foreach ( $found as $f ) {
+			if ( 'activity' === $f->css_id ) {
+				$activity_item = $f;
+				break;
+			}
+		}
+
+		$this->assertSame( trailingslashit( $user_domain ) . 'activity/', $activity_item->link );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/suggestions-nonauth.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/suggestions-nonauth.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/suggestions-nonauth.php	(revision 12265)
@@ -0,0 +1,176 @@
+<?php
+/**
+ * Suggestions API tests specifically for non-authenticated (anonymous) users.
+ *
+ * @group api
+ * @group suggestions
+ */
+class BP_Tests_Suggestions_Non_Authenticated extends BP_UnitTestCase {
+	protected static $group_ids    = array();
+	protected static $group_slugs  = array();
+	protected static $user_ids     = array();
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		$users = array(
+			// user_login, display_name
+			array( 'aardvark',    'Bob Smith' ),
+			array( 'alpaca red',  'William Quinn' ),
+			array( 'cat',         'Lauren Curtis' ),
+			array( 'caterpillar', 'Eldon Burrows' ),
+			array( 'dog green',   'Reece Thornton' ),
+			array( 'pig',         'Joshua Barton' ),
+			array( 'rabbit blue', 'Amber Hooper' ),
+			array( 'smith',       'Robert Bar' ),
+			array( 'snake',       'Eleanor Moore' ),
+			array( 'xylo',        'Silver McFadden' ),
+			array( 'zoom',        'Lisa Smithy' ),
+		);
+
+		// Create some dummy users.
+		foreach( $users as $user_index => $user ) {
+			$new_user = $factory->user->create( array(
+				'display_name' => $user[1],
+				'user_login'   => $user[0],
+				'user_email'   => "test-$user_index@example.com",
+			) );
+
+			self::$user_ids[ $user[0] ] = $new_user;
+		}
+
+		self::$group_slugs['hidden']  = 'the-maw';
+		self::$group_slugs['public']  = 'the-great-journey';
+		self::$group_slugs['private'] = 'tsavo-highway';
+
+		// Create dummy groups.
+		self::$group_ids['hidden'] = $factory->group->create( array(
+			'creator_id' => self::$user_ids['xylo'],
+			'slug'       => self::$group_slugs['hidden'],
+			'status'     => 'hidden',
+		) );
+		self::$group_ids['public'] = $factory->group->create( array(
+			'creator_id' => self::$user_ids['xylo'],
+			'slug'       => self::$group_slugs['public'],
+			'status'     => 'public',
+		) );
+		self::$group_ids['private'] = $factory->group->create( array(
+			'creator_id' => self::$user_ids['xylo'],
+			'slug'       => self::$group_slugs['private'],
+			'status'     => 'private',
+		) );
+
+		// Add dummy users to dummy hidden groups.
+		groups_join_group( self::$group_ids['hidden'], self::$user_ids['pig'] );
+		groups_join_group( self::$group_ids['hidden'], self::$user_ids['alpaca red'] );
+
+		// Add dummy users to dummy public groups.
+		groups_join_group( self::$group_ids['public'], self::$user_ids['aardvark'] );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['alpaca red'] );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['cat'] );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['smith'] );
+
+		// Add dummy users to dummy private groups.
+		groups_join_group( self::$group_ids['private'], self::$user_ids['cat'] );
+		groups_join_group( self::$group_ids['private'], self::$user_ids['caterpillar'] );
+
+		self::commit_transaction();
+	}
+
+	public static function tearDownAfterClass() {
+		foreach ( self::$group_ids as $group_id ) {
+			groups_delete_group( $group_id );
+		}
+
+		foreach ( self::$user_ids as $user_id ) {
+			if ( is_multisite() ) {
+				wpmu_delete_user( $user_id );
+			} else {
+				wp_delete_user( $user_id );
+			}
+		}
+
+		self::commit_transaction();
+	}
+
+	/**
+	 * Tests below this point are expected to fail.
+	 */
+
+	public function test_suggestions_with_type_members_and_only_friends() {
+		// only_friends requires authenticated requests
+		$suggestions = bp_core_get_suggestions( array(
+			'only_friends' => true,
+			'type'         => 'members',
+			'term'         => 'smith',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+
+	public function test_suggestions_with_type_groupmembers_and_only_friends() {
+		// only_friends requires authenticated requests
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id'     => self::$group_ids['public'],
+			'only_friends' => true,
+			'type'         => 'members',
+			'term'         => 'smith',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+
+	public function test_suggestions_with_type_groupmembers_hidden() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'pig',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+
+	public function test_suggestions_with_type_groupmembers_private() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'cat',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+
+	public function test_suggestions_with_type_groupmembers_public_and_exclude_group_from_results() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // aardvark, smith.
+
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // zoom
+	}
+
+	public function test_suggestions_with_type_groupmembers_private_and_exclude_group_from_results() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'cat',
+		) );
+		$this->assertTrue( is_wp_error( $suggestions ) );  // no access to group.
+	}
+
+	public function test_suggestions_with_type_groupmembers_hidden_and_exclude_group_from_results() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'pig',
+		) );
+		$this->assertTrue( is_wp_error( $suggestions ) );  // no access to group.
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/suggestions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/suggestions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/suggestions.php	(revision 12265)
@@ -0,0 +1,503 @@
+<?php
+/**
+ * Suggestions API tests for authenticated (logged in) users.
+ *
+ * @group api
+ * @group suggestions
+ */
+class BP_Tests_Suggestions_Authenticated extends BP_UnitTestCase {
+	protected static $current_user = null;
+	protected static $group_ids    = array();
+	protected static $group_slugs  = array();
+	protected static $old_user_id  = 0;
+	protected static $user_ids     = array();
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$old_user_id  = get_current_user_id();
+		self::$current_user = $factory->user->create( array(
+			'display_name' => 'Katie Parker',
+			'user_login'   => 'katie',
+			'user_email'   => 'test-katie@example.com',
+		) );
+
+		$users = array(
+			// user_login, display_name
+			array( 'aardvark',    'Bob Smith' ),
+			array( 'alpaca red',  'William Quinn' ),
+			array( 'cat',         'Lauren Curtis' ),
+			array( 'caterpillar', 'Eldon Burrows' ),
+			array( 'dog green',   'Reece Thornton' ),
+			array( 'pig',         'Joshua Barton' ),
+			array( 'rabbit blue', 'Amber Hooper' ),
+			array( 'smith',       'Robert Bar' ),
+			array( 'snake',       'Eleanor Moore' ),
+			array( 'xylo',        'Silver McFadden' ),
+			array( 'zoom',        'Lisa Smithy' ),
+		);
+
+		// Create some dummy users.
+		foreach ( $users as $user_index => $user ) {
+			$new_user = $factory->user->create( array(
+				'display_name' => $user[1],
+				'user_login'   => $user[0],
+				'user_email'   => "test-$user_index@example.com",
+			) );
+
+			self::$user_ids[ $user[0] ] = $new_user;
+		}
+
+		// Create some dummy friendships (but not the corresponding activity items).
+		remove_action( 'friends_friendship_accepted', 'bp_friends_friendship_accepted_activity', 10 );
+		friends_add_friend( self::$current_user, self::$user_ids['aardvark'], true );
+		friends_add_friend( self::$current_user, self::$user_ids['cat'], true );
+		friends_add_friend( self::$current_user, self::$user_ids['caterpillar'], true );
+		friends_add_friend( self::$current_user, self::$user_ids['pig'], true );
+		add_action( 'friends_friendship_accepted', 'bp_friends_friendship_accepted_activity', 10, 4 );
+
+		self::$group_slugs['hidden']  = 'the-maw';
+		self::$group_slugs['public']  = 'the-great-journey';
+		self::$group_slugs['private'] = 'tsavo-highway';
+
+		// Create dummy groups.
+		self::$group_ids['hidden'] = $factory->group->create( array(
+			'creator_id' => self::$user_ids['xylo'],
+			'slug'       => self::$group_slugs['hidden'],
+			'status'     => 'hidden',
+		) );
+		self::$group_ids['public'] = $factory->group->create( array(
+			'creator_id' => self::$user_ids['xylo'],
+			'slug'       => self::$group_slugs['public'],
+			'status'     => 'public',
+		) );
+		self::$group_ids['private'] = $factory->group->create( array(
+			'creator_id' => self::$user_ids['xylo'],
+			'slug'       => self::$group_slugs['private'],
+			'status'     => 'private',
+		) );
+
+		// Add dummy users to dummy hidden groups.
+		groups_join_group( self::$group_ids['hidden'], self::$user_ids['pig'] );
+		groups_join_group( self::$group_ids['hidden'], self::$user_ids['alpaca red'] );
+
+		// Add dummy users to dummy public groups.
+		groups_join_group( self::$group_ids['public'], self::$current_user );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['aardvark'] );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['alpaca red'] );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['cat'] );
+		groups_join_group( self::$group_ids['public'], self::$user_ids['smith'] );
+
+		// Add dummy users to dummy private groups.
+		groups_join_group( self::$group_ids['private'], self::$user_ids['cat'] );
+		groups_join_group( self::$group_ids['private'], self::$user_ids['caterpillar'] );
+
+		self::commit_transaction();
+	}
+
+	public static function tearDownAfterClass() {
+		foreach ( self::$group_ids as $group_id ) {
+			groups_delete_group( $group_id );
+		}
+
+		foreach ( array_merge( self::$user_ids, array( self::$current_user ) ) as $user_id ) {
+			if ( is_multisite() ) {
+				wpmu_delete_user( $user_id );
+			} else {
+				wp_delete_user( $user_id );
+			}
+		}
+
+		self::commit_transaction();
+	}
+
+	public function setUp() {
+		parent::setUp();
+		$this->set_current_user( self::$current_user );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( self::$old_user_id );
+	}
+
+	public function test_suggestions_with_type_members() {
+		$suggestions = bp_core_get_suggestions( array(
+			'type' => 'members',
+			'term' => 'smith',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 3, count( $suggestions ) );  // aardvark, smith, zoom.
+	}
+
+	public function test_suggestions_with_type_members_and_limit() {
+		$suggestions = bp_core_get_suggestions( array(
+			'limit' => 2,
+			'type'  => 'members',
+			'term'  => 'smith',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // two of: aardvark, smith, zoom.
+	}
+
+	public function test_suggestions_with_type_members_and_only_friends() {
+		$suggestions = bp_core_get_suggestions( array(
+			'only_friends' => true,
+			'type'         => 'members',
+			'term'         => 'smith',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // aardvark.
+
+		$suggestions = bp_core_get_suggestions( array(
+			'only_friends' => true,
+			'type'         => 'members',
+			'term'         => 'cat',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // cat, caterpillar.
+	}
+
+	public function test_suggestions_with_type_members_and_term_as_displayname() {
+		$suggestions = bp_core_get_suggestions( array(
+			'type' => 'members',
+			'term' => 'aardvark',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // aardvark.
+	}
+
+	public function test_suggestions_with_type_members_and_term_as_usernicename() {
+		$suggestions = bp_core_get_suggestions( array(
+			'type' => 'members',
+			'term' => 'eleanor',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // snake.
+	}
+
+	public function test_suggestions_with_term_as_current_user() {
+		$suggestions = bp_core_get_suggestions( array(
+			'type' => 'members',
+			'term' => 'katie',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );
+		$this->assertSame( 'katie', $suggestions[0]->ID );
+	}
+
+
+	public function test_suggestions_with_type_groupmembers_public() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // aardvark, smith.
+	}
+
+	public function test_suggestions_with_type_groupmembers_public_and_limit() {
+		$suggestions = bp_core_get_suggestions( array(
+			'limit'    => 1,
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // one of: aardvark, smith.
+	}
+
+	public function test_suggestions_with_type_groupmembers_public_and_only_friends() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id'     => self::$group_ids['public'],
+			'only_friends' => true,
+			'type'         => 'members',
+			'term'         => 'smith',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // aardvark.
+	}
+
+	public function test_suggestions_with_type_groupmembers_public_and_term_as_displayname() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'aardvark',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // aardvark.
+	}
+
+	public function test_suggestions_with_type_groupmembers_public_and_term_as_usernicename() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'robert',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // smith.
+	}
+
+	public function test_suggestions_with_type_groupmembers_public_as_id() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // aardvark, smith.
+	}
+
+	public function test_suggestions_with_type_groupmembers_hidden() {
+		// current_user isn't a member of the hidden group
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'pig',
+		) );
+		$this->assertTrue( is_wp_error( $suggestions ) );
+
+		// "alpaca red" is in the hidden group
+		$this->set_current_user( self::$user_ids['alpaca red'] );
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'pig',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // pig
+	}
+
+	public function test_suggestions_with_type_groupmembers_private() {
+		// current_user isn't a member of the private group.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'cat',
+		) );
+		$this->assertTrue( is_wp_error( $suggestions ) );
+
+		// "caterpillar" is in the private group
+		$this->set_current_user( self::$user_ids['caterpillar'] );
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'cat',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // cat, caterpillar
+	}
+
+
+	public function test_suggestions_with_type_groupmembers_public_and_exclude_group_from_results() {
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 2, count( $suggestions ) );  // aardvark, smith.
+
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['public'],
+			'type'     => 'members',
+			'term'     => 'smith',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // zoom
+	}
+
+	public function test_suggestions_with_type_groupmembers_private_and_exclude_group_from_results() {
+		// current_user isn't a member of the private group.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'cat',
+		) );
+		$this->assertTrue( is_wp_error( $suggestions ) );
+
+
+		$this->set_current_user( self::$user_ids['caterpillar'] );
+
+		// "cat" is in the private group, so won't show up here.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'cat',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEmpty( $suggestions );
+
+		// "zoo" is not the private group, so will show up here.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['private'],
+			'type'     => 'members',
+			'term'     => 'zoo',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // zoo
+	}
+
+	public function test_suggestions_with_type_groupmembers_hidden_and_exclude_group_from_results() {
+		// current_user isn't a member of the hidden group.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'pig',
+		) );
+		$this->assertTrue( is_wp_error( $suggestions ) );
+
+
+		$this->set_current_user( self::$user_ids['alpaca red'] );
+
+		// "alpaca red" is in the hidden group, so won't show up here.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'alpaca red',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEmpty( $suggestions );
+
+		// "zoo" is not the hidden group, so will show up here.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => -self::$group_ids['hidden'],
+			'type'     => 'members',
+			'term'     => 'zoo',
+		) );
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertEquals( 1, count( $suggestions ) );  // zoo
+	}
+
+
+	/**
+	 * These next tests check the format of the response from the Suggestions API.
+	 */
+
+	public function test_suggestions_response_no_matches() {
+		$suggestions = bp_core_get_suggestions( array(
+			'term' => 'abcdefghijklmnopqrstuvwxyz',
+			'type' => 'members',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertInternalType( 'array', $suggestions );
+		$this->assertEmpty( $suggestions );
+	}
+
+	public function test_suggestions_response_single_match() {
+		$suggestion = bp_core_get_suggestions( array(
+			'term' => 'zoom',
+			'type' => 'members',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestion ) );
+		$this->assertInternalType( 'array', $suggestion );
+		$this->assertNotEmpty( $suggestion );
+
+		$suggestion = array_shift( $suggestion );
+
+		$this->assertInternalType( 'object', $suggestion );
+		$this->assertAttributeNotEmpty( 'image', $suggestion );
+		$this->assertAttributeNotEmpty( 'ID', $suggestion );
+		$this->assertAttributeNotEmpty( 'name', $suggestion );
+	}
+
+	public function test_suggestions_response_multiple_matches() {
+		$suggestions = bp_core_get_suggestions( array(
+			'term' => 'cat',
+			'type' => 'members',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestions ) );
+		$this->assertInternalType( 'array', $suggestions );
+		$this->assertNotEmpty( $suggestions );
+
+		foreach ( $suggestions as $suggestion ) {
+			$this->assertInternalType( 'object', $suggestion );
+			$this->assertAttributeNotEmpty( 'image', $suggestion );
+			$this->assertAttributeNotEmpty( 'ID', $suggestion );
+			$this->assertAttributeNotEmpty( 'name', $suggestion );
+		}
+	}
+
+	public function test_suggestions_term_is_case_insensitive() {
+		$lowercase = bp_core_get_suggestions( array(
+			'term' => 'lisa',
+			'type' => 'members',
+		) );
+		$this->assertFalse( is_wp_error( $lowercase ) );
+		$this->assertEquals( 1, count( $lowercase ) );
+
+		$uppercase = bp_core_get_suggestions( array(
+			'term' => 'LISA',
+			'type' => 'members',
+		) );
+		$this->assertFalse( is_wp_error( $uppercase ) );
+		$this->assertEquals( 1, count( $uppercase ) );
+
+		$this->assertSame( $lowercase[0]->ID, $uppercase[0]->ID );
+		$this->assertSame( 'zoom', $lowercase[0]->ID );
+	}
+
+	public function test_suggestions_response_property_types() {
+		$suggestion = bp_core_get_suggestions( array(
+			'term' => 'zoom',
+			'type' => 'members',
+		) );
+
+		$this->assertFalse( is_wp_error( $suggestion ) );
+		$this->assertInternalType( 'array', $suggestion );
+		$this->assertNotEmpty( $suggestion );
+
+		$suggestion = array_shift( $suggestion );
+
+		$this->assertInternalType( 'object', $suggestion );
+		$this->assertAttributeInternalType( 'string', 'image', $suggestion );
+		$this->assertAttributeInternalType( 'string', 'ID', $suggestion );
+		$this->assertAttributeInternalType( 'string', 'name', $suggestion );
+	}
+
+
+	/**
+	 * Tests below this point are expected to fail.
+	 */
+
+	public function test_suggestions_with_bad_type() {
+		$suggestions = bp_core_get_suggestions( array(
+			'type' => 'fake_type',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+
+	public function test_suggestions_with_type_groupmembers_and_bad_group_ids() {
+		// group_ids can't be a group slug.
+		$suggestions = bp_core_get_suggestions( array(
+			'group_id' => 'fake-group-slug',
+			'type'     => 'members',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+
+	public function test_suggestions_with_bad_term() {
+		// a non-empty term is mandatory
+		$suggestions = bp_core_get_suggestions( array(
+			'term' => '',
+			'type' => 'members',
+		) );
+
+		$this->assertTrue( is_wp_error( $suggestions ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/template/bpCreateExcerpt.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/template/bpCreateExcerpt.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/template/bpCreateExcerpt.php	(revision 12265)
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @group core
+ * @group bp_create_excerpt
+ */
+class BP_Tests_Core_Template_BPCreateExcerpt extends BP_UnitTestCase {
+	public function test_should_ignore_html_tag_when_html_true() {
+		$text = 'foo <hr> bar baz';
+		$expected = 'foo <hr /> bar';
+		$this->assertSame( $expected, bp_create_excerpt( $text, 8, array(
+			'ending' => '',
+			'html' => true,
+			'exact' => true,
+		) ) );
+	}
+
+	/**
+	 * @ticket BP3680
+	 */
+	public function test_should_ignore_single_word_html_comments_when_html_true() {
+		$text = 'foo <!--more--> bar baz';
+		$expected = 'foo <!--more--> bar';
+		$this->assertSame( $expected, bp_create_excerpt( $text, 8, array(
+			'ending' => '',
+			'html' => true,
+			'exact' => true,
+		) ) );
+	}
+
+	/**
+	 * @ticket BP3680
+	 */
+	public function test_should_ignore_multiple_word_html_comments_when_html_true() {
+		$text = 'foo <!--one two three--> bar baz';
+		$expected = 'foo <!--one two three--> bar';
+		$this->assertSame( $expected, bp_create_excerpt( $text, 8, array(
+			'ending' => '',
+			'html' => true,
+			'exact' => true,
+		) ) );
+	}
+
+	public function test_should_break_on_prior_word_boundary_when_exact_is_false() {
+		$text = 'aaaaa aaaaaa';
+		$expected = 'aaaaa';
+		$this->assertSame( $expected, bp_create_excerpt( $text, 7, array(
+			'exact' => false,
+			'ending' => '',
+		) ) );
+	}
+
+	/**
+	 * @ticket BP6517
+	 */
+	public function test_exact_false_should_properly_account_for_accented_characters() {
+		$text = 'Toutes les connaissances que les hommes avaient mis sur Internet lui étaient accessible. Les grandes bibliothèques du monde entier n’avaient plus de secret pour lui. Il pouvait apprendre très vite, beaucoup plus vite que n’importe quel humain.
+Il avait appris toutes les connaissances du monde entier, visiter tout les pays. C’est lui qui avait fait en sorte qu’Internet se déploie ainsi.';
+		$expected = 'Toutes les connaissances que les hommes avaient mis sur Internet lui étaient accessible. Les';
+		$this->assertSame( $expected, bp_create_excerpt( $text, 98, array(
+			'ending' => '',
+			'exact' => false,
+		) ) );
+	}
+
+	/**
+	 * @ticket BP6254
+	 */
+	public function test_should_trim_too_long_first_word_to_max_characters_even_when_exact_is_false() {
+		$text = 'aaaaaaaaaaa';
+		$expected = 'aaa';
+		$this->assertSame( $expected, bp_create_excerpt( $text, 3, array(
+			'exact' => false,
+			'ending' => '',
+		) ) );
+	}
+
+	/**
+	 * @ticket BP6517
+	 */
+	public function test_string_should_not_be_cut_mid_tag_when_exact_is_false() {
+		$text = '<p><span>Foo</span> <a href="http://example.com">Bar</a> Baz.</p><p>Foo Bar Baz</p>';
+		$actual = bp_create_excerpt( $text, 7, array(
+			'html' => true,
+			'ending' => '',
+			'exact' => false,
+		) );
+		$this->assertSame( '<p><span>Foo</span> <a href="http://example.com">Bar</a></p>', $actual );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/core/template/bpUserHasAccess.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/core/template/bpUserHasAccess.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/core/template/bpUserHasAccess.php	(revision 12265)
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @group core
+ * @group template
+ */
+class BP_Tests_Core_Template_BpUserHasAccess extends BP_UnitTestCase {
+	public function test_should_return_true_for_bp_moderate_user() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$this->grant_bp_moderate( $users[0] );
+		$this->set_current_user( $users[0] );
+
+		$this->go_to( bp_core_get_user_domain( $users[1] ) );
+
+		$this->assertTrue( bp_user_has_access( $users[0] ) );
+	}
+
+	public function test_should_return_false_on_anothers_profile_for_user_without_bp_moderate() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$this->set_current_user( $users[0] );
+
+		$this->go_to( bp_core_get_user_domain( $users[1] ) );
+
+		$this->assertFalse( bp_user_has_access( $users[0] ) );
+	}
+
+	public function test_should_return_true_on_own_profile() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$this->set_current_user( $users[0] );
+
+		$this->go_to( bp_core_get_user_domain( $users[0] ) );
+
+		$this->assertTrue( bp_user_has_access( $users[0] ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/friends/activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/friends/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/friends/activity.php	(revision 12265)
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * @group friends
+ * @group activity
+ */
+class BP_Tests_Friends_Activity extends BP_UnitTestCase {
+	/**
+	 * @group activity_action
+	 * @group bp_friends_format_activity_action_friendship_accepted
+	 */
+	public function test_bp_friends_format_activity_action_friendship_accepted() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->friends->id,
+			'type' => 'friendship_accepted',
+			'user_id' => $u1,
+			'secondary_item_id' => $u2,
+		) );
+
+		$expected = sprintf( __( '%1$s and %2$s are now friends', 'buddypress' ), bp_core_get_userlink( $u1 ), bp_core_get_userlink( $u2 ) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_friends_format_activity_action_friendship_created
+	 */
+	public function test_bp_friends_format_activity_action_friendship_created() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->friends->id,
+			'type' => 'friendship_created',
+			'user_id' => $u1,
+			'secondary_item_id' => $u2,
+		) );
+
+		$expected = sprintf( __( '%1$s and %2$s are now friends', 'buddypress' ), bp_core_get_userlink( $u1 ), bp_core_get_userlink( $u2 ) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group friends_delete_activity
+	 */
+	public function test_friends_delete_activity() {
+		$old_user = get_current_user_id();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		friends_add_friend( $u2, $u1 );
+		$friendship_id = friends_get_friendship_id( $u2, $u1 );
+
+		// Set current user to u1 to accepte the friendship
+		$this->set_current_user( $u1 );
+		friends_accept_friendship( $friendship_id );
+
+		// Reset the current user
+		$this->set_current_user( $old_user );
+
+		// Random activities
+		$au1 = self::factory()->activity->create( array( 'user_id' => $u1 ) );
+		$au2 = self::factory()->activity->create( array( 'user_id' => $u2 ) );
+
+		$fc_act = bp_activity_get( array(
+			'component'   => buddypress()->friends->id,
+			'item_id'     => $friendship_id,
+			'filter'      => array( 'action' => array( 'friendship_created' ) ),
+			'show_hidden' => false
+		) );
+
+		$this->assertTrue( count( $fc_act['activities'] ) == 1, '1 public activity should be created when a friendship is confirmed' );
+
+		// Remove the friendship
+		friends_remove_friend( $u2, $u1 );
+
+		$this->assertFalse( friends_check_friendship( $u2, $u1 ), '2 users should not be friend once the friendship is removed' );
+
+		$fd_act = bp_activity_get( array(
+			'component'   => buddypress()->friends->id,
+			'item_id'     => $friendship_id,
+			'filter'      => array( 'action' => array( 'friendship_created' ) ),
+			'show_hidden' => true
+		) );
+
+		$this->assertTrue( count( $fd_act['activities'] ) == 0, 'friends_delete_activity() should remove "friendship_created" activities about a deleted friendship' );
+	}
+
+	/**
+	 * @group bp_friends_friendship_accepted_activity
+	 */
+	public function test_bp_friends_friendship_accepted_activity() {
+		$old_user = get_current_user_id();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		friends_add_friend( $u2, $u1 );
+		$friendship_id = friends_get_friendship_id( $u2, $u1 );
+
+		// Set current user to u1 to accept the friendship
+		$this->set_current_user( $u1 );
+		friends_accept_friendship( $friendship_id );
+
+		// Reset the current user
+		$this->set_current_user( $old_user );
+
+		$u1_act = bp_activity_get( array(
+			'component'   => buddypress()->friends->id,
+			'item_id'     => $friendship_id,
+			'scope'       => 'just-me',
+			'filter'      => array( 'action' => array( 'friendship_created' ), 'user_id' => $u1 ),
+		) );
+
+		$this->assertTrue( count( $u1_act['activities'] ) == 1, 'a public activity should be listed in the friend stream' );
+
+		$u2_act = bp_activity_get( array(
+			'component'   => buddypress()->friends->id,
+			'item_id'     => $friendship_id,
+			'scope'       => 'just-me',
+			'filter'      => array( 'action' => array( 'friendship_created' ), 'user_id' => $u2 ),
+		) );
+
+		$this->assertTrue( count( $u2_act['activities'] ) == 1, 'a public activity should be listed in the initiator stream' );
+	}
+
+	/**
+	 * @group bp_cleanup_friendship_activities
+	 */
+	public function test_bp_cleanup_friendship_activities() {
+		$old_user = get_current_user_id();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$users = array( $u1, $u2 );
+
+		friends_add_friend( $u2, $u1 );
+		$friendship_id = friends_get_friendship_id( $u2, $u1 );
+
+		// Set current user to u1 to accepte the friendship and generate a public activity
+		$this->set_current_user( $u1 );
+		friends_accept_friendship( $friendship_id );
+
+		// Reset the current user
+		$this->set_current_user( $old_user );
+
+		$users[] = self::factory()->user->create();
+		$users[] = self::factory()->user->create();
+
+		foreach( $users as $u ) {
+			bp_activity_add( array(
+				'user_id'       => $u,
+				'item_id'       => $friendship_id,
+				'type'          => 'friendship_created',
+				'component'     => buddypress()->friends->id,
+				'hide_sitewide' => true,
+			) );
+		}
+
+		$hidden = bp_activity_get( array(
+			'component'   => buddypress()->friends->id,
+			'filter'      => array( 'action' => array( 'friendship_created' ) ),
+			'show_hidden' => true,
+		) );
+
+		bp_cleanup_friendship_activities();
+
+		$check = bp_activity_get( array(
+			'component'   => buddypress()->friends->id,
+			'item_id'     => $friendship_id,
+			'filter'      => array( 'action' => array( 'friendship_created' ) ),
+			'show_hidden' => true,
+		) );
+
+		$this->assertTrue( count( $check['activities'] ) == 1 );
+	}
+
+	/**
+	 * @group friends_delete_activity
+	 */
+	public function test_delete_friendship_activity_on_user_delete() {
+		$old_user = get_current_user_id();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		friends_add_friend( $u2, $u1 );
+		$friendship_id = friends_get_friendship_id( $u2, $u1 );
+
+		// Set current user to u1 to accept the friendship
+		$this->set_current_user( $u1 );
+		friends_accept_friendship( $friendship_id );
+
+		// Reset the current user
+		$this->set_current_user( $old_user );
+
+		// Delete $u1.
+		wp_delete_user( $u1 );
+
+		// 'friendship_created' activity item should not exist.
+		$friendship_activity = bp_activity_get( array(
+			'component' => buddypress()->friends->id,
+			'filter'    => array(
+				'action' => array( 'friendship_created' ),
+				'primary_id' => $friendship_id,
+			)
+		) );
+
+		$this->assertEmpty( $friendship_activity['activities'] );
+	}
+
+	public function test_delete_user_with_no_activities() {
+		$old_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$this->set_current_user( $u1 );
+
+		bp_activity_remove_all_user_data( $u1 );
+		wp_cache_delete( $u1, 'bp_last_activity' );
+		wp_delete_user( $u1 );
+
+		$this->set_current_user( $old_user );
+	}
+}
+
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/friends/class-bp-friends-friendship.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/friends/class-bp-friends-friendship.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/friends/class-bp-friends-friendship.php	(revision 12265)
@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * @group friends
+ */
+class BP_Tests_BP_Friends_Friendship_TestCases extends BP_UnitTestCase {
+	public function test_search_friends() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		xprofile_set_field_data( 1, $u2, 'Cool Dude' );
+		xprofile_set_field_data( 1, $u3, 'Rock And Roll America Yeah' );
+
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, true );
+
+		$friends = BP_Friends_Friendship::search_friends( 'Coo', $u1 );
+		$this->assertEquals( array( $u2 ), $friends['friends'] );
+	}
+
+	/**
+	 * @ticket BP6546
+	 */
+	public function test_search_friends_with_xprofile_inactive() {
+		$this->deactivate_component( 'xprofile' );
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		add_user_meta( $u2, 'nickname', 'Cool Dude' );
+		add_user_meta( $u3, 'nickname', 'Rock And Roll America Yeah' );
+
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, true );
+
+		$friends = BP_Friends_Friendship::search_friends( 'Coo', $u1 );
+		$this->assertEquals( array( $u2 ), $friends['friends'] );
+	}
+
+	public function test_get_bulk_last_active() {
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s' ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 1000 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 50 ),
+		) );
+
+		$friends = BP_Friends_Friendship::get_bulk_last_active( array( $u1, $u2, $u3, 'junk' ) );
+		$friend_ids = wp_list_pluck( $friends, 'user_id' );
+		$this->assertEquals( array( $u1, $u3, $u2 ), $friend_ids );
+	}
+
+	public function test_search_users() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		xprofile_set_field_data( 1, $u1, 'Freedom Isn\'t Free' );
+		xprofile_set_field_data( 1, $u2, 'Cool Dude' );
+		xprofile_set_field_data( 1, $u3, 'Rock And Roll America Yeah' );
+
+		// Needs a user_id param though it does nothing
+		$friends = BP_Friends_Friendship::search_users( 'Coo', 1 );
+		$this->assertEquals( array( $u2 ), $friends );
+	}
+
+	public function test_search_users_count() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		xprofile_set_field_data( 1, $u1, 'Freedom Isn\'t Free' );
+		xprofile_set_field_data( 1, $u2, 'Cool Dude' );
+		xprofile_set_field_data( 1, $u3, 'Rock And Roll America Yeah' );
+
+		// Needs a user_id param though it does nothing
+		$friends = BP_Friends_Friendship::search_users_count( 'Coo' );
+		$this->assertEquals( 1, $friends );
+	}
+
+	/**
+	 * @group check_is_friend
+	 */
+	public function test_check_is_friend_not_friends() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$this->assertEquals( 'not_friends', BP_Friends_Friendship::check_is_friend( $u1, $u2 ) );
+	}
+
+	/**
+	 * @group check_is_friend
+	 */
+	public function test_check_is_friend_pending() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		friends_add_friend( $u1, $u2, false );
+		$this->assertEquals( 'pending', BP_Friends_Friendship::check_is_friend( $u1, $u2 ) );
+	}
+
+	/**
+	 * @group check_is_friend
+	 */
+	public function test_check_is_friend_awaiting_response() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		friends_add_friend( $u1, $u2, false );
+		$this->assertEquals( 'awaiting_response', BP_Friends_Friendship::check_is_friend( $u2, $u1 ) );
+	}
+
+	/**
+	 * @group check_is_friend
+	 */
+	public function test_check_is_friend_is_friend() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		friends_add_friend( $u1, $u2, true );
+		$this->assertEquals( 'is_friend', BP_Friends_Friendship::check_is_friend( $u1, $u2 ) );
+	}
+
+	/**
+	 * @group BP6247
+	 */
+	public function test_save_method_should_update_existing_row() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$friendship = new BP_Friends_Friendship();
+		$friendship->initiator_user_id = $u1;
+		$friendship->friend_user_id = $u2;
+		$friendship->is_confirmed = 0;
+		$friendship->is_limited = 0;
+		$friendship->date_created = bp_core_current_time();
+		$friendship->is_confirmed = 1;
+		$friendship->save();
+
+		$fid = $friendship->id;
+
+		$f = new BP_Friends_Friendship( $fid );
+		$f->is_confirmed = 1;
+		$f->save();
+
+		$f2 = new BP_Friends_Friendship( $fid );
+		$this->assertEquals( 1, $f2->is_confirmed );
+	}
+
+	/**
+	 * @group friendship_caching
+	 */
+	public function test_new_bp_friends_friendship_object_should_hit_friendship_object_cache() {
+		global $wpdb;
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		friends_add_friend( $u1, $u2, true );
+		$fid = friends_get_friendship_id( $u1, $u2 );
+
+		$friendship_obj = new BP_Friends_Friendship( $fid, false, false );
+		$first_query_count = $wpdb->num_queries;
+		// Create it again.
+		$friendship_obj = new BP_Friends_Friendship( $fid, false, false );
+
+		$this->assertEquals( $first_query_count, $wpdb->num_queries );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/friends/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/friends/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/friends/functions.php	(revision 12265)
@@ -0,0 +1,486 @@
+<?php
+
+/**
+ * @group friends
+ * @group functions
+ */
+class BP_Tests_Friends_Functions extends BP_UnitTestCase {
+	static public $user_ids;
+
+	protected $filter_fired;
+
+	public function setUp() {
+		parent::setUp();
+		$this->filter_fired = '';
+	}
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		$now = time();
+
+		self::$user_ids[0] = $factory->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now ),
+		) );
+		self::$user_ids[1] = $factory->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		self::$user_ids[2] = $factory->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 200 ),
+		) );
+		self::$user_ids[3] = $factory->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 300 ),
+		) );
+		self::$user_ids[4] = $factory->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 400 ),
+		) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		foreach ( self::$user_ids as $user_id ) {
+			self::delete_user( $user_id );
+		}
+	}
+
+	/**
+	 * @group friends_get_friendship_request_user_ids
+	 * @group friends_add_friend
+	 * @group friends_accept_friendship
+	 */
+	public function test_requests_on_accept() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+
+		// request friendship
+		friends_add_friend( $u2, $u1 );
+		friends_add_friend( $u3, $u1 );
+
+		// Set the time of the earlier friendship for reliable ordering of the results.
+		$fid = friends_get_friendship_id( $u2, $u1 );
+		$friendship = new BP_Friends_Friendship( $fid, false, false );
+		$friendship->date_created = date( 'Y-m-d H:i:s', time() - 60 );
+		$friendship->save();
+
+		// get request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array( $u3, $u2 ), $requests );
+
+		// accept friendship
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+		friends_accept_friendship( friends_get_friendship_id( $u2, $u1 ) );
+
+		// refetch request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array( $u3 ), $requests );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group friends_get_friendship_request_user_ids
+	 * @group friends_add_friend
+	 */
+	public function test_requests_on_request() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+
+		// request friendship
+		friends_add_friend( $u2, $u1 );
+		// Set the time of the earlier friendship for reliable ordering of the results.
+		$fid = friends_get_friendship_id( $u2, $u1 );
+		$friendship = new BP_Friends_Friendship( $fid, false, false );
+		$friendship->date_created = date( 'Y-m-d H:i:s', time() - 60 );
+		$friendship->save();
+
+		// get request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array( $u2 ), $requests );
+
+		// request another friendship
+		friends_add_friend( $u3, $u1 );
+
+		// refetch request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array( $u3, $u2 ), $requests );
+	}
+
+	/**
+	 * @group friends_get_friendship_request_user_ids
+	 * @group friends_add_friend
+	 * @group friends_withdraw_friendship
+	 */
+	public function test_requests_on_withdraw() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		// request friendship
+		friends_add_friend( $u2, $u1 );
+
+		// get request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array( $u2 ), $requests );
+
+		// user 2 withdraws friendship
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+		friends_withdraw_friendship( $u2, $u1 );
+
+		// refetch request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array(), $requests );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group friends_get_friendship_request_user_ids
+	 * @group friends_add_friend
+	 * @group friends_reject_friendship
+	 */
+	public function test_requests_on_reject() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		// request friendship
+		friends_add_friend( $u2, $u1 );
+
+		// get request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array( $u2 ), $requests );
+
+		// user 1 rejects friendship
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+		friends_reject_friendship( friends_get_friendship_id( $u2, $u1 ) );
+
+		// refetch request count for user 1 and assert
+		$requests = friends_get_friendship_request_user_ids( $u1 );
+		$this->assertEquals( array(), $requests );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group friends_add_friend
+	 */
+	public function test_friends_add_friend_fail_on_self() {
+		$u1 = self::$user_ids[0];
+		$this->assertFalse( friends_add_friend( $u1, $u1 ) );
+	}
+
+	/**
+	 * @group friends_add_friend
+	 */
+	public function test_friends_add_friend_already_friends() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		friends_add_friend( $u1, $u2, true );
+
+		$this->assertTrue( friends_add_friend( $u1, $u2 ) );
+	}
+
+	/**
+	 * @group friends_check_friendship_status
+	 */
+	public function test_friends_check_friendship_status_in_members_loop() {
+		$now = time();
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+		$u4 = self::$user_ids[3];
+		$u5 = self::$user_ids[4];
+
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, false );
+		friends_add_friend( $u4, $u1, false );
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+
+		$found = array();
+		if ( bp_has_members() ) : while ( bp_members() ) : bp_the_member();
+			$found[ bp_get_member_user_id() ] = friends_check_friendship_status( $u1, bp_get_member_user_id() );
+		endwhile; endif;
+
+		$expected = array(
+			$u1 => 'not_friends',
+			$u2 => 'is_friend',
+			$u3 => 'pending',
+			$u4 => 'awaiting_response',
+			$u5 => 'not_friends',
+		);
+
+		$this->assertSame( $expected, $found );
+
+		// clean up
+		$GLOBALS['members_template'] = null;
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group friends_check_friendship_status
+	 */
+	public function test_friends_check_friendship_status_not_in_members_loop() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+		$u4 = self::$user_ids[3];
+		$u5 = self::$user_ids[4];
+
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, false );
+		friends_add_friend( $u4, $u1, false );
+
+		$found = array(
+			$u1 => friends_check_friendship_status( $u1, $u1 ),
+			$u2 => friends_check_friendship_status( $u1, $u2 ),
+			$u3 => friends_check_friendship_status( $u1, $u3 ),
+			$u4 => friends_check_friendship_status( $u1, $u4 ),
+			$u5 => friends_check_friendship_status( $u1, $u5 ),
+		);
+
+		$expected = array(
+			$u1 => 'not_friends',
+			$u2 => 'is_friend',
+			$u3 => 'pending',
+			$u4 => 'awaiting_response',
+			$u5 => 'not_friends',
+		);
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group friends_add_friend
+	 */
+	public function test_friends_add_friend_friends_friendship_requested() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		add_filter( 'friends_friendship_requested', array( $this, 'friends_friendship_filter_callback' ) );
+		$n = friends_add_friend( $u1, $u2, false );
+		remove_filter( 'friends_friendship_requested', array( $this, 'friends_friendship_filter_callback' ) );
+
+		$this->assertSame( 'friends_friendship_requested', $this->filter_fired );
+	}
+
+	/**
+	 * @group friends_add_friend
+	 */
+	public function test_friends_add_friend_friends_friendship_accepted() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		add_filter( 'friends_friendship_accepted', array( $this, 'friends_friendship_filter_callback' ) );
+		$n = friends_add_friend( $u1, $u2, true );
+		remove_filter( 'friends_friendship_accepted', array( $this, 'friends_friendship_filter_callback' ) );
+
+		$this->assertSame( 'friends_friendship_accepted', $this->filter_fired );
+	}
+
+	public function friends_friendship_filter_callback( $value ) {
+		$this->filter_fired = current_filter();
+		return $value;
+	}
+
+	/**
+	 * @group friends_remove_friend
+	 */
+	public function test_friends_check_is_friend_after_remove() {
+		$old_user = get_current_user_id();
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		/*
+		 * Pretend user 1 is on the Members Directory page.
+		 *
+		 * Also primes the friendship cache in the process.
+		 * @see bp_friends_filter_user_query_populate_extras()
+		 */
+		$this->set_current_user( $u1 );
+		$this->go_to( bp_get_members_directory_permalink() );
+		ob_start();
+		bp_get_template_part( 'members/members-loop' );
+		ob_end_clean();
+
+		// User 1 initiates friendship.
+		friends_add_friend( $u1, $u2 );
+
+		/*
+		 * Pretend user 2 is logged in and accepts friendship.
+		 *
+		 * User 2 needs to be logged in for friends_accept_friendship() to work
+		 * properly.
+		 */
+		$this->set_current_user( $u2 );
+		friends_accept_friendship( friends_get_friendship_id( $u1, $u2 ) );
+
+		// Afterwards, user 1 decides to cancel friendship.
+		$this->set_current_user( $u1 );
+		friends_remove_friend( $u1, $u2 );
+
+		// Assert that users are no longer friends.
+		$this->assertEquals( 'not_friends', BP_Friends_Friendship::check_is_friend( $u1, $u2 ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group friendship_caching
+	 */
+	public function test_friends_check_friendship_should_hit_friendship_object_cache() {
+		global $wpdb;
+
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+
+		friends_add_friend( $u1, $u2, true );
+
+		friends_check_friendship_status( $u1, $u2 );
+		$first_query_count = $wpdb->num_queries;
+
+		/*
+		 * This should access the previous friendship check's cached items.
+		 */
+		friends_check_friendship_status( $u2, $u1 );
+
+		$this->assertEquals( $first_query_count, $wpdb->num_queries );
+	}
+
+	public function test_friends_get_recently_active() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+		$u4 = self::$user_ids[3];
+
+		// request friendship
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, true );
+		friends_add_friend( $u1, $u4, true );
+
+		bp_update_user_last_activity( $u2, date( 'Y-m-d H:i:s', time() - ( 2 * DAY_IN_SECONDS ) ) );
+		bp_update_user_last_activity( $u3, date( 'Y-m-d H:i:s', time() - ( 5 * DAY_IN_SECONDS ) ) );
+		bp_update_user_last_activity( $u4, date( 'Y-m-d H:i:s', time() - ( 3 * DAY_IN_SECONDS ) ) );
+
+		$recent = friends_get_recently_active( $u1 );
+
+		$this->assertEquals( $recent['users'][0]->id, $u2 );
+		$this->assertEquals( $recent['users'][1]->id, $u4 );
+		$this->assertEquals( $recent['users'][2]->id, $u3 );
+	}
+
+	public function test_friends_get_alphabetically() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+
+		// request friendship
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, true );
+
+		$field_id = bp_xprofile_fullname_field_id();
+		xprofile_set_field_data( $field_id, $u2, 'Dave Lister' );
+		xprofile_set_field_data( $field_id, $u3, 'Arnold Rimmer' );
+		xprofile_sync_wp_profile( $u2 );
+		xprofile_sync_wp_profile( $u3 );
+
+		$alpha = friends_get_alphabetically( $u1 );
+
+		$this->assertEquals( $alpha['users'][0]->id, $u3 );
+		$this->assertEquals( $alpha['users'][1]->id, $u2 );
+	}
+
+	public function test_friends_get_newest() {
+		$u1 = self::$user_ids[0];
+		$u2 = self::$user_ids[1];
+		$u3 = self::$user_ids[2];
+		$u4 = self::$user_ids[3];
+
+		// request friendship
+		friends_add_friend( $u1, $u2, true );
+		friends_add_friend( $u1, $u3, true );
+		friends_add_friend( $u1, $u4, true );
+
+		bp_update_user_last_activity( $u2, date( 'Y-m-d H:i:s', time() - ( 2 * DAY_IN_SECONDS ) ) );
+		bp_update_user_last_activity( $u3, date( 'Y-m-d H:i:s', time() - ( 5 * DAY_IN_SECONDS ) ) );
+		bp_update_user_last_activity( $u4, date( 'Y-m-d H:i:s', time() - ( 3 * DAY_IN_SECONDS ) ) );
+
+		$newest = friends_get_newest( $u1 );
+
+		$this->assertEquals( $newest['users'][0]->id, $u4 );
+		$this->assertEquals( $newest['users'][1]->id, $u3 );
+		$this->assertEquals( $newest['users'][2]->id, $u2 );
+	}
+
+	/**
+	 * @ticket BP7821
+	 * @ticket BP7698
+	 */
+	public function test_bp_friends_personal_data_exporter() {
+		friends_add_friend( self::$user_ids[0], self::$user_ids[1], true );
+		friends_add_friend( self::$user_ids[0], self::$user_ids[2], false );
+		friends_add_friend( self::$user_ids[3], self::$user_ids[0], true );
+		friends_add_friend( self::$user_ids[4], self::$user_ids[0], false );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_friends_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 2, $actual['data'] );
+
+		$expected_ids = array(
+			'bp-friends-' . self::$user_ids[1],
+			'bp-friends-' . self::$user_ids[3],
+		);
+		$this->assertEqualSets( $expected_ids, wp_list_pluck( $actual['data'], 'item_id' ) );
+	}
+
+	/**
+	 * @ticket BP7821
+	 * @ticket BP7698
+	 */
+	public function test_bp_friends_pending_sent_requests_personal_data_exporter() {
+		friends_add_friend( self::$user_ids[0], self::$user_ids[1], true );
+		friends_add_friend( self::$user_ids[0], self::$user_ids[2], false );
+		friends_add_friend( self::$user_ids[3], self::$user_ids[0], true );
+		friends_add_friend( self::$user_ids[4], self::$user_ids[0], false );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_friends_pending_sent_requests_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 1, $actual['data'] );
+
+		$expected_ids = array(
+			'bp-friends-pending-sent-request-' . self::$user_ids[2],
+		);
+		$this->assertEqualSets( $expected_ids, wp_list_pluck( $actual['data'], 'item_id' ) );
+	}
+
+	/**
+	 * @ticket BP7821
+	 * @ticket BP7698
+	 */
+	public function test_bp_friends_pending_received_requests_personal_data_exporter() {
+		friends_add_friend( self::$user_ids[0], self::$user_ids[1], true );
+		friends_add_friend( self::$user_ids[0], self::$user_ids[2], false );
+		friends_add_friend( self::$user_ids[3], self::$user_ids[0], true );
+		friends_add_friend( self::$user_ids[4], self::$user_ids[0], false );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_friends_pending_received_requests_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 1, $actual['data'] );
+
+		$expected_ids = array(
+			'bp-friends-pending-received-request-' . self::$user_ids[4],
+		);
+		$this->assertEqualSets( $expected_ids, wp_list_pluck( $actual['data'], 'item_id' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/friends/notifications.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/friends/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/friends/notifications.php	(revision 12265)
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @group friends
+ */
+class BP_Tests_Friends_Notifications extends BP_UnitTestcase {
+	protected $filter_fired;
+	protected $current_user;
+	protected $friend;
+
+	public function setUp() {
+		parent::setUp();
+		$this->current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create() );
+
+		$this->friend = self::factory()->user->create();
+		$this->filter_fired = '';
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->current_user );
+	}
+
+	/**
+	 * @group friends_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_friends_multiple_friendship_accepted_notification_filter() {
+		add_filter( 'bp_friends_multiple_friendship_accepted_notification', array( $this, 'notification_filter_callback' ) );
+		$n = friends_format_notifications( 'friendship_accepted', $this->friend, 0, 5 );
+		remove_filter( 'bp_friends_multiple_friendship_accepted_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_friends_multiple_friendship_accepted_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group friends_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_friends_single_friendship_accepted_notification_filter() {
+		add_filter( 'bp_friends_single_friendship_accepted_notification', array( $this, 'notification_filter_callback' ) );
+		$n = friends_format_notifications( 'friendship_accepted', $this->friend, 0, 1 );
+		remove_filter( 'bp_friends_single_friendship_accepted_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_friends_single_friendship_accepted_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group friends_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_friends_multiple_friendship_request_notification_filter() {
+		add_filter( 'bp_friends_multiple_friendship_request_notification', array( $this, 'notification_filter_callback' ) );
+		$n = friends_format_notifications( 'friendship_request', $this->friend, 0, 5 );
+		remove_filter( 'bp_friends_multiple_friendship_request_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_friends_multiple_friendship_request_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group friends_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_friends_single_friendship_request_notification_filter() {
+		add_filter( 'bp_friends_single_friendship_request_notification', array( $this, 'notification_filter_callback' ) );
+		$n = friends_format_notifications( 'friendship_request', $this->friend, 0, 1 );
+		remove_filter( 'bp_friends_single_friendship_request_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_friends_single_friendship_request_notification', $this->filter_fired );
+	}
+
+	public function notification_filter_callback( $value ) {
+		$this->filter_fired = current_filter();
+		return $value;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/activity.php	(revision 12265)
@@ -0,0 +1,265 @@
+<?php
+
+/**
+ * @group groups
+ * @group activity
+ */
+class BP_Tests_Groups_Activity extends BP_UnitTestCase {
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_created_group
+	 */
+	public function test_bp_groups_format_activity_action_created_group() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'type' => 'created_group',
+			'user_id' => $u,
+			'item_id' => $g,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+		$g_obj = groups_get_group( $g );
+
+		$expected = sprintf( __( '%s created the group %s', 'buddypress' ), bp_core_get_userlink( $u ),  '<a href="' . bp_get_group_permalink( $g_obj ) . '">' . $g_obj->name . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_joined_group
+	 */
+	public function test_bp_groups_format_activity_action_joined_group() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'type' => 'joined_group',
+			'user_id' => $u,
+			'item_id' => $g,
+		) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+		$g_obj = groups_get_group( $g );
+
+		$expected = sprintf( __( '%s joined the group %s', 'buddypress' ), bp_core_get_userlink( $u ),  '<a href="' . bp_get_group_permalink( $g_obj ) . '">' . $g_obj->name . '</a>' );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_group_details_updated
+	 */
+	public function test_bp_groups_format_activity_action_group_details_updated_with_no_change() {
+		$group = self::factory()->group->create_and_get();
+		groups_edit_base_group_details( array(
+				'group_id'       => $group->id,
+				'name'           => $group->name,
+				'slug'           => $group->slug,
+				'description'    => $group->description,
+				'notify_members' => true,
+		) );
+
+		$a = bp_activity_get( array(
+			'component' => buddypress()->groups->id,
+			'action' => 'group_details_updated',
+			'item_id' => $group->id,
+		) );
+
+		$this->assertTrue( empty( $a['activities'] ) );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_group_details_updated
+	 */
+	public function test_bp_groups_format_activity_action_group_details_updated_with_notify_members_false() {
+		$group = self::factory()->group->create_and_get();
+		groups_edit_base_group_details( array(
+			'group_id'       => $group->id,
+			'name'           => 'Foo',
+			'slug'           => $group->slug,
+			'description'    => $group->description,
+			'notify_members' => false,
+		) );
+
+		$a = bp_activity_get( array(
+			'component' => buddypress()->groups->id,
+			'action' => 'group_details_updated',
+			'item_id' => $group->id,
+		) );
+
+		$this->assertTrue( empty( $a['activities'] ) );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_group_details_updated
+	 */
+	public function test_bp_groups_format_activity_action_group_details_updated_with_updated_name() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		$group = self::factory()->group->create_and_get();
+		groups_edit_base_group_details( array(
+			'group_id'       => $group->id,
+			'name'           => 'Foo',
+			'slug'           => $group->slug,
+			'description'    => $group->description,
+			'notify_members' => true,
+		) );
+
+		$a = bp_activity_get( array(
+			'component' => buddypress()->groups->id,
+			'action' => 'group_details_updated',
+			'item_id' => $group->id,
+		) );
+
+		$this->assertNotEmpty( $a['activities'] );
+
+		$expected = sprintf( __( '%s changed the name of the group %s from "%s" to "%s"', 'buddypress' ), bp_core_get_userlink( $u ),  '<a href="' . bp_get_group_permalink( $group ) . '">Foo</a>', $group->name, 'Foo' );
+		$this->assertSame( $expected, $a['activities'][0]->action );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_group_details_updated
+	 */
+	public function test_bp_groups_format_activity_action_group_details_updated_with_updated_description() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		$group = self::factory()->group->create_and_get();
+		groups_edit_base_group_details( array(
+			'group_id'       => $group->id,
+			'name'           => $group->name,
+			'slug'           => $group->slug,
+			'description'    => 'Bar',
+			'notify_members' => true,
+		) );
+
+		$a = bp_activity_get( array(
+			'component' => buddypress()->groups->id,
+			'action' => 'group_details_updated',
+			'item_id' => $group->id,
+		) );
+
+		$this->assertNotEmpty( $a['activities'] );
+
+		$expected = sprintf( __( '%s changed the description of the group %s from "%s" to "%s"', 'buddypress' ), bp_core_get_userlink( $u ),  '<a href="' . bp_get_group_permalink( $group ) . '">' . $group->name . '</a>', $group->description, 'Bar' );
+		$this->assertSame( $expected, $a['activities'][0]->action );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_group_details_updated
+	 */
+	public function test_bp_groups_format_activity_action_group_details_updated_with_updated_slug() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		$group = self::factory()->group->create_and_get();
+		groups_edit_base_group_details( array(
+			'group_id'       => $group->id,
+			'name'           => $group->name,
+			'slug'           => 'flaxen',
+			'description'    => $group->description,
+			'notify_members' => true,
+		) );
+		$new_group_details = groups_get_group( $group->id );
+
+		$a = bp_activity_get( array(
+			'component' => buddypress()->groups->id,
+			'action' => 'group_details_updated',
+			'item_id' => $group->id,
+		) );
+
+		$this->assertNotEmpty( $a['activities'] );
+
+		$expected = sprintf( __( '%s changed the permalink of the group %s.', 'buddypress' ), bp_core_get_userlink( $u ),  '<a href="' . bp_get_group_permalink( $new_group_details ) . '">' . $group->name . '</a>' );
+		$this->assertSame( $expected, $a['activities'][0]->action );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_groups_format_activity_action_group_details_updated
+	 */
+	public function test_bp_groups_format_activity_action_group_details_updated_with_updated_name_and_description() {
+		$old_user = get_current_user_id();
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		$group = self::factory()->group->create_and_get();
+		groups_edit_base_group_details( array(
+			'group_id'       => $group->id,
+			'name'           => 'Foo',
+			'slug'           => $group->slug,
+			'description'    => 'Bar',
+			'notify_members' => true,
+		) );
+
+		$a = bp_activity_get( array(
+			'component' => buddypress()->groups->id,
+			'action' => 'group_details_updated',
+			'item_id' => $group->id,
+		) );
+
+		$this->assertNotEmpty( $a['activities'] );
+
+		$expected = sprintf( __( '%s changed the name and description of the group %s', 'buddypress' ), bp_core_get_userlink( $u ),  '<a href="' . bp_get_group_permalink( $group ) . '">Foo</a>' );
+		$this->assertSame( $expected, $a['activities'][0]->action );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_activity_can_comment
+	 */
+	public function test_groups_activity_can_comment() {
+		$old_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$g = self::factory()->group->create();
+
+		// User 1 is a group member, while user 2 isn't.
+		groups_join_group( $g, $u1 );
+
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->groups->id,
+			'type' => 'created_group',
+			'user_id' => $u1,
+			'item_id' => $g,
+		) );
+
+		$this->set_current_user( $u1 );
+		if ( bp_has_activities( array( 'in' => $a ) ) ) {
+			while ( bp_activities() ) : bp_the_activity();
+				// User 1 should be able to comment.
+				$this->assertTrue( bp_activity_can_comment() );
+			endwhile;
+		}
+
+		$this->set_current_user( $u2 );
+		if ( bp_has_activities( array( 'in' => $a ) ) ) {
+			while ( bp_activities() ) : bp_the_activity();
+				// User 2 should not be able to comment.
+				$this->assertFalse( bp_activity_can_comment() );
+			endwhile;
+		}
+
+		$this->set_current_user( $old_user );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/avatars.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/avatars.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/avatars.php	(revision 12265)
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @group avatars
+ * @group groups
+ */
+class BP_Tests_Groups_Avatars extends BP_UnitTestCase {
+	/**
+	 * @group bp_get_group_has_avatar
+	 */
+	public function test_bp_get_group_has_avatar_no_avatar_uploaded() {
+		$g = self::factory()->group->create();
+		$this->assertFalse( bp_get_group_has_avatar( $g ) );
+	}
+
+	/**
+	 * @group bp_get_group_has_avatar
+	 */
+	public function test_bp_get_group_has_avatar_has_avatar_uploaded() {
+		$g = self::factory()->group->create();
+
+		// Fake it
+		add_filter( 'bp_core_fetch_avatar_url', array( $this, 'avatar_cb' ) );
+
+		$this->assertTrue( bp_get_group_has_avatar( $g ) );
+
+		remove_filter( 'bp_core_fetch_avatar_url', array( $this, 'avatar_cb' ) );
+	}
+
+	public function avatar_cb() {
+		return 'foo';
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/cache.php	(revision 12265)
@@ -0,0 +1,262 @@
+<?php
+
+/**
+ * @group groups
+ * @group cache
+ */
+class BP_Tests_Group_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_groups_update_meta_cache
+	 */
+	public function test_bp_groups_update_meta_cache() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+
+		$time = bp_core_current_time();
+
+		// Set up some data
+		groups_update_groupmeta( $g1, 'total_member_count', 4 );
+		groups_update_groupmeta( $g1, 'last_activity', $time );
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+
+		groups_update_groupmeta( $g2, 'total_member_count', 81 );
+		groups_update_groupmeta( $g2, 'last_activity', $time );
+		groups_update_groupmeta( $g2, 'foo', 'baz' );
+
+		// Prime the cache for $g1
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+		groups_get_groupmeta( $g1, 'foo' );
+
+		// Ensure an empty cache for $g2
+		wp_cache_delete( $g2, 'group_meta' );
+
+		bp_groups_update_meta_cache( array( $g1, $g2 ) );
+
+		$expected = array(
+			$g1 => array(
+				'total_member_count' => array(
+					4,
+				),
+				'last_activity' => array(
+					$time,
+				),
+				'foo' => array(
+					'bar',
+				),
+			),
+			$g2 => array(
+				'total_member_count' => array(
+					81,
+				),
+				'last_activity' => array(
+					$time,
+				),
+				'foo' => array(
+					'baz',
+				),
+			),
+		);
+
+		$found = array(
+			$g1 => wp_cache_get( $g1, 'group_meta' ),
+			$g2 => wp_cache_get( $g2, 'group_meta' ),
+		);
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	/**
+	 * @group groups_update_groupmeta
+	 * @group groups_delete_group_cache_on_metadata_change
+	 */
+	public function test_bp_groups_delete_group_cache_on_metadata_add() {
+		$g = self::factory()->group->create();
+
+		// Prime cache
+		groups_get_group( $g );
+
+		$this->assertNotEmpty( wp_cache_get( $g, 'bp_groups' ) );
+
+		// Trigger flush
+		groups_update_groupmeta( $g, 'foo', 'bar' );
+
+		$this->assertFalse( wp_cache_get( $g, 'bp_groups' ) );
+	}
+
+	/**
+	 * @group groups_update_groupmeta
+	 * @group groups_delete_group_cache_on_metadata_change
+	 */
+	public function test_bp_groups_delete_group_cache_on_metadata_change() {
+		$g = self::factory()->group->create();
+
+		// Prime cache
+		groups_update_groupmeta( $g, 'foo', 'bar' );
+		groups_get_group( $g );
+
+		$this->assertNotEmpty( wp_cache_get( $g, 'bp_groups' ) );
+
+		// Trigger flush
+		groups_update_groupmeta( $g, 'foo', 'baz' );
+		$this->assertFalse( wp_cache_get( $g, 'bp_groups' ) );
+	}
+
+	/**
+	 * @group bp_groups_prefetch_activity_object_data
+	 */
+	public function test_bp_groups_prefetch_activity_object_data_all_cached() {
+		$g = self::factory()->group->create();
+
+		// Prime cache
+		groups_get_group( $g );
+
+		// fake an activity
+		$a = new stdClass;
+		$a->component = buddypress()->groups->id;
+		$a->item_id = $g;
+		$activities = array(
+			$a,
+		);
+
+		bp_groups_prefetch_activity_object_data( $activities );
+
+		// This assertion is not really necessary - just checks to see
+		// whether a fatal error has occurred above
+		$this->assertNotEmpty( wp_cache_get( $g, 'bp_groups' ) );
+	}
+
+	/**
+	 * @group bp_groups_prefetch_activity_object_data
+	 */
+	public function test_bp_groups_prefetch_activity_object_data_some_cached() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+
+		// Prime cache
+		groups_get_group( $g1 );
+
+		// fake activities
+		$a1 = new stdClass;
+		$a1->component = buddypress()->groups->id;
+		$a1->item_id = $g1;
+
+		$a2 = new stdClass;
+		$a2->component = buddypress()->groups->id;
+		$a2->item_id = $g2;
+
+		$activities = array(
+			$a1,
+			$a2,
+		);
+
+		bp_groups_prefetch_activity_object_data( $activities );
+
+		$this->assertNotEmpty( wp_cache_get( $g1, 'bp_groups' ) );
+		$this->assertNotEmpty( wp_cache_get( $g2, 'bp_groups' ) );
+	}
+
+	/**
+	 * @group groups_get_group_admins
+	 */
+	public function test_groups_get_group_admins_cache() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// User 2 joins the group
+		groups_join_group( $g, $u2 );
+
+		// prime cache
+		groups_get_group_admins( $g );
+
+		// promote user 2 to an admin
+		bp_update_is_item_admin( true );
+		groups_promote_member( $u2, $g, 'admin' );
+
+		// assert that cache is invalidated
+		$this->assertEmpty( wp_cache_get( $g, 'bp_group_admins' ) );
+
+		// assert new cached value
+		$this->assertEquals( 2, count( groups_get_group_admins( $g ) ) );
+	}
+
+	/**
+	 * @group groups_get_group_mods
+	 */
+	public function test_groups_get_group_mods_cache() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// User 2 joins the group
+		groups_join_group( $g, $u2 );
+
+		// prime cache
+		groups_get_group_mods( $g );
+
+		// promote user 2 to an admin
+		bp_update_is_item_admin( true );
+		groups_promote_member( $u2, $g, 'mod' );
+
+		// assert new cached value
+		$this->assertEquals( 1, count( groups_get_group_mods( $g ) ) );
+	}
+
+	/**
+	 * @group groups_get_group_mods
+	 */
+	public function test_groups_get_group_mods_cache_on_member_save() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// prime cache
+		groups_get_group_mods( $g );
+
+		// promote user 2 to an admin via BP_Groups_Member::save()
+		self::add_user_to_group( $u2, $g, array( 'is_mod' => 1 ) );
+
+		// assert new cached value
+		$this->assertEquals( 1, count( groups_get_group_mods( $g ) ) );
+	}
+
+	/**
+	 * @group groups_get_group_admins
+	 */
+	public function test_groups_get_group_admins_cache_on_member_save() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// prime cache
+		groups_get_group_admins( $g );
+
+		// promote user 2 to an admin via BP_Groups_Member::save()
+		self::add_user_to_group( $u2, $g, array( 'is_admin' => 1 ) );
+
+		// assert that cache is invalidated
+		$this->assertEmpty( wp_cache_get( $g, 'bp_group_admins' ) );
+
+		// assert new cached value
+		$this->assertEquals( 2, count( groups_get_group_admins( $g ) ) );
+	}
+
+	/**
+	 * @group groups_get_total_group_count
+	 * @group counts
+	 */
+	public function test_groups_get_total_group_count_should_respect_cached_value_of_0() {
+		global $wpdb;
+
+		// prime cache
+		// no groups are created by default, so count is zero
+		groups_get_total_group_count();
+		$first_query_count = $wpdb->num_queries;
+
+		// run function again
+		groups_get_total_group_count();
+
+		// check if function references cache or hits the DB by comparing query count
+		$this->assertEquals( $first_query_count, $wpdb->num_queries );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-group-extension.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-group-extension.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-group-extension.php	(revision 12265)
@@ -0,0 +1,890 @@
+<?php
+
+include_once BP_TESTS_DIR . '/assets/group-extensions.php';
+
+/**
+ * @group groups
+ * @group BP_Group_Extension
+ */
+class BP_Tests_Group_Extension_TestCases extends BP_UnitTestCase {
+	public function test_parse_legacy_properties() {
+		$class_name = 'BPTest_Group_Extension_Parse_Legacy_Properties';
+		$class_slug = sanitize_title( $class_name );
+		$e = new $class_name();
+		$e->_register();
+
+		// Test most items separately so we can ignore irrelevant props
+		$l = $e->_get_legacy_properties_converted();
+		$this->assertEquals( $l['name'], $class_name );
+		$this->assertEquals( $l['slug'], $class_slug );
+		$this->assertEquals( $l['visibility'], 'private' );
+		$this->assertEquals( $l['nav_item_position'], 63 );
+		$this->assertEquals( $l['enable_nav_item'], true );
+		$this->assertEquals( $l['nav_item_name'], $class_name . ' Nav' );
+		$this->assertEquals( $l['display_hook'], 'foo_hook' );
+		$this->assertEquals( $l['template_file'], 'foo_template' );
+
+		// Build the screens array manually
+		$expected = array(
+			'create' => array(
+				'name' => $class_name . ' Create',
+				'slug' => $class_slug . '-create',
+				'position' => 58,
+				'enabled' => false,
+			),
+			'edit' => array(
+				'name' => $class_name . ' Edit',
+				'slug' => $class_slug . '-edit',
+				'enabled' => false,
+			),
+			'admin' => array(
+				'enabled' => true,
+				'metabox_context' => 'high',
+				'metabox_priority' => 'side',
+			),
+		);
+
+		$this->assertEquals( $expected, $l['screens'] );
+	}
+
+	public function test_setup_screens_use_global_fallbacks() {
+		$class_name = 'BPTest_Group_Extension_Setup_Screens_Use_Global_Fallbacks';
+		$e = new $class_name();
+		$e->_setup_class_info();
+		$screens = $e->_get_default_screens();
+
+		$fallback = array(
+			'screen_callback' => array( $e, 'settings_screen' ),
+			'screen_save_callback' => array( $e, 'settings_screen_save' ),
+		);
+		$fallbacks = array(
+			'create' => $fallback,
+			'edit' => $fallback,
+			'admin' => $fallback,
+		);
+
+		// strip everything from the screens array but what we we're
+		// testing
+		foreach ( $screens as &$screen ) {
+			foreach ( $screen as $k => $v ) {
+				if ( ! in_array( $k, array( 'screen_callback', 'screen_save_callback' ) ) ) {
+					unset( $screen[ $k ] );
+				}
+			}
+		}
+
+		$this->assertEquals( $fallbacks, $screens );
+	}
+
+	public function test_setup_screens_define_edit_screens_locally() {
+		$class_name = 'BPTest_Group_Extension_Setup_Screens_Define_Edit_Screens_Locally';
+		$e = new $class_name();
+		$e->_setup_class_info();
+		$screens = $e->_get_default_screens();
+
+		$fallback = array(
+			'screen_callback' => array( $e, 'settings_screen' ),
+			'screen_save_callback' => array( $e, 'settings_screen_save' ),
+		);
+		$expected = array(
+			'create' => $fallback,
+			'edit' => array(
+				'screen_callback' => array( $e, 'edit_screen' ),
+				'screen_save_callback' => array( $e, 'edit_screen_save' ),
+			),
+			'admin' => $fallback,
+		);
+
+		// strip everything from the screens array but what we we're
+		// testing
+		foreach ( $screens as &$screen ) {
+			foreach ( $screen as $k => $v ) {
+				if ( ! in_array( $k, array( 'screen_callback', 'screen_save_callback' ) ) ) {
+					unset( $screen[ $k ] );
+				}
+			}
+		}
+
+		$this->assertEquals( $screens, $expected );
+	}
+
+	public function test_parse_args_r() {
+		$a = array(
+			'veggies' => 'yes',
+			'ice_cream' => 'dope',
+			'fruit' => array(
+				'apple' => 'gross',
+				'berries' => array(
+					'blueberries' => array(
+						'in_season' => 'never',
+						'oh' => 'boy',
+					),
+					'cherries' => 'sometimes',
+				),
+			),
+		);
+
+		$b = array(
+			'veggies' => 'no',
+			'cheese' => array(
+				'cheddar' => 'good',
+			),
+			'fruit' => array(
+				'apple' => 'yum',
+				'berries' => array(
+					'strawberries' => 'awesome',
+					'blueberries' => array(
+						'in_season' => 'yes',
+						'out_of_season' => 'no',
+					),
+				),
+			),
+		);
+
+		$expected = array(
+			'veggies' => 'yes',
+			'ice_cream' => 'dope',
+			'cheese' => array(
+				'cheddar' => 'good',
+			),
+			'fruit' => array(
+				'apple' => 'gross',
+				'berries' => array(
+					'strawberries' => 'awesome',
+					'blueberries' => array(
+						'in_season' => 'never',
+						'out_of_season' => 'no',
+						'oh' => 'boy',
+					),
+					'cherries' => 'sometimes',
+				),
+			),
+		);
+
+		$this->assertEquals( $expected, BP_Group_Extension::parse_args_r( $a, $b ) );
+	}
+
+	/**
+	 * Config that gets intentionally stored as a direct property of object
+	 */
+	public function test_access_root_property() {
+		$class_name = 'BPTest_Group_Extension_Access_Root_Property';
+		$e = new $class_name();
+		$e->_register();
+
+		$this->assertEquals( 39, $e->nav_item_position );
+	}
+
+	/**
+	 * Config that gets registered using init(), but is then accessed via
+	 * the legacy location
+	 */
+	public function test_access_init_property_using_legacy_location() {
+		$class_name = 'BPTest_Group_Extension_Access_Init_Property_Using_Legacy_Location';
+		$e = new $class_name();
+		$e->_register();
+
+		$this->assertEquals( 18, $e->create_step_position );
+	}
+
+	/**
+	 * Provides settings_screen* and edit_screen*
+	 */
+	public function test_get_screen_callback_fallbacks() {
+		$class_name = 'BPTest_Group_Extension_Get_Screen_Callback_Fallbacks';
+		$e = new $class_name();
+		$e->_register();
+
+		$this->assertEquals( array( $e, 'settings_screen' ), $e->screens['create']['screen_callback'] );
+		$this->assertEquals( array( $e, 'settings_screen_save' ), $e->screens['create']['screen_save_callback'] );
+		$this->assertEquals( array( $e, 'settings_screen' ), $e->screens['admin']['screen_callback'] );
+		$this->assertEquals( array( $e, 'settings_screen_save' ), $e->screens['admin']['screen_save_callback'] );
+		$this->assertEquals( array( $e, 'edit_screen' ), $e->screens['edit']['screen_callback'] );
+		$this->assertEquals( array( $e, 'edit_screen_save' ), $e->screens['edit']['screen_save_callback'] );
+	}
+
+	public function test_has_submit_button() {
+		$a = '<p>Foo bar</p><input type="text" name="awesome" /><input name="save" type="submit" id="saverrrr" />sweet';
+		$this->assertTrue( BP_Group_Extension::has_submit_button( $a ) );
+
+		$b = '<p>Foo bar</p><input type="text" name="awesome" />sweet';
+		$this->assertFalse( BP_Group_Extension::has_submit_button( $b ) );
+
+		// switch the quotation marks
+		$c = "<p>Foo bar</p><input type='text' name='awesome' /><input name='save' type='submit' id='saverrrr' />sweet";
+		$this->assertTrue( BP_Group_Extension::has_submit_button( $c ) );
+	}
+
+	/**
+	 * @group enable_nav_item
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_enable_nav_item_true() {
+		$old_options_nav = buddypress()->bp_options_nav;
+
+		$g = self::factory()->group->create();
+		$g_obj = groups_get_group( $g );
+
+		$class_name = 'BPTest_Group_Extension_Enable_Nav_Item_True';
+		$e = new $class_name();
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e->_register();
+
+		$this->assertTrue( isset( buddypress()->bp_options_nav[ $g_obj->slug ][ $e->slug ] ) );
+
+		// Clean up
+		buddypress()->bp_options_nav = $old_options_nav;
+	}
+
+	/**
+	 * @group enable_nav_item
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_enable_nav_item_false() {
+		$old_options_nav = buddypress()->bp_options_nav;
+
+		$g = self::factory()->group->create();
+		$g_obj = groups_get_group( $g );
+
+		$class_name = 'BPTest_Group_Extension_Enable_Nav_Item_False';
+		$e = new $class_name();
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e->_register();
+
+		$this->assertFalse( isset( buddypress()->bp_options_nav[ $g_obj->slug ][ $e->slug ] ) );
+
+		// Clean up
+		buddypress()->bp_options_nav = $old_options_nav;
+	}
+
+	/**
+	 * @group visibility
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	public function test_visibility_private() {
+		$old_options_nav = buddypress()->bp_options_nav;
+		$old_current_user = get_current_user_id();
+
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$class_name = 'BPTest_Group_Extension_Visibility_Private';
+		$e = new $class_name();
+
+		// Test as non-logged-in user
+		$this->set_current_user( 0 );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+		$e->_register();
+		$this->assertFalse( isset( buddypress()->bp_options_nav[ $g_obj->slug ][ $e->slug ] ) );
+
+		// Clean up
+		buddypress()->bp_options_nav = $old_options_nav;
+
+		// Test as group member
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$this->add_user_to_group( $u, $g );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+		$e->_register();
+		$this->assertTrue( isset( buddypress()->bp_options_nav[ $g_obj->slug ][ $e->slug ] ) );
+
+		// Clean up
+		buddypress()->bp_options_nav = $old_options_nav;
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group visibility
+	 * @expectedIncorrectUsage bp_nav
+	 *
+	 * visibility=public + status=private results in adding the item to
+	 * the nav. However, BP_Groups_Component::setup_globals() bounces the
+	 * user away from this page on a regular pageload (BP 2.0 and under)
+	 *
+	 * @see https://buddypress.trac.wordpress.org/ticket/4785
+	 */
+	public function test_visibility_public() {
+		$old_options_nav = buddypress()->bp_options_nav;
+		$old_current_user = get_current_user_id();
+
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$class_name = 'BPTest_Group_Extension_Visibility_Public';
+		$e = new $class_name();
+
+		// Test as non-logged-in user
+		$this->set_current_user( 0 );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+		$e->_register();
+		$this->assertTrue( isset( buddypress()->bp_options_nav[ $g_obj->slug ][ $e->slug ] ) );
+
+		// Clean up
+		buddypress()->bp_options_nav = $old_options_nav;
+
+		// Test as group member
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+		$this->add_user_to_group( $u, $g );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+		$e->_register();
+		$this->assertTrue( isset( buddypress()->bp_options_nav[ $g_obj->slug ][ $e->slug ] ) );
+
+		// Clean up
+		buddypress()->bp_options_nav = $old_options_nav;
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_visit
+	 */
+	public function test_user_can_visit_inferred_from_enable_nav_item() {
+		$old_current_user = get_current_user_id();
+
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$this->set_current_user( 0 );
+
+		$e = new BPTest_Group_Extension_Inferred_Access_Settings_EnableNavItem_True();
+		$e->_register();
+		$this->assertTrue( $e->user_can_visit() );
+
+		$e2 = new BPTest_Group_Extension_Inferred_Access_Settings_EnableNavItem_False();
+		$e2->_register();
+		$this->assertFalse( $e2->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_visit
+	 */
+	public function test_user_can_visit_explicit_for_logged_out_user() {
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Access_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_visit() );
+
+		$e2 = new BPTest_Group_Extension_Access_Loggedin();
+		$e2->_register();
+		$this->assertFalse( $e2->user_can_visit() );
+
+		$e3 = new BPTest_Group_Extension_Access_Member();
+		$e3->_register();
+		$this->assertFalse( $e3->user_can_visit() );
+
+		$e4 = new BPTest_Group_Extension_Access_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_visit() );
+
+		$e5 = new BPTest_Group_Extension_Access_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_visit() );
+
+		$e6 = new BPTest_Group_Extension_Access_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_visit
+	 */
+	public function test_user_can_visit_explicit_for_logged_in_user() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Access_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_visit() );
+
+		$e2 = new BPTest_Group_Extension_Access_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_visit() );
+
+		$e3 = new BPTest_Group_Extension_Access_Member();
+		$e3->_register();
+		$this->assertFalse( $e3->user_can_visit() );
+
+		$e4 = new BPTest_Group_Extension_Access_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_visit() );
+
+		$e5 = new BPTest_Group_Extension_Access_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_visit() );
+
+		$e6 = new BPTest_Group_Extension_Access_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_visit
+	 */
+	public function test_user_can_visit_explicit_for_group_member() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->add_user_to_group( $u, $g );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Access_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_visit() );
+
+		$e2 = new BPTest_Group_Extension_Access_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_visit() );
+
+		$e3 = new BPTest_Group_Extension_Access_Member();
+		$e3->_register();
+		$this->assertTrue( $e3->user_can_visit() );
+
+		$e4 = new BPTest_Group_Extension_Access_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_visit() );
+
+		$e5 = new BPTest_Group_Extension_Access_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_visit() );
+
+		$e6 = new BPTest_Group_Extension_Access_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_visit
+	 */
+	public function test_user_can_visit_explicit_for_group_mod() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$m = $this->add_user_to_group( $u, $g );
+		$gm = new BP_Groups_Member( $u, $g );
+		$gm->promote( 'mod' );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Access_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_visit() );
+
+		$e2 = new BPTest_Group_Extension_Access_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_visit() );
+
+		$e3 = new BPTest_Group_Extension_Access_Member();
+		$e3->_register();
+		$this->assertTrue( $e3->user_can_visit() );
+
+		$e4 = new BPTest_Group_Extension_Access_AdminMod();
+		$e4->_register();
+		$this->assertTrue( $e4->user_can_visit() );
+
+		$e5 = new BPTest_Group_Extension_Access_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_visit() );
+
+		$e6 = new BPTest_Group_Extension_Access_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_visit
+	 */
+	public function test_user_can_visit_explicit_for_group_admin() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$m = $this->add_user_to_group( $u, $g );
+		$gm = new BP_Groups_Member( $u, $g );
+		$gm->promote( 'admin' );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Access_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_visit() );
+
+		$e2 = new BPTest_Group_Extension_Access_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_visit() );
+
+		$e3 = new BPTest_Group_Extension_Access_Member();
+		$e3->_register();
+		$this->assertTrue( $e3->user_can_visit() );
+
+		$e4 = new BPTest_Group_Extension_Access_AdminMod();
+		$e4->_register();
+		$this->assertTrue( $e4->user_can_visit() );
+
+		$e5 = new BPTest_Group_Extension_Access_Admin();
+		$e5->_register();
+		$this->assertTrue( $e5->user_can_visit() );
+
+		$e6 = new BPTest_Group_Extension_Access_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_see_nav_item
+	 */
+	public function test_user_can_see_nav_item_implied() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Access_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_see_nav_item() );
+
+		$e2 = new BPTest_Group_Extension_Access_Loggedin();
+		$e2->_register();
+		$this->assertFalse( $e2->user_can_see_nav_item() );
+
+		$e3 = new BPTest_Group_Extension_Access_Member();
+		$e3->_register();
+		$this->assertFalse( $e3->user_can_see_nav_item() );
+
+		$e4 = new BPTest_Group_Extension_Access_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_see_nav_item() );
+
+		$e5 = new BPTest_Group_Extension_Access_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_see_nav_item() );
+
+		$e6 = new BPTest_Group_Extension_Access_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_visit() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_see_nav_item
+	 */
+	public function test_user_can_see_nav_item_explicit_for_logged_out_user() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_ShowTab_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_see_nav_item() );
+
+		$e2 = new BPTest_Group_Extension_ShowTab_Loggedin();
+		$e2->_register();
+		$this->assertFalse( $e2->user_can_see_nav_item() );
+
+		$e3 = new BPTest_Group_Extension_ShowTab_Member();
+		$e3->_register();
+		$this->assertFalse( $e3->user_can_see_nav_item() );
+
+		$e4 = new BPTest_Group_Extension_ShowTab_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_see_nav_item() );
+
+		$e5 = new BPTest_Group_Extension_ShowTab_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_see_nav_item() );
+
+		$e6 = new BPTest_Group_Extension_ShowTab_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_see_nav_item() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_see_nav_item
+	 */
+	public function test_user_can_see_nav_item_explicit_for_logged_in_user() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_ShowTab_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_see_nav_item() );
+
+		$e2 = new BPTest_Group_Extension_ShowTab_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_see_nav_item() );
+
+		$e3 = new BPTest_Group_Extension_ShowTab_Member();
+		$e3->_register();
+		$this->assertFalse( $e3->user_can_see_nav_item() );
+
+		$e4 = new BPTest_Group_Extension_ShowTab_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_see_nav_item() );
+
+		$e5 = new BPTest_Group_Extension_ShowTab_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_see_nav_item() );
+
+		$e6 = new BPTest_Group_Extension_ShowTab_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_see_nav_item() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_see_nav_item
+	 */
+	public function test_user_can_see_nav_item_explicit_for_group_member() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->add_user_to_group( $u, $g );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_ShowTab_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_see_nav_item() );
+
+		$e2 = new BPTest_Group_Extension_ShowTab_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_see_nav_item() );
+
+		$e3 = new BPTest_Group_Extension_ShowTab_Member();
+		$e3->_register();
+		$this->assertTrue( $e3->user_can_see_nav_item() );
+
+		$e4 = new BPTest_Group_Extension_ShowTab_AdminMod();
+		$e4->_register();
+		$this->assertFalse( $e4->user_can_see_nav_item() );
+
+		$e5 = new BPTest_Group_Extension_ShowTab_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_see_nav_item() );
+
+		$e6 = new BPTest_Group_Extension_ShowTab_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_see_nav_item() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_see_nav_item
+	 */
+	public function test_user_can_see_nav_item_explicit_for_group_mod() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->add_user_to_group( $u, $g );
+		$gm = new BP_Groups_Member( $u, $g );
+		$gm->promote( 'mod' );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_ShowTab_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_see_nav_item() );
+
+		$e2 = new BPTest_Group_Extension_ShowTab_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_see_nav_item() );
+
+		$e3 = new BPTest_Group_Extension_ShowTab_Member();
+		$e3->_register();
+		$this->assertTrue( $e3->user_can_see_nav_item() );
+
+		$e4 = new BPTest_Group_Extension_ShowTab_AdminMod();
+		$e4->_register();
+		$this->assertTrue( $e4->user_can_see_nav_item() );
+
+		$e5 = new BPTest_Group_Extension_ShowTab_Admin();
+		$e5->_register();
+		$this->assertFalse( $e5->user_can_see_nav_item() );
+
+		$e6 = new BPTest_Group_Extension_ShowTab_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_see_nav_item() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group user_can_see_nav_item
+	 */
+	public function test_user_can_see_nav_item_explicit_for_group_admin() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$u = self::factory()->user->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->add_user_to_group( $u, $g );
+		$gm = new BP_Groups_Member( $u, $g );
+		$gm->promote( 'admin' );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_ShowTab_Anyone();
+		$e1->_register();
+		$this->assertTrue( $e1->user_can_see_nav_item() );
+
+		$e2 = new BPTest_Group_Extension_ShowTab_Loggedin();
+		$e2->_register();
+		$this->assertTrue( $e2->user_can_see_nav_item() );
+
+		$e3 = new BPTest_Group_Extension_ShowTab_Member();
+		$e3->_register();
+		$this->assertTrue( $e3->user_can_see_nav_item() );
+
+		$e4 = new BPTest_Group_Extension_ShowTab_AdminMod();
+		$e4->_register();
+		$this->assertTrue( $e4->user_can_see_nav_item() );
+
+		$e5 = new BPTest_Group_Extension_ShowTab_Admin();
+		$e5->_register();
+		$this->assertTrue( $e5->user_can_see_nav_item() );
+
+		$e6 = new BPTest_Group_Extension_ShowTab_Noone();
+		$e6->_register();
+		$this->assertFalse( $e6->user_can_see_nav_item() );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @ticket BP7131
+	 */
+	public function test_widget_on_group_home_page() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+
+		$e1 = new BPTest_Group_Extension_Widget_Method();
+		$e1->_register();
+
+		ob_start();
+		bp_custom_group_boxes();
+		$content = ob_get_clean();
+
+		$this->assertTrue( $content === 'Widget Displayed' );
+	}
+
+	/**
+	 * @ticket BP7131
+	 */
+	public function test_widget_on_group_members_page() {
+		$g = self::factory()->group->create( array(
+			'status' => 'public',
+		) );
+		$g_obj = groups_get_group( $g );
+
+		$this->go_to( trailingslashit( bp_get_group_permalink( $g_obj ) ) . 'members/' );
+
+		$e1 = new BPTest_Group_Extension_Widget_Method();
+		$e1->_register();
+
+		ob_start();
+		bp_custom_group_boxes();
+		$content = ob_get_clean();
+
+		$this->assertFalse( $content === 'Widget Displayed' );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-group-member-query.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-group-member-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-group-member-query.php	(revision 12265)
@@ -0,0 +1,762 @@
+<?php
+/**
+ * @group groups
+ * @group BP_Group_Member_Query
+ */
+class BP_Tests_BP_Group_Member_Query_TestCases extends BP_UnitTestCase {
+
+	/**
+	 * Make sure that a manual 'include' param is parsed correctly with
+	 * BP_Group_Member_Query's limiting of the query to group members
+	 */
+	public function test_with_include() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+
+		$query = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'include' => array( $u2 ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query->results ) );
+		$this->assertEquals( array( $u2, ), $ids );
+	}
+
+	// Make sure we're falling back on 'member'
+	public function test_with_group_role_null() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+		) );
+
+		$expected = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'member' ),
+		) );
+
+		$this->assertEquals( $expected->results, $query->results );
+	}
+
+	public function test_with_group_role_member() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'member' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u3, ), $ids );
+	}
+
+	public function test_with_group_role_mod() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'mod' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2, ), $ids );
+	}
+
+	public function test_with_group_role_admin() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'creator_id' => $u1
+		) );
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'admin' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, ), $ids );
+	}
+
+	public function test_with_group_role_member_mod() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'member', 'mod' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2, $u3, ), $ids );
+	}
+
+	public function test_with_group_role_member_admin() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'member', 'admin' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, $u3, ), $ids );
+	}
+
+	public function test_with_group_role_mod_admin() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'mod', 'admin' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, $u2, ), $ids );
+	}
+
+	public function test_with_group_role_member_mod_admin() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'member', 'mod', 'admin' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, $u2, $u3, ), $ids );
+	}
+
+	public function test_with_group_role_member_mod_admin_banned() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$u4 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+		$this->add_user_to_group( $u3, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+		$this->add_user_to_group( $u4, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+		$m3 = new BP_Groups_Member( $u3, $g );
+		$m3->ban();
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'member', 'mod', 'admin', 'banned' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, $u2, $u3, $u4, ), $ids );
+	}
+
+	/**
+	 * @group role
+	 */
+	public function test_with_group_role_banned() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+		$this->add_user_to_group( $u2, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ) ) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->ban();
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'banned' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, ), $ids );
+	}
+
+	public function test_group_has_no_members_of_role_mod() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ) ) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'group_role' => array( 'mod' ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array(), $ids );
+	}
+
+	public function test_confirmed_members() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'is_confirmed' => 0,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'is_confirmed' => 1,
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2 ), $ids );
+	}
+
+	/**
+	 * @group type
+	 */
+	public function test_get_with_type_last_joined() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 500 ),
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'last_joined',
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2, $u1 ), $ids );
+	}
+
+	/**
+	 * @group type
+	 */
+	public function test_get_with_type_first_joined() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 500 ),
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'first_joined',
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, $u2 ), $ids );
+	}
+
+	/**
+	 * @group type
+	 * @group group_activity
+	 */
+	public function test_get_with_type_group_activity_with_activity_component_disabled() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$c = buddypress()->groups->id;
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 500 ),
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 400 ),
+		) );
+
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u3,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 250 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u1,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 200 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u2,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+
+		// Deactivate activity component
+		$activity_active = isset( buddypress()->active_components['activity'] );
+		if ( $activity_active ) {
+			unset( buddypress()->active_components['activity'] );
+		}
+
+		$query_members1 = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'group_activity',
+		) );
+
+		$query_members2 = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'last_joined',
+		) );
+
+		if ( $activity_active ) {
+			buddypress()->active_components['activity'] = '1';
+		}
+
+		$this->assertSame( wp_list_pluck( $query_members2->results, 'ID' ), wp_list_pluck( $query_members1->results, 'ID' ) );
+	}
+
+	/**
+	 * @group type
+	 * @group group_activity
+	 */
+	public function test_get_with_type_group_activity() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$c = buddypress()->groups->id;
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 500 ),
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 400 ),
+		) );
+
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u3,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 250 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u1,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 200 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u2,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'group_activity',
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2, $u1, $u3 ), $ids );
+	}
+
+	/**
+	 * @group type
+	 * @group group_activity
+	 */
+	public function test_get_with_type_group_activity_no_dupes() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$c = buddypress()->groups->id;
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 500 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u1,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 250 ),
+		) );
+
+		self::factory()->activity->create( array(
+			'component' => $c,
+			'type' => 'activity_update',
+			'user_id' => $u1,
+			'item_id' => $g,
+			'recorded_time' => gmdate( 'Y-m-d H:i:s', $time - 200 ),
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'group_activity',
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, ), $ids );
+	}
+	/**
+	 * @group type
+	 */
+	public function test_get_with_type_alphabetical() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create( array(
+			'display_name' => 'AAA',
+		) );
+		$u2 = self::factory()->user->create( array(
+			'display_name' => 'CCC',
+		) );
+		$u3 = self::factory()->user->create( array(
+			'display_name' => 'BBB',
+		) );
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ),
+		) );
+
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ),
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'type' => 'alphabetical',
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1, $u3, $u2 ), $ids );
+	}
+
+	/**
+	 * @group invite_sent
+	 */
+	public function test_with_invite_sent_true() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'is_confirmed' => 0,
+			'invite_sent' => 0,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'is_confirmed' => 0,
+			'invite_sent' => 1,
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'is_confirmed' => false,
+			'invite_sent' => true,
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2 ), $ids );
+	}
+
+	/**
+	 * @group invite_sent
+	 */
+	public function test_with_invite_sent_false() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'is_confirmed' => 0,
+			'invite_sent' => 0,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'is_confirmed' => 0,
+			'invite_sent' => 1,
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'is_confirmed' => false,
+			'invite_sent' => false,
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1 ), $ids );
+	}
+
+	/**
+	 * @group inviter_id
+	 */
+	public function test_with_inviter_id_false() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'inviter_id' => 0,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'inviter_id' => 1,
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'inviter_id' => false,
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u1 ), $ids );
+	}
+
+	/**
+	 * @group inviter_id
+	 */
+	public function test_with_inviter_id_specific() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$u4 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'inviter_id' => 0,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ),
+			'inviter_id' => 1,
+		) );
+
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ),
+			'inviter_id' => 6,
+		) );
+
+		$this->add_user_to_group( $u4, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 400 ),
+			'inviter_id' => 2,
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'inviter_id' => array( 2, 6 ),
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u3, $u4 ), $ids );
+	}
+
+	/**
+	 * @group inviter_id
+	 */
+	public function test_with_inviter_id_any() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$u4 = self::factory()->user->create();
+		$time = time();
+
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+			'inviter_id' => 0,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 200 ),
+			'inviter_id' => 1,
+		) );
+
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 300 ),
+			'inviter_id' => 6,
+		) );
+
+		$this->add_user_to_group( $u4, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time - 400 ),
+			'inviter_id' => 2,
+		) );
+
+		$query_members = new BP_Group_Member_Query( array(
+			'group_id' => $g,
+			'inviter_id' => 'any',
+		) );
+
+		$ids = wp_parse_id_list( array_keys( $query_members->results ) );
+		$this->assertEquals( array( $u2, $u3, $u4 ), $ids );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-groups-group.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-groups-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-groups-group.php	(revision 12265)
@@ -0,0 +1,2287 @@
+<?php
+/**
+ * @group groups
+ * @group BP_Groups_Group
+ */
+class BP_Tests_BP_Groups_Group_TestCases extends BP_UnitTestCase {
+
+	/** __construct()  ***************************************************/
+
+	/**
+	 * @group __construct
+	 */
+	public function test_non_existent_group() {
+		$group = new BP_Groups_Group( 123456789 );
+		$this->assertSame( 0, $group->id );
+	}
+
+	/** get() ************************************************************/
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_exclude() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'exclude' => array(
+				$g1,
+				'foobar',
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g2 ) );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_include() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'include' => array(
+				$g1,
+				'foobar',
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g1 ) );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 */
+	public function test_get_with_meta_query() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g1 ) );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 */
+	public function test_get_empty_meta_query() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g1, $g2, ) );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 */
+	public function test_get_with_meta_query_multiple_clauses() {
+		$now = time();
+		$g1 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*2 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*3 ),
+		) );
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+		groups_update_groupmeta( $g2, 'foo', 'bar' );
+		groups_update_groupmeta( $g1, 'bar', 'barry' );
+
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				'relation' => 'AND',
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+				array(
+					'key' => 'bar',
+					'value' => 'barry',
+				),
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g1 ) );
+		$this->assertEquals( 1, $groups['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 */
+	public function test_get_with_meta_query_multiple_clauses_relation_or() {
+		$now = time();
+		$g1 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*2 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*3 ),
+		) );
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+		groups_update_groupmeta( $g2, 'foo', 'baz' );
+		groups_update_groupmeta( $g3, 'bar', 'barry' );
+
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				'relation' => 'OR',
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+				array(
+					'key' => 'bar',
+					'value' => 'barry',
+				),
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g3 ), $ids );
+		$this->assertEquals( 2, $groups['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 * @ticket BP5874
+	 */
+	public function test_get_with_meta_query_multiple_clauses_relation_or_shared_meta_key() {
+		$now = time();
+		$g1 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*2 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*3 ),
+		) );
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+		groups_update_groupmeta( $g2, 'foo', 'baz' );
+		groups_update_groupmeta( $g3, 'foo', 'barry' );
+
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				'relation' => 'OR',
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+				array(
+					'key' => 'foo',
+					'value' => 'baz',
+				),
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g2 ), $ids );
+		$this->assertEquals( 2, $groups['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 * @ticket BP5824
+	 */
+	public function test_get_with_meta_query_multiple_keys_with_same_value() {
+		$now = time();
+		$g1 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*2 ),
+		) );
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+		groups_update_groupmeta( $g2, 'foo2', 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+					'compare' => '=',
+				),
+			),
+		) );
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g1 ) );
+		$this->assertEquals( 1, $groups['total'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_normal_search() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => 'This is one cool group',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => 'Cool',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_underscores() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => '_cool_ dude',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => '_cool_',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_percent_sign() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => '100% awesome',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => '100%',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_quotes() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => "'tis sweet",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => "'tis ",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_left_wildcard() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => "*ads",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_left_wildcard_should_miss() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => "*la",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array(), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_right_wildcard() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => "Ye*",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_right_wildcard_should_miss() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => "la*",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array(), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_with_both_wildcard() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms' => "*la*",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_limited_to_name_column() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create( array(
+			'name' => 'Bonnie Lasses',
+			'description' => "That lad is unknown to me",
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms'   => "lad",
+			'search_columns' => array( 'name' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_search_limited_to_description_column() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Ye Lads',
+			'description' => "My Bonnie lies over the ocean",
+		) );
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create( array(
+			'name' => 'Bonnie Lasses',
+			'description' => "That lad is unknown to me",
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'search_terms'   => "lad",
+			'search_columns' => array( 'description' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g3 ), $found );
+	}
+
+	/**
+	 * BP 1.8 will change the default 'type' param in favor of default
+	 * 'order' and 'orderby'. This is to make sure that existing plugins
+	 * will work appropriately
+	 *
+	 * @group get
+	 */
+	public function test_get_with_default_type_value_should_be_newest() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'A Group',
+			'date_created' => bp_core_current_time(),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'D Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', time() - 100 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'B Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', time() - 100000 ),
+		) );
+		$g4 = self::factory()->group->create( array(
+			'name' => 'C Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', time() - 1000 ),
+		) );
+
+		$found = BP_Groups_Group::get();
+
+		$this->assertEquals( BP_Groups_Group::get( array( 'type' => 'newest' ) ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_type_newest() {
+		$time = time();
+		$g1 = self::factory()->group->create( array(
+			'name' => 'A Group',
+			'date_created' => bp_core_current_time(),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'D Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'B Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100000 ),
+		) );
+		$g4 = self::factory()->group->create( array(
+			'name' => 'C Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 1000 ),
+		) );
+
+		$groups = BP_Groups_Group::get( array( 'type' => 'newest' ) );
+		$found = wp_parse_id_list( wp_list_pluck( $groups['groups'], 'id' ) );
+		$this->assertEquals( array( $g1, $g2, $g4, $g3 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_type_popular() {
+		$time = time();
+		$g1 = self::factory()->group->create( array(
+			'name' => 'A Group',
+			'date_created' => bp_core_current_time(),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'D Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'B Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100000 ),
+		) );
+		$g4 = self::factory()->group->create( array(
+			'name' => 'C Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 1000 ),
+		) );
+
+		groups_update_groupmeta( $g1, 'total_member_count', 1 );
+		groups_update_groupmeta( $g2, 'total_member_count', 4 );
+		groups_update_groupmeta( $g3, 'total_member_count', 2 );
+		groups_update_groupmeta( $g4, 'total_member_count', 3 );
+
+		$groups = BP_Groups_Group::get( array( 'type' => 'popular' ) );
+		$found = wp_parse_id_list( wp_list_pluck( $groups['groups'], 'id' ) );
+		$this->assertEquals( array( $g2, $g4, $g3, $g1 ), $found );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_type_alphabetical() {
+		$time = time();
+		$g1 = self::factory()->group->create( array(
+			'name' => 'A Group',
+			'date_created' => bp_core_current_time(),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'D Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'B Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100000 ),
+		) );
+		$g4 = self::factory()->group->create( array(
+			'name' => 'C Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 1000 ),
+		) );
+
+		$groups = BP_Groups_Group::get( array( 'type' => 'alphabetical' ) );
+		$found = wp_parse_id_list( wp_list_pluck( $groups['groups'], 'id' ) );
+		$this->assertEquals( array( $g1, $g3, $g4, $g2 ), $found );
+	}
+
+	/**
+	 * @group get
+	 * @group group_meta_query
+	 * @ticket BP5099
+	 */
+	public function test_meta_query_and_total_groups() {
+		$time = time();
+
+		$g1 = self::factory()->group->create( array(
+			'name' => 'A Group',
+			'date_created' => bp_core_current_time(),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'D Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'B Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100000 ),
+		) );
+		$g4 = self::factory()->group->create( array(
+			'name' => 'C Group',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 1000 ),
+		) );
+
+		// mark one group with the metakey 'supergroup'
+		groups_update_groupmeta( $g1, 'supergroup', 1 );
+
+		// fetch groups with our 'supergroup' metakey
+		$groups = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				array(
+					'key'     => 'supergroup',
+					'compare' => 'EXISTS',
+				)
+			)
+		) );
+
+		// group total should match 1
+		$this->assertEquals( '1', $groups['total'] );
+	}
+
+	/**
+	 * @group get
+	 * @ticket BP5477
+	 */
+	public function test_get_groups_page_perpage_params() {
+		// Create more than 20 groups (20 is the default per_page number)
+		$group_ids = array();
+
+		for ( $i = 1; $i <= 25; $i++ ) {
+			$group_ids[] = self::factory()->group->create();
+		}
+
+		// Tests
+		// Passing false to 'per_page' and 'page' should result in pagination not being applied
+		$groups = BP_Groups_Group::get( array(
+			'per_page' => false,
+			'page'     => false
+		) );
+
+		// Should return all groups; "paged" group total should be 25
+		$this->assertEquals( count( $group_ids ), count( $groups['groups'] ) );
+
+		unset( $groups );
+
+		// Passing 'per_page' => -1 should result in pagination not being applied.
+		$groups = BP_Groups_Group::get( array(
+			'per_page' => -1
+		) );
+
+		// Should return all groups; "paged" group total should match 25
+		$this->assertEquals( count( $group_ids ), count( $groups['groups'] ) );
+
+		unset( $groups );
+
+		// If "per_page" and "page" are both set, should result in pagination being applied.
+		$groups = BP_Groups_Group::get( array(
+			'per_page' => 12,
+			'page'     => 1
+		) );
+
+		// Should return top 12 groups only
+		$this->assertEquals( '12', count( $groups['groups'] ) );
+	}
+
+	/**
+	 * @group cache
+	 * @ticket BP5451
+	 * @ticket BP6643
+	 */
+	public function test_get_queries_should_be_cached() {
+		global $wpdb;
+
+		$g = self::factory()->group->create();
+
+		$found1 = BP_Groups_Group::get();
+
+		$num_queries = $wpdb->num_queries;
+
+		$found2 = BP_Groups_Group::get();
+
+		$this->assertEqualSets( $found1, $found2 );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @group cache
+	 * @ticket BP5451
+	 * @ticket BP6643
+	 */
+	public function test_get_query_caches_should_be_busted_by_groupmeta_update() {
+		global $wpdb;
+
+		$groups = self::factory()->group->create_many( 2 );
+		groups_update_groupmeta( $groups[0], 'foo', 'bar' );
+		groups_update_groupmeta( $groups[1], 'foo', 'bar' );
+
+		$found1 = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( $groups[0], $groups[1] ), wp_list_pluck( $found1['groups'], 'id' ) );
+
+		groups_update_groupmeta( $groups[1], 'foo', 'baz' );
+
+		$found2 = BP_Groups_Group::get( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( $groups[0] ), wp_list_pluck( $found2['groups'], 'id' ) );
+	}
+
+	/**
+	 * @group cache
+	 * @group group_types
+	 * @ticket BP5451
+	 * @ticket BP6643
+	 */
+	public function test_get_query_caches_should_be_busted_by_group_term_change() {
+		global $wpdb;
+
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+
+		$groups = self::factory()->group->create_many( 2 );
+		bp_groups_set_group_type( $groups[0], 'foo' );
+		bp_groups_set_group_type( $groups[1], 'bar' );
+
+		$found1 = BP_Groups_Group::get( array(
+			'group_type' => 'foo',
+		) );
+
+		$this->assertEqualSets( array( $groups[0] ), wp_list_pluck( $found1['groups'], 'id' ) );
+
+		bp_groups_set_group_type( $groups[1], 'foo' );
+
+		$found2 = BP_Groups_Group::get( array(
+			'group_type' => 'foo',
+		) );
+
+		$this->assertEqualSets( array( $groups[0], $groups[1] ), wp_list_pluck( $found2['groups'], 'id' ) );
+	}
+
+	/**
+	 * @group cache
+	 * @group group_types
+	 * @ticket BP5451
+	 * @ticket BP6643
+	 */
+	public function test_get_query_caches_should_be_busted_by_group_term_removal() {
+		global $wpdb;
+
+		bp_groups_register_group_type( 'foo' );
+
+		$groups = self::factory()->group->create_many( 2 );
+		bp_groups_set_group_type( $groups[0], 'foo' );
+		bp_groups_set_group_type( $groups[1], 'foo' );
+
+		$found1 = BP_Groups_Group::get( array(
+			'group_type' => 'foo',
+		) );
+
+		$this->assertEqualSets( array( $groups[0], $groups[1] ), wp_list_pluck( $found1['groups'], 'id' ) );
+
+		bp_groups_remove_group_type( $groups[1], 'foo' );
+
+		$found2 = BP_Groups_Group::get( array(
+			'group_type' => 'foo',
+		) );
+
+		$this->assertEqualSets( array( $groups[0] ), wp_list_pluck( $found2['groups'], 'id' ) );
+	}
+
+	/**
+	 * @group cache
+	 * @ticket BP5451
+	 * @ticket BP6643
+	 */
+	public function test_get_query_caches_should_be_busted_by_group_save() {
+		global $wpdb;
+
+		$groups = self::factory()->group->create_many( 2 );
+		groups_update_groupmeta( $groups[0], 'foo', 'bar' );
+		groups_update_groupmeta( $groups[1], 'foo', 'bar' );
+
+		$found1 = BP_Groups_Group::get( array(
+			'search_terms' => 'Foo',
+		) );
+
+		$this->assertEmpty( $found1['groups'] );
+
+		$group0 = groups_get_group( $groups[0] );
+		$group0->name = 'Foo';
+		$group0->save();
+
+		$found2 = BP_Groups_Group::get( array(
+			'search_terms' => 'Foo',
+		) );
+
+		$this->assertEqualSets( array( $groups[0] ), wp_list_pluck( $found2['groups'], 'id' ) );
+	}
+
+	/**
+	 * @group cache
+	 * @ticket BP5451
+	 * @ticket BP6643
+	 */
+	public function test_get_query_caches_should_be_busted_by_group_delete() {
+		global $wpdb;
+
+		$groups = self::factory()->group->create_many( 2 );
+
+		$found1 = BP_Groups_Group::get();
+
+		$this->assertEqualSets( $groups, wp_list_pluck( $found1['groups'], 'id' ) );
+
+		$group0 = groups_get_group( $groups[0] );
+		$group0->delete();
+
+		$found2 = BP_Groups_Group::get();
+
+		$this->assertEqualSets( array( $groups[1] ), wp_list_pluck( $found2['groups'], 'id' ) );
+	}
+
+	/**
+	 * @ticket BP5451
+	 */
+	public function test_bp_groups_group_magic_isset_with_empty_check() {
+		$this->old_current_user = get_current_user_id();
+
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u ) );
+
+		// Instantiate group object.
+		$this->set_current_user( $u );
+		$group = new BP_Groups_Group( $g );
+
+		// Assert ! empty() check is not false.
+		$this->assertTrue( ! empty( $group->is_member ) );
+
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	/** convert_type_to_order_orderby() **********************************/
+
+	/**
+	 * @group convert_type_to_order_orderby
+	 */
+	public function test_convert_type_to_order_orderby_newest() {
+		$expected = array(
+			'order' => 'DESC',
+			'orderby' => 'date_created',
+		);
+		$this->assertEquals( $expected, _BP_Groups_Group::_convert_type_to_order_orderby( 'newest' ) );
+	}
+
+	/**
+	 * @group convert_type_to_order_orderby
+	 */
+	public function test_convert_type_to_order_orderby_active() {
+		$expected = array(
+			'order' => 'DESC',
+			'orderby' => 'last_activity',
+		);
+		$this->assertEquals( $expected, _BP_Groups_Group::_convert_type_to_order_orderby( 'active' ) );
+	}
+
+	/**
+	 * @group convert_type_to_order_orderby
+	 */
+	public function test_convert_type_to_order_orderby_popular() {
+		$expected = array(
+			'order' => 'DESC',
+			'orderby' => 'total_member_count',
+		);
+		$this->assertEquals( $expected, _BP_Groups_Group::_convert_type_to_order_orderby( 'popular' ) );
+	}
+
+	/**
+	 * @group convert_type_to_order_orderby
+	 */
+	public function test_convert_type_to_order_orderby_alphabetical() {
+		$expected = array(
+			'order' => 'ASC',
+			'orderby' => 'name',
+		);
+		$this->assertEquals( $expected, _BP_Groups_Group::_convert_type_to_order_orderby( 'alphabetical' ) );
+	}
+
+	/**
+	 * @group convert_type_to_order_orderby
+	 */
+	public function test_convert_type_to_order_orderby_random() {
+		$expected = array(
+			// order gets thrown out
+			'order' => '',
+			'orderby' => 'random',
+		);
+		$this->assertEquals( $expected, _BP_Groups_Group::_convert_type_to_order_orderby( 'random' ) );
+	}
+
+	/**
+	 * @group convert_type_to_order_orderby
+	 */
+	public function test_convert_type_to_order_orderby_invalid() {
+		$expected = array(
+			'order' => '',
+			'orderby' => '',
+		);
+		$this->assertEquals( $expected, _BP_Groups_Group::_convert_type_to_order_orderby( 'foooooooooooooooobar' ) );
+	}
+
+	/** convert_orderby_to_order_by_term() **********************************/
+
+	/**
+	 * @group convert_orderby_to_order_by_term
+	 */
+	public function test_convert_orderby_to_order_by_term_date_created() {
+		$this->assertEquals( 'g.date_created', _BP_Groups_Group::_convert_orderby_to_order_by_term( 'date_created' ) );
+	}
+
+	/**
+	 * @group convert_orderby_to_order_by_term
+	 */
+	public function test_convert_orderby_to_order_by_term_last_activity() {
+		$c = new _BP_Groups_Group();
+		$this->assertEquals( 'gm_last_activity.meta_value', _BP_Groups_Group::_convert_orderby_to_order_by_term( 'last_activity' ) );
+	}
+
+	/**
+	 * @group convert_orderby_to_order_by_term
+	 */
+	public function test_convert_orderby_to_order_by_term_total_member_count() {
+		$c = new _BP_Groups_Group();
+		$this->assertEquals( 'CONVERT(gm_total_member_count.meta_value, SIGNED)', _BP_Groups_Group::_convert_orderby_to_order_by_term( 'total_member_count' ) );
+	}
+
+	/**
+	 * @group convert_orderby_to_order_by_term
+	 */
+	public function test_convert_orderby_to_order_by_term_name() {
+		$c = new _BP_Groups_Group();
+		$this->assertEquals( 'g.name', _BP_Groups_Group::_convert_orderby_to_order_by_term( 'name' ) );
+	}
+
+	/**
+	 * @group convert_orderby_to_order_by_term
+	 */
+	public function test_convert_orderby_to_order_by_term_random() {
+		$c = new _BP_Groups_Group();
+		$this->assertEquals( 'rand()', _BP_Groups_Group::_convert_orderby_to_order_by_term( 'random' ) );
+	}
+
+	/**
+	 * @group convert_orderby_to_order_by_term
+	 */
+	public function test_convert_orderby_to_order_by_term_invalid_fallback_to_date_created() {
+		$c = new _BP_Groups_Group();
+		$this->assertEquals( _BP_Groups_Group::_convert_orderby_to_order_by_term( 'date_created' ), _BP_Groups_Group::_convert_orderby_to_order_by_term( 'I am a bad boy' ) );
+	}
+
+	/**
+	 * @group groups_get_orderby_meta_id
+	 */
+	public function test_get_orderby_meta_id() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create();
+
+		groups_update_groupmeta( $g2, 'orderup', 'sammy' );
+		groups_update_groupmeta( $g1, 'orderup', 'sammy' );
+
+		$args = array(
+			'meta_query'         => array(
+				array(
+					'key'   => 'orderup',
+					'value' => 'sammy'
+				),
+			),
+			'orderby'           => 'meta_id',
+			'order'             => 'ASC',
+		);
+		$groups = BP_Groups_Group::get( $args );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g2, $g1 ), $found );
+	}
+
+	/**
+	 * @group groups_get_orderby_meta_id
+	 */
+	public function test_get_orderby_meta_id_invalid_fallback_to_date_created() {
+		$time = time();
+		$g1 = self::factory()->group->create( array(
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 10000 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 1000 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'date_created' => gmdate( 'Y-m-d H:i:s', $time - 100 ),
+		) );
+
+		$args = array(
+			'orderby' => 'meta_id',
+		);
+		$groups = BP_Groups_Group::get( $args );
+
+		// Orderby meta_id should be ignored if no meta query is present.
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g3, $g2, $g1 ), $found );
+	}
+
+	public function test_filter_user_groups_normal_search() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => 'This is one cool group',
+		) );
+		$g2 = self::factory()->group->create();
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+
+		$groups = BP_Groups_Group::filter_user_groups( 'Cool', $u );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_filter_user_groups_normal_search_middle_of_string() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => 'This group is for mandocellos and oboes.',
+		) );
+		$g2 = self::factory()->group->create();
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+
+		$groups = BP_Groups_Group::filter_user_groups( 'cello', $u );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_filter_user_groups_search_with_underscores() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => '_cool_ dude',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$groups = BP_Groups_Group::filter_user_groups( '_cool_', $u );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_filter_user_groups_search_with_percent_sign() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => '100% awesome',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$groups = BP_Groups_Group::filter_user_groups( '100%', $u );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_filter_user_groups_search_with_quotes() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => "'tis sweet",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$groups = BP_Groups_Group::filter_user_groups( "'tis ", $u );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+
+		// @todo
+		//$this->assertEquals( array( $g1->id ), $found );
+	}
+
+	public function test_search_groups_normal_search() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => 'This is one cool group',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::search_groups( 'Cool' );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_search_groups_search_with_underscores() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => '_cool_ dude',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::search_groups( '_cool_' );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_search_groups_search_with_percent_sign() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => '100% awesome',
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::search_groups( '100%' );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_search_groups_search_with_quotes() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Cool Group',
+			'description' => "'tis sweet",
+		) );
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::search_groups( "'tis " );
+
+		$found = wp_list_pluck( $groups['groups'], 'group_id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	public function test_get_by_letter_typical_use() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Awesome Cool Group',
+			'description' => 'Neat',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Babylon Kong',
+			'description' => 'Awesome',
+		) );
+
+		$groups = BP_Groups_Group::get_by_letter( 'A' );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+
+	}
+
+	public function test_get_by_letter_with_exclude() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Awesome Cool Group',
+			'description' => 'Neat',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Another Cool Group',
+			'description' => 'Awesome',
+		) );
+
+		$groups = BP_Groups_Group::get_by_letter( 'A', null, null, true, array( $g1, 'stringthatshouldberemoved' ) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g2 ), $found );
+
+	}
+
+	public function test_get_by_letter_starts_with_apostrophe() {
+		$g1 = self::factory()->group->create( array(
+			'name' => "'Tis Sweet",
+			'description' => 'Neat',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Another Cool Group',
+			'description' => 'Awesome',
+		) );
+
+		$groups = BP_Groups_Group::get_by_letter( "'" );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		// @todo
+		// The test fails but at least it's sanitized
+		//$this->assertEquals( array( $g1->id ), $found );
+	}
+
+	public function test_get_random_with_exclude() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+
+		// There are only two groups, so excluding one should give us the other
+		$groups = BP_Groups_Group::get_random( null, null, 0, false, true, array( $g1, 'ignore this' ) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g2 ), $found );
+	}
+
+	public function test_get_random_with_search_terms() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Bodacious',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Crummy group',
+		) );
+
+		// Only one group will match, so the random part doesn't matter
+		$groups = BP_Groups_Group::get_random( null, null, 0, 'daci' );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group delete
+	 * @group cache
+	 */
+	public function test_delete_clear_cache() {
+		$g = self::factory()->group->create();
+
+		// Prime cache
+		groups_get_group( $g );
+
+		$this->assertNotEmpty( wp_cache_get( $g, 'bp_groups' ) );
+
+		$group = new BP_Groups_Group( $g );
+		$group->delete();
+
+		$this->assertFalse( wp_cache_get( $g, 'bp_groups' ) );
+	}
+
+	/**
+	 * @group save
+	 * @group cache
+	 */
+	public function test_save_clear_cache() {
+		$g = self::factory()->group->create();
+
+		// Prime cache
+		groups_get_group( $g );
+
+		$this->assertNotEmpty( wp_cache_get( $g, 'bp_groups' ) );
+
+		$group = new BP_Groups_Group( $g );
+		$group->name = 'Foo';
+		$group->save();
+
+		$this->assertFalse( wp_cache_get( $g, 'bp_groups' ) );
+	}
+	/**
+	 * @group get_group_extras
+	 */
+	public function test_get_group_extras_non_logged_in() {
+		$paged_groups = array();
+		$paged_groups[] = new stdClass;
+		$paged_groups[] = new stdClass;
+
+		$paged_groups[0]->id = 5;
+		$paged_groups[1]->id = 10;
+
+		$group_ids = array( 5, 10 );
+
+		$expected = array();
+		foreach ( $paged_groups as $key => $value ) {
+			$expected[ $key ] = new stdClass;
+			$expected[ $key ]->id = $value->id;
+			$expected[ $key ]->is_member = '0';
+			$expected[ $key ]->is_invited = '0';
+			$expected[ $key ]->is_pending = '0';
+			$expected[ $key ]->is_banned = false;
+		}
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertEquals( $expected, BP_Groups_Group::get_group_extras( $paged_groups, $group_ids ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group get_group_extras
+	 */
+	public function test_get_group_extras_non_member() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+
+		$paged_groups = array();
+		$paged_groups[] = new stdClass;
+		$paged_groups[0]->id = $g;
+
+		$group_ids = array( $g );
+
+		$expected = array();
+		foreach ( $paged_groups as $key => $value ) {
+			$expected[ $key ] = new stdClass;
+			$expected[ $key ]->id = $value->id;
+			$expected[ $key ]->is_member = '0';
+			$expected[ $key ]->is_invited = '0';
+			$expected[ $key ]->is_pending = '0';
+			$expected[ $key ]->is_banned = false;
+		}
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->assertEquals( $expected, BP_Groups_Group::get_group_extras( $paged_groups, $group_ids ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group get_group_extras
+	 */
+	public function test_get_group_extras_member() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$this->add_user_to_group( $u, $g );
+
+		$paged_groups = array();
+		$paged_groups[] = new stdClass;
+		$paged_groups[0]->id = $g;
+
+		$group_ids = array( $g );
+
+		$expected = array();
+		foreach ( $paged_groups as $key => $value ) {
+			$expected[ $key ] = new stdClass;
+			$expected[ $key ]->id = $value->id;
+			$expected[ $key ]->is_member = '1';
+			$expected[ $key ]->is_invited = '0';
+			$expected[ $key ]->is_pending = '0';
+			$expected[ $key ]->is_banned = false;
+		}
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->assertEquals( $expected, BP_Groups_Group::get_group_extras( $paged_groups, $group_ids ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group get_group_extras
+	 */
+	public function test_get_group_extras_invited() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+
+		$invite                = new BP_Groups_Member;
+		$invite->group_id      = $g;
+		$invite->user_id       = $u;
+		$invite->date_modified = bp_core_current_time();
+		$invite->invite_sent   = true;
+		$invite->is_confirmed  = false;
+		$invite->save();
+
+		$paged_groups = array();
+		$paged_groups[] = new stdClass;
+		$paged_groups[0]->id = $g;
+
+		$group_ids = array( $g );
+
+		$expected = array();
+		foreach ( $paged_groups as $key => $value ) {
+			$expected[ $key ] = new stdClass;
+			$expected[ $key ]->id = $value->id;
+			$expected[ $key ]->is_member = '0';
+			$expected[ $key ]->is_invited = '1';
+			$expected[ $key ]->is_pending = '0';
+			$expected[ $key ]->is_banned = false;
+		}
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->assertEquals( $expected, BP_Groups_Group::get_group_extras( $paged_groups, $group_ids ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group get_group_extras
+	 */
+	public function test_get_group_extras_pending() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+
+		$invite                = new BP_Groups_Member;
+		$invite->group_id      = $g;
+		$invite->user_id       = $u;
+		$invite->date_modified = bp_core_current_time();
+		$invite->invite_sent   = false;
+		$invite->is_confirmed  = false;
+		$invite->save();
+
+		$paged_groups = array();
+		$paged_groups[] = new stdClass;
+		$paged_groups[0]->id = $g;
+
+		$group_ids = array( $g );
+
+		$expected = array();
+		foreach ( $paged_groups as $key => $value ) {
+			$expected[ $key ] = new stdClass;
+			$expected[ $key ]->id = $value->id;
+			$expected[ $key ]->is_member = '0';
+			$expected[ $key ]->is_invited = '0';
+			$expected[ $key ]->is_pending = '1';
+			$expected[ $key ]->is_banned = false;
+		}
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->assertEquals( $expected, BP_Groups_Group::get_group_extras( $paged_groups, $group_ids ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group get_group_extras
+	 */
+	public function test_get_group_extras_banned() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create();
+
+		$member                = new BP_Groups_Member;
+		$member->group_id      = $g;
+		$member->user_id       = $u;
+		$member->date_modified = bp_core_current_time();
+		$member->is_banned     = true;
+		$member->save();
+
+		$paged_groups = array();
+		$paged_groups[] = new stdClass;
+		$paged_groups[0]->id = $g;
+
+		$group_ids = array( $g );
+
+		$expected = array();
+		foreach ( $paged_groups as $key => $value ) {
+			$expected[ $key ] = new stdClass;
+			$expected[ $key ]->id = $value->id;
+			$expected[ $key ]->is_member = '0';
+			$expected[ $key ]->is_invited = '0';
+			$expected[ $key ]->is_pending = '0';
+			$expected[ $key ]->is_banned = true;
+		}
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u );
+
+		$this->assertEquals( $expected, BP_Groups_Group::get_group_extras( $paged_groups, $group_ids ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @ticket BP5451
+	 */
+	public function test_admins_property() {
+		$user_1 = self::factory()->user->create_and_get();
+		$g = self::factory()->group->create( array(
+			'creator_id' => $user_1->ID,
+		) );
+
+		$group = new BP_Groups_Group( $g );
+
+		$expected_admin_props = array(
+			'user_id' => $user_1->ID,
+			'user_login' => $user_1->user_login,
+			'user_email' => $user_1->user_email,
+			'user_nicename' => $user_1->user_nicename,
+			'is_admin' => 1,
+			'is_mod' => 0,
+		);
+
+		$found_admin = $group->admins[0];
+		foreach ( $expected_admin_props as $prop => $value ) {
+			$this->assertEquals( $value, $found_admin->{$prop} );
+		}
+	}
+
+	/**
+	 * @ticket BP7497
+	 */
+	public function test_admins_property_should_match_users_without_wp_role() {
+		$user_1 = self::factory()->user->create_and_get();
+		$g = self::factory()->group->create( array(
+			'creator_id' => $user_1->ID,
+		) );
+
+		$user_1->remove_all_caps();
+
+		$group = new BP_Groups_Group( $g );
+
+		$this->assertEqualSets( array( $user_1->ID ), wp_list_pluck( $group->admins, 'user_id' ) );
+	}
+
+	/**
+	 * @ticket BP7677
+	 */
+	public function test_demoting_sole_admin() {
+		$user = self::factory()->user->create_and_get();
+		$group = self::factory()->group->create_and_get( array(
+			'creator_id' => $user->ID,
+		) );
+		$member = new BP_Groups_Member( $user->ID, $group->id );
+		$member->demote();
+
+		$this->assertEmpty( $group->admins );
+		$this->assertEmpty( $group->mods );
+	}
+
+	/**
+	 * @ticket BP5451
+	 */
+	public function test_mods_property() {
+		$users = self::factory()->user->create_many( 2 );
+		$user_1 = new WP_User( $users[0] );
+		$user_2 = new WP_User( $users[1] );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $user_1->ID,
+		) );
+
+		$this->add_user_to_group( $user_2->ID, $g, array( 'is_mod' => 1 ) );
+
+		$group = new BP_Groups_Group( $g );
+
+		$expected_mod_props = array(
+			'user_id' => $user_2->ID,
+			'user_login' => $user_2->user_login,
+			'user_email' => $user_2->user_email,
+			'user_nicename' => $user_2->user_nicename,
+			'is_admin' => 0,
+			'is_mod' => 1,
+		);
+
+		$found_mod = $group->mods[0];
+		foreach ( $expected_mod_props as $prop => $value ) {
+			$this->assertEquals( $value, $found_mod->{$prop} );
+		}
+	}
+
+	/**
+	 * @ticket BP5451
+	 */
+	public function test_is_member_property() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $users[0],
+		) );
+
+		wp_set_current_user( $users[1] );
+
+		$group_a = new BP_Groups_Group( $g );
+		$this->assertFalse( $group_a->is_member );
+
+		$this->add_user_to_group( $users[1], $g );
+		$group_b = new BP_Groups_Group( $g );
+		$this->assertFalse( $group_b->is_member );
+	}
+
+	/**
+	 * @ticket BP5451
+	 */
+	public function test_is_invited_property() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $users[0],
+		) );
+
+		wp_set_current_user( $users[1] );
+
+		$group_a = new BP_Groups_Group( $g );
+		$this->assertFalse( $group_a->is_invited );
+
+		$this->add_user_to_group( $users[1], $g, array(
+			'invite_sent' => 1,
+			'inviter_id' => $users[0],
+			'is_confirmed' => 0,
+		) );
+		$group_b = new BP_Groups_Group( $g );
+		$this->assertFalse( $group_b->is_invited );
+	}
+
+	/**
+	 * @ticket BP5451
+	 */
+	public function test_is_pending_property() {
+		$users = self::factory()->user->create_many( 2 );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $users[0],
+		) );
+
+		wp_set_current_user( $users[1] );
+
+		$group_a = new BP_Groups_Group( $g );
+		$this->assertFalse( $group_a->is_pending );
+
+		$this->add_user_to_group( $users[1], $g, array(
+			'is_confirmed' => 0,
+			'invite_sent' => 0,
+			'inviter_id' => 0,
+		) );
+		$group_b = new BP_Groups_Group( $g );
+		$this->assertFalse( $group_b->is_pending );
+	}
+
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_single_value() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => 'foo',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_array_with_single_value() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => array( 'foo' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_with_comma_separated_list() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => 'foo, bar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g2 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_array_with_multiple_values() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => array( 'foo', 'bar' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g2 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_should_discart_non_existing_types_in_comma_separated_value() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => 'foo, baz',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_should_return_empty_when_no_groups_match_specified_types() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => 'foo, baz',
+		) );
+
+		$this->assertEmpty( $groups['groups'] );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__in_single_value() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => 'bar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g2 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__in_comma_separated_values() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => 'foo, bar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g2 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__in_array_multiple_values() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => array( 'foo', 'bar' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g2 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__in_array_with_single_value() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => array( 'foo' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__in_should_discart_non_existing_types_in_comma_separated_value() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => 'foo, baz',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g1 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__in_should_return_empty_when_no_groups_match_specified_types() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => 'foo, baz',
+		) );
+
+		$this->assertEmpty( $groups['groups'] );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type_should_take_precedence_over_group_type__in() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__in' => 'foo',
+			'group_type' => 'bar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g2 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__not_in_should_return_groups_with_types_and_without_types() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__not_in' => 'foo',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g2, $g3 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__not_in_comma_separated_values() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+		bp_groups_set_group_type( $g3, 'baz' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__not_in' => 'foo, bar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g3 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__not_array_with_multiple_values() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'bar' );
+		bp_groups_set_group_type( $g3, 'baz' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__not_in' => array( 'foo', 'bar' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g3 ), $found );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__not_in_should_return_no_results_when_all_groups_mathc_sepecified_type() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'foo' );
+		bp_groups_set_group_type( $g3, 'foo' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type__not_in' => 'foo',
+		) );
+
+		$this->assertEmpty( $groups['groups'] );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_type__not_in_takes_precedence_over_group_type() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		$g3 = self::factory()->group->create();
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_set_group_type( $g1, 'foo' );
+		bp_groups_set_group_type( $g2, 'foo' );
+		bp_groups_set_group_type( $g3, 'foo' );
+
+		$groups = BP_Groups_Group::get( array(
+			'group_type' => 'foo',
+			'group_type__not_in' => 'foo',
+		) );
+
+		$this->assertEmpty( $groups['groups'] );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_get_by_parent_id() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'parent_id' => $g1,
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g2 ), $found );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_get_by_parent_id_ignore_grandparent() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'parent_id' => $g2,
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( array( $g3 ), $found );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_get_by_parent_id_array() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'parent_id' => array( $g1, $g2 ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g2, $g3 ), $found );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_get_by_parent_id_comma_separated_string() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'parent_id' => "$g1, $g2",
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g2, $g3 ), $found );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_get_by_parent_id_top_level_groups() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'parent_id' => 0,
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g4 ), $found );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_get_by_parent_id_top_level_groups_using_false() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		$groups = BP_Groups_Group::get( array(
+			'parent_id' => false,
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g4 ), $found );
+	}
+
+	/**
+	 * @group get_by_slug
+	 */
+	public function test_get_by_slug() {
+		$g1 = self::factory()->group->create(array(
+			'slug'      => 'apr'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'slug'      => 'jan'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'slug'      => 'mar'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'slug' => array( 'apr', 'mar' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g3 ), $found );
+	}
+
+	/**
+	 * @group get_by_slug
+	 */
+	public function test_get_by_slug_accept_string() {
+		$g1 = self::factory()->group->create(array(
+			'slug'      => 'apr'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'slug'      => 'jan'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'slug'      => 'mar'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'slug' => 'jan',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g2 ), $found );
+	}
+
+	/**
+	 * @group get_by_slug
+	 */
+	public function test_get_by_slug_accept_comma_separated_string() {
+		$g1 = self::factory()->group->create(array(
+			'slug'      => 'apr'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'slug'      => 'jan'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'slug'      => 'mar'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'slug' => 'apr, mar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g3 ), $found );
+	}
+
+	/**
+	 * @group get_by_slug
+	 */
+	public function test_get_by_slug_accept_space_separated_string() {
+		$g1 = self::factory()->group->create(array(
+			'slug'      => 'apr'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'slug'      => 'jan'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'slug'      => 'mar'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'slug' => 'apr mar',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g3 ), $found );
+	}
+
+	/**
+	 * @group get_by_status
+	 */
+	public function test_get_by_status() {
+		$g1 = self::factory()->group->create(array(
+			'status'      => 'private'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'status'      => 'public'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'status'      => 'hidden'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'status' => array( 'private', 'hidden' ),
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g3 ), $found );
+	}
+
+	/**
+	 * @group get_by_status
+	 */
+	public function test_get_by_status_accept_string() {
+		$g1 = self::factory()->group->create(array(
+			'status'      => 'private'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'status'      => 'public'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'status'      => 'hidden'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'status' => 'public',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g2 ), $found );
+	}
+
+	/**
+	 * @group get_by_status
+	 */
+	public function test_get_by_status_accept_comma_separated_string() {
+		$g1 = self::factory()->group->create(array(
+			'status'      => 'private'
+		) );
+		$g2 = self::factory()->group->create( array(
+			'status'      => 'public'
+		) );
+		$g3 = self::factory()->group->create( array(
+			'status'      => 'hidden'
+		) );
+
+		$groups = BP_Groups_Group::get( array(
+			'status' => 'private, hidden',
+		) );
+
+		$found = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEqualSets( array( $g1, $g3 ), $found );
+	}
+
+	/**
+	 * @group get_ids_only
+	 */
+	public function test_get_return_ids_only() {
+		$now = time();
+		$g1 = $this->factory->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60 ),
+		) );
+		$g2 = $this->factory->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*2 ),
+		) );
+		$g3 = $this->factory->group->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 60*60*3 ),
+		)  );
+
+		$groups = BP_Groups_Group::get( array(
+			'fields' => 'ids',
+		) );
+
+		$this->assertSame( array( $g1, $g2, $g3 ), $groups['groups'] );
+	}
+
+}
+
+/**
+ * Stub class for accessing protected methods
+ */
+class _BP_Groups_Group extends BP_Groups_Group {
+	public static function _convert_type_to_order_orderby( $type ) {
+		return self::convert_type_to_order_orderby( $type );
+	}
+
+	public static function _convert_orderby_to_order_by_term( $term ) {
+		return self::convert_orderby_to_order_by_term( $term );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-groups-member.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-groups-member.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/class-bp-groups-member.php	(revision 12265)
@@ -0,0 +1,1457 @@
+<?php
+/**
+ * @group groups
+ * @group BP_Groups_Member
+ */
+class BP_Tests_BP_Groups_Member_TestCases extends BP_UnitTestCase {
+	static public $user_ids;
+	static public $group_ids;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		global $wpdb, $bp;
+		self::$user_ids  = $factory->user->create_many( 4 );
+		self::$group_ids = $factory->group->create_many( 3, array(
+			'creator_id' => self::$user_ids[3],
+		) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		array_map( array( __CLASS__, 'delete_user' ), self::$user_ids );
+		array_map( 'groups_delete_group', self::$group_ids );
+	}
+
+	public static function invite_user_to_group( $user_id, $group_id, $inviter_id ) {
+		$invite                = new BP_Groups_Member;
+		$invite->group_id      = $group_id;
+		$invite->user_id       = $user_id;
+		$invite->date_modified = bp_core_current_time();
+		$invite->inviter_id    = $inviter_id;
+		$invite->is_confirmed  = 0;
+		$invite->invite_sent   = 1;
+
+		$invite->save();
+		return $invite->id;
+	}
+
+	public static function create_group_membership_request( $user_id, $group_id ) {
+		$request                = new BP_Groups_Member;
+		$request->group_id      = $group_id;
+		$request->user_id       = $user_id;
+		$request->date_modified = bp_core_current_time();
+		$request->inviter_id    = 0;
+		$request->is_confirmed  = 0;
+
+		$request->save();
+		return $request->id;
+	}
+
+	public function test_get_recently_joined_with_filter() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Tab',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Diet Rite',
+		) );
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$groups = BP_Groups_Member::get_recently_joined( $u, false, false, 'Rite' );
+
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g2 ) );
+	}
+
+	public function test_get_is_admin_of_with_filter() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'RC Cola',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Pepsi',
+		) );
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$m1 = new BP_Groups_Member( $u, $g1 );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u, $g2 );
+		$m2->promote( 'admin' );
+
+		$groups = BP_Groups_Member::get_is_admin_of( $u, false, false, 'eps' );
+
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g2 ) );
+	}
+
+	public function test_get_is_mod_of_with_filter() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'RC Cola',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Pepsi',
+		) );
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$m1 = new BP_Groups_Member( $u, $g1 );
+		$m1->promote( 'mod' );
+		$m2 = new BP_Groups_Member( $u, $g2 );
+		$m2->promote( 'mod' );
+
+		$groups = BP_Groups_Member::get_is_mod_of( $u, false, false, 'eps' );
+
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g2 ) );
+	}
+
+	public function test_get_is_banned_of_with_filter() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'RC Cola',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Pepsi',
+		) );
+
+		$u = self::factory()->user->create();
+		self::add_user_to_group( $u, $g1 );
+		self::add_user_to_group( $u, $g2 );
+
+		$m1 = new BP_Groups_Member( $u, $g1 );
+		$m1->ban();
+		$m2 = new BP_Groups_Member( $u, $g2 );
+		$m2->ban();
+
+		$groups = BP_Groups_Member::get_is_banned_of( $u, false, false, 'eps' );
+
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g2 ) );
+	}
+
+	public function test_get_invites_with_exclude() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'RC Cola',
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'Pepsi',
+		) );
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		self::add_user_to_group( $u1, $g1 );
+		self::add_user_to_group( $u1, $g2 );
+		self::invite_user_to_group( $u2, $g1, $u1 );
+		self::invite_user_to_group( $u2, $g2, $u1 );
+
+		$groups = BP_Groups_Member::get_invites( $u2, false, false, array( 'awesome', $g1 ) );
+
+		$ids = wp_list_pluck( $groups['groups'], 'id' );
+		$this->assertEquals( $ids, array( $g2 ) );
+	}
+
+	/**
+	 * @expectedDeprecated BP_Groups_Member::get_all_for_group
+	 */
+	public function test_get_all_for_group_with_exclude() {
+		$g1 = self::factory()->group->create();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		self::add_user_to_group( $u1, $g1 );
+		self::add_user_to_group( $u2, $g1 );
+
+		$members = BP_Groups_Member::get_all_for_group( $g1, false, false, true, true, array( $u1 ) );
+
+		$mm = (array) $members['members'];
+		$ids = wp_list_pluck( $mm, 'user_id' );
+		$this->assertEquals( array( $u2 ), $ids );
+	}
+
+	/**
+	 * @group bp_groups_user_can_send_invites
+	 */
+	public function test_bp_groups_user_can_send_invites() {
+		$u_nonmembers = self::factory()->user->create();
+		$u_members    = self::factory()->user->create();
+		$u_mods       = self::factory()->user->create();
+		$u_admins     = self::factory()->user->create();
+		$u_siteadmin  = self::factory()->user->create();
+
+		$user_siteadmin = new WP_User( $u_siteadmin );
+		$user_siteadmin->add_role( 'administrator' );
+
+		$g = self::factory()->group->create();
+
+		$time = time() - 60;
+		$old_current_user = get_current_user_id();
+
+		// Create member-level user
+		$this->add_user_to_group( $u_members, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// Create mod-level user
+		$this->add_user_to_group( $u_mods, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+		$m_mod = new BP_Groups_Member( $u_mods, $g );
+		$m_mod->promote( 'mod' );
+
+		// Create admin-level user
+		$this->add_user_to_group( $u_admins, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+		$m_admin = new BP_Groups_Member( $u_admins, $g );
+		$m_admin->promote( 'admin' );
+
+		// Test with no status
+		// In bp_group_get_invite_status(), no status falls back to "members"
+		$this->assertTrue( '' == groups_get_groupmeta( $g, 'invite_status' ) );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_nonmembers ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_members ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_mods ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_admins ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_siteadmin ) );
+
+		// Test with members status
+		groups_update_groupmeta( $g, 'invite_status', 'members' );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_nonmembers ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_members ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_mods ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_admins ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_siteadmin ) );
+		// Falling back to current user
+		$this->set_current_user( $u_members );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, null ) );
+
+		// Test with mod status
+		groups_update_groupmeta( $g, 'invite_status', 'mods' );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_nonmembers ) );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_members ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_mods ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_admins ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_siteadmin ) );
+		// Falling back to current user
+		$this->set_current_user( $u_members );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, null ) );
+		$this->set_current_user( $u_mods );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, null ) );
+
+		// Test with admin status
+		groups_update_groupmeta( $g, 'invite_status', 'admins' );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_nonmembers ) );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_members ) );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, $u_mods ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_admins ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, $u_siteadmin ) );
+		// Falling back to current user
+		$this->set_current_user( $u_mods );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, null ) );
+		$this->set_current_user( $u_admins );
+		$this->assertTrue( bp_groups_user_can_send_invites( $g, null ) );
+
+		// Bad or null parameters
+		$this->assertFalse( bp_groups_user_can_send_invites( 59876454257, $u_members ) );
+		$this->assertFalse( bp_groups_user_can_send_invites( $g, 958647515 ) );
+		// Not in group context
+		$this->assertFalse( bp_groups_user_can_send_invites( null, $u_members ) );
+		// In group context
+		$g_obj = groups_get_group( $g );
+		$this->go_to( bp_get_group_permalink( $g_obj ) );
+		groups_update_groupmeta( $g, 'invite_status', 'mods' );
+		$this->assertFalse( bp_groups_user_can_send_invites( null, $u_nonmembers ) );
+		$this->assertFalse( bp_groups_user_can_send_invites( null, $u_members ) );
+		$this->assertTrue( bp_groups_user_can_send_invites( null, $u_mods ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_reject_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_bp_groups_reject_membership_request_remove_request() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		// Membership requests should be removed.
+		self::create_group_membership_request( $u1, $g );
+		groups_reject_membership_request( null, $u1, $g );
+		$u1_has_request = groups_check_for_membership_request( $u1, $g );
+		$this->assertEquals( 0, $u1_has_request );
+	}
+
+	/**
+	 * @group groups_reject_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_bp_groups_reject_membership_request_leave_memberships_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$this->add_user_to_group( $u1, $g );
+
+		// Confirmed memberships should be left intact.
+		groups_reject_membership_request( null, $u1, $g );
+		$u1_is_member = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_is_member ) && $u1_is_member > 0 );
+	}
+
+	/**
+	 * @group groups_reject_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_bp_groups_reject_membership_request_leave_invites_intact() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// Outstanding invitations should be left intact.
+		self::invite_user_to_group( $u2, $g, $u1 );
+		groups_reject_membership_request( null, $u2, $g );
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g );
+		$this->assertTrue( is_numeric( $u2_has_invite ) && $u2_has_invite > 0 );
+	}
+
+	/**
+	 * @group groups_delete_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_membership_request_remove_request() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		// Membership requests should be removed.
+		self::create_group_membership_request( $u1, $g );
+		groups_delete_membership_request( null, $u1, $g );
+		$u1_has_request = groups_check_for_membership_request( $u1, $g );
+		$this->assertEquals( 0, $u1_has_request );
+	}
+
+	/**
+	 * @group groups_delete_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_membership_request_leave_memberships_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$this->add_user_to_group( $u1, $g );
+
+		// Confirmed memberships should be left intact.
+		groups_delete_membership_request( null, $u1, $g );
+		$u1_is_member = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_is_member ) && $u1_is_member > 0 );
+	}
+
+	/**
+	 * @group groups_delete_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_membership_request_leave_invites_intact() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// Outstanding invitations should be left intact.
+		self::invite_user_to_group( $u2, $g, $u1 );
+		groups_delete_membership_request( null, $u2, $g );
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g );
+		$this->assertTrue( is_numeric( $u2_has_invite ) && $u2_has_invite > 0 );
+	}
+
+	/**
+	 * @group groups_reject_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_reject_invite_remove_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// The invitation should be removed.
+		self::invite_user_to_group( $u2, $g, $u1 );
+		groups_reject_invite( $u2, $g );
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g, 'all' );
+		$this->assertEquals( 0, $u2_has_invite );
+	}
+
+	/**
+	 * @group groups_reject_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_reject_invite_leave_memberships_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// Confirmed memberships should be left intact.
+		groups_reject_invite( $u1, $g );
+		$u1_is_member = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_is_member ) && $u1_is_member > 0 );
+	}
+
+	/**
+	 * @group groups_reject_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_reject_invite_leave_requests_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		// Membership requests should be left intact.
+		self::create_group_membership_request( $u1, $g );
+		groups_reject_invite( $u1, $g );
+		$u1_has_request = groups_check_for_membership_request( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_has_request ) && $u1_has_request > 0 );
+	}
+
+	/**
+	 * @group groups_delete_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_invite_remove_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// The invitation should be removed.
+		self::invite_user_to_group( $u2, $g, $u1 );
+		groups_delete_invite( $u2, $g );
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g, 'all' );
+		$this->assertEquals( 0, $u2_has_invite );
+	}
+
+	/**
+	 * @group groups_delete_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_invite_remove_draft_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// Create the draft invitation.
+		groups_invite_user( array(
+			'user_id'    => $u2,
+			'group_id'   => $g,
+			'inviter_id' => $u1
+		) );
+
+		// Check that the invite got created.
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g, 'all' );
+		$this->assertTrue( is_numeric( $u2_has_invite ) && $u2_has_invite > 0 );
+
+		// The invitation should be removed.
+		groups_delete_invite( $u2, $g );
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g, 'all' );
+		$this->assertEquals( 0, $u2_has_invite );
+	}
+
+	/**
+	 * @group groups_delete_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_invite_leave_memberships_intact() {
+		$u1 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		groups_delete_invite( $u1, $g );
+		$u1_is_member = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_is_member ) && $u1_is_member > 0 );
+	}
+
+	/**
+	 * @group groups_delete_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_delete_invite_leave_requests_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		// Membership requests should be left intact.
+		self::create_group_membership_request( $u1, $g );
+		groups_delete_invite( $u1, $g );
+		$u1_has_request = groups_check_for_membership_request( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_has_request ) && $u1_has_request > 0 );
+	}
+
+	/**
+	 * @group groups_uninvite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_uninvite_user_remove_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// The invitation should be removed.
+		self::invite_user_to_group( $u2, $g, $u1 );
+		groups_uninvite_user( $u2, $g );
+		$u2_has_invite = groups_check_user_has_invite( $u2, $g, 'all' );
+		$this->assertEquals( 0, $u2_has_invite );
+	}
+
+	/**
+	 * @group groups_uninvite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_uninvite_user_leave_memberships_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		$time = time() - 60;
+		$this->add_user_to_group( $u1, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $time ),
+		) );
+
+		// Confirmed memberships should be left intact.
+		groups_is_user_member( $u1, $g );
+		groups_uninvite_user( $u1, $g );
+		$u1_is_member = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_is_member ) && $u1_is_member > 0 );
+	}
+
+	/**
+	 * @group groups_uninvite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_bp_groups_uninvite_user_leave_requests_intact() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'status' => 'private',
+		) );
+
+		// Membership requests should be left intact.
+		self::create_group_membership_request( $u1, $g );
+		groups_uninvite_user( $u1, $g );
+		$u1_has_request = groups_check_for_membership_request( $u1, $g );
+		$this->assertTrue( is_numeric( $u1_has_request ) && $u1_has_request > 0 );
+	}
+
+	/**
+	 * @group groups_join_group
+	 * @group group_membership
+	 */
+	public function test_groups_join_group_basic_join() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create();
+
+		groups_join_group( $g, $u1 );
+		$membership_id = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $membership_id ) && $membership_id > 0 );
+	}
+
+	/**
+	 * @group groups_join_group
+	 * @group group_membership
+	 */
+	public function test_groups_join_group_basic_join_use_current_user() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( $u1 );
+
+		groups_join_group( $g );
+		$membership_id = groups_is_user_member( $u1, $g );
+		$this->assertTrue( is_numeric( $membership_id ) && $membership_id > 0 );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_join_group
+	 * @group group_membership
+	 */
+	public function test_groups_join_group_already_member() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$this->add_user_to_group( $u1, $g );
+
+		$this->assertTrue( groups_join_group( $g, $u1 ) );
+	}
+
+	/**
+	 * @group groups_join_group
+	 * @group group_membership
+	 */
+	public function test_groups_join_group_cleanup_invites() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		$this->add_user_to_group( $u1, $g );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+
+		self::invite_user_to_group( $u2, $g, $u1 );
+
+		groups_join_group( $g, $u2 );
+		// Upon joining the group, outstanding invitations should be cleaned up.
+		$this->assertEquals( null, groups_check_user_has_invite( $u2, $g, 'any' ) );
+	}
+
+	/**
+	 * @group groups_join_group
+	 * @group group_membership
+	 */
+	public function test_groups_join_group_cleanup_requests() {
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		self::create_group_membership_request( $u1, $g );
+
+		groups_join_group( $g, $u1 );
+		// Upon joining the group, outstanding requests should be cleaned up.
+		$this->assertEquals( null, groups_check_for_membership_request( $u1, $g ) );
+	}
+
+	/**
+	 * @group groups_leave_group
+	 * @group group_membership
+	 */
+	public function test_groups_leave_group_basic_leave_self_initiated() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$u2 = self::factory()->user->create();
+		$this->add_user_to_group( $u2, $g );
+
+		$before = groups_get_total_member_count( $g );
+		$this->set_current_user( $u2 );
+		groups_leave_group( $g, $u2 );
+		$after = groups_get_total_member_count( $g );
+
+		$this->assertEquals( $before - 1, $after );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_leave_group
+	 * @group group_membership
+	 */
+	public function test_groups_leave_group_basic_leave_use_current_user() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$u2 = self::factory()->user->create();
+		$this->add_user_to_group( $u2, $g );
+
+		$before = groups_get_total_member_count( $g );
+		$this->set_current_user( $u2 );
+		groups_leave_group( $g );
+		$after = groups_get_total_member_count( $g );
+
+		$this->assertEquals( $before - 1, $after );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_leave_group
+	 * @group group_membership
+	 */
+	public function test_groups_leave_group_basic_leave_group_admin_initiated() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$u2 = self::factory()->user->create();
+		$this->add_user_to_group( $u2, $g );
+
+		$before = groups_get_total_member_count( $g );
+		$this->set_current_user( $u1 );
+		groups_leave_group( $g, $u2 );
+		$after = groups_get_total_member_count( $g );
+
+		$this->assertEquals( $before - 1, $after );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_leave_group
+	 * @group group_membership
+	 */
+	public function test_groups_leave_group_basic_leave_site_admin_initiated() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$u1_siteadmin = new WP_User( $u1 );
+		$u1_siteadmin->add_role( 'administrator' );
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$u2 = self::factory()->user->create();
+		$this->add_user_to_group( $u2, $g );
+
+		$before = groups_get_total_member_count( $g );
+		$this->set_current_user( $u1 );
+		groups_leave_group( $g, $u2 );
+		$after = groups_get_total_member_count( $g );
+
+		$this->assertEquals( $before - 1, $after );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_leave_group
+	 * @group group_membership
+	 */
+	public function test_groups_leave_group_single_admin_prevent_leave() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$u2 = self::factory()->user->create();
+		$this->add_user_to_group( $u2, $g );
+
+		$before = groups_get_total_member_count( $g );
+		$this->set_current_user( $u1 );
+		groups_leave_group( $g, $u1 );
+		$after = groups_get_total_member_count( $g );
+
+		$this->assertEquals( $before, $after );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_leave_group
+	 * @group group_membership
+	 */
+	public function test_groups_leave_group_multiple_admins_allow_leave() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$u2 = self::factory()->user->create();
+		$this->add_user_to_group( $u2, $g );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'admin' );
+
+		$before = groups_get_total_member_count( $g );
+		$this->set_current_user( $u1 );
+		groups_leave_group( $g, $u1 );
+		$after = groups_get_total_member_count( $g );
+
+		$this->assertEquals( $before - 1, $after );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_get_invites_for_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_get_invites_for_user() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g3 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		self::invite_user_to_group( $u2, $g1, $u1 );
+		self::invite_user_to_group( $u2, $g2, $u1 );
+		self::invite_user_to_group( $u2, $g3, $u1 );
+
+		$groups = groups_get_invites_for_user( $u2 );
+
+		$this->assertEqualSets( array( $g1, $g2, $g3 ), wp_list_pluck( $groups['groups'], 'id' ) );
+	}
+
+	/**
+	 * @group groups_get_invites_for_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_get_invites_for_user_infer_user() {
+		$old_current_user = get_current_user_id();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g3 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		self::invite_user_to_group( $u2, $g1, $u1 );
+		self::invite_user_to_group( $u2, $g2, $u1 );
+		self::invite_user_to_group( $u2, $g3, $u1 );
+
+		$this->set_current_user( $u2 );
+		$groups = groups_get_invites_for_user();
+		$this->assertEqualSets( array( $g1, $g2, $g3 ), wp_list_pluck( $groups['groups'], 'id' ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group groups_get_invites_for_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_get_invites_for_user_with_exclude() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g3 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		self::invite_user_to_group( $u2, $g1, $u1 );
+		self::invite_user_to_group( $u2, $g2, $u1 );
+		self::invite_user_to_group( $u2, $g3, $u1 );
+
+		$groups = groups_get_invites_for_user( $u2, false, false, array( $g2 ) );
+		$this->assertEqualSets( array( $g1, $g3 ), wp_list_pluck( $groups['groups'], 'id' ) );
+	}
+
+	/**
+	 * @group groups_get_invite_count_for_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_get_invite_count_for_user() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g3 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		self::invite_user_to_group( $u2, $g1, $u1 );
+		self::invite_user_to_group( $u2, $g2, $u1 );
+		self::invite_user_to_group( $u2, $g3, $u1 );
+
+		$this->assertEquals( 3, groups_get_invite_count_for_user( $u2 ) );
+	}
+
+	/**
+	 * @group groups_get_invite_count_for_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_get_invite_count_for_user_ignore_drafts() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation.
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// groups_get_invite_count_for_user should ignore draft invitations.
+		$this->assertEquals( 0, groups_get_invite_count_for_user( $u2 ) );
+	}
+
+	/**
+	 * @group groups_invite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_invite_user() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Check that the draft invitation has been created.
+		$draft = groups_check_user_has_invite( $u2, $g1, 'all' );
+		$this->assertTrue( is_numeric( $draft ) && $draft > 0 );
+	}
+
+	/**
+	 * @group groups_send_invites
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_send_invites() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Send the invitation
+		groups_send_invites( $u1, $g1 );
+
+		// Check that the invitation has been sent.
+		$sent = groups_check_user_has_invite( $u2, $g1, $type = 'sent' );
+		$this->assertTrue( is_numeric( $sent ) && $sent > 0 );
+	}
+
+	/**
+	 * @group groups_accept_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_accept_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Send the invitation
+		groups_send_invites( $u1, $g1 );
+
+		// Accept the invitation
+		groups_accept_invite( $u2, $g1 );
+
+		// Check that the user is a member of the group.
+		$member = groups_is_user_member( $u2, $g1 );
+		$this->assertTrue( is_numeric( $member ) && $member > 0 );
+		// Check that the invite has been removed.
+		$invite = groups_check_user_has_invite( $u2, $g1, 'all' );
+		$this->assertFalse( $invite );
+	}
+
+	/**
+	 * @group groups_accept_invite
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_accept_invite_removes_membership_requests() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Create membership request
+		groups_send_membership_request( $u2, $g1 );
+		$request = groups_check_for_membership_request( $u2, $g1 );
+		$this->assertTrue( is_numeric( $request ) && $request > 0 );
+
+		// Send the invitation
+		groups_send_invites( $u1, $g1 );
+
+		// Accept the invitation
+		groups_accept_invite( $u2, $g1 );
+
+		// Check that the membership request has been removed.
+		$this->assertTrue( 0 == groups_check_for_membership_request( $u2, $g1 ) );
+	}
+
+	/**
+	 * @group groups_send_invites
+	 * @group group_invitations
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_groups_sent_invite_plus_request_equals_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Send the invitation
+		groups_send_invites( $u1, $g1 );
+
+		// Create membership request
+		groups_send_membership_request( $u2, $g1 );
+
+		// User should now be a group member
+		$member = groups_is_user_member( $u2, $g1 );
+		$this->assertTrue( is_numeric( $member ) && $member > 0 );
+	}
+
+	/**
+	 * @group groups_delete_all_group_invites
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_delete_all_group_invites() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		self::invite_user_to_group( $u2, $g1, $u1 );
+		self::invite_user_to_group( $u3, $g1, $u1 );
+
+		groups_delete_all_group_invites( $g1 );
+
+		// Get group invitations of any type, from any user in the group.
+		$invitees = new BP_Group_Member_Query( array(
+			'group_id'     => $g1,
+			'is_confirmed' => 0,
+			'invite_sent'  => null,
+			'inviter_id'   => 'any',
+		) );
+
+		$this->assertTrue( empty( $invitees->results ) );
+	}
+
+	/**
+	 * @group groups_invite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_send_invites_fail_on_empty_group_id() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// Create draft invitation with empty inviter_id
+		$invite_created = groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => 0,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		$this->assertFalse( $invite_created );
+	}
+
+	/**
+	 * @group groups_invite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_send_invites_fail_on_empty_user_id() {
+		$u1 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation with empty inviter_id
+		$invite_created = groups_invite_user( array(
+			'user_id'       => 0,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		$this->assertFalse( $invite_created );
+	}
+
+	/**
+	 * @group groups_invite_user
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_send_invites_fail_on_empty_inviter_id() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation with empty inviter_id
+		$invite_created = groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => 0,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		$this->assertFalse( $invite_created );
+	}
+
+	/**
+	 * @group groups_get_invites_for_group
+	 * @group group_send_invites
+	 * @group group_invitations
+	 * @group group_membership
+	 */
+	public function test_groups_get_invites_for_group_with_sent_parameter() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Send the invitation; this will set the 'invite_sent' value to 1.
+		groups_send_invites( $u1, $g1 );
+
+		// Default groups_get_invites_for_group() call
+		$i = groups_get_invites_for_group( $u1, $g1 );
+		$this->assertEqualSets( array( $u2 ), $i );
+
+		// Fetch users whose invites have been sent out; should be the same as above.
+		$i = groups_get_invites_for_group( $u1, $g1 );
+		$this->assertEqualSets( array( $u2 ), $i );
+
+		// Fetch users whose invites haven't been sent yet.
+		$i = groups_get_invites_for_group( $u1, $g1, 0 );
+		$this->assertEmpty( $i );
+	}
+
+	/**
+	 * @group groups_send_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_groups_send_membership_request() {
+		$u1 = self::factory()->user->create();
+		$g1 = self::factory()->group->create();
+
+		// Create membership request
+		groups_send_membership_request( $u1, $g1 );
+
+		$request = groups_check_for_membership_request( $u1, $g1 );
+		$this->assertTrue( is_numeric( $request ) && $request > 0 );
+	}
+
+	/**
+	 * @group groups_accept_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_groups_accept_membership_request_by_membership_id() {
+		$u1 = self::factory()->user->create();
+		$g1 = self::factory()->group->create();
+
+		// Create membership request
+		groups_send_membership_request( $u1, $g1 );
+
+		// Get group invitations of any type, from any user in the group.
+		$member = new BP_Groups_Member( $u1, $g1 );
+
+		groups_accept_membership_request( $member->id );
+
+		// User should now be a group member.
+		$member = groups_is_user_member( $u1, $g1 );
+
+		$this->assertTrue( is_numeric( $member ) && $member > 0 );
+	}
+
+	/**
+	 * @group groups_accept_membership_request
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_groups_accept_membership_request_by_user_id_group_id() {
+		$u1 = self::factory()->user->create();
+		$g1 = self::factory()->group->create();
+
+		// Create membership request
+		groups_send_membership_request( $u1, $g1 );
+
+		groups_accept_membership_request( null, $u1, $g1 );
+
+		// User should now be a group member
+		$member = groups_is_user_member( $u1, $g1 );
+		$this->assertTrue( is_numeric( $member ) && $member > 0 );
+	}
+
+	/**
+	 * @group groups_send_invites
+	 * @group group_invitations
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_groups_membership_request_plus_invite_equals_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// Create membership request
+		groups_send_membership_request( $u2, $g1 );
+
+		// Create draft invitation
+		groups_invite_user( array(
+			'user_id'       => $u2,
+			'group_id'      => $g1,
+			'inviter_id'    => $u1,
+			'date_modified' => bp_core_current_time(),
+			'is_confirmed'  => 0
+		) );
+
+		// Send the invitation
+		groups_send_invites( $u1, $g1 );
+
+		// User should now be a group member
+		$member = groups_is_user_member( $u2, $g1 );
+		$this->assertTrue( is_numeric( $member ) && $member > 0 );
+	}
+
+	/**
+	 * @group groups_accept_all_pending_membership_requests
+	 * @group group_membership_requests
+	 * @group group_membership
+	 */
+	public function test_groups_accept_all_pending_membership_requests() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g1 = self::factory()->group->create();
+
+		// Create membership request
+		groups_send_membership_request( $u1, $g1 );
+		groups_send_membership_request( $u2, $g1 );
+		groups_send_membership_request( $u3, $g1 );
+
+		groups_accept_all_pending_membership_requests( $g1 );
+
+		// All users should now be group members.
+		$members = new BP_Group_Member_Query( array( 'group_id' => $g1 ) );
+		$this->assertEqualSets( array( $u1, $u2, $u3 ), $members->user_ids );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @ticket BP6813
+	 */
+	public function test_total_group_count_should_return_integer() {
+		$this->assertInternalType( 'int', BP_Groups_Member::total_group_count( 123 ) );
+	}
+
+	/**
+	 * @group get_memberships_by_id
+	 */
+	public function test_get_memberships_by_id_with_single_id() {
+		$users = self::factory()->user->create_many( 2 );
+		$groups = self::factory()->group->create_many( 2 );
+
+		$m0 = $this->add_user_to_group( $users[0], $groups[0] );
+		$m1 = $this->add_user_to_group( $users[1], $groups[1] );
+
+		$found = BP_Groups_Member::get_memberships_by_id( $m0 );
+
+		$this->assertSame( 1, count( $found ) );
+		$this->assertEquals( $m0, $found[0]->id );
+	}
+
+	/**
+	 * @group get_memberships_by_id
+	 */
+	public function test_get_memberships_by_id_with_multiple_ids() {
+		$users = self::factory()->user->create_many( 2 );
+		$groups = self::factory()->group->create_many( 2 );
+
+		$m0 = $this->add_user_to_group( $users[0], $groups[0] );
+		$m1 = $this->add_user_to_group( $users[1], $groups[1] );
+
+		$found = BP_Groups_Member::get_memberships_by_id( array( $m0, $m1 ) );
+
+		$this->assertSame( 2, count( $found ) );
+		$this->assertEqualSets( array( $m0, $m1 ), wp_list_pluck( $found, 'id' ) );
+	}
+
+	/**
+	 * @ticket BP7382
+	 */
+	public function test_user_property_should_be_accessible() {
+		$user = self::factory()->user->create();
+		$group = self::factory()->group->create();
+
+		$this->add_user_to_group( $user, $group );
+
+		$membership = new BP_Groups_Member( $user, $group );
+
+		$user_obj = $membership->user;
+
+		$this->assertInstanceOf( 'BP_Core_User', $user_obj );
+		$this->assertEquals( $user, $user_obj->id );
+	}
+
+	/**
+	 * @group get_group_moderator_ids
+	 */
+	public function test_groups_get_group_mods_bad_id() {
+		$mods = groups_get_group_mods( null );
+
+		$this->assertTrue( is_array( $mods ) && empty( $mods ) );
+	}
+
+	/**
+	 * @group get_group_moderator_ids
+	 */
+	public function test_groups_get_group_admins_bad_id() {
+		$admins = groups_get_group_admins( null );
+
+		$this->assertTrue( is_array( $admins ) && empty( $admins ) );
+	}
+
+	/**
+	 * @ticket BP7859
+	 */
+	public function test_get_user_memberships_type_membership() {
+		groups_join_group( self::$group_ids[0], self::$user_ids[0] );
+
+		$memberships = BP_Groups_Member::get_user_memberships( self::$user_ids[0], array(
+			'type' => 'membership',
+		) );
+
+		$this->assertCount( 1, $memberships );
+		$this->assertSame( self::$group_ids[0], $memberships[0]->group_id );
+	}
+
+	/**
+	 * @ticket BP7859
+	 */
+	public function test_get_user_memberships_type_pending_request() {
+		groups_join_group( self::$group_ids[0], self::$user_ids[0] );
+		groups_send_membership_request( self::$user_ids[0], self::$group_ids[1] );
+
+		$memberships = BP_Groups_Member::get_user_memberships( self::$user_ids[0], array(
+			'type' => 'pending_request',
+		) );
+
+		$this->assertCount( 1, $memberships );
+		$this->assertSame( self::$group_ids[1], $memberships[0]->group_id );
+	}
+
+	/**
+	 * @ticket BP7859
+	 */
+	public function test_get_user_memberships_type_pending_received_invitation() {
+		groups_join_group( self::$group_ids[0], self::$user_ids[0] );
+		groups_invite_user( array(
+			'user_id'    => self::$user_ids[0],
+			'group_id'   => self::$group_ids[1],
+			'inviter_id' => self::$user_ids[1],
+		) );
+
+		$memberships = BP_Groups_Member::get_user_memberships( self::$user_ids[0], array(
+			'type' => 'pending_received_invitation',
+		) );
+
+		$this->assertCount( 1, $memberships );
+		$this->assertSame( self::$group_ids[1], $memberships[0]->group_id );
+	}
+
+	/**
+	 * @ticket BP7859
+	 */
+	public function test_get_user_memberships_type_pending_sent_invitation() {
+		groups_join_group( self::$group_ids[0], self::$user_ids[0] );
+		groups_invite_user( array(
+			'user_id'    => self::$user_ids[1],
+			'group_id'   => self::$group_ids[1],
+			'inviter_id' => self::$user_ids[0],
+		) );
+
+		$memberships = BP_Groups_Member::get_user_memberships( self::$user_ids[0], array(
+			'type' => 'pending_sent_invitation',
+		) );
+
+		$this->assertCount( 1, $memberships );
+		$this->assertSame( self::$group_ids[1], $memberships[0]->group_id );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions.php	(revision 12265)
@@ -0,0 +1,938 @@
+<?php
+
+/**
+ * @group groups
+ * @group functions
+ */
+class BP_Tests_Groups_Functions extends BP_UnitTestCase {
+	static public $group_ids;
+	static public $user_ids;
+
+	static public function wpSetUpBeforeClass( $factory ) {
+		self::$user_ids  = $factory->user->create_many( 3 );
+		self::$group_ids = $factory->group->create_many( 2, array(
+			'creator_id' => self::$user_ids[2],
+		) );
+	}
+
+	static public function wpTearDownAfterClass() {
+		array_map( array( __CLASS__, 'delete_user' ), self::$user_ids );
+		array_map( 'groups_delete_group', self::$group_ids );
+	}
+
+	public function test_creating_new_group_as_authenticated_user() {
+		$u = self::factory()->user->create();
+		wp_set_current_user( $u );
+
+		self::factory()->group->create();
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_join_group
+	 */
+	public function test_total_group_count_groups_join_group() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		groups_join_group( $g, $u2 );
+		$this->assertEquals( 1, bp_get_user_meta( $u2, 'total_group_count', true ) );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_leave_group
+	 */
+	public function test_total_group_count_groups_leave_group() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+		groups_join_group( $g2, $u2 );
+
+		// Set the current user so the leave group request goes through.
+		$this->set_current_user( $u2 );
+		groups_leave_group( $g1, $u2 );
+		$this->assertEquals( 1, bp_get_user_meta( $u2, 'total_group_count', true ) );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_ban_member
+	 */
+	public function test_total_group_count_groups_ban_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+		groups_join_group( $g2, $u2 );
+
+		// Fool the admin check
+		$this->set_current_user( $u1 );
+		buddypress()->is_item_admin = true;
+
+		groups_ban_member( $u2, $g1 );
+
+		$this->assertEquals( 1, bp_get_user_meta( $u2, 'total_group_count', true ) );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_unban_member
+	 */
+	public function test_total_group_count_groups_unban_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+		groups_join_group( $g2, $u2 );
+
+		// Fool the admin check
+		$this->set_current_user( $u1 );
+		buddypress()->is_item_admin = true;
+
+		groups_ban_member( $u2, $g1 );
+
+		groups_unban_member( $u2, $g1 );
+
+		$this->assertEquals( 2, bp_get_user_meta( $u2, 'total_group_count', true ) );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_accept_invite
+	 */
+	public function test_total_group_count_groups_accept_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create();
+		groups_invite_user( array(
+			'user_id' => $u1,
+			'group_id' => $g,
+			'inviter_id' => $u2,
+		) );
+
+		groups_accept_invite( $u2, $g );
+
+		$this->assertEquals( 1, bp_get_user_meta( $u2, 'total_group_count', true ) );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_accept_membership_request
+	 */
+	public function test_total_group_count_groups_accept_membership_request() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$current_user = bp_loggedin_user_id();
+		$this->set_current_user( $u2 );
+
+		$g = self::factory()->group->create();
+		groups_send_membership_request( $u1, $g );
+
+		groups_accept_membership_request( 0, $u1, $g );
+
+		$this->assertEquals( 1, bp_get_user_meta( $u1, 'total_group_count', true ) );
+
+		$this->set_current_user( $current_user );
+	}
+
+	/**
+	 * @group total_group_count
+	 * @group groups_remove_member
+	 */
+	public function test_total_group_count_groups_remove_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+		groups_join_group( $g2, $u2 );
+
+		// Fool the admin check
+		$this->set_current_user( $u1 );
+		buddypress()->is_item_admin = true;
+
+		groups_remove_member( $u2, $g1 );
+
+		$this->assertEquals( 1, bp_get_user_meta( $u2, 'total_group_count', true ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_join_group
+	 */
+	public function test_total_member_count_groups_join_group() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		groups_join_group( $g, $u2 );
+		$this->assertEquals( 2, groups_get_groupmeta( $g, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_leave_group
+	 */
+	public function test_total_member_count_groups_leave_group() {
+		$u1 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u1 );
+
+		groups_leave_group( $g1, $u1 );
+		$this->assertEquals( 1, groups_get_groupmeta( $g1, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_ban_member
+	 */
+	public function test_total_member_count_groups_ban_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+
+		// Fool the admin check
+		$this->set_current_user( $u1 );
+		buddypress()->is_item_admin = true;
+
+		groups_ban_member( $u2, $g1 );
+
+		$this->assertEquals( 1, groups_get_groupmeta( $g1, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_unban_member
+	 */
+	public function test_total_member_count_groups_unban_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+
+		// Fool the admin check
+		$this->set_current_user( $u1 );
+		buddypress()->is_item_admin = true;
+
+		groups_ban_member( $u2, $g1 );
+
+		groups_unban_member( $u2, $g1 );
+
+		$this->assertEquals( 2, groups_get_groupmeta( $g1, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_accept_invite
+	 */
+	public function test_total_member_count_groups_accept_invite() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_invite_user( array(
+			'user_id' => $u1,
+			'group_id' => $g,
+			'inviter_id' => $u2,
+		) );
+
+		groups_accept_invite( $u2, $g );
+
+		$this->assertEquals( 2, groups_get_groupmeta( $g, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_accept_membership_request
+	 */
+	public function test_total_member_count_groups_accept_membership_request() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		groups_send_membership_request( $u2, $g );
+		groups_accept_membership_request( 0, $u2, $g );
+
+		$this->assertEquals( 2, groups_get_groupmeta( $g, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_remove_member
+	 */
+	public function test_total_member_count_groups_remove_member() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		groups_join_group( $g1, $u2 );
+
+		// Fool the admin check
+		$this->set_current_user( $u1 );
+		buddypress()->is_item_admin = true;
+
+		groups_remove_member( $u2, $g1 );
+
+		$this->assertEquals( 1, groups_get_groupmeta( $g1, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group total_member_count
+	 * @group groups_create_group
+	 */
+	public function test_total_member_count_groups_create_group() {
+		$u1 = self::factory()->user->create();
+		$g = groups_create_group( array(
+			'creator_id' => $u1,
+			'name' => 'Boone Is Handsome',
+			'description' => 'Yes',
+			'slug' => 'boone-is-handsome',
+			'status' => 'public',
+			'enable_forum' => 0,
+			'date_created' => bp_core_current_time(),
+		) );
+
+		$this->assertEquals( 1, groups_get_groupmeta( $g, 'total_member_count' ) );
+	}
+
+	/**
+	 * @group groups_create_group
+	 */
+	public function test_groups_create_group_dont_delete_description_for_existing_group_when_no_description_is_passed() {
+		$g = self::factory()->group->create();
+
+		$group_before = groups_get_group( $g );
+		groups_create_group( array(
+			'group_id' => $g,
+			'enable_forum' => 1,
+		) );
+
+		$group_after = groups_get_group( $g );
+		$this->assertSame( $group_before->description, $group_after->description );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @ticket BP5180
+	 */
+	public function test_groups_update_groupmeta_with_line_breaks() {
+		$g = self::factory()->group->create();
+		$meta_value = 'Foo!
+
+Bar!';
+		groups_update_groupmeta( $g, 'linebreak_test', $meta_value );
+
+		$this->assertEquals( $meta_value, groups_get_groupmeta( $g, 'linebreak_test' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_update_groupmeta_non_numeric_id() {
+		$this->assertFalse( groups_update_groupmeta( 'foo', 'bar', 'baz' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_update_groupmeta_stripslashes() {
+		$g = self::factory()->group->create();
+		$value = "This string is totally slashin\'!";
+		groups_update_groupmeta( $g, 'foo', $value );
+
+		$this->assertSame( stripslashes( $value ), groups_get_groupmeta( $g, 'foo' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_update_groupmeta_new() {
+		$g = self::factory()->group->create();
+		$this->assertSame( '', groups_get_groupmeta( $g, 'foo' ), '"foo" meta should be empty for this group.' );
+		$this->assertNotEmpty( groups_update_groupmeta( $g, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_update_groupmeta_existing() {
+		$g = self::factory()->group->create();
+		groups_update_groupmeta( $g, 'foo', 'bar' );
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo' ), '"foo" meta should be set already for this group.' );
+		$this->assertTrue( groups_update_groupmeta( $g, 'foo', 'baz' ) );
+		$this->assertSame( 'baz', groups_get_groupmeta( $g, 'foo' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_update_groupmeta_existing_same_value() {
+		$g = self::factory()->group->create();
+		groups_update_groupmeta( $g, 'foo', 'bar' );
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo' ), '"foo" meta should be set already for this group.' );
+		$this->assertFalse( groups_update_groupmeta( $g, 'foo', 'bar' ) );
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_update_groupmeta
+	 */
+	public function test_groups_update_groupmeta_prev_value() {
+		$g = self::factory()->group->create();
+		groups_add_groupmeta( $g, 'foo', 'bar' );
+
+		// In earlier versions of WordPress, bp_activity_update_meta()
+		// returns true even on failure. However, we know that in these
+		// cases the update is failing as expected, so we skip this
+		// assertion just to keep our tests passing
+		// See https://core.trac.wordpress.org/ticket/24933
+		if ( version_compare( $GLOBALS['wp_version'], '3.7', '>=' ) ) {
+			$this->assertFalse( groups_update_groupmeta( $g, 'foo', 'bar2', 'baz' ) );
+		}
+
+		$this->assertTrue( groups_update_groupmeta( $g, 'foo', 'bar2', 'bar' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_get_groupmeta
+	 * @ticket BP5399
+	 */
+	public function test_groups_get_groupmeta_with_illegal_key_characters() {
+		$g = self::factory()->group->create();
+		groups_update_groupmeta( $g, 'foo', 'bar' );
+
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		$this->assertSame( '', groups_get_groupmeta( $g, $krazy_key ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_get_groupmeta_all_metas() {
+		$g = self::factory()->group->create();
+		groups_update_groupmeta( $g, 'foo', 'bar' );
+		groups_update_groupmeta( $g, 'Boone', 'is cool' );
+
+		// There's likely some other keys (total_member_count etc)
+		// Just check to make sure both of ours are there
+		$metas = groups_get_groupmeta( $g );
+		$count = count( $metas );
+		$found = array_slice( $metas, $count - 2 );
+
+		$expected = array(
+			'foo' => array(
+				'bar',
+			),
+			'Boone' => array(
+				'is cool',
+			),
+		);
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_get_groupmeta_all_metas_empty() {
+		$g = self::factory()->group->create();
+
+		// Get rid of any auto-created values
+		global $wpdb;
+
+		$bp = buddypress();
+		$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_groupmeta} WHERE group_id = %d", $g ) );
+		wp_cache_delete( $g, 'group_meta' );
+
+		$metas = groups_get_groupmeta( $g );
+		$this->assertSame( array(), $metas );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_get_groupmeta_empty() {
+		$g = self::factory()->group->create();
+		$this->assertSame( '', groups_get_groupmeta( $g, 'foo' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_get_groupmeta
+	 */
+	public function test_bp_activity_get_meta_single_true() {
+		$g = self::factory()->group->create();
+		groups_add_groupmeta( $g, 'foo', 'bar' );
+		groups_add_groupmeta( $g, 'foo', 'baz' );
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo' ) ); // default is true
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo', true ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_get_groupmeta
+	 */
+	public function test_bp_activity_get_meta_single_false() {
+		$g = self::factory()->group->create();
+		groups_add_groupmeta( $g, 'foo', 'bar' );
+		groups_add_groupmeta( $g, 'foo', 'baz' );
+		$this->assertSame( array( 'bar', 'baz' ), groups_get_groupmeta( $g, 'foo', false ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_get_groupmeta
+	 * @group cache
+	 */
+	public function test_groups_get_groupmeta_cache_all_on_get() {
+		$g = self::factory()->group->create();
+		groups_add_groupmeta( $g, 'foo', 'bar' );
+		groups_add_groupmeta( $g, 'foo1', 'baz' );
+		$this->assertFalse( wp_cache_get( $g, 'group_meta' ) );
+
+		// A single query should prime the whole meta cache
+		groups_get_groupmeta( $g, 'foo' );
+
+		$c = wp_cache_get( $g, 'group_meta' );
+		$this->assertNotEmpty( $c['foo1'] );
+	}
+
+	/**
+	 * @group groupmeta
+	 */
+	public function test_groups_delete_groupmeta_non_numeric_id() {
+		$this->assertFalse( groups_delete_groupmeta( 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_delete_groupmeta
+	 * @ticket BP5399
+	 */
+	public function test_groups_delete_groupmeta_with_illegal_key_characters() {
+		$g = self::factory()->group->create();
+		$this->assertNotEmpty( groups_update_groupmeta( $g, 'foo', 'bar' ), 'Value of "foo" should be set at this point.' );
+
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		$this->assertSame( 'bar', groups_get_groupmeta( $g, 'foo' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_delete_groupmeta
+	 * @ticket BP6326
+	 */
+	public function test_groups_delete_groupmeta_with_no_meta_key_when_group_has_metadata() {
+		global $wpdb;
+
+		$g = self::factory()->group->create();
+		$m = groups_get_groupmeta( $g );
+		foreach ( $m as $mk => $mv ) {
+			groups_delete_groupmeta( $g, $mk );
+		}
+
+		$found = groups_delete_groupmeta( $g );
+		$this->assertTrue( $found );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_delete_groupmeta
+	 */
+	public function test_groups_delete_groupmeta_with_delete_all_but_no_meta_key() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_add_groupmeta( $g1, 'foo', 'bar' );
+		groups_add_groupmeta( $g1, 'foo1', 'bar1' );
+		groups_add_groupmeta( $g2, 'foo', 'bar' );
+		groups_add_groupmeta( $g2, 'foo1', 'bar1' );
+
+		$this->assertTrue( groups_delete_groupmeta( $g1, '', '', true ) );
+		$this->assertEmpty( groups_get_groupmeta( $g1 ) );
+		$this->assertSame( 'bar', groups_get_groupmeta( $g2, 'foo' ) );
+		$this->assertSame( 'bar1', groups_get_groupmeta( $g2, 'foo1' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_delete_groupmeta
+	 */
+	public function test_groups_delete_groupmeta_with_delete_all() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_add_groupmeta( $g1, 'foo', 'bar' );
+		groups_add_groupmeta( $g1, 'foo1', 'bar1' );
+		groups_add_groupmeta( $g2, 'foo', 'bar' );
+		groups_add_groupmeta( $g2, 'foo1', 'bar1' );
+
+		$this->assertTrue( groups_delete_groupmeta( $g1, 'foo', '', true ) );
+		$this->assertSame( '', groups_get_groupmeta( $g1, 'foo' ) );
+		$this->assertSame( '', groups_get_groupmeta( $g2, 'foo' ) );
+		$this->assertSame( 'bar1', groups_get_groupmeta( $g1, 'foo1' ) );
+		$this->assertSame( 'bar1', groups_get_groupmeta( $g2, 'foo1' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_add_groupmeta
+	 */
+	public function test_groups_add_groupmeta_no_meta_key() {
+		$this->assertFalse( groups_add_groupmeta( 1, '', 'bar' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_add_groupmeta
+	 */
+	public function test_groups_add_groupmeta_empty_object_id() {
+		$this->assertFalse( groups_add_groupmeta( 0, 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_add_groupmeta
+	 */
+	public function test_groups_add_groupmeta_existing_unique() {
+		$g = self::factory()->group->create();
+		groups_add_groupmeta( $g, 'foo', 'bar' );
+		$this->assertFalse( groups_add_groupmeta( $g, 'foo', 'baz', true ) );
+	}
+
+	/**
+	 * @group groupmeta
+	 * @group groups_add_groupmeta
+	 */
+	public function test_groups_add_groupmeta_existing_not_unique() {
+		$g = self::factory()->group->create();
+		groups_add_groupmeta( $g, 'foo', 'bar' );
+		$this->assertNotEmpty( groups_add_groupmeta( $g, 'foo', 'baz' ) );
+	}
+
+	/**
+	 * @group counts
+	 */
+	public function test_get_invite_count_for_user() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+
+		// create invitation
+		groups_invite_user( array(
+			'user_id'    => $u2,
+			'group_id'   => $g,
+			'inviter_id' => $u1,
+		) );
+
+		// send the invite
+		// this function is imperative to set the 'invite_sent' flag in the DB
+		// why is this separated from groups_invite_user()?
+		// @see groups_screen_group_invite()
+		groups_send_invites( $u1, $g );
+
+		// assert invite count
+		$this->assertEquals( 1, groups_get_invite_count_for_user( $u2 ) );
+
+		// accept the invite and reassert
+		groups_accept_invite( $u2, $g );
+		$this->assertEquals( 0, groups_get_invite_count_for_user( $u2 ) );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_update_orphaned_groups_on_group_delete_top_level() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+
+		groups_delete_group( $g1 );
+
+		$child = groups_get_group( array( 'group_id' => $g2 ) );
+		$this->assertEquals( 0, $child->parent_id );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_update_orphaned_groups_on_group_delete_two_levels() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+
+		groups_delete_group( $g2 );
+
+		$child = groups_get_group( array( 'group_id' => $g3 ) );
+		$this->assertEquals( $g1, $child->parent_id );
+	}
+
+	/**
+	 * @group groups_get_group
+ 	 * @ticket BP7302
+	 */
+	public function test_groups_get_group_accept_integer() {
+		$g1 = self::factory()->group->create();
+		$group = groups_get_group( $g1 );
+
+		$this->assertEquals( $g1, $group->id );
+	}
+
+	/**
+	 * @group groups_get_group
+ 	 * @ticket BP7302
+	 */
+	public function test_groups_get_group_accept_numeric() {
+		$g1 = self::factory()->group->create();
+		$group = groups_get_group( (string) $g1 );
+
+		$this->assertEquals( $g1, $group->id );
+	}
+
+	/**
+	 * @group groups_get_group
+ 	 * @ticket BP7302
+	 */
+	public function test_groups_get_group_accept_array() {
+		$g1 = self::factory()->group->create();
+		$group = groups_get_group( array( 'group_id' => $g1 ) );
+
+		$this->assertEquals( $g1, $group->id );
+	}
+
+	/**
+	 * @group groups_get_group
+ 	 * @ticket BP7302
+	 */
+	public function test_groups_get_group_accept_query_string() {
+		$g1 = self::factory()->group->create();
+		$group = groups_get_group( 'group_id=' . $g1 );
+
+		$this->assertEquals( $g1, $group->id );
+	}
+
+	/**
+	 * @expectedDeprecated groups_edit_base_group_details
+	 * @group groups_edit_base_group_details
+	 */
+	public function test_groups_edit_base_group_details_test_backcompat_arguments() {
+		$g1 = self::factory()->group->create();
+		$name = 'Great Scott';
+		$description = 'A must-see in time for the holidays!';
+		groups_edit_base_group_details( $g1, $name, $description, false );
+
+		$expected = array(
+			'id'          => $g1,
+			'name'        => $name,
+			'description' => $description
+		);
+		$updated_group_object = groups_get_group( $g1 );
+		$updated = array(
+			'id'          => $updated_group_object->id,
+			'name'        => $updated_group_object->name,
+			'description' => $updated_group_object->description
+		);
+
+		$this->assertEqualSets( $expected, $updated );
+	}
+
+	/**
+	 * @group groups_edit_base_group_details
+	 */
+	public function test_groups_edit_base_group_details_test_new_arguments() {
+		$g1 = self::factory()->group->create();
+		$name = 'Great Scott';
+		$slug = 'what-about-it';
+		$description = 'A must-see in time for the holidays!';
+		groups_edit_base_group_details( array(
+				'group_id'       => $g1,
+				'name'           => $name,
+				'slug'           => $slug,
+				'description'    => $description,
+				'notify_members' => false,
+		) );
+
+		$expected = array(
+			'id'          => $g1,
+			'slug'        => $slug,
+			'name'        => $name,
+			'description' => $description
+		);
+		$updated_group_object = groups_get_group( $g1 );
+		$updated = array(
+			'id'          => $updated_group_object->id,
+			'slug'        => $updated_group_object->slug,
+			'name'        => $updated_group_object->name,
+			'description' => $updated_group_object->description
+		);
+
+		$this->assertEqualSets( $expected, $updated );
+	}
+
+	/**
+	 * @group groups_edit_base_group_details
+	 */
+	public function test_groups_edit_base_group_details_avoid_slug_collisions() {
+		$slug = 'circe';
+		$g1 = self::factory()->group->create( array( 'slug' => $slug ) );
+		$g2 = self::factory()->group->create( array( 'slug' => 'loom' ) );
+
+		// Attempt to use a duplicate slug.
+		groups_edit_base_group_details( array(
+				'group_id'       => $g2,
+				'slug'           => $slug,
+		) );
+
+		$updated_group_object = groups_get_group( $g2 );
+
+		$this->assertNotEquals( $slug, $updated_group_object->slug );
+	}
+
+	/**
+	 * @group groups_edit_base_group_details
+	 */
+	public function test_groups_edit_base_group_details_slug_no_change() {
+		$slug = 'circe';
+		$g1 = self::factory()->group->create( array( 'slug' => $slug ) );
+
+		// Make sure the slug doesn't get incremented when there's no change.
+		groups_edit_base_group_details( array(
+				'group_id'       => $g1,
+				'slug'           => $slug,
+		) );
+
+		$updated_group_object = groups_get_group( $g1 );
+
+		$this->assertEquals( $slug, $updated_group_object->slug );
+	}
+
+	/**
+	 * @group groups_edit_base_group_details
+	 */
+	public function test_groups_edit_base_group_details_slug_null_value() {
+		$slug = 'circe';
+		$g1 = self::factory()->group->create( array( 'slug' => $slug ) );
+
+		// Make sure the slug doesn't get changed when null is passed.
+		groups_edit_base_group_details( array(
+				'group_id'       => $g1,
+				'slug'           => null,
+		) );
+
+		$updated_group_object = groups_get_group( $g1 );
+
+		$this->assertEquals( $slug, $updated_group_object->slug );
+	}
+
+	/**
+	 * @group groups_get_id_by_previous_slug
+	 */
+	public function test_groups_get_id_by_previous_slug() {
+		$slug = 'circe';
+		$g1 = self::factory()->group->create( array( 'slug' => $slug ) );
+		$g2 = self::factory()->group->create( array( 'slug' => 'loom' ) );
+
+		groups_edit_base_group_details( array(
+			'group_id'       => $g1,
+			'slug'           => 'newslug',
+		) );
+
+		// Function should return the group ID as an integer.
+		$this->assertSame( $g1, groups_get_id_by_previous_slug( $slug ) );
+	}
+
+	/**
+	 * @group groups_get_id_by_previous_slug
+	 */
+	public function test_groups_get_id_by_previous_slug_null_no_results() {
+		$this->assertNull( groups_get_id_by_previous_slug( 'woohoo' ) );
+	}
+
+	/**
+	 * @ticket BP7820
+	 * @ticket BP7698
+	 */
+	public function test_bp_groups_memberships_personal_data_exporter() {
+		groups_join_group( self::$group_ids[0], self::$user_ids[0] );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_groups_memberships_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 1, $actual['data'] );
+		$this->assertSame( 'bp-group-membership-' . self::$group_ids[0], $actual['data'][0]['item_id'] );
+	}
+
+	/**
+	 * @ticket BP7820
+	 * @ticket BP7698
+	 */
+	public function test_bp_groups_pending_requests_personal_data_exporter() {
+		groups_send_membership_request( self::$user_ids[0], self::$group_ids[0] );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_groups_pending_requests_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 1, $actual['data'] );
+		$this->assertSame( 'bp-group-pending-request-' . self::$group_ids[0], $actual['data'][0]['item_id'] );
+	}
+
+	/**
+	 * @ticket BP7820
+	 * @ticket BP7698
+	 */
+	public function test_bp_groups_pending_sent_invitations_personal_data_exporter() {
+		groups_invite_user( array(
+			'user_id'    => self::$user_ids[1],
+			'group_id'   => self::$group_ids[0],
+			'inviter_id' => self::$user_ids[0],
+		) );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_groups_pending_sent_invitations_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 1, $actual['data'] );
+		$this->assertSame( 'bp-group-pending-sent-invitation-' . self::$group_ids[0], $actual['data'][0]['item_id'] );
+	}
+
+	/**
+	 * @ticket BP7820
+	 * @ticket BP7698
+	 */
+	public function test_bp_groups_pending_received_invitations_personal_data_exporter() {
+		groups_invite_user( array(
+			'user_id'    => self::$user_ids[0],
+			'group_id'   => self::$group_ids[0],
+			'inviter_id' => self::$user_ids[1],
+		) );
+
+		$test_user = new WP_User( self::$user_ids[0] );
+
+		$actual = bp_groups_pending_received_invitations_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+		$this->assertCount( 1, $actual['data'] );
+		$this->assertSame( 'bp-group-pending-received-invitation-' . self::$group_ids[0], $actual['data'][0]['item_id'] );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/bpGetUserGroups.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/bpGetUserGroups.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/bpGetUserGroups.php	(revision 12265)
@@ -0,0 +1,565 @@
+<?php
+
+/**
+ * @group groups
+ */
+class BP_Tests_Groups_Functions_BpGetUserGroups extends BP_UnitTestCase {
+	static $user;
+	static $admin_user;
+	static $groups;
+
+	public function setUp() {
+		parent::setUp();
+		groups_remove_member( self::$user, self::$groups[2] );
+	}
+
+	public static function wpSetUpBeforeClass( $f ) {
+		$f = new BP_UnitTest_Factory();
+
+		self::$user = $f->user->create( array(
+			'user_login' => 'bp_get_user_groups_user',
+			'user_email' => 'bp_get_user_groups_user@example.com',
+		) );
+		self::$admin_user = $f->user->create( array(
+			'user_login' => 'bp_get_user_groups_admin_user',
+			'user_email' => 'bp_get_user_groups_admin_user@example.com',
+		) );
+		self::$groups = $f->group->create_many( 4, array(
+			'creator_id' => self::$admin_user,
+		) );
+
+		$now = time();
+		self::add_user_to_group( self::$user, self::$groups[1], array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 10 ),
+		) );
+		self::add_user_to_group( self::$user, self::$groups[0], array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		self::commit_transaction();
+	}
+
+	public static function tearDownAfterClass() {
+		foreach ( self::$groups as $group ) {
+			groups_delete_group( $group );
+		}
+
+		if ( is_multisite() ) {
+			wpmu_delete_user( self::$user );
+			wpmu_delete_user( self::$admin_user );
+		} else {
+			wp_delete_user( self::$user );
+			wp_delete_user( self::$admin_user );
+		}
+
+		self::commit_transaction();
+	}
+
+	public function test_default_params() {
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( array( self::$groups[0], self::$groups[1] ), wp_list_pluck( $found, 'group_id' ) );
+
+		foreach ( $found as $index => $f ) {
+			$this->assertInternalType( 'int', $index );
+			$this->assertInternalType( 'object', $f );
+			$this->assertInternalType( 'int', $f->group_id );
+			$this->assertSame( $index, $f->group_id );
+		}
+	}
+
+	public function test_is_confirmed_true() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_confirmed' => false,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => true,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_confirmed_false() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_confirmed' => false,
+		) );
+
+		$expected = array( self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => false,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_confirmed_null() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_confirmed' => false,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_banned_true() {
+		$this->add_user_to_group( self::$user, self::$groups[2] );
+		$member = new BP_Groups_Member( self::$user, self::$groups[2] );
+		$member->ban();
+
+		$expected = array( self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_banned' => true,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_banned_false() {
+		$this->add_user_to_group( self::$user, self::$groups[2] );
+		$member = new BP_Groups_Member( self::$user, self::$groups[2] );
+		$member->ban();
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_banned' => false,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_banned_null() {
+		$this->add_user_to_group( self::$user, self::$groups[2] );
+		$member = new BP_Groups_Member( self::$user, self::$groups[2] );
+		$member->ban();
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_banned' => null,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_admin_true() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_admin' => true,
+		) );
+
+		$expected = array( self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_admin' => true,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_admin_false() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_admin' => true,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_admin' => false,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_admin_null() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_admin' => false,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_admin' => null,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_mod_true() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_mod' => true,
+		) );
+
+		$expected = array( self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_mod' => true,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_mod_false() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_mod' => true,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_mod' => false,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_is_mod_null() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'is_mod' => false,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_mod' => null,
+		) );
+
+		$this->assertEqualSets( $expected, wp_list_pluck( $found, 'group_id' ) );
+	}
+
+	public function test_orderby_should_default_to_group_id() {
+		$expected = bp_get_user_groups( self::$user );
+		$found = bp_get_user_groups( self::$user, array(
+			'orderby' => 'group_id',
+		) );
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	public function test_orderby_group_id() {
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'orderby' => 'group_id',
+		) );
+
+		$this->assertSame( $expected, array_keys( $found ) );
+	}
+
+	public function test_orderby_id() {
+		$expected = array( self::$groups[1], self::$groups[0] );
+		$found = bp_get_user_groups( self::$user, array(
+			'orderby' => 'id',
+		) );
+
+		$this->assertSame( $expected, array_values( wp_list_pluck( $found, 'group_id' ) ) );
+	}
+
+	public function test_orderby_date_modified() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'date_modified' => '2015-03-14 09:26:53',
+		) );
+
+		$expected = array( self::$groups[2], self::$groups[1], self::$groups[0] );
+		$found = bp_get_user_groups( self::$user, array(
+			'orderby' => 'date_modified',
+		) );
+
+		$this->assertSame( $expected, array_values( wp_list_pluck( $found, 'group_id' ) ) );
+	}
+
+	public function test_orderby_group_id_with_order_desc() {
+		$expected = array( self::$groups[1], self::$groups[0] );
+		$found = bp_get_user_groups( self::$user, array(
+			'orderby' => 'group_id',
+			'order' => 'DESC',
+		) );
+
+		$this->assertSame( $expected, array_keys( $found ) );
+	}
+
+	public function test_orderby_date_modified_with_order_desc() {
+		$this->add_user_to_group( self::$user, self::$groups[2], array(
+			'date_modified' => '2015-03-14 09:26:53',
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'orderby' => 'date_modified',
+			'order' => 'DESC',
+		) );
+
+		$this->assertSame( $expected, array_values( wp_list_pluck( $found, 'group_id' ) ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_results_should_be_cached() {
+		global $wpdb;
+
+		$g1 = bp_get_user_groups( self::$user );
+
+		$num_queries = $wpdb->num_queries;
+		$g2 = bp_get_user_groups( self::$user );
+
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+		$this->assertEquals( $g1, $g2 );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_join() {
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		groups_join_group( self::$groups[2], self::$user );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_leave() {
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		groups_leave_group( self::$groups[1], self::$user );
+
+		$expected = array( self::$groups[0] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_remove() {
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->remove();
+
+		$expected = array( self::$groups[0] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_ban() {
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->ban();
+
+		$expected = array( self::$groups[0] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_unban() {
+		$m1 = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m1->ban();
+
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		$m2 = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m2->unban();
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_invite() {
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[2],
+			'inviter_id' => self::$admin_user,
+		) );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_uninvite() {
+		groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[2],
+			'inviter_id' => self::$admin_user,
+		) );
+
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		groups_uninvite_user( self::$user, self::$groups[2] );
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_invite_acceptance() {
+		groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[2],
+			'inviter_id' => self::$admin_user,
+		) );
+
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		groups_accept_invite( self::$user, self::$groups[2] );
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_invite_reject() {
+		groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[2],
+			'inviter_id' => self::$admin_user,
+		) );
+
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		groups_reject_invite( self::$user, self::$groups[2] );
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_invite_delete() {
+		groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[2],
+			'inviter_id' => self::$admin_user,
+		) );
+
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		groups_delete_invite( self::$user, self::$groups[2] );
+
+		$expected = array( self::$groups[0], self::$groups[1] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_request() {
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		// For `wp_mail()`.
+		$server_name = isset( $_SERVER['SERVER_NAME'] ) ? $_SERVER['SERVER_NAME'] : null;
+		$_SERVER['SERVER_NAME'] = '';
+
+		groups_send_membership_request( self::$user, self::$groups[2] );
+
+		// For `wp_mail()`.
+		if ( is_null( $server_name ) ) {
+			unset( $_SERVER['SERVER_NAME'] );
+		} else {
+			$_SERVER['SERVER_NAME'] = $server_name;
+		}
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user, array(
+			'is_confirmed' => null,
+		) );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_should_be_invalidated_on_group_request_acceptance() {
+		// For `wp_mail()`.
+		$server_name = isset( $_SERVER['SERVER_NAME'] ) ? $_SERVER['SERVER_NAME'] : null;
+		$_SERVER['SERVER_NAME'] = '';
+
+		groups_send_membership_request( self::$user, self::$groups[2] );
+
+		// Populate cache.
+		$g1 = bp_get_user_groups( self::$user );
+
+		$m = new BP_Groups_Member( self::$user, self::$groups[2] );
+
+		groups_accept_membership_request( $m->id, self::$user, self::$groups[2] );
+
+		// For `wp_mail()`.
+		if ( is_null( $server_name ) ) {
+			unset( $_SERVER['SERVER_NAME'] );
+		} else {
+			$_SERVER['SERVER_NAME'] = $server_name;
+		}
+
+		$expected = array( self::$groups[0], self::$groups[1], self::$groups[2] );
+		$found = bp_get_user_groups( self::$user );
+
+		$this->assertEqualSets( $expected, array_keys( $found ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsCreateGroup.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsCreateGroup.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsCreateGroup.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Tests for the `groups_create_group()` function.
+ *
+ * @group groups
+ */
+class BP_Tests_Groups_Functions_GroupsCreateGroup extends BP_UnitTestCase {
+	static $user_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$user_id = $factory->user->create();
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$user_id );
+	}
+
+	/**
+	 * @ticket BP7619
+	 */
+	public function test_should_respect_creator_id() {
+		$old_user_id = bp_loggedin_user_id();
+		$this->set_current_user( self::$user_id );
+
+		$group_id = groups_create_group( array(
+			'name' => 'Foo',
+			'creator_id' => self::$user_id + 1,
+		) );
+
+		$group = groups_get_group( $group_id );
+
+		$this->set_current_user( $old_user_id );
+
+		$this->assertSame( self::$user_id + 1, $group->creator_id );
+	}
+
+	/**
+	 * @ticket BP7619
+	 */
+	public function test_creator_id_should_be_fall_back_to_loggedin_user_for_new_group() {
+		$old_user_id = bp_loggedin_user_id();
+		$this->set_current_user( self::$user_id );
+
+		$group_id = groups_create_group( array(
+			'name' => 'Foo',
+		) );
+
+		$group = groups_get_group( $group_id );
+
+		$this->set_current_user( $old_user_id );
+
+		$this->assertSame( self::$user_id, $group->creator_id );
+	}
+
+	/**
+	 * @ticket BP7619
+	 */
+	public function test_creator_id_should_be_fall_back_to_existing_creator_id_for_existing_group() {
+		$group_id = self::factory()->group->create( array(
+			'creator_id' => self::$user_id + 1,
+		) );
+
+		$old_user_id = bp_loggedin_user_id();
+		$this->set_current_user( self::$user_id );
+
+		$group_id = groups_create_group( array(
+			'group_id' => $group_id,
+		) );
+
+		$group = groups_get_group( $group_id );
+
+		$this->set_current_user( $old_user_id );
+
+		$this->assertSame( self::$user_id + 1, $group->creator_id );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsIsUser.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsIsUser.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsIsUser.php	(revision 12265)
@@ -0,0 +1,221 @@
+<?php
+
+/**
+ * Tests for the `groups_is_user_*()` functions.
+ *
+ * @group groups
+ */
+class BP_Tests_Groups_Functions_GroupsIsUser extends BP_UnitTestCase {
+	static $user;
+	static $admin_user;
+	static $groups;
+
+	public static function wpSetUpBeforeClass( $f ) {
+		self::$user = $f->user->create( array(
+			'user_login' => 'groups_is_user',
+			'user_email' => 'groups_is_user@example.com',
+		) );
+		self::$admin_user = $f->user->create( array(
+			'user_login' => 'groups_is_user_admin',
+			'user_email' => 'groups_is_user_admin@example.com',
+		) );
+		self::$groups = $f->group->create_many( 3, array(
+			'creator_id' => self::$admin_user,
+		) );
+
+		$now = time();
+
+		self::commit_transaction();
+	}
+
+	public static function tearDownAfterClass() {
+		foreach ( self::$groups as $group ) {
+			groups_delete_group( $group );
+		}
+
+		if ( is_multisite() ) {
+			wpmu_delete_user( self::$user );
+			wpmu_delete_user( self::$admin_user );
+		} else {
+			wp_delete_user( self::$user );
+			wp_delete_user( self::$admin_user );
+		}
+
+		self::commit_transaction();
+	}
+
+	public function test_groups_is_user_admin_expected_true() {
+		$this->add_user_to_group( self::$user, self::$groups[0], array(
+			'is_admin' => false,
+		) );
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_admin' => true,
+		) );
+
+		$this->assertNotEmpty( groups_is_user_admin( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_admin_expected_false() {
+		$this->add_user_to_group( self::$user, self::$groups[0], array(
+			'is_admin' => false,
+		) );
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_admin' => true,
+		) );
+
+		$this->assertEquals( false, groups_is_user_admin( self::$user, self::$groups[0] ) );
+	}
+
+	public function test_groups_is_user_mod_expected_true() {
+		$this->add_user_to_group( self::$user, self::$groups[0], array(
+			'is_mod' => false,
+		) );
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_mod' => true,
+		) );
+
+		$this->assertNotEmpty( groups_is_user_mod( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_mod_expected_false() {
+		$this->add_user_to_group( self::$user, self::$groups[0], array(
+			'is_mod' => false,
+		) );
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_mod' => true,
+		) );
+
+		$this->assertEquals( false, groups_is_user_mod( self::$user, self::$groups[0] ) );
+	}
+
+	public function test_groups_is_user_mod_should_return_false_when_user_is_also_banned() {
+		$this->add_user_to_group( self::$user, self::$groups[0], array(
+			'is_mod' => false,
+		) );
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_mod' => true,
+		) );
+
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->ban();
+
+		$this->assertEquals( false, groups_is_user_mod( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_member_expected_true() {
+		$this->add_user_to_group( self::$user, self::$groups[1] );
+
+		$this->assertNotEmpty( groups_is_user_member( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_member_should_return_true_for_admin() {
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_admin' => true,
+		) );
+
+		$this->assertNotEmpty( groups_is_user_member( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_member_should_return_true_for_mod() {
+		$this->add_user_to_group( self::$user, self::$groups[1], array(
+			'is_mod' => true,
+		) );
+
+		$this->assertNotEmpty( groups_is_user_member( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_member_expected_false() {
+		$this->add_user_to_group( self::$user, self::$groups[1] );
+
+		$this->assertEquals( false, groups_is_user_member( self::$user, self::$groups[0] ) );
+	}
+
+	public function test_groups_is_user_member_should_return_false_when_user_is_also_banned() {
+		$this->add_user_to_group( self::$user, self::$groups[1] );
+
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->ban();
+
+		$this->assertEquals( false, groups_is_user_member( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_banned_should_return_false_for_non_member() {
+		$this->assertEquals( false, groups_is_user_banned( self::$user, self::$groups[1] ) );
+	}
+
+	/**
+	 * Return values for these functions are terrible.
+	 */
+	public function test_groups_is_user_banned_should_return_false_for_non_banned_member() {
+		$this->add_user_to_group( self::$user, self::$groups[1] );
+		$this->assertEquals( 0, groups_is_user_banned( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_banned_should_return_true_for_banned_member() {
+		$this->add_user_to_group( self::$user, self::$groups[1] );
+
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->ban();
+
+		$this->assertNotEmpty( groups_is_user_banned( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_invited_should_return_false_for_confirmed_member() {
+		$this->add_user_to_group( self::$user, self::$groups[1] );
+		$this->assertEquals( false, groups_is_user_invited( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_invited_should_return_false_for_uninvited_member() {
+		$this->assertEquals( false, groups_is_user_invited( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_invited_should_return_true_for_invited_member() {
+		$i = groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[1],
+			'inviter_id' => 123,
+		) );
+
+		// Send invite.
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->invite_sent = 1;
+		$m->save();
+
+		$this->assertNotEmpty( groups_is_user_invited( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_pending_should_return_false_for_pending_member() {
+		groups_invite_user( array(
+			'user_id' => self::$user,
+			'group_id' => self::$groups[1],
+			'inviter_id' => 123,
+		) );
+
+		// Send invite.
+		$m = new BP_Groups_Member( self::$user, self::$groups[1] );
+		$m->invite_sent = 1;
+		$m->save();
+
+		$this->assertEquals( false, groups_is_user_pending( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_pending_should_return_false_for_member_with_no_request() {
+		$this->assertEquals( false, groups_is_user_pending( self::$user, self::$groups[1] ) );
+	}
+
+	public function test_groups_is_user_pending_should_return_true_for_pending_member() {
+
+		$m                = new BP_Groups_Member;
+		$m->group_id      = self::$groups[1];
+		$m->user_id       = self::$user;
+		$m->inviter_id    = 0;
+		$m->is_admin      = 0;
+		$m->user_title    = '';
+		$m->date_modified = bp_core_current_time();
+		$m->is_confirmed  = 0;
+		$m->comments      = 'request';
+		$m->save();
+
+		$this->assertNotEmpty( groups_is_user_pending( self::$user, self::$groups[1] ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsTotalGroupsForUser.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsTotalGroupsForUser.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/functions/groupsTotalGroupsForUser.php	(revision 12265)
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @group groups
+ * @covers ::groups_total_groups_for_user
+ */
+class BP_Tests_Groups_Functions_GroupsTotalGroupsForUser extends BP_UnitTestCase {
+	/**
+	 * @ticket BP6813
+	 */
+	public function test_should_return_integer() {
+		$this->assertInternalType( 'int', groups_total_groups_for_user( 123 ) );
+	}
+
+	/**
+	 * @ticket BP6813
+	 */
+	public function test_should_return_integer_when_fetching_from_cache() {
+		/*
+		 * Put a string in the cache.
+		 * In-memory cache will respect type, but persistent caching engines return all scalars as strings.
+		 */
+		wp_cache_set( 'bp_total_groups_for_user_123', '321', 'bp' );
+
+		$this->assertInternalType( 'int', groups_total_groups_for_user( 123 ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/notifications.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/notifications.php	(revision 12265)
@@ -0,0 +1,261 @@
+<?php
+
+/**
+ * @group groups
+ * @group notifications
+ */
+class BP_Tests_Groups_Notifications extends BP_UnitTestCase {
+	protected $filter_fired;
+	protected $current_user;
+	protected $requesting_user_id;
+	protected $group;
+
+	public function setUp() {
+		parent::setUp();
+		$this->current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create() );
+
+		$this->requesting_user_id = self::factory()->user->create();
+		$this->group = self::factory()->group->create();
+		$this->filter_fired = '';
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->current_user );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_multiple_new_membership_requests_notification() {
+		add_filter( 'bp_groups_multiple_new_membership_requests_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'new_membership_request', $this->group, $this->requesting_user_id, 5 );
+		remove_filter( 'bp_groups_multiple_new_membership_requests_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_multiple_new_membership_requests_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_single_new_membership_request_notification() {
+		add_filter( 'bp_groups_single_new_membership_request_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'new_membership_request', $this->group, 0, 1 );
+		remove_filter( 'bp_groups_single_new_membership_request_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_single_new_membership_request_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_multiple_membership_request_accepted_notification() {
+		add_filter( 'bp_groups_multiple_membership_request_accepted_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'membership_request_accepted', $this->group, 0, 5 );
+		remove_filter( 'bp_groups_multiple_membership_request_accepted_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_multiple_membership_request_accepted_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_single_membership_request_accepted_notification() {
+		add_filter( 'bp_groups_single_membership_request_accepted_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'membership_request_accepted', $this->group, 0, 1 );
+		remove_filter( 'bp_groups_single_membership_request_accepted_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_single_membership_request_accepted_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_multiple_membership_request_rejected_notification() {
+		add_filter( 'bp_groups_multiple_membership_request_rejected_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'membership_request_rejected', $this->group, 0, 5 );
+		remove_filter( 'bp_groups_multiple_membership_request_rejected_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_multiple_membership_request_rejected_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_single_membership_request_rejected_notification() {
+		add_filter( 'bp_groups_single_membership_request_rejected_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'membership_request_rejected', $this->group, 0, 1 );
+		remove_filter( 'bp_groups_single_membership_request_rejected_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_single_membership_request_rejected_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_multiple_member_promoted_to_admin_notification() {
+		add_filter( 'bp_groups_multiple_member_promoted_to_admin_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'member_promoted_to_admin', $this->group, 0, 5 );
+		remove_filter( 'bp_groups_multiple_member_promoted_to_admin_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_multiple_member_promoted_to_admin_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_single_member_promoted_to_admin_notification() {
+		add_filter( 'bp_groups_single_member_promoted_to_admin_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'member_promoted_to_admin', $this->group, 0, 1 );
+		remove_filter( 'bp_groups_single_member_promoted_to_admin_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_single_member_promoted_to_admin_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_multiple_member_promoted_to_mod_notification() {
+		add_filter( 'bp_groups_multiple_member_promoted_to_mod_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'member_promoted_to_mod', $this->group, 0, 5 );
+		remove_filter( 'bp_groups_multiple_member_promoted_to_mod_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_multiple_member_promoted_to_mod_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_single_member_promoted_to_mod_notification() {
+		add_filter( 'bp_groups_single_member_promoted_to_mod_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'member_promoted_to_mod', $this->group, 0, 1 );
+		remove_filter( 'bp_groups_single_member_promoted_to_mod_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_single_member_promoted_to_mod_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_multiple_group_invite_notification() {
+		add_filter( 'bp_groups_multiple_group_invite_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'group_invite', $this->group, 0, 5 );
+		remove_filter( 'bp_groups_multiple_group_invite_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_multiple_group_invite_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group groups_format_notifications
+	 */
+	public function test_groups_format_notifications_bp_groups_single_group_invite_notification() {
+		add_filter( 'bp_groups_single_group_invite_notification', array( $this, 'notification_filter_callback' ) );
+		$n = groups_format_notifications( 'group_invite', $this->group, 0, 1 );
+		remove_filter( 'bp_groups_single_group_invite_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_groups_single_group_invite_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group bp_groups_delete_promotion_notifications
+	 */
+	public function test_bp_groups_delete_promotion_notifications() {
+		// Dummy group and user IDs
+		$u = 5;
+		$g = 12;
+
+		// Admin
+		$n = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'item_id' => $g,
+			'component_action' => 'member_promoted_to_admin',
+		) );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $n ), wp_list_pluck( $notifications, 'id' ) );
+
+		// fire the hook
+		do_action( 'groups_demoted_member', $u, $g );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+		) );
+
+		$this->assertEmpty( $notifications );
+
+		// Mod
+		$n = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'item_id' => $g,
+			'component_action' => 'member_promoted_to_mod',
+		) );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+		) );
+
+		// Double check it's there
+		$this->assertEquals( array( $n ), wp_list_pluck( $notifications, 'id' ) );
+
+		// fire the hook
+		do_action( 'groups_demoted_member', $u, $g );
+
+		$notifications = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+		) );
+
+		$this->assertEmpty( $notifications );
+	}
+
+	public function notification_filter_callback( $value ) {
+		$this->filter_fired = current_filter();
+		return $value;
+	}
+
+	/**
+	 * @group BP7375
+	 */
+	public function test_membership_request_notifications_should_be_cleared_when_request_is_accepted() {
+		$users = self::factory()->user->create_many( 3 );
+
+		$this->add_user_to_group( $users[0], $this->group, array(
+			'is_admin' => 1,
+		) );
+		$this->add_user_to_group( $users[1], $this->group, array(
+			'is_admin' => 1,
+		) );
+
+		groups_send_membership_request( $users[2], $this->group );
+
+		// Both admins should get a notification.
+		$get_args = array(
+			'user_id' => $users[0],
+			'item_id' => $this->group,
+			'secondary_item_id' => $users[2],
+			'component_action' => 'new_membership_request',
+			'is_new' => true,
+		);
+		$u0_notifications = BP_Notifications_Notification::get( $get_args );
+		$u1_notifications = BP_Notifications_Notification::get( $get_args );
+		$this->assertNotEmpty( $u0_notifications );
+		$this->assertNotEmpty( $u1_notifications );
+
+		$this->assertTrue( groups_invite_user( array(
+			'user_id' => $users[2],
+			'group_id' => $this->group,
+		) ) );
+
+		$u0_notifications = BP_Notifications_Notification::get( $get_args );
+		$u1_notifications = BP_Notifications_Notification::get( $get_args );
+		$this->assertEmpty( $u0_notifications );
+		$this->assertEmpty( $u1_notifications );
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/template.php	(revision 12265)
@@ -0,0 +1,1249 @@
+<?php
+/**
+ * @group groups
+ * @group template
+ */
+class BP_Tests_Groups_Template extends BP_UnitTestCase {
+	/**
+	 * Integration test to make sure meta_query is getting passed through
+	 *
+	 * @group bp_has_groups
+	 */
+	public function test_bp_has_groups_with_meta_query() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create();
+		groups_update_groupmeta( $g1, 'foo', 'bar' );
+
+		global $groups_template;
+		bp_has_groups( array(
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$ids = wp_list_pluck( $groups_template->groups, 'id' );
+		$this->assertEquals( $ids, array( $g1, ) );
+	}
+
+	/**
+	 * Integration test to make sure order and orderby are interpreted when
+	 * no 'type' value has been passed
+	 *
+	 * @group bp_has_groups
+	 */
+	public function test_bp_has_groups_with_order_orderby_with_null_type() {
+		$now = time();
+		$g1 = self::factory()->group->create( array(
+			'name' => 'AAAAA',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $now - 100 ),
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 1000000 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'BBBBB',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $now - 1000000 ),
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 10000 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'CCCCC',
+			'date_created' => gmdate( 'Y-m-d H:i:s', $now - 10000 ),
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 10 ),
+		) );
+
+		global $groups_template;
+		bp_has_groups( array(
+			'order' => 'ASC',
+			'orderby' => 'name',
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEquals( array( $g1, $g2, $g3, ), $ids );
+	}
+
+	/**
+	 * Integration test to make sure 'order' is set to 'DESC' and 'orderby'
+	 * to 'last_activity' when no type or order/orderby params are passed.
+	 * This ensures backpat with the old system, where 'active' was the
+	 * default type param, and there were no order/orderby params.
+	 *
+	 * @group bp_has_groups
+	 */
+	public function test_bp_has_groups_defaults_to_DESC_last_activity_for_default_type_active_backpat() {
+		$now = time();
+		$g1 = self::factory()->group->create( array(
+			'name' => 'AAAAA',
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$g2 = self::factory()->group->create( array(
+			'name' => 'BBBBB',
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 1000000 ),
+		) );
+		$g3 = self::factory()->group->create( array(
+			'name' => 'CCCCC',
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 10000 ),
+		) );
+
+		global $groups_template;
+		bp_has_groups();
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEquals( array( $g1, $g3, $g2, ), $ids );
+	}
+
+	/**
+	 * Test using the 'slug' parameter in bp_has_groups()
+	 *
+	 * Note: The 'slug' parameter currently also requires the 'type' to be set
+	 * to 'single-group'.
+	 *
+	 * @group bp_has_groups
+	 */
+	public function test_bp_has_groups_single_group_with_slug() {
+		$g1 = self::factory()->group->create( array(
+			'name' => 'Test Group',
+			'slug' => 'test-group',
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 100 ),
+		) );
+
+		global $groups_template;
+		bp_has_groups( array(
+			'type' => 'single-group',
+			'slug' => 'test-group',
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEquals( array( $g1 ), $ids );
+
+		$this->assertEquals( 1, $groups_template->group_count );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_bp_has_groups_parent_id() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		global $groups_template;
+		bp_has_groups( array(
+			'parent_id' => $g1,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEquals( array( $g2 ), $ids );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_bp_has_groups_parent_id_array() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		global $groups_template;
+		bp_has_groups( array(
+			'parent_id' => array( $g1, $g2 ),
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEqualSets( array( $g2, $g3 ), $ids );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_bp_has_groups_parent_id_comma_separated() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		global $groups_template;
+		bp_has_groups( array(
+			'parent_id' => "{$g1},{$g2}",
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEqualSets( array( $g2, $g3 ), $ids );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_bp_has_groups_parent_id_null() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		global $groups_template;
+		bp_has_groups( array(
+			'parent_id' => null,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEqualSets( array( $g1, $g2, $g3, $g4 ), $ids );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_bp_has_groups_parent_id_top_level_groups() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		global $groups_template;
+		bp_has_groups( array(
+			'parent_id' => 0,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEqualSets( array( $g1, $g4 ), $ids );
+	}
+
+	/**
+	 * @group hierarchical_groups
+	 */
+	public function test_bp_has_groups_parent_id_top_level_groups_using_false() {
+		$g1 = self::factory()->group->create();
+		$g2 = self::factory()->group->create( array(
+			'parent_id' => $g1,
+		) );
+		$g3 = self::factory()->group->create( array(
+			'parent_id' => $g2,
+		) );
+		$g4 = self::factory()->group->create();
+
+		global $groups_template;
+		bp_has_groups( array(
+			'parent_id' => false,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $groups_template->groups, 'id' ) );
+		$this->assertEqualSets( array( $g1, $g4 ), $ids );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_vanilla() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g  = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$this->add_user_to_group( $u1, $g );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude_admins_mods' => false,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u1, ), $ids );
+	}
+
+	/**
+	 * Switching from BP_Groups_Member to BP_Group_Member_Query meant a
+	 * change in the format of the values returned from the query. For
+	 * backward compatibility, we translate some of the return values
+	 * of BP_Group_Member_Query to the older format. This test makes sure
+	 * that the translation happens properly.
+	 *
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_backpat_retval_format() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'creator_id' => $u2 ) );
+
+		$date_modified = gmdate( 'Y-m-d H:i:s', time() - 100 );
+
+		$this->add_user_to_group( $u1, $g, array( 'date_modified' => $date_modified ) );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+		) );
+
+		$u1_object = new WP_User( $u1 );
+
+		$expected = new stdClass;
+		$expected->user_id = $u1;
+		$expected->date_modified = $date_modified;
+		$expected->is_banned = 0;
+		$expected->user_login = $u1_object->user_login;
+		$expected->user_nicename = $u1_object->user_nicename;
+		$expected->user_email = $u1_object->user_email;
+		$expected->display_name = $u1_object->display_name;
+
+		// In order to use assertEquals, we need to discard the
+		// irrelevant properties of the found object. Hack alert
+		$found = new stdClass;
+		foreach ( array( 'user_id', 'date_modified', 'is_banned', 'user_login', 'user_nicename', 'user_email', 'display_name' ) as $key ) {
+			if ( isset( $members_template->members[0]->{$key} ) ) {
+				$found->{$key} = $members_template->members[0]->{$key};
+			}
+		}
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_per_page() {
+		$g = self::factory()->group->create();
+
+		$users = array();
+		for ( $i = 1; $i <= 5; $i++ ) {
+			$users[ $i ] = self::factory()->user->create();
+		}
+
+		$expected = array();
+		$now = time();
+		for ( $i = 3; $i <= 5; $i++ ) {
+			$this->add_user_to_group( $users[ $i ], $g, array(
+				'date_modified' => date( 'Y-m-d H:i:s', $now - ( 60 * $i ) ),
+			) );
+			$expected[] = $users[ $i ];
+		}
+
+		// hack it down to 2 (per page arg below)
+		$expected = array_slice( $expected, 0, 2 );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'per_page' => 2,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( $expected, $ids );
+	}
+
+	/**
+	 * Note: 'max' is a weird parameter. It just changes the member_count
+	 * in the global - not the sql query at all. I'm testing what it
+	 * appears to be designed to do, not what it feels like it ought to do
+	 * if it made any sense. Programming is fun, QED.
+	 *
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_max() {
+		$g = self::factory()->group->create();
+
+		$users = array();
+		for ( $i = 1; $i <= 5; $i++ ) {
+			$users[ $i ] = self::factory()->user->create();
+		}
+
+		$expected = array();
+		for ( $i = 3; $i <= 5; $i++ ) {
+			$this->add_user_to_group( $users[ $i ], $g );
+			$expected[] = $users[ $i ];
+		}
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'max' => 1,
+		) );
+
+		$this->assertEquals( 1, $members_template->member_count );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_exclude() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$this->add_user_to_group( $u1, $g );
+		$this->add_user_to_group( $u2, $g );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude' => $u1,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u2 ), $ids );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_exclude_admins_mods_1() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$this->add_user_to_group( $u1, $g );
+		$this->add_user_to_group( $u2, $g );
+		$this->add_user_to_group( $u3, $g );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude_admins_mods' => 1,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u3 ), $ids );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_exclude_admins_mods_0() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$now = time();
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 60 ),
+		) );
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 60*60 ),
+		) );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->promote( 'admin' );
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->promote( 'mod' );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude_admins_mods' => 0,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u1, $u2, $u3 ), $ids );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_exclude_banned_1() {
+		$g = self::factory()->group->create();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$this->add_user_to_group( $u1, $g );
+		$this->add_user_to_group( $u2, $g );
+
+		$m1 = new BP_Groups_Member( $u1, $g );
+		$m1->ban();
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude_banned' => 1,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u2, ), $ids );
+	}
+
+	/**
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_with_exclude_banned_0() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$now = time();
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*24 ),
+		) );
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*12 ),
+		) );
+
+		$m2 = new BP_Groups_Member( $u2, $g );
+		$m2->ban();
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude_banned' => 0,
+			'exclude_admins_mods' => false,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u1, $u3, $u2 ), $ids );
+	}
+
+	/**
+	 * Default sort order should be the joined date
+	 *
+	 * @tickett BP5106
+	 * @group bp_group_has_members
+	 */
+	public function test_bp_group_has_members_default_order() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 60 ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 600 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 6000 ),
+		) );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*24 ),
+		) );
+
+		$this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*12 ),
+		) );
+
+		global $members_template;
+		bp_group_has_members( array(
+			'group_id' => $g,
+			'exclude_banned' => 0,
+			'exclude_admins_mods' => false,
+		) );
+
+		$ids = wp_parse_id_list( wp_list_pluck( $members_template->members, 'user_id' ) );
+		$this->assertEquals( array( $u1, $u3, $u2, ), $ids );
+	}
+
+	/**
+	 * @group bp_group_has_invites
+	 * @group BP_Groups_Invite_Template
+	 */
+	public function test_bp_group_has_invites_template_structure() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 60 ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 600 ),
+		) );
+		$u3 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 6000 ),
+		) );
+		$u4 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 60000 ),
+		) );
+
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$m2 = $this->add_user_to_group( $u2, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*24 ),
+			'is_confirmed' => 0,
+			'inviter_id' => $u1,
+			'invite_sent' => true,
+		) );
+
+		$m3 = $this->add_user_to_group( $u3, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*12 ),
+			'is_confirmed' => 0,
+			'inviter_id' => $u1,
+			'invite_sent' => true,
+		) );
+
+		$m4 = $this->add_user_to_group( $u4, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $now - 60*60*36 ),
+			'is_confirmed' => 1,
+			'inviter_id' => $u1,
+			'invite_sent' => true,
+		) );
+
+		// Populate the global
+		bp_group_has_invites( array(
+			'group_id' => $g,
+			'user_id' => $u1,
+		) );
+
+		global $invites_template;
+
+		$found_users = array(
+			0 => $u2,
+			1 => $u3,
+		);
+
+		// Invites array
+		$this->assertEquals( $found_users, $invites_template->invites );
+
+		// Make sure user is set when loop starts
+		$counter = 0;
+		while ( bp_group_invites() ) : bp_group_the_invite();
+			$this->assertEquals( $g, $invites_template->invite->group_id );
+
+			$this_user = new BP_Core_User( $found_users[ $counter ] );
+			foreach ( get_object_vars( $this_user ) as $k => $v ) {
+				// Doesn't matter if the backpat provides *more*
+				// details than the old method, so we skip cases
+				// where the BP_Core_User value is empty
+				if ( empty( $v ) ) {
+					continue;
+				}
+
+				$this->assertEquals( $v, $invites_template->invite->user->{$k} );
+			}
+			$counter++;
+		endwhile;
+	}
+
+	/**
+	 * @group bp_group_has_invites
+	 * @group BP_Groups_Invite_Template
+	 */
+	public function test_bp_group_has_invites_pagination() {
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 60 ),
+		) );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$users = array();
+		$now = time();
+		for ( $i = 1; $i < 6; $i++ ) {
+			$users[ $i ] = self::factory()->user->create( array(
+				'last_activity' => gmdate( 'Y-m-d H:i:s', $now - $i ),
+			) );
+
+			$this->add_user_to_group( $users[ $i ], $g, array(
+				'date_modified' => gmdate( 'Y-m-d H:i:s', $now - $i ),
+				'is_confirmed' => 0,
+				'inviter_id' => $u1,
+				'invite_sent' => true,
+			) );
+		}
+
+		// Populate the global
+		bp_group_has_invites( array(
+			'group_id' => $g,
+			'user_id' => $u1,
+			'page' => 2,
+			'per_page' => 2,
+		) );
+
+		global $invites_template;
+
+		$this->assertEquals( array( $users[ 3 ], $users[ 2 ] ), $invites_template->invites );
+	}
+
+	/**
+	 * Checks for proper queried items
+	 *
+	 * @group bp_group_has_membership_requests
+	 * @group BP_Group_Membership_Requests_Template
+	 */
+	public function test_bp_group_has_membership_requests_results() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $now - 60 ),
+		) );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$users = array();
+		$memberships = array();
+		for ( $i = 1; $i < 5; $i++ ) {
+			$users[ $i ] = self::factory()->user->create( array(
+				'last_activity' => gmdate( 'Y-m-d H:i:s', $now - ( 100 - $i ) ),
+			) );
+
+			$memberships[ $i ] = $this->add_user_to_group( $users[ $i ], $g, array(
+				// this date_modified ensures that order will match
+				// id order. necessary due to a quirk in the legacy
+				// implementation
+				'date_modified' => gmdate( 'Y-m-d H:i:s', $now - ( 100 - $i ) ),
+				'is_confirmed' => 0,
+				'inviter_id' => 0,
+				'invite_sent' => false,
+			) );
+		}
+
+		// Fake the current group
+		global $groups_template;
+
+		if ( ! isset( $groups_template ) ) {
+			$groups_template = new stdClass;
+		}
+
+		if ( ! isset( $groups_template->group ) ) {
+			$groups_template->group = new stdClass;
+		}
+
+		$groups_template->group->id = $g;
+
+		// Populate the global
+		bp_group_has_membership_requests( array(
+			'group_id' => $g,
+			'per_page' => 3,
+		) );
+
+		global $requests_template;
+
+		$expected_user_ids = array();
+		$expected_mem_ids = array();
+		for ( $j = 1; $j <= 3; $j++ ) {
+			$expected_user_ids[] = (string) $users[ $j ];
+			$expected_mem_ids[] = (string) $memberships[ $j ];
+		}
+
+		$this->assertEquals( $expected_user_ids, wp_list_pluck( $requests_template->requests, 'user_id' ) );
+		$this->assertEquals( $expected_mem_ids, wp_list_pluck( $requests_template->requests, 'id' ) );
+	}
+
+	/**
+	 * Checks that the requests_template object is properly formatted
+	 *
+	 * @group bp_group_has_membership_requests
+	 * @group BP_Group_Membership_Requests_Template
+	 */
+	public function test_bp_group_has_membership_requests_format() {
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', time() - 60 ),
+		) );
+
+		$g = self::factory()->group->create( array(
+			'creator_id' => $u1,
+		) );
+
+		$time = time();
+
+		$user = self::factory()->user->create( array(
+			'last_activity' => gmdate( 'Y-m-d H:i:s', $time ),
+		) );
+
+		$membership = $this->add_user_to_group( $user, $g, array(
+			'date_modified' => gmdate( 'Y-m-d H:i:s', $time ),
+			'is_confirmed' => 0,
+			'inviter_id' => 0,
+			'invite_sent' => false,
+		) );
+
+		// Fake the current group
+		global $groups_template;
+
+		if ( ! isset( $groups_template ) ) {
+			$groups_template = new stdClass;
+		}
+
+		if ( ! isset( $groups_template->group ) ) {
+			$groups_template->group = new stdClass;
+		}
+
+		$groups_template->group->id = $g;
+
+		// Populate the global
+		bp_group_has_membership_requests( array(
+			'group_id' => $g,
+			'per_page' => 1,
+			'max' => 1,
+		) );
+
+		global $requests_template;
+
+		$expected = new stdClass;
+		$expected->id = $membership;
+		$expected->group_id = $g;
+		$expected->user_id = $user;
+		$expected->inviter_id = '0';
+		$expected->is_admin = '0';
+		$expected->is_mod = '0';
+		$expected->user_title = '';
+		$expected->date_modified = gmdate( 'Y-m-d H:i:s', $time );
+		$expected->comments = '';
+		$expected->is_confirmed = '0';
+		$expected->is_banned = '0';
+		$expected->invite_sent = '0';
+
+		// Check each expected value. If there are more in the results,
+		// that's OK
+		foreach ( get_object_vars( $expected ) as $k => $v ) {
+			$this->assertEquals( $v, $requests_template->requests[0]->{$k} );
+		}
+	}
+
+	/**
+	 * @group bp_group_is_user_banned
+	 */
+	public function test_bp_group_is_user_banned_in_groups_loop() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		$g1 = self::factory()->group->create( array(
+			'creator_id' => $u1,
+			'last_activity' => $now - 100,
+		) );
+		$g2 = self::factory()->group->create( array(
+			'creator_id' => $u2,
+			'last_activity' => $now - 200,
+		) );
+
+		$this->add_user_to_group( $u1, $g2, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+		$this->add_user_to_group( $u2, $g2, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 500 ),
+		) );
+		$this->add_user_to_group( $u1, $g2, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		// Ban user 1 from group 2
+		// Fool the admin check
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u1, $g2 );
+
+		// Start the groups loop
+		$this->set_current_user( $u1 );
+		if ( bp_has_groups() ) : while ( bp_groups() ) : bp_the_group();
+			$found[] = bp_group_is_user_banned();
+		endwhile; endif;
+
+		// Assert
+		$expected = array( false, true );
+		$this->assertEquals( $expected, $found );
+
+		// Clean up
+		$GLOBALS['groups_template'] = null;
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_group_is_user_banned
+	 */
+	public function test_bp_group_is_user_banned_not_in_groups_loop() {
+		$now = time();
+		$u1 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$u2 = self::factory()->user->create( array(
+			'last_activity' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
+		$g2 = self::factory()->group->create( array( 'creator_id' => $u2 ) );
+
+		$this->add_user_to_group( $u1, $g2, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+		$this->add_user_to_group( $u2, $g2, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 500 ),
+		) );
+		$this->add_user_to_group( $u1, $g2, array(
+			'date_modified' => date( 'Y-m-d H:i:s', $now - 50 ),
+		) );
+
+		// Ban user 1 from group 2
+		// Fool the admin check
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u1, $g2 );
+
+		// Do group ban checks
+		$group1 = new BP_Groups_Group( $g1 );
+		$group2 = new BP_Groups_Group( $g2 );
+
+		$found = array();
+		$found[] = bp_group_is_user_banned( $group1, $u1 );
+		$found[] = bp_group_is_user_banned( $group2, $u1 );
+
+		// Assert
+		$expected = array( false, true );
+		$this->assertEquals( $expected, $found );
+
+		// Clean up
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_get_group_member_count
+	 */
+	public function test_bp_get_group_member_count_0_members() {
+		global $groups_template;
+		$gt = $groups_template;
+		$groups_template = new stdClass;
+		$groups_template->group = new stdClass;
+		$groups_template->group->total_member_count = 0;
+
+		$found = bp_get_group_member_count();
+
+		$groups_template = $gt;
+
+		$this->assertSame( '0 members', $found );
+	}
+
+	/**
+	 * @group bp_get_group_member_count
+	 */
+	public function test_bp_get_group_member_count_1_member() {
+		global $groups_template;
+		$gt = $groups_template;
+		$groups_template = new stdClass;
+		$groups_template->group = new stdClass;
+		$groups_template->group->total_member_count = 1;
+
+		$found = bp_get_group_member_count();
+
+		$groups_template = $gt;
+
+		$this->assertSame( '1 member', $found );
+	}
+
+	/**
+	 * @group bp_get_group_member_count
+	 */
+	public function test_bp_get_group_member_count_2_members() {
+		global $groups_template;
+		$gt = $groups_template;
+		$groups_template = new stdClass;
+		$groups_template->group = new stdClass;
+		$groups_template->group->total_member_count = 2;
+
+		$found = bp_get_group_member_count();
+
+		$groups_template = $gt;
+
+		$this->assertSame( '2 members', $found );
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Template
+	 */
+	public function test_bp_groups_template_should_give_precedence_to_grpage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['grpage'] = '5';
+
+		$at = new BP_Groups_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Template
+	 */
+	public function test_bp_groups_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['grpage'] = '0';
+
+		$at = new BP_Groups_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Template
+	 */
+	public function test_bp_groups_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Groups_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Template
+	 */
+	public function test_bp_groups_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Groups_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Group_Members_Template
+	 */
+	public function test_bp_groups_group_members_template_should_give_precedence_to_mlpage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['mlpage'] = '5';
+
+		$at = new BP_Groups_Group_Members_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Group_Members_Template
+	 */
+	public function test_bp_groups_group_members_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['mlpage'] = '0';
+
+		$at = new BP_Groups_Group_Members_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Group_Members_Template
+	 */
+	public function test_bp_groups_group_members_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Groups_Group_Members_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Group_Members_Template
+	 */
+	public function test_bp_groups_group_members_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Groups_Group_Members_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Membership_Requests_Template
+	 */
+	public function test_bp_groups_membership_requests_template_should_give_precedence_to_mrpage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['mrpage'] = '5';
+
+		$at = new BP_Groups_Membership_Requests_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Membership_Requests_Template
+	 */
+	public function test_bp_groups_membership_requests_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['mrpage'] = '0';
+
+		$at = new BP_Groups_Membership_Requests_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Membership_Requests_Template
+	 */
+	public function test_bp_groups_membership_requests_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Groups_Membership_Requests_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Membership_Requests_Template
+	 */
+	public function test_bp_groups_membership_requests_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Groups_Membership_Requests_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Invite_Template
+	 */
+	public function test_bp_groups_invite_template_should_give_precedence_to_invitepage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['invitepage'] = '5';
+
+		$at = new BP_Groups_Invite_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Invite_Template
+	 */
+	public function test_bp_groups_invite_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['invitepage'] = '0';
+
+		$at = new BP_Groups_Invite_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Invite_Template
+	 */
+	public function test_bp_groups_invite_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Groups_Invite_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Groups_Invite_Template
+	 */
+	public function test_bp_groups_invite_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Groups_Invite_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/template/bpGroupStatusMessage.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/template/bpGroupStatusMessage.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/template/bpGroupStatusMessage.php	(revision 12265)
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * @group groups
+ */
+class BP_Tests_Groups_Template_BpGroupStatusMessage extends BP_UnitTestCase {
+	private $current_user;
+	private $groups_template = null;
+
+	public function setUp() {
+		parent::setUp();
+		$this->current_user = bp_loggedin_user_id();
+		$this->set_current_user( 0 );
+
+		if ( isset( $GLOBALS['groups_template'] ) ) {
+			$this->groups_template = $GLOBALS['groups_template'];
+		}
+	}
+
+	public function tearDown() {
+		$this->set_current_user( $this->current_user );
+		if ( $this->groups_template ) {
+			$GLOBALS['groups_template'] = $this->groups_template;
+		}
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @group BP6319
+	 */
+	public function test_private_group_where_logged_in_user_has_not_requested_membership_but_has_been_invited() {
+		$users = self::factory()->user->create_many( 2 );
+		$g = self::factory()->group->create( array( 'status' => 'private' ) );
+
+		$this->set_current_user( $users[0] );
+
+		groups_invite_user( array(
+			'user_id' => $users[0],
+			'group_id' => $g,
+			'inviter_id' => $users[1],
+		) );
+
+		if ( bp_has_groups( array( 'include' => array( $g ) ) ) ) {
+			while ( bp_groups() ) {
+				bp_the_group();
+				$found = get_echo( 'bp_group_status_message' );
+			}
+		}
+
+		$expected = __( 'This is a private group and you must request group membership in order to join.', 'buddypress' );
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group BP6319
+	 */
+	public function test_private_group_where_logged_in_user_has_not_requested_membership_and_has_not_been_invited() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'status' => 'private' ) );
+
+		$this->set_current_user( $u );
+
+		if ( bp_has_groups( array( 'include' => array( $g ) ) ) ) {
+			while ( bp_groups() ) {
+				bp_the_group();
+				$found = get_echo( 'bp_group_status_message' );
+			}
+		}
+
+		$expected = __( 'This is a private group and you must request group membership in order to join.', 'buddypress' );
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group BP6319
+	 */
+	public function test_private_group_visited_by_a_non_logged_in_user() {
+		$g = self::factory()->group->create( array( 'status' => 'private' ) );
+
+		if ( bp_has_groups( array( 'include' => array( $g ) ) ) ) {
+			while ( bp_groups() ) {
+				bp_the_group();
+				$found = get_echo( 'bp_group_status_message' );
+			}
+		}
+
+		$expected = __( 'This is a private group. To join you must be a registered site member and request group membership.', 'buddypress' );
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group BP6319
+	 */
+	public function test_private_group_where_loggedin_user_has_requested_membership() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'status' => 'private' ) );
+
+		$this->set_current_user( $u );
+
+		groups_send_membership_request( $u, $g );
+
+		if ( bp_has_groups( array( 'include' => array( $g ) ) ) ) {
+			while ( bp_groups() ) {
+				bp_the_group();
+				$found = get_echo( 'bp_group_status_message' );
+			}
+		}
+
+		$expected = __( 'This is a private group. Your membership request is awaiting approval from the group administrator.', 'buddypress' );
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group BP6319
+	 */
+	public function test_hidden_group() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->group->create( array( 'status' => 'hidden' ) );
+
+		$this->set_current_user( $u );
+
+		$group = groups_get_group( $g );
+
+		$found = get_echo( 'bp_group_status_message', array( $group ) );
+
+		$expected = __( 'This is a hidden group and only invited members can join.', 'buddypress' );
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group BP6319
+	 */
+	public function test_group_parameter_should_be_obeyed() {
+		$u = self::factory()->user->create();
+		$groups = self::factory()->group->create_many( 2, array( 'status' => 'private' ) );
+
+		$this->set_current_user( $u );
+
+		// Fake the current group.
+		$GLOBALS['groups_template'] = new stdClass;
+		$GLOBALS['groups_template']->group = groups_get_group( $groups[0] );
+
+		groups_send_membership_request( $u, $groups[1] );
+
+		$group1 = groups_get_group( array(
+			'group_id' => $groups[1],
+			'populate_extras' => true,
+		) );
+
+		$found = get_echo( 'bp_group_status_message', array( $group1 ) );
+
+		$expected = __( 'This is a private group. Your membership request is awaiting approval from the group administrator.', 'buddypress' );
+		$this->assertSame( $expected, $found );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/types.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/types.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/types.php	(revision 12265)
@@ -0,0 +1,310 @@
+<?php
+
+/**
+ * @group groups
+ * @group group_types
+ */
+class BP_Tests_Groups_Types extends BP_UnitTestCase {
+	protected static $u1 = null;
+
+	public function setUp() {
+		parent::setUp();
+
+		buddypress()->groups->types = array();
+	}
+
+	public static function wpSetUpBeforeClass( $f ) {
+		self::$u1 = $f->user->create( array(
+			'user_email' => 'group-types-tests@example.com',
+			'user_login' => 'grouptypestests',
+		) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$u1 );
+	}
+
+	public function test_groups_register_type_should_fail_for_existing_group_type() {
+		bp_groups_register_group_type( 'foo' );
+		$this->assertWPError( bp_groups_register_group_type( 'foo' ) );
+	}
+
+	/**
+	 * @dataProvider illegal_names
+	 */
+	public function test_illegal_names( $name ) {
+		$this->assertWPError( bp_groups_register_group_type( $name ) );
+	}
+
+	public function illegal_names() {
+		return array(
+			array( 'any' ),
+			array( 'null' ),
+			array( '_none' ),
+		);
+	}
+
+	public function test_groups_register_type_should_sanitize_group_type_key() {
+		$key = 'F//oo% -Bar';
+		$sanitized_key = 'foo-bar';
+
+		$object = bp_groups_register_group_type( $key );
+		$this->assertSame( $sanitized_key, $object->name );
+	}
+
+	public function test_groups_register_type_should_store_group_type_string_as_name_property() {
+		$object = bp_groups_register_group_type( 'foo' );
+		$this->assertSame( 'foo', $object->name );
+	}
+
+	public function test_groups_register_type_should_fill_missing_labels_with_ucfirst_group_type() {
+		$object = bp_groups_register_group_type( 'foo' );
+		foreach ( $object->labels as $label ) {
+			$this->assertSame( 'Foo', $label );
+		}
+	}
+
+	public function test_groups_register_type_should_respect_custom_name_labels() {
+		$object = bp_groups_register_group_type( 'foo', array(
+			'labels' => array(
+				'name' => 'Bar',
+			),
+		) );
+
+		$this->assertSame( 'Bar', $object->labels['name'] );
+		$this->assertSame( 'Bar', $object->labels['singular_name'] );
+	}
+
+	public function test_groups_register_type_should_respect_custom_singular_name_labels() {
+		$object = bp_groups_register_group_type( 'foo', array(
+			'labels' => array(
+				'singular_name' => 'Bar',
+			),
+		) );
+
+		$this->assertSame( 'Foo', $object->labels['name'] );
+		$this->assertSame( 'Bar', $object->labels['singular_name'] );
+	}
+
+	public function test_groups_get_type_object_should_return_null_for_non_existing_group_type() {
+		$this->assertSame( null, bp_groups_get_group_type_object( 'foo' ) );
+	}
+
+	public function test_groups_get_type_object_should_return_type_object() {
+		bp_groups_register_group_type( 'foo' );
+		$this->assertInternalType( 'object', bp_groups_register_group_type( 'foo' ) );
+	}
+
+	public function test_groups_set_type_should_return_false_for_invalid_group_type() {
+		$this->assertFalse( bp_groups_set_group_type( 1, 'foo' ) );
+	}
+
+	public function test_groups_set_type_success() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+
+		$this->assertNotEmpty( bp_groups_set_group_type( $g, 'foo' ) );
+	}
+
+	public function test_groups_set_type_should_remove_type_when_passing_an_empty_value() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_set_group_type( $g, 'foo' );
+
+		// Make sure group type is set.
+		$this->assertSame( 'foo', bp_groups_get_group_type( $g ) );
+
+		$this->assertSame( array(), bp_groups_set_group_type( $g, '' ) );
+		$this->assertFalse( bp_groups_get_group_type( $g ) );
+	}
+
+	public function test_groups_get_type_with_default_value_for_single() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+		bp_groups_set_group_type( $g, 'bar', true );
+
+		$this->assertSame( 'foo', bp_groups_get_group_type( $g ) );
+	}
+
+	public function test_groups_get_type_with_single_true() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+		bp_groups_set_group_type( $g, 'bar', true );
+
+		$this->assertSame( 'foo', bp_groups_get_group_type( $g, true ) );
+	}
+
+	public function test_groups_get_type_with_single_false() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+		bp_groups_set_group_type( $g, 'bar', true );
+
+		$this->assertEqualSets( array( 'foo', 'bar' ), bp_groups_get_group_type( $g, false ) );
+	}
+
+	public function test_groups_get_type_should_return_false_when_no_value_is_found() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+
+		$this->assertFalse( bp_groups_get_group_type( $g ) );
+	}
+
+	public function test_groups_remove_type_should_return_false_when_group_type_is_empty() {
+		$this->assertFalse( bp_groups_remove_group_type( 9, '' ) );
+	}
+
+	public function test_groups_remove_type_should_return_false_when_group_type_is_invalid() {
+		$this->assertFalse( bp_groups_remove_group_type( 9, 'foo' ) );
+	}
+
+	public function test_groups_remove_type_should_return_false_when_group_is_not_of_provided_type() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+
+		$this->assertFalse( bp_groups_remove_group_type( $g, 'bar' ) );
+		$this->assertEquals( array( 'foo' ), bp_groups_get_group_type( $g, false ) );
+	}
+
+	public function tests_groups_remove_type_should_return_true_on_successful_deletion() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+		bp_groups_set_group_type( $g, 'bar', true );
+
+		$this->assertTrue( bp_groups_remove_group_type( $g, 'foo' ) );
+		$this->assertEquals( array( 'bar' ), bp_groups_get_group_type( $g, false ) );
+	}
+
+	public function test_groups_has_type_should_return_false_when_group_type_is_empty() {
+		$this->assertFalse( bp_groups_has_group_type( 9, '' ) );
+	}
+
+	public function test_groups_has_type_should_return_false_when_group_type_is_invalid() {
+		$this->assertFalse( bp_groups_has_group_type( 9, 'foo' ) );
+	}
+
+	public function test_groups_has_type_should_return_false_when_group_id_is_empty() {
+		bp_groups_register_group_type( 'foo' );
+
+		$this->assertFalse( bp_groups_has_group_type( '', 'foo' ) );
+	}
+
+	public function test_groups_has_type_should_return_false_when_group_is_not_of_provided_type() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+
+		$this->assertFalse( bp_groups_has_group_type( $g, 'bar' ) );
+	}
+
+	public function test_groups_has_type_should_return_true_on_success() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+		bp_groups_set_group_type( $g, 'foo' );
+		bp_groups_set_group_type( $g, 'bar', true );
+
+		$this->assertTrue( bp_groups_has_group_type( $g, 'foo' ) );
+		$this->assertEqualSets( array( 'bar', 'foo' ), bp_groups_get_group_type( $g, false ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_groups_get_type_should_hit_cache() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_set_group_type( $g, 'foo' );
+
+		global $wpdb;
+
+		// Initial query. Should hit DB.
+		bp_groups_get_group_type( $g );
+		$num_queries = $wpdb->num_queries;
+
+		// Next query should hit cache
+		bp_groups_get_group_type( $g );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	public function test_bp_groups_get_group_types_filter() {
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+
+		$types = bp_groups_get_group_types( array( 'name' => 'bar' ) );
+
+		$this->assertEqualSets( array( 'bar' ), $types );
+	}
+
+	public function test_groups_get_type_should_not_return_unregistered_types() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_set_group_type( $g, 'foo' );
+
+		// Directly set a type that hasn't been registered.
+		bp_set_object_terms( $g, 'ugh', 'bp_group_type', true );
+
+		$type = bp_groups_get_group_type( $g, false );
+		$this->assertEquals( array( 'foo' ), $type );
+	}
+
+	public function test_bp_groups_register_group_type_show_in_list_true_when_show_in_create_screen_true() {
+		$object = bp_groups_register_group_type( 'foo', array(
+			'show_in_create_screen' => true,
+		) );
+
+		$this->assertTrue( $object->show_in_list );
+	}
+
+	public function test_bp_groups_register_group_type_show_in_list_false_when_show_in_create_screen_false() {
+		$object = bp_groups_register_group_type( 'foo', array(
+			'show_in_create_screen' => false,
+		) );
+
+		$this->assertFalse( $object->show_in_list );
+	}
+
+	public function test_bp_groups_register_group_type_show_in_list_false_and_show_in_create_screen_true() {
+		$object = bp_groups_register_group_type( 'foo', array(
+			'show_in_create_screen' => true,
+			'show_in_list' => false,
+		) );
+
+		$this->assertFalse( $object->show_in_list );
+	}
+
+	public function test_bp_groups_set_group_type_should_remove_types_when_passing_an_empty_value() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_set_group_type( $g, 'foo' );
+
+		// Make sure it's set up.
+		$this->assertSame( 'foo', bp_groups_get_group_type( $g ) );
+
+		$this->assertSame( array(), bp_groups_set_group_type( $g, '' ) );
+		$this->assertFalse( bp_groups_get_group_type( $g ) );
+	}
+
+	public function test_bp_groups_set_group_type_should_set_multiple_types_when_passing_array_of_types() {
+		$g = self::factory()->group->create( array( 'creator_id' => self::$u1 ) );
+		bp_groups_register_group_type( 'foo' );
+		bp_groups_register_group_type( 'bar' );
+
+		// Set multiple group types.
+		$types = array( 'foo', 'bar' );
+		bp_groups_set_group_type( $g, $types );
+
+		// Assert!
+		$this->assertEqualSets( $types, bp_groups_get_group_type( $g, false ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/groups/user_can.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/groups/user_can.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/groups/user_can.php	(revision 12265)
@@ -0,0 +1,430 @@
+<?php
+
+/**
+ * @group bp_user_can
+ */
+class BP_Tests_Groups_User_Can_Filter extends BP_UnitTestCase {
+
+	public function test_user_can_join_public_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_join_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_join_public_group_if_already_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_join_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	/**
+	 * @ticket BP7610
+	 */
+	public function test_user_cannot_join_public_group_if_already_member_even_superadmin() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		// Grant super admin status.
+		grant_super_admin( $u1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_join_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_join_private_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_join_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_join_group_if_banned() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_join_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_request_membership_in_public_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_request_membership_in_private_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_request_membership_in_hidden_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_request_membership_in_private_group_if_already_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_request_membership_in_private_group_if_already_requested() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		groups_send_membership_request( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_request_membership_in_private_group_if_banned() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_receive_invitation_to_public_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_receive_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_receive_invitation_to_private_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_receive_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_receive_invitation_to_private_group_if_already_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_receive_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	/**
+	 * @ticket BP7610
+	 */
+	public function test_user_cannot_receive_invitation_to_private_group_if_already_member_even_superadmin() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		// Grant super admin status.
+		grant_super_admin( $u1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_receive_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_receive_invitation_to_private_group_if_banned() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_receive_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_receive_invitation_to_hidden_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_receive_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_send_invitation_to_public_group_if_not_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_send_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_send_invitation_to_private_group_if_not_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_send_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_send_invitation_to_private_group_if_banned() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		groups_update_groupmeta( $g1, 'invite_status', 'members' );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		buddypress()->is_item_admin = true;
+		groups_ban_member( $u1, $g1 );
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_send_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_send_invitation_to_private_group_if_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private',
+
+		) );
+		groups_update_groupmeta( $g1, 'invite_status', 'members' );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_send_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_send_invitation_to_hidden_group_if_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_send_invitation', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_access_public_group_even_when_not_logged_in() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertTrue( bp_user_can( 0, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_user_can_access_public_group_if_not_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_access_private_group_if_not_logged_in() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertFalse( bp_user_can( 0, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_user_cannot_access_private_group_if_not_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_access_private_group_if_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_access_hidden_group_if_not_logged_in() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertFalse( bp_user_can( 0, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_user_cannot_access_hidden_group_if_not_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_access_hidden_group_if_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_access_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_see_public_group_even_when_not_logged_in() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertTrue( bp_user_can( 0, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_user_can_see_public_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_see_private_group_even_when_not_logged_in() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertTrue( bp_user_can( 0, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_user_can_see_private_group() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'private'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_cannot_see_hidden_group_if_not_logged_in() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$old_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertFalse( bp_user_can( 0, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	public function test_user_cannot_see_hidden_group_if_not_a_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+
+		$this->assertFalse( bp_user_can( $u1, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	public function test_user_can_see_hidden_group_if_member() {
+		$g1 = $this->factory->group->create( array(
+			'status'      => 'hidden'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		$this->assertTrue( bp_user_can( $u1, 'groups_see_group', array( 'group_id' => $g1 ) ) );
+	}
+
+	/**
+	 * @ticket BP7610
+	 */
+	public function test_user_can_groups_request_membership_for_super_admin() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$g1 = $this->factory->group->create( array(
+			'status' => 'public'
+		) );
+		$u1 = $this->factory->user->create();
+		$this->add_user_to_group( $u1, $g1 );
+
+		// Grant super admin status.
+		grant_super_admin( $u1 );
+
+		// Assert false since public groups shouldn't be able to request membership.
+		$this->assertFalse( bp_user_can( $u1, 'groups_request_membership', array( 'group_id' => $g1 ) ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/activity.php	(revision 12265)
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @group members
+ * @group activity
+ */
+class BP_Tests_Members_Activity extends BP_UnitTestCase {
+
+	/**
+	 * @group activity_action
+	 * @group bp_core_format_activity_action_new_member
+	 */
+	public function test_bp_members_format_activity_action_new_member() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->members->id,
+			'type' => 'new_member',
+			'user_id' => $u,
+		) );
+
+		$expected = sprintf( __( '%s became a registered member', 'buddypress' ), bp_core_get_userlink( $u ) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group bp_migrate_new_member_activity_component
+	 */
+	public function test_bp_migrate_new_member_activity_component() {
+		global $wpdb;
+		$bp = buddypress();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$au1 = self::factory()->activity->create( array(
+			'component' => 'xprofile',
+			'type' => 'new_member',
+			'user_id' => $u1,
+		) );
+
+		$au2 = self::factory()->activity->create( array(
+			'component' => 'xprofile',
+			'type' => 'new_member',
+			'user_id' => $u2,
+		) );
+
+		$au3 = self::factory()->activity->create( array(
+			'component' => 'xprofile',
+			'type' => 'new_member',
+			'user_id' => $u3,
+		) );
+
+		bp_migrate_new_member_activity_component();
+
+		$expected = array(
+			$u1 => $au1,
+			$u2 => $au2,
+			$u3 => $au3,
+		);
+
+		$in = "'" . implode( "', '", array_keys( $expected ) ) . "'";
+		$found = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT user_id, id FROM {$bp->members->table_name_last_activity} WHERE component = %s AND type = %s AND user_id IN ({$in}) ORDER BY user_id ASC",
+				$bp->members->id,
+				'new_member'
+		), OBJECT_K );
+
+		$found = array_map( 'intval', wp_list_pluck( $found, 'id' ) );
+
+		$this->assertSame( $expected, $found );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/cache.php	(revision 12265)
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @group cache
+ * @group members
+ */
+class BP_Tests_Members_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_core_get_total_member_count
+	 * @group cache
+	 */
+	public function test_bp_core_get_total_member_count_should_respect_cached_value_of_0() {
+		global $wpdb;
+
+		// set cached value to zero
+		wp_cache_set( 'bp_total_member_count', 0, 'bp' );
+		$num_queries = $wpdb->num_queries;
+
+		// run function
+		bp_core_get_total_member_count();
+
+		// check if function references cache or hits the DB by comparing query count
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7237
+	 * @ticket BP6643
+	 * @ticket BP7245
+	 */
+	public function test_last_activity_should_bust_activity_with_last_activity_cache() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$time_1 = date( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
+		$time_2 = date( 'Y-m-d H:i:s', time() - ( HOUR_IN_SECONDS * 2 ) );
+		bp_update_user_last_activity( $u1, $time_1 );
+		bp_update_user_last_activity( $u2, $time_2 );
+
+		$activity_args_a = array(
+			'filter' => array(
+				'object' => buddypress()->members->id,
+				'action' => 'last_activity',
+			),
+			'max' => 1,
+		);
+
+		$activity_args_b = array(
+			'filter' => array(
+				'action' => 'new_member',
+			),
+			'fields' => 'ids',
+		);
+
+		// Prime bp_activity and bp_activity_with_last_activity caches.
+		$a1 = bp_activity_get( $activity_args_a );
+		$expected = array( $u1, $u2 );
+		$found = array_map( 'intval', wp_list_pluck( $a1['activities'], 'user_id' ) );
+		$this->assertSame( $expected, $found );
+
+		$b1 = bp_activity_get( $activity_args_b );
+
+		// Bump u2 activity so it should appear first.
+		$new_time = date( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
+		bp_update_user_last_activity( $u2, $new_time );
+
+		$a2 = bp_activity_get( $activity_args_a );
+		$expected = array( $u2, $u1 );
+		$found = array_map( 'intval', wp_list_pluck( $a2['activities'], 'user_id' ) );
+		$this->assertSame( $expected, $found );
+
+		$num_queries = $wpdb->num_queries;
+
+		// bp_activity cache should not have been touched.
+		$b2 = bp_activity_get( $activity_args_b );
+		$this->assertEqualSets( $b1, $b2 );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+}
+
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/class-bp-signup.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/class-bp-signup.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/class-bp-signup.php	(revision 12265)
@@ -0,0 +1,309 @@
+<?php
+
+/**
+ * @group members
+ * @group signups
+ * @group BP_Signup
+ */
+class BP_Tests_BP_Signup extends BP_UnitTestCase {
+	protected $signup_allowed;
+
+	public function setUp() {
+
+		if ( is_multisite() ) {
+			$this->signup_allowed = get_site_option( 'registration' );
+			update_site_option( 'registration', 'all' );
+		} else {
+			bp_get_option( 'users_can_register' );
+			bp_update_option( 'users_can_register', 1 );
+		}
+
+		parent::setUp();
+	}
+
+	public function tearDown() {
+		if ( is_multisite() ) {
+			update_site_option( 'registration', $this->signup_allowed );
+		} else {
+			bp_update_option( 'users_can_register', $this->signup_allowed );
+		}
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @group add
+	 */
+	public function test_add() {
+		$time = bp_core_current_time();
+		$args = array(
+			'domain' => 'foo',
+			'path' => 'bar',
+			'title' => 'Foo bar',
+			'user_login' => 'user1',
+			'user_email' => 'user1@example.com',
+			'registered' => $time,
+			'activation_key' => '12345',
+			'meta' => array(
+				'field_1' => 'Foo Bar',
+				'meta1' => 'meta2',
+			),
+		);
+
+		$signup = BP_Signup::add( $args );
+		$this->assertNotEmpty( $signup );
+
+		$s = new BP_Signup( $signup );
+
+		// spot check
+		$this->assertSame( $signup, $s->id );
+		$this->assertSame( 'user1', $s->user_login );
+		$this->assertSame( '12345', $s->activation_key );
+	}
+
+	/**
+	 * @group add
+	 */
+	public function test_add_no_visibility_level_set_should_use_default_visiblity_level() {
+		// Update field_1's default visiblity to 'adminsonly'
+		bp_xprofile_update_field_meta( 1, 'default_visibility', 'adminsonly' );
+
+		// Add new signup without a custom field visibility set for field_1.
+		$signup = BP_Signup::add( array(
+			'title' => 'Foo bar',
+			'user_login' => 'user1',
+			'user_email' => 'user1@example.com',
+			'registered' => bp_core_current_time(),
+			'activation_key' => '12345',
+			'meta' => array(
+				'field_1' => 'Foo Bar',
+				'meta1' => 'meta2',
+				'password' => 'password',
+
+				/*
+				 * Ensure we pass the field ID.
+				 *
+				 * See bp_core_activate_signup() and BP_Signup::add_backcompat().
+				 */
+				'profile_field_ids' => '1'
+			),
+		) );
+
+		// Activate the signup.
+		$activate = BP_Signup::activate( (array) $signup );
+
+		// Assert that field 1's visibility for the signup is still 'adminsonly'
+		$vis = xprofile_get_field_visibility_level( 1, $activate['activated'][0] );
+		$this->assertSame( 'adminsonly', $vis );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_offset() {
+		$s1 = self::factory()->signup->create();
+		$s2 = self::factory()->signup->create();
+		$s3 = self::factory()->signup->create();
+
+		$ss = BP_Signup::get( array(
+			'offset' => 1,
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s2 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_number() {
+		$s1 = self::factory()->signup->create();
+		$s2 = self::factory()->signup->create();
+		$s3 = self::factory()->signup->create();
+
+		$ss = BP_Signup::get( array(
+			'number' => 2,
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s3, $s2 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_usersearch() {
+		$s1 = self::factory()->signup->create( array(
+			'user_email' => 'fghij@example.com',
+		) );
+		$s2 = self::factory()->signup->create();
+		$s3 = self::factory()->signup->create();
+
+		$ss = BP_Signup::get( array(
+			'usersearch' => 'ghi',
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s1 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_orderby_email() {
+		$s1 = self::factory()->signup->create( array(
+			'user_email' => 'fghij@example.com',
+		) );
+		$s2 = self::factory()->signup->create( array(
+			'user_email' => 'abcde@example.com',
+		) );
+		$s3 = self::factory()->signup->create( array(
+			'user_email' => 'zzzzz@example.com',
+		) );
+
+		$ss = BP_Signup::get( array(
+			'orderby' => 'email',
+			'number' => 3,
+			'fields' => 'ids',
+		) );
+
+		// default order is DESC.
+		$this->assertEquals( array( $s3, $s1, $s2 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_orderby_email_asc() {
+		$s1 = self::factory()->signup->create( array(
+			'user_email' => 'fghij@example.com',
+		) );
+		$s2 = self::factory()->signup->create( array(
+			'user_email' => 'abcde@example.com',
+		) );
+		$s3 = self::factory()->signup->create( array(
+			'user_email' => 'zzzzz@example.com',
+		) );
+
+		$ss = BP_Signup::get( array(
+			'orderby' => 'email',
+			'number' => 3,
+			'order' => 'ASC',
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s2, $s1, $s3 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_include() {
+		$s1 = self::factory()->signup->create();
+		$s2 = self::factory()->signup->create();
+		$s3 = self::factory()->signup->create();
+
+		$ss = BP_Signup::get( array(
+			'include' => array( $s1, $s3 ),
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s1, $s3 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_activation_key() {
+		$s1 = self::factory()->signup->create( array(
+			'activation_key' => 'foo',
+		) );
+		$s2 = self::factory()->signup->create( array(
+			'activation_key' => 'bar',
+		) );
+		$s3 = self::factory()->signup->create( array(
+			'activation_key' => 'baz',
+		) );
+
+		$ss = BP_Signup::get( array(
+			'activation_key' => 'bar',
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s2 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_with_user_login() {
+		$s1 = self::factory()->signup->create( array(
+			'user_login' => 'aaaafoo',
+		) );
+		$s2 = self::factory()->signup->create( array(
+			'user_login' => 'zzzzfoo',
+		) );
+		$s3 = self::factory()->signup->create( array(
+			'user_login' => 'jjjjfoo',
+		) );
+
+		$ss = BP_Signup::get( array(
+			'user_login' => 'zzzzfoo',
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s2 ), $ss['signups'] );
+	}
+
+	/**
+	 * @group activate
+	 */
+	public function test_activate_user_accounts() {
+		$signups = array();
+
+		$signups['accountone'] = self::factory()->signup->create( array(
+			'user_login'     => 'accountone',
+			'user_email'     => 'accountone@example.com',
+			'activation_key' => 'activationkeyone',
+		) );
+
+		$signups['accounttwo'] = self::factory()->signup->create( array(
+			'user_login'     => 'accounttwo',
+			'user_email'     => 'accounttwo@example.com',
+			'activation_key' => 'activationkeytwo',
+		) );
+
+		$signups['accountthree'] = self::factory()->signup->create( array(
+			'user_login'     => 'accountthree',
+			'user_email'     => 'accountthree@example.com',
+			'activation_key' => 'activationkeythree',
+		) );
+
+		$results = BP_Signup::activate( $signups );
+		$this->assertNotEmpty( $results['activated'] );
+
+		$users = array();
+
+		foreach ( $signups as $login => $signup_id  ) {
+			$users[ $login ] = get_user_by( 'login', $login );
+		}
+
+		$this->assertEqualSets( $results['activated'], wp_list_pluck( $users, 'ID' ) );
+	}
+
+	/**
+	 * @group get
+	 */
+	public function test_get_signup_ids_only() {
+		$s1 = self::factory()->signup->create();
+		$s2 = self::factory()->signup->create();
+		$s3 = self::factory()->signup->create();
+
+		$ss = BP_Signup::get( array(
+			'number' => 3,
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $s3, $s2, $s1 ), $ss['signups'] );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/functions.php	(revision 12265)
@@ -0,0 +1,607 @@
+<?php
+/**
+ * @group members
+ */
+class BP_Tests_Members_Functions extends BP_UnitTestCase {
+
+	/**
+	 * @ticket BP4915
+	 * @group bp_core_delete_account
+	 */
+	public function test_bp_core_delete_account() {
+		// Stash
+		$current_user = get_current_user_id();
+		$deletion_disabled = bp_disable_account_deletion();
+
+		// Create an admin for testing
+		$admin_user = self::factory()->user->create( array( 'role' => 'administrator' ) );
+		$this->grant_super_admin( $admin_user );
+
+		// 1. Admin can delete user account
+		$this->set_current_user( $admin_user );
+		$user1 = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+		bp_core_delete_account( $user1 );
+		$maybe_user = new WP_User( $user1 );
+		$this->assertEquals( 0, $maybe_user->ID );
+		unset( $maybe_user );
+		$this->restore_admins();
+
+		// 2. Admin cannot delete superadmin account
+		$user2 = self::factory()->user->create( array( 'role' => 'administrator' ) );
+		$this->grant_super_admin( $user2 );
+		bp_core_delete_account( $user2 );
+		$maybe_user = new WP_User( $user2 );
+		$this->assertNotEquals( 0, $maybe_user->ID );
+		unset( $maybe_user );
+
+		// User cannot delete other's account
+		$user3 = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+		$user4 = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+		$this->set_current_user( $user3 );
+		bp_core_delete_account( $user4 );
+		$maybe_user = new WP_User( $user4 );
+		$this->assertNotEquals( 0, $maybe_user->ID );
+		unset( $maybe_user );
+
+		// Cleanup
+		$this->set_current_user( $current_user );
+		bp_update_option( 'bp-disable-account-deletion', $deletion_disabled );
+	}
+
+	/**
+	 * @group object_cache
+	 * @group bp_core_get_directory_pages
+	 */
+	public function test_bp_core_get_user_domain_after_directory_page_update() {
+		// Generate user
+		$user_id = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+
+		// Set object cache first for user domain
+		$user_domain = bp_core_get_user_domain( $user_id );
+
+		// Now change the members directory slug
+		$pages = bp_core_get_directory_pages();
+		$members_page = get_post( $pages->members->id );
+		$members_page->post_name = 'new-members-slug';
+		wp_update_post( $members_page );
+
+		// Go back to members directory page and recheck user domain
+		$this->go_to( trailingslashit( home_url( 'new-members-slug' ) ) );
+		$user = new WP_User( $user_id );
+
+		$this->assertSame( home_url( 'new-members-slug' ) . '/' . $user->user_nicename . '/', bp_core_get_user_domain( $user_id ) );
+	}
+
+	/**
+	 * @group bp_core_get_user_displayname
+	 */
+	public function test_bp_core_get_user_displayname_empty_username() {
+		$this->assertFalse( bp_core_get_user_displayname( '' ) );
+	}
+
+	/**
+	 * @group bp_core_get_user_displayname
+	 */
+	public function test_bp_core_get_user_displayname_translate_username() {
+		$u = self::factory()->user->create();
+
+		$user = new WP_User( $u );
+
+		$found = bp_core_get_user_displayname( $u );
+		$this->assertNotEmpty( $found );
+		$this->assertSame( $found, bp_core_get_user_displayname( $user->user_login ) );
+	}
+
+	/**
+	 * @group bp_core_get_user_displayname
+	 */
+	public function test_bp_core_get_user_displayname_bad_username() {
+		$this->assertFalse( bp_core_get_user_displayname( 'i_dont_exist' ) );
+	}
+
+	/**
+	 * @group bp_core_get_user_displayname
+	 */
+	public function test_bp_core_get_user_displayname_xprofile_exists() {
+		$xprofile_is_active = bp_is_active( 'xprofile' );
+		buddypress()->active_components['xprofile'] = '1';
+
+		$u = self::factory()->user->create();
+		xprofile_set_field_data( 1, $u, 'Foo Foo' );
+
+		$this->assertSame( 'Foo Foo', bp_core_get_user_displayname( $u ) );
+
+		if ( ! $xprofile_is_active ) {
+			unset( buddypress()->active_components['xprofile'] );
+		}
+	}
+
+	/**
+	 * @group bp_core_get_user_displaynames
+	 */
+	public function test_bp_core_get_user_displayname_arrays_all_bad_entries() {
+		$this->assertSame( array(), bp_core_get_user_displaynames( array( 0, 'foo', ) ) );
+	}
+
+	/**
+	 * @group bp_core_get_user_displaynames
+	 */
+	public function test_bp_core_get_user_displaynames_all_uncached() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		xprofile_set_field_data( 1, $u1, 'Foo' );
+		xprofile_set_field_data( 1, $u2, 'Bar' );
+
+		$expected = array(
+			$u1 => 'Foo',
+			$u2 => 'Bar',
+		);
+
+		$this->assertSame( $expected, bp_core_get_user_displaynames( array( $u1, $u2, ) ) );
+	}
+
+	/**
+	 * @group bp_core_get_user_displaynames
+	 */
+	public function test_bp_core_get_user_displaynames_one_not_in_xprofile() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create( array(
+			'display_name' => 'Bar',
+		) );
+		xprofile_set_field_data( 1, $u1, 'Foo' );
+
+		$expected = array(
+			$u1 => 'Foo',
+			$u2 => 'Bar',
+		);
+
+		$this->assertSame( $expected, bp_core_get_user_displaynames( array( $u1, $u2, ) ) );
+	}
+
+	/**
+	 * @group bp_members_migrate_signups
+	 */
+	public function test_bp_members_migrate_signups_standard() {
+		$u = self::factory()->user->create();
+		$u_obj = new WP_User( $u );
+
+		// Fake an old-style registration
+		$key = wp_generate_password( 32, false );
+		update_user_meta( $u, 'activation_key', $key );
+
+		global $wpdb;
+		$wpdb->update(
+			$wpdb->users,
+			array( 'user_status' => '2', ),
+			array( 'ID' => $u, ),
+			array( '%d', ),
+			array( '%d', )
+		);
+		clean_user_cache( $u );
+
+		bp_members_migrate_signups();
+
+		$found = BP_Signup::get();
+
+		// Use email address as a sanity check
+		$found_email = isset( $found['signups'][0]->user_email ) ? $found['signups'][0]->user_email : '';
+		$this->assertSame( $u_obj->user_email, $found_email );
+
+		// Check that activation keys match
+		$found_key = isset( $found['signups'][0]->activation_key ) ? $found['signups'][0]->activation_key : '';
+		$this->assertSame( $key, $found_key );
+	}
+
+	/**
+	 * @group bp_members_migrate_signups
+	 */
+	public function test_bp_members_migrate_signups_activation_key_but_user_status_0() {
+		$u = self::factory()->user->create();
+		$u_obj = new WP_User( $u );
+
+		// Fake an old-style registration
+		$key = wp_generate_password( 32, false );
+		update_user_meta( $u, 'activation_key', $key );
+
+		// ...but ensure that user_status is 0. This mimics the
+		// behavior of certain plugins that disrupt the BP registration
+		// flow
+		global $wpdb;
+		$wpdb->update(
+			$wpdb->users,
+			array( 'user_status' => '0', ),
+			array( 'ID' => $u, ),
+			array( '%d', ),
+			array( '%d', )
+		);
+		clean_user_cache( $u );
+
+		bp_members_migrate_signups();
+
+		// No migrations should have taken place
+		$found = BP_Signup::get();
+		$this->assertEmpty( $found['total'] );
+	}
+
+	/**
+	 * @group bp_members_migrate_signups
+	 */
+	public function test_bp_members_migrate_signups_no_activation_key_but_user_status_2() {
+		$u = self::factory()->user->create();
+		$u_obj = new WP_User( $u );
+
+		// Fake an old-style registration but without an activation key
+		global $wpdb;
+		$wpdb->update(
+			$wpdb->users,
+			array( 'user_status' => '2', ),
+			array( 'ID' => $u, ),
+			array( '%d', ),
+			array( '%d', )
+		);
+		clean_user_cache( $u );
+
+		bp_members_migrate_signups();
+
+		// Use email address as a sanity check
+		$found = BP_Signup::get();
+		$found_email = isset( $found['signups'][0]->user_email ) ? $found['signups'][0]->user_email : '';
+		$this->assertSame( $u_obj->user_email, $found_email );
+	}
+
+	/**
+	 * @group bp_last_activity_migrate
+	 * @expectedIncorrectUsage update_user_meta( $user_id, 'last_activity' )
+	 * @expectedIncorrectUsage get_user_meta( $user_id, 'last_activity' )
+	 */
+	public function test_bp_last_activity_migrate() {
+		// We explicitly do not want last_activity created, so use the
+		// WP factory methods
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$u3 = self::factory()->user->create();
+
+		$time = time();
+		$t1 = date( 'Y-m-d H:i:s', $time - 50 );
+		$t2 = date( 'Y-m-d H:i:s', $time - 500 );
+		$t3 = date( 'Y-m-d H:i:s', $time - 5000 );
+
+		update_user_meta( $u1, 'last_activity', $t1 );
+		update_user_meta( $u2, 'last_activity', $t2 );
+		update_user_meta( $u3, 'last_activity', $t3 );
+
+		// Create an existing entry in last_activity to test no dupes
+		global $wpdb;
+		$bp = buddypress();
+		$wpdb->query( $wpdb->prepare(
+			"INSERT INTO {$bp->members->table_name_last_activity}
+				(`user_id`, `component`, `type`, `action`, `content`, `primary_link`, `item_id`, `date_recorded` ) VALUES
+				( %d, %s, %s, %s, %s, %s, %d, %s )",
+			$u2, $bp->members->id, 'last_activity', '', '', '', 0, $t1
+		) );
+
+		bp_last_activity_migrate();
+
+		$expected = array(
+			$u1 => $t1,
+			$u2 => $t2,
+			$u3 => $t3,
+		);
+
+		$found = array(
+			$u1 => '',
+			$u2 => '',
+			$u3 => '',
+		);
+
+		foreach ( $found as $uid => $v ) {
+			$found[ $uid ] = bp_get_user_last_activity( $uid );
+		}
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group bp_core_get_userid_from_nicename
+	 */
+	public function test_bp_core_get_userid_from_nicename_failure() {
+		$this->assertSame( NULL, bp_core_get_userid_from_nicename( 'non_existent_user' ) );
+	}
+
+	/**
+	 * @group bp_update_user_last_activity
+	 */
+	public function test_bp_last_activity_multi_network() {
+
+		// Filter the usermeta key
+		add_filter( 'bp_get_user_meta_key', array( $this, 'filter_usermeta_key' ) );
+
+		// We explicitly do not want last_activity created, so use the
+		// WP factory methods
+		$user = self::factory()->user->create();
+		$time = date( 'Y-m-d H:i:s', time() - 50 );
+
+		// Update last user activity
+		bp_update_user_last_activity( $user, $time );
+
+		// Setup parameters to assert to be the same
+		$expected = $time;
+		$found    = bp_get_user_meta( $user, 'last_activity', true );
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group bp_update_user_last_activity
+	 * @global object $wpdb
+	 * @param  string $key
+	 * @return string
+	 */
+	public function filter_usermeta_key( $key ) {
+		global $wpdb;
+		return $wpdb->prefix . $key;
+	}
+
+	/**
+	 * @group bp_core_process_spammer_status
+	 */
+	public function test_bp_core_process_spammer_status() {
+		if ( is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$bp = buddypress();
+		$displayed_user = $bp->displayed_user;
+
+		$u1 = self::factory()->user->create();
+		$bp->displayed_user->id = $u1;
+
+		// Spam the user
+		bp_core_process_spammer_status( $u1, 'spam' );
+
+		$this->assertTrue( bp_is_user_spammer( $u1 ) );
+
+		// Unspam the user
+		bp_core_process_spammer_status( $u1, 'ham' );
+
+		$this->assertFalse( bp_is_user_spammer( $u1 ) );
+
+		// Reset displayed user
+		$bp->displayed_user = $displayed_user;
+	}
+
+	/**
+	 * @group bp_core_process_spammer_status
+	 */
+	public function test_bp_core_process_spammer_status_ms_bulk_spam() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$bp = buddypress();
+		$displayed_user = $bp->displayed_user;
+
+		$u1 = self::factory()->user->create();
+		$bp->displayed_user->id = $u1;
+
+		// Bulk spam in network admin uses update_user_status
+		update_user_status( $u1, 'spam', '1' );
+
+		$this->assertTrue( bp_is_user_spammer( $u1 ) );
+
+		// Unspam the user
+		bp_core_process_spammer_status( $u1, 'ham' );
+
+		$this->assertFalse( bp_is_user_spammer( $u1 ) );
+
+		// Reset displayed user
+		$bp->displayed_user = $displayed_user;
+	}
+
+	/**
+	 * @group bp_core_process_spammer_status
+	 */
+	public function test_bp_core_process_spammer_status_ms_bulk_ham() {
+		if ( ! is_multisite() ) {
+			$this->markTestSkipped();
+		}
+
+		$bp = buddypress();
+		$displayed_user = $bp->displayed_user;
+
+		$u1 = self::factory()->user->create();
+		$bp->displayed_user->id = $u1;
+
+		// Spam the user
+		bp_core_process_spammer_status( $u1, 'spam' );
+
+		$this->assertTrue( bp_is_user_spammer( $u1 ) );
+
+		// Bulk unspam in network admin uses update_user_status
+		update_user_status( $u1, 'spam', '0' );
+
+		$this->assertFalse( bp_is_user_spammer( $u1 ) );
+
+		// Reset displayed user
+		$bp->displayed_user = $displayed_user;
+	}
+
+	/**
+	 * @group bp_core_process_spammer_status
+	 */
+	public function test_bp_core_process_spammer_status_make_spam_user_filter() {
+		add_filter( 'make_spam_user', array( $this, 'notification_filter_callback' ) );
+
+		$u1 = self::factory()->user->create();
+		$n = bp_core_process_spammer_status( $u1, 'spam' );
+
+		remove_filter( 'make_spam_user', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'make_spam_user', $this->filter_fired );
+
+	}
+
+	public function test_bp_core_process_spammer_status_make_ham_user_filter() {
+		add_filter( 'make_ham_user', array( $this, 'notification_filter_callback' ) );
+
+		$u1 = self::factory()->user->create();
+		$n = bp_core_process_spammer_status( $u1, 'ham' );
+
+		remove_filter( 'make_ham_user', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'make_ham_user', $this->filter_fired );
+
+	}
+
+	public function test_bp_core_process_spammer_status_bp_make_spam_user_filter() {
+		add_filter( 'bp_make_spam_user', array( $this, 'notification_filter_callback' ) );
+
+		$u1 = self::factory()->user->create();
+		$n = bp_core_process_spammer_status( $u1, 'spam' );
+
+		remove_filter( 'bp_make_spam_user', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_make_spam_user', $this->filter_fired );
+
+	}
+
+	public function test_bp_core_process_spammer_status_bp_make_ham_user_filter() {
+		add_filter( 'bp_make_ham_user', array( $this, 'notification_filter_callback' ) );
+
+		$u1 = self::factory()->user->create();
+		$n = bp_core_process_spammer_status( $u1, 'ham' );
+
+		remove_filter( 'bp_make_ham_user', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_make_ham_user', $this->filter_fired );
+
+	}
+
+	public function notification_filter_callback( $value ) {
+		$this->filter_fired = current_filter();
+		return $value;
+	}
+
+	/**
+	 * @ticket BP6208
+	 *
+	 * Note - it's not possible to test this when registration is not configured properly,
+	 * because `bp_has_custom_signup_page()` stores its value in a static variable that cannot
+	 * be toggled.
+	 */
+	public function test_wp_registration_url_should_return_bp_register_page_when_register_page_is_configured_properly() {
+		$this->assertSame( bp_get_signup_page(), wp_registration_url() );
+	}
+
+	/**
+	 * @group bp_core_activate_signup
+	 */
+	public function test_bp_core_activate_signup_password() {
+		global $wpdb;
+
+
+		$signups = array( 'no-blog' =>
+			array( 'signup_id' => self::factory()->signup->create( array(
+					'user_login'     => 'noblog',
+					'user_email'     => 'noblog@example.com',
+					'activation_key' => 'no-blog',
+					'meta' => array(
+						'field_1' => 'Foo Bar',
+						'password' => 'foobar',
+					),
+			) ),
+				'password' => 'foobar',
+			),
+		);
+
+		if ( is_multisite() ) {
+			if ( function_exists( 'wp_initialize_site' ) ) {
+				$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			}
+
+			$signups['ms-blog'] = array( 'signup_id' => self::factory()->signup->create( array(
+					'user_login'     => 'msblog',
+					'user_email'     => 'msblog@example.com',
+					'domain'         => get_current_site()->domain,
+					'path'           => get_current_site()->path . 'ms-blog',
+					'title'          => 'Ding Dang',
+					'activation_key' => 'ms-blog',
+					'meta' => array(
+						'field_1'  => 'Ding Dang',
+						'password' => 'dingdang',
+					),
+				) ),
+				'password' => 'dingdang',
+			);
+		}
+
+		// Neutralize db errors
+		$suppress = $wpdb->suppress_errors();
+
+		foreach ( $signups as $key => $data ) {
+			$u = bp_core_activate_signup( $key );
+
+			$this->assertEquals( get_userdata( $u )->user_pass, $data['password'] );
+		}
+
+		$wpdb->suppress_errors( $suppress );
+	}
+
+	/**
+	 * @ticket BP7461
+	 *
+	 * Test function before and after adding custom illegal names from WordPress.
+	 */
+	public function test_bp_core_get_illegal_names() {
+
+		// Making sure BP custom illegals are in the array.
+		$this->assertTrue( in_array( 'profile', bp_core_get_illegal_names(), true ) );
+		$this->assertTrue( in_array( 'forums', bp_core_get_illegal_names(), true ) );
+
+		add_filter( 'illegal_user_logins', array( $this, '_illegal_user_logins' ) );
+
+		// Testing fake custom illegal names.
+		$this->assertTrue( in_array( 'testuser', bp_core_get_illegal_names(), true ) );
+		$this->assertTrue( in_array( 'admins', bp_core_get_illegal_names(), true ) );
+		$this->assertFalse( in_array( 'buddypresss', bp_core_get_illegal_names(), true ) );
+
+		// Making sure BP custom illegals are in the array after including the custom ones.
+		$this->assertTrue( in_array( 'profile', bp_core_get_illegal_names(), true ) );
+		$this->assertTrue( in_array( 'forums', bp_core_get_illegal_names(), true ) );
+
+		remove_filter( 'illegal_user_logins', array( $this, '_illegal_user_logins' ) );
+	}
+
+	public function _illegal_user_logins() {
+		return array(
+			'testuser',
+			'admins',
+			'buddypress',
+		);
+	}
+
+	/**
+	 * @group bp_core_activate_signup
+	 */
+	public function test_bp_core_activate_signup_should_add_user_role() {
+		$key = 'test';
+
+		// Create the signup.
+		$this->factory->signup->create( array(
+			'user_login'     => 'test',
+			'user_email'     => 'test@example.com',
+			'activation_key' => $key,
+			'meta' => array(
+				'field_1' => 'Foo Bar',
+				'password' => 'foobar',
+			),
+		) );
+
+		// Activate user.
+		$user_id = bp_core_activate_signup( $key );
+
+		// Assert that user has a role.
+		$user = get_userdata( $user_id );
+		$this->assertNotEmpty( $user->roles );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/template.php	(revision 12265)
@@ -0,0 +1,416 @@
+<?php
+/**
+ * @group members
+ */
+class BP_Tests_Members_Template extends BP_UnitTestCase {
+	public function test_bp_has_members_include_on_user_page() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$this->go_to( bp_core_get_user_domain( $u1 ) );
+
+		global $members_template;
+		bp_has_members( array(
+			'include' => array( $u1, $u2 ),
+		) );
+
+		$users = is_array( $members_template->members ) ? array_values( $members_template->members ) : array();
+		$user_ids = wp_list_pluck( $users, 'ID' );
+		sort( $user_ids );
+
+		$shouldbe = array( $u1, $u2 );
+		sort( $shouldbe );
+
+		$this->assertEquals( $user_ids, $shouldbe );
+	}
+
+	/**
+	 * @ticket BP5898
+	 * @group bp_has_members
+	 */
+	public function test_bp_has_members_search_pagination_with_spaces() {
+		$u1 = self::factory()->user->create( array( 'display_name' => '~ tilde u1' ) );
+		$u2 = self::factory()->user->create( array( 'display_name' => '~ tilde u2' ) );
+
+		$template_args = array(
+			'search_terms' => '~ tilde',
+			'per_page'     => 1,
+		);
+
+		global $members_template;
+		$reset_members_template = $members_template;
+
+		bp_has_members( $template_args );
+
+		preg_match( '/&#038;members_search=(.*)\'/', $members_template->pag_links, $matches );
+
+		$this->assertEquals( urldecode( $matches[1] ), urldecode( $template_args['search_terms'] ) );
+
+		// reset the members template global
+		$members_template = $reset_members_template;
+	}
+
+	public function test_bp_has_members_friendship_requests() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		friends_add_friend( $u1, $u2 );
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+
+		$this->go_to( bp_core_get_user_domain( $u2 ) . bp_get_friends_slug() . '/requests/' );
+		$this->restore_admins();
+
+		global $members_template;
+		bp_has_members( array(
+			'include' => bp_get_friendship_requests( $u2 ),
+		) );
+
+		$requests = is_array( $members_template->members ) ? array_values( $members_template->members ) : array();
+		$request_ids = wp_list_pluck( $requests, 'ID' );
+		$this->assertEquals( $request_ids, array( $u1 ) );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_has_members
+	 * @group friends
+	 * @ticket BP5071
+	 */
+	public function test_bp_has_members_friendship_requests_with_no_requests() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+
+		// For some reason, in all the user switching, the cache gets
+		// confused. Never comes up when BP runs normally, because the
+		// loggedin_user doesn't change on a pageload. @todo Fix for
+		// real in BP
+		wp_cache_delete( 'bp_user_domain_' . $u2, 'bp' );
+
+		$this->go_to( bp_core_get_user_domain( $u2 ) . bp_get_friends_slug() . '/requests/' );
+		$this->restore_admins();
+
+		global $members_template;
+		bp_has_members( array(
+			'include' => bp_get_friendship_requests( $u2 ),
+		) );
+
+		$requests = is_array( $members_template->members ) ? array_values( $members_template->members ) : array();
+		$request_ids = wp_list_pluck( $requests, 'ID' );
+		$this->assertEquals( array(), $request_ids );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @group bp_has_members
+	 */
+	public function test_bp_has_members_should_pass_member_type_param_to_query() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		global $members_template;
+		$old_members_template = $members_template;
+
+		bp_has_members( array(
+			'member_type' => 'bar',
+		) );
+
+		$members = is_array( $members_template->members ) ? array_values( $members_template->members ) : array();
+		$member_ids = wp_list_pluck( $members, 'ID' );
+		$this->assertEquals( array( $users[1]), $member_ids );
+
+		$GLOBALS['members_template'] = $old_members_template;
+	}
+
+	/**
+	 * @group bp_has_members
+	 * @ticket BP6286
+	 */
+	public function test_bp_has_members_should_infer_member_type_from_get_param() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		$users = self::factory()->user->create_many( 3 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+
+		global $members_template;
+		$old_members_template = $members_template;
+
+		$old_get = $_GET;
+		$_GET['member_type'] = 'bar';
+
+		bp_has_members();
+
+		$members = is_array( $members_template->members ) ? array_values( $members_template->members ) : array();
+		$member_ids = wp_list_pluck( $members, 'ID' );
+		$this->assertEquals( array( $users[1] ), $member_ids );
+
+		$GLOBALS['members_template'] = $old_members_template;
+	}
+
+	/**
+	 * @group bp_has_members
+	 * @ticket BP6286
+	 */
+	public function test_bp_has_members_should_infer_member_type_from_get_param_comma_sep() {
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_register_member_type( 'baz' );
+		$users = self::factory()->user->create_many( 4 );
+		bp_set_member_type( $users[0], 'foo' );
+		bp_set_member_type( $users[1], 'bar' );
+		bp_set_member_type( $users[2], 'baz' );
+
+		global $members_template;
+		$old_members_template = $members_template;
+
+		$old_get = $_GET;
+		$_GET['member_type'] = 'foo,bar';
+
+		bp_has_members();
+
+		$members = is_array( $members_template->members ) ? array_values( $members_template->members ) : array();
+		$member_ids = wp_list_pluck( $members, 'ID' );
+		$this->assertEqualSets( array( $users[0], $users[1] ), $member_ids );
+
+		$GLOBALS['members_template'] = $old_members_template;
+	}
+
+	/**
+	 * @group bp_get_member_last_active
+	 */
+	public function test_bp_get_member_last_active_default_params() {
+		// Fake the global
+		global $members_template;
+
+		$time = date( 'Y-m-d H:i:s', time() - 24*60*60 );
+		$members_template = new stdClass;
+		$members_template->member = new stdClass;
+		$members_template->member->last_activity = $time;
+
+		$this->assertEquals( bp_core_get_last_activity( $time, __( 'active %s', 'buddypress' ) ), bp_get_member_last_active() );
+	}
+
+	/**
+	 * @group bp_get_member_last_active
+	 */
+	public function test_bp_get_member_last_active_active_format_true() {
+		// Fake the global
+		global $members_template;
+
+		$time = date( 'Y-m-d H:i:s', time() - 24*60*60 );
+		$members_template = new stdClass;
+		$members_template->member = new stdClass;
+		$members_template->member->last_activity = $time;
+
+		$this->assertEquals( bp_core_get_last_activity( $time, __( 'active %s', 'buddypress' ) ), bp_get_member_last_active( array( 'active_format' => true, ) ) );
+	}
+
+	/**
+	 * @group bp_get_member_last_active
+	 */
+	public function test_bp_get_member_last_active_active_format_false() {
+		// Fake the global
+		global $members_template;
+
+		$time = date( 'Y-m-d H:i:s', time() - 24*60*60 );
+		$members_template = new stdClass;
+		$members_template->member = new stdClass;
+		$members_template->member->last_activity = $time;
+
+		$this->assertEquals( bp_core_time_since( $time ), bp_get_member_last_active( array( 'active_format' => false, ) ) );
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Core_Members_Template
+	 */
+	public function test_bp_groups_template_should_give_precedence_to_upage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['upage'] = '5';
+
+		$r = array(
+			'type'            => 'active',
+			'page'            => 8,
+			'per_page'        => 20,
+			'max'             => false,
+			'page_arg'        => 'upage',
+			'include'         => false,
+			'exclude'         => false,
+			'user_id'         => 0,
+			'member_type'     => '',
+			'search_terms'    => null,
+			'meta_key'        => false,
+			'meta_value'	  => false,
+			'populate_extras' => true,
+			'search_terms'    => ''
+		);
+
+		$at = new BP_Core_Members_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['include'],
+			$r['populate_extras'],
+			$r['exclude'],
+			$r['meta_key'],
+			$r['meta_value'],
+			$r['page_arg'],
+			$r['member_type']
+		);
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Core_Members_Template
+	 */
+	public function test_bp_members_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['upage'] = '0';
+
+		$r = array(
+			'type'            => 'active',
+			'page'            => 8,
+			'per_page'        => 20,
+			'max'             => false,
+			'page_arg'        => 'upage',
+			'include'         => false,
+			'exclude'         => false,
+			'user_id'         => 0,
+			'member_type'     => '',
+			'search_terms'    => null,
+			'meta_key'        => false,
+			'meta_value'	  => false,
+			'populate_extras' => true,
+			'search_terms'    => ''
+		);
+
+		$at = new BP_Core_Members_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['include'],
+			$r['populate_extras'],
+			$r['exclude'],
+			$r['meta_key'],
+			$r['meta_value'],
+			$r['page_arg'],
+			$r['member_type']
+		);
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Core_Members_Template
+	 */
+	public function test_bp_members_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$r = array(
+			'type'            => 'active',
+			'page'            => 1,
+			'per_page'        => 13,
+			'max'             => false,
+			'page_arg'        => 'upage',
+			'include'         => false,
+			'exclude'         => false,
+			'user_id'         => 0,
+			'member_type'     => '',
+			'search_terms'    => null,
+			'meta_key'        => false,
+			'meta_value'	  => false,
+			'populate_extras' => true,
+			'search_terms'    => ''
+		);
+
+		$at = new BP_Core_Members_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['include'],
+			$r['populate_extras'],
+			$r['exclude'],
+			$r['meta_key'],
+			$r['meta_value'],
+			$r['page_arg'],
+			$r['member_type']
+		);
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Core_Members_Template
+	 */
+	public function test_bp_members_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$r = array(
+			'type'            => 'active',
+			'page'            => 1,
+			'per_page'        => 13,
+			'max'             => false,
+			'page_arg'        => 'upage',
+			'include'         => false,
+			'exclude'         => false,
+			'user_id'         => 0,
+			'member_type'     => '',
+			'search_terms'    => null,
+			'meta_key'        => false,
+			'meta_value'	  => false,
+			'populate_extras' => true,
+			'search_terms'    => ''
+		);
+
+		$at = new BP_Core_Members_Template(
+			$r['type'],
+			$r['page'],
+			$r['per_page'],
+			$r['max'],
+			$r['user_id'],
+			$r['search_terms'],
+			$r['include'],
+			$r['populate_extras'],
+			$r['exclude'],
+			$r['meta_key'],
+			$r['meta_value'],
+			$r['page_arg'],
+			$r['member_type']
+		);
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/template/bpGetMemberClass.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/template/bpGetMemberClass.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/template/bpGetMemberClass.php	(revision 12265)
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @group members
+ */
+class BP_Tests_Members_Template_BpGetMemberClass extends BP_UnitTestCase {
+	/**
+	 * @ticket BP6996
+	 */
+	public function test_should_contain_member_type_classes() {
+		buddypress()->members->types = array();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		$u = self::factory()->user->create();
+		bp_set_member_type( $u, 'bar' );
+
+		if ( bp_has_members( array( 'include' => array( $u ) ) ) ) {
+			while ( bp_members() ) {
+				bp_the_member();
+				$found = bp_get_member_class();
+			}
+		}
+
+		global $members_template;
+		unset( $members_template );
+		buddypress()->members->types = array();
+
+		$this->assertContains( 'member-type-bar', $found );
+		$this->assertNotContains( 'member-type-foo', $found );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/template/bpGetMemberTypeDirectoryPermalink.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/template/bpGetMemberTypeDirectoryPermalink.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/template/bpGetMemberTypeDirectoryPermalink.php	(revision 12265)
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @group members
+ * @group member_types
+ */
+class BP_Tests_Members_Template_BpGetMemberTypeDirectoryPermalink extends BP_UnitTestCase {
+	public function setUp() {
+		parent::setUp();
+
+		buddypress()->members->types = array();
+	}
+
+	/**
+	 * @ticket BP6840
+	 */
+	public function test_should_default_to_current_member_type() {
+		bp_register_member_type( 'foo', array(
+			'has_directory' => true,
+		) );
+
+		add_filter( 'bp_get_current_member_type', array( $this, 'fake_current_member_type' ) );
+		$found = bp_get_member_type_directory_permalink( 'foo' );
+		remove_filter( 'bp_get_current_member_type', array( $this, 'fake_current_member_type' ) );
+
+		$this->assertContains( '/type/foo/', $found );
+	}
+
+	/**
+	 * @ticket BP6840
+	 */
+	public function test_member_type_param_should_override_current_member_type() {
+		bp_register_member_type( 'foo', array(
+			'has_directory' => true,
+		) );
+		bp_register_member_type( 'bar', array(
+			'has_directory' => true,
+		) );
+
+		add_filter( 'bp_get_current_member_type', array( $this, 'fake_current_member_type' ) );
+		$found = bp_get_member_type_directory_permalink( 'bar' );
+		remove_filter( 'bp_get_current_member_type', array( $this, 'fake_current_member_type' ) );
+
+		$this->assertContains( '/type/bar/', $found );
+	}
+
+	public function fake_current_member_type() {
+		return 'foo';
+	}
+
+	/**
+	 * @ticket BP6840
+	 */
+	public function test_should_return_empty_string_when_type_does_not_exist() {
+		$this->assertSame( '', bp_get_member_type_directory_permalink( 'foo' ) );
+	}
+
+	/**
+	 * @ticket BP6840
+	 */
+	public function test_should_return_empty_string_when_has_directory_is_false_for_type() {
+		bp_register_member_type( 'foo', array(
+			'has_directory' => false,
+		) );
+
+		$this->assertSame( '', bp_get_member_type_directory_permalink( 'foo' ) );
+	}
+
+	/**
+	 * @ticket BP6840
+	 */
+	public function test_successful_format() {
+		bp_register_member_type( 'foo', array(
+			'has_directory' => true,
+		) );
+
+		$expected = bp_get_members_directory_permalink() . 'type/foo/';
+
+		$this->assertSame( $expected, bp_get_member_type_directory_permalink( 'foo' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/members/types.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/members/types.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/members/types.php	(revision 12265)
@@ -0,0 +1,407 @@
+<?php
+
+/**
+ * @group members
+ * @group member_types
+ */
+class BP_Tests_Members_Types extends BP_UnitTestCase {
+	public function setUp() {
+		parent::setUp();
+
+		buddypress()->members->types = array();
+	}
+
+	public function test_bp_register_member_type_should_fail_for_existing_member_type() {
+		bp_register_member_type( 'foo' );
+		$this->assertWPError( bp_register_member_type( 'foo' ) );
+	}
+
+	public function test_bp_register_member_type_should_return_type_object() {
+		$this->assertInternalType( 'object', bp_register_member_type( 'foo' ) );
+	}
+
+	/**
+	 * @dataProvider illegal_names
+	 * @ticket BP5192
+	 */
+	public function test_illegal_names( $name ) {
+		$this->assertWPError( bp_register_member_type( $name ) );
+	}
+
+	public function illegal_names() {
+		return array(
+			array( 'any' ),
+			array( 'null' ),
+			array( '_none' ),
+		);
+	}
+
+	/**
+	 * @ticket BP6139
+	 */
+	public function test_bp_register_member_type_should_sanitize_member_type_key() {
+		$key = 'F//oo% -Bar';
+		$sanitized_key = 'foo-bar';
+
+		$object = bp_register_member_type( $key );
+		$this->assertSame( $sanitized_key, $object->name );
+	}
+
+	public function test_bp_register_member_type_should_store_member_type_string_as_name_property() {
+		$object = bp_register_member_type( 'foo' );
+		$this->assertSame( 'foo', $object->name );
+	}
+
+	public function test_bp_register_member_type_should_fill_in_missing_labels_with_ucfirst_member_type() {
+		$object = bp_register_member_type( 'foo' );
+		foreach ( $object->labels as $label ) {
+			$this->assertSame( 'Foo', $label );
+		}
+	}
+
+	/**
+	 * @ticket BP6125
+	 */
+	public function test_bp_register_member_type_should_respect_custom_name_label() {
+		$object = bp_register_member_type( 'foo', array(
+			'labels' => array(
+				'name' => 'Bar',
+			),
+		) );
+
+		// 'singular_name' falls back on 'name'.
+		$this->assertSame( 'Bar', $object->labels['name'] );
+		$this->assertSame( 'Bar', $object->labels['singular_name'] );
+	}
+
+	/**
+	 * @ticket BP6125
+	 */
+	public function test_bp_register_member_type_should_respect_custom_singular_name_label() {
+		$object = bp_register_member_type( 'foo', array(
+			'labels' => array(
+				'singular_name' => 'Bar',
+			),
+		) );
+
+		// 'name' is set to upper-case version of member type name.
+		$this->assertSame( 'Foo', $object->labels['name'] );
+		$this->assertSame( 'Bar', $object->labels['singular_name'] );
+	}
+
+	/**
+	 * @ticket BP6286
+	 */
+	public function test_bp_register_member_type_has_directory_should_default_to_true() {
+		$object = bp_register_member_type( 'foo', array(
+			'has_directory' => true,
+		) );
+
+		$this->assertTrue( $object->has_directory );
+		$this->assertSame( 'foo', $object->directory_slug );
+	}
+
+	/**
+	 * @ticket BP6286
+	 */
+	public function test_bp_register_member_type_has_directory_true() {
+		$object = bp_register_member_type( 'foo', array(
+			'has_directory' => true,
+		) );
+
+		$this->assertTrue( $object->has_directory );
+		$this->assertSame( 'foo', $object->directory_slug );
+	}
+
+	/**
+	 * @ticket BP6286
+	 */
+	public function test_bp_register_member_type_should_store_has_directory_false() {
+		$object = bp_register_member_type( 'foo', array(
+			'has_directory' => false,
+		) );
+
+		$this->assertFalse( $object->has_directory );
+		$this->assertSame( '', $object->directory_slug );
+	}
+
+	/**
+	 * @ticket BP6286
+	 */
+	public function test_bp_register_member_type_should_store_has_directory_string() {
+		$object = bp_register_member_type( 'foo', array(
+			'has_directory' => 'foos',
+		) );
+
+		$this->assertTrue( $object->has_directory );
+		$this->assertSame( 'foos', $object->directory_slug );
+	}
+
+	public function test_bp_get_member_type_object_should_return_null_for_non_existent_member_type() {
+		$this->assertSame( null, bp_get_member_type_object( 'foo' ) );
+	}
+
+	public function test_bp_get_member_type_object_should_return_type_object() {
+		bp_register_member_type( 'foo' );
+		$this->assertInternalType( 'object', bp_get_member_type_object( 'foo' ) );
+	}
+
+	public function test_bp_set_member_type_should_return_false_for_invalid_member_type() {
+		$this->assertFalse( bp_set_member_type( 'foo', 1 ) );
+	}
+
+	public function test_bp_set_member_type_should_remove_member_type_when_passing_an_empty_value() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_set_member_type( $u, 'foo' );
+
+		// Make sure it's set up.
+		$this->assertSame( 'foo', bp_get_member_type( $u ) );
+
+		$this->assertSame( array(), bp_set_member_type( $u, '' ) );
+		$this->assertFalse( bp_get_member_type( $u ) );
+	}
+
+	public function test_bp_set_member_type_success() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+
+		$this->assertNotEmpty( bp_set_member_type( $u, 'foo' ) );
+	}
+
+	public function test_bp_get_member_type_with_default_value_for_single() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u, 'foo' );
+		bp_set_member_type( $u, 'bar', true );
+
+		$this->assertSame( 'foo', bp_get_member_type( $u ) );
+	}
+
+	public function test_bp_get_member_type_with_single_true() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u, 'foo' );
+		bp_set_member_type( $u, 'bar', true );
+
+		$this->assertSame( 'foo', bp_get_member_type( $u, true ) );
+	}
+
+	public function test_bp_get_member_type_with_single_false() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u, 'foo' );
+		bp_set_member_type( $u, 'bar', true );
+
+		$this->assertEqualSets( array( 'foo', 'bar' ), bp_get_member_type( $u, false ) );
+	}
+
+	public function test_bp_get_member_type_should_return_false_when_no_value_is_found() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+
+		$this->assertFalse( bp_get_member_type( $u ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_bp_get_member_type_should_hit_cache() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_set_member_type( $u, 'foo' );
+
+		global $wpdb;
+
+		// Initial query should hit the database.
+		bp_get_member_type( $u );
+		$num_queries = $wpdb->num_queries;
+
+		// Next query should not hit the database.
+		bp_get_member_type( $u );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @group BP6193
+	 */
+	public function test_bp_members_prefetch_member_type_array_cache_set() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u1, 'foo' );
+		bp_set_member_type( $u1, 'bar', true );
+
+		// Get users so that the 'bp_user_query_populate_extras' is fired
+		// and members type prefetched
+		$users = bp_core_get_users( array( 'include' => array( $u1, $u2 ) ) );
+
+		// Get single member type
+		$this->assertSame( 'foo', bp_get_member_type( $u1, true ) );
+		$this->assertEmpty( bp_get_member_type( $u2, true ) );
+
+		// Get all member types for the user
+		$this->assertEqualSets( array( 'foo', 'bar' ), bp_get_member_type( $u1, false ) );
+		$this->assertEmpty( bp_get_member_type( $u2, false ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_bp_get_member_type_should_return_false_for_deleted_user() {
+		$u = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_set_member_type( $u, 'foo' );
+
+		// Prime the cache.
+		bp_get_member_type( $u );
+
+		if ( is_multisite() ) {
+			wpmu_delete_user( $u );
+		} else {
+			wp_delete_user( $u );
+		}
+
+		$this->assertFalse( wp_cache_get( $u, 'bp_member_type' ) );
+		$this->assertFalse( bp_get_member_type( $u ) );
+	}
+
+	/**
+	 * @group BP6242
+	 * @group cache
+	 */
+	public function test_bp_get_member_type_should_not_conflict_with_term_cache() {
+		global $wpdb;
+
+		// Offset IDs.
+		$dummy_terms = self::factory()->tag->create_many( 7 );
+
+		$u1 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_set_member_type( $u1, 'foo' );
+
+		// Fetch a term ID.
+		$terms = get_terms( bp_get_member_type_tax_name(), array( 'hide_empty' => false, 'fields' => 'all' ) );
+
+		// Make sure the user's ID matches a term ID, to force a cache confusion.
+		$u2 = self::factory()->user->create();
+		$new_user_id = $terms[0]->term_id;
+		$wpdb->update( $wpdb->users, array( 'ID' => $new_user_id ), array( 'ID' => $u2 ) );
+
+		bp_set_member_type( $new_user_id, 'foo' );
+
+		// Reprime the taxonomy cache.
+		$terms = get_terms( bp_get_member_type_tax_name(), array( 'hide_empty' => false, 'fields' => 'all' ) );
+
+		$this->assertSame( 'foo', bp_get_member_type( $new_user_id, true ) );
+	}
+
+	/**
+	 * @group BP6188
+	 */
+	public function test_bp_remove_member_type_should_return_false_when_member_type_is_empty() {
+		$this->assertFalse( bp_remove_member_type( 5, '' ) );
+	}
+
+	/**
+	 * @group BP6188
+	 */
+	public function test_bp_remove_member_type_should_return_false_when_member_type_is_invalid() {
+		$this->assertFalse( bp_remove_member_type( 5, 'foo' ) );
+	}
+
+	/**
+	 * @group BP6188
+	 */
+	public function test_bp_remove_member_type_should_return_false_when_member_is_not_of_provided_type() {
+		$u1 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u1, 'bar' );
+
+		$this->assertFalse( bp_remove_member_type( $u1, 'foo' ) );
+		$types = bp_get_member_type( $u1, false );
+		$this->assertEquals( array( 'bar' ), $types );
+	}
+
+	/**
+	 * @group BP6188
+	 */
+	public function test_bp_remove_member_type_should_return_true_for_successful_deletion() {
+		$u1 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u1, 'foo' );
+		bp_set_member_type( $u1, 'bar', true );
+
+		$this->assertTrue( bp_remove_member_type( $u1, 'foo' ) );
+		$types = bp_get_member_type( $u1, false );
+		$this->assertEquals( array( 'bar' ), $types );
+	}
+
+	/**
+	 * @group BP6138
+	 */
+	function test_bp_has_member_type_should_return_false_when_member_type_is_empty() {
+		$this->assertFalse( bp_has_member_type( 5, '' ) );
+	}
+
+	/**
+	 * @group BP6138
+	 */
+	function test_bp_has_member_type_should_return_false_when_member_type_is_invalid() {
+		$this->assertFalse( bp_has_member_type( 5, 'foo' ) );
+	}
+
+	/**
+	 * @group BP6138
+	 */
+	public function test_bp_has_member_type_should_return_false_when_member_id_is_empty() {
+		bp_register_member_type( 'foo' );
+
+		$this->assertFalse( bp_has_member_type( '', 'foo' ) );
+	}
+
+	/**
+	 * @group BP6138
+	 */
+	public function test_bp_has_member_type_should_return_false_when_member_is_not_of_provided_type() {
+		$u1 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u1, 'bar' );
+
+		$this->assertFalse( bp_has_member_type( $u1, 'foo' ) );
+	}
+
+	/**
+	 * @group BP6138
+	 */
+	public function test_bp_has_member_type_should_return_true_on_success() {
+		$u1 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+		bp_set_member_type( $u1, 'foo' );
+		bp_set_member_type( $u1, 'bar', true );
+
+		$this->assertTrue( bp_has_member_type( $u1, 'foo' ) );
+		$types = bp_get_member_type( $u1, false );
+		$this->assertEqualSets( array( 'bar', 'foo' ), $types );
+	}
+
+	public function test_bp_get_member_type_should_not_return_unregistered_types() {
+		$u1 = self::factory()->user->create();
+		bp_register_member_type( 'foo' );
+		bp_set_member_type( $u1, 'foo' );
+
+		// Directly set a type that hasn't been registered.
+		bp_set_object_terms( $u1, 'ugh', bp_get_member_type_tax_name(), true );
+
+		$type = bp_get_member_type( $u1, false );
+		$this->assertEquals( array( 'foo' ), $type );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/cache.php	(revision 12265)
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * @group messages
+ * @group cache
+ */
+class BP_Tests_Message_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_messages_update_meta_cache
+	 */
+	public function test_bp_messages_update_meta_cache() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create the thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+
+		// create a reply
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $message_1->thread_id,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+
+		$m1 = $message_1->id;
+		$m2 = $message_2->id;
+
+		// add cache for each message
+		bp_messages_update_meta( $m1, 'utensil',  'knive' );
+		bp_messages_update_meta( $m1, 'is_knive', 'yes' );
+
+		bp_messages_update_meta( $m2, 'utensil',  'spoon' );
+		bp_messages_update_meta( $m2, 'is_knive', 'no' );
+		bp_messages_update_meta( $m2, 'is_spoon', 'yes' );
+
+		// prime cache
+		bp_messages_get_meta( $m1, 'utensil' );
+
+		// Ensure an empty cache for second message
+		wp_cache_delete( $m2, 'message_meta' );
+
+		// update message meta cache
+		bp_messages_update_meta_cache( array( $m1, $m2 ) );
+
+		$expected = array(
+			$m1 => array(
+				'utensil' => array(
+					'knive',
+				),
+				'is_knive' => array(
+					'yes',
+				),
+			),
+			$m2 => array(
+				'utensil' => array(
+					'spoon',
+				),
+				'is_knive' => array(
+					'no',
+				),
+				'is_spoon' => array(
+					'yes',
+				),
+			),
+		);
+
+		$found = array(
+			$m1 => wp_cache_get( $m1, 'message_meta' ),
+			$m2 => wp_cache_get( $m2, 'message_meta' ),
+		);
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	/**
+	 * @group bp_messages_update_meta_cache
+	 * @group bp_thread_has_messages
+	 */
+	public function test_bp_messages_metadata_cache_in_message_loop() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create the message and thread
+		$m = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'Oy',
+		) );
+
+		// add message cache
+		bp_messages_update_meta( $m->id, 'yolo', 'gah' );
+
+		// prime meta cache in message loop
+		bp_thread_has_messages( array(
+			'thread_id' => $m->thread_id,
+			'update_meta_cache' => true
+		) );
+
+		$this->assertNotEmpty( wp_cache_get( $m->id, 'message_meta' ) );
+	}
+
+	/**
+	 * @group bp_messages_delete_meta
+	 * @group messages_delete_thread
+	 */
+	public function test_bp_messages_delete_metadata_cache_on_thread_delete() {
+		$this->old_current_user = get_current_user_id();
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create the thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'Oy',
+		) );
+
+		// create a reply
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $message_1->thread_id,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => 'Yo',
+		) );
+
+		$m1 = $message_1->id;
+		$m2 = $message_2->id;
+		$t1 = $message_1->thread_id;
+
+		// add message meta
+		bp_messages_update_meta( $m1, 'yolo', 'gah' );
+		bp_messages_update_meta( $m2, 'yolo', 'bah' );
+
+		// prime message meta cache
+		bp_messages_get_meta( $m1, 'yolo' );
+		bp_messages_get_meta( $m2, 'yolo' );
+
+		// delete thread
+		// to outright delete a thread, both recipients must delete it
+		$this->set_current_user( $u1 );
+		messages_delete_thread( $t1 );
+		$this->set_current_user( $u2 );
+		messages_delete_thread( $t1 );
+
+		// assert empty meta cache
+		$this->assertEmpty( wp_cache_get( $m1, 'message_meta' ) );
+		$this->assertEmpty( wp_cache_get( $m2, 'message_meta' ) );
+
+		// cleanup
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	/**
+	 * Helper method to grab the message IDs from a message thread.
+	 *
+	 * @param int $thread_id The message thread ID
+	 * @return array
+	 */
+	protected function get_message_ids( $thread_id = 0 ) {
+		$thread = new BP_Messages_Thread( $thread_id );
+		return wp_list_pluck( $thread->messages, 'id' );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/class.bp-messages-notice.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/class.bp-messages-notice.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/class.bp-messages-notice.php	(revision 12265)
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @group notices
+ */
+class BP_Tests_BP_Messages_Notice_TestCases extends BP_UnitTestCase {
+
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_get_active_notices() {
+		// send notice
+		$subject = 'Test notice';
+		$message = 'This is a notice';
+		messages_send_notice( $subject, $message );
+
+		// now get the active notice and assert
+		$notice = BP_Messages_Notice::get_active();
+		$this->assertEquals( $subject, $notice->subject );
+		$this->assertEquals( $message, $notice->message );
+
+		// deactivate notice and make sure cache is invalidated
+		$notice->deactivate();
+		$this->assertFalse( wp_cache_get( 'active_notice', 'bp_messages' ) );
+
+		// create a new notice
+		$subject2 = 'Another notice';
+		$message2 = 'Say what?';
+		messages_send_notice( $subject2, $message2 );
+
+		// now get the new active notice
+		BP_Messages_Notice::get_active();
+
+		// grab the cache and make sure it equals our new notice
+		$cache = wp_cache_get( 'active_notice', 'bp_messages' );
+		$this->assertEquals( $subject2, $cache->subject );
+		$this->assertEquals( $message2, $cache->message );
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/class.bp-messages-thread.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/class.bp-messages-thread.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/class.bp-messages-thread.php	(revision 12265)
@@ -0,0 +1,440 @@
+<?php
+
+/**
+ * @group BP_Messages_Thread
+ * @group messages
+ */
+class BP_Tests_BP_Messages_Thread extends BP_UnitTestCase {
+
+	/**
+	 * @group cache
+	 */
+	public function test_construct_cache() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		// prime cache
+		new BP_Messages_Thread( $message->thread_id );
+
+		// Cache should exist
+		$this->assertThat(
+			wp_cache_get( $message->thread_id, 'bp_messages_threads' ),
+			$this->logicalNot( $this->equalTo( false ) ),
+			'Message thread cache should exist.'
+		);
+	}
+
+	/**
+	 * @group order
+	 */
+	public function test_construct_order_desc() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+		$m1 = $message_1->id;
+
+		// create reply
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id' => $message_1->thread_id,
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'content' => 'Bar'
+		) );
+		$m2 = $message_2->id;
+
+		// now get thread by DESC
+		$thread = new BP_Messages_Thread( $message_1->thread_id, 'DESC' );
+
+		// assert!
+		$this->assertEquals(
+			array( $m2, $m1 ),
+			wp_list_pluck( $thread->messages, 'id' )
+		);
+	}
+
+	/**
+	 * @group get_current_threads_for_user
+	 */
+	public function test_get_current_threads_for_user_with_search_terms_inbox() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$message_2 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Bar',
+		) );
+
+		$threads = BP_Messages_Thread::get_current_threads_for_user( array(
+			'user_id' => $u2,
+			'search_terms' => 'ar',
+		) );
+
+		$expected = array( $message_2->thread_id );
+		$found = wp_parse_id_list( wp_list_pluck( $threads['threads'], 'thread_id' ) );
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group get_current_threads_for_user
+	 */
+	public function test_get_current_threads_for_user_with_search_terms_sentbox() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$message_2 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Bar',
+		) );
+
+		$threads = BP_Messages_Thread::get_current_threads_for_user( array(
+			'user_id' => $u1,
+			'box' => 'sentbox',
+			'search_terms' => 'ar',
+		) );
+
+		$expected = array( $message_2->thread_id );
+		$found = wp_parse_id_list( wp_list_pluck( $threads['threads'], 'thread_id' ) );
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group get_current_threads_for_user
+	 * @expectedDeprecated BP_Messages_Thread::get_current_threads_for_user
+	 */
+	public function test_get_current_threads_for_user_with_old_args() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$message_2 = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Bar',
+		) );
+
+		$threads = BP_Messages_Thread::get_current_threads_for_user( $u1, 'sentbox', 'all', null, null, 'ar' );
+
+		$expected = array( $message_2->thread_id );
+		$found = wp_parse_id_list( wp_list_pluck( $threads['threads'], 'thread_id' ) );
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group get_recipients
+	 * @group cache
+	 */
+	public function test_get_recipients_should_cache_its_values() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$thread = new BP_Messages_Thread( $message->thread_id );
+		$recipients = $thread->get_recipients();
+
+		$num_queries = $wpdb->num_queries;
+		$recipients_cached = $thread->get_recipients();
+
+		$this->assertEquals( $recipients, $recipients_cached );
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @group get_recipients
+	 * @group cache
+	 */
+	public function test_get_recipients_cache_should_be_busted_when_thread_message_is_sent() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$thread = new BP_Messages_Thread( $message->thread_id );
+		$recipients = $thread->get_recipients();
+
+		// Verify that the cache is populated.
+		$num_queries = $wpdb->num_queries;
+		$recipients_cached = $thread->get_recipients();
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+
+		messages_new_message( array(
+			'sender_id' => $u2,
+			'thread_id' => $message->thread_id,
+			'recipients' => array( $u1 ),
+			'subject' => 'Bar',
+			'content' => 'Baz',
+		) );
+
+		// Cache should be empty.
+		$num_queries = $wpdb->num_queries;
+		$recipients_uncached = $thread->get_recipients();
+		$this->assertEquals( $num_queries + 1, $wpdb->num_queries );
+	}
+
+	/**
+	 * @group get_recipients
+	 * @group cache
+	 */
+	public function test_get_recipients_cache_should_be_busted_when_single_thread_is_deleted() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$t1 = $message->thread_id;
+
+		$thread = new BP_Messages_Thread( $t1 );
+		$recipients = $thread->get_recipients();
+
+		// Verify that the cache is populated.
+		$num_queries = $wpdb->num_queries;
+		$recipients_cached = $thread->get_recipients();
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+
+		messages_delete_thread( $t1 );
+
+		// Cache should be empty.
+		$this->assertFalse( wp_cache_get( 'thread_recipients_' . $t1, 'bp_messages' ) );
+	}
+
+	/**
+	 * @group get_recipients
+	 * @group cache
+	 */
+	public function test_get_recipients_cache_should_be_busted_when_array_of_threads_is_deleted() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$t1 = $message->thread_id;
+
+		$thread = new BP_Messages_Thread( $t1 );
+		$recipients = $thread->get_recipients();
+
+		// Verify that the cache is populated.
+		$num_queries = $wpdb->num_queries;
+		$recipients_cached = $thread->get_recipients();
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+
+		messages_delete_thread( array( $t1 ) );
+
+		// Cache should be empty.
+		$this->assertFalse( wp_cache_get( 'thread_recipients_' . $t1, 'bp_messages' ) );
+	}
+
+	/**
+	 * @group get_recipients
+	 * @group cache
+	 */
+	public function test_get_recipients_cache_should_be_busted_when_thread_is_read() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$t1 = $message->thread_id;
+
+		$thread = new BP_Messages_Thread( $t1 );
+		$recipients = $thread->get_recipients();
+
+		// Verify that the cache is populated.
+		$num_queries = $wpdb->num_queries;
+		$recipients_cached = $thread->get_recipients();
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+
+		// Mark thread as read
+		$current_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+		messages_mark_thread_read( $t1 );
+
+		// Cache should be empty.
+		$this->assertFalse( wp_cache_get( 'thread_recipients_' . $t1, 'bp_messages' ) );
+
+		$this->set_current_user( $current_user );
+	}
+
+	/**
+	 * @group get_recipients
+	 * @group cache
+	 */
+	public function test_get_recipients_cache_should_be_busted_when_thread_is_unread() {
+		global $wpdb;
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$t1 = $message->thread_id;
+
+		$thread = new BP_Messages_Thread( $t1 );
+		$recipients = $thread->get_recipients();
+
+		// Verify that the cache is populated.
+		$num_queries = $wpdb->num_queries;
+		$recipients_cached = $thread->get_recipients();
+		$this->assertEquals( $num_queries, $wpdb->num_queries );
+
+		// Mark thread as unread
+		$current_user = get_current_user_id();
+		$this->set_current_user( $u2 );
+		messages_mark_thread_unread( $t1 );
+
+		// Cache should be empty.
+		$this->assertFalse( wp_cache_get( 'thread_recipients_' . $t1, 'bp_messages' ) );
+
+		$this->set_current_user( $current_user );
+	}
+
+	/**
+	 * @group check_access
+	 */
+	public function test_check_access_valid_thread() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$t1 = $message->thread_id;
+
+		// save recipient ID
+		$thread = new BP_Messages_Thread( $t1 );
+		$r1 = wp_list_pluck( $thread->recipients, 'id' );
+		$r1 = array_pop( $r1 );
+
+		$this->assertEquals( $r1, BP_Messages_Thread::check_access( $t1, $u1 ) );
+	}
+
+	/**
+	 * @group check_access
+	 */
+	public function test_check_access_invalid_thread() {
+		$this->assertEquals( null, BP_Messages_Thread::check_access( 999, 1 ) );
+	}
+
+	/**
+	 * @group is_valid
+	 */
+	public function test_is_valid_valid_thread() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+		) );
+
+		$t1 = $message->thread_id;
+
+		$this->assertEquals( $t1, BP_Messages_Thread::is_valid( $t1 ) );
+	}
+
+	/**
+	 * @group is_valid
+	 */
+	public function test_is_valid_invalid_thread() {
+		$this->assertEquals( null, BP_Messages_Thread::is_valid( 999 ) );
+	}
+
+	/**
+	 * @group last_message
+	 */
+	public function test_last_message_populated() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$date = bp_core_current_time();
+
+		$message = self::factory()->message->create_and_get( array(
+			'sender_id' => $u1,
+			'recipients' => array( $u2 ),
+			'subject' => 'Foo',
+			'date_sent' => $date,
+			'content' => 'Bar and baz.',
+		) );
+
+		$t1 = $message->thread_id;
+
+		$thread = new BP_Messages_Thread( $t1 );
+
+		$this->assertNotNull( $thread->last_message_id );
+		$this->assertEquals( 'Foo', $thread->last_message_subject );
+		$this->assertEquals( $u1, $thread->last_sender_id );
+		$this->assertEquals( $date, $thread->last_message_date );
+		$this->assertEquals( 'Bar and baz.', $thread->last_message_content );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/functions.php	(revision 12265)
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @group messages
+ * @group functions
+ */
+class BP_Tests_Messages_Functions extends BP_UnitTestCase {
+
+	/**
+	 * @group counts
+	 */
+	public function test_get_unread_count() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// send a private message
+		$t1 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+		) );
+
+		// get unread count for $u2
+		$this->set_current_user( $u2 );
+		$this->assertEquals( 1, messages_get_unread_count( $u2 ) );
+
+		// send another message and get recheck unread count
+		$t2 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+		) );
+		$this->assertEquals( 2, messages_get_unread_count( $u2 ) );
+
+		// mark one message as read
+		messages_mark_thread_read( $t1 );
+
+		// recheck unread count
+		$this->assertEquals( 1, messages_get_unread_count( $u2 ) );
+	}
+
+	/**
+	 * @group messages_new_message
+	 */
+	public function test_messages_new_message_invalid_recipient_error_message() {
+		$u1 = self::factory()->user->create();
+
+		// attempt to send a private message to an invalid username
+		$t1 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'recipients' => array( 'homerglumpkin' ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+			'error_type' => 'wp_error'
+		) );
+
+		$this->assertSame( 'Message could not be sent because you have entered an invalid username. Please try again.', $t1->get_error_message() );
+	}
+
+	/**
+	 * @group messages_new_message
+	 */
+	public function test_messages_new_message_wp_error_generic() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// Emulate a plugin disabling messages.
+		add_action( 'messages_message_before_save', array( $this, 'remove_recipients_before_save' ) );
+
+		// send a private message
+		$t1 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+			'error_type' => 'wp_error'
+		) );
+
+		$this->assertNotEmpty( $t1->get_error_code() );
+
+		remove_action( 'messages_message_before_save', array( $this, 'remove_recipients_before_save' ) );
+	}
+
+	/**
+	 * Helper method for test_messages_new_message_wp_error_generic().
+	 */
+	public function remove_recipients_before_save( $message ) {
+		$message->recipients = array();
+	}
+
+	/**
+	 * @ticket BP7819
+	 * @ticket BP7698
+	 */
+	public function test_bp_messages_personal_data_exporter() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$time = time();
+
+		$t1 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+			'date_sent'  => date( 'Y-m-d H:i:s', $time - ( 3 * HOUR_IN_SECONDS ) ),
+		) );
+
+		$t1m1 = messages_new_message( array(
+			'sender_id'  => $u2,
+			'thread_id'  => $t1,
+			'recipients' => array( $u1 ),
+			'subject'    => 'Reply to ' . $t1,
+			'content'    => 'Hey there!',
+			'date_sent'  => date( 'Y-m-d H:i:s', $time - ( 2 * HOUR_IN_SECONDS ) ),
+		) );
+
+		$t1m2 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'thread_id'  => $t1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'Reply to ' . $t1,
+			'content'    => 'Hey there!',
+			'date_sent'  => date( 'Y-m-d H:i:s', $time - ( 2 * HOUR_IN_SECONDS ) ),
+		) );
+
+		$t2 = messages_new_message( array(
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+			'date_sent'  => date( 'Y-m-d H:i:s', $time - ( 5 * HOUR_IN_SECONDS ) ),
+		) );
+
+		$t2m1 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'thread_id'  => $t2,
+			'recipients' => array( $u2 ),
+			'subject'    => 'Reply to ' . $t2,
+			'content'    => 'Hey there!',
+			'date_sent'  => date( 'Y-m-d H:i:s', $time - ( 4 * HOUR_IN_SECONDS ) ),
+		) );
+
+		$test_user = new WP_User( $u1 );
+
+		$actual = bp_messages_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+
+		// Number of exported messages.
+		$this->assertSame( 3, count( $actual['data'] ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/notifications.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/notifications.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/notifications.php	(revision 12265)
@@ -0,0 +1,151 @@
+<?php
+
+/**
+ * @group notifications
+ * @group messages
+ */
+class BP_Tests_Messages_Notifications extends BP_UnitTestCase {
+
+	protected $filter_fired;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->filter_fired = '';
+	}
+
+	/**
+	 * @group messages_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_messages_multiple_new_message_notification_nonstring_filter() {
+		// Dummy thread ID
+		$t = 12;
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Admin
+		$n = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'item_id' => $t,
+			'component_action' => 'new_message',
+		) );
+
+		add_filter( 'bp_messages_multiple_new_message_notification', array( $this, 'notification_filter_callback' ) );
+		$n = messages_format_notifications( 'new_message', $n, '', 2, 'array' );
+		remove_filter( 'bp_messages_multiple_new_message_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_messages_multiple_new_message_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group messages_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_messages_single_new_message_notification_nonstring_filter() {
+		// Dummy thread ID
+		$t = 12;
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Admin
+		$n = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'item_id' => $t,
+			'component_action' => 'new_message',
+		) );
+
+		add_filter( 'bp_messages_single_new_message_notification', array( $this, 'notification_filter_callback' ) );
+		$n = messages_format_notifications( 'new_message', $n, '', 1, 'array' );
+		remove_filter( 'bp_messages_single_new_message_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_messages_single_new_message_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group messages_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_messages_multiple_new_message_notification_string_filter() {
+		// Dummy thread ID
+		$t = 12;
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Admin
+		$n = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'item_id' => $t,
+			'component_action' => 'new_message',
+		) );
+
+		add_filter( 'bp_messages_multiple_new_message_notification', array( $this, 'notification_filter_callback' ) );
+		$n = messages_format_notifications( 'new_message', $n, '', 2 );
+		remove_filter( 'bp_messages_multiple_new_message_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_messages_multiple_new_message_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @group messages_format_notifications
+	 */
+	public function test_friends_format_notifications_bp_messages_single_new_message_notification_string_filter() {
+		// Dummy thread ID
+		$t = 12;
+		$u = self::factory()->user->create();
+		$this->set_current_user( $u );
+
+		// Admin
+		$n = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'item_id' => $t,
+			'component_action' => 'new_message',
+		) );
+
+		add_filter( 'bp_messages_single_new_message_notification', array( $this, 'notification_filter_callback' ) );
+		$n = messages_format_notifications( 'new_message', $n, '', 1 );
+		remove_filter( 'bp_messages_single_new_message_notification', array( $this, 'notification_filter_callback' ) );
+
+		$this->assertSame( 'bp_messages_single_new_message_notification', $this->filter_fired );
+	}
+
+	/**
+	 * @ticket BP6329
+	 */
+	public function test_messages_notifications_should_be_deleted_when_corresponding_message_is_deleted() {
+		if ( ! bp_is_active( 'messages' ) ) {
+			$this->markTestSkipped( __METHOD__ . ' requires the Messages component.' );
+		}
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$t1 = messages_new_message( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'A new message',
+			'content'    => 'Hey there!',
+		) );
+
+		// Verify that a notification has been created for the message.
+		$n1 = BP_Notifications_Notification::get( array(
+			'component' => 'messages',
+			'user_id' => $u2,
+		) );
+		$this->assertNotEmpty( $n1 );
+
+		$this->assertTrue( messages_delete_thread( $t1 ) );
+
+		$n2 = BP_Notifications_Notification::get( array(
+			'component' => 'messages',
+			'user_id' => $u2,
+		) );
+		$this->assertSame( array(), $n2 );
+	}
+
+	public function notification_filter_callback( $value ) {
+		$this->filter_fired = current_filter();
+		return $value;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/star.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/star.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/star.php	(revision 12265)
@@ -0,0 +1,184 @@
+<?php
+/**
+ * @group messages
+ * @group star
+ */
+class BP_Tests_Messages_Star_ extends BP_UnitTestCase {
+
+	/**
+	 * @group bp_messages_is_message_starred
+	 * @group bp_messages_star_set_action
+	 */
+	public function test_is_message_starred() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create the thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+
+		// create a reply
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $message_1->thread_id,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+
+		$m1 = $message_1->id;
+		$m2 = $message_2->id;
+
+		// star the second message
+		$star = bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m2,
+		) );
+
+		// assert that star is set
+		$this->assertTrue( $star );
+		$this->assertTrue( bp_messages_is_message_starred( $m2, $u1 ) );
+
+		// unstar the second message
+		$unstar = bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m2,
+			'action'     => 'unstar'
+		) );
+
+		// assert that star is removed
+		$this->assertTrue( $unstar );
+		$this->assertFalse( bp_messages_is_message_starred( $m2, $u1 ) );
+	}
+
+	/**
+	 * @group bp_messages_star_set_action
+	 * @group bulk
+	 */
+	public function test_star_set_action_bulk_unstar() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create the thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+
+		// create a reply
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $message_1->thread_id,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+
+		$m1 = $message_1->id;
+		$m2 = $message_2->id;
+		$t1 = $message_1->thread_id;
+
+		// star all messages
+		bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m1,
+		) );
+		bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m2,
+		) );
+
+		// assert that stars are set
+		$this->assertTrue( bp_messages_is_message_starred( $m1, $u1 ) );
+		$this->assertTrue( bp_messages_is_message_starred( $m2, $u1 ) );
+
+		// unstar all messages
+		bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'thread_id'  => $t1,
+			'action'     => 'unstar',
+			'bulk'       => true
+		) );
+
+		// assert that star is removed
+		$this->assertFalse( bp_messages_is_message_starred( $m1, $u1 ) );
+		$this->assertFalse( bp_messages_is_message_starred( $m2, $u1 ) );
+	}
+
+	/**
+	 * @group bp_messages_filter_starred_message_threads
+	 */
+	public function test_get_starred_threads_should_not_include_deleted_thread() {
+		$old_current_user = get_current_user_id();
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create three threads
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'A',
+		) );
+		$message_2 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'B',
+		) );
+		$message_3 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'C',
+		) );
+
+		// grab the message ids as individual variables
+		$m1 = $message_1->id;
+		$m2 = $message_2->id;
+		$m3 = $message_3->id;
+
+		$t2 = $message_2->thread_id;
+
+		// star all threads
+		bp_messages_star_set_action( array(
+			'user_id'    => $u2,
+			'message_id' => $m1,
+		) );
+		bp_messages_star_set_action( array(
+			'user_id'    => $u2,
+			'message_id' => $m2,
+		) );
+		bp_messages_star_set_action( array(
+			'user_id'    => $u2,
+			'message_id' => $m3,
+		) );
+
+		// delete the second thread
+		$this->set_current_user( $u2 );
+		messages_delete_thread( $t2 );
+
+		// load the starred threads loop
+		global $messages_template;
+		add_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+		bp_has_message_threads();
+		remove_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+
+		// assert that second thread isn't in starred thread loop
+		$thread_ids = wp_list_pluck( $messages_template->threads, 'thread_id' );
+		$this->assertFalse( in_array( $t2, $thread_ids ) );
+
+		// reset
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * Helper method to grab the message IDs from a message thread.
+	 *
+	 * @param int $thread_id The message thread ID
+	 * @return array
+	 */
+	protected function get_message_ids( $thread_id = 0 ) {
+		$thread = new BP_Messages_Thread( $thread_id );
+		return wp_list_pluck( $thread->messages, 'id' );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/messages/template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/messages/template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/messages/template.php	(revision 12265)
@@ -0,0 +1,362 @@
+<?php
+
+/**
+ * @group messages
+ * @group template
+ */
+class BP_Tests_Messages_Template extends BP_UnitTestCase {
+	/**
+	 * @group bp_has_message_threads
+	 */
+	public function test_has_message_threads() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create initial thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+		) );
+
+		// create some replies to thread
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id' => $message_1->thread_id,
+			'sender_id' => $u2,
+			'recipients' => array( $u1 ),
+		) );
+		$message_3 = self::factory()->message->create_and_get( array(
+			'thread_id' => $message_1->thread_id,
+			'sender_id' => $u2,
+			'recipients' => array( $u1 ),
+		) );
+
+		$messages_template = new BP_Messages_Box_Template( array( 'user_id' => $u1 ) );
+
+		$this->assertEquals( 1, $messages_template->thread_count );
+		$this->assertSame( array( $message_1->thread_id ), wp_list_pluck( $messages_template->threads, 'thread_id' ) );
+	}
+
+	/**
+	 * @group bp_has_message_threads
+	 *
+	 * @expectedDeprecated BP_Messages_Box_Template::__construct
+	 */
+	public function test_has_message_threads_old_args() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create initial thread
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+		) );
+
+		// create some replies to thread
+		$message_2 = self::factory()->message->create_and_get( array(
+			'thread_id' => $message_1->thread_id,
+			'sender_id' => $u2,
+			'recipients' => array( $u1 ),
+		) );
+		$message_3 = self::factory()->message->create_and_get( array(
+			'thread_id' => $message_1->thread_id,
+			'sender_id' => $u2,
+			'recipients' => array( $u1 ),
+		) );
+
+		$messages_template = new BP_Messages_Box_Template( $u1 );
+
+		$this->assertEquals( 1, $messages_template->thread_count );
+		$this->assertSame( array( $message_1->thread_id ), wp_list_pluck( $messages_template->threads, 'thread_id' ) );
+	}
+
+	/**
+	 * @group bp_has_message_threads
+	 * @group meta_query
+	 */
+	public function test_thread_has_messages_meta_query() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create some threads
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+		$message_2 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'Oy',
+		) );
+
+		// misc threads
+		self::factory()->message->create_many( 3, array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+		) );
+
+		$t1 = $message_1->thread_id;
+		$t2 = $message_2->thread_id;
+
+		// create some replies for thread 1
+		$message_3 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+		$message_4 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'content'    => "I see you've played knivey-spooney before.",
+		) );
+		$message_5 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => '*crickets*',
+		) );
+
+		// create some replies for thread 2
+		$message_6 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t2,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "Oy yourself.",
+		) );
+
+		// belong to $t1
+		$m1 = $message_1->id;
+		$m3 = $message_3->id;
+		$m4 = $message_4->id;
+		$m5 = $message_5->id;
+
+		// belong to $t2
+		$m2 = $message_2->id;
+		$m6 = $message_6->id;
+
+		// add meta for some of the messages
+		bp_messages_update_meta( $m1, 'utensil',  'knive' );
+		bp_messages_update_meta( $m1, 'is_knive', 'yes' );
+		bp_messages_update_meta( $m1, "starred_by_user_{$u2}", true );
+
+		bp_messages_update_meta( $m2, 'utensil',  'spoon' );
+		bp_messages_update_meta( $m2, 'is_knive', 'no' );
+		bp_messages_update_meta( $m2, 'is_spoon', 'yes' );
+
+		bp_messages_update_meta( $m3, "starred_by_user_{$u2}", true );
+
+		bp_messages_update_meta( $m6, "starred_by_user_{$u2}", true );
+
+		// now, do the message thread loop query
+		global $messages_template;
+		bp_has_message_threads( array(
+			'user_id' => $u2,
+			'meta_query' => array(
+				array(
+					'key' => "starred_by_user_{$u2}"
+				),
+			)
+		) );
+
+		$this->assertEquals( 2, $messages_template->thread_count );
+		$this->assertEqualSets( array( $t1, $t2 ), wp_list_pluck( $messages_template->threads, 'thread_id' ) );
+	}
+
+	/**
+	 * @group bp_has_message_threads
+	 * @group meta_query
+	 */
+	public function test_thread_has_messages_meta_query_multiple_clauses_relation_and() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create some threads
+		$message_1 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+		$message_2 = self::factory()->message->create_and_get( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'Oy',
+		) );
+
+		// misc threads
+		self::factory()->message->create_many( 3, array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+		) );
+
+		$t1 = $message_1->thread_id;
+		$t2 = $message_2->thread_id;
+
+		// create some replies for thread 1
+		$message_3 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+		$message_4 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'content'    => "I see you've played knivey-spooney before.",
+		) );
+		$message_5 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => '*crickets*',
+		) );
+
+		// create some replies for thread 2
+		$message_6 = self::factory()->message->create_and_get( array(
+			'thread_id'  => $t2,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "Oy yourself.",
+		) );
+
+		// belong to $t1
+		$m1 = $message_1->id;
+		$m3 = $message_3->id;
+		$m4 = $message_4->id;
+		$m5 = $message_5->id;
+
+		// belong to $t2
+		$m2 = $message_2->id;
+		$m6 = $message_6->id;
+
+		// add meta for some of the messages
+		bp_messages_update_meta( $m1, 'utensil',  'knive' );
+		bp_messages_update_meta( $m1, 'is_knive', 'yes' );
+		bp_messages_update_meta( $m1, "starred_by_user_{$u2}", true );
+
+		bp_messages_update_meta( $m2, 'utensil',  'spoon' );
+		bp_messages_update_meta( $m2, 'is_knive', 'no' );
+		bp_messages_update_meta( $m2, 'is_spoon', 'yes' );
+
+		bp_messages_update_meta( $m3, "starred_by_user_{$u2}", true );
+
+		bp_messages_update_meta( $m6, "starred_by_user_{$u2}", true );
+
+		// now, do the message thread loop query
+		global $messages_template;
+		bp_has_message_threads( array(
+			'user_id' => $u2,
+			'meta_query' => array(
+				'relation' => 'AND',
+				array(
+					'key' => "starred_by_user_{$u2}"
+				),
+				array(
+					'key' => 'utensil',
+					'value' => 'knive',
+				),
+			)
+		) );
+
+		$this->assertEquals( 1, $messages_template->thread_count );
+		$this->assertEqualSets( array( $t1 ), wp_list_pluck( $messages_template->threads, 'thread_id' ) );
+	}
+
+	/**
+	 * @group bp_has_message_threads
+	 */
+	public function test_has_message_threads_anonymous_user_should_not_see_threads() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create initial thread
+		self::factory()->message->create( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+		) );
+
+		// set user to anonymous
+		$old_current_user = get_current_user_id();
+		$this->set_current_user( 0 );
+
+		// now, do the message thread query
+		global $messages_template;
+		bp_has_message_threads();
+
+		// assert!
+		$this->assertEquals( 0, $messages_template->thread_count );
+		$this->assertEmpty( $messages_template->threads );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Messages_Box_Template
+	 */
+	public function test_bp_messages_template_should_give_precedence_to_mpage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['mpage'] = '5';
+
+		$at = new BP_Messages_Box_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Messages_Box_Template
+	 */
+	public function test_bp_messages_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['mpage'] = '0';
+
+		$at = new BP_Messages_Box_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Messages_Box_Template
+	 */
+	public function test_bp_messages_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Messages_Box_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Messages_Box_Template
+	 */
+	public function test_bp_messages_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Messages_Box_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/class-bp-notifications-notification.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/class-bp-notifications-notification.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/class-bp-notifications-notification.php	(revision 12265)
@@ -0,0 +1,312 @@
+<?php
+
+/**
+ * @group notifications
+ */
+class BP_Tests_BP_Notifications_Notification_TestCases extends BP_UnitTestCase {
+	/**
+	 * @group get
+	 */
+	public function test_get_null_component_name() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+		) );
+
+		// temporarily turn on groups, shut off messages
+		$groups_toggle = isset( buddypress()->active_components['groups'] );
+		$messages_toggle = isset( buddypress()->active_components['messages'] );
+		buddypress()->active_components['groups'] = 1;
+		unset( buddypress()->active_components['messages'] );
+
+		$n = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+		) );
+
+		// Check that the correct items are pulled up
+		$expected = array( $n1 );
+		$actual = wp_list_pluck( $n, 'id' );
+		$this->assertEquals( $expected, $actual );
+
+		// reset copmonent toggles
+		if ( $groups_toggle ) {
+			buddypress()->active_components['groups'] = 1;
+		} else {
+			unset( buddypress()->active_components['groups'] );
+		}
+
+		if ( $messages_toggle ) {
+			buddypress()->active_components['messages'] = 1;
+		} else {
+			unset( buddypress()->active_components['messages'] );
+		}
+	}
+
+	/**
+	 * @group get_total_count
+	 * @ticket BP5300
+	 */
+	public function test_get_total_count_null_component_name() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+		) );
+
+		// temporarily turn on groups, shut off messages
+		$groups_toggle = isset( buddypress()->active_components['groups'] );
+		$messages_toggle = isset( buddypress()->active_components['messages'] );
+		buddypress()->active_components['groups'] = 1;
+		unset( buddypress()->active_components['messages'] );
+
+		$n = BP_Notifications_Notification::get_total_count( array(
+			'user_id' => $u,
+		) );
+
+		// Check that the correct items are pulled up
+		$this->assertEquals( 1, $n );
+
+		// reset copmonent toggles
+		if ( $groups_toggle ) {
+			buddypress()->active_components['groups'] = 1;
+		} else {
+			unset( buddypress()->active_components['groups'] );
+		}
+
+		if ( $messages_toggle ) {
+			buddypress()->active_components['messages'] = 1;
+		} else {
+			unset( buddypress()->active_components['messages'] );
+		}
+	}
+
+	/**
+	 * @group get_total_count
+	 * @ticket BP5300
+	 */
+	public function test_get_total_count_with_component_name() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+		) );
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+		) );
+
+		$n = BP_Notifications_Notification::get_total_count( array(
+			'user_id' => $u,
+			'component_name' => array( 'messages' ),
+		) );
+
+		$this->assertEquals( 1, $n );
+	}
+
+	/**
+	 * @group order_by
+	 * @group sort_order
+	 */
+	public function test_order_by_date() {
+		$now = time();
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'friends',
+			'user_id' => $u,
+			'date_notified' => date( 'Y-m-d H:i:s', $now - 500 ),
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'date_notified' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'date_notified' => date( 'Y-m-d H:i:s', $now - 1000 ),
+		) );
+
+		$n = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+			'order_by' => 'date_notified',
+			'sort_order' => 'DESC',
+		) );
+
+		// Check that the correct items are pulled up
+		$expected = array( $n2, $n1, $n3 );
+		$actual = wp_list_pluck( $n, 'id' );
+		$this->assertEquals( $expected, $actual );
+	}
+
+	/**
+	 * @group is_new
+	 */
+	public function test_is_new_true() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'friends',
+			'user_id' => $u,
+			'is_new' => false,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+
+		$n = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+
+		// Check that the correct items are pulled up
+		$expected = array( $n2, $n3 );
+		$actual = wp_list_pluck( $n, 'id' );
+		$this->assertEquals( $expected, $actual );
+	}
+
+	/**
+	 * @group is_new
+	 */
+	public function test_is_new_false() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'friends',
+			'user_id' => $u,
+			'is_new' => false,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+
+		$n = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+			'is_new' => false,
+		) );
+
+		// Check that the correct items are pulled up
+		$expected = array( $n1 );
+		$actual = wp_list_pluck( $n, 'id' );
+		$this->assertEquals( $expected, $actual );
+	}
+
+	/**
+	 * @group is_new
+	 */
+	public function test_is_new_both() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'friends',
+			'user_id' => $u,
+			'is_new' => false,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+
+		$n = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+			'is_new' => 'both',
+		) );
+
+		// Check that the correct items are pulled up
+		$expected = array( $n1, $n2, $n3 );
+		$actual = wp_list_pluck( $n, 'id' );
+		$this->assertEquals( $expected, $actual );
+	}
+
+	/**
+	 * @group get
+	 * @group search_terms
+	 */
+	public function test_get_with_search_terms() {
+		$u = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'friends',
+			'user_id' => $u,
+			'is_new' => false,
+		) );
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id' => $u,
+			'is_new' => true,
+		) );
+
+		$n = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+			'search_terms' => 'roup',
+		) );
+
+		// Check that the correct items are pulled up
+		$expected = array( $n2 );
+		$actual = wp_list_pluck( $n, 'id' );
+		$this->assertEquals( $expected, $actual );
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP6229
+	 */
+	public function test_get_paged_sql() {
+		$u = self::factory()->user->create();
+
+		$notifications = array();
+		for ( $i = 1; $i <= 6; $i++ ) {
+			$notifications[] = self::factory()->notification->create( array(
+				'component_name' => 'activity',
+				'secondary_item_id' => $i,
+				'user_id' => $u,
+				'is_new' => true,
+			) );
+		}
+
+		$found = BP_Notifications_Notification::get( array(
+			'user_id' => $u,
+			'is_new' => true,
+			'page' => 2,
+			'per_page' => 2,
+			'order_by' => 'id',
+		) );
+
+		// Check that the correct number of items are pulled up
+		$expected = array( $notifications[2], $notifications[3] );
+		$this->assertEquals( $expected, wp_list_pluck( $found, 'id' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/class-bp-notifications-template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/class-bp-notifications-template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/class-bp-notifications-template.php	(revision 12265)
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @group notifications
+ * @group template
+ */
+
+class BP_Tests_Notifications_BPNotificationsTemplate extends BP_UnitTestCase {
+	/**
+	 * @group pagination
+	 * @group BP6229
+	 */
+	public function test_pagination_params_in_url_should_be_passed_to_query() {
+		$u = self::factory()->user->create();
+
+		$notifications = array();
+		for ( $i = 1; $i <= 6; $i++ ) {
+			$notifications[] = self::factory()->notification->create( array(
+				'component_name' => 'activity',
+				'secondary_item_id' => $i,
+				'user_id' => $u,
+				'is_new' => true,
+			) );
+		}
+
+		$_REQUEST['npage'] = 2;
+
+		$template = new BP_Notifications_Template( array(
+			'user_id' => $u,
+			'is_new' => true,
+			'per_page' => 2,
+			'order_by' => 'id',
+		) );
+
+		unset( $_REQUEST['npage'] );
+
+		// Check that the correct number of items are pulled up
+		$expected = array( $notifications[3], $notifications[2] );
+		$this->assertEquals( $expected, wp_list_pluck( $template->notifications, 'id' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/functions.php	(revision 12265)
@@ -0,0 +1,466 @@
+<?php
+
+/**
+ * @group notifications
+ */
+class BP_Tests_Notifications_Functions extends BP_UnitTestCase {
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_invalidation_all_for_user_on_save() {
+		$u = self::factory()->user->create();
+
+		self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id'        => $u
+		) );
+		self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id'        => $u,
+			'item_id'        => 1
+		) );
+
+		// prime cache
+		$count = bp_notifications_get_unread_notification_count( $u );
+
+		// just to be sure...
+		$this->assertEquals( 2, $count, 'Cache count should be 2 before invalidation.' );
+
+		// Trigger invalidation via save
+		self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id'        => $u,
+			'item_id'        => 2
+		) );
+
+		$this->assertFalse( wp_cache_get( 'all_for_user_' . $u, 'bp_notifications' ) );
+		$this->assertFalse( wp_cache_get( $u, 'bp_notifications_unread_count' ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_invalidation_all_for_user_on_delete() {
+		$u  = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id'        => $u
+		) );
+		self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id'        => $u
+		) );
+
+		// prime cache
+		$count = bp_notifications_get_unread_notification_count( $u );
+
+		// just to be sure...
+		$this->assertEquals( 2, $count, 'Cache count should be 2 before invalidation.' );
+
+		// delete
+		BP_Notifications_Notification::delete( array( 'id' => $n1, ) );
+
+		$this->assertFalse( wp_cache_get( 'all_for_user_' . $u, 'bp_notifications' ) );
+		$this->assertFalse( wp_cache_get( $u, 'bp_notifications_unread_count' ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_invalidation_all_for_user_on_update_user_id() {
+		$u = self::factory()->user->create();
+
+		self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id'        => $u
+		) );
+		self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id'        => $u
+		) );
+
+		// prime cache
+		$count = bp_notifications_get_unread_notification_count( $u );
+
+		// just to be sure...
+		$this->assertEquals( 2, $count, 'Cache count should be 2 before invalidation.' );
+
+		// mark all notifications by user as read
+		BP_Notifications_Notification::update(
+			array( 'is_new'  => false ),
+			array( 'user_id' => $u    )
+		);
+
+		$this->assertFalse( wp_cache_get( 'all_for_user_' . $u, 'bp_notifications' ) );
+		$this->assertFalse( wp_cache_get( $u, 'bp_notifications_unread_count' ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_cache_invalidation_all_for_user_on_update_id() {
+		$u  = self::factory()->user->create();
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id'        => $u
+		) );
+
+		self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id'        => $u
+		) );
+
+		// prime cache
+		$count = bp_notifications_get_unread_notification_count( $u );
+
+		// just to be sure...
+		$this->assertEquals( 2, $count, 'Cache count should be 2 before invalidation.' );
+
+		// mark one notification as read
+		BP_Notifications_Notification::update(
+			array( 'is_new' => false ),
+			array( 'id'     => $n1   )
+		);
+
+		$this->assertFalse( wp_cache_get( 'all_for_user_' . $u, 'bp_notifications' ) );
+		$this->assertFalse( wp_cache_get( $u, 'bp_notifications_unread_count' ) );
+	}
+
+	/**
+	 * @group bp_notifications_update_meta_cache
+	 */
+	public function test_bp_notifications_update_meta_cache() {
+		$u = self::factory()->user->create();
+
+		$n1 = self::factory()->notification->create( array(
+			'component_name' => 'messages',
+			'user_id'        => $u
+		) );
+
+		$n2 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id'        => $u
+		) );
+
+		// Add cache for each notification.
+		bp_notifications_update_meta( $n1, 'meta', 'data' );
+		bp_notifications_update_meta( $n1, 'data', 'meta' );
+		bp_notifications_update_meta( $n2, 'meta', 'human' );
+
+		// Prime cache.
+		bp_notifications_get_meta( $n1, 'meta' );
+
+		// Ensure an empty cache for second notification.
+		wp_cache_delete( $n2, 'notification_meta' );
+
+		// Update notification meta cache.
+		bp_notifications_update_meta_cache( array( $n1, $n2 ) );
+
+		$expected = array(
+			$n1 => array(
+				'meta' => array(
+					'data',
+				),
+				'data' => array(
+					'meta',
+				),
+			),
+			$n2 => array(
+				'meta' => array(
+					'human',
+				),
+			),
+		);
+
+		$found = array(
+			$n1 => wp_cache_get( $n1, 'notification_meta' ),
+			$n2 => wp_cache_get( $n2, 'notification_meta' ),
+		);
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	/**
+	 * @group bp_notifications_add_notification
+	 */
+	public function test_bp_notifications_add_notification_no_dupes() {
+		$args = array(
+			'user_id'           => 5,
+			'item_id'           => 10,
+			'secondary_item_id' => 25,
+			'component_name'    => 'messages',
+			'component_action'  => 'new_message'
+		);
+
+		self::factory()->notification->create( $args );
+
+		$this->assertFalse( bp_notifications_add_notification( $args ) );
+	}
+
+	/**
+	 * @group bp_notifications_add_notification
+	 */
+	public function test_bp_notifications_add_notification_allow_duplicate() {
+		$args = array(
+			'user_id'           => 5,
+			'item_id'           => 10,
+			'secondary_item_id' => 25,
+			'component_name'    => 'messages',
+			'component_action'  => 'new_message'
+		);
+
+		self::factory()->notification->create( $args );
+
+		$args['allow_duplicate'] = true;
+
+		$this->assertNotEmpty( bp_notifications_add_notification( $args ) );
+	}
+
+	/**
+	 * @group bp_notifications_get_unread_notification_count
+	 * @group cache
+	 */
+	public function test_bp_notifications_get_unread_notification_count_cache() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		self::factory()->notification->create( array(
+			'component_name'    => 'messages',
+			'component_action'  => 'new_message',
+			'item_id'           => 99,
+			'user_id'           => $u2,
+			'secondary_item_id' => $u1,
+			'is_new'            => true
+		) );
+
+		// prime cache
+		bp_notifications_get_unread_notification_count( $u2 );
+
+		// mark the created notification as read
+		bp_notifications_mark_notifications_by_item_id( $u2, 99, 'messages', 'new_message', $u1 );
+
+		// now grab the updated notification count
+		$n = bp_notifications_get_unread_notification_count( $u2 );
+
+		// assert
+		$this->assertEquals( 0, $n );
+	}
+
+	/**
+	 * @group bp_has_notifications
+	 */
+	public function test_bp_has_notifications_filtering() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		// create a mixture of different notifications
+		self::factory()->notification->create( array(
+			'component_name'    => 'messages',
+			'component_action'  => 'new_message',
+			'item_id'           => 99,
+			'user_id'           => $u2,
+			'secondary_item_id' => $u1,
+			'is_new'            => true
+		) );
+
+		self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 99,
+			'user_id'           => $u2,
+			'secondary_item_id' => $u1,
+			'is_new'            => true
+		) );
+
+		self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 100,
+			'user_id'           => $u2,
+			'secondary_item_id' => $u1,
+			'is_new'            => true
+		) );
+
+		// now fetch only activity notifications
+		bp_has_notifications( array(
+			'component_name' => 'activity',
+			'user_id'        => $u2
+		) );
+
+		// assert
+		$this->assertEquals( 2, buddypress()->notifications->query_loop->total_notification_count );
+	}
+
+	/**
+	 * @group bp_notifications_delete_notifications_on_user_delete
+	 * @ticket BP6681
+	 */
+	public function test_bp_notifications_delete_notifications_on_user_delete_should_delete_all_notifications() {
+		$u = self::factory()->user->create();
+
+		// Create notifications
+		$n1 = self::factory()->notification->create( array(
+			'component_name'    => 'messages',
+			'component_action'  => 'new_message',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		$n2 = self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		$n3 = self::factory()->notification->create( array(
+			'component_name' => 'groups',
+			'user_id'        => $u,
+		) );
+
+		$n4 = self::factory()->notification->create( array(
+			'component_name'   => 'friends',
+			'component_action' => 'friendship_request',
+			'user_id'          => $u,
+		) );
+
+		// Create notification for non-core component
+		$n5 = self::factory()->notification->create( array(
+			'component_name'    => 'foo',
+			'component_action'  => 'bar',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		global $wpdb, $bp;
+
+		/**
+		 * Can't use BP_Notifications_Notification::get(), because class::parse_args,
+		 * checks against bp_notifications_get_registered_components()
+		 * and if component is disabled it will be ignored.
+		 */
+		$query = $wpdb->prepare( "SELECT id FROM {$bp->notifications->table_name} WHERE user_id = %d and is_new = 1", $u );
+
+		// Make sure notifications have been added.
+		$found1 = $wpdb->get_col( $query );
+		$this->assertEqualSets( array( $n1, $n2, $n3, $n4, $n5 ), $found1 );
+
+		wp_delete_user( $u );
+
+		// Check if notifications are deleted.
+		$found2 = $wpdb->get_col( $query );
+		$this->assertEmpty( $found2 );
+	}
+
+	/**
+	 * @group  notification_callback
+	 * @ticket BP7141
+	 */
+	public function test_notification_callback_parameter_integrity() {
+		$u = self::factory()->user->create();
+
+		$n = self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		// Override activity notification callback so we can test integrity.
+		buddypress()->activity->notification_callback = array( $this, 'dummy_notification_callback' );
+
+		// Fetch notifications with string format.
+		bp_notifications_get_notifications_for_user( $u, 'string' );
+
+		// Assert!
+		// @todo When we cast all numeric strings as integers, this needs to be changed.
+		$expected = array(
+			'action'            => 'new_at_mention',
+			'item_id'           => '99',
+			'secondary_item_id' => '0',
+			'total_items'       => 1,
+			'id'                => (string) $n,
+			'format'            => 'string'
+		);
+		$this->assertEquals( $expected, $this->n_args );
+
+		// Fetch notifications with object format this time.
+		bp_notifications_get_notifications_for_user( $u, 'object' );
+
+		// Assert!
+		$expected['format'] = 'array';
+		$this->assertEquals( $expected, $this->n_args );
+
+		// Reset!
+		buddypress()->activity->notification_callback = 'bp_activity_format_notifications';
+		unset( $this->n_args );
+	}
+
+	/**
+	 * Used in test_notification_callback_parameter_integrity() test.
+	 */
+	public function dummy_notification_callback( $action, $item_id, $secondary_item_id, $total_items, $format = 'string', $id = 0 ) {
+		$this->n_args = compact( 'action', 'item_id', 'secondary_item_id', 'total_items', 'id', 'format' );
+	}
+
+	/**
+	 * @group cache
+	 * @ticket BP7130
+	 */
+	public function test_get_grouped_notifications_for_user_cache_invalidation() {
+		$u = self::factory()->user->create();
+
+		$n1 = self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		// Prime cache.
+		$found = bp_notifications_get_grouped_notifications_for_user( $u );
+		$this->assertEquals( 1, $found[0]->total_count );
+
+		$n2 = self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 100,
+			'user_id'           => $u,
+		) );
+
+		$found = bp_notifications_get_grouped_notifications_for_user( $u );
+		$this->assertEquals( 2, $found[0]->total_count );
+	}
+
+	/**
+	 * @ticket BP7827
+	 */
+	public function test_bp_notifications_personal_data_exporter() {
+		$u = self::factory()->user->create();
+
+		// Create notifications
+		$n1 = self::factory()->notification->create( array(
+			'component_name'    => 'messages',
+			'component_action'  => 'new_message',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		$n2 = self::factory()->notification->create( array(
+			'component_name'    => 'activity',
+			'component_action'  => 'new_at_mention',
+			'item_id'           => 99,
+			'user_id'           => $u,
+		) );
+
+		$test_user = new WP_User( $u );
+
+		$actual = bp_notifications_personal_data_exporter( $test_user->user_email, 1 );
+
+		$this->assertTrue( $actual['done'] );
+
+		// Number of exported notification items.
+		$this->assertSame( 2, count( $actual['data'] ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/template.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/template.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/notifications/template.php	(revision 12265)
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @group notifications
+ * @group template
+ */
+class BP_Tests_Notifications_Template extends BP_UnitTestCase {
+	/**
+	 * @group pagination
+	 * @group BP_Notifications_Template
+	 */
+	public function test_bp_notifications_template_should_give_precedence_to_npage_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['npage'] = '5';
+
+		$at = new BP_Notifications_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 5, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Notifications_Template
+	 */
+	public function test_bp_notifications_template_should_reset_0_pag_page_URL_param_to_default_pag_page_value() {
+		$request = $_REQUEST;
+		$_REQUEST['npage'] = '0';
+
+		$at = new BP_Notifications_Template( array(
+			'page' => 8,
+		) );
+
+		$this->assertEquals( 8, $at->pag_page );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Notifications_Template
+	 */
+	public function test_bp_notifications_template_should_give_precedence_to_num_URL_param() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '14';
+
+		$at = new BP_Notifications_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 14, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+
+	/**
+	 * @group pagination
+	 * @group BP_Notifications_Template
+	 */
+	public function test_bp_notifications_template_should_reset_0_pag_num_URL_param_to_default_pag_num_value() {
+		$request = $_REQUEST;
+		$_REQUEST['num'] = '0';
+
+		$at = new BP_Notifications_Template( array(
+			'per_page' => 13,
+		) );
+
+		$this->assertEquals( 13, $at->pag_num );
+
+		$_REQUEST = $request;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/activity.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @group activity
+ * @group routing
+ */
+class BP_Tests_Routing_Activity extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	function test_activity_directory() {
+		$this->go_to( bp_get_activity_directory_permalink() );
+		$this->assertEquals( bp_get_activity_root_slug(), bp_current_component() );
+	}
+
+	/**
+	 * Can't test using bp_activity_get_permalink(); see bp_activity_action_permalink_router().
+	 */
+	function test_activity_permalink() {
+		$a = self::factory()->activity->create();
+		$activity = self::factory()->activity->get_object_by_id( $a );
+
+		$url = bp_core_get_user_domain( $activity->user_id ) . bp_get_activity_slug() . '/' . $activity->id . '/';
+		$this->go_to( $url );
+		$this->assertTrue( bp_is_single_activity() );
+	}
+
+	function test_member_activity() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_activity_slug() );
+		$this->assertTrue( bp_is_user_activity() );
+	}
+
+	function test_member_activity_mentions() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_activity_slug() . '/mentions'  );
+		$this->assertTrue( bp_is_user_activity() );
+	}
+
+	function test_member_activity_favourites() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_activity_slug() . '/favorites'  );
+		$this->assertTrue( bp_is_user_activity() );
+	}
+
+	/**
+	 * @group friends
+	 */
+	function test_member_activity_friends() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_activity_slug() . '/' . bp_get_friends_slug() );
+		$this->assertTrue( bp_is_user_friends_activity() );
+	}
+
+	/**
+	 * @group groups
+	 */
+	function test_member_activity_groups() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_activity_slug() . '/' . bp_get_groups_slug() );
+		$this->assertTrue( bp_is_user_groups_activity() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/anonymous.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/anonymous.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/anonymous.php	(revision 12265)
@@ -0,0 +1,16 @@
+<?php
+/**
+ * @group routing
+ */
+class BP_Tests_Routing_Anonymous extends BP_UnitTestCase {
+	function test_wordpress_page() {
+		$this->go_to( '/' );
+		$this->assertEmpty( bp_current_component() );
+	}
+
+	function test_nav_menu() {
+		$this->go_to( '/' );
+		$nav = buddypress()->members->nav->get_item_nav();
+		$this->assertEmpty( $nav );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/core.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/core.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/core.php	(revision 12265)
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @group core
+ * @group routing
+ */
+class BP_Tests_Routing_Core extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+	function test_wordpress_page() {
+		$this->go_to( '/' );
+		$this->assertEmpty( bp_current_component() );
+	}
+
+	/**
+	 * @expectedIncorrectUsage bp_nav
+	 */
+	function test_nav_menu() {
+		$this->go_to( '/' );
+		$this->assertTrue( isset( buddypress()->bp_nav['activity'] ) );
+		$this->assertTrue( isset( buddypress()->bp_nav['profile'] ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/friends.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/friends.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/friends.php	(revision 12265)
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @group friends
+ * @group routing
+ */
+class BP_Tests_Routing_Friends extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	function test_member_friends() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_friends_slug() );
+		$this->assertTrue( bp_is_user_friends() );
+	}
+
+	function test_member_friends_requests() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_friends_slug()  . '/requests' );
+		$this->assertTrue( bp_is_user_friend_requests() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/groups.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/groups.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/groups.php	(revision 12265)
@@ -0,0 +1,127 @@
+<?php
+/**
+ * @group groups
+ * @group routing
+ */
+class BP_Tests_Routing_Groups extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		buddypress()->members->types = array();
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	function test_member_groups() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_groups_slug() );
+		$this->assertTrue( bp_is_user_groups() );
+	}
+
+	function test_member_groups_invitations() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_groups_slug() . '/invites' );
+		$this->assertTrue( bp_is_user_groups() && bp_is_current_action( 'invites' ) );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_directory_with_type() {
+		bp_groups_register_group_type( 'foo' );
+		$this->go_to( bp_get_groups_directory_permalink() . 'type/foo/' );
+		$this->assertTrue( bp_is_groups_component() && ! bp_is_group() && bp_is_current_action( bp_get_groups_group_type_base() ) && bp_is_action_variable( 'foo', 0 ) );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_directory_with_type_that_has_custom_directory_slug() {
+		bp_groups_register_group_type( 'foo', array( 'has_directory' => 'foos' ) );
+		$this->go_to( bp_get_groups_directory_permalink() . 'type/foos/' );
+		$this->assertTrue( bp_is_groups_component() && ! bp_is_group() && bp_is_current_action( bp_get_groups_group_type_base() ) && bp_is_action_variable( 'foos', 0 ) );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_directory_should_404_for_group_types_that_have_no_directory() {
+		bp_register_member_type( 'foo', array( 'has_directory' => false ) );
+		$this->go_to( bp_get_members_directory_permalink() . 'type/foo/' );
+		$this->assertTrue( is_404() );
+	}
+
+	/**
+	 * @group group_types
+	 */
+	public function test_group_directory_should_404_for_invalid_group_types() {
+		$this->go_to( bp_get_members_directory_permalink() . 'type/foo/' );
+		$this->assertTrue( is_404() );
+	}
+
+	/**
+	 * @group group_previous_slug
+	 */
+	public function test_group_previous_slug_current_slug_should_resolve() {
+		$g1 = self::factory()->group->create( array(
+			'slug' => 'george',
+		) );
+		groups_edit_base_group_details( array(
+			'group_id' => $g1,
+			'slug'     => 'ralph',
+		) );
+
+		$this->go_to( bp_get_groups_directory_permalink() . 'ralph' );
+
+		$this->assertEquals( $g1, bp_get_current_group_id() );
+	}
+
+	/**
+	 * @group group_previous_slug
+	 */
+	public function test_group_previous_slug_should_resolve() {
+		$g1 = self::factory()->group->create( array(
+			'slug' => 'george',
+		) );
+
+		groups_edit_base_group_details( array(
+			'group_id'       => $g1,
+			'slug'           => 'sam!',
+			'notify_members' => false,
+		) );
+		$this->go_to( bp_get_groups_directory_permalink() . 'george' );
+
+		$this->assertEquals( $g1, bp_get_current_group_id() );
+	}
+
+	/**
+	 * @group group_previous_slug
+	 */
+	public function test_group_previous_slug_most_recent_takes_precedence() {
+		$g1 = self::factory()->group->create( array(
+			'slug' => 'george',
+		) );
+		groups_edit_base_group_details( array(
+			'group_id'       => $g1,
+			'slug'           => 'ralph',
+			'notify_members' => false,
+		) );
+		$g2 = self::factory()->group->create( array(
+			'slug' => 'george',
+		) );
+		groups_edit_base_group_details( array(
+			'group_id'       => $g2,
+			'slug'           => 'sam',
+			'notify_members' => false,
+		) );
+
+		$this->go_to( bp_get_groups_directory_permalink() . 'george' );
+		$this->assertEquals( $g2, bp_get_current_group_id() );
+	}
+
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/members.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/members.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/members.php	(revision 12265)
@@ -0,0 +1,122 @@
+<?php
+/**
+ * @group members
+ * @group routing
+ */
+class BP_Tests_Routing_Members extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		buddypress()->members->types = array();
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'user_login' => 'paulgibbs', 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		$this->set_current_user( $this->old_current_user );
+		parent::tearDown();
+	}
+
+	function test_members_directory() {
+		$this->go_to( bp_get_members_directory_permalink() );
+		$this->assertEquals( bp_get_members_root_slug(), bp_current_component() );
+	}
+
+	function test_member_permalink() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) );
+		$this->assertTrue( bp_is_my_profile() );
+	}
+
+	/**
+	 * @ticket BP6475
+	 */
+	public function test_member_directory_when_nested_under_wp_page() {
+		$p = self::factory()->post->create( array(
+			'post_type' => 'page',
+			'post_name' => 'foo',
+		) );
+
+		$members_page = get_page_by_path( 'members' );
+
+		wp_update_post( array(
+			'ID' => $members_page->ID,
+			'post_parent' => $p,
+		) );
+
+		$members_page_permalink = bp_get_root_domain() . '/foo/members/';
+		$this->go_to( $members_page_permalink );
+
+		$this->assertTrue( bp_is_members_component() );
+		$this->assertEquals( '', bp_current_action() );
+	}
+
+	/**
+	 * @ticket BP6286
+	 * @group member_types
+	 */
+	public function test_member_directory_with_member_type() {
+		bp_register_member_type( 'foo' );
+		$this->go_to( bp_get_members_directory_permalink() . 'type/foo/' );
+		$this->assertTrue( bp_is_members_component() );
+	}
+
+	/**
+	 * @ticket BP6286
+	 * @group member_types
+	 */
+	public function test_member_directory_with_member_type_should_obey_filtered_type_slug() {
+		bp_register_member_type( 'foo' );
+
+		add_filter( 'bp_members_member_type_base', array( $this, 'filter_member_type_base' ) );
+		$this->go_to( bp_get_members_directory_permalink() . 'buddypress-member-type/foo/' );
+		remove_filter( 'bp_members_member_type_base', array( $this, 'filter_member_type_base' ) );
+		$this->assertTrue( bp_is_members_component() );
+	}
+
+	public function filter_member_type_base( $base ) {
+		return 'buddypress-member-type';
+	}
+
+	/**
+	 * @ticket BP6286
+	 * @group member_types
+	 */
+	public function test_member_directory_with_member_type_that_has_custom_directory_slug() {
+		bp_register_member_type( 'foo', array( 'has_directory' => 'foos' ) );
+		$this->go_to( bp_get_members_directory_permalink() . 'type/foos/' );
+		$this->assertTrue( bp_is_members_component() );
+	}
+
+	/**
+	 * @ticket BP6286
+	 * @group member_types
+	 */
+	public function test_member_directory_with_member_type_should_be_overridden_by_member_with_same_nicename() {
+		$u = self::factory()->user->create( array( 'user_nicename' => 'foo' ) );
+		bp_register_member_type( 'foo' );
+		$this->go_to( bp_get_members_directory_permalink() . 'type/foo/' );
+	}
+
+	/**
+	 * @ticket BP6286
+	 * @group member_types
+	 */
+	public function test_member_directory_should_404_for_member_types_that_have_no_directory() {
+		bp_register_member_type( 'foo', array( 'has_directory' => false ) );
+		$this->go_to( bp_get_members_directory_permalink() . 'type/foo/' );
+		$this->assertTrue( is_404() );
+	}
+
+	/**
+	 * @ticket BP6325
+	 */
+	function test_members_shortlink_redirector() {
+		$shortlink_member_slug = 'me';
+
+		$this->go_to( bp_get_members_directory_permalink() . $shortlink_member_slug );
+
+		$this->assertSame( get_current_user_id(), bp_displayed_user_id() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/messages.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/messages.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/messages.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @group messages
+ * @group routing
+ */
+class BP_Tests_Routing_Messages extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	function test_member_messages() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_messages_slug() );
+		$this->assertTrue( bp_is_messages_inbox() );
+	}
+
+	function test_member_messages_sentbox() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_messages_slug() . '/sentbox' );
+		$this->assertTrue( bp_is_messages_sentbox() );
+	}
+
+	function test_member_messages_compose() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_messages_slug() . '/compose' );
+		$this->assertTrue( bp_is_messages_compose_screen() );
+	}
+
+	function test_member_messages_notices() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_messages_slug() . '/notices' );
+		$this->assertTrue( bp_is_notices() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/root-profiles.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/root-profiles.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/root-profiles.php	(revision 12265)
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @group members
+ * @group routing
+ * @group root_profiles
+ */
+class BP_Tests_Routing_Members_Root_Profiles extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+	protected $u;
+
+	public function setUp() {
+		parent::setUp();
+
+		add_filter( 'bp_core_enable_root_profiles', '__return_true' );
+
+		$this->old_current_user = get_current_user_id();
+		$uid = self::factory()->user->create( array(
+			'user_login' => 'boone',
+			'user_nicename' => 'boone',
+		) );
+		$this->u = new WP_User( $uid );
+		$this->set_current_user( $uid );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+		remove_filter( 'bp_core_enable_root_profiles', '__return_true' );
+	}
+
+	public function test_members_directory() {
+		$this->go_to( home_url( bp_get_members_root_slug() ) );
+		$this->assertEquals( bp_get_members_root_slug(), bp_current_component() );
+	}
+
+	public function test_member_permalink() {
+		$domain = home_url( $this->u->user_nicename );
+		$this->go_to( $domain );
+
+		$this->assertTrue( bp_is_user() );
+		$this->assertTrue( bp_is_my_profile() );
+		$this->assertEquals( $this->u->ID, bp_displayed_user_id() );
+	}
+
+	/**
+	 * @ticket BP6475
+	 */
+	public function test_member_permalink_when_members_page_is_nested_under_wp_page() {
+		$p = self::factory()->post->create( array(
+			'post_type' => 'page',
+			'post_name' => 'foo',
+		) );
+
+		$members_page = get_page_by_path( 'members' );
+		wp_update_post( array(
+			'ID' => $members_page->ID,
+			'post_parent' => $p,
+		) );
+
+		$domain = home_url( $this->u->user_nicename );
+		$this->go_to( $domain );
+
+		$this->assertTrue( bp_is_user() );
+		$this->assertTrue( bp_is_my_profile() );
+		$this->assertEquals( $this->u->ID, bp_displayed_user_id() );
+	}
+
+	public function test_member_activity_page() {
+		$url = home_url( $this->u->user_nicename ) . '/' . bp_get_activity_slug();
+		$this->go_to( $url );
+
+		$this->assertTrue( bp_is_user() );
+		$this->assertTrue( bp_is_my_profile() );
+		$this->assertEquals( $this->u->ID, bp_displayed_user_id() );
+
+		$this->assertTrue( bp_is_activity_component() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/settings.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/settings.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/settings.php	(revision 12265)
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @group settings
+ * @group routing
+ */
+class BP_Tests_Routing_Settings extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	function test_member_settings() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_settings_slug() );
+		$this->assertTrue( bp_is_user_settings_general() );
+	}
+
+	function test_member_settings_notifications() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_settings_slug() . '/notifications' );
+		$this->assertTrue( bp_is_user_settings_notifications() );
+	}
+
+	// @todo How best to test this?
+	/*function bp_is_user_settings_account_capbilities() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_settings_slug() . '/capabilities' );
+	}*/
+
+	function bp_is_user_settings_account_delete() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_settings_slug() . '/delete-account' );
+		$this->assertTrue( bp_is_user_settings_account_delete() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/url.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/url.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/url.php	(revision 12265)
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @group url
+ */
+class BP_Tests_URL extends BP_UnitTestCase {
+	function test_bp_core_ajax_url() {
+		$forced = force_ssl_admin();
+		$old_https = isset( $_SERVER['HTTPS'] ) ? $_SERVER['HTTPS'] : null;
+
+		// (1) HTTPS off
+		force_ssl_admin( false );
+		$_SERVER['HTTPS'] = 'off';
+
+		// (1a) Front-end
+		$this->go_to( '/' );
+		$this->assertEquals( bp_core_ajax_url(), get_site_url( bp_get_root_blog_id(), '/wp-admin/admin-ajax.php', 'http' ) );
+
+		// (1b) Dashboard
+		$this->go_to( '/wp-admin' );
+		$this->assertEquals( bp_core_ajax_url(), get_site_url( bp_get_root_blog_id(), '/wp-admin/admin-ajax.php', 'http' ) );
+
+		// (2) FORCE_SSL_ADMIN
+		force_ssl_admin( true );
+
+		// (2a) Front-end
+		$this->go_to( '/' );
+		$this->assertEquals( bp_core_ajax_url(), get_site_url( bp_get_root_blog_id(), '/wp-admin/admin-ajax.php', 'http' ) );
+
+		// (2b) Dashboard
+		$_SERVER['HTTPS'] = 'on';
+		$this->go_to( '/wp-admin' );
+		$this->assertEquals( bp_core_ajax_url(), get_site_url( bp_get_root_blog_id(), '/wp-admin/admin-ajax.php', 'https' ) );
+
+		// Restore to defaults.
+		force_ssl_admin( $forced );
+		if ( is_null( $old_https ) ) {
+			unset( $_SERVER['HTTPS'] );
+		} else {
+			$_SERVER['HTTPS'] = $old_https;
+		}
+
+		// (3) Multisite, root blog other than 1
+		if ( is_multisite() ) {
+			if ( function_exists( 'wp_initialize_site' ) ) {
+				$this->setExpectedDeprecated( 'wpmu_new_blog' );
+			}
+
+			$original_root_blog = bp_get_root_blog_id();
+			$blog_id = self::factory()->blog->create( array(
+				'path' => '/path' . rand() . time() . '/',
+			) );
+
+			buddypress()->root_blog_id = $blog_id;
+			$blog_url = get_blog_option( $blog_id, 'siteurl' );
+
+			$this->go_to( trailingslashit( $blog_url ) );
+			switch_to_blog( $blog_id );
+
+			buddypress()->root_blog_id = $original_root_blog;
+			$ajax_url = bp_core_ajax_url();
+
+			restore_current_blog();
+			$this->go_to( '/' );
+
+			$this->assertEquals( $blog_url . '/wp-admin/admin-ajax.php', $ajax_url );
+		}
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/routing/xprofile.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/routing/xprofile.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/routing/xprofile.php	(revision 12265)
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @group xprofile
+ * @group routing
+ */
+class BP_Tests_Routing_XProfile extends BP_UnitTestCase {
+	protected $old_current_user = 0;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->old_current_user = get_current_user_id();
+		$this->set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->set_current_user( $this->old_current_user );
+	}
+
+	function test_member_profile() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_profile_slug() );
+		$this->assertTrue( bp_is_user_profile() );
+	}
+
+	function test_member_profile_edit() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_profile_slug() . '/edit' );
+		$this->assertTrue( bp_is_user_profile_edit() );
+	}
+
+	function test_member_profile_change_avatar() {
+		$this->go_to( bp_core_get_user_domain( bp_loggedin_user_id() ) . bp_get_profile_slug() . '/change-avatar' );
+		$this->assertTrue( bp_is_user_change_avatar() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/settings/bpSettingsSanitizeNotificationSettings.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/settings/bpSettingsSanitizeNotificationSettings.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/settings/bpSettingsSanitizeNotificationSettings.php	(revision 12265)
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @group settings
+ */
+class BP_Tests_Settings_BpSettingsSanitizeNotificationSettings extends BP_UnitTestCase {
+	protected $core_notification_settings = array(
+		'notification_messages_new_message',
+		'notification_activity_new_mention',
+		'notification_activity_new_reply',
+		'notification_groups_invite',
+		'notification_groups_group_updated',
+		'notification_groups_admin_promotion',
+		'notification_groups_membership_request',
+		'notification_friends_friendship_request',
+		'notification_friends_friendship_accepted',
+	);
+
+	public function test_should_ignore_setting_not_added_via_bp_notification_settings_callback() {
+		$settings = array(
+			'foo' => 'yes',
+		);
+
+		$sanitized = bp_settings_sanitize_notification_settings( $settings );
+
+		$this->assertArrayNotHasKey( 'foo', $sanitized );
+	}
+
+	public function test_should_accept_setting_added_via_bp_notification_settings_callback() {
+		$settings = array(
+			'foo' => 'yes',
+		);
+
+		add_action( 'bp_notification_settings', array( $this, 'add_custom_notification_setting' ) );
+		$sanitized = bp_settings_sanitize_notification_settings( $settings );
+		remove_action( 'bp_notification_settings', array( $this, 'add_custom_notification_setting' ) );
+
+		$this->assertArrayHasKey( 'foo', $sanitized );
+		$this->assertSame( 'yes', $sanitized['foo'] );
+	}
+
+	public function test_should_sanitize_invalid_values_to_no_for_core_settings() {
+
+		$settings = array();
+		foreach ( $this->core_notification_settings as $key ) {
+			$settings[ $key ] = 'foo';
+		}
+
+		add_action( 'bp_notification_settings', array( $this, 'add_core_notification_settings' ) );
+		$sanitized = bp_settings_sanitize_notification_settings( $settings );
+		remove_action( 'bp_notification_settings', array( $this, 'add_core_notification_settings' ) );
+
+		$expected = array();
+		foreach ( $this->core_notification_settings as $key ) {
+			$expected[ $key ] = 'no';
+		}
+
+		$this->assertEqualSets( $expected, $sanitized );
+	}
+
+	public function test_should_not_sanitize_values_for_custom_setting() {
+		$settings = array(
+			'foo' => 'bar',
+		);
+
+		add_action( 'bp_notification_settings', array( $this, 'add_custom_notification_setting' ) );
+		$sanitized = bp_settings_sanitize_notification_settings( $settings );
+		remove_action( 'bp_notification_settings', array( $this, 'add_custom_notification_setting' ) );
+
+		$this->assertArrayHasKey( 'foo', $sanitized );
+		$this->assertSame( 'bar', $sanitized['foo'] );
+	}
+
+	public function add_custom_notification_setting() {
+		echo '<input name="notifications[foo]" value="yes" />';
+	}
+
+	public function add_core_notification_settings() {
+		foreach ( $this->core_notification_settings as $key ) {
+			echo '<input name="notifications[' . $key . ']" value="yes" />';
+		}
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/testsuite/factory.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/testsuite/factory.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/testsuite/factory.php	(revision 12265)
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @group testsuite
+ */
+class BPTests_Testsuite_Factory extends BP_UnitTestCase {
+	/**
+	 * @ticket BP7234
+	 */
+	public function test_message_create_and_get_should_return_message_object() {
+		$m = self::factory()->message->create_and_get();
+
+		$this->assertTrue( $m instanceof BP_Messages_Message );
+	}
+
+	/**
+	 * @ticket BP7234
+	 */
+	public function test_message_should_create_default_sender_and_recipient() {
+		$m = self::factory()->message->create_and_get();
+
+		$this->assertNotEmpty( $m->sender_id );
+		$this->assertNotEmpty( $m->get_recipients() );
+	}
+
+	/**
+	 * @ticket BP7243
+	 */
+	public function test_friendship_should_create_default_initiator_and_friend() {
+		$f = self::factory()->friendship->create_and_get();
+
+		$u1 = new WP_User( $f->initiator_user_id );
+		$u2 = new WP_User( $f->friend_user_id );
+
+		$this->assertTrue( $u1->exists() );
+		$this->assertTrue( $u2->exists() );
+	}
+
+	/**
+	 * @ticket BP7243
+	 */
+	public function test_friendship_should_respect_passed_initiator_and_friend() {
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+
+		$f = self::factory()->friendship->create_and_get( array(
+			'initiator_user_id' => $u1,
+			'friend_user_id' => $u2,
+		) );
+
+		$this->assertSame( $u1, $f->initiator_user_id );
+		$this->assertSame( $u2, $f->friend_user_id );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/BP_XProfile_Field/member_types.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/BP_XProfile_Field/member_types.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/BP_XProfile_Field/member_types.php	(revision 12265)
@@ -0,0 +1,215 @@
+<?php
+
+/**
+ * @group member_types
+ * @group xprofile
+ * @ticket BP5192
+ */
+class BP_Tests_XProfile_BpXprofileField_MemberTypes extends BP_UnitTestCase {
+	protected $field_group_id;
+	protected $field_id;
+	protected $field;
+
+	public function setUp() {
+		parent::setUp();
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		$this->field_group_id = self::factory()->xprofile_group->create();
+		$this->field_id = self::factory()->xprofile_field->create( array( 'field_group_id' => $this->field_group_id ) );
+		$this->field = new BP_XProfile_Field( $this->field_id );
+	}
+
+	public function tearDown() {
+		buddypress()->members->types = array();
+		parent::tearDown();
+	}
+
+	public function test_get_single_member_type() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEqualSets( array( 'foo' ), $this->field->get_member_types() );
+	}
+
+	public function test_get_multiple_member_types() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'bar' );
+		$this->assertEqualSets( array( 'foo', 'bar' ), $this->field->get_member_types() );
+	}
+
+	public function test_invalid_member_types_should_not_be_returned() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'phony' );
+		$this->assertEquals( array( 'foo' ), $this->field->get_member_types() );
+	}
+
+	public function test_when_no_stored_types_are_found_all_registered_member_types_as_well_as_null_type_should_be_returned() {
+		$this->assertEqualSets( array( 'null', 'foo', 'bar' ), $this->field->get_member_types() );
+	}
+
+	public function test__none_meta_should_result_in_empty_array() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', '_none' );
+		$this->assertEquals( array(), $this->field->get_member_types() );
+	}
+
+	public function test__none_meta_should_override_other_values() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', '_none' );
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEquals( array(), $this->field->get_member_types() );
+	}
+
+	public function test_set_should_not_append_by_default() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEquals( array( 'bar' ), $this->field->set_member_types( array( 'bar' ) ) );
+	}
+
+	public function test_set_should_not_append_when_append_is_set_to_false() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEquals( array( 'bar' ), $this->field->set_member_types( array( 'bar', false ) ) );
+	}
+
+	public function test_set_should_append_when_append_is_set_to_true() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEqualSets( array( 'foo', 'bar' ), $this->field->set_member_types( array( 'bar' ), true ) );
+	}
+
+	public function test_set_empty_array_with_append_true_should_have_no_effect_on_saved_types() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEqualSets( array( 'foo' ), $this->field->set_member_types( array(), true ) );
+	}
+
+	public function test_set_empty_array_with_append_false_should_result_in_field_being_associated_with_no_member_types() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$this->assertEqualSets( array(), $this->field->set_member_types( array() ) );
+	}
+
+	public function test_set_should_interpret_null_flag_properly() {
+		$this->assertEqualSets( array( 'null' ), $this->field->set_member_types( array( 'null' ) ) );
+	}
+
+	public function test_set_all_types_plus_null_should_result_in_nothing_stored_in_db() {
+		$types = array( 'null', 'foo', 'bar' );
+		$this->assertEqualSets( $types, $this->field->set_member_types( $types ) );
+
+		$types_db = bp_xprofile_get_meta( $this->field_id, 'field', 'member_type', false );
+		$this->assertEqualSets( array(), $types_db );
+	}
+
+	public function test_get_fields_for_member_type_should_get_field_with_explicit_member_type() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		$found = BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+		$this->assertEqualSets( array( 1, $this->field_id ), array_keys( $found ) );
+	}
+
+	public function test_get_fields_for_member_type_should_ignore_field_that_applies_to_no_member_types() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', '_none' );
+		$found = BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+		$this->assertEqualSets( array( 1 ), array_keys( $found ) );
+	}
+
+	public function test_get_fields_for_member_type_should_ignore_field_that_applies_to_different_member_types() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'bar' );
+		$found = BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+		$this->assertEqualSets( array( 1 ), array_keys( $found ) );
+	}
+
+	public function test_get_fields_for_member_type_should_include_fields_with_no_member_type_restrictions() {
+		// A field with no member_type metadata applies to all member types.
+		$found = BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+		$this->assertEqualSets( array( 1, $this->field_id ), array_keys( $found ) );
+	}
+
+	public function test_passing_member_type_any_to_get_fields_for_member_type_should_return_unrestricted_fields() {
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $this->field_group_id ) );
+
+		$field_2 = new BP_XProfile_Field( $f2 );
+		$field_2->set_member_types( array( 'foo' ) );
+
+		$found = BP_XProfile_Field::get_fields_for_member_type( 'any' );
+		$this->assertEqualSets( array( 1, $this->field_id ), array_keys( $found ) );
+	}
+
+	public function test_passing_empty_member_type_to_get_fields_for_member_type_should_return_unrestricted_fields() {
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $this->field_group_id ) );
+
+		$field_2 = new BP_XProfile_Field( $f2 );
+		$field_2->set_member_types( array( 'foo' ) );
+
+		$found = BP_XProfile_Field::get_fields_for_member_type( '' );
+		$this->assertEqualSets( array( 1, $this->field_id ), array_keys( $found ) );
+	}
+
+	public function test_passing_member_type_none_to_get_fields_for_member_type_should_return_fields_unavailable_to_all_member_types() {
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', '_none' );
+		$found = BP_XProfile_Field::get_fields_for_member_type( '_none' );
+		$this->assertEqualSets( array( $this->field_id ), array_keys( $found ) );
+	}
+
+	public function test_get_fields_for_member_type_should_accept_an_array_of_member_types() {
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $this->field_group_id ) );
+		bp_xprofile_add_meta( $this->field_id, 'field', 'member_type', 'foo' );
+		bp_xprofile_add_meta( $f2, 'field', 'member_type', 'bar' );
+
+		$found = BP_XProfile_Field::get_fields_for_member_type( array( 'foo', 'bar' ) );
+		$this->assertEqualSets( array( 1, $this->field_id, $f2 ), array_keys( $found ) );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_get_fields_for_member_type_should_fetch_restricted_fields_from_cache() {
+		global $wpdb;
+
+		$this->field->set_member_types( array( 'foo' ) );
+
+		// Prime cache.
+		BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+
+		$num_queries = $wpdb->num_queries;
+
+		BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+
+		// get_fields_for_member_type() always does at least one query.
+		$num_queries++;
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @group cache
+	 */
+	public function test_get_fields_for_member_type_should_skip_cache_after_a_fields_member_type_is_modified() {
+		global $wpdb;
+
+		$this->field->set_member_types( array( 'foo' ) );
+
+		// Prime cache.
+		BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+
+		$num_queries = $wpdb->num_queries;
+
+		$this->field->set_member_types( array( 'none' ) );
+
+		$found = BP_XProfile_Field::get_fields_for_member_type( 'foo' );
+
+		$this->assertTrue( $num_queries + 2 <= $wpdb->num_queries );
+		$this->assertEqualSets( array( 1 ), array_keys( $found ) );
+	}
+
+	public function test_get_member_type_label_order() {
+		// Should be alphabetical, with the null option last.
+		bp_register_member_type( 'aaa', array(
+			'labels' => array(
+				'name' => 'AAA',
+			),
+		) );
+		bp_register_member_type( 'zzz', array(
+			'labels' => array(
+				'name' => 'ZZZ',
+			),
+		) );
+
+		$this->field->set_member_types( array( 'aaa', 'zzz', 'null' ) );
+
+		$expected = '(Member types: AAA, ZZZ, Users with no member type)';
+		$this->assertSame( $expected, $this->field->get_member_type_label() );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/activity.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/activity.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/activity.php	(revision 12265)
@@ -0,0 +1,321 @@
+<?php
+
+/**
+ * @group xprofile
+ * @group activity
+ */
+class BP_Tests_XProfile_Activity extends BP_UnitTestCase {
+	protected $updated_profile_data = array();
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_with_errors() {
+		$d = $this->setup_updated_profile_data();
+
+		// Fake new/old values to ensure a change
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo2',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertFalse( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), true ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_throttled() {
+		$d = $this->setup_updated_profile_data();
+
+		$time = time();
+		$prev_time = date( 'Y-m-d H:i:s', $time - ( 119 * 60 ) );
+		$now_time = date( 'Y-m-d H:i:s', $time );
+
+		self::factory()->activity->create( array(
+			'user_id' => $d['u'],
+			'component' => buddypress()->profile->id,
+			'type' => 'updated_profile',
+			'date_recorded' => $prev_time,
+		) );
+
+		// Fake new/old values to ensure a change
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo2',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertFalse( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_outside_of_throttle() {
+		$d = $this->setup_updated_profile_data();
+
+		$time = strtotime( bp_core_current_time() );
+		$prev_time = date( 'Y-m-d H:i:s', $time - ( 121 * 60 ) );
+		$now_time = date( 'Y-m-d H:i:s', $time );
+
+		self::factory()->activity->create( array(
+			'user_id' => $d['u'],
+			'component' => buddypress()->profile->id,
+			'type' => 'updated_profile',
+			'recorded_time' => $prev_time,
+		) );
+
+		// Fake new/old values to ensure a change
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo2',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertTrue( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+
+		$existing = bp_activity_get( array(
+			'max' => 1,
+			'filter' => array(
+				'user_id' => $d['u'],
+				'object' => buddypress()->profile->id,
+				'action' => 'updated_profile',
+			),
+			'count_total' => 'count_query',
+		) );
+
+		$this->assertEquals( 1, $existing['total'] );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_no_existing_activity() {
+		$d = $this->setup_updated_profile_data();
+
+		// Fake new/old values to ensure a change
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo2',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertTrue( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+
+		$existing = bp_activity_get( array(
+			'max' => 1,
+			'filter' => array(
+				'user_id' => $d['u'],
+				'object' => buddypress()->profile->id,
+				'action' => 'updated_profile',
+			),
+			'count_total' => 'count_query',
+		) );
+
+		$this->assertEquals( 1, $existing['total'] );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_no_changes() {
+		$d = $this->setup_updated_profile_data();
+
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertFalse( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_no_public_changes() {
+		$d = $this->setup_updated_profile_data();
+
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'loggedin',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'bar',
+				'visibility' => 'loggedin',
+			),
+		);
+
+		$this->assertFalse( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_public_changed_to_private() {
+		$d = $this->setup_updated_profile_data();
+
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'bar',
+				'visibility' => 'loggedin',
+			),
+		);
+
+		$this->assertFalse( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_private_changed_to_public() {
+		$d = $this->setup_updated_profile_data();
+
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'loggedin',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertTrue( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_field_didnt_previously_exist() {
+		$d = $this->setup_updated_profile_data();
+
+		$old_values = array();
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'bar',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertTrue( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+	}
+
+	/**
+	 * @group bp_xprofile_updated_profile_activity
+	 */
+	public function test_bp_xprofile_updated_profile_activity_public_changes() {
+		$d = $this->setup_updated_profile_data();
+
+		$old_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'foo',
+				'visibility' => 'public',
+			),
+		);
+		$new_values = array(
+			$this->updated_profile_data['f'] => array(
+				'value'      => 'bar',
+				'visibility' => 'public',
+			),
+		);
+
+		$this->assertTrue( bp_xprofile_updated_profile_activity( $d['u'], array( $d['f'] ), false, $old_values, $new_values ) );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_xprofile_format_activity_action_new_avatar
+	 */
+	public function test_bp_xprofile_format_activity_action_new_avatar() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => 'profile',
+			'type' => 'new_avatar',
+			'user_id' => $u,
+		) );
+
+		$expected = sprintf( __( '%s changed their profile picture', 'buddypress' ), bp_core_get_userlink( $u ) );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	/**
+	 * @group activity_action
+	 * @group bp_xprofile_format_activity_action_updated_profile
+	 */
+	public function test_bp_xprofile_format_activity_action_updated_profile() {
+		$u = self::factory()->user->create();
+		$a = self::factory()->activity->create( array(
+			'component' => buddypress()->profile->id,
+			'type' => 'updated_profile',
+			'user_id' => $u,
+		) );
+
+		$expected = sprintf( __( "%s's profile was updated", 'buddypress' ), '<a href="' . bp_core_get_user_domain( $u ) . bp_get_profile_slug() . '/">' . bp_core_get_user_displayname( $u ) . '</a>' );
+
+		$a_obj = new BP_Activity_Activity( $a );
+
+		$this->assertSame( $expected, $a_obj->action );
+	}
+
+	protected function setup_updated_profile_data() {
+		$this->updated_profile_data['u'] = self::factory()->user->create();
+		$this->updated_profile_data['g'] = self::factory()->xprofile_group->create();
+		$this->updated_profile_data['f'] = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $this->updated_profile_data['g'],
+		) );
+
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/cache.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/cache.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/cache.php	(revision 12265)
@@ -0,0 +1,237 @@
+<?php
+
+/**
+ * @group xprofile
+ * @group cache
+ */
+class BP_Tests_XProfile_Cache extends BP_UnitTestCase {
+	/**
+	 * @group bp_xprofile_update_meta_cache
+	 */
+	public function test_bp_xprofile_update_meta_cache() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$d = new BP_XProfile_ProfileData( $f, $u );
+		$d->user_id = $u;
+		$d->field_id = $f;
+		$d->value = 'foo';
+		$d->last_updated = bp_core_current_time();
+		$d->save();
+
+		bp_xprofile_add_meta( $g, 'group', 'group_foo', 'group_bar' );
+		bp_xprofile_add_meta( $f, 'field', 'field_foo', 'field_bar' );
+		bp_xprofile_add_meta( $d->id, 'data', 'data_foo', 'data_bar' );
+
+		// prime cache
+		bp_xprofile_update_meta_cache( array(
+			'group' => array( $g ),
+			'field' => array( $f ),
+			'data' => array( $d->id ),
+		) );
+
+		$g_expected = array(
+			'group_foo' => array(
+				'group_bar',
+			),
+		);
+
+		$this->assertSame( $g_expected, wp_cache_get( $g, 'xprofile_group_meta' ) );
+
+		$f_expected = array(
+			'field_foo' => array(
+				'field_bar',
+			),
+		);
+
+		$this->assertSame( $f_expected, wp_cache_get( $f, 'xprofile_field_meta' ) );
+
+		$d_expected = array(
+			'data_foo' => array(
+				'data_bar',
+			),
+		);
+
+		$this->assertSame( $d_expected, wp_cache_get( $d->id, 'xprofile_data_meta' ) );
+	}
+
+	/**
+	 * @group bp_xprofile_update_meta_cache
+	 * @group bp_has_profile
+	 */
+	public function test_bp_has_profile_meta_cache() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$d = new BP_XProfile_ProfileData( $f, $u );
+		$d->user_id = $u;
+		$d->field_id = $f;
+		$d->value = 'foo';
+		$d->last_updated = bp_core_current_time();
+		$d->save();
+
+		bp_xprofile_add_meta( $g, 'group', 'group_foo', 'group_bar' );
+		bp_xprofile_add_meta( $f, 'field', 'field_foo', 'field_bar' );
+		bp_xprofile_add_meta( $d->id, 'data', 'data_foo', 'data_bar' );
+
+		// prime cache
+		bp_has_profile( array(
+			'user_id' => $u,
+			'profile_group_id' => $g,
+		) );
+
+		$g_expected = array(
+			'group_foo' => array(
+				'group_bar',
+			),
+		);
+
+		$this->assertSame( $g_expected, wp_cache_get( $g, 'xprofile_group_meta' ) );
+
+		$f_expected = array(
+			'field_foo' => array(
+				'field_bar',
+			),
+		);
+
+		$this->assertSame( $f_expected, wp_cache_get( $f, 'xprofile_field_meta' ) );
+
+		$d_expected = array(
+			'data_foo' => array(
+				'data_bar',
+			),
+		);
+
+		$this->assertSame( $d_expected, wp_cache_get( $d->id, 'xprofile_data_meta' ) );
+	}
+
+	/**
+	 * @group bp_xprofile_update_meta_cache
+	 * @group bp_has_profile
+	 */
+	public function test_bp_has_profile_meta_cache_update_meta_cache_false() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$d = new BP_XProfile_ProfileData( $f, $u );
+		$d->user_id = $u;
+		$d->field_id = $f;
+		$d->value = 'foo';
+		$d->last_updated = bp_core_current_time();
+		$d->save();
+
+		bp_xprofile_add_meta( $g, 'group', 'group_foo', 'group_bar' );
+		bp_xprofile_add_meta( $f, 'field', 'field_foo', 'field_bar' );
+		bp_xprofile_add_meta( $d->id, 'data', 'data_foo', 'data_bar' );
+
+		// prime cache
+		bp_has_profile( array(
+			'user_id' => $u,
+			'profile_group_id' => $g,
+			'update_meta_cache' => false,
+		) );
+
+		$this->assertFalse( wp_cache_get( $g, 'xprofile_group_meta' ) );
+		$this->assertFalse( wp_cache_get( $f, 'xprofile_field_meta' ) );
+		$this->assertFalse( wp_cache_get( $d->id, 'xprofile_data_meta' ) );
+	}
+
+	/**
+	 * @ticket BP6638
+	 */
+	public function test_field_cache_should_be_invalidated_on_save() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Foo',
+		) );
+
+		$field = xprofile_get_field( $f );
+		$this->assertSame( 'Foo', $field->name );
+
+		$field->name = 'Bar';
+		$this->assertNotEmpty( $field->save() );
+
+		$field_2 = xprofile_get_field( $f );
+		$this->assertSame( 'Bar', $field_2->name );
+	}
+
+	/**
+	 * @ticket BP7407
+	 */
+	public function test_get_field_id_from_name_should_be_cached() {
+		global $wpdb;
+
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Foo',
+		) );
+
+		// Prime cache.
+		BP_XProfile_Field::get_id_from_name( 'Foo' );
+
+		$num_queries = $wpdb->num_queries;
+
+		$this->assertSame( $f, BP_XProfile_Field::get_id_from_name( 'Foo' ) );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7407
+	 */
+	public function test_get_field_id_from_name_cache_should_be_invalidated_on_field_update() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Foo',
+		) );
+		$f2 = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Bar',
+		) );
+
+		// Prime cache.
+		xprofile_get_field_id_from_name( 'Foo' );
+		xprofile_get_field_id_from_name( 'Bar' );
+
+		// Free up the name 'Bar'.
+		$field2 = xprofile_get_field( $f2 );
+		$field2->name = 'Quz';
+		$field2->save();
+
+		// Take the name 'Bar'.
+		$field1 = xprofile_get_field( $f1 );
+		$field1->name = 'Bar';
+		$field1->save();
+
+		$this->assertSame( $f1, BP_XProfile_Field::get_id_from_name( 'Bar' ) );
+	}
+
+	/**
+	 * @ticket BP7407
+	 */
+	public function test_get_field_id_from_name_cache_should_be_invalidated_on_field_deletion() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Foo',
+		) );
+
+		// Prime cache.
+		xprofile_get_field_id_from_name( 'Foo' );
+
+		xprofile_delete_field( $f );
+
+		$this->assertNull( BP_XProfile_Field::get_id_from_name( 'Bar' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-field-type.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-field-type.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-field-type.php	(revision 12265)
@@ -0,0 +1,186 @@
+<?php
+/**
+ * @group xprofile
+ * @group BP_XProfile_Field_Type
+ */
+class BP_Tests_XProfile_Field_Type extends BP_UnitTestCase {
+	public function test_unregistered_field_type_returns_textbox() {
+		$field = bp_xprofile_create_field_type( 'fakeyfield' );
+		$this->assertEquals( get_class( $field ), 'BP_XProfile_Field_Type_Placeholder' );
+	}
+
+	public function test_textbox_validate_empty_string() {
+		$field = bp_xprofile_create_field_type( 'textbox' );
+		$this->assertTrue( $field->is_valid( '' ) );
+	}
+
+	public function test_textbox_validate_string() {
+		$field = bp_xprofile_create_field_type( 'textbox' );
+		$this->assertTrue( $field->is_valid( 'my 117th input string!' ) );
+	}
+
+	public function test_textbox_validate_integer() {
+		$field = bp_xprofile_create_field_type( 'textbox' );
+		$this->assertTrue( $field->is_valid( 123 ) );
+	}
+
+	public function test_textbox_validate_whitelisted_string() {
+		$field = bp_xprofile_create_field_type( 'textbox' );
+
+		$this->assertTrue( $field->is_valid( 'a string' ) );
+		$this->assertFalse( $field->set_whitelist_values( 'pizza' )->is_valid( 'pasta' ) );
+		$this->assertTrue( $field->is_valid( 'pizza' ) );
+	}
+
+	public function test_multiselectbox_validate_whitelisted_array() {
+		$field = bp_xprofile_create_field_type( 'multiselectbox' );
+		$field->set_whitelist_values( array( 'cheese', 'pepporoni' ) );
+
+		$this->assertTrue( $field->is_valid( array( 'cheese', 'pepporoni' ) ) );
+		$this->assertTrue( $field->is_valid( array( 'cheese' ) ) );
+		$this->assertFalse( $field->is_valid( array( 'cheese', 'pinapple', 'pepporoni' ) ) );
+		$this->assertFalse( $field->is_valid( array( 'pinapple' ) ) );
+	}
+
+	public function test_multiselectbox_validate_null_value() {
+		$field = bp_xprofile_create_field_type( 'multiselectbox' );
+		$field->set_whitelist_values( array( 'cheese', 'pepporoni' ) );
+
+		$this->assertFalse( $field->is_valid( array( '' ) ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+		$this->assertTrue( $field->is_valid( array() ) );
+	}
+
+	public function test_datebox_do_not_validate_string() {
+		$field = bp_xprofile_create_field_type( 'datebox' );
+		$this->assertFalse( $field->is_valid( 'datebox fields only accept strings in: Y-m-d 00:00:00' ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+	}
+
+	public function test_datebox_do_not_validate_integer() {
+		$field = bp_xprofile_create_field_type( 'datebox' );
+		$this->assertFalse( $field->is_valid( 221213 ) );
+	}
+
+	public function test_datebox_validate_date() {
+		$field = bp_xprofile_create_field_type( 'datebox' );
+		$this->assertTrue( $field->is_valid( '2013-12-22 00:00:00' ) );
+	}
+
+	public function test_datebox_do_not_validate_date_with_timestamp() {
+		$field = bp_xprofile_create_field_type( 'datebox' );
+		$this->assertFalse( $field->is_valid( '2013-12-22 19:11:30' ) );
+		$this->assertFalse( $field->is_valid( '2013-12-22' ) );
+	}
+
+	public function test_number_do_not_validate_string() {
+		$field = bp_xprofile_create_field_type( 'number' );
+		$this->assertFalse( $field->is_valid( 'number fields only accept integers' ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+	}
+
+	public function test_number_validate_positive_integer() {
+		$field = bp_xprofile_create_field_type( 'number' );
+		$this->assertTrue( $field->is_valid( 12345678901 ) );
+		$this->assertTrue( $field->is_valid( '12345678901' ) );
+	}
+
+	public function test_number_validate_negative_integer() {
+		$field = bp_xprofile_create_field_type( 'number' );
+		$this->assertTrue( $field->is_valid( -987654321 ) );
+		$this->assertTrue( $field->is_valid( '-987654321' ) );
+	}
+
+	public function test_number_validate_null_value() {
+		$field = bp_xprofile_create_field_type( 'number' );
+		$this->assertTrue( $field->is_valid( '0' ) );
+		$this->assertFalse( $field->is_valid( '-' ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+	}
+
+	public function test_number_validate_whitelisted_array() {
+		$field = bp_xprofile_create_field_type( 'number' );
+		$field->set_whitelist_values( array( 123, 456 ) );
+
+		$this->assertTrue( $field->is_valid( array( 123 ) ) );
+		$this->assertTrue( $field->is_valid( array( 456 ) ) );
+		$this->assertFalse( $field->is_valid( array( 789, 456, 123 ) ) );
+		$this->assertFalse( $field->is_valid( array( 789 ) ) );
+	}
+
+	public function test_radiobutton_validate_whitelisted_array() {
+		$field = bp_xprofile_create_field_type( 'radio' );
+		$field->set_whitelist_values( array( 'cheese', 'pepporoni' ) );
+
+		$this->assertTrue( $field->is_valid( array( 'cheese', 'pepporoni' ) ) );
+		$this->assertTrue( $field->is_valid( array( 'cheese' ) ) );
+		$this->assertFalse( $field->is_valid( array(  'pinapple', 'cheese', 'pepporoni' ) ) );
+		$this->assertFalse( $field->is_valid( array( 'pinapple' ) ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+	}
+
+	public function test_radiobutton_do_not_validate_empty_items_in_whitelist() {
+		$field = bp_xprofile_create_field_type( 'radio' );
+		$field->set_whitelist_values( array( '' ) );
+
+		$this->assertFalse( $field->is_valid( array( '' ) ) );
+	}
+
+	public function test_checkbox_validate_whitelisted_array() {
+		$field = bp_xprofile_create_field_type( 'checkbox' );
+		$field->set_whitelist_values( array( 'cheese', 'pepporoni' ) );
+
+		$this->assertTrue( $field->is_valid( array( 'cheese', 'pepporoni' ) ) );
+		$this->assertTrue( $field->is_valid( array( 'cheese' ) ) );
+		$this->assertFalse( $field->is_valid( array( 'pepporoni', 'cheese', 'pinapple' ) ) );
+		$this->assertFalse( $field->is_valid( array( 'pinapple' ) ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+	}
+
+	public function test_checkbox_validate_null_value() {
+		$field = bp_xprofile_create_field_type( 'checkbox' );
+		$field->set_whitelist_values( array( 'cheese', 'pepporoni' ) );
+
+		$this->assertFalse( $field->is_valid( array( '' ) ) );
+		$this->assertFalse( $field->is_valid( '' ) );
+		$this->assertTrue( $field->is_valid( array() ) );
+	}
+
+	/**
+	 * @group url
+	 */
+	public function test_url_validate_url() {
+		$field = bp_xprofile_create_field_type( 'url' );
+
+		$this->assertTrue( $field->is_valid( 'http://foo.com' ) );
+		$this->assertTrue( $field->is_valid( 'https://foo.com' ) );
+		$this->assertTrue( $field->is_valid( 'http://foo.bar.com' ) );
+		$this->assertTrue( $field->is_valid( 'http://foo.bar.com/' ) );
+		$this->assertTrue( $field->is_valid( 'http://foo.com/bar' ) );
+		$this->assertTrue( $field->is_valid( 'http://foo.com/index.php' ) );
+		$this->assertTrue( $field->is_valid( 'http://foo.com/?bar=baz' ) );
+
+		$this->assertFalse( $field->is_valid( 'htp://foo.com' ) );
+		$this->assertFalse( $field->is_valid( 'http:/foo.com' ) );
+		$this->assertFalse( $field->is_valid( 'http//foo.com' ) );
+		$this->assertFalse( $field->is_valid( 'http://foo' ) );
+		$this->assertFalse( $field->is_valid( 'foo.com' ) );
+	}
+
+	/**
+	 * @group BP_XProfile_Field_Group_Type_Placeholder
+	 */
+	public function test_placeholder_validate_any_value() {
+		$field = bp_xprofile_create_field_type( 'foo' );
+		$this->assertTrue( $field->is_valid( '' ) );
+		$this->assertTrue( $field->is_valid( 'bar' ) );
+		$this->assertTrue( $field->is_valid( array( 'bar' ) ) );
+	}
+
+	public function test_telephone_validate_number_formats() {
+		$field = bp_xprofile_create_field_type( 'telephone' );
+		$this->assertTrue( $field->is_valid( '07700 900461' ) );
+		$this->assertTrue( $field->is_valid( '555-2368' ) );
+		$this->assertTrue( $field->is_valid( '(212) 664-7665' ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-field.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-field.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-field.php	(revision 12265)
@@ -0,0 +1,261 @@
+<?php
+/**
+ * @group xprofile
+ * @group BP_XProfile_Field
+ */
+class BP_Tests_BP_XProfile_Field_TestCases extends BP_UnitTestCase {
+
+	/**
+	 * @group xprofile_field_save
+	 */
+	public function test_can_delete_save() {
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+		) );
+
+		$f = new BP_XProfile_Field( $field );
+		$f->can_delete = 0;
+		$f->save();
+
+		$f2 = new BP_XProfile_Field( $field );
+		$this->assertEquals( '0', $f2->can_delete );
+	}
+
+	/**
+	 * @group xprofile_get_field_id_from_name
+	 */
+	public function test_get_id_from_name_field_name_option_value_conflict() {
+		$group = self::factory()->xprofile_group->create();
+
+		// force some checkbox options for our profile field
+		$_POST['checkbox_option'] = array(
+			1 => 'BuddyPress',
+			2 => 'WordPress'
+		);
+
+		// checkbox field
+		$f1 = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+			'type' => 'checkbox',
+			'name' => 'Interests'
+		) );
+
+		// textbox field with the same name as our checkbox value
+		$f2 = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+			'type' => 'textbox',
+			'name' => 'BuddyPress'
+		) );
+
+		$this->assertEquals( $f2, xprofile_get_field_id_from_name( 'BuddyPress' ) );
+
+		// cleanup!
+		unset( $_POST['checkbox_option'] );
+	}
+
+	/**
+	 * @group xprofile_field_admin_validate
+	 */
+	public function test_admin_validate_zero_field_name() {
+
+		// Mock POST global values
+		$_POST['title']     = '0';
+		$_POST['required']  = false;
+		$_POST['fieldtype'] = 'textbox';
+
+		// Validate the mocked POST radio button options
+		$result = BP_XProfile_Field::admin_validate();
+
+		// Assert valid
+		$this->assertEquals( $result, true );
+
+		// cleanup
+		unset(
+			$_POST['title'],
+			$_POST['required'],
+			$_POST['fieldtype']
+		);
+	}
+
+	/**
+	 * @group xprofile_field_admin_validate
+	 */
+	public function test_admin_validate_field_options() {
+
+		// Mock POST global values
+		$_POST['title']        = 'Foo';
+		$_POST['required']     = false;
+		$_POST['fieldtype']    = 'radio';
+		$_POST['radio_option'] = array(
+			1 => '0',
+			2 => '1',
+			3 => '4',
+		);
+
+		// Validate the mocked POST radio button options
+		$result = BP_XProfile_Field::admin_validate();
+
+		// Assert valid
+		$this->assertEquals( $result, true );
+
+		// cleanup
+		unset(
+			$_POST['title'],
+			$_POST['required'],
+			$_POST['fieldtype'],
+			$_POST['radio_option' ]
+		);
+	}
+
+	/**
+	 * @ticket BP6545
+	 */
+	public function test_newly_created_field_should_have_field_id_property_set() {
+		$field = new BP_XProfile_Field();
+		$field->group_id = 1;
+		$field->name = 'Foo';
+
+		$new_field_id = $field->save();
+
+		$this->assertSame( $new_field_id, $field->id );
+	}
+
+	/**
+	 * @ticket BP6638
+	 */
+	public function test_default_visibility_should_be_lazy_loaded() {
+		global $wpdb;
+
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+		) );
+
+		bp_xprofile_update_meta( $field, 'field', 'default_visibility', 'loggedin' );
+
+		// Initial setup takes just one query.
+		$num_queries = $wpdb->num_queries;
+		$field_obj = new BP_XProfile_Field( $field );
+		$num_queries++;
+
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+
+		// Fetching the default_visibility should cause another query.
+		$this->assertSame( 'loggedin', $field_obj->default_visibility );
+		$num_queries++;
+
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP6638
+	 */
+	public function test_allow_custom_visibility_should_be_lazy_loaded() {
+		global $wpdb;
+
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+		) );
+
+		bp_xprofile_update_meta( $field, 'field', 'allow_custom_visibility', 'disabled' );
+
+		// Initial setup takes just one query.
+		$num_queries = $wpdb->num_queries;
+		$field_obj = new BP_XProfile_Field( $field );
+		$num_queries++;
+
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+
+		// Fetching the allow_custom_visibility should cause another query.
+		$this->assertSame( 'disabled', $field_obj->allow_custom_visibility );
+		$num_queries++;
+
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP7073
+	 */
+	public function test_bad_field_id_should_not_be_cached() {
+		BP_XProfile_Field::get_instance( 12345 );
+
+		$this->assertFalse( wp_cache_get( 12345, 'bp_xprofile_fields' ) );
+	}
+
+	/**
+	 * @ticket BP7112
+	 */
+	public function test_update_position_should_invalidate_cache() {
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+		) );
+
+		// Prime cache.
+		$fetched_field = xprofile_get_field( $field );
+		$new_field_order = 12345;
+
+		// Update field position.
+		BP_XProfile_Field::update_position( $field, $new_field_order, $group );
+
+		// Cache call should miss; fresh data should be fetched.
+		$updated_fetched_field = xprofile_get_field( $field );
+		$this->assertEquals( $new_field_order, $updated_fetched_field->field_order );
+	}
+
+	/**
+	 * @ticket BP7351
+	 */
+	public function test_empty_datebox_fields_should_not_return_unix_epoch() {
+		$user  = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group,
+			'type' => 'datebox',
+		) );
+
+		$old_user = get_current_user_id();
+		$this->set_current_user( $user );
+
+		$value = bp_get_profile_field_data( array( 'user_id' => $user, 'field' => $field ) );
+		$this->assertEmpty( $value );
+
+		$this->set_current_user( $old_user );
+	}
+
+	/**
+	 * @ticket BP6658
+	 */
+	public function test_delete_field_should_delete_default_field_metadata() {
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group
+		) );
+
+		$field_obj = new BP_XProfile_Field( $field );
+		$field_obj->delete();
+
+		$value = bp_xprofile_get_meta( $field, 'field', 'default_visibility' );
+		$this->assertEmpty( $value );
+	}
+
+	/**
+	 * @ticket BP6658
+	 */
+	public function test_delete_field_should_delete_custom_field_metadata() {
+		$group = self::factory()->xprofile_group->create();
+		$field = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $group
+		) );
+
+		bp_xprofile_update_meta( $field, 'field', 'custom', 'metadata' );
+
+		$field_obj = new BP_XProfile_Field( $field );
+		$field_obj->delete();
+
+		$value = bp_xprofile_get_meta( $field, 'field', 'custom' );
+		$this->assertEmpty( $value );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-group.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-group.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-group.php	(revision 12265)
@@ -0,0 +1,444 @@
+<?php
+
+/**
+ * @group BP_XProfile_Group
+ * @group xprofile
+ */
+class BP_Tests_BP_XProfile_Group extends BP_UnitTestCase {
+
+	/**
+	 * @ticket BP6552
+	 */
+	public function test_save_should_not_return_false_when_no_fields_have_been_altered() {
+		$g = self::factory()->xprofile_group->create();
+		$group = new BP_XProfile_Group( $g );
+
+		$saved = $group->save();
+
+		$this->assertEquals( $g, $saved );
+	}
+
+	/**
+	 * @ticket BP7916
+	 */
+	public function test_delete() {
+		$g = self::factory()->xprofile_group->create();
+
+		$groups    = bp_xprofile_get_groups();
+		$group_ids = wp_list_pluck( $groups, 'id' );
+		$this->assertContains( $g, $group_ids );
+
+		$group = new BP_XProfile_Group( $g );
+		$this->assertTrue( $group->delete() );
+
+		$groups    = bp_xprofile_get_groups();
+		$group_ids = wp_list_pluck( $groups, 'id' );
+		$this->assertNotContains( $g, $group_ids );
+	}
+
+	/**
+	 * @group fetch_visibility_level
+	 */
+	public function test_fetch_visibility_level() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$f_obj = new BP_XProfile_Field( $f );
+
+		$fields = array(
+			0 => new stdClass,
+		);
+
+		$fields[0]->id = $f;
+		$fields[0]->name = $f_obj->name;
+		$fields[0]->description = $f_obj->description;
+		$fields[0]->type = $f_obj->type;
+		$fields[0]->group_id = $f_obj->group_id;
+		$fields[0]->is_required = $f_obj->is_required;
+		$fields[0]->data = new stdClass;
+		$fields[0]->data->value = 'foo';
+		$fields[0]->data->id = 123;
+
+		// custom visibility enabled, but no fallback
+		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
+		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'enabled' );
+
+		$found = BP_XProfile_Group::fetch_visibility_level( $u, $fields );
+
+		$expected = $fields;
+		$expected[0]->visibility_level = 'adminsonly';
+
+		$this->assertSame( $expected, $found );
+
+		// custom visibility enabled, with user-provided value
+		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
+		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'enabled' );
+		xprofile_set_field_visibility_level( $f, $u, 'public' );
+
+		$found = BP_XProfile_Group::fetch_visibility_level( $u, $fields );
+
+		$expected = $fields;
+		$expected[0]->visibility_level = 'public';
+
+		$this->assertSame( $expected, $found );
+
+		// custom visibility disabled
+		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
+		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'disabled' );
+		xprofile_set_field_visibility_level( $f, $u, 'public' );
+
+		$found = BP_XProfile_Group::fetch_visibility_level( $u, $fields );
+
+		$expected = $fields;
+		$expected[0]->visibility_level = 'adminsonly';
+
+		$this->assertSame( $expected, $found );
+	}
+
+	/**
+	 * @group get_xprofile_groups
+	 */
+	public function test_get_xprofile_groups() {
+		$g1 = self::factory()->xprofile_group->create();
+		$g2 = self::factory()->xprofile_group->create();
+		$g3 = self::factory()->xprofile_group->create();
+
+		$all = BP_XProfile_Group::get();
+		$all_results = array_map( 'absint', wp_list_pluck( $all, 'id' ) );
+
+		$e1 = array( $g1, $g2 );
+		$groups1 = BP_XProfile_Group::get( array(
+			'exclude_groups' => implode( ',', $e1 ),
+		) );
+
+		$r_groups1 = array_map( 'absint', wp_list_pluck( $groups1, 'id' ) );
+		$found1 = array_diff( $all_results, $r_groups1 );
+
+		$this->assertSame( $e1, array_merge( $found1, array() ) );
+
+		$e2 = array( $g2, $g3 );
+		$groups2 = BP_XProfile_Group::get( array(
+			'exclude_groups' => $e2,
+		) );
+
+		$r_groups2 = array_map( 'absint', wp_list_pluck( $groups2, 'id' ) );
+		$found2 = array_diff( $all_results, $r_groups2 );
+
+		$this->assertSame( $e2, array_merge( $found2, array() ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_restrictions_should_be_ignored_when_user_id_is_null_and_member_type_is_not_explicitly_provided() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+
+		$field = new BP_XProfile_Field( $f );
+		$field->set_member_types( array( 'foo' ) );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'user_id' => false,
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertContains( $f, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_restrictions_should_be_ignored_when_user_id_is_0_and_member_type_is_false() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+
+		$field = new BP_XProfile_Field( $f );
+		$field->set_member_types( array( 'foo' ) );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'user_id' => 0,
+			'member_type' => false,
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertContains( $f, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_restrictions_should_be_obeyed_for_nonzero_user_id() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f3 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f4 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		// Field 1 is visible only to 'foo' users.
+		$field1 = new BP_XProfile_Field( $f1 );
+		$field1->set_member_types( array( 'foo' ) );
+
+		// Field 2 is visible only to 'bar' users.
+		$field2 = new BP_XProfile_Field( $f2 );
+		$field2->set_member_types( array( 'bar' ) );
+
+		// Field 3 is visible to all users (no member type set).
+
+		// Field 4 is visible to no one.
+		$field4 = new BP_XProfile_Field( $f4 );
+		$field4->set_member_types( array() );
+
+		// User is in 'foo', so should have f1 and f3 only.
+		$u = self::factory()->user->create();
+		bp_set_member_type( $u, 'foo' );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'user_id' => $u,
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertContains( $f1, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertContains( $f3, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertNotContains( $f2, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertNotContains( $f4, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_restrictions_should_be_obeyed_for_nonzero_user_id_with_no_member_types() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f3 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		// Field 1 is visible only to 'foo' users.
+		$field1 = new BP_XProfile_Field( $f1 );
+		$field1->set_member_types( array( 'foo' ) );
+
+		// Field 2 is visible only to 'null' users.
+		$field2 = new BP_XProfile_Field( $f2 );
+		$field2->set_member_types( array( 'null' ) );
+
+		// Field 3 is visible to all users (no member type set).
+
+		// User has no member types, so should see f2 and f3 .
+		$u = self::factory()->user->create();
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'user_id' => $u,
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertNotContains( $f1, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertContains( $f2, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertContains( $f3, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_types_of_provided_user_id_should_take_precedence_over_provided_member_type() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		$field1 = new BP_XProfile_Field( $f1 );
+		$field1->set_member_types( array( 'foo' ) );
+		$field2 = new BP_XProfile_Field( $f2 );
+		$field2->set_member_types( array( 'bar' ) );
+
+		$u = self::factory()->user->create();
+		bp_set_member_type( $u, 'foo' );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'user_id' => $u,
+			'member_type' => 'bar',
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertContains( $f1, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_single_value_should_be_respected() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		$field1 = new BP_XProfile_Field( $f1 );
+		$field1->set_member_types( array( 'foo' ) );
+		$field2 = new BP_XProfile_Field( $f2 );
+		$field2->set_member_types( array( 'bar' ) );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'member_type' => 'bar',
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertNotContains( $f1, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertContains( $f2, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_array_value_should_be_respected() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		$field1 = new BP_XProfile_Field( $f1 );
+		$field1->set_member_types( array( 'foo' ) );
+		$field2 = new BP_XProfile_Field( $f2 );
+		$field2->set_member_types( array( 'bar' ) );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'member_type' => array( 'bar' ),
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertNotContains( $f1, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertContains( $f2, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group member_types
+	 * @ticket BP5192
+	 */
+	public function test_member_type_null_should_be_respected() {
+		$g = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		$f2 = self::factory()->xprofile_field->create( array( 'field_group_id' => $g ) );
+		bp_register_member_type( 'foo' );
+		bp_register_member_type( 'bar' );
+
+		$field1 = new BP_XProfile_Field( $f1 );
+		$field1->set_member_types( array( 'foo' ) );
+
+		$found_groups = BP_XProfile_Group::get( array(
+			'member_type' => array( 'null' ),
+			'fetch_fields' => true,
+		) );
+
+		// The groups aren't indexed, so we have to go looking for it.
+		foreach ( $found_groups as $fg ) {
+			if ( $g == $fg->id ) {
+				$the_group = $fg;
+			}
+		}
+
+		$this->assertNotContains( $f1, wp_list_pluck( $the_group->fields, 'id' ) );
+		$this->assertContains( $f2, wp_list_pluck( $the_group->fields, 'id' ) );
+	}
+
+	/**
+	 * @group save_xprofile_group_name
+	 */
+	public function test_save_xprofile_group_name() {
+		$g1 = self::factory()->xprofile_group->create( array(
+			'name' => "Test ' Name"
+		) );
+
+		$e1 = new BP_XProfile_Group( $g1 );
+		$e1->save();
+
+		wp_cache_delete( $g1, 'bp_xprofile_groups' );
+
+		$e2 = new BP_XProfile_Group( $g1 );
+
+		$this->assertSame( $e1->name, $e2->name );
+	}
+
+	/**
+	 * @group save_xprofile_group_name
+	 */
+	public function test_save_xprofile_group_name_with_single_quote() {
+
+		// Set the original group name with no slashes
+		$pristine_name = "Test \' Name";
+
+		// Create a group
+		$g1 = self::factory()->xprofile_group->create( array(
+			'name' => $pristine_name
+		) );
+
+		// Get the field
+		$e1 = new BP_XProfile_Group( $g1 );
+
+		$this->assertSame( "Test ' Name", $e1->name );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-profiledata.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-profiledata.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-profiledata.php	(revision 12265)
@@ -0,0 +1,425 @@
+<?php
+
+/**
+ * @group xprofile
+ * @group BP_XProfile_ProfileData
+ */
+class BP_Tests_BP_XProfile_ProfileData_TestCases extends BP_UnitTestCase {
+	/**
+	 * @group exists
+	 */
+	public function test_exists_when_doesnt_exist() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$d = new BP_XProfile_ProfileData( $f, $u );
+
+		$this->assertFalse( $d->exists() );
+	}
+
+	/**
+	 * @group exists
+	 */
+	public function test_exists_when_exists_uncached() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		xprofile_set_field_data( $f, $u, 'foo' );
+
+		$d = new BP_XProfile_ProfileData( $f, $u );
+
+		wp_cache_delete( "{$u}:{$f}", 'bp_xprofile_data' );
+
+		$this->assertTrue( $d->exists() );
+	}
+
+	/**
+	 * @group exists
+	 */
+	public function test_exists_when_exists_in_cache() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+		$d = new BP_XProfile_ProfileData( $f, $u );
+
+		// Fake the cache
+		$c = new stdClass;
+		$c->id = 3;
+		wp_cache_set( "{$u}:{$f}", $c, 'bp_xprofile_data' );
+
+		$this->assertTrue( $d->exists() );
+	}
+
+	/**
+	 * @group get_fielddataid_byid
+	 */
+	public function test_get_fielddataid_byid_when_doesnt_exist() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		// Just to be sure
+		wp_cache_delete( "{$u}:{$f}", 'bp_xprofile_data' );
+
+		$this->assertEquals( 0, BP_XProfile_ProfileData::get_fielddataid_byid( $f, $u ) );
+	}
+
+	/**
+	 * @group get_fielddataid_byid
+	 */
+	public function test_get_fielddataid_byid_when_exists_uncached() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$d = new BP_XProfile_ProfileData();
+		$d->user_id = $u;
+		$d->field_id = $f;
+		$d->value = 'foo';
+		$d->save();
+
+		// Ensure it's deleted from cache
+		wp_cache_delete( "{$u}:{$f}", 'bp_xprofile_data' );
+
+		$this->assertEquals( $d->id, BP_XProfile_ProfileData::get_fielddataid_byid( $f, $u ) );
+	}
+
+	/**
+	 * @group get_fielddataid_byid
+	 */
+	public function test_get_fielddataid_byid_when_exists_in_cache() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		// Fake the cache
+		$d = new stdClass;
+		$d->id = 5;
+		wp_cache_set( "{$u}:{$f}", $d, 'bp_xprofile_data' );
+
+		$this->assertSame( 5, BP_XProfile_ProfileData::get_fielddataid_byid( $f, $u ) );
+	}
+
+	/**
+	 * @group get_value_byid
+	 */
+	public function test_get_value_byid_singleuser_uncached() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$d = new BP_XProfile_ProfileData();
+		$d->user_id = $u;
+		$d->field_id = $f;
+		$d->value = 'foo';
+		$d->save();
+
+		// Ensure it's deleted from cache
+		wp_cache_delete( "{$u}:{$f}", 'bp_xprofile_data' );
+
+		$this->assertSame( 'foo', BP_XProfile_ProfileData::get_value_byid( $f, $u ) );
+	}
+
+	/**
+	 * @group get_value_byid
+	 */
+	public function test_get_value_byid_multipleusers_uncached() {
+		$time = date( 'Y-m-d H:i:s', time() - 60*60*24 );
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		// To make the tests reliable, we have to force the last_updated
+		// BP doesn't easily allow this except via filter
+		$this->last_updated = $time;
+		add_filter( 'xprofile_data_last_updated_before_save', array( $this, 'filter_time' ) );
+
+		$d1 = new BP_XProfile_ProfileData();
+		$d1->user_id = $u1;
+		$d1->field_id = $f;
+		$d1->value = 'foo';
+		$d1->last_updated = $time;
+		$d1->save();
+
+		$d2 = new BP_XProfile_ProfileData();
+		$d2->user_id = $u2;
+		$d2->field_id = $f;
+		$d2->value = 'bar';
+		$d2->last_updated = $time;
+		$d2->save();
+
+		remove_filter( 'xprofile_data_last_updated_before_save', array( $this, 'filter_time' ) );
+
+		// Ensure it's deleted from cache
+		wp_cache_delete( "{$u1}:{$f}", 'bp_xprofile_data' );
+		wp_cache_delete( "{$u2}:{$f}", 'bp_xprofile_data' );
+
+		$eu1 = new stdClass;
+		$eu1->user_id = $u1;
+		$eu1->value = 'foo';
+		$eu1->id = $d1->id;
+		$eu1->field_id = $f;
+		$eu1->last_updated = $time;
+
+		$eu2 = new stdClass;
+		$eu2->user_id = $u2;
+		$eu2->value = 'bar';
+		$eu2->id = $d2->id;
+		$eu2->field_id = $f;
+		$eu2->last_updated = $time;
+
+		$expected = array( $eu1, $eu2 );
+
+		$this->assertEquals( $expected, BP_XProfile_ProfileData::get_value_byid( $f, array( $u1, $u2 ) ) );
+	}
+
+	/**
+	 * @group get_value_byid
+	 */
+	public function test_get_value_byid_singleuser_cached() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$time = bp_core_current_time();
+
+		// Fake the cache
+		$d = new stdClass;
+		$d->value = 'foo';
+		$d->field_id = $f;
+		wp_cache_set( "{$u}:{$f}", $d, 'bp_xprofile_data' );
+
+		$this->assertSame( 'foo', BP_XProfile_ProfileData::get_value_byid( $f, $u ) );
+	}
+
+	/**
+	 * @group get_value_byid
+	 */
+	public function test_get_value_byid_multipleusers_cached() {
+		$time = date( 'Y-m-d H:i:s', time() - 60*60*24 );
+
+		$u1 = self::factory()->user->create();
+		$u2 = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		// Fake the cache
+		$d1 = new stdClass;
+		$d1->id = 10;
+		$d1->user_id = $u1;
+		$d1->field_id = $f;
+		$d1->value = 'foo';
+		$d1->last_updated = $time;
+
+		$d2 = new stdClass;
+		$d1->id = 21;
+		$d2->user_id = $u2;
+		$d2->field_id = $f;
+		$d2->value = 'bar';
+		$d2->last_updated = $time;
+
+		wp_cache_set( "{$u1}:{$f}", $d1, 'bp_xprofile_data' );
+		wp_cache_set( "{$u2}:{$f}", $d2, 'bp_xprofile_data' );
+
+		$eu1 = new stdClass;
+		$eu1->id = 10;
+		$eu1->user_id = $u1;
+		$eu1->field_id = $f;
+		$eu1->value = 'foo';
+		$eu1->last_updated = $time;
+
+		$eu2 = new stdClass;
+		$eu1->id = 21;
+		$eu2->user_id = $u2;
+		$eu2->field_id = $f;
+		$eu2->value = 'bar';
+		$eu2->last_updated = $time;
+
+		$expected = array( $eu1, $eu2 );
+
+		$this->assertEquals( $expected, BP_XProfile_ProfileData::get_value_byid( $f, array( $u1, $u2 ) ) );
+	}
+
+	/**
+	 * @group get_all_for_user
+	 */
+	public function test_get_all_for_user_uncached() {
+		$u = self::factory()->user->create();
+		$g1 = self::factory()->xprofile_group->create();
+		$g2 = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array(
+			'type' => 'textbox',
+			'field_group_id' => $g1,
+		) );
+		$f2 = self::factory()->xprofile_field->create( array(
+			'type' => 'radio',
+			'field_group_id' => $g2,
+		) );
+
+		$time = bp_core_current_time();
+
+		// Get the fullname field - hackish
+		$f0_id = xprofile_get_field_id_from_name( bp_xprofile_fullname_field_name() );
+		$f0 = new BP_XProfile_Field( $f0_id );
+		$g0 = new BP_XProfile_Group( $f0->group_id );
+		$d0 = new BP_XProfile_ProfileData( $f0->id, $u );
+
+		$d1 = new BP_XProfile_ProfileData();
+		$d1->user_id = $u;
+		$d1->field_id = $f1;
+		$d1->value = 'foo';
+		$d1->last_updated = $time;
+		$d1->save();
+
+		$d2 = new BP_XProfile_ProfileData();
+		$d2->user_id = $u;
+		$d2->field_id = $f2;
+		$d2->value = 'bar';
+		$d2->last_updated = $time;
+		$d2->save();
+
+		// Ensure it's deleted from cache
+		wp_cache_delete( "{$u}:{$f1}", 'bp_xprofile_data' );
+		wp_cache_delete( "{$u}:{$f2}", 'bp_xprofile_data' );
+
+		$u_obj = new WP_User( $u );
+
+		$g1_obj = new BP_XProfile_Group( $g1 );
+		$g2_obj = new BP_XProfile_Group( $g2 );
+		$f1_obj = new BP_XProfile_Field( $f1 );
+		$f2_obj = new BP_XProfile_Field( $f2 );
+
+		$expected = array(
+			'user_login' => $u_obj->user_login,
+			'user_nicename' => $u_obj->user_nicename,
+			'user_email' => $u_obj->user_email,
+			$f0->name => array(
+				'field_group_id' => $g0->id,
+				'field_group_name' => $g0->name,
+				'field_id' => $f0->id,
+				'field_type' => $f0->type,
+				'field_data' => $d0->value,
+			),
+			$f1_obj->name => array(
+				'field_group_id' => $g1,
+				'field_group_name' => $g1_obj->name,
+				'field_id' => $f1,
+				'field_type' => $f1_obj->type,
+				'field_data' => $d1->value,
+			),
+			$f2_obj->name => array(
+				'field_group_id' => $g2,
+				'field_group_name' => $g2_obj->name,
+				'field_id' => $f2,
+				'field_type' => $f2_obj->type,
+				'field_data' => $d2->value,
+			),
+		);
+
+		$this->assertEquals( $expected, BP_XProfile_ProfileData::get_all_for_user( $u ) );
+	}
+
+	/**
+	 * @group get_all_for_user
+	 */
+	public function test_get_all_for_user_cached() {
+		$u = self::factory()->user->create();
+		$g1 = self::factory()->xprofile_group->create();
+		$g2 = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create( array(
+			'type' => 'textbox',
+			'field_group_id' => $g1,
+		) );
+		$f2 = self::factory()->xprofile_field->create( array(
+			'type' => 'radio',
+			'field_group_id' => $g2,
+		) );
+
+		$time = bp_core_current_time();
+
+		$g0 = new BP_XProfile_Group( 1 );
+		$f0 = new BP_XProfile_Field( 1 );
+		$d0 = new BP_XProfile_ProfileData( 1, $u );
+
+		$d1 = new stdClass;
+		$d1->user_id = $u;
+		$d1->field_id = $f1;
+		$d1->value = 'foo';
+		$d1->last_updated = $time;
+		$d1->id = 1;
+
+		$d2 = new stdClass;
+		$d2->user_id = $u;
+		$d2->field_id = $f2;
+		$d2->value = 'bar';
+		$d2->last_updated = $time;
+		$d2->id = 2;
+
+		wp_cache_set( "{$u}:{$f1}", $d1, 'bp_xprofile_data' );
+		wp_cache_set( "{$u}:{$f2}", $d2, 'bp_xprofile_data' );
+
+		$u_obj = new WP_User( $u );
+
+		$g1_obj = new BP_XProfile_Group( $g1 );
+		$g2_obj = new BP_XProfile_Group( $g2 );
+		$f1_obj = new BP_XProfile_Field( $f1 );
+		$f2_obj = new BP_XProfile_Field( $f2 );
+
+		$expected = array(
+			'user_login' => $u_obj->user_login,
+			'user_nicename' => $u_obj->user_nicename,
+			'user_email' => $u_obj->user_email,
+			$f0->name => array(
+				'field_group_id' => $g0->id,
+				'field_group_name' => $g0->name,
+				'field_id' => $f0->id,
+				'field_type' => $f0->type,
+				'field_data' => $d0->value,
+			),
+			$f1_obj->name => array(
+				'field_group_id' => $g1,
+				'field_group_name' => $g1_obj->name,
+				'field_id' => $f1,
+				'field_type' => $f1_obj->type,
+				'field_data' => $d1->value,
+			),
+			$f2_obj->name => array(
+				'field_group_id' => $g2,
+				'field_group_name' => $g2_obj->name,
+				'field_id' => $f2,
+				'field_type' => $f2_obj->type,
+				'field_data' => $d2->value,
+			),
+		);
+
+		$this->assertEquals( $expected, BP_XProfile_ProfileData::get_all_for_user( $u ) );
+	}
+
+	public function filter_time() {
+		return $this->last_updated;
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-query.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-query.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/class-bp-xprofile-query.php	(revision 12265)
@@ -0,0 +1,644 @@
+<?php
+
+/**
+ * @group xprofile
+ * @group BP_XProfile_Query
+ */
+class BP_Tests_BP_XProfile_Query extends BP_UnitTestCase {
+	protected $group;
+	protected $fields = array();
+	protected $users = array();
+
+	public function tearDown() {
+		parent::tearDown();
+		$this->group = '';
+		$this->fields = array();
+		$this->users = array();
+	}
+
+	public function test_no_field() {
+		$this->create_fields( 2 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo');
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'value' => 'foo',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0], $this->users[2] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_no_value() {
+		$this->create_fields( 2 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo');
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[1],
+				),
+			),
+		) );
+
+		$expected = array( $this->users[2] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_translate_field_name_to_field_id() {
+		$this->create_fields( 0 );
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $this->group,
+			'type' => 'textbox',
+			'name' => 'Foo Field',
+		) );
+		$this->create_users( 2 );
+
+		xprofile_set_field_data( $f, $this->users[0], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => 'Foo Field',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_default() {
+		$this->create_fields( 2 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo');
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'foo',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEquals( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_equals() {
+		$this->create_fields( 2 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo');
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'foo',
+					'compare' => '=',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEquals( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_not_equals() {
+		$this->create_fields( 1 );
+		$this->create_users( 2 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'foo',
+					'compare' => '!=',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1] );
+		$this->assertEquals( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_arithmetic_comparisons() {
+		$this->create_fields( 1 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], '1' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], '2' );
+		xprofile_set_field_data( $this->fields[0], $this->users[2], '3' );
+
+		// <
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 2,
+					'compare' => '<',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+
+		// <=
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 2,
+					'compare' => '<=',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0], $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+
+		// >=
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 2,
+					'compare' => '>=',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1], $this->users[2] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+
+		// >
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 2,
+					'compare' => '>',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[2] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_like() {
+		$this->create_fields( 1 );
+		$this->create_users( 2 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'ba',
+					'compare' => 'LIKE',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_not_like() {
+		$this->create_fields( 1 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'rab' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'ba',
+					'compare' => 'NOT LIKE',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_between_not_between() {
+		$this->create_fields( 1 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], '1' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], '10' );
+		xprofile_set_field_data( $this->fields[0], $this->users[2], '100' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => array( 9, 12 ),
+					'compare' => 'BETWEEN',
+					'type' => 'NUMERIC',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => array( 9, 12 ),
+					'compare' => 'NOT BETWEEN',
+					'type' => 'NUMERIC',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0], $this->users[2] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_regexp_rlike() {
+		$this->create_fields( 1 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'baz' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'z$',
+					'compare' => 'REGEXP',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+
+		// RLIKE is a synonym for REGEXP.
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'z$',
+					'compare' => 'RLIKE',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_not_regexp() {
+		$this->create_fields( 1 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'baz' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'z$',
+					'compare' => 'NOT REGEXP',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_single_clause_compare_not_exists() {
+		$this->create_fields( 2 );
+		$this->create_users( 2 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[1], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'compare' => 'NOT EXISTS',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_relation_default_to_and() {
+		$this->create_fields( 2 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[3], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[3], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				array(
+					'field' => $this->fields[0],
+					'value' => 'foo',
+				),
+				array(
+					'field' => $this->fields[1],
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[3] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_relation_and() {
+		$this->create_fields( 2 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[3], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[3], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'AND',
+				array(
+					'field' => $this->fields[0],
+					'value' => 'foo',
+				),
+				array(
+					'field' => $this->fields[1],
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[3] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_relation_or() {
+		$this->create_fields( 2 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[3], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[3], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'OR',
+				array(
+					'field' => $this->fields[0],
+					'value' => 'foo',
+				),
+				array(
+					'field' => $this->fields[1],
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0], $this->users[3] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_relation_and_with_compare_not_exists() {
+		$this->create_fields( 2 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[3], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'AND',
+				array(
+					'field' => $this->fields[0],
+					'compare' => 'NOT EXISTS',
+				),
+				array(
+					'field' => $this->fields[1],
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[3] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	/**
+	 * @ticket BP7202
+	 */
+	public function test_relation_and_with_compare_not_in() {
+		$this->create_fields( 1 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'boo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[3], 'far' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[2], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'AND',
+				array(
+					'field' => $this->fields[0],
+					'compare' => 'NOT IN',
+					'value' => array( 'foo', 'bar' )
+				),
+				array(
+					'field' => $this->fields[0],
+					'compare' => '!=',
+					'value' => 'far',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_relation_or_with_compare_not_exists() {
+		$this->create_fields( 2 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[3], 'bar' );
+		xprofile_set_field_data( $this->fields[0], $this->users[3], 'bar' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'OR',
+				array(
+					'field' => $this->fields[0],
+					'compare' => 'NOT EXISTS',
+				),
+				array(
+					'field' => $this->fields[1],
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[2], $this->users[3] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	/**
+	 * Tests for table join logic.
+	 */
+	public function test_relation_or_compare_equals_and_like() {
+		$this->create_fields( 2 );
+		$this->create_users( 4 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[3], 'barry' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'OR',
+				array(
+					'field' => $this->fields[0],
+					'compare' => '=',
+					'value' => 'foo',
+				),
+				array(
+					'field' => $this->fields[1],
+					'value' => 'bar',
+					'compare' => 'LIKE',
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0], $this->users[3] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	public function test_nested() {
+		$this->create_fields( 2 );
+		$this->create_users( 3 );
+
+		xprofile_set_field_data( $this->fields[0], $this->users[0], 'foo' );
+		xprofile_set_field_data( $this->fields[0], $this->users[1], 'bar' );
+		xprofile_set_field_data( $this->fields[1], $this->users[2], 'foo' );
+		xprofile_set_field_data( $this->fields[1], $this->users[1], 'foo' );
+
+		$q = new BP_User_Query( array(
+			'xprofile_query' => array(
+				'relation' => 'OR',
+				array(
+					'field' => $this->fields[0],
+					'compare' => '=',
+					'value' => 'foo',
+				),
+				array(
+					'relation' => 'AND',
+					array(
+						'field' => $this->fields[0],
+						'value' => 'bar',
+					),
+					array(
+						'field' => $this->fields[1],
+						'value' => 'foo',
+					),
+				),
+			),
+		) );
+
+		$expected = array( $this->users[0], $this->users[1] );
+		$this->assertEqualSets( $expected, array_keys( $q->results ) );
+	}
+
+	/**
+	 * @group BP7202
+	 */
+	public function test_find_compatible_table_alias_should_match_negative_siblings_joined_with_relation_and() {
+		$this->create_fields( 1 );
+
+		$q = new BP_XProfile_Query( array(
+			'relation' => 'AND',
+			array(
+				'field' => $this->fields[0],
+				'compare' => '!=',
+				'value' => 'foo',
+			),
+			array(
+				'field' => $this->fields[0],
+				'compare' => 'NOT IN',
+				'value' => array( 'bar', 'baz' ),
+			)
+		) );
+
+		$sql = $q->get_sql( buddypress()->profile->table_name_data, 'user_id' );
+
+		$this->assertSame( 1, substr_count( $sql['join'], 'INNER JOIN' ) );
+	}
+
+	/** Helpers **********************************************************/
+
+	protected function create_fields( $count ) {
+		$this->group = self::factory()->xprofile_group->create();
+		for ( $i = 0; $i < $count; $i++ ) {
+			$this->fields[] = self::factory()->xprofile_field->create( array(
+				'field_group_id' => $this->group,
+				'type' => 'textbox',
+			) );
+		}
+	}
+
+	protected function create_users( $count ) {
+		for ( $i = 0; $i < $count; $i++ ) {
+			$this->users[] = self::factory()->user->create();
+		}
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/filters.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/filters.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/filters.php	(revision 12265)
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @group xprofile
+ */
+class BP_Tests_XProfile_Filters extends BP_UnitTestCase {
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_filter_meta_query
+	 */
+	public function test_bp_xprofile_filter_meta_query_select() {
+		global $wpdb;
+
+		// update_meta_cache
+		$q = "SELECT xprofile_group_id, meta_key, meta_value FROM {$wpdb->xprofile_groupmeta} WHERE xprofile_group_id IN (1,2,3) ORDER BY xprofile_group_id ASC";
+		$this->assertSame( "SELECT object_id AS xprofile_group_id, meta_key, meta_value FROM {$wpdb->xprofile_groupmeta} WHERE object_type = 'group' AND object_id IN (1,2,3) ORDER BY object_id ASC", bp_xprofile_filter_meta_query( $q ) );
+
+		// add_metadata
+		$q = "SELECT COUNT(*) FROM {$wpdb->xprofile_groupmeta} WHERE meta_key = 'foo' AND xprofile_group_id = 5";
+		$this->assertSame( "SELECT COUNT(*) FROM {$wpdb->xprofile_groupmeta} WHERE object_type = 'group' AND meta_key = 'foo' AND object_id = 5", bp_xprofile_filter_meta_query( $q ) );
+
+		// delete_metadata
+		$q = "SELECT meta_id FROM {$wpdb->xprofile_groupmeta} WHERE meta_key = 'foo' AND xprofile_group_id = 5 AND meta_value = 'bar'";
+		$this->assertSame( "SELECT meta_id FROM {$wpdb->xprofile_groupmeta} WHERE object_type = 'group' AND meta_key = 'foo' AND object_id = 5 AND meta_value = 'bar'", bp_xprofile_filter_meta_query( $q ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_filter_meta_query
+	 */
+	public function test_bp_xprofile_filter_meta_query_insert() {
+		global $wpdb;
+		$q = "INSERT INTO `{$wpdb->xprofile_groupmeta}` (`xprofile_group_id`,`meta_key`,`meta_value`) VALUES (3,'foo','bar')";
+		$this->assertSame( "INSERT INTO `{$wpdb->xprofile_groupmeta}` (`object_type`,`object_id`,`meta_key`,`meta_value`) VALUES ('group',3,'foo','bar')", bp_xprofile_filter_meta_query( $q ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_filter_meta_query
+	 */
+	public function test_bp_xprofile_filter_meta_query_update() {
+		global $wpdb;
+
+		$q = "UPDATE `{$wpdb->xprofile_groupmeta}` SET meta_value = 'bar' WHERE xprofile_group_id = 3 AND meta_key = 'foo'";
+		$this->assertSame( "UPDATE `{$wpdb->xprofile_groupmeta}` SET meta_value = 'bar' WHERE object_type = 'group' AND object_id = 3 AND meta_key = 'foo'", bp_xprofile_filter_meta_query( $q ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_filter_meta_query
+	 */
+	public function test_bp_xprofile_filter_meta_query_delete() {
+		global $wpdb;
+		$q = "DELETE FROM {$wpdb->xprofile_groupmeta} WHERE xprofile_group_id IN(1,2,3)";
+		$this->assertSame( "DELETE FROM {$wpdb->xprofile_groupmeta} WHERE object_type = 'group' AND object_id IN(1,2,3)", bp_xprofile_filter_meta_query( $q ) );
+	}
+}
Index: /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/functions.php
===================================================================
--- /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/functions.php	(revision 12265)
+++ /tags/4.0.0-beta1/tests/phpunit/testcases/xprofile/functions.php	(revision 12265)
@@ -0,0 +1,1134 @@
+<?php
+
+/**
+ * @group xprofile
+ * @group functions
+ */
+class BP_Tests_XProfile_Functions extends BP_UnitTestCase {
+	public function test_get_hidden_field_types_for_user_loggedout() {
+		$duser = self::factory()->user->create();
+
+		$old_current_user = bp_loggedin_user_id();
+		$this->set_current_user( 0 );
+
+		$this->assertEquals( array( 'friends', 'loggedin', 'adminsonly' ), bp_xprofile_get_hidden_field_types_for_user( $duser, bp_loggedin_user_id() ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_get_hidden_field_types_for_user_loggedin() {
+		$duser = self::factory()->user->create();
+		$cuser = self::factory()->user->create();
+
+		$old_current_user = bp_loggedin_user_id();
+		$this->set_current_user( $cuser );
+
+		$this->assertEquals( array( 'friends', 'adminsonly' ), bp_xprofile_get_hidden_field_types_for_user( $duser, bp_loggedin_user_id() ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_get_hidden_field_types_for_user_friends() {
+		$duser = self::factory()->user->create();
+		$cuser = self::factory()->user->create();
+		friends_add_friend( $duser, $cuser, true );
+
+		$old_current_user = bp_loggedin_user_id();
+		$this->set_current_user( $cuser );
+
+		$this->assertEquals( array( 'adminsonly' ), bp_xprofile_get_hidden_field_types_for_user( $duser, bp_loggedin_user_id() ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_get_hidden_field_types_for_user_admin() {
+		$duser = self::factory()->user->create();
+		$cuser = self::factory()->user->create();
+		$this->grant_bp_moderate( $cuser );
+
+		$old_current_user = bp_loggedin_user_id();
+		$this->set_current_user( $cuser );
+
+		$this->assertEquals( array(), bp_xprofile_get_hidden_field_types_for_user( $duser, bp_loggedin_user_id() ) );
+
+		$this->revoke_bp_moderate( $cuser );
+		$this->set_current_user( $old_current_user );
+	}
+
+	/**
+	 * @group bp_xprofile_update_meta
+	 * @ticket BP5180
+	 */
+	public function test_bp_xprofile_update_meta_with_line_breaks() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		$meta_value = 'Foo!
+
+Bar!';
+		bp_xprofile_update_meta( $f, 'field', 'linebreak_field', $meta_value );
+		$this->assertEquals( $meta_value, bp_xprofile_get_meta( $f, 'field', 'linebreak_field' ) );
+	}
+
+	/**
+	 * @group bp_xprofile_fullname_field_id
+	 * @group cache
+	 */
+	public function test_bp_xprofile_fullname_field_id_invalidation() {
+		// Prime the cache
+		$id = bp_xprofile_fullname_field_id();
+
+		bp_update_option( 'bp-xprofile-fullname-field-name', 'foo' );
+
+		$this->assertFalse( wp_cache_get( 'fullname_field_id', 'bp_xprofile' ) );
+	}
+
+	/**
+	 * @group xprofile_get_field_visibility_level
+	 */
+	public function test_bp_xprofile_get_field_visibility_level_missing_params() {
+		$this->assertSame( '', xprofile_get_field_visibility_level( 0, 1 ) );
+		$this->assertSame( '', xprofile_get_field_visibility_level( 1, 0 ) );
+	}
+
+	/**
+	 * @group xprofile_get_field_visibility_level
+	 */
+	public function test_bp_xprofile_get_field_visibility_level_user_set() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
+		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'allowed' );
+
+		xprofile_set_field_visibility_level( $f, $u, 'loggedin' );
+
+		$this->assertSame( 'loggedin', xprofile_get_field_visibility_level( $f, $u ) );
+	}
+
+	/**
+	 * @group xprofile_get_field_visibility_level
+	 */
+	public function test_bp_xprofile_get_field_visibility_level_user_unset() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
+		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'allowed' );
+
+		$this->assertSame( 'adminsonly', xprofile_get_field_visibility_level( $f, $u ) );
+
+	}
+
+	/**
+	 * @group xprofile_get_field_visibility_level
+	 */
+	public function test_bp_xprofile_get_field_visibility_level_admin_override() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
+		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'disabled' );
+
+		xprofile_set_field_visibility_level( $f, $u, 'loggedin' );
+
+		$this->assertSame( 'adminsonly', xprofile_get_field_visibility_level( $f, $u ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_empty_object_id() {
+		$this->assertFalse( bp_xprofile_delete_meta( '', 'group' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_empty_object_type() {
+		$this->assertFalse( bp_xprofile_delete_meta( 1, '' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_illegal_object_type() {
+		$this->assertFalse( bp_xprofile_delete_meta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 * @ticket BP5399
+	 */
+	public function test_bp_xprofile_delete_meta_illegal_characters() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		bp_xprofile_delete_meta( $g, 'group', $krazy_key );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 * @ticket BP5399
+	 */
+	public function test_bp_xprofile_delete_meta_trim_meta_value() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+
+		bp_xprofile_delete_meta( $g, 'group', 'foo', ' bar  ' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_meta_value_match() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+		$this->assertTrue( bp_xprofile_delete_meta( $g, 'group', 'foo', 'bar' ) );
+		$this->assertSame( '', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_delete_all() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_update_meta( $g, 'group', 'foo2', 'bar' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo2' ) );
+
+		$this->assertTrue( bp_xprofile_delete_meta( $g, 'group' ) );
+
+		$this->assertSame( '', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+		$this->assertSame( '', bp_xprofile_get_meta( $g, 'group', 'foo2' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_with_delete_all_but_no_meta_key() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		$g1 = self::factory()->xprofile_group->create();
+		$g2 = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g1, 'group', 'foo', 'bar' );
+		bp_xprofile_add_meta( $g1, 'group', 'foo1', 'bar1' );
+		bp_xprofile_add_meta( $g2, 'group', 'foo', 'bar' );
+		bp_xprofile_add_meta( $g2, 'group', 'foo1', 'bar1' );
+
+		$this->assertTrue( bp_xprofile_delete_meta( $g1, 'group', '', '', true ) );
+		$this->assertEmpty( bp_xprofile_get_meta( $g1, 'group' ) );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g2, 'group', 'foo' ) );
+		$this->assertSame( 'bar1', bp_xprofile_get_meta( $g2, 'group', 'foo1' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_delete_meta
+	 */
+	public function test_bp_xprofile_delete_meta_with_delete_all() {
+		// With no meta key, don't delete for all items - just delete
+		// all for a single item
+		$g1 = self::factory()->xprofile_group->create();
+		$g2 = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g1, 'group', 'foo', 'bar' );
+		bp_xprofile_add_meta( $g1, 'group', 'foo1', 'bar1' );
+		bp_xprofile_add_meta( $g2, 'group', 'foo', 'bar' );
+		bp_xprofile_add_meta( $g2, 'group', 'foo1', 'bar1' );
+
+		$this->assertTrue( bp_xprofile_delete_meta( $g1, 'group', 'foo', '', true ) );
+		$this->assertSame( '', bp_xprofile_get_meta( $g1, 'group', 'foo' ) );
+		$this->assertSame( '', bp_xprofile_get_meta( $g2, 'group', 'foo' ) );
+		$this->assertSame( 'bar1', bp_xprofile_get_meta( $g1, 'group', 'foo1' ) );
+		$this->assertSame( 'bar1', bp_xprofile_get_meta( $g2, 'group', 'foo1' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_empty_object_id() {
+		$this->assertFalse( bp_xprofile_get_meta( 0, 'group' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_empty_object_type() {
+		$this->assertFalse( bp_xprofile_get_meta( 1, '' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_illegal_object_type() {
+		$this->assertFalse( bp_xprofile_get_meta( 1, 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_no_meta_key() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_update_meta( $g, 'group', 'foo2', 'bar' );
+
+		$expected = array(
+			'foo' => array(
+				'bar',
+			),
+			'foo2' => array(
+				'bar',
+			),
+		);
+		$this->assertSame( $expected, bp_xprofile_get_meta( $g, 'group' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_single_true() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'baz' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) ); // default is true
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo', true ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_single_false() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'baz' );
+		$this->assertSame( array( 'bar', 'baz' ), bp_xprofile_get_meta( $g, 'group', 'foo', false ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_get_meta
+	 */
+	public function test_bp_xprofile_get_meta_no_meta_key_no_results() {
+		$g = self::factory()->xprofile_group->create();
+
+		$expected = array();
+		$this->assertSame( $expected, bp_xprofile_get_meta( $g, 'group' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_no_object_id() {
+		$this->assertFalse( bp_xprofile_update_meta( 0, 'group', 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_no_object_type() {
+		$this->assertFalse( bp_xprofile_update_meta( 1, '', 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_illegal_object_type() {
+		$this->assertFalse( bp_xprofile_update_meta( 1, 'foo', 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 * @ticket BP5399
+	 */
+	public function test_bp_xprofile_update_meta_illegal_characters() {
+		$g = self::factory()->xprofile_group->create();
+		$krazy_key = ' f!@#$%^o *(){}o?+';
+		bp_xprofile_update_meta( $g, 'group', $krazy_key, 'bar' );
+		$this->assertSame( '', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_stripslashes() {
+		$g = self::factory()->xprofile_group->create();
+		$v = "Totally \'tubular\'";
+		bp_xprofile_update_meta( $g, 'group', 'foo', $v );
+		$this->assertSame( stripslashes( $v ), bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_empty_value_delete() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_update_meta( $g, 'group', 'foo', '' );
+		$this->assertSame( '', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_new() {
+		$g = self::factory()->xprofile_group->create();
+		$this->assertSame( '', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+		$this->assertNotEmpty( bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' ) );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_existing() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+		$this->assertTrue( bp_xprofile_update_meta( $g, 'group', 'foo', 'baz' ) );
+		$this->assertSame( 'baz', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_same_value() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertSame( 'bar', bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+		$this->assertFalse( bp_xprofile_update_meta( $g, 'group', 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 */
+	public function test_bp_xprofile_update_meta_prev_value() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+
+		// In earlier versions of WordPress, bp_activity_update_meta()
+		// returns true even on failure. However, we know that in these
+		// cases the update is failing as expected, so we skip this
+		// assertion just to keep our tests passing
+		// See https://core.trac.wordpress.org/ticket/24933
+		if ( version_compare( $GLOBALS['wp_version'], '3.7', '>=' ) ) {
+			$this->assertFalse( bp_xprofile_update_meta( $g, 'group', 'foo', 'bar2', 'baz' ) );
+		}
+
+		$this->assertTrue( bp_xprofile_update_meta( $g, 'group', 'foo', 'bar2', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 * @ticket BP5919
+	 */
+	public function test_bp_xprofile_update_meta_where_sql_filter_keywords_are_in_quoted_value() {
+		$g = self::factory()->xprofile_group->create();
+		$value = "SELECT object_id FROM wp_bp_xprofile_groups WHERE \"foo\" VALUES (foo = 'bar'";
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_update_meta( $g, 'group', 'foo', $value );
+		$this->assertSame( $value, bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_update_meta
+	 * @ticket BP5919
+	 */
+	public function test_bp_xprofile_update_meta_where_meta_id_is_in_quoted_value() {
+		$g = self::factory()->xprofile_group->create();
+		$value = "foo meta_id bar";
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+		bp_xprofile_update_meta( $g, 'group', 'foo', $value );
+		$this->assertSame( $value, bp_xprofile_get_meta( $g, 'group', 'foo' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_add_meta
+	 */
+	public function test_bp_xprofile_add_meta_no_meta_key() {
+		$this->assertFalse( bp_xprofile_add_meta( 1, 'group', '', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_add_meta
+	 */
+	public function test_bp_xprofile_add_meta_empty_object_id() {
+		$this->assertFalse( bp_xprofile_add_meta( 0, 'group', 'foo', 'bar' ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_add_meta
+	 */
+	public function test_bp_xprofile_add_meta_existing_unique() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertFalse( bp_xprofile_add_meta( $g, 'group', 'foo', 'baz', true ) );
+	}
+
+	/**
+	 * @group xprofilemeta
+	 * @group bp_xprofile_add_meta
+	 */
+	public function test_bp_xprofile_add_meta_existing_not_unique() {
+		$g = self::factory()->xprofile_group->create();
+		bp_xprofile_add_meta( $g, 'group', 'foo', 'bar' );
+		$this->assertNotEmpty( bp_xprofile_add_meta( $g, 'group', 'foo', 'baz' ) );
+	}
+
+	/**
+	 * @group bp_get_member_profile_data
+	 */
+	public function test_bp_get_member_profile_data_inside_loop() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Neato',
+		) );
+		xprofile_set_field_data( $f, $u, 'foo' );
+
+		if ( bp_has_members() ) : while ( bp_members() ) : bp_the_member();
+		$found = bp_get_member_profile_data( array(
+			'user_id' => $u,
+			'field' => 'Neato',
+		) );
+		endwhile; endif;
+
+		// Cleanup
+		unset( $GLOBALS['members_template'] );
+
+		$this->assertSame( 'foo', $found );
+	}
+	/**
+	 * @group bp_get_member_profile_data
+	 */
+	public function test_bp_get_member_profile_data_outside_of_loop() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'name' => 'Kewl',
+		) );
+		xprofile_set_field_data( $f, $u, 'foo' );
+
+		$found = bp_get_member_profile_data( array(
+			'user_id' => $u,
+			'field' => 'Kewl',
+		) );
+
+		$this->assertSame( 'foo', $found );
+	}
+
+	/**
+	 * @group xprofile_set_field_data
+	 */
+	public function test_get_field_data_integer_zero() {
+		$u = self::factory()->user->create();
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'number',
+			'name' => 'Pens',
+		) );
+		xprofile_set_field_data( $f, $u, 0 );
+
+		$this->assertEquals( 0, xprofile_get_field_data( 'Pens', $u ) );
+	}
+
+	/**
+	 * @group xprofile_set_field_data
+	 * @ticket BP5836
+	 */
+	public function test_xprofile_sync_bp_profile_new_user() {
+		$post_vars = $_POST;
+
+		$_POST = array(
+			'user_login' => 'foobar',
+			'pass1'      => 'password',
+			'pass2'      => 'password',
+			'role'       => 'subscriber',
+			'email'      => 'foo@bar.com',
+			'first_name' => 'Foo',
+			'last_name'  => 'Bar',
+		);
+
+		$id = add_user();
+
+		$display_name = 'Bar Foo';
+
+		$_POST = array(
+			'display_name' => $display_name,
+			'email' => 'foo@bar.com',
+			'nickname' => 'foobar',
+		);
+
+		$id = edit_user( $id );
+
+		// clean up post vars
+		$_POST = $post_vars;
+
+		$this->assertEquals( $display_name, xprofile_get_field_data( bp_xprofile_fullname_field_id(), $id ) );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 */
+	public function test_xprofile_insert_field_type_option() {
+		$g = self::factory()->xprofile_group->create();
+		$parent = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Parent',
+		) );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'parent_id' => $parent,
+			'type' => 'option',
+			'name' => 'Option 1',
+			'field_order' => 5,
+		) );
+
+		$this->assertNotEmpty( $f );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 * @ticket BP6354
+	 */
+	public function test_xprofile_insert_field_should_process_falsey_values_for_boolean_params_on_existing_fields() {
+		$g = self::factory()->xprofile_group->create();
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'type' => 'textbox',
+			'name' => 'Foo',
+			'is_required' => true,
+			'can_delete' => true,
+			'is_default_option' => true,
+			'parent_id' => 13,
+			'field_order' => 5,
+			'option_order' => 8,
+			'description' => 'foo',
+			'order_by' => 'custom',
+		) );
+
+		$this->assertNotEmpty( $f );
+
+		$field = new BP_XProfile_Field( $f );
+		$this->assertEquals( 1, $field->is_required );
+		$this->assertEquals( 1, $field->can_delete );
+		$this->assertEquals( 1, $field->is_default_option );
+		$this->assertEquals( 13, $field->parent_id );
+		$this->assertEquals( 5, $field->field_order );
+		$this->assertEquals( 8, $field->option_order );
+		$this->assertEquals( 'foo', $field->description );
+		$this->assertEquals( 'custom', $field->order_by );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'type' => 'textbox',
+			'name' => 'Foo',
+			'is_required' => false,
+			'can_delete' => false,
+			'is_default_option' => false,
+			'parent_id' => 0,
+			'field_order' => 0,
+			'option_order' => 0,
+			'description' => '',
+			'order_by' => '',
+		) );
+
+		$this->assertNotEmpty( $f );
+
+		$field = new BP_XProfile_Field( $f );
+		$this->assertEquals( 0, $field->is_required );
+		$this->assertEquals( 0, $field->can_delete );
+		$this->assertEquals( 0, $field->is_default_option );
+		$this->assertEquals( 0, $field->parent_id );
+		$this->assertEquals( 0, $field->field_order );
+		$this->assertEquals( 0, $field->option_order );
+		$this->assertEquals( '', $field->description );
+		$this->assertEquals( '', $field->order_by );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 */
+	public function test_xprofile_insert_field_type_option_option_order() {
+		$g = self::factory()->xprofile_group->create();
+		$parent = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Parent',
+		) );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'parent_id' => $parent,
+			'type' => 'option',
+			'name' => 'Option 1',
+			'option_order' => 5,
+		) );
+
+		$field = new BP_XProfile_Field( $f );
+
+		$this->assertEquals( 5, $field->option_order );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 * @ticket BP6137
+	 */
+	public function test_xprofile_insert_field_should_set_is_default_option_to_false_for_new_option() {
+		$g = self::factory()->xprofile_group->create();
+		$parent = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Parent',
+		) );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'parent_id' => $parent,
+			'type' => 'option',
+			'name' => 'Option 1',
+			'field_order' => 5,
+			'is_default_option' => false,
+		) );
+
+		$this->assertNotEmpty( $f );
+		$field = new BP_XProfile_Field( $f );
+		$this->assertEquals( 0, $field->is_default_option );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 * @ticket BP6137
+	 */
+	public function test_xprofile_insert_field_should_set_is_default_option_to_true_for_new_option() {
+		$g = self::factory()->xprofile_group->create();
+		$parent = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Parent',
+		) );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'parent_id' => $parent,
+			'type' => 'option',
+			'name' => 'Option 1',
+			'field_order' => 5,
+			'is_default_option' => true,
+		) );
+
+		$this->assertNotEmpty( $f );
+		$field = new BP_XProfile_Field( $f );
+		$this->assertEquals( 1, $field->is_default_option );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 * @ticket BP6137
+	 */
+	public function test_xprofile_insert_field_should_set_is_default_option_to_false_for_existing_option() {
+		$g = self::factory()->xprofile_group->create();
+		$parent = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Parent',
+		) );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'parent_id' => $parent,
+			'type' => 'option',
+			'name' => 'Option 1',
+			'field_order' => 5,
+			'is_default_option' => true,
+		) );
+
+		$this->assertNotEmpty( $f );
+		$field = new BP_XProfile_Field( $f );
+		$this->assertEquals( 1, $field->is_default_option );
+
+		$f = xprofile_insert_field( array(
+			'field_id' => $f,
+			'field_group_id' => $g,
+			'type' => 'textbox',
+			'is_default_option' => false,
+		) );
+
+		$field2 = new BP_XProfile_Field( $f );
+		$this->assertEquals( 0, $field2->is_default_option );
+	}
+
+	/**
+	 * @group xprofile_insert_field
+	 * @ticket BP6137
+	 */
+	public function test_xprofile_insert_field_should_set_is_default_option_to_true_for_existing_option() {
+		$g = self::factory()->xprofile_group->create();
+		$parent = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Parent',
+		) );
+
+		$f = xprofile_insert_field( array(
+			'field_group_id' => $g,
+			'parent_id' => $parent,
+			'type' => 'option',
+			'name' => 'Option 1',
+			'field_order' => 5,
+			'is_default_option' => false,
+		) );
+
+		$this->assertNotEmpty( $f );
+		$field = new BP_XProfile_Field( $f );
+		$this->assertEquals( 0, $field->is_default_option );
+
+		$f = xprofile_insert_field( array(
+			'field_id' => $f,
+			'field_group_id' => $g,
+			'type' => 'textbox',
+			'is_default_option' => true,
+		) );
+
+		$field2 = new BP_XProfile_Field( $f );
+
+		$this->assertEquals( 1, $field2->is_default_option );
+	}
+
+	/**
+	 * @group xprofile_update_field_group_position
+	 * @group bp_profile_get_field_groups
+	 */
+	public function test_bp_profile_get_field_groups_update_position() {
+		$g1 = self::factory()->xprofile_group->create();
+		$g2 = self::factory()->xprofile_group->create();
+		$g3 = self::factory()->xprofile_group->create();
+
+		// prime the cache
+		bp_profile_get_field_groups();
+
+		// switch the field group positions for the last two groups
+		xprofile_update_field_group_position( $g2, 3 );
+		xprofile_update_field_group_position( $g3, 2 );
+
+		// now refetch field groups
+		$field_groups = bp_profile_get_field_groups();
+
+		// assert!
+		$this->assertEquals( array( 1, $g1, $g3, $g2 ), wp_list_pluck( $field_groups, 'id' ) );
+	}
+
+	/**
+	 * @ticket BP6638
+	 */
+	public function test_xprofile_get_field_should_return_bp_xprofile_field_object() {
+		global $wpdb;
+
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Foo',
+		) );
+
+		$field = xprofile_get_field( $f );
+
+		$this->assertTrue( $field instanceof BP_XProfile_Field );
+	}
+
+	/**
+	 * @ticket BP6638
+	 * @group cache
+	 */
+	public function test_xprofile_get_field_should_prime_field_cache() {
+		global $wpdb;
+
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'selectbox',
+			'name' => 'Foo',
+		) );
+
+		$num_queries = $wpdb->num_queries;
+
+		// Prime the cache.
+		$field_1 = xprofile_get_field( $f );
+		$num_queries++;
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+
+		// No more queries.
+		$field_2 = xprofile_get_field( $f );
+		$this->assertEquals( $field_1, $field_2 );
+		$this->assertSame( $num_queries, $wpdb->num_queries );
+	}
+
+	/**
+	 * @ticket BP5625
+	 */
+	public function test_bp_xprofie_is_richtext_enabled_for_field_should_default_to_true_for_textareas() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'textarea',
+		) );
+
+		$this->assertTrue( bp_xprofile_is_richtext_enabled_for_field( $f ) );
+	}
+
+	/**
+	 * @ticket BP5625
+	 */
+	public function test_bp_xprofie_is_richtext_enabled_for_field_should_default_to_false_for_non_textareas() {
+		$g = self::factory()->xprofile_group->create();
+		$f = self::factory()->xprofile_field->create( array(
+			'field_group_id' => $g,
+			'type' => 'radio',
+		) );
+
+		$this->assertFalse( bp_xprofile_is_richtext_enabled_for_field( $f ) );
+	}
+
+	/**
+	 * @group bp_get_field_css_class
+	 */
+	public function test_bp_get_field_css_class_empty_param() {
+		// Fake the global
+		global $profile_template;
+		$reset_profile_template = $profile_template;
+
+		$profile_template = new stdClass;
+		// Avoid the 'alt' class being added
+		$profile_template->current_field = 2;
+		$profile_template->field = new stdClass;
+		$profile_template->field->id = 145;
+		$profile_template->field->name = 'Pie';
+		$profile_template->field->type = 'textbox';
+
+		$expected_classes = array(
+			'optional-field',
+			'field_' . $profile_template->field->id,
+			'field_' . sanitize_title( $profile_template->field->name ),
+			'field_type_' . sanitize_title( $profile_template->field->type ),
+			'visibility-public'
+			);
+
+		$classes = bp_get_field_css_class();
+		preg_match( '/class=["\']?([^"\']*)["\' ]/is', $classes, $matches );
+		$ret_classes = explode( ' ', $matches[1] );
+		$this->assertEqualSets( $expected_classes, $ret_classes );
+
+		// Clean up!
+		$profile_template = $reset_profile_template;
+	}
+
+	/**
+	 * @group bp_get_field_css_class
+	 */
+	public function test_bp_get_field_css_class_space_delimited_string() {
+		// Fake the global
+		global $profile_template;
+		$reset_profile_template = $profile_template;
+
+		$profile_template = new stdClass;
+		// Avoid the 'alt' class being added
+		$profile_template->current_field = 2;
+		$profile_template->field = new stdClass;
+		$profile_template->field->id = 145;
+		$profile_template->field->name = 'Pie';
+		$profile_template->field->type = 'textbox';
+
+		$expected_classes = array(
+			'optional-field',
+			'field_' . $profile_template->field->id,
+			'field_' . sanitize_title( $profile_template->field->name ),
+			'field_type_' . sanitize_title( $profile_template->field->type ),
+			'visibility-public',
+			'rhubarb',
+			'apple'
+			);
+
+		$classes = bp_get_field_css_class( 'rhubarb apple' );
+		preg_match( '/class=["\']?([^"\']*)["\' ]/is', $classes, $matches );
+		$ret_classes = explode( ' ', $matches[1] );
+		$this->assertEqualSets( $expected_classes, $ret_classes );
+
+		// Clean up!
+		$profile_template = $reset_profile_template;
+	}
+
+	/**
+	 * @group bp_get_field_css_class
+	 */
+	public function test_bp_get_field_css_class_array() {
+		// Fake the global
+		global $profile_template;
+		$reset_profile_template = $profile_template;
+
+		$profile_template = new stdClass;
+		// Avoid the 'alt' class being added
+		$profile_template->current_field = 2;
+		$profile_template->field = new stdClass;
+		$profile_template->field->id = 145;
+		$profile_template->field->name = 'Pie';
+		$profile_template->field->type = 'textbox';
+
+		$expected_classes = array(
+			'optional-field',
+			'field_' . $profile_template->field->id,
+			'field_' . sanitize_title( $profile_template->field->name ),
+			'field_type_' . sanitize_title( $profile_template->field->type ),
+			'visibility-public',
+			'blueberry',
+			'gooseberry'
+			);
+
+		$classes = bp_get_field_css_class( array( 'blueberry', 'gooseberry' ) );
+		preg_match( '/class=["\']?([^"\']*)["\' ]/is', $classes, $matches );
+		$ret_classes = explode( ' ', $matches[1] );
+		$this->assertEqualSets( $expected_classes, $ret_classes );
+
+		// Clean up!
+		$profile_template = $reset_profile_template;
+	}
+
+	/**
+	 * @group xprofile_filter_link_profile_data
+	 */
+	public function test_field_comma_seperated_values_are_autolinked() {
+		$field_group_id = self::factory()->xprofile_group->create();
+		$field_id = self::factory()->xprofile_field->create( array( 'field_group_id' => $field_group_id ) );
+		$GLOBALS['field'] = new BP_XProfile_Field( $field_id );
+		$GLOBALS['field']->do_autolink = true;
+
+		$output = xprofile_filter_link_profile_data( 'Hello world this is a test; with, some, words', 'textbox' );
+		$regex = '#^Hello world this is a test; with, <a href="([^"]+)" rel="nofollow">some</a>, <a href="([^"]+)" rel="nofollow">words</a>$#i';
+
+		$this->assertRegExp( $regex, $output );
+		unset( $GLOBALS['field'] );
+	}
+
+	/**
+	 * @group xprofile_filter_link_profile_data
+	 */
+	public function test_field_semicolon_seperated_values_are_autolinked() {
+		$field_group_id = self::factory()->xprofile_group->create();
+		$field_id = self::factory()->xprofile_field->create( array( 'field_group_id' => $field_group_id ) );
+		$GLOBALS['field'] = new BP_XProfile_Field( $field_id );
+		$GLOBALS['field']->do_autolink = true;
+
+		$output = xprofile_filter_link_profile_data( 'Hello world this is a test with; some; words', 'textbox' );
+		$regex = '#^Hello world this is a test with; <a href="([^"]+)" rel="nofollow">some</a>; <a href="([^"]+)" rel="nofollow">words</a>$#i';
+
+		$this->assertRegExp( $regex, $output );
+		unset( $GLOBALS['field'] );
+	}
+
+	/**
+	 * @ticket BP7817
+	 * @ticket BP7698
+	 */
+	public function test_bp_xprofile_personal_data_exporter() {
+		$u = self::factory()->user->create();
+
+		$field_group_id = self::factory()->xprofile_group->create();
+		$f1 = self::factory()->xprofile_field->create(
+			array(
+				'field_group_id' => $field_group_id,
+			)
+		);
+		$f2 = self::factory()->xprofile_field->create(
+			array(
+				'field_group_id' => $field_group_id,
+				'type'           => 'checkbox',
+			)
+		);
+
+		$option1 = xprofile_insert_field( array(
+			'field_group_id' => $field_group_id,
+			'parent_id'      => $f2,
+			'type'           => 'option',
+			'name'           => 'Option 1',
+		) );
+
+		$option2 = xprofile_insert_field( array(
+			'field_group_id' => $field_group_id,
+			'parent_id'      => $f2,
+			'type'           => 'option',
+			'name'           => 'Option 2',
+		) );
+
+		$option3 = xprofile_insert_field( array(
+			'field_group_id' => $field_group_id,
+			'parent_id'      => $f2,
+			'type'           => 'option',
+			'name'           => 'Option 3',
+		) );
+
+		xprofile_set_field_data( $f1, $u, 'foo' );
+		xprofile_set_field_data( $f2, $u, array( 'Option 1', 'Option 3' ) );
+
+		$test_user = new WP_User( $u );
+
+		$actual = bp_xprofile_personal_data_exporter( $test_user->user_email );
+
+		$this->assertTrue( $actual['done'] );
+
+		// Number of exported users.
+		$this->assertSame( 1, count( $actual['data'] ) );
+
+		// Number of exported user properties.
+		$this->assertSame( 3, count( $actual['data'][0]['data'] ) );
+	}
+}
